สร้าง 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 เยอะกว่าอังกฤษ” — และปักเส้นความซื่อสัตย์ทั้งสี่เส้นอีกครั้งก่อนบอกลา
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)
1. เทรน GPT ตัวจิ๋วบน CPU — config ที่รันจบได้จริง
หัวข้อที่มีชื่อว่า “1. เทรน GPT ตัวจิ๋วบน CPU — config ที่รันจบได้จริง”ตัว 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 ของเลกเชอร์มารันบน CPUn_layer = 4n_head = 4n_embd = 128block_size = 64 # context length: model เห็นย้อนหลังได้ 64 tokenbatch_size = 12max_iters = 2000learning_rate = 1e-3dropout = 0.0device = "cpu"โครง GPT เป็นตัวเดียวกับบท7 เป๊ะ — เปลี่ยนแค่ตัวเลข config ด้านบน loop เทรนก็เหมือนบท6: AdamW + F.cross_entropy บน logits ดิบ + zero_grad → backward → step:
import torchimport 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 ทำได้ — และมันก็ควรจะเป็นแบบนั้น
“เราสร้างเพื่อเข้าใจ ไม่ใช่เพื่อ 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)
2. generate() — คายข้อความออกมาทีละ token
หัวข้อที่มีชื่อว่า “2. generate() — คายข้อความออกมาทีละ token”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 ไม่ต้องเก็บ gradientdef 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สี่บรรทัดที่ต้องเข้าใจให้ขาด:
- ตัด context
idx[:, -block_size:]— เอาแค่ 64 token ท้ายสุด เพราะ pos-emb รู้จักแค่ตำแหน่ง0..63ลืมบรรทัดนี้ = model พังทันทีที่ข้อความยาวเกิน block_size @torch.no_grad()— ตอน generate ไม่ต้อง backward ปิด autograd tape ประหยัดทั้ง RAM และเวลา (เจอครั้งแรกบท6)temperatureหาร logits ก่อน softmax — temperature สูง (มากกว่า 1) ทำให้การกระจายแบน (สุ่มหลากหลาย/มั่ว) temperature ต่ำ (น้อยกว่า 1) ทำให้ชัด (sharp, เดาแน่นอนขึ้น) พอtemperature → 0ก็เหมือนหารด้วยศูนย์ → logits ระเบิดเป็น inf ต้องมี guard สลับไปใช้ argmax- สุ่มด้วย
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 ใหม่เข้าท้าย วนจนครบจำนวนที่ขอ ทุกก้าวคือการทาย “ตัวถัดไป” แค่ตัวเดียว
“backprop ตัวเดียวกันนี้แหละที่หมุนอยู่ข้างใน model ที่ agent ของคุณใน #15-#18 เรียกผ่าน API — เข้าใจมันแล้วจะเห็นทั้งกองตั้งแต่ neuron ยัน tool-call; แต่พูดตรงๆ: ไม่รู้ backprop ก็สร้าง agent ได้”
แปลว่า: loop generate() ข้างบนนี้แหละคือสิ่งที่หมุนอยู่หลัง client.messages.create(...) ที่คุณเรียกใน #15-#18 — parameter temperature ที่คุณเคยส่งเข้า API ก็คือบรรทัด logits / temperature ตัวนี้ตรงๆ พอเห็นครบทั้งกองแล้ว ค่าพวกนั้นจะไม่ใช่ปุ่มปริศนาอีกต่อไป
3. BPE — ตัวที่หั่นข้อความก่อนป้อน model
หัวข้อที่มีชื่อว่า “3. BPE — ตัวที่หั่นข้อความก่อนป้อน model”จนถึงตอนนี้เรา “หั่น” ข้อความด้วยวิธีง่ายสุดคือ 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) แทนการพัง
4. ทำไมภาษาไทยกิน token เยอะกว่า — เดโม run-and-observe
หัวข้อที่มีชื่อว่า “4. ทำไมภาษาไทยกิน token เยอะกว่า — เดโม run-and-observe”นี่คือจุดที่ 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 tiktokenenc = tiktoken.get_encoding("cl100k_base") # tokenizer ตระกูล GPT-4print(len(enc.encode("hello"))) # อังกฤษสั้น ๆprint(len(enc.encode("สวัสดีครับ"))) # ไทยที่คนอ่านว่า "ยาวพอ ๆ กัน"ในการรันครั้งหนึ่ง "hello" ออกมาราว 1 token ส่วน "สวัสดีครับ" ออกมา หลาย token — มักหลายเท่าตัวของ hello ถึงแม้คนไทยจะรู้สึกว่ามัน “ยาวพอกัน” — ตัวเลขเป๊ะขึ้นกับ version tiktoken และสารบบ merge ของมัน จึงต้อง รันเองแล้วดูเลขจริง ไม่ควร hard-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) กับบทปิดนี้แบกครบทั้งสี่โดยตั้งใจ:
“ของสดใช้ repo, ของนิ่งใช้ตำรา — code ของ Karpathy คือ repo ที่ยังมีชีวิต เราจึงอ้างด้วย commit SHA + วันที่ดึง ไม่ใช่เลขรุ่น; ส่วนคณิตที่ไม่เน่าเราอ้าง Goodfellow-Bengio-Courville (MIT Press, 2016)”
หลักฐานสดๆ: nanoGPT (S4) ถูกทำเครื่องหมาย deprecated ในเดือนพฤศจิกายน 2025 — repo ขยับใต้เท้าเราจริงๆ ระหว่างที่คอร์สนี้ยังเขียนอยู่ นี่แหละคือเหตุผลที่เรา pin ทุกอย่างด้วย SHA + วันที่ดึง ไม่ใช่ “version ล่าสุด”
“เราสร้างเพื่อเข้าใจ ไม่ใช่เพื่อ deploy — micrograd กับ nano-GPT ตัวจิ๋วในคอร์สนี้รันบน CPU เครื่องเดียวได้; แต่ LLM จริงกินทั้ง GPU cluster, ข้อมูลระดับเทระ, เวลาเป็นเดือน — สเกลที่คอร์สนี้ ‘ไม่’ จำลอง”
“backprop ตัวเดียวกันนี้แหละที่หมุนอยู่ข้างใน model ที่ agent ของคุณใน #15-#18 เรียกผ่าน API — เข้าใจมันแล้วจะเห็นทั้งกองตั้งแต่ neuron ยัน tool-call; แต่พูดตรงๆ: ไม่รู้ backprop ก็สร้าง agent ได้”
“เราสอน ‘สัญชาตญาณ’ ของ 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(...) คุณจะรู้ว่าอะไรกำลังหมุนอยู่ข้างใน
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Karpathy — nanoGPT (CPU recipe + garbled-Shakespeare) (S4, commit
3adf61e154c3fe3fca428ad6bc3818b27a3b8291, 2025-11-12; เข้าถึง 2026-07-23) — CPU tiny config (4L/4H/128d/64ctx/2000it → ~3 นาที, val loss ≈1.88) และตัวอย่างผลลัพธ์ ⚠️ repo นี้ถูก deprecated พฤศจิกายน 2025 → ชี้ไป nanochat - Karpathy — minbpe (BPE tokenizer) (S6, commit
1acefe89412b20245db5a22d2a02001e547dc602, 2024-04-22; re-verified 2026-07-23) —get_stats/merge/train/decode(byte-level, เริ่มจาก byte ดิบ 0..255) - Sennrich, Haddow, Birch — Neural Machine Translation of Rare Words with Subword Units (S15, ACL 2016; arXiv preprint 2015) — ต้นทางไอเดีย BPE สำหรับ subword tokenization
- Vaswani et al. — Attention Is All You Need (S14, NeurIPS 2017; arXiv:1706.03762) — สถาปัตยกรรม transformer ที่ GPT ในบทนี้ต่อขึ้นมา
- Karpathy — nanochat (pointer “ไปต่อ”) (S18, active ปลายปี 2025; เข้าถึง 2026-07-23) — successor ของ nanoGPT: tokenizer → pretrain → finetune → inference ครบสาย
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3ใน loop generate() ทำไมต้องมีบรรทัด idx_cond = idx[:, -block_size:] (ตัด context) ก่อนป้อน model?
Footnotes
หัวข้อที่มีชื่อว่า “Footnotes”-
Sennrich, Haddow, Birch — Neural Machine Translation of Rare Words with Subword Units. ตีพิมพ์ที่ ACL 2016; เผยแพร่เป็น arXiv:1508.07909 preprint ตั้งแต่ปี 2015 (อ้างปีตีพิมพ์ 2016 ตามธรรมเนียม เชิงอรรถปีคือ preprint) ↩