MLP + embedding — model ที่ 'จำ context' ได้ (Bengio 2003)
บท3 เราสร้าง bigram Language ModelLanguage Modelmodel ที่ทายตัวถัดไปจากที่ผ่านมา — งานหลักของทั้งคอร์สArchitecture — model ที่ดู “ตัวอักษรก่อนหน้าแค่ตัวเดียว” แล้วทายตัวถัดไป มันไปได้ไกลสุดที่ loss ≈ 2.45 เพราะ context แค่ตัวเดียวมันน้อยเกินไป ถ้าอยากทายเก่งขึ้นต้องให้มันเห็น context ยาวขึ้น — แต่พอจะขยายเป็น trigram (ดู 2 ตัว) หรือมากกว่านั้นด้วยตาราง count ตรงๆ จำนวนช่องมันระเบิดแบบ 27ⁿ ช่อง เร็วเกินจะไหว และช่องส่วนใหญ่จะว่างเปล่า (ไม่เคยเจอ context นั้นในข้อมูล)
ปี 2003 Bengio และคณะ (S9) เสนอทางออกที่กลายเป็นรากของ LLM ยุคนี้: แทนที่จะ “นับ” ให้ (1) แปลงแต่ละตัวอักษรเป็นเวกเตอร์หนาแน่นที่ “เรียนรู้ได้” เรียกว่า EmbeddingEmbeddingตาราง weight ที่แปลง token เป็นเวกเตอร์หนาแน่นที่เรียนรู้ได้ (สร้างด้วยมือที่นี่ เพื่อเห็นว่าเป็นแค่ lookup table)Architecture แล้ว (2) ป้อนเวกเตอร์เหล่านั้นเข้า MLPMLPเน็ตชั้นซ่อนแบบเชื่อมทุกจุด (Bengio 2003) — ก้าวจาก bigram ไป model ที่ "จำ context" ได้Architecture (multi-layer perceptron — เน็ตที่มี “ชั้นซ่อน”) ให้มันเรียนความสัมพันธ์เอง ผลคือ model ที่ “จำ context” ได้หลายตัวโดยไม่ต้องมีตารางระเบิด และ generalize ข้าม context ที่คล้ายกันได้ (context ที่ไม่เคยเจอเป๊ะ ก็ยังทายได้ถ้ามันคล้ายอันที่เคยเจอ) บทนี้เราจะสร้างมันด้วย NumPy ล้วน
code ลงมือของบทนี้อยู่ใน repo kaen-nn-from-scratch (code ตัวอย่างกำลังจัดทำ) file 04_mlp.py — pure Python + NumPy เท่านั้น ไม่มี import torch ตามกติกา framework-quarantine ของบท2–5 รันบน CPU เครื่องเดียวได้ในไม่กี่นาที
โน้ตบุ๊ก makemore_part2_mlp.ipynb ของ Karpathy (S1) เขียนด้วย PyTorch ตั้งแต่ต้น (torch.tensor, autograd, F.cross_entropy) แต่คอร์สนี้กัน framework ออกจากบท2–5 ไว้ก่อน — เราจึง เขียนใหม่เป็น pure NumPy และคำนวณ backward ด้วยมือ เพื่อให้เห็นทุกการคูณเมทริกซ์ชัดๆ คณิตเหมือนกันเป๊ะ ต่างแค่เครื่องมือ พอถึงบท6 ค่อย graduate ไป PyTorch ที่ทำ backward ให้อัตโนมัติ — อย่า copy-paste จาก notebook ของเขามาปนกับ code บทนี้
จากตัวเดียวเป็นสามตัว: sliding context window
หัวข้อที่มีชื่อว่า “จากตัวเดียวเป็นสามตัว: sliding context window”หัวใจแรกคือเปลี่ยน “มองย้อน 1 ตัว” เป็น “มองย้อน block_size ตัว” เราตั้ง block_size = 3 แล้วเดิน window เลื่อนไปทีละตัวอักษรตลอดทั้งชื่อ โดยเติม . (index 0) เป็น padding ตอนต้นชื่อ ยกตัวอย่างชื่อ emma (block_size=3):
| context (3 ตัว) | target ตัวถัดไป |
|---|---|
. . . | e |
. . e | m |
. e m | m |
e m m | a |
m m a | . (จบชื่อ) |
แต่ละแถวคือหนึ่งตัวอย่างเทรน (X = 3 index ของ context, Y = index ตัวถัดไป) function build_dataset แปลงทุกชื่อเป็นตารางแบบนี้:
import numpy as np, random
words = open('names.txt').read().splitlines() # 32,033 ชื่อ (จากบท3)chars = sorted(set(''.join(words)))stoi = {s: i + 1 for i, s in enumerate(chars)}; stoi['.'] = 0 # off-by-one จากบท3itos = {i: s for s, i in stoi.items()} # vocab = 27
block_size = 3 # ดู context กี่ตัวอักษรก่อนหน้า
def build_dataset(words): X, Y = [], [] for w in words: context = [0] * block_size # เริ่มด้วย '.' (index 0) เต็ม padding for ch in w + '.': # เดินทุกตัวอักษร + '.' ปิดท้าย ix = stoi[ch] X.append(context) # context ปัจจุบัน → input Y.append(ix) # ตัวถัดไป → target context = context[1:] + [ix] # เลื่อน window: ทิ้งตัวซ้ายสุด ต่อตัวใหม่ขวาสุด return np.array(X), np.array(Y) # X:(N,3) int, Y:(N,) intเราแบ่งข้อมูล 80/10/10 เป็น train / dev / test ตั้งแต่ตอนนี้ (เดี๋ยวอธิบายว่าทำไม):
random.seed(42)random.shuffle(words)n1, n2 = int(0.8 * len(words)), int(0.9 * len(words))Xtr, Ytr = build_dataset(words[:n1]) # 80% เทรน (~182,000 ตัวอย่าง)Xdev, Ydev = build_dataset(words[n1:n2]) # 10% dev (จูน hyperparameter)Xte, Yte = build_dataset(words[n2:]) # 10% test (แตะให้น้อยที่สุด)embedding: ตาราง lookup ที่ “เทรนได้”
หัวข้อที่มีชื่อว่า “embedding: ตาราง lookup ที่ “เทรนได้””Embedding ใน model นี้ จริงๆ มันคือแค่ ตาราง 2 มิติ ตัวหนึ่ง เราตั้งชื่อมันว่า C ขนาด (27, 10): 27 แถว (หนึ่งแถวต่อหนึ่งตัวอักษรใน vocab) แถวละ 10 ตัวเลข การ “lookup” embedding ของตัวอักษร index i ก็คือหยิบแถวที่ i ออกมา — C[i] เท่านั้นเอง ไม่มีการค้นหา ไม่มีการวัดระยะ เป็น integer lookup ตรงๆ
ตอนเริ่ม C เป็นตัวเลขสุ่มล้วน ไม่มีความหมายอะไร แต่มันคือ weight ตัวหนึ่งเหมือน W1/W2 — Gradient DescentGradient Descentloop ฝึก: ขยับ weight สวนทาง gradient ทีละก้าวให้ loss ลดProcess จะค่อยๆ ขยับมันทุกก้าว จนแถวของตัวอักษรที่ “ใช้แทนกันได้” (เช่นกลุ่มสระ) เลื่อนเข้ามาใกล้กันเอง นี่คือจุดที่ต้องแยกให้คมกับคอร์ส #16:
“backprop ตัวเดียวกันนี้แหละที่หมุนอยู่ข้างใน model ที่ agent ของคุณใน #15-#18 เรียกผ่าน API — เข้าใจมันแล้วจะเห็นทั้งกองตั้งแต่ neuron ยัน tool-call; แต่พูดตรงๆ: ไม่รู้ backprop ก็สร้าง agent ได้”
แปลว่า (ในบริบท embedding): ใน #16 context-engineering คุณใช้ embedding เป็น เครื่องมือ (tool) — เวกเตอร์ที่เอาไปวัด cosine distance เพื่อ retrieve เอกสารเข้า context (จำได้ไหมว่าเราหมายเหตุไว้ว่า Claude ไม่มี embeddings endpoint ต้องใช้ provider แยก) ที่นี่ embedding เป็น วัตถุ (object) — ตาราง weight C ที่เริ่มจากเลขสุ่มแล้ว backprop ตัวเดียวกับที่หมุนใน model ที่ agent เรียก คอย “ขยับ” มันระหว่างเทรน คำเดียวกัน บทบาทคนละเรื่อง: อันหนึ่งเอาไปค้นเอกสาร อีกอันเป็น parameter เรียนรู้ได้ ข้างใน model — และที่นี่เราสร้างมันเองด้วยมือได้เต็มๆ
forward pass: จาก index ถึง logits
หัวข้อที่มีชื่อว่า “forward pass: จาก index ถึง logits”ประกอบร่าง model — ตาราง embedding บวก weight อีก 2 ชั้น:
block_size, n_embd, n_hidden, V = 3, 10, 200, 27
np.random.seed(2147483647) # seed คงที่เพื่อผลลัพธ์ทวนซ้ำได้C = np.random.randn(V, n_embd) # (27, 10) ตาราง embeddingW1 = np.random.randn(block_size * n_embd, n_hidden) # (30, 200) fan_in ต้อง = 3×10 = 30b1 = np.random.randn(n_hidden) # (200,)W2 = np.random.randn(n_hidden, V) # (200, 27)b2 = np.random.randn(V) # (27,)parameters = [C, W1, b1, W2, b2]loss เราใช้ fused cross-entropy — คำนวณ log-softmax + NLL ในก้าวเดียว ทำงานใน log-space ตลอด (ไม่สร้าง probs แล้วค่อย .log() แบบบท3 ที่เสี่ยง log(0) = −inf):
def cross_entropy(logits, Y): logits = logits - logits.max(axis=1, keepdims=True) # STABILITY: ลบ max ต่อแถว logsumexp = np.log(np.exp(logits).sum(axis=1, keepdims=True)) logprobs = logits - logsumexp # log P ของทุก class โดยตรง return -logprobs[np.arange(len(Y)), Y].mean() # −mean(log P ของ class ที่ถูก)ทีนี้ forward — จับตา shape ทุกก้าว ตรงนี้แหละที่พังบ่อยที่สุดในบท:
emb = C[Xtr] # (N, 3, 10) fancy-index lookupembcat = emb.reshape(-1, block_size * n_embd) # (N, 30) ต่อ 3 เวกเตอร์เป็นเส้นเดียวh = np.tanh(embcat @ W1 + b1) # (N, 200) ชั้นซ่อน tanhlogits = h @ W2 + b2 # (N, 27) คะแนนดิบ (raw logits)loss = cross_entropy(logits, Ytr)print(loss) # ก้าวแรก: สูงกว่า 3.2958 อยู่มาก แล้วจะร่วงเร็วใน 2-3 ก้าวแรกจุดที่ต้องเข้าใจให้ขาด: C[Xtr] เมื่อ Xtr มีรูป (N, 3) จะได้ (N, 3, 10) — แต่ละตัวอย่างมี 3 ตัวอักษร แต่ละตัวเป็นเวกเตอร์ 10 มิติ เราต้อง ต่อสามเวกเตอร์เป็นเส้นเดียว ด้วย .reshape(-1, 30) ให้ได้ (N, 30) และเลข 30 นี้ต้องเท่ากับมิติแรกของ W1 เป๊ะ ไม่งั้น @ W1 จะ error ทันที (3 × 10 = 30 คือที่มาของ block_size * n_embd)
flowchart LR X["X : (N, 3)<br/>index ของ 3 ตัวอักษร context"] C["ตาราง embedding C : (27, 10)<br/>weight ที่เทรนได้"] E["emb = C[X] : (N, 3, 10)<br/>lookup ทีละแถว"] R["reshape : (N, 30)<br/>ต่อ 3 เวกเตอร์ = 3×10 = 30"] H["h = tanh(embcat@W1+b1) : (N, 200)<br/>ชั้นซ่อน"] L["logits = h@W2+b2 : (N, 27)<br/>คะแนนดิบ 27 ตัวอักษร"] C --> E X --> E E --> R --> H --> L classDef tbl fill:#fde68a,stroke:#92400e,color:#451a03; classDef acc fill:#7c2d12,stroke:#431407,color:#fed7aa; class X,E,R,H tbl; class C,L acc;
คำบรรยายภาพ: เส้นทางของ forward pass ใน model MLP — เริ่มจาก index ของ 3 ตัวอักษร context (X รูป (N,3)) วิ่งเข้าตาราง embedding C ที่เป็น weight เทรนได้ (27 แถว แถวละ 10 มิติ) ได้ผลลัพธ์ (N,3,10) แล้ว reshape ต่อสามเวกเตอร์เป็นเส้นเดียว (N,30) — เลข 30 นี้ต้องเท่ากับมิติแรกของ W1 พอดี ไม่งั้น matmul พัง — ผ่านชั้นซ่อน tanh ได้ (N,200) จบที่ logits (N,27) คือคะแนนดิบของตัวอักษรถัดไปทั้ง 27 ตัว
ถ้า model “ไม่มีความมั่นใจเลย” (แจกความน่าจะเป็นเท่ากันทั้ง 27 ตัว) loss เริ่มต้นควรเป็น −ln(1/27) = ln(27) ≈ 3.2958 — เลขนี้ derive ได้ตรงๆ ไม่ขึ้นกับการรัน แต่ด้วย init แบบสุ่มดิบ (randn ล้วน ไม่ได้สเกล + bias สุ่ม) logits เริ่มต้นจะกระจายกว้างเกินไป → loss ก้าวแรกจึง “สูงกว่า 3.2958 มาก” แล้วร่วงพรวดใน 2-3 ก้าวแรก (graph รูป “ไม้ฮอกกี้”) นั่นคือ การเทรนเสียเปล่า ไปกับการรีดให้ logits เล็กลงก่อนจะเริ่มเรียนของจริง เราจะ ไม่ แก้มันในบทนี้ — วิธีตั้ง init ให้ loss เริ่มที่ ≈3.2958 พอดี (สเกล gain/√fan_in + กด last layer ให้ W2*=0.01, b2=0) เป็นเนื้อหาของ บท5 เต็มๆ
minibatch: gradient หยาบๆ ที่ถี่ ชนะ gradient เป๊ะ ที่นานๆ ที
หัวข้อที่มีชื่อว่า “minibatch: gradient หยาบๆ ที่ถี่ ชนะ gradient เป๊ะ ที่นานๆ ที”ชุดเทรนมี ~182,000 ตัวอย่าง ถ้าจะคำนวณ gradient “เป๊ะ” จากทั้งชุดทุกก้าว มันช้ามาก MinibatchMinibatchฝึกทีละก้อนย่อยของข้อมูล (ไม่ใช่ทั้งชุด) ให้เร็วและเสถียรพอดีProcess คือการสุ่มก้อนย่อย (เช่น 32 ตัวอย่าง) มาคำนวณ gradient แบบ “ประมาณ” ต่อก้าว — ทิศทางมันหยาบ (noisy) แต่ถูกพอ และเราเดินได้ถี่กว่ามาก สรุปเป็นประโยคเดียว: gradient หยาบๆ ที่เดินหลายก้าว ชนะ gradient เป๊ะ ที่เดินไม่กี่ก้าว
และนี่คือ loop เทรนเต็มๆ — backward ข้างในคือ Chain RuleChain Ruleกฎลูกโซ่: คูณอนุพันธ์ต่อกันทีละ node — หัวใจที่ทำให้ backprop เป็นไปได้Process ตัวเดียวกับบท2 เป๊ะ เพียงแต่คราวนี้ทำทีเดียวทั้ง array (นี่จะเป็น ครั้งสุดท้าย ที่เราเขียน backward ด้วยมือ — บท6 PyTorch จะทำให้อัตโนมัติ):
batch_size = 32
for step in range(200000): # 1) minibatch: สุ่ม 32 ตัวอย่างจากชุดเทรน ix = np.random.randint(0, Xtr.shape[0], (batch_size,)) Xb, Yb = Xtr[ix], Ytr[ix]
# 2) forward emb = C[Xb] # (32, 3, 10) embcat = emb.reshape(-1, block_size * n_embd) # (32, 30) h = np.tanh(embcat @ W1 + b1) # (32, 200) logits = h @ W2 + b2 # (32, 27) loss = cross_entropy(logits, Yb)
# 3) backward — chain rule เดียวกับบท2 แต่ทำทั้ง array พร้อมกัน probs = np.exp(logits - logits.max(axis=1, keepdims=True)) probs /= probs.sum(axis=1, keepdims=True) # softmax (ใช้เฉพาะตอน backward) dlogits = probs dlogits[np.arange(batch_size), Yb] -= 1 dlogits /= batch_size # d(loss)/d(logits) = (softmax − onehot)/B dW2 = h.T @ dlogits # (200, 27) db2 = dlogits.sum(0) # (27,) dh = dlogits @ W2.T # (32, 200) dhpreact = dh * (1 - h**2) # tanh': 1 − h² dW1 = embcat.T @ dhpreact # (30, 200) db1 = dhpreact.sum(0) # (200,) dembcat = dhpreact @ W1.T # (32, 30) demb = dembcat.reshape(-1, block_size, n_embd) # (32, 3, 10) dC = np.zeros_like(C) np.add.at(dC, Xb, demb) # scatter-add: index ที่ซ้ำต้อง "บวกสะสม" (กฎ += จากบท2!) grads = [dC, dW1, db1, dW2, db2]
# 4) update — SGD หนึ่งก้าว + LR decay lr = 0.1 if step < 100000 else 0.01 for p, g in zip(parameters, grads): p -= lr * gบรรทัด np.add.at(dC, Xb, demb) คือหัวใจ: ถ้าตัวอักษรเดียวโผล่ในหลายตำแหน่งของ batch (ซึ่งเกิดตลอด) gradient ของมันต้อง บวกสะสม ทุกจุด — นี่คือกฎ += fan-out ตัวเดียวกับที่บท2 ย้ำ (ถ้าเผลอเขียนทับด้วย = gradient จะเหลือแค่จุดสุดท้าย ผิดทันที) np.add.at ทำ “บวกทับ index ที่ซ้ำ” ให้เราถูกต้องโดยไม่ต้องวนเอง
หา learning rate ด้วย LR-finder แล้วค่อย decay
หัวข้อที่มีชื่อว่า “หา learning rate ด้วย LR-finder แล้วค่อย decay”lr เท่าไรดี? แทนที่จะเดา ให้ กวาดหา: รันสัก 1,000 ก้าว โดยไล่ lr แบบ exponential จากเล็ก (10⁻³) ไปใหญ่ (10⁰) แล้วดูว่า graph loss เทลงต่ำสุดที่ช่วงไหน
lre = np.linspace(-3, 0, 1000) # exponent จาก −3 ถึง 0lrs = 10 ** lre # lr จาก 0.001 ถึง 1.0# ...รัน loop เทรน 1,000 ก้าว โดยก้าวที่ i ใช้ lr = lrs[i] แล้วเก็บ (lre[i], loss) ไว้พล็อต...“หุบเขา” ของ graph (จุดที่ loss ต่ำสุด ก่อนจะเด้งกลับขึ้นเพราะ lr ใหญ่เกิน) มักอยู่แถว lr ≈ 0.1 (exponent ≈ −1) — เราจึงเลือก 0.1 เป็น lr หลัก แล้วพอเทรนไปได้สักครึ่งทาง ค่อย decay ลงเหลือ 0.01 เพื่อ “ขยับละเอียด” ในช่วงท้าย (ใน loop ข้างบนคือบรรทัด lr = 0.1 if step < 100000 else 0.01) — ตำแหน่งหุบเขาจะขยับได้เล็กน้อยตามการรัน อย่ายึดเลขเป๊ะ
วัดผลให้ถูก: train / dev / test และสัญญาณ overfitting
หัวข้อที่มีชื่อว่า “วัดผลให้ถูก: train / dev / test และสัญญาณ overfitting”ที่แบ่ง 80/10/10 ไว้ตอนต้นก็เพื่อตรงนี้: train เอาไว้เทรน, dev เอาไว้จูน hyperparameter (เช่น lr / ขนาดเน็ต / n_embd), test แตะให้น้อยที่สุด เก็บไว้ตัดสินครั้งสุดท้าย การวัด loss ของ split ต้องทำ บนทั้ง split ไม่ใช่บน minibatch — เพราะ minibatch ให้เลขที่แกว่ง เอาไปเทียบ model ไม่ได้:
def split_loss(X, Y): emb = C[X] # ทั้ง split ไม่ใช่ minibatch! h = np.tanh(emb.reshape(-1, block_size * n_embd) @ W1 + b1) logits = h @ W2 + b2 return cross_entropy(logits, Y)
print("train", split_loss(Xtr, Ytr))print("dev ", split_loss(Xdev, Ydev))หลังเทรน dev loss จะลงมาราว ~2.1–2.2 — ต่ำกว่าเพดานของ bigram (≈2.45) อย่างชัดเจน context 3 ตัว + embedding ที่เรียนรู้ได้ ช่วยจริง (เลขจะแกว่งเล็กน้อยตาม seed / รอบการรัน อย่ายึดเป๊ะ)
อ่านสัญญาณยังไง: ถ้า train loss ≪ dev loss = overfitting — เน็ตจำชุดเทรนได้แต่ generalize ไม่ได้ (แก้ด้วยข้อมูลมากขึ้น / เน็ตเล็กลง / regularization) แต่ในเน็ตจิ๋วตัวนี้ train กับ dev มัก ใกล้กัน ซึ่งแปลว่าเรากำลัง underfit ต่างหาก — คอขวดคือเน็ตยังเล็กไป ไม่ใช่ overfit นี่คือเหตุผลที่บทต่อๆ ไปเราขยายเน็ตให้ใหญ่และลึกขึ้น
“เราสร้างเพื่อเข้าใจ ไม่ใช่เพื่อ deploy — micrograd กับ nano-GPT ตัวจิ๋วในคอร์สนี้รันบน CPU เครื่องเดียวได้; แต่ LLM จริงกินทั้ง GPU cluster, ข้อมูลระดับเทระ, เวลาเป็นเดือน — สเกลที่คอร์สนี้ ‘ไม่’ จำลอง”
แปลว่า: embedding ของเราคือตาราง (27, 10) — จิ๋วมาก model จริง vocab เป็นแสน มิติ embedding เป็นพันๆ; ชั้นซ่อน 200 ตัวของเราเทียบกับ transformer จริงที่ลึกเป็นสิบๆ ชั้น สิ่งที่เหมือนกันเป๊ะคือ กลไก — lookup → matmul → nonlinearity → logits → cross-entropy → backprop → step; สิ่งที่ต่างคือ สเกล ล้วนๆ
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้เราขยับจาก bigram (context ตัวเดียว) เป็น MLP ของ Bengio 2003 ที่เห็น context 3 ตัว: embedding C คือตาราง weight ที่เทรนได้ (เป็น “วัตถุ” ไม่ใช่ retrieval tool แบบ #16); forward คือ C[X] → reshape (ต้อง match fan_in=30 ของ W1) → tanh → logits; เทรนด้วย minibatch SGD + LR-finder + decay; ใช้ fused cross-entropy บน raw logits; วัดผลบน ทั้ง split ได้ dev ≈2.1–2.2 ชนะเพดาน bigram
แต่ model นี้ยังมีปัญหาซ่อนอยู่ที่ init (loss ก้าวแรกสูงเกินเพราะยังไม่ได้สเกล weight) กับพฤติกรรมของ Activation FunctionActivation Functionfunction บีบไม่เชิงเส้น (tanh/ReLU) ที่ทำให้เน็ตเรียนของซับซ้อนได้Architecture กลางเน็ต — บท5 จะเปิดฝากระโปรงชั้นในเลย: ทำไม init สำคัญ, tanh อิ่มตัว (saturate) แล้วเน็ต “ตาย” ยังไง, และ Batch Normalization เข้ามาช่วยตรงไหน — พร้อมชุด “graph พัง → ตั้งชื่ออาการ → แก้บรรทัดเดียว → graph หาย” ×4
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Bengio, Ducharme, Vincent, Jauvin — A Neural Probabilistic Language Model, JMLR vol. 3, pp. 1137–1155 (2003) (S9) — เปเปอร์ต้นทางของไอเดีย embedding + MLP language model ที่บทนี้สร้างตาม
- Karpathy — nn-zero-to-hero,
makemore_part2_mlp.ipynb(S1, commit73c3fcc741f0ec104ca850b1fb0df90e7e8d4cde, 2024-02-20; เข้าถึง 2026-07-23) — โน้ตบุ๊กต้นแบบ (เขาใช้ PyTorch; เรา จงใจ reimplement เป็น NumPy ล้วนตามเขตกักกัน framework บท2–5) - Karpathy — makemore (
names.txt, 32,033 ชื่อ) (S3, commit988aa59e4d8fefa526d06f3b453ad116258398d4, 2022-11-20; เข้าถึง 2026-07-23) — ชุดข้อมูลตัวอักษรที่ใช้เทรน
เช็กความเข้าใจ — บทที่ 4
ข้อ 1 / 3ในบทนี้ 'embedding' คืออะไร และต่างจาก embedding ในคอร์ส #16 (context-engineering) อย่างไร?