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

micrograd — สร้าง autograd engine แล้ว​ไล่ backprop ย้อน​กลับ​ด้วย​มือ

บท​ที่​แล้ว​วางแผนที่​ไว้​แล้ว — บท​นี้​เริ่ม​ลงมือ เรา​จะ​สร้าง AutogradAutogradgraph ที่​จำ​ว่า​ค่า​ไหน​มา​จาก​ค่า​ไหน (computational graph) แล้ว​ไล่ chain rule ให้​อัตโนมัติ — คือ​สิ่ง​ที่ micrograd เป็นProcess engine ตัว​แรก​ด้วย​มือ นั่น​คือ micrograd ของ Karpathy: code ~150 บรรทัด​ที่​ทำ​สิ่ง​เดียว​กับ​หัวใจ​ของ PyTorch — จำ​ว่า​ค่า​ไหน​มา​จาก​ค่า​ไหน แล้ว​ไล่ BackpropagationBackpropagationเดิน​ย้อน​จาก loss หา​ว่า weight แต่ละ​ตัว​มี​ส่วน​ผิด​แค่​ไหน (คือ gradient)Process ย้อน​กลับ​หา gradient ให้​อัตโนมัติ ทั้งหมด​เป็น pure Python ไม่มี framework (ไม่มี import torch แม้แต่​บรรทัด​เดียว — โควตา​นี้​จะ​คง​ไป​จนถึง​บท5)

ที่​พิเศษ​ของ micrograd คือ​มัน​เล็ก​พอ​จะ​อ่าน​จบ​ทั้ง file ใน​หนึ่ง​นั่ง แต่​ครบ​พอ​จะ​เทรน​เน็ต​จริง​ได้ พอ​เข้าใจ 150 บรรทัด​นี้ คุณ​จะ​เข้าใจ​ว่า loss.backward() ในบท6–8 (และ​ใน model ที่ agent ของ​คุณ​เรียก​ผ่าน API) จริงๆ แล้ว​ทำ​อะไร ข้าง​ใน

📦 kaen-nn-from-scratch

code ลงมือ​ของ​บท​นี้​อยู่​ใน repo kaen-nn-from-scratch (code ตัวอย่าง​กำลัง​จัด​ทำ) ที่ folder 02-micrograd/ — pure Python ล้วน รัน​ด้วย python 02-micrograd/engine_demo.py ได้​เลย ไม่​ต้อง​ลง dependency อะไร class Value ที่​ลง​ใน​บท​นี้​คือ​ของ​จริง​จาก micrograd (engine.py) แบบ​ไล่​บรรทัด​ตรง — คุณ copy ไป​วาง​แล้ว​รัน​ตาม​ได้​ทันที

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

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

แปล​ว่า: micrograd คือ counter-example ที่ “นิ่ง” ของ​เส้น​นี้ — commit ล่าสุด​ของ​มัน​คือ c911406e ลง​วัน​ที่ 2020-04-18 และ​ไม่​ขยับ​มา​ราว 6 ปี เรา​จึง pin มัน​ได้​สนิท (code ใน​บท​นี้ = file นั้น​เป๊ะ) ต่าง​จาก PyTorch ในบท6 ที่​ออกรุ่น​ใหม่​ทุก​ไตรมาส — ตัว code สด เรา​ถึง​อ้าง​ด้วย SHA + วัน​ที่​ดึง ไม่ใช่​เลข​รุ่น

หัวใจ​ของ micrograd คือ class เดียว: Value มัน​ห่อ scalar (เลข​ตัว​เดียว) หนึ่ง​ค่า แต่​แทนที่​จะ​เป็น​แค่​ตัวเลข​เปล่าๆ มัน​จำ​เพิ่ม​อีก​สาม​อย่าง — (1) grad ช่อง​เก็บ GradientGradientทิศ+ขนาด​ที่ loss จะ​เพิ่ม​ถ้า​ขยับ weight นิด​หนึ่ง; เรา​ขยับ​สวน​ทาง​มันProcess ของ​ค่า​นี้​เทียบ​กับ loss (2) _prev เซต​ของ node ต้นทาง​ที่​สร้าง​มัน​ขึ้น​มา และ (3) _backward closure ที่​รู้​วิธี​ไหล gradient จาก​ตัว​มัน​กลับ​เข้า​ต้นทาง สาม​อย่าง​นี้​แหละ​ที่​ทำให้​เรา​วาด computational graph ขึ้น​มา​โดย​อัตโนมัติ​ทุก​ครั้ง​ที่​คำนวณ

class Value:
""" เก็บค่า scalar หนึ่งตัว พร้อม gradient ของมัน """
def __init__(self, data, _children=(), _op=''):
self.data = data
self.grad = 0 # เริ่มที่ 0 เสมอ — เดี๋ยวจะเห็นว่าทำไม (ไม่ใช่บังเอิญ)
self._backward = lambda: None # closure: ไหล gradient จาก out กลับเข้า input
self._prev = set(_children) # node ต้นทาง (ใครสร้างค่านี้ขึ้นมา)
self._op = _op # ป้ายว่าเกิดจาก op อะไร (ไว้ debug/วาด graph)

_children=() เริ่ม​เป็น tuple ว่าง — ค่าที่​เรา​พิมพ์​เอง​ด้วย​มือ (เช่น Value(3.0)) คือ “ใบไม้” ของ graph ไม่มี​ต้นทาง ส่วน​ค่าที่​เกิด​จาก​การ​คำนวณ (เช่น a + b) จะ​เก็บ a กับ b ไว้​ใน _children เพื่อ​ให้ backward รู้​ว่า​ต้อง​ไหล gradient กลับ​ไป​ที่​ใคร

ทุก op ทำ​สอง​งาน​พร้อม​กัน: (1) Forward PassForward Passป้อน input เดิน​หน้า​ผ่าน​เน็ต​จน​ได้​ผลลัพธ์/lossProcess — คำนวณ​ค่า​ใหม่ (2) ผูก closure _backward ที่​ถือ local derivative ของ op นั้น​ไว้ หัวใจ​คือ Chain RuleChain Ruleกฎ​ลูกโซ่: คูณ​อนุพันธ์​ต่อ​กัน​ที​ละ node — หัวใจ​ที่​ทำให้ backprop เป็น​ไป​ได้Process: gradient ที่​ไหล​เข้า input = (local derivative ของ op) × (gradient ที่​ไหล​เข้า output) code ข้าง​ล่าง​นี้​คือ engine.py ตัว​จริง​แบบ​ไล่​บรรทัด — สังเกต​ว่า​ทุก _backward ใช้ += ไม่ใช่ = (เดี๋ยว​จะ​เจาะ​ว่า​ทำไม​มัน​คือ bug อันดับ​หนึ่ง):

def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), '+')
def _backward():
self.grad += out.grad # d(a+b)/da = 1
other.grad += out.grad # d(a+b)/db = 1
out._backward = _backward
return out
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data, (self, other), '*')
def _backward():
self.grad += other.data * out.grad # d(a*b)/da = b
other.grad += self.data * out.grad # d(a*b)/db = a
out._backward = _backward
return out
def __pow__(self, other):
assert isinstance(other, (int, float)), "only supporting int/float powers for now"
out = Value(self.data ** other, (self,), f'**{other}')
def _backward():
self.grad += (other * self.data ** (other - 1)) * out.grad # d(a^n)/da = n·a^(n-1)
out._backward = _backward
return out
def relu(self):
out = Value(0 if self.data < 0 else self.data, (self,), 'ReLU')
def _backward():
self.grad += (out.data > 0) * out.grad # 1 ถ้า a>0, ไม่งั้น 0
out._backward = _backward
return out

สังเกต __pow__: assert isinstance(other, (int, float)) — มัน​รองรับ เลขชี้กำลัง​ที่​เป็น​ค่า​คงที่​เท่านั้น (a ** 2 ได้, a ** b เมื่อ b เป็น Value จะ raise) เพราะ​สูตร n·a^(n-1) ใช้ได้​ต่อ​เมื่อ n คงที่ ส่วน __sub__ / __neg__ / __truediv__ ไม่มี _backward ของ​ตัวเอง — เพราะ​มัน​ถูก ประกอบ จาก + * ** (เช่น a / b คือ a * b**-1) chain rule จึง​ไหล​ผ่าน​มันได้ฟรีๆ

ตาราง​สรุป local derivative ที่​ต้อง​จำ (สังเกต​หลาย op คำนวณ grad จาก out ที่​มี​อยู่​แล้ว — ไม่​ต้อง​คำนวณ forward ซ้ำ):

opforwardlocal derivative
a + ba.data + b.data∂/∂a = 1, ∂/∂b = 1
a * ba.data * b.data∂/∂a = b, ∂/∂b = a
a ** n (n คงที่)a.data ** nn · an−1
a.relu()max(0, a.data)1 ถ้า a > 0, ไม่​งั้น 0
a.tanh()tanh(a.data)1 − out²
a.exp()e^(a.data)out

ตอน​นี้​เรา​มี graph ที่​แต่ละ node ถือ _backward ของ​ตัวเอง​ไว้​แล้ว เหลือ​แค่ “เรียก​มัน​ตาม​ลำดับ​ที่​ถูกต้อง” — และ​ลำดับ​นี้​แหละ​ที่​ห้าม​ผิด backward() ทำ​สาม​สเต็ป:

def backward(self):
# 1) เรียง node แบบ topological — ลูกก่อน พ่อทีหลัง (visited กันนับซ้ำ sub-expression)
topo, visited = [], set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build_topo(child)
topo.append(v) # append หลังไล่ลูกครบแล้วเท่านั้น
build_topo(self)
# 2) seed: dL/dL = 1 (ถ้าลืมบรรทัดนี้ grad ทุกตัวจะเป็น 0)
self.grad = 1
# 3) เดินย้อน — พ่อก่อนลูก — เรียก _backward ทีละ node
for v in reversed(topo):
v._backward()
# --- op ที่ประกอบจาก + * ** (ไม่มี _backward เป็นของตัวเอง) ---
def __neg__(self): return self * -1
def __sub__(self, other): return self + (-other)
def __radd__(self, other): return self + other
def __rmul__(self, other): return self * other
def __truediv__(self, other): return self * other ** -1
def __repr__(self): return f"Value(data={self.data}, grad={self.grad})"

ทำไม​ต้อง reverse-topological? เพราะ _backward ของ node หนึ่ง​จะ​ไหล gradient เข้า​ลูก​ของ​มัน​ได้ ก็​ต่อ​เมื่อ out.grad ของ​มัน​ครบ​สมบูรณ์​แล้ว — และ out.grad จะ​ครบ​ก็​ต่อ​เมื่อ ทุก node ปลายทาง​ที่​ใช้​มัน ไหล gradient กลับ​มา​หมด​แล้ว การ​เดิน reversed(topo) (พ่อ​ก่อน​ลูก) รับประกัน​เงื่อนไข​นี้​พอดี ถ้า​เดิน​สลับ​ลำดับ gradient จะ​ไหล​ออก​ทั้ง​ที่​ยัง​นับ​ไม่​ครบ → ผิด​เงียบๆ

2 invariant ที่​ห้าม​พลาด​ใน block นี้: (1) topo.append(v) อยู่ หลัง loop ไล่​ลูก (ไม่ใช่​ก่อน) (2) self.grad = 1 เป็นการ​จุด​ชนวน ถ้า​ลืม gradient ทุก​ตัว​จะ​เป็น 0 เพราะ​ทุก​อย่าง​คูณ​ด้วย out.grad ที่​ยัง​เป็น 0

นี่​คือ​จุด​ที่​ผู้​เริ่มต้น​พลาด​กัน​มาก​ที่สุด และ​เป็น​เหตุผล​ที่ grad เริ่มต้น​ที่ 0 เสมอ ลอง​ดู​ตัวอย่าง​สั้น​ที่สุด — d = a * a:

a = Value(3.0)
d = a * a # a "แตกออกเป็นสองขา" ของตัวคูณ (fan-out)
d.backward()
print(a.grad) # 6.0 == 2 * a.data (= 2 × 3.0)

ใน graph a ตัว​เดียว​ป้อน​เข้า node * ทั้ง self และ other — มัน​คือ input เดียวกัน​สอง​ขา multivariable chain rule บอกว่า gradient ที่มา​จาก​หลาย​เส้นทาง​ต้อง บวก​กัน ตอน _backward ของ * รัน มัน​ทำ self.grad += other.data * out.grad แล้ว other.grad += self.data * out.grad — แต่ self กับ other คือ a ตัว​เดียวกัน! a.grad จึง​ถูก​บวก​สอง​ครั้ง: 3×1 + 3×1 = 6 พอดี 2 * a.data

ทีนี้​ดู version bug — ถ้า​เผลอ​ใช้ =:

# ❌ version bug: ใช้ = แทน += ใน _backward ของ __mul__
def _backward():
self.grad = other.data * out.grad # ❌ ทับ ไม่ใช่บวกสะสม
other.grad = self.data * out.grad # ❌
# ผล: การเขียนขาที่สองไปทับขาแรก → a.grad เหลือ 3.0 (== a.data) ไม่ใช่ 6.0
# ครึ่งหนึ่งของ gradient หายไปเงียบ ๆ — เน็ตยังรันได้ แต่เรียนผิด
flowchart LR
  a["a = Value(3.0)"]
  mul["node '*'<br/>out = a × a = 9"]
  d["d (= out)<br/>seed: d.grad = 1"]
  a -->|"forward"| mul
  a -->|"forward · ขาที่ 2"| mul
  mul -->|"forward"| d
  d -.->|"backward เริ่มที่นี่"| mul
  mul -.->|"a.grad += 3 × 1"| a
  mul -.->|"a.grad += 3 × 1 · ขาที่ 2"| a

  classDef seed fill:#7c2d12,stroke:#431407,color:#fed7aa;
  classDef node fill:#fde68a,stroke:#92400e,color:#451a03;
  class d seed;
  class a,mul node;

คำ​บรรยาย​ภาพ: บทเรียน​หลัก​ของ micrograd ใน​ภาพ​เดียว — a ตัว​เดียว​ป้อน​เข้า node * ทั้ง​สอง​ขา (fan-out) ตอน forward ตอน backward gradient จึง​ไหล​กลับ​มา​ที่ a สอง​ครั้ง ต้อง += (บวก​สะสม) ให้​เป็น 2 × a.data = 6 ถ้า​เผลอ​ใช้ = ขา​ที่​สอง​จะ​ทับ​ขา​แรก​เหลือ​แค่ 3 นี่​คือ​เหตุผล​ที่ grad ต้อง​เริ่มต้น​ที่ 0 เสมอ — เพราะ​เรา​ตั้งใจ​ให้​มัน​เป็น “ตัว​สะสม” ที่​รับ gradient จาก​ทุก​เส้นทาง

Karpathy เพิ่ม tanh กับ exp สด​ใน​วิดีโอ เพื่อ​สาธิต ไม่​ได้​อยู่​ใน file engine.py ที่ commit ไว้ (ตัว file จริง​มี​แค่ + * ** ReLU) เรา​จึง​แยก​ออก​มา​ให้​ชัด​ว่า​มัน​เป็น add-on — แปะ​เข้า class ทีหลัง​ด้วย​มือ:

import math
# --- add-on ของผู้เรียน: ไม่ได้อยู่ใน engine.py จริง (Karpathy เพิ่มสดในวิดีโอ) ---
def tanh(self):
t = math.tanh(self.data)
out = Value(t, (self,), 'tanh')
def _backward():
self.grad += (1 - t * t) * out.grad # d(tanh)/dx = 1 - tanh(x)^2 = 1 - out^2
out._backward = _backward
return out
def exp(self):
out = Value(math.exp(self.data), (self,), 'exp')
def _backward():
self.grad += out.data * out.grad # d(e^x)/dx = e^x = out
out._backward = _backward
return out
Value.tanh = tanh # แปะเข้า class ทีหลัง — ย้ำสายตาว่ามันเป็น add-on ไม่ใช่ของเดิม
Value.exp = exp

ทั้ง​คู่​เป็น​ตัวอย่าง​สวย​ของ “local derivative คำนวณ​จาก out ที่​มี​อยู่​แล้ว”: tanh ใช้ 1 - out², exp ใช้ out ตรงๆ — ไม่​ต้อง​คำนวณ forward ซ้ำ​เลย

เขียน _backward เอง​ด้วย​มือ​เสี่ยง​พิมพ์​ผิด (สลับ self/other, ลืม +=) เรา​ตรวจ​ได้​ด้วย​นิยามอนุพันธ์ตรงๆ — central difference: ขยับ input ที​ละ​นิด ±h แล้ว​ดู​ว่า output ขยับ​เท่าไร (f(x+h) − f(x−h)) / (2h) ถ้า​ใกล้​กับ gradient ที่ backprop ให้ ก็ “น่า​จะ​ถูก”:

def f(a, b):
return a * b + b ** 3 # ใช้ได้ทั้งกับ Value และ float
# analytic: ให้ micrograd ไล่ backprop
a, b = Value(2.0), Value(-3.0)
loss = f(a, b)
loss.backward()
# a.grad, b.grad ถูกเติมแล้ว (analytic: da = b = -3, db = a + 3b^2 = 29)
# numerical: central difference บนค่า float ล้วน
h = 1e-5
num_a = (f(2.0 + h, -3.0) - f(2.0 - h, -3.0)) / (2 * h)
num_b = (f(2.0, -3.0 + h) - f(2.0, -3.0 - h)) / (2 * h)
assert abs(a.grad - num_a) < 1e-4 # ต่างกันนิดเดียว → "น่าจะถูก"
assert abs(b.grad - num_b) < 1e-4

ระวัง​กรอบ​ความคิด: assert ผ่าน ไม่​ได้​พิสูจน์​ว่า backprop ถูก — มัน​เป็น sanity check ที่​บอกว่า “ที่​จุด​นี้ ทิศทาง​นี้ ตัวเลข​สอง​วิธี​ตรง​กัน” เท่านั้น (เลือก h เล็ก​เกิน​ก็​เจอ floating-point error, ใหญ่​เกิน​ก็​ไม่ใช่​อนุพันธ์​แล้ว) มัน​จับ bug ได้​เยอะ แต่​ไม่ใช่​บท​พิสูจน์

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

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

แปล​ว่า: อยาก​ได้ chain rule กับ backprop แบบ​ครบ​เชิง​พิสูจน์ (รวม​ถึง​เหตุผล​ว่า​ทำไม reverse-mode ถึง​คุ้ม​ค่า​กว่า forward-mode สำหรับ​เน็ต) เปิด Goodfellow-Bengio-Courville, Deep Learning (MIT Press, 2016) §6.5.1–6.5.3 (chain rule + การ​ไล่ backprop บน computational graph) และ §6.5.6 (algorithm ทั่วไป) — คณิต​ส่วน​นี้​เป็น “คณิต​ที่​ไม่​เน่า” จึง​อ้าง​ตำรา​ปี 2016 ได้​สนิท ไม่​ต้อง​ติด​ป้าย stale

เพราะ​ทุก _backward ใช้ += gradient จึง สะสม — ดี​ตอน fan-out ใน1 pass แต่​กลาย​เป็น​กับดัก​ตอน​เท​รน​หลายๆ ก้าว: ถ้า​ไม่​ล้าง grad ให้​เป็น 0 ก่อน​เริ่ม pass ใหม่ gradient ของ​ก้าว​นี้​จะ​ไป​บวก​ทับ​กับ​ก้าว​ก่อน → อัปเดต​เพี้ยน (โครง loop เทรน​แบบ​ร่าง):

for step in range(100):
loss = forward(params) # เดินหน้าคำนวณ loss
for p in params:
p.grad = 0 # ❗ ล้าง grad ก่อน backward — ไม่งั้นสะสมข้ามก้าว
loss.backward() # ไล่ย้อน เติม grad ชุดใหม่
for p in params:
p.data += -0.01 * p.grad # ก้าวสวนทาง gradient (gradient descent)

bug คลาสสิก​เดียวกัน​นี้​จะ​กลับ​มา​ใน​บท6 ตอน​ใช้ PyTorch — ตรง​นั้น​เรา​จะ​เรียก optimizer.zero_grad() แทน loop p.grad = 0 ด้วย​มือ แต่ เจตนา​เหมือนกันเป๊ะ: ล้าง​ตัว​สะสม​ก่อน backprop รอบ​ใหม่ ลืม​เมื่อไร gradient ก็​ทับซ้อน​เมื่อนั้น

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

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

แปล​ว่า: micrograd เป็น scalar engine — บวก/คูณ​ที​ละ​ค่า ไม่มี tensor, ไม่มี batching, ไม่มี double-backprop (grad ของ grad), ไม่ optimize อะไร​ให้​เร็ว มัน​มี​ไว้​ให้​คุณ เห็น backprop ชัดๆ ไม่ใช่​ให้​ใช้​เทรน​ของ​จริง บท6 เรา​ถึง​ย้าย​ไป PyTorch ที่​ทำ tensor + autograd แบบ​ใช้งาน​จริง​บน​สเกล​ใหญ่ — ความ​ช้า​ของ micrograd คือ design เพื่อ​การ​สอน ไม่ใช่​ข้อ​บกพร่อง

micrograd สอน​ทั้ง​หัวใจ​ของ deep learning ใน class เดียว: Value ห่อ scalar พร้อม grad/_prev/_backward เพื่อ​วาด computational graph อัตโนมัติ; แต่ละ primitive op (+ * ** ReLU, และ add-on tanh/exp) ถือ local derivative ของ​ตัวเอง​ไว้​ใน closure; .backward() สร้าง topo แบบ ลูก​ก่อน​พ่อ แล้ว​เดิน reversed เพื่อ​ไล่ chain rule จาก loss ย้อน​กลับ โดย seed self.grad = 1 เป็น​ชนวน invariant ที่​ห้าม​พลาด​สาม​ข้อ — += ไม่ใช่ = (fan-out ต้อง​บวก​สะสม, จึง​เป็น​เหตุ​ที่ grad เริ่ม​ที่ 0), reverse-topological order (พ่อ​ก่อน​ลูก), และ ล้าง grad ทุก​ก้าว ใน loop เทรน — ทั้ง​สาม​จะ​ตาม​เรา​ไป​ถึง PyTorch ในบท6 แบบ​ไม่​เปลี่ยน​แก่น

บท​หน้า​เรา​เอา engine ตัว​นี้ (หรือ NumPy เทียบเท่า) ไป​สร้าง​ของ​จริง​ชิ้น​แรก: bigram language model — ทาย​ตัว​อักษร​ถัด​ไป​จาก​ตัว​ก่อนหน้า ด้วย one-hot → softmax → cross-entropy แล้ว​เทรน​ด้วย gradient descent จริงๆ นี่​คือ​ครั้ง​แรก​ที่ backprop ที่​เรา​เพิ่ง​สร้าง​จะ​ได้ “เรียน” อะไร​บาง​อย่าง​จาก​ข้อมูล​จริง


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

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

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

ข้อ 1 / 3

ทำไมทุก _backward ใน micrograd ต้องใช้ += ไม่ใช่ = (และทำไม grad จึงเริ่มต้นที่ 0)?