DEV Community

Cover image for สร้าง LLM เองตั้งแต่ต้น: สิ่งที่คุณจะได้เรียนรู้
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

สร้าง LLM เองตั้งแต่ต้น: สิ่งที่คุณจะได้เรียนรู้

สรุปย่อ (TL;DR)

การสร้างแบบจำลองภาษาขนาดเล็ก (minimal language model) ด้วย Python ไม่ถึง 300 บรรทัด ช่วยให้เข้าใจการทำงานของ Tokenization, Attention และ Inference อย่างชัดเจน ซึ่งเป็นประโยชน์โดยตรงต่อการพัฒนาและรวม LLM API เข้ากับแอปพลิเคชันของคุณ

ลองใช้ Apidog วันนี้

บทนำ

นักพัฒนาส่วนใหญ่มองแบบจำลองภาษาเป็นกล่องดำ—ใส่ข้อความเข้าไป รับโทเค็นออกมา ระหว่างนั้นเกิดอะไรขึ้นไม่รู้ จนกว่าจะเจอปัญหา เช่น บั๊กในการรวม API, ปรับ sampling parameter หรือหาสาเหตุข้อมูลผิดโครงสร้าง

GuppyLM (ที่ติดหน้าแรก HackerNews) เปิดเผยการทำงานภายใน LLM มันคือ Transformer 8.7M พารามิเตอร์ เขียนด้วย Python ไฟล์เดียว ฝึกบน GPU ทั่วไปในเวลาสั้น เป้าหมายคือให้เข้าใจ LLM แทนที่จะ “แข่ง” กับ GPT-4

บทความนี้จะแนะนำวิธีสร้าง LLM ขนาดเล็ก แต่ละส่วนทำงานอย่างไร และข้อคิดที่นำไปใช้กับ AI API จริง

💡 หากคุณกำลังทดสอบการรวม AI API, Test Scenarios ของ Apidog ช่วยให้คุณตรวจสอบการตอบกลับแบบสตรีมมิ่ง, ตรวจสอบโครงสร้างโทเค็น และจำลองการเติมข้อความในกรณีพิเศษได้โดยไม่เปลืองเครดิต API

อะไรทำให้แบบจำลองภาษา "เล็ก"?

  • LLM ขนาดเล็ก = 1–25 ล้านพารามิเตอร์ (เช่น GuppyLM 8.7M, nanoGPT 124M, MicroLM 1-2M)
  • ข้อดี: ฝึกบนแล็ปท็อป, เขียนโค้ดแก้บั๊ก/ปรับแต่งได้, เหมาะสำหรับการศึกษา
  • ข้อจำกัด: ไม่สามารถ reasoning ซับซ้อน/เขียนข้อความยาวๆ ได้ดี

จุดสำคัญ: คุณค่าของ LLM เล็กอยู่ที่การ “เข้าใจ” ไม่ใช่ที่คุณภาพเอาต์พุต

ส่วนประกอบหลัก: LLM ทำงานอย่างไรจริงๆ

ตัวแบ่งโทเค็น (Tokenizer)

  • แปลงข้อความเป็น integer IDs เช่น "Hello, world!"[15496, 11, 995, 0]
  • โทเค็นเยอะขึ้น = ต้นทุนและ latency สูงขึ้น
  • GPT-4 ใช้ BPE; ตัวอย่าง LLM เล็กใช้ character-level

เลเยอร์การฝัง (Embedding layer)

  • แปลง token IDs เป็นเวกเตอร์ (embedding) ที่เรียนรู้ได้
  • เพิ่ม position embedding ให้โมเดลรู้ลำดับ

บล็อก Transformer

  • Self-attention: แต่ละโทเค็นเห็นโทเค็นอื่นๆ ในลำดับ
  • Feed-forward network: MLP 2 ชั้นในแต่ละบล็อก
  • ตัวอย่าง: GuppyLM ใช้ 6 attention heads, 6 layers

ส่วนหัวเอาต์พุต (Output head)

  • เลเยอร์เชิงเส้น map เวกเตอร์สุดท้ายเป็น vocab size
  • Softmax → sampling/เลือกโทเค็นถัดไป

การสร้าง LLM ขนาดเล็กใน Python

ต่อไปนี้เป็น LLM ขนาดเล็กด้วย PyTorch ที่ใช้งานได้จริง:

import torch
import torch.nn as nn
import torch.nn.functional as F

# Hyperparameters
VOCAB_SIZE = 256     # character-level: one slot per ASCII char
D_MODEL = 128        # embedding dimension
N_HEADS = 4          # attention heads
N_LAYERS = 3         # transformer blocks
SEQ_LEN = 64         # context window
DROPOUT = 0.1

class SelfAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.proj = nn.Linear(d_model, d_model, bias=False)
        self.dropout = nn.Dropout(DROPOUT)

    def forward(self, x):
        B, T, C = x.shape
        qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.unbind(dim=2)
        q = q.transpose(1, 2)
        k = k.transpose(1, 2)
        v = v.transpose(1, 2)
        # Causal mask: each token can only attend to previous tokens
        scale = self.head_dim ** -0.5
        attn = (q @ k.transpose(-2, -1)) * scale
        mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
        attn = attn.masked_fill(mask, float('-inf'))
        attn = F.softmax(attn, dim=-1)
        attn = self.dropout(attn)
        out = (attn @ v).transpose(1, 2).reshape(B, T, C)
        return self.proj(out)

class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.attn = SelfAttention(d_model, n_heads)
        self.ff = nn.Sequential(
            nn.Linear(d_model, 4 * d_model),
            nn.ReLU(),
            nn.Linear(4 * d_model, d_model),
            nn.Dropout(DROPOUT),
        )
        self.ln1 = nn.LayerNorm(d_model)
        self.ln2 = nn.LayerNorm(d_model)

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.ff(self.ln2(x))
        return x

class TinyLLM(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
        self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
        self.blocks = nn.ModuleList([
            TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
        ])
        self.ln_f = nn.LayerNorm(D_MODEL)
        self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)

    def forward(self, idx):
        B, T = idx.shape
        tok_emb = self.embed(idx)
        pos = torch.arange(T, device=idx.device)
        pos_emb = self.pos_embed(pos)
        x = tok_emb + pos_emb
        for block in self.blocks:
            x = block(x)
        x = self.ln_f(x)
        logits = self.head(x)
        return logits

# Initialize and count parameters
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Model size: {total_params:,} parameters")  # ~1.2M
Enter fullscreen mode Exit fullscreen mode

ลูปการฝึกฝน (Training loop)

import torch.optim as optim

def train(model, data, epochs=100, lr=3e-4):
    optimizer = optim.AdamW(model.parameters(), lr=lr)
    model.train()
    for epoch in range(epochs):
        # data: tensor of token IDs, shape [batch, seq_len+1]
        x = data[:, :-1]   # input: all tokens except last
        y = data[:, 1:]    # target: all tokens shifted by 1
        logits = model(x)
        loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Epoch {epoch}, loss: {loss.item():.4f}")
Enter fullscreen mode Exit fullscreen mode

การอนุมาน (การสร้างข้อความ)

@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
    model.eval()
    ids = torch.tensor([prompt_ids])
    for _ in range(max_new_tokens):
        idx_cond = ids[:, -SEQ_LEN:]  # crop to context window
        logits = model(idx_cond)
        logits = logits[:, -1, :] / temperature  # last token only
        # top-k sampling
        v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
        logits[logits < v[:, [-1]]] = float('-inf')
        probs = F.softmax(logits, dim=-1)
        next_id = torch.multinomial(probs, num_samples=1)
        ids = torch.cat([ids, next_id], dim=1)
    return ids[0].tolist()
Enter fullscreen mode Exit fullscreen mode

สิ่งนี้สอนอะไรคุณเกี่ยวกับพฤติกรรมของ AI API

อุณหภูมิและการสุ่มตัวอย่างเป็นเชิงกลไก ไม่ใช่เวทมนตร์

  • temperature ปรับความชัน logits → ควบคุมความสุ่ม
  • temperature=0.0 = argmax (greedy) จริงๆ API บางเจ้าปัดเศษขึ้นเล็กน้อย

หน้าต่างบริบทเป็นขีดจำกัดจริง

  • บรรทัด idx_cond = ids[:, -SEQ_LEN:] คือจุดตัด—โทเค็นเก่าจะถูกทิ้ง
  • ไม่สามารถจำประวัติทั้งหมดได้ถ้าเกินขีดจำกัด

การสตรีมโทเค็นเป็นเพียงขั้นตอนการอนุมาน

  • สตรีมมิ่ง API = ส่งแต่ละโทเค็นทันทีที่สร้าง ไม่ต่างเชิงสถาปัตยกรรม
  • สตรีมที่ขาดตอนต้องเริ่มใหม่

Logits อธิบายว่าทำไมการสร้างเอาต์พุตที่มีโครงสร้างจึงยาก

  • ต้องชนะด้วยโทเค็นที่ “ถูกต้อง” ทุกตำแหน่ง
  • ไลบรารีอย่าง Outlines/Guidance จะจำกัด logit distribution เพื่อบังคับไวยากรณ์

วิธีทดสอบการรวม AI API ด้วย Apidog

เข้าใจกลไก LLM แล้ว ทดสอบ API ได้ดีขึ้นด้วย Test Scenarios ของ Apidog:

  1. สร้าง Test Scenario ที่ endpoint /v1/chat/completions
  2. ตั้ง assertion เช่น
    • response.choices[0].finish_reason == "stop"
    • response.usage.total_tokens < 4096
  3. เพิ่ม step ส่ง response กลับเป็นบริบท จำลอง multi-turn
  4. ใช้ Smart Mock ของ Apidog จำลอง finish_reason, content_filter, network timeout

ทดสอบ integration โดยไม่เปลืองเครดิต API ทุกครั้ง ดู [internal: api-testing-tutorial] สำหรับแนวทางการทดสอบ API เพิ่มเติม

การทดสอบการยืนยันจำนวนโทเค็น

{
  "assertions": [
    {
      "field": "response.usage.completion_tokens",
      "operator": "less_than",
      "value": 512
    },
    {
      "field": "response.choices[0].finish_reason",
      "operator": "equals",
      "value": "stop"
    },
    {
      "field": "response.choices[0].message.content",
      "operator": "not_empty"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

รันกับโมเดลหลายตัว (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) ด้วย Scenario เดียวเพื่อจับ schema difference ก่อนขึ้น production

ขั้นสูง: การหาปริมาณและการเพิ่มประสิทธิภาพการอนุมาน

การหาปริมาณ (Quantization)

ลด float32 → int8/int4 เพื่อลดหน่วยความจำ (4-8 เท่า) คุณภาพแทบไม่ลด

# Example: dynamic INT8 quantization in PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
    model, {nn.Linear}, dtype=torch.qint8
)
Enter fullscreen mode Exit fullscreen mode

แคช KV (KV cache)

  • ใน inference จริง จะ cache ค่า KV ของโทเค็นก่อนหน้า
  • โทเค็นใหม่คำนวณ attention เฉพาะกับ KV ล่าสุด → ทำให้ streaming token หลังแรกเร็วขึ้น

Tiny LLM vs. API สำหรับการผลิต: จะใช้เมื่อใด

กรณีการใช้งาน Tiny LLM API สำหรับการผลิต
เรียนรู้การทำงานภายในของโมเดล ดีที่สุดสำหรับ เกินความจำเป็น
สร้างแอปต้นแบบใหม่ คุณภาพไม่เพียงพอ ดีที่สุดสำหรับ
ข้อมูลส่วนตัว/ละเอียดอ่อน ตัวเลือกที่ดี ขึ้นอยู่กับผู้ให้บริการ
การปรับใช้แบบออฟไลน์/Edge เป็นไปได้ ไม่สามารถทำได้
ต้นทุนต่ำ, ปริมาณมาก เป็นไปได้โดยมีการแลกเปลี่ยน แพงเมื่อขยายขนาด
งานที่เน้นการให้เหตุผล ไม่สามารถทำได้ จำเป็น

สรุป: สำหรับ dev ส่วนใหญ่ ใช้ API สำหรับ production แต่รัน Tiny LLM เพื่อเข้าใจกลไกจริง ทั้งสองไม่ใช่คู่แข่งกัน ดู [internal: open-source-coding-assistants-2026] สำหรับเครื่องมือแบบ BYOM

สรุป

การสร้าง LLM เล็กใช้เวลาแค่วันหยุด แต่ทำให้คุณเข้าใจกลไกเบื้องหลัง LLM production ทุกตัว ตั้งแต่ GuppyLM ถึง GPT-4o ความเข้าใจนี้มีค่ามากกับการดีบั๊ก ปรับแต่ง sampling หรือออกแบบ test assertion สำหรับ AI API ของคุณ

GuppyLM คือจุดเริ่มต้นที่ดี Fork, ฝึกกับชุดข้อความใดก็ได้ แล้วอ่านลูป inference แล้วกลับมาดู production API integration ของคุณ—คุณจะมองเห็นภาพต่างออกไป

ลองใช้ Test Scenarios ของ Apidog เพื่อยกระดับการทดสอบ AI API ของคุณให้เข้มงวดเหมือน backend อื่นๆ

คำถามที่พบบ่อย

LLM "ขนาดเล็ก" ต้องการกี่พารามิเตอร์ถึงจะสร้างข้อความที่สอดคล้อง?

ประมาณ 10M–50M พารามิเตอร์ + dataset ที่ดีจะสร้างประโยคที่สอดคล้องกันในโดเมนได้ ต่ำกว่า 1M มักจะพูดไร้สาระ GuppyLM 8.7M ใช้สนทนาสั้นในโดเมนฝึก (60 หัวข้อ) ได้

รัน LLM ขนาดเล็กโดยไม่มี GPU ได้ไหม?

ได้ < 100M พารามิเตอร์รันบน CPU ได้ แม้ inference จะช้ากว่า โค้ดข้างต้น (1.2M) รันบนแล็ปท็อปได้เร็ว

ควรฝึกกับ dataset อะไร?

ระดับ character ใช้ Project Gutenberg, Wikipedia, หรือ text corpus ใดก็ได้ GuppyLM ใช้ชุดสนทนา 60K จาก HuggingFace (arman-bd/guppylm-60k-generic) งานโค้ดใช้ The Stack หรือ CodeParrot

ความต่างระหว่าง temperature กับ top-k sampling?

Temperature ปรับความชัน logits (ควบคุมความสุ่ม) Top-k จำกัด sampling แค่ k ตัวที่มีโอกาสสูงก่อน softmax โดยมักใช้ร่วมกัน

LLM บางครั้งพูดซ้ำเพราะอะไร?

โมเดล assign prob สูงกับโทเค็นล่าสุดที่เห็นใน context ใช้ repetition_penalty=1.1 ใน API call เพื่อลดปัญหานี้

ใช้เวลาฝึก LLM ขนาดเล็กเท่าไร?

โมเดลข้างต้นฝึกได้ผลลัพธ์ใน 2 ชั่วโมงบน GPU เดียว (RTX 3060) GuppyLM ใน Colab ก็ใกล้เคียง โมเดลใหญ่ (100M+) ใช้ multi-GPU และหลายวัน

ทางลัดจาก LLM เล็กไป API endpoint จริง?

Export เป็น GGUF (llama.cpp) แล้วรันกับ llama-server ได้ OpenAI-compatible endpoint ทันที ใช้ Apidog ทดสอบได้เลย ดู [internal: rest-api-best-practices]

LLM production จัดการ context window เกิน training window อย่างไร?

เทคนิค RoPE, sliding window, retrieval-augmented generation ช่วยขยาย context ได้โดยไม่เปลี่ยน Transformer architecture หลัก (แค่เปลี่ยน encoding/context management)

Top comments (0)