ข้าม​ไป​ยัง​เนื้อหา

สร้าง nano-GPT ตัว​จิ๋ว — capstone: เทรน​บน CPU, generate, แล้ว tokenize ภาษา​ไทย​ด้วย BPE

บท7 คุณ​ต่อ​ชิ้น​ส่วน​ครบ​แล้ว — Head ที่​มี causal mask, multi-head, Block (attention + MLP, pre-norm + residual) แล้ว​เดินสาย​ทั้ง​ตัว wte + wpe → blocks → LayerNorm → lm_head นี่​คือ​บท​ปิด: เอา GPT ตัว​นั้น เทรน​จริง​บน CPU เครื่อง​เดียว ให้​มัน​คาย​ข้อความ​ออก​มา แล้ว​เติม​ชิ้น​สุดท้าย​ที่​คอร์ส​ยัง​ไม่​แตะ — TokenizerTokenizerหั่น​ข้อความ​เป็น​หน่วย​ย่อย​ก่อน​ป้อน model; 1 token ≠ 1 char ≠ 1 wordProcess: ตัว​ที่​หั่น​ข้อความ​เป็น token ก่อน​ป้อน model เรา​จะ​สร้าง BPE แบบ byte-level ~15 บรรทัด แล้ว​ปิด​ท้าย​ด้วย​เดโม​ที่​ตอบ​ว่า “ทำไม​ภาษา​ไทย​ถึง​กิน token เยอะ​กว่า​อังกฤษ” — และ​ปัก​เส้น​ความ​ซื่อสัตย์​ทั้ง​สี่​เส้น​อีก​ครั้ง​ก่อน​บอกลา

📦 kaen-nn-from-scratch

code ลงมือ​ของ​บท​นี้​อยู่​ใน repo kaen-nn-from-scratch (code ตัวอย่าง​กำลัง​จัด​ทำ) — folder 08-nano-gpt/ มี model.py (GPT ทั้ง​ตัว​จาก​บท7 + generate()), train.py (CPU tiny config), และ bpe.py (minbpe แบบ​ตัด​ให้​เหลือ​แกน) ทุก file pin torch==2.13.0 (ดึง 2026-07-23) และ รัน​จบ​ได้​บน​แล็ปท็อป​ธรรมดา ไม่​ต้อง​มี GPU ส่วน​เดโม tokenizer ภาษา​ไทย​แยก​ไว้​เป็น thai_tokens.py (run-and-observe — ไม่​อยู่​ใน core training)

ตัว default ใน​เลกเชอร์​ของ Karpathy (6 ชั้น / 6 head / 384 emb / block_size 256, ~10M parameter) ออกแบบ​มา​ให้​รัน​บน GPU — เอา​มา​รัน​บน CPU จะ​กิน​เวลา​เป็น​ชั่ว​โมงๆ เรา​จึง​ใช้ CPU tiny config ที่ nanoGPT README (S4) ระบุ​ไว้​ว่า​รัน​จบ​ได้​บน​เครื่อง​ธรรมดา:

# CPU tiny config — อ้าง nanoGPT README (S4). อย่าเอา default 10M param ของเลกเชอร์มารันบน CPU
n_layer = 4
n_head = 4
n_embd = 128
block_size = 64 # context length: model เห็นย้อนหลังได้ 64 token
batch_size = 12
max_iters = 2000
learning_rate = 1e-3
dropout = 0.0
device = "cpu"

โครง GPT เป็น​ตัว​เดียว​กับ​บท7 เป๊ะ — เปลี่ยน​แค่​ตัวเลข config ด้าน​บน loop เทรน​ก็​เหมือน​บท6: AdamW + F.cross_entropy บน logits ดิบ + zero_grad → backward → step:

import torch
import torch.nn.functional as F
model = GPT(config).to("cpu") # class เดียวกับ GPTLanguageModel ในบท7 — repo ย่อชื่อเป็น GPT + รับ config (แบบ nanoGPT จริง)
opt = torch.optim.AdamW(model.parameters(), lr=learning_rate)
for it in range(max_iters):
xb, yb = get_batch("train") # get_batch/xb/yb = ชื่อแบบ nanoGPT จริง (บท4-6 ใช้ Xb/Yb) — คุณกำลัง graduate ไป repo จริง
logits = model(xb) # (B, T, vocab) — logits ดิบ
B, T, V = logits.shape
loss = F.cross_entropy(logits.view(B * T, V), yb.view(B * T))
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()
if it % 500 == 0:
print(it, float(loss)) # float(loss) — อย่าเก็บ tensor ทั้ง graph ไว้ (บท6)

รัน​บน CPU จะ​ใช้​เวลา ประมาณ 3 นาที แล้ว​ได้ val loss ราวๆ 1.88 (ตัวเลข​นี้​อ้าง​จาก nanoGPT README, S4 — ของ​คุณ​อาจ​ต่าง​เล็กน้อย​ตาม seed/เครื่อง) ผลลัพธ์​ที่ generate ออก​มา​จะ​เป็น “Shakespeare ที่​พอ​เดา​ออกว่า​เป็น​บท​ละคร แต่​เพี้ยน” — คำ​สะกด​มั่ว ประโยค​ไม่​ต่อ​เนื่อง แบบ​ที่ Karpathy โชว์​ใน README ว่า​ออก​มา​ประมาณ GLEORKEN VINGHARD III: ... นั่น​คือ นั่น​คือ​สิ่ง​ที่ model 4 ชั้น​เทรน 3 นาที​บน CPU ทำได้ — และ​มัน​ก็​ควร​จะ​เป็น​แบบ​นั้น

เส้น B — สร้าง​เพื่อ​เข้าใจ ไม่ใช่​เพื่อ deploy

“เรา​สร้าง​เพื่อ​เข้าใจ ไม่ใช่​เพื่อ deploy — micrograd กับ nano-GPT ตัว​จิ๋ว​ใน​คอร์ส​นี้​รัน​บน CPU เครื่อง​เดียว​ได้; แต่ LLM จริง​กิน​ทั้ง GPU cluster, ข้อมูล​ระดับ​เท​ระ, เวลา​เป็น​เดือน — สเกล​ที่​คอร์ส​นี้ ‘ไม่’ จำลอง”

แปล​ว่า: val loss ≈1.88 กับ Shakespeare เพี้ยนๆ คือ ปลายทาง​ที่​ตั้งใจ ไม่ใช่​ความ​ล้มเหลว — คุณ​ได้ mental model ครบ​ทั้ง forward → backward → generate แต่​ไม่​ได้ training stack จริง (data pipeline, distributed, mixed-precision, checkpointing) นั่น​คือ design Karpathy สรุป​เอง​ว่า nanoGPT “prioritizes teeth over education” (S4 README)

model ทาย​ได้​แค่ “token ถัด​ไป” ที​ละ​ตัว การ​จะ​ได้​ประโยค​ยาวๆ ต้อง​วน: ทาย​ตัว​ถัด​ไป → ต่อ​เข้าไป​ท้าย → เอา​ทั้ง​ก้อน​ป้อน​กลับ → ทาย​อีก นี่​คือ autoregressive generation และ​เป็น​จุด​ที่ SamplingSamplingสุ่ม​ตัว​ถัด​ไป​จาก​ความ​น่า​จะ​เป็น​ที่ model ให้ เพื่อ "สร้าง" ข้อความ​ใหม่Process เข้า​มา — เรา​ไม่​ได้​เลือก​ตัว​ที่​น่า​จะ​เป็น​สูงสุด​เสมอ แต่ สุ่ม จาก​ความ​น่า​จะ​เป็น​ที่ model ให้

ก่อน​ดู​ตัว​ที่​ถูก มาดู version ที่​พัง​ก่อน:

# ❌ version ดิบ — พังสองที่
def generate_buggy(self, idx, max_new_tokens):
for _ in range(max_new_tokens):
logits = self(idx)[:, -1, :] # (1) ไม่ตัด context!
idx_next = logits.argmax(dim=-1, keepdim=True) # (2) argmax เสมอ
idx = torch.cat((idx, idx_next), dim=1)
return idx

จุด​พัง (1): พอ idx ยาว​เกิน block_size แล้ว​ป้อน​เข้า model ตรงๆ — positional embedding เรียนรู้​ไว้​แค่​ตำแหน่ง 0..block_size-1 พอ​มี​ตำแหน่ง​ที่ 64, 65, … ที่​ไม่มี embedding ให้ → index error / พัง​ทันที จุด​พัง (2): argmax เลือก​ตัว​เดิม​ทุก​ครั้ง​เมื่อ context เหมือน​เดิม → ข้อความ วน​ซ้ำ​ตายตัว น่า​เบื่อ (“the the the the…”)

version ที่​ถูก แก้​ทั้ง​สอง​จุด + เพิ่ม temperature:

@torch.no_grad() # generate ไม่ต้องเก็บ gradient
def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
for _ in range(max_new_tokens):
idx_cond = idx[:, -block_size:] # (1) ตัด context ให้ยาวไม่เกิน block_size
logits = self(idx_cond)[:, -1, :] # เอา logits เฉพาะ step สุดท้าย -> (B, vocab)
if temperature <= 0: # (2) กัน temperature -> 0 (หารด้วย ~0 พัง)
idx_next = logits.argmax(dim=-1, keepdim=True) # temperature 0 = greedy = argmax
else:
logits = logits / temperature # (3) หาร temperature "ก่อน" softmax
if top_k is not None: # (ทางเลือก) เก็บแค่ k ตัวบนสุด
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = F.softmax(logits, dim=-1) # logits -> ความน่าจะเป็น
idx_next = torch.multinomial(probs, num_samples=1) # (4) สุ่ม ไม่ใช่ argmax
idx = torch.cat((idx, idx_next), dim=1) # ต่อ token ใหม่เข้าท้าย
return idx

สี่​บรรทัด​ที่​ต้อง​เข้าใจ​ให้​ขาด:

  1. ตัด context idx[:, -block_size:] — เอา​แค่ 64 token ท้าย​สุด เพราะ pos-emb รู้จัก​แค่​ตำแหน่ง 0..63 ลืม​บรรทัด​นี้ = model พัง​ทันที​ที่​ข้อความ​ยาว​เกิน block_size
  2. @torch.no_grad() — ตอน generate ไม่​ต้อง backward ปิด autograd tape ประหยัด​ทั้ง RAM และ​เวลา (เจอ​ครั้ง​แรก​บท6)
  3. temperature หาร logits ก่อน softmax — temperature สูง (มากกว่า 1) ทำให้การกระจาย​แบน (สุ่ม​หลากหลาย/มั่ว) temperature ต่ำ (น้อย​กว่า 1) ทำให้​ชัด (sharp, เดา​แน่นอน​ขึ้น) พอ temperature → 0 ก็​เหมือน​หาร​ด้วย​ศูนย์ → logits ระเบิด​เป็น inf ต้อง​มี guard สลับ​ไป​ใช้ argmax
  4. สุ่ม​ด้วย torch.multinomial ไม่ใช่ argmax — นี่​คือ​หัวใจ​ของ Sampling: model ให้ ความ​น่า​จะ​เป็น แล้ว​เรา​สุ่ม​ตาม​นั้น ทำให้​รัน​สอง​ครั้ง​ได้​ข้อความ​ต่าง​กัน และ​ไม่​ติด loop วน​ซ้ำ
flowchart TB
  S["context เริ่มต้น<br/>idx = ประโยคตั้งต้น (หรือแค่ newline เดียว)"]
  C["ตัด context<br/>idx_cond = idx[:, -block_size:]<br/>(pos-emb รู้จักแค่ 0..block_size-1)"]
  F["forward GPT<br/>เอา logits เฉพาะ step สุดท้าย<br/>self(idx_cond)[:, -1, :]"]
  T["หาร temperature 'ก่อน' softmax<br/>logits / temperature → softmax → probs"]
  M["สุ่ม (multinomial) ไม่ใช่ argmax<br/>idx_next = torch.multinomial(probs, 1)"]
  A["ต่อ token ใหม่เข้าท้าย<br/>idx = cat(idx, idx_next)"]
  D{"ครบ max_new_tokens?"}
  S --> C --> F --> T --> M --> A --> D
  D -->|"ยัง"| C
  D -->|"ครบแล้ว"| E["ได้ข้อความที่ 'สร้าง' ขึ้นใหม่"]

  classDef base fill:#fde68a,stroke:#92400e,color:#451a03;
  classDef top fill:#7c2d12,stroke:#431407,color:#fed7aa;
  class S,C,F,T,M,A base;
  class E top;

คำ​บรรยาย​ภาพ: loop autoregressive ของ generate() — เริ่ม​จาก context ตั้งต้น วน​สี่​ก้าว​ซ้ำๆ: ตัด context ให้​ไม่​เกิน block_size (เพราะ positional embedding รู้จัก​แค่​ตำแหน่ง 0 ถึง block_size-1) → forward เอา logits ของ step สุดท้าย → หาร temperature แล้ว softmax เป็น​ความ​น่า​จะ​เป็น → สุ่ม​ด้วย multinomial (ไม่ใช่ argmax) แล้ว​ต่อ token ใหม่​เข้า​ท้าย วน​จน​ครบ​จำนวน​ที่​ขอ ทุก​ก้าว​คือ​การ​ทาย “ตัว​ถัด​ไป” แค่​ตัว​เดียว

เส้น C — นี่​คือ​เครื่อง​ที่ agent ของ​คุณ​เรียก​ใช้

“backprop ตัว​เดียวกัน​นี้​แหละ​ที่​หมุน​อยู่​ข้าง​ใน model ที่ agent ของ​คุณ​ใน #15-#18 เรียก​ผ่าน API — เข้าใจ​มัน​แล้ว​จะ​เห็น​ทั้ง​กอง​ตั้งแต่ neuron ยัน tool-call; แต่​พูดตรงๆ: ไม่รู้ backprop ก็​สร้าง agent ได้”

แปล​ว่า: loop generate() ข้าง​บน​นี้​แหละ​คือ​สิ่ง​ที่​หมุน​อยู่​หลัง client.messages.create(...) ที่​คุณ​เรียก​ใน #15-#18 — parameter temperature ที่​คุณ​เคย​ส่ง​เข้า API ก็​คือ​บรรทัด logits / temperature ตัว​นี้ตรงๆ พอ​เห็น​ครบ​ทั้ง​กอง​แล้ว ค่า​พวก​นั้น​จะ​ไม่ใช่​ปุ่ม​ปริศนา​อีก​ต่อ​ไป

จนถึง​ตอน​นี้​เรา “หั่น” ข้อความ​ด้วย​วิธี​ง่าย​สุด​คือ char-level (1 ตัว​อักษร = 1 token, vocab 27 ตัว​ใน​บท3) ใช้ได้​กับ names.txt แต่ LLM จริง​ใช้ BPEBPE(Byte-Pair Encoding) tokenizer ที่​รวม​คู่ byte ที่​พบ​บ่อย​เป็น token ใหม่ (motivate ด้วย "ภาษา​ไทย​ไม่มี​เว้น​วรรค​คำ" — ไม่​เอา combining-char เข้า core code)Process (Byte-Pair Encoding) — เริ่ม​จาก byte ดิบ แล้ว เรียนรู้ ว่า​คู่ byte ไหน​พบ​บ่อย​ก็​ยุบ​รวม​เป็น token ใหม่ ซ้ำ​ไป​เรื่อยๆ จน​ได้ vocab ตาม​ขนาด​ที่​ตั้ง​ไว้ ไอเดีย​นี้​มา​จาก Sennrich, Haddow, Birch — Neural Machine Translation of Rare Words with Subword Units (ACL 2016; เผยแพร่​เป็น arXiv preprint ตั้งแต่ 2015)1 เรา​จะ​สร้าง version byte-level แบบ Karpathy’s minbpe (S6)

หัวใจ​มี​แค่2 function ช่วย (ตัด​ให้​เหลือ​แกน​จาก minbpe base.py, S6):

def get_stats(ids):
counts = {}
for pair in zip(ids, ids[1:]): # ทุกคู่ที่ติดกัน: (a,b), (b,c), ...
counts[pair] = counts.get(pair, 0) + 1
return counts # {(a, b): จำนวนครั้งที่เจอคู่นี้}
def merge(ids, pair, idx):
newids, i = [], 0
while i < len(ids):
if ids[i] == pair[0] and i < len(ids) - 1 and ids[i + 1] == pair[1]:
newids.append(idx) # เจอคู่ -> ยุบเป็น token ใหม่ 1 ตัว
i += 2
else:
newids.append(ids[i])
i += 1
return newids

แล้ว train() ก็​คือ: เริ่ม​จาก byte ดิบ วน​หา​คู่​ที่​พบ​บ่อย​ที่สุด​แล้ว​ยุบ​มัน ทำซ้ำ vocab_size - 256 รอบ:

class BasicTokenizer:
def train(self, text, vocab_size):
assert vocab_size >= 256
ids = list(text.encode("utf-8")) # เริ่มจาก "byte ดิบ" 0..255 เท่านั้น
merges = {} # (int, int) -> int (คู่ -> token ใหม่)
vocab = {i: bytes([i]) for i in range(256)} # token 0..255 = byte ตัวมันเอง
for i in range(vocab_size - 256):
stats = get_stats(ids)
pair = max(stats, key=stats.get) # คู่ที่พบบ่อยที่สุด
idx = 256 + i # token ใหม่เริ่มนับจาก 256
ids = merge(ids, pair, idx) # ยุบคู่นั้นทั้ง list
merges[pair] = idx
vocab[idx] = vocab[pair[0]] + vocab[pair[1]] # token ใหม่ = byte ของสองตัวต่อกัน
self.merges, self.vocab = merges, vocab
def decode(self, ids):
text_bytes = b"".join(self.vocab[i] for i in ids)
return text_bytes.decode("utf-8", errors="replace") # ⚠️ ต้องมี errors="replace"

สอง​จุด​ที่​ห้าม​พลาด:

  • เริ่ม​จาก byte ดิบ 0..255 ไม่ใช่​จาก​ตัว​อักษร — text.encode("utf-8") แปลง​ข้อความ​เป็น​ลำดับ byte ก่อน token เริ่มต้น​จึง​มี​แค่ 256 ตัว (ทุก​ค่า byte ที่​เป็น​ไป​ได้) แล้ว​ค่อย เรียน token ที่​ยาว​ขึ้น​เอง​จาก​สถิติ นี่​คือ​เหตุผล​ที่​มัน​หั่น​อะไร​ก็ได้ — ภาษา, code, emoji — โดย​ไม่​ต้อง​รู้​กฎ​ภาษา​เลย
  • decode ต้อง​ใส่ errors="replace" เพราะ 1 token อาจ​เป็น​แค่ ส่วน​หนึ่ง ของ​อักขระ (อักขระ​ไทย 1 ตัว​ยาว 3 byte ใน UTF-8 — token อาจ​ตัด​กลาง​อักขระ​ได้) ลำดับ token ที่​สุ่ม​มา​จึง​อาจ​ถอด​เป็น UTF-8 ที่​ไม่ valid ถ้า​ไม่​ใส่ errors="replace" จะ throw ทันที ใส่​แล้ว​มัน​แทน​ด้วย (U+FFFD) แทน​การ​พัง

นี่​คือ​จุด​ที่ byte-level BPE ส่อง​ประกาย และ​เป็น​เหตุผล​ว่า​ทำไม LLM ถึง​เลือก​วิธี​นี้:

ภาษา​อังกฤษ “โกง” ได้ เพราะ​มัน​มี​เว้น​วรรค​คั่น​คำ​อยู่​แล้ว — "hello world" หั่น​ตรง​ช่องว่าง​ก็ได้​คำ​สอง​คำ​เป๊ะ แต่​ภาษา​ไทย​ไม่มี​ขอบเขต​คำฉันกินข้าว คือ ฉัน (I) + กิน (eat) + ข้าว (rice) เขียน​ติด​กัน​หมด ไม่มี​ตัว​คั่น ตัว​หั่น​ที่ split ด้วย​ช่องว่าง​จะ​เห็น​ทั้ง​ก้อน​เป็น “คำ​เดียว” ทันที

byte-level BPE ไม่​แคร์​เรื่อง​นี้​เลย — มัน​เรียน​จาก สถิติ byte ล้วน ไม่​ต้อง​รู้​ว่า​คำ​จบ​ตรง​ไหน จึง tokenize ไทย/จีน/code/emoji ได้​ด้วย​กลไก​เดียวกัน​โดย​ไม่​ต้อง​มี​กฎ​เฉพาะ​ภาษา นั่น​คือ กำไร แต่​มี ต้นทุน​ที่​ต้อง​พูดตรงๆ: อักขระ​ไทย 1 ตัว = 3 byte UTF-8 (ยัง​ไม่​นับ​สระ/วรรณยุกต์​ที่​เป็น combining mark ซ้อน​เข้าไป​อีก) ข้อความ​ไทย​จึง​กิน token ต่อ “คำ” มากกว่า​อังกฤษ — context window เต็ม​เร็ว​กว่า และ​คิด​เงิน​ต่อ​คำ​แพง​กว่า

ลอง​รัน​เอง​เพื่อ เห็น ความ​ต่าง (อย่า​เชื่อ​เลข​ที่​ผม​พิมพ์ — รัน​เอง):

# รันเอง — pip install tiktoken ก่อน; ตัวเลขขึ้นกับ "version tiktoken" ที่คุณติดตั้ง ณ วันที่รัน
import tiktoken
enc = tiktoken.get_encoding("cl100k_base") # tokenizer ตระกูล GPT-4
print(len(enc.encode("hello"))) # อังกฤษสั้น ๆ
print(len(enc.encode("สวัสดีครับ"))) # ไทยที่คนอ่านว่า "ยาวพอ ๆ กัน"

ใน​การ​รัน​ครั้ง​หนึ่ง "hello" ออก​มา​ราว 1 token ส่วน "สวัสดีครับ" ออก​มา หลาย token — มัก​หลาย​เท่าตัว​ของ hello ถึง​แม้​คน​ไทย​จะ​รู้สึก​ว่า​มัน “ยาว​พอกัน” — ตัวเลข​เป๊ะ​ขึ้น​กับ version tiktoken และ​สารบบ merge ของ​มัน จึง​ต้อง รัน​เอง​แล้ว​ดู​เลข​จริง ไม่​ควร hard-code ไว้ (นี่​คือ​เหตุผล​ที่​บท​นี้​ไม่​ปัก​ตัวเลข​ตายตัว​ให้​คุณ)

ทำไม​เดโม​นี้ 'ไม่' อยู่​ใน core training code

core training ของ nano-GPT ใน​บท​นี้​ยัง​เดิน​บน tinyshakespeare (Latin) เหมือน​เดิม — เรา​จงใจ​กัน combining-char ของ​ไทย (สระ​ลอย/วรรณยุกต์​ที่​เป็น byte ซ้อน) ออก​จาก loop เทรน เพราะ​มัน​เพิ่ม​ความ​ซับซ้อน​ที่ บดบัง สิ่ง​ที่​บท​นี้​อยาก​ให้​เห็น (forward → generate → BPE) เดโม​ภาษา​ไทย​จึง​เป็น “run-and-observe” ล้วนๆ — ไว้​เห็น​เหตุผล​ของ byte-level BPE ไม่ใช่​ของ​ที่​เอา​ไป​เทรน

5. ปิด​คอร์ส — เส้น​ความ​ซื่อสัตย์​ทั้ง​สี่ + ไป​ต่อ​ที่ไหน

หัวข้อ​ที่​มีชื่อ​ว่า “5. ปิด​คอร์ส — เส้น​ความ​ซื่อสัตย์​ทั้ง​สี่ + ไป​ต่อ​ที่ไหน”

คุณ​ไต่​บันได​ครบ​ทั้ง​แปด​ขั้น​แล้ว: neuron → autograd (micrograd) → bigram → MLP + embedding → training internals → PyTorch → transformer → nano-GPT ที่ generate ได้​จริง ก่อน​บอกลา ปัก​เส้น​ความ​ซื่อสัตย์​ทั้ง​สี่​อีก​ครั้ง — บท​เปิด (บท1) กับ​บท​ปิด​นี้​แบก​ครบ​ทั้ง​สี่​โดย​ตั้งใจ:

เส้น A — ของสด​ใช้ repo, ของ​นิ่ง​ใช้​ตำรา

“ของสด​ใช้ repo, ของ​นิ่ง​ใช้​ตำรา — code ของ Karpathy คือ repo ที่​ยัง​มี​ชีวิต เรา​จึง​อ้าง​ด้วย commit SHA + วัน​ที่​ดึง ไม่ใช่​เลข​รุ่น; ส่วน​คณิต​ที่​ไม่​เน่า​เรา​อ้าง Goodfellow-Bengio-Courville (MIT Press, 2016)”

หลัก​ฐานสดๆ: nanoGPT (S4) ถูก​ทำ​เครื่องหมาย deprecated ใน​เดือน​พฤศจิกายน 2025 — repo ขยับ​ใต้เท้า​เราจริงๆ ระหว่าง​ที่​คอร์ส​นี้​ยัง​เขียน​อยู่ นี่แหละ​คือ​เหตุผล​ที่​เรา pin ทุก​อย่าง​ด้วย SHA + วัน​ที่​ดึง ไม่ใช่ “version ล่าสุด”

เส้น B — สร้าง​เพื่อ​เข้าใจ ไม่ใช่​เพื่อ deploy

“เรา​สร้าง​เพื่อ​เข้าใจ ไม่ใช่​เพื่อ deploy — micrograd กับ nano-GPT ตัว​จิ๋ว​ใน​คอร์ส​นี้​รัน​บน CPU เครื่อง​เดียว​ได้; แต่ LLM จริง​กิน​ทั้ง GPU cluster, ข้อมูล​ระดับ​เท​ระ, เวลา​เป็น​เดือน — สเกล​ที่​คอร์ส​นี้ ‘ไม่’ จำลอง”

เส้น C — นี่​คือ​เครื่อง​ที่ agent ของ​คุณ​เรียก​ใช้

“backprop ตัว​เดียวกัน​นี้​แหละ​ที่​หมุน​อยู่​ข้าง​ใน model ที่ agent ของ​คุณ​ใน #15-#18 เรียก​ผ่าน API — เข้าใจ​มัน​แล้ว​จะ​เห็น​ทั้ง​กอง​ตั้งแต่ neuron ยัน tool-call; แต่​พูดตรงๆ: ไม่รู้ backprop ก็​สร้าง agent ได้”

เส้น D — คณิต​เป็น​พื้น​ให้​ยืน ไม่ใช่​กำแพง​กั้น​ทาง

“เรา​สอน ‘สัญชาตญาณ’ ของ gradient กับ chain rule พอ​ให้ code มี​ความหมาย ไม่ใช่​พิสูจน์​ทฤษฎีบท — อยาก​ลึก​เชิง​พิสูจน์ เปิด Goodfellow บท​ที่​อ้าง​ไว้; คณิต​ตรง​นี้​เป็น​พื้น​ให้​ยืน ไม่ใช่​กำแพง​กั้น​ทาง”

แล้วไป​ต่อ​ที่ไหน? nano-GPT ใน​คอร์ส​นี้​จบ​ที่ “ทาย​ตัว​ถัด​ไป​แบบ​ดิบ” (pretraining ล้วนๆ) — ยัง​ไม่มี​ขั้น finetune ให้​มัน​คุย​เป็น​ผู้​ช่วย ถ้า​อยาก​เดิน​ต่อ​ทั้ง​กอง​ตั้งแต่ tokenizer → pretrain → finetune → inference เป็น ChatGPT จิ๋ว​ตัว​เดียว​จบ ให้ graduate ไป​ที่ nanochat (S18, github.com/karpathy/nanochat) — ตัว​ที่ Karpathy ทำ​ต่อ​จาก nanoGPT (ซึ่ง​ตอน​นี้ deprecated แล้ว) มัน​คือ “ขั้น​ถัด​ไป” ที่​ต่อยอด​จาก​ทุก​อย่าง​ที่​คุณ​เพิ่ง​สร้าง​ด้วย​มือ​ใน​แปดบท​นี้​พอดี

จบ​แล้ว — จาก​ที่​เคย ใช้ model ผ่าน API ใน #15-#18 ตอน​นี้​คุณ สร้าง มัน​เอง​ครบ​ทั้ง​กอง ตั้งแต่ neuron ตัว​เดียว​จน generate ข้อความ​ออก​มา​ได้ ครั้ง​หน้าที่​พิมพ์ client.messages.create(...) คุณ​จะ​รู้​ว่า​อะไร​กำลัง​หมุน​อยู่​ข้าง​ใน


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

เช็กความเข้าใจ — บทที่ 8

ข้อ 1 / 3

ใน loop generate() ทำไมต้องมีบรรทัด idx_cond = idx[:, -block_size:] (ตัด context) ก่อนป้อน model?

  1. Sennrich, Haddow, Birch — Neural Machine Translation of Rare Words with Subword Units. ตี​พิมพ์​ที่ ACL 2016; เผยแพร่​เป็น arXiv:1508.07909 preprint ตั้งแต่​ปี 2015 (อ้าง​ปี​ตี​พิมพ์ 2016 ตาม​ธรรมเนียม เชิงอรรถ​ปี​คือ preprint)