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

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 ล้วน

📦 kaen-nn-from-scratch

code ลงมือ​ของ​บท​นี้​อยู่​ใน repo kaen-nn-from-scratch (code ตัวอย่าง​กำลัง​จัด​ทำ) file 04_mlp.pypure Python + NumPy เท่านั้น ไม่มี import torch ตาม​กติกา framework-quarantine ของ​บท2–5 รัน​บน CPU เครื่อง​เดียว​ได้​ใน​ไม่​กี่​นาที

code บท​นี้ 'จงใจ' ต่าง​จาก Karpathy

โน้ตบุ๊ก 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 บท​นี้

หัวใจ​แรก​คือ​เปลี่ยน “มอง​ย้อน 1 ตัว” เป็น “มอง​ย้อน block_size ตัว” เรา​ตั้ง block_size = 3 แล้ว​เดิน window เลื่อน​ไป​ที​ละ​ตัว​อักษร​ตลอด​ทั้ง​ชื่อ โดย​เติม . (index 0) เป็น padding ตอน​ต้น​ชื่อ ยก​ตัวอย่าง​ชื่อ emma (block_size=3):

context (3 ตัว)target ตัว​ถัด​ไป
. . .e
. . em
. e mm
e m ma
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 จากบท3
itos = {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 ใน model นี้ จริงๆ มัน​คือ​แค่ ตาราง 2 มิติ ตัว​หนึ่ง เรา​ตั้ง​ชื่อ​มัน​ว่า C ขนาด (27, 10): 27 แถว (หนึ่ง​แถว​ต่อ​หนึ่ง​ตัว​อักษร​ใน vocab) แถว​ละ 10 ตัวเลข การ “lookup” embedding ของ​ตัว​อักษร index i ก็​คือ​หยิบ​แถว​ที่ i ออก​มา — C[i] เท่านั้น​เอง ไม่มี​การ​ค้นหา ไม่มี​การ​วัด​ระยะ เป็น integer lookup ตรงๆ

ตอน​เริ่ม C เป็น​ตัวเลข​สุ่ม​ล้วน ไม่มี​ความหมาย​อะไร แต่​มัน​คือ weight ตัว​หนึ่ง​เหมือน W1/W2Gradient DescentGradient Descentloop ฝึก: ขยับ weight สวน​ทาง gradient ที​ละ​ก้าว​ให้ loss ลดProcess จะ​ค่อยๆ ขยับ​มัน​ทุก​ก้าว จน​แถว​ของ​ตัว​อักษร​ที่ “ใช้​แทน​กัน​ได้” (เช่น​กลุ่ม​สระ) เลื่อน​เข้า​มา​ใกล้​กันเอง นี่​คือ​จุด​ที่​ต้อง​แยก​ให้​คม​กับ​คอร์ส #16:

เส้น C — embedding ที่​นี่​คือ 'วัตถุ' ไม่ใช่ 'เครื่องมือ retrieval'

“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 — และ​ที่​นี่​เรา​สร้าง​มัน​เอง​ด้วย​มือ​ได้​เต็มๆ

ประกอบ​ร่าง 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) ตาราง embedding
W1 = np.random.randn(block_size * n_embd, n_hidden) # (30, 200) fan_in ต้อง = 3×10 = 30
b1 = 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 lookup
embcat = emb.reshape(-1, block_size * n_embd) # (N, 30) ต่อ 3 เวกเตอร์เป็นเส้นเดียว
h = np.tanh(embcat @ W1 + b1) # (N, 200) ชั้นซ่อน tanh
logits = 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 ตัว

init loss ควร​อยู่​ที่ไหน — และ​ทำไม​เรา​ยัง 'ไม่' แก้​ตรง​นี้

ถ้า 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 เต็มๆ

ชุด​เทรน​มี ~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 ที่​ซ้ำ” ให้​เรา​ถูกต้อง​โดย​ไม่​ต้อง​วน​เอง

lr เท่าไร​ดี? แทนที่​จะ​เดา ให้ กวาด​หา: รัน​สัก 1,000 ก้าว โดย​ไล่ lr แบบ exponential จาก​เล็ก (10⁻³) ไป​ใหญ่ (10⁰) แล้ว​ดู​ว่า graph loss เท​ลง​ต่ำ​สุด​ที่​ช่วง​ไหน

lre = np.linspace(-3, 0, 1000) # exponent จาก −3 ถึง 0
lrs = 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) — ตำแหน่ง​หุบเขา​จะ​ขยับ​ได้​เล็กน้อย​ตาม​การ​รัน อย่า​ยึด​เลข​เป๊ะ

ที่​แบ่ง 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 นี่​คือ​เหตุผล​ที่​บท​ต่อๆ ไป​เรา​ขยาย​เน็ต​ให้​ใหญ่​และ​ลึก​ขึ้น

เส้น B — นี่​คือ​ของ​จิ๋ว​เพื่อ​เข้าใจ ไม่ใช่​ของ deploy

“เรา​สร้าง​เพื่อ​เข้าใจ ไม่ใช่​เพื่อ 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


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

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

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

ข้อ 1 / 3

ในบทนี้ 'embedding' คืออะไร และต่างจาก embedding ในคอร์ส #16 (context-engineering) อย่างไร?