跟著做
共 2 支 · 在本機 venv 跑 · 還沒裝環境?
10_math_foundations.py
python套件:
在 GitHub 看完整原始碼 numpy原始碼
"""
10_math_foundations.py
================================
第 0 站「數學基礎」的可跑版本——把路線圖裡「用 numpy 手刻線性回歸 + 梯度下降」
和「自己驗證鏈鎖律 / 反向傳播算得對不對」變成看得見的數字。
對照講義 docs/00(第 0 站)、docs/08(深入講義:梯度下降與反向傳播)。
這支程式回答三個問題:
1. 「梯度」到底是什麼?→ 用「數值梯度」(把參數動一點點看 loss 變多少)驗證。
2. 鏈鎖律對不對?→ 手算解析梯度,和數值梯度比對(這叫 gradient check,業界除錯必備)。
3. 梯度下降為什麼能讓 loss 變小?→ 手刻線性回歸,看 loss 一路下降。
執行:python3 10_math_foundations.py (只需要 numpy)
"""
import sys
import numpy as np
# Windows 主控台預設可能是 cp950(Big5),遇到 ² ∇ ← 這類符號會報錯;
# 統一把輸出切成 UTF-8(VS Code 內建終端機預設就是 UTF-8,可正確顯示中文與數學符號)。
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
np.random.seed(0)
# ──────────────────────────────────────────────────────────────
# 1. 數值梯度:梯度的「定義」就是「動一點點,看輸出變多少」
# ──────────────────────────────────────────────────────────────
def numerical_gradient(f, x, eps=1e-6):
"""
對函數 f 在點 x 的每個分量做中央差分:
∂f/∂xᵢ ≈ ( f(x+eps·eᵢ) − f(x−eps·eᵢ) ) / (2·eps)
這是「梯度的定義」,慢但幾乎一定對,所以拿來當「標準答案」驗證解析梯度。
"""
grad = np.zeros_like(x, dtype=float)
it = np.nditer(x, flags=["multi_index"])
while not it.finished:
idx = it.multi_index
old = x[idx]
x[idx] = old + eps
f_plus = f(x)
x[idx] = old - eps
f_minus = f(x)
x[idx] = old
grad[idx] = (f_plus - f_minus) / (2 * eps)
it.iternext()
return grad
def demo_gradient_meaning():
print("=" * 64)
print("1) 梯度是什麼:f(x,y) = x² + 3y,在 (2, 1) 的梯度")
print("=" * 64)
f = lambda v: v[0] ** 2 + 3 * v[1]
point = np.array([2.0, 1.0])
g_num = numerical_gradient(f, point.copy())
g_analytic = np.array([2 * point[0], 3.0]) # 手算:∂f/∂x=2x, ∂f/∂y=3
print(f" 解析梯度(手算公式):{g_analytic}")
print(f" 數值梯度(動一點點):{g_num.round(5)}")
print(" → 兩者相同。梯度 = 『往哪個方向走,f 上升最快』;反方向就是讓 f 變小的方向。\n")
# ──────────────────────────────────────────────────────────────
# 2. 鏈鎖律 / 反向傳播:手算梯度 vs 數值梯度(gradient check)
# ──────────────────────────────────────────────────────────────
def demo_chain_rule():
print("=" * 64)
print("2) 鏈鎖律驗證:一個小神經元 loss = (sigmoid(w·x + b) − y)²")
print("=" * 64)
x = np.array([1.5, -2.0, 1.0])
y = 1.0
w = np.array([0.2, -0.5, 0.1])
b = 0.3
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def forward(w, b):
z = w @ x + b
a = sigmoid(z)
return (a - y) ** 2
# 手算解析梯度(一層一層套鏈鎖律):
# loss = (a - y)², a = sigmoid(z), z = w·x + b
# dloss/da = 2(a - y)
# da/dz = a(1 - a) ← sigmoid 的導數
# dz/dw = x , dz/db = 1
z = w @ x + b
a = sigmoid(z)
dloss_da = 2 * (a - y)
da_dz = a * (1 - a)
dz = dloss_da * da_dz # = dloss/dz
grad_w_analytic = dz * x
grad_b_analytic = dz * 1.0
# 數值梯度當標準答案
grad_w_num = numerical_gradient(lambda ww: forward(ww, b), w.copy())
grad_b_num = numerical_gradient(lambda bb: forward(w, bb[0]), np.array([b]))[0]
print(f" ∂loss/∂w 解析:{grad_w_analytic.round(6)}")
print(f" ∂loss/∂w 數值:{grad_w_num.round(6)}")
print(f" ∂loss/∂b 解析:{grad_b_analytic:.6f} 數值:{grad_b_num:.6f}")
rel_err = np.abs(grad_w_analytic - grad_w_num).max()
print(f" 最大誤差:{rel_err:.2e} → 幾乎為 0,代表手算的反向傳播是對的。")
print(" 這就是 PyTorch .backward() 在做的事,只是它自動幫你套完整張計算圖。\n")
# ──────────────────────────────────────────────────────────────
# 3. 梯度下降:手刻線性回歸(不准用現成函式庫)
# ──────────────────────────────────────────────────────────────
def demo_linear_regression():
print("=" * 64)
print("3) 梯度下降:手刻線性回歸 y ≈ w·x + b(資料來自 y = 2x + 1 + 雜訊)")
print("=" * 64)
# 造資料
X = np.linspace(-3, 3, 40)
true_w, true_b = 2.0, 1.0
Y = true_w * X + true_b + np.random.randn(40) * 0.5
w, b = 0.0, 0.0 # 從零開始
lr = 0.05
n = len(X)
for step in range(201):
pred = w * X + b
loss = np.mean((pred - Y) ** 2) # 均方誤差
# 梯度(對 MSE 手算):
grad_w = np.mean(2 * (pred - Y) * X)
grad_b = np.mean(2 * (pred - Y))
# 更新:θ ← θ − η·∇L
w -= lr * grad_w
b -= lr * grad_b
if step % 40 == 0:
print(f" step {step:3d} | loss={loss:6.3f} | w={w:.3f} b={b:.3f}")
print(f"\n 學到的 w={w:.3f}, b={b:.3f}(真值 w=2, b=1)。")
print(" loss 一路下降 → 這就是『所有模型訓練』的最小核心,從這裡放大就是訓練 GPT。\n")
if __name__ == "__main__":
demo_gradient_meaning()
demo_chain_rule()
demo_linear_regression()
print("一句話:梯度下降 = 反覆『算梯度、往反方向走一小步』。")
print("會 gradient check(解析 vs 數值)你就有了除錯一切模型的底牌。")
04_autograd_from_scratch.py
python原始碼
"""
04_autograd_from_scratch.py
================================
手刻一個「自動微分(autograd)」引擎,再用它訓練一個小神經網路解 XOR。
這補上 docs/01 講的「反向傳播 backprop」——PyTorch 的 .backward() 底層就是這個想法。
核心觀念:
- 每個數值都用一個 Value 物件包起來,記住「自己是怎麼被算出來的」(建立計算圖)。
- forward:照算式往前算出答案。
- backward:用鏈鎖律,從輸出往回,把梯度傳回每個參數(這就是反向傳播)。
- 有了梯度,就能做梯度下降 θ ← θ − η·∇L 來訓練。
執行:python3 04_autograd_from_scratch.py (純 Python,不需 numpy、不需 GPU)
參考:Andrej Karpathy 的 micrograd。
"""
import math
import random
random.seed(1337)
class Value:
"""一個純量,外加它的梯度與「怎麼算出來的」紀錄。"""
def __init__(self, data, _children=(), _op=""):
self.data = data
self.grad = 0.0
self._backward = lambda: None # 這個節點把梯度往回傳的方法
self._prev = set(_children)
self._op = _op
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), "+")
def _backward(): # d(out)/d(self)=1, d(out)/d(other)=1
self.grad += out.grad
other.grad += out.grad
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
other.grad += self.data * out.grad
out._backward = _backward
return out
def __pow__(self, p): # 只支援常數次方
assert isinstance(p, (int, float))
out = Value(self.data ** p, (self,), f"**{p}")
def _backward():
self.grad += (p * self.data ** (p - 1)) * out.grad
out._backward = _backward
return out
def tanh(self): # 非線性激活函數
t = math.tanh(self.data)
out = Value(t, (self,), "tanh")
def _backward():
self.grad += (1 - t * t) * out.grad # tanh 的導數 = 1 - tanh²
out._backward = _backward
return out
# 一些方便的運算
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 backward(self):
"""反向傳播:先把節點做拓撲排序,再從輸出往回呼叫每個 _backward。"""
topo, visited = [], set()
def build(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build(child)
topo.append(v)
build(self)
self.grad = 1.0 # 輸出對自己的梯度 = 1
for v in reversed(topo):
v._backward()
# ---------- 用上面的引擎組一個小神經網路 ----------
class Neuron:
def __init__(self, n_in):
self.w = [Value(random.uniform(-1, 1)) for _ in range(n_in)]
self.b = Value(0.0)
def __call__(self, x):
act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)
return act.tanh()
def parameters(self):
return self.w + [self.b]
class Layer:
def __init__(self, n_in, n_out):
self.neurons = [Neuron(n_in) for _ in range(n_out)]
def __call__(self, x):
outs = [n(x) for n in self.neurons]
return outs[0] if len(outs) == 1 else outs
def parameters(self):
return [p for n in self.neurons for p in n.parameters()]
class MLP:
def __init__(self, n_in, sizes):
dims = [n_in] + sizes
self.layers = [Layer(dims[i], dims[i + 1]) for i in range(len(sizes))]
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x
def parameters(self):
return [p for layer in self.layers for p in layer.parameters()]
def main():
# XOR 問題:輸入兩個 0/1,輸出它們是否「不同」。用 +1/-1 當標籤配合 tanh。
X = [[0, 0], [0, 1], [1, 0], [1, 1]]
Y = [-1.0, 1.0, 1.0, -1.0] # XOR:相同→-1,不同→+1
model = MLP(2, [4, 4, 1]) # 2 輸入 → 兩層各 4 神經元 → 1 輸出
n_params = len(model.parameters())
print(f"模型參數量:{n_params}(每個都是一個會記梯度的 Value)\n開始訓練 XOR ...\n")
lr = 0.1
for epoch in range(200):
# forward:算預測與損失(均方誤差)
preds = [model(x) for x in X]
loss = sum((p - y) ** 2 for p, y in zip(preds, Y))
# backward:先把梯度歸零,再反向傳播
for p in model.parameters():
p.grad = 0.0
loss.backward()
# 梯度下降:沿梯度反方向更新參數
for p in model.parameters():
p.data -= lr * p.grad
if epoch % 40 == 0 or epoch == 199:
print(f"epoch {epoch:3d} | loss = {loss.data:.4f}")
print("\n訓練後預測(target:相同→-1,不同→+1):")
for x, y in zip(X, Y):
pred = model(x).data
ok = "✓" if (pred > 0) == (y > 0) else "✗"
print(f" 輸入 {x} → 預測 {pred:+.2f}(應為 {y:+.0f}){ok}")
print("\n重點:上面那條 loss.backward() 就是反向傳播;PyTorch 幫你把這套做到大規模、跑在 GPU 上。")
if __name__ == "__main__":
main()