จาก scratch สู่ PyTorch — framework ทำสิ่งที่คุณเขียนมือ
สี่บทที่ผ่านมา (บท2–5) เราเขียน ทุกอย่าง ด้วยมือ — class Value ที่จำได้ว่าค่าไหนมาจากค่าไหนแล้วไล่ BackpropagationBackpropagationเดินย้อนจาก loss หาว่า weight แต่ละตัวมีส่วนผิดแค่ไหน (คือ gradient)Process ย้อนกลับเอง, softmax ที่ลบ max ต่อแถวกันล้น, cross-entropy บน raw logits, และ loop for p, g: p -= lr * g ที่ขยับ weight ทีละก้าว เราทำแบบนั้นด้วยเหตุผลเดียว: ให้เห็น ทุกการคูณ ทุกการบวก gradient ชัดๆ ไม่มีอะไรซ่อนหลังกล่องดำ
บทนี้เป็น import torch ครั้งแรกของทั้งคอร์ส — และนั่นไม่ใช่เรื่องบังเอิญ บท2–5 เรากัน framework ออกไปโดยจงใจ (framework-quarantine) เพื่อไม่ให้ “ของที่เน่าเร็ว” มาบังของที่ไม่เน่า พอถึงตรงนี้คุณเข้าใจกลไกครบแล้ว เราจึง graduate ขึ้นไปใช้ PyTorchPyTorchframework ที่ทำ autograd+tensor ให้เรา — graduate จากเขียนมือมาเป็นเครื่องมือจริง (pin version+วันที่ดึง)Architecture — เครื่องมือจริงที่ทำสิ่งที่คุณเพิ่งเขียนมือให้อัตโนมัติ หัวใจของบทนี้ไม่ใช่ “PyTorch API มีอะไรบ้าง” แต่คือ ตารางเทียบ 1-ต่อ-1 ว่าแต่ละบรรทัดที่คุณเขียนมือในบท2–5 กลายเป็น primitive อะไรของ framework — พอเห็นตารางนี้ PyTorch จะเลิกเป็นเวทมนตร์ทันที มันคือของที่คุณทำเป็นอยู่แล้ว แค่มีคนห่อให้
code ลงมือของบทนี้อยู่ใน repo kaen-nn-from-scratch (code ตัวอย่างกำลังจัดทำ) file 06_pytorch.py — บทนี้คือ file แรกที่มี import torch ก่อนหน้านี้ (บท2–5) grep หา torch จะได้0 hit ตามกติกา framework-quarantine รันบน CPU เครื่องเดียวได้ ไม่ต้องมี GPU
ติดตั้ง PyTorch — และทำไมต้อง pin version
หัวข้อที่มีชื่อว่า “ติดตั้ง PyTorch — และทำไมต้อง pin version”ก่อนอื่นติดตั้งลง virtualenv สะอาดๆ เราใช้ CPU-only (ไม่ต้องมี GPU) และเอา torch ตัวเดียว ไม่เอา torchvision/torchaudio เพราะคอร์สนี้ไม่แตะภาพ/เสียง:
python3.12 -m venv .venv && source .venv/bin/activatepip install torch==2.13.0 --index-url https://download.pytorch.org/whl/cputorch==2.13.0 คือรุ่น stable ปัจจุบัน ณ วันที่ดึง 2026-07-23 (ขึ้น PyPI เมื่อ 2026-07-08) และ ใช้กับ Python 3.12 ได้ (torch 2.13.0 รองรับ 3.10–3.14) นี่ไม่ใช่เลขสมมติ — มันเป็นรุ่นจริงที่รันได้วันนี้ แต่ PyTorch ออกรุ่นใหม่ราวๆ ทุกไตรมาส เพราะฉะนั้นถ้าคุณอ่านบทนี้หลังจากนั้นนาน ให้ไปเช็ก pypi.org/project/torch ก่อน แล้วปรับ pin + วันที่ดึงให้ตรงจริง (Colab CPU ก็ใช้ได้ ปัก!pip install torch==2.13.0 ใน cell แรกถ้ามันเลื่อนไปรุ่นอื่น)
“ของสดใช้ repo, ของนิ่งใช้ตำรา — code ของ Karpathy คือ repo ที่ยังมีชีวิต เราจึงอ้างด้วย commit SHA + วันที่ดึง ไม่ใช่เลขรุ่น; ส่วนคณิตที่ไม่เน่าเราอ้าง Goodfellow-Bengio-Courville (MIT Press, 2016)”
แปลว่า (ในบทนี้): บทนี้คือ บทที่เน่าเร็วที่สุดของทั้งคอร์ส — API ของ PyTorch เปลี่ยน, เลขรุ่นเลื่อน, ชื่อ argument ถูก deprecate นี่คือเหตุผลที่เรากัน framework ไว้ในบท6–8 แล้ว pin ทุกอย่างด้วย version + วันที่ดึง แต่หัวใจของบทนี้ — ตารางเทียบ ว่า framework ทำอะไรแทนมือคุณ — คือแนวคิดที่ไม่เน่า เลขรุ่น 2.13.0 จะหมดอายุ แต่ความจริงที่ว่า loss.backward() คือ topo-backward ของบท2 นั้นไม่หมดอายุ จำแนวคิด อย่าจำเลขรุ่น
หัวใจของบท: ตารางเทียบ “สิ่งที่คุณเขียนมือ → สิ่งที่ framework ทำให้”
หัวข้อที่มีชื่อว่า “หัวใจของบท: ตารางเทียบ “สิ่งที่คุณเขียนมือ → สิ่งที่ framework ทำให้””นี่คือส่วนที่สำคัญที่สุด อ่านตารางนี้ช้าๆ ทุกแถวคือบรรทัดที่คุณ เขียนมือไปแล้ว ในบท2–5 column ขวาแค่ห่อมันด้วยชื่อของ PyTorch:
| สิ่งที่คุณเขียนมือ (บท2–5) | PyTorch ทำให้ (บทนี้) | มันทำอะไร |
|---|---|---|
Value.data / Value.grad (บท2) | Tensor(requires_grad=True) แล้วอ่าน .grad | tensor เก็บทั้ง ค่า และ gradient ในก้อนเดียว เหมือน Value แต่เป็น array หลายมิติทีเดียว |
topo backward() เขียนเอง (บท2) | autograd tape + loss.backward() | PyTorch จด computational graph ให้อัตโนมัติตอน forward แล้วไล่ chain rule ย้อนให้ — คือ AutogradAutogradgraph ที่จำว่าค่าไหนมาจากค่าไหน (computational graph) แล้วไล่ chain rule ให้อัตโนมัติ — คือสิ่งที่ micrograd เป็นProcess ตัวเดียวกับ micrograd แต่คุณไม่ต้องเขียน _backward เอง |
parameters = [C, W1, b1, W2, b2] (บท4) | nn.Parameter / model.parameters() | ประกาศ weight เป็น field ของ nn.Module แล้ว .parameters() รวบให้เองทั้งหมด ไม่ต้องถือ list เอง |
one-hot @ W (บท3) | nn.Embedding(V, d) | lookup แถวของตาราง embedding ตาม index ตรงๆ — เร็วและตรงกว่าคูณ one-hot (ผลลัพธ์เท่ากันเป๊ะ) |
x @ W + b (บท4) | nn.Linear(in, out) | ชั้น linear หนึ่งชั้น เก็บ W/b ให้ในตัว init สเกลให้เหมาะให้ด้วย |
| softmax + NLL เขียนมือ (บท3–4) | F.cross_entropy(logits, y) | fuse log-softmax + NLL ในก้าวเดียว รับ raw logits + integer target — ทำ max-subtraction กัน overflow ให้ในตัว |
for p, g: p -= lr * g (บท4) | optim.SGD / optim.AdamW + opt.step() | optimizer อัปเดต weight ทุกตัวให้ในบรรทัดเดียว (AdamW ปรับ lr ต่อ parameter ให้อีก) |
reset grad เอง W.grad = 0 (บท2) | optimizer.zero_grad() | ล้าง gradient เก่าก่อนก้าวใหม่ — จำเป็นเพราะ PyTorch สะสม grad เหมือนกฎ += ในบท2 เป๊ะ |
flowchart LR
subgraph HAND["เขียนมือ (บท2–5)"]
H1["Value.data / .grad"]
H2["topo backward() เขียนเอง"]
H3["parameters = [C, W1, b1, W2, b2]"]
H4["one-hot @ W · x@W+b"]
H5["softmax + NLL เขียนมือ"]
H6["for p: p -= lr*p.grad · W.grad = 0"]
end
subgraph TORCH["PyTorch (บทนี้)"]
T1["Tensor(requires_grad=True)"]
T2["autograd tape + loss.backward()"]
T3["nn.Parameter / model.parameters()"]
T4["nn.Embedding · nn.Linear"]
T5["F.cross_entropy (raw logits)"]
T6["optim.AdamW: step() + zero_grad()"]
end
H1 --> T1
H2 --> T2
H3 --> T3
H4 --> T4
H5 --> T5
H6 --> T6
classDef tbl fill:#fde68a,stroke:#92400e,color:#451a03;
classDef acc fill:#7c2d12,stroke:#431407,color:#fed7aa;
class H1,H2,H3,H4,H5,H6 tbl;
class T1,T2,T3,T4,T5,T6 acc;
คำบรรยายภาพ: ตารางเทียบแบบ 1-ต่อ-1 ระหว่างสิ่งที่เราเขียนมือในบท2–5 (column ซ้าย) กับ primitive ของ PyTorch ที่ทำสิ่งเดียวกันให้ (column ขวา) — Value.data/.grad กลายเป็น Tensor(requires_grad=True), topo-backward ที่เขียนเองกลายเป็น loss.backward(), list ของ weight กลายเป็น model.parameters(), one-hot คูณ W กับ x@W+b กลายเป็น nn.Embedding/nn.Linear, softmax+NLL เขียนมือกลายเป็น F.cross_entropy บน raw logits, และ loop อัปเดต + การล้าง grad กลายเป็น optim.AdamW กับ zero_grad()/step() ทุกลูกศรชี้จาก “ของที่คุณทำเป็นแล้ว” ไปยัง “ชื่อที่ framework เรียกมัน” — ไม่มีแนวคิดใหม่ มีแต่ชื่อใหม่
“backprop ตัวเดียวกันนี้แหละที่หมุนอยู่ข้างใน model ที่ agent ของคุณใน #15-#18 เรียกผ่าน API — เข้าใจมันแล้วจะเห็นทั้งกองตั้งแต่ neuron ยัน tool-call; แต่พูดตรงๆ: ไม่รู้ backprop ก็สร้าง agent ได้”
แปลว่า (ในบทนี้): column ขวาของตารางข้างบน — nn.Embedding, nn.Linear, F.cross_entropy, loss.backward(), AdamW.step() — คือ primitive ชุดเดียวกันเป๊ะ ที่ model จริงหลัง API ของ #15–#18 ใช้เทรนตัวเอง model ของ Anthropic/OpenAI ไม่ได้ใช้เวทมนตร์ลึกลับ มันคือ tensor + autograd + optimizer ที่สเกลขึ้นมหาศาลบน primitive ชุดนี้ พอคุณอ่านตารางออก คุณกำลังมองเห็น “ทั้งกอง” ที่หมุนอยู่หลัง client.messages.create(...) — แต่ย้ำเหมือนเดิม: นี่เป็น สะพานความเข้าใจ ไม่ใช่ prerequisite ไม่รู้ตารางนี้ก็สร้าง agent ได้
เขียน MLP ของบท4 ใหม่เป็น nn.Module
หัวข้อที่มีชื่อว่า “เขียน MLP ของบท4 ใหม่เป็น nn.Module”ทีนี้เอาตารางมาใช้จริง — เราจะเขียน MLP ของบท4 ใหม่ทั้งตัว แต่คราวนี้ให้ PyTorch ทำ backward ให้ จำ model บท4 ได้ไหม: ตาราง embedding C → ต่อเวกเตอร์ → tanh(·@W1+b1) → @W2+b2 → logits แล้วเราคำนวณ dC, dW1, ..., dW2 ด้วยมือกันยาวเหยียด ตอนนี้ทุกอย่างนั้นยุบเหลือเท่านี้:
import torchimport torch.nn as nnimport torch.nn.functional as F
V, n_embd, n_hidden = 27, 10, 200 # vocab / มิติ embedding / ชั้นซ่อน — เลขเดียวกับบท4
class BigramMLP(nn.Module): def __init__(self, V, n_embd=10, n_hidden=200): super().__init__() self.emb = nn.Embedding(V, n_embd) # แทนตาราง C (27,10) ของบท4 self.fc1 = nn.Linear(n_embd, n_hidden) # แทน W1,b1 (คูณ + บวก bias ในตัว) self.fc2 = nn.Linear(n_hidden, V) # แทน W2,b2
def forward(self, idx): x = self.emb(idx) # lookup embedding h = torch.tanh(self.fc1(x)) # ชั้นซ่อน tanh — เหมือนบท4 return self.fc2(h) # <<< คืน RAW LOGITS (ยัง 'ไม่' softmax)
model = BigramMLP(V).to("cpu")opt = torch.optim.AdamW(model.parameters(), lr=0.01) # optimizer รวบ weight ให้เองBigramMLP ตัวนี้รับ index ตัวเดียว (idx รูป (N,)) เพื่อโชว์ primitive ให้สะอาดที่สุด — เป็น bigram (มองย้อน 1 ตัว) ถ้าอยากได้ context 3 ตัวเต็มแบบบท4 (block_size=3) หลักการเดิมเป๊ะ เพิ่มแค่ขั้น flatten: x = self.emb(idx) จะได้ (N, 3, 10) แล้ว x = x.view(x.shape[0], -1) ให้เป็น (N, 30) ก่อนเข้า fc1 (และตั้ง nn.Linear(block_size*n_embd, n_hidden)) — นี่คือ .reshape(-1, 30) ที่ต้อง match fan_in=30 ของบท4 นั่นเอง โครงเดิม เพิ่มบรรทัดเดียว
สังเกตว่า forward คืน raw logits ไม่มี softmax เลย — นี่ตั้งใจ เพราะ F.cross_entropy อยากได้ logits ดิบๆ (เดี๋ยวอธิบายว่าทำไมในกับดักถัดไป) ตอนนี้มาดู loop เทรน เทียบกับ loop บท4 ที่ยาวเป็นหน้า:
for step in range(10000): logits = model(Xb) # forward — autograd จด graph ให้เอง loss = F.cross_entropy(logits, Yb) # Yb: LongTensor ของ 'class index' (ไม่ใช่ one-hot)
opt.zero_grad() # 1) ล้าง grad เก่า (ไม่งั้นสะสม — bug เดียวกับบท2) loss.backward() # 2) autograd ไล่ chain rule ย้อนให้ทั้ง graph opt.step() # 3) optimizer ขยับ weight ทุกตัวหนึ่งก้าว
if step % 1000 == 0: print(step, float(loss)) # float(loss) ดึงตัวเลขออกโดยไม่แตะ tapeทั้ง dW2 = h.T @ dlogits, dh = dlogits @ W2.T, np.add.at(dC, Xb, demb) และเพื่อนๆ ของบท4 — หายไปหมด เหลือแค่ loss.backward() บรรทัดเดียว นี่คือสิ่งที่ framework ซื้อให้คุณ: คุณเข้าใจว่ามันทำอะไร (เพราะเขียนมือมาแล้ว) แต่ไม่ต้องเขียนซ้ำอีก ลำดับสามบรรทัด zero_grad → backward → step เป็นท่าประจำ — จำให้ขึ้นใจ
กับดัก API-churn ที่ต้องระวัง
หัวข้อที่มีชื่อว่า “กับดัก API-churn ที่ต้องระวัง”PyTorch อยู่มานาน API เปลี่ยนหลายรอบ code เก่าใน Stack Overflow เต็มไปด้วยท่าที่ตายแล้ว นี่คือกับดักที่เจอบ่อยที่สุด:
❌ Variable ตายแล้ว — code เก่าห่อ tensor ด้วย torch.autograd.Variable(x) เพื่อให้มัน track gradient เดี๋ยวนี้ Variable ถูกรวมเข้ากับ Tensor ไปนานแล้ว
✅ ใช้ torch.tensor(x, requires_grad=True) หรือ x.requires_grad_(True) ตรงๆ เห็น Variable ที่ไหน = สัญญาณว่า code นั้นเก่ามาก
❌ ยุ่งกับ .data ดิบๆ → grad เพี้ยนเงียบๆ — การเขียน x.data เพื่อ “แอบ” แก้ค่าโดยเลี่ยง autograd เป็นบ่อเกิดของ bug ที่ไม่ error แต่ grad ผิด
✅ อยากตัดออกจาก tape ใช้ x.detach(); อยากรันช่วงที่ไม่ต้อง track grad (เช่นตอน eval) ครอบด้วย with torch.no_grad():; อยาก log ตัวเลข loss ใช้ float(loss) หรือ loss.item() — อย่าไปแตะ .data
❌ ลืม zero_grad() → grad บวกสะสมข้ามก้าว — PyTorch สะสม gradient โดย design (+= เหมือนกฎ fan-out ของบท2 เป๊ะ) ถ้าลืมล้าง grad ของก้าวที่แล้วจะบวกทับก้าวนี้ ทิศเพี้ยน model ไม่ลู่เข้า
✅ เรียก opt.zero_grad() ก่อน backward() ทุกก้าว — นี่คือ bug เดียวกับที่บท2 ย้ำเรื่อง reset grad
❌ softmax เองก่อนแล้วส่งเข้า F.cross_entropy (double-softmax) — F.cross_entropy ทำ log-softmax ในตัวอยู่แล้ว ถ้าคุณ F.softmax(logits) ก่อนแล้วส่งเข้าไป = softmax สองรอบ ผิด (loss จะดูโอเคแต่ model เรียนไม่เต็มที่)
✅ ส่ง raw logits เข้า F.cross_entropy ตรงๆ (นี่คือเหตุผลที่ forward ของเราคืน logits ดิบ) และ target เป็น integer class index ไม่ใช่ one-hot ไม่ใช่ log-prob
❌ สลับลำดับสามบรรทัด — เช่น step() ก่อน backward() (อัปเดตด้วย grad เก่า/ว่าง) หรือ zero_grad() หลัง backward() (ล้าง grad ที่เพิ่งคำนวณทิ้ง)
✅ ลำดับต้องเป็น zero_grad() → backward() → step() เสมอ — ล้าง, คำนวณ, ขยับ ตามนั้น
“เราสร้างเพื่อเข้าใจ ไม่ใช่เพื่อ deploy — micrograd กับ nano-GPT ตัวจิ๋วในคอร์สนี้รันบน CPU เครื่องเดียวได้; แต่ LLM จริงกินทั้ง GPU cluster, ข้อมูลระดับเทระ, เวลาเป็นเดือน — สเกลที่คอร์สนี้ ‘ไม่’ จำลอง”
แปลว่า (ในบทนี้): การ graduate มา PyTorch ไม่ได้ แปลว่าเรากำลังทำของ production ขึ้นมา BigramMLP ยังรันบน CPU เครื่องเดียว เทรนได้ในไม่กี่นาที เหมือนเดิม สิ่งที่ framework ให้คือ ความสะดวก (backward อัตโนมัติ, optimizer สำเร็จรูป) และ ประตูสู่ GPU — แต่ training stack จริง (data pipeline, distributed, mixed-precision, checkpointing) ยังอยู่นอกคอร์สนี้ตาม design PyTorch เป็นเครื่องมือ ไม่ใช่เส้นชัย
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้เรา graduate จาก “เขียนมือทุกอย่าง” มาเป็น PyTorch — import torch ครั้งแรกของคอร์ส: TensorTensorarray หลายมิติ หน่วยข้อมูลพื้นฐานของ PyTorchArchitecture เก็บทั้งค่าและ gradient เหมือน Value แต่เป็น array, loss.backward() คือ topo-backward ของบท2 ที่ autograd ทำให้อัตโนมัติ, nn.Embedding/nn.Linear คือ one-hot@W และ x@W+b, F.cross_entropy fuse softmax+NLL บน raw logits, และ optim.AdamW + zero_grad()→backward()→step() แทน loop อัปเดตที่เขียนมือ หัวใจคือ ตารางเทียบ — PyTorch ไม่ใช่แนวคิดใหม่ มันคือชื่อใหม่ของสิ่งที่คุณทำเป็นแล้ว และกับดัก API-churn (Variable ตาย, อย่าแตะ .data, อย่าลืม zero_grad, อย่า double-softmax, ลำดับสามบรรทัดห้ามสลับ) คือราคาของการอยู่บนเครื่องมือที่ยังมีชีวิต จึงต้อง pin version + วันที่ดึงเสมอ
บทหน้าเราจะใช้ primitive ชุดนี้สร้างของที่ใหญ่กว่า MLP มาก — self-attention และ transformer ตัวจิ๋ว กลไกที่ทำให้แต่ละ token “มองย้อน” ไป token อื่นแล้วชั่งน้ำหนักว่าอันไหนสำคัญ นี่คือโครงที่ LLM จริงหลัง API ใช้ และเป็นบันไดขั้นสุดท้ายก่อน capstone
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- PyTorch — project บน PyPI (
torch==2.13.0) (S16, รุ่น 2.13.0 ขึ้น PyPI 2026-07-08; เข้าถึง 2026-07-23 ⚠️VERIFY-AT-BUILD) — เลขรุ่นที่ pin ไว้ในบทนี้; PyTorch ออกรุ่นใหม่ราวทุกไตรมาส ตรวจซ้ำก่อน build ทุกครั้ง แล้วปรับ pin + วันที่ดึงให้ตรงจริง (เส้น A) - PyTorch — Get Started / Locally (คำสั่งติดตั้ง CPU/CUDA) (S16, เข้าถึง 2026-07-23 ⚠️VERIFY-AT-BUILD) — ที่มาของคำสั่ง
pip install torch --index-url .../whl/cpu; หน้านี้สร้างคำสั่งติดตั้งตาม OS/platform ให้ - Goodfellow, Bengio, Courville — Deep Learning (MIT Press, 2016) (S8) — ตำราคณิตที่ “นิ่ง”: หลักการ autograd/backprop ที่
loss.backward()ทำให้อัตโนมัติอ้างได้ที่ §6.5 (timeless-math-only; ติดป้าย “stale-on-modern-practice” ตรงที่แตะ optimizer/normalization สมัยใหม่) (เส้น A/D)
เช็กความเข้าใจ — บทที่ 6
ข้อ 1 / 3`F.cross_entropy` ต้องการอะไรเป็น input และทำไม?