Lesson 08 · 約 25 分鐘
核心數學與直覺
線代 / 機率 / 微積分 — 只挑用得到的
08 ・ 核心數學與直覺:把影片要講的,直接寫成文字 + 數字
這份的用意:原本的學習路線把「數學直覺」外包給影片(3Blue1Brown、Karpathy)。 一直切出去看影片很煩、也打斷節奏。所以這份把最關鍵、最常被「靠看影片才懂」的四件事—— 梯度下降、反向傳播、注意力、softmax/交叉熵——用文字 + 實際數字講清楚, 每一節都配一支可以馬上跑、把數字印出來的程式。
看完這份 + 跑完對應程式,你就不需要為了「聽懂」而追影片了。 影片變成「想換個講法加深」時的選配,不是必修。
配套程式:
code/10_math_foundations.py、code/04_autograd_from_scratch.py、code/01_attention_from_scratch.py、code/09_eval_basics.py。
0. 你只需要四個底層觀念
整個深度學習(LLM、Diffusion、世界模型……)其實只站在四根柱子上:
- 模型 = 一個有很多旋鈕(參數)的函數:給輸入,吐輸出。
- 損失(loss)= 一個分數:衡量「輸出離正確答案多遠」。越小越好。
- 梯度 = 每個旋鈕該往哪轉:loss 對每個參數的偏微分。
- 訓練 = 反覆「算梯度、把旋鈕往讓 loss 變小的方向轉一點點」:這就是梯度下降。
剩下的全是細節與規模。下面把第 3、4 點(最容易卡)講到「看得見」。
1. 梯度下降:為什麼「往梯度反方向走」能讓 loss 變小
直覺:你站在山上、起大霧看不見路,想下到谷底。你能做的,是用腳感覺「腳下哪個方向最陡」, 然後往下坡方向踏一小步。重複很多次,就會到谷底。
- 「哪個方向最陡(上坡)」= 梯度 ∇L。
- 「往下坡走」= 往梯度的反方向走。
- 「一小步」= 乘上一個小的學習率 η。
公式就一行:
θ ← θ − η · ∇L
參數 學習率 梯度
用數字看(code/10_math_foundations.py 第 1 節會印出來):
取 f(x, y) = x² + 3y,在點 (2, 1):
- ∂f/∂x = 2x = 4,∂f/∂y = 3 → 梯度 =
[4, 3]。 - 想讓 f 變小,就往
[-4, -3]方向移動一點。
怎麼確定梯度算對了? 用「數值梯度」當標準答案:把某個參數 +ε 和 −ε,看 f 差多少:
∂f/∂xᵢ ≈ ( f(x+ε) − f(x−ε) ) / (2ε)
這叫 gradient check,是業界除錯模型的底牌。程式裡解析梯度 [4,3] 和數值梯度 [4,3] 完全吻合。
學習率 η 的取捨:太小 → 走太慢;太大 → 跨過谷底甚至發散。 跑
code/10把lr從 0.05 改成 0.5 再改成 1.5,看 loss 怎麼從「平順下降」變成「震盪/爆掉」。
👉 跑這個:python3 code/10_math_foundations.py(第 1、3 節)。
2. 反向傳播:鏈鎖律,一層一層把「責任」往回分
神經網路是「函數套函數套函數」。要知道「最外層的 loss 對最裡層的某個參數」有多敏感, 就用微積分的鏈鎖律:把一路上的偏微分乘起來。
用一個真的小神經元算給你看(code/10 第 2 節):
loss = (a − y)² ← 最外層:預測 a 和答案 y 的誤差平方
a = sigmoid(z) ← 激活函數
z = w·x + b ← 線性層
要 ∂loss/∂w,鏈鎖律一節一節接:
∂loss/∂a = 2(a − y)
∂a/∂z = a(1 − a) ← sigmoid 的導數
∂z/∂w = x
∂loss/∂w = ∂loss/∂a · ∂a/∂z · ∂z/∂w = 2(a−y) · a(1−a) · x
就這樣,從外往內把每一段導數乘起來。這就是反向傳播的全部祕密—— 名字嚇人,其實是「把鏈鎖律有系統地、從輸出往輸入跑一遍,順手把每個參數的梯度都收集好」。
code/10會印出「解析梯度 vs 數值梯度」最大誤差 ~1e-11 → 證明手算對了。code/04_autograd_from_scratch.py更進一步:手刻一個會自動套鏈鎖律的引擎(Value物件), 你會看到loss.backward()底層到底在幹嘛——PyTorch 的.backward()就是把這套做到大規模、跑在 GPU 上。
為什麼要殘差連接(residual)? 層一多,梯度一路乘下來容易變超小(梯度消失)。 殘差
x + f(x)讓梯度有一條「直通車」往回傳,深層網路才訓練得動。code/02_tiny_gpt.py的Block就是。
👉 跑這個:python3 code/10_math_foundations.py(第 2 節)→ 再 python3 code/04_autograd_from_scratch.py。
3. 注意力:一步一步用數字算出「誰該看誰」
這是 Transformer 的心臟,也是最值得「看著數字」理解的一段。公式:
Attention(Q, K, V) = softmax( Q · Kᵀ / √d ) · V
把它拆成五個動作(假設序列是 the cat sat down,每個字先被 embedding 成一個向量):
- 投影出 Q、K、V:每個字向量分別乘三個學來的矩陣,得到
- Q(我在找什麼)、K(我有什麼)、V(我能提供的內容)。
- 算相關度
Q · Kᵀ:字 i 的 Q 和字 j 的 K 做點積。點積大 = 兩者越「對盤」。 得到一個 T×T 的分數表(誰對誰多有興趣)。 - 除以 √d:維度越高,點積數值越大,會把 softmax 推到極端。除 √d 把它拉回合理範圍(穩定訓練)。
- 因果遮罩 + softmax:把「未來位置」設成 −∞(GPT 只能看過去), 再對每一列做 softmax → 每個字分配給其他字的注意力權重,一列加總為 1。
- 加權平均 V:用這些權重去加權所有字的 V → 得到「融合了上下文」的新表示。
用數字看(跑 code/01_attention_from_scratch.py,4 個字、因果):
the cat sat down
the 1.000 0.000 0.000 0.000 ← 第一個字只能看自己 → 權重 1
cat 0.512 0.488 0.000 0.000 ← 看得到 the 和自己
sat 0.330 0.341 0.329 0.000
down 0.251 0.250 0.245 0.254 ← 最後一個字看得到前面全部
- 右上角全是 0 → 那就是因果遮罩(看不到未來)。
- 每一列加總 = 1 → 那就是 softmax。
- 這張表是依輸入內容當場算出來的(不是固定權重)——所以注意力像一個「資料相依的濾波器」, 任何字都能一步直連到任意遠的另一個字(不像 RNN 要一格一格傳)。
多頭(multi-head):把上面這套同時做好幾組,每組各看一種關係(語法、指代、長程依賴…),最後拼起來。
👉 跑這個:python3 code/01_attention_from_scratch.py,把上面五個動作對著輸出看一遍。
4. softmax 與交叉熵:為什麼模型用這兩個
softmax:把一串任意實數(分數 / logits)變成「加總為 1 的機率」。
softmax(zᵢ) = exp(zᵢ) / Σ exp(zⱼ)
- 為什麼用
exp?它讓大的分數拉得更開(贏家更突出),且永遠為正。 - 實作要先減去最大值再 exp(數值穩定,避免溢位)——
code/01的softmax()就這樣寫。
交叉熵(cross-entropy):衡量「模型給的機率分佈」離「正確答案」多遠。 語言模型每一步的 loss 就是:
loss = − log P(真正的下一個字)
- 模型給真正的字 0.9 → loss = −log(0.9) ≈ 0.10(很小,猜得好)。
- 模型給真正的字 0.01 → loss = −log(0.01) ≈ 4.6(很大,猜得爛)。
- 對整段文字取平均,就是訓練時印出來的那個 loss。
困惑度(perplexity)= exp(loss):直覺是「模型平均在幾個字之間猶豫」。 完全亂猜、詞表 50 個字 → 困惑度 ≈ 50;學得好 → 往 1 靠。
👉 跑這個:python3 code/09_eval_basics.py(B 節),看三種模型的交叉熵與困惑度差多少。
5. 把四節串起來:一次訓練步到底發生什麼
當 code/02_tiny_gpt.py 跑一個訓練步時,發生的就是上面四節的合體:
1. 前向:embedding → 多層(注意力[第3節] + MLP,含殘差[第2節]) → 線性層 → logits
2. softmax + 交叉熵[第4節] 算出 loss
3. loss.backward():反向傳播[第2節],鏈鎖律把梯度傳回每個參數
4. optimizer.step():梯度下降[第1節] θ ← θ − η·∇L 更新參數
重複幾千次 → loss 下降 → 模型學會預測下一個字
這就是 GPT / Claude 的完整機制,差別只在規模、資料、後訓練手藝、基礎設施(見 docs/01 第 7 節)。
6. 現在你可以不追影片了——但想加深,這些值得看
你已經能「用數字」解釋核心觀念了。以下影片不是必修,是「想換個視覺講法再加深」時的選配:
- 3Blue1Brown《神經網路》《線性代數的本質》——視覺直覺最強(對應第 1~2 節)。
- Andrej Karpathy「Neural Networks: Zero to Hero」——
micrograd對應code/04、nanoGPT對應code/02。 - 李宏毅《生成式 AI》——中文,對應
docs/01全篇。
但學懂的關鍵不是再多看,而是動手:去做 docs/09_習題與自我檢測.md,把這份的每一節都「自己算一次、自己改程式驗證一次」。
看完不等於會,做完才算。
🛠 跟著做
共 2 支 · 在本機 venv 跑 · 還沒裝環境?
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
================================
手刻一個「自動微分(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()