7. 综合练习:手搓一个迷你 Transformer
这是什么:迷你 Transformer = 把前面各章的组件拼起来:嵌入 → 位置编码 → 自注意力 → 预测下一个词,一个只有"前向传播"的最小模型。
为什么用:零件全见过了,但只有亲手拧在一起,才能看清数据在真实模型里的一趟全程路线——这正是每一步"为什么存在"的总验收。
如何用:照下面的 50 多行代码从头到尾跑一遍,对不上输出就回头看对应章节;跑通后按"动手改"继续折腾。
全程一次看:四步流水线
真实 GPT 是几十层"注意力 + 前馈网络"的重复,我们砍到最小配置:一步注意力。整个流程只有四步——查嵌入、加位置、过注意力、接龙预测:
import numpy as np
rng = np.random.default_rng(2026) # 固定随机种子:你跑出的数字和我的一模一样
vocab = {"猫": 0, "吃": 1, "鱼": 2, "了": 3}
tokens = ["猫", "吃", "鱼", "了"]
T, d_model = len(tokens), 8
E = rng.normal(size=(len(vocab), d_model)) # [第2章] 嵌入矩阵(示意权重)
X = E[[vocab[t] for t in tokens]] # ① 查表:整句变 4×8 向量
pos = np.arange(T)[:, None]
i = np.arange(d_model)[None, :]
angle = pos / (10000 ** (2 * (i // 2) / d_model)) # [第5章] 三角函数位置编码
H = X + np.where(i % 2 == 0, np.sin(angle), np.cos(angle)) # ② 加上位置
def attention(X): # [第3章] 自注意力
scores = X @ X.T / np.sqrt(d_model) # 这里 Q=K=V,最简化版本
A = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True)
return A @ X
H = attention(H) # ③ 每个词吸收语境
logits = H[-1] @ E.T # [第1章] 点积相似度复用
p = np.exp(logits) / np.exp(logits).sum() # ④ softmax:下一个词的概率
for t, prob in zip(vocab, p):
print(t, np.round(prob, 3))
你会看到:
猫 0.001
吃 0.0
鱼 0.0
了 0.999
| 代码 | 对应章节 | 作用 |
|---|---|---|
E[[vocab[t] for t in tokens]] |
第 2 章 | 查表:4 个词各领一行 8 维向量 |
H = X + np.where(...) |
第 5 章 | 位置编码按位相加,谁在前谁在后从此有数 |
scores / np.sqrt(d_model) |
第 3 章 | 缩放点积:这里图省事 Q=K=V 直接用 X,咱已见过完整版 |
H[-1] @ E.T |
第 1 章 | 拿"了"位置的向量与每个词向量打分,选"接下来像谁" |
np.exp(logits) / ... |
第 3 章 | softmax:分数变概率,这里"了"以 0.999 大胜 |
一点必须讲清楚:这份输出没有任何"学"的成分。权重全是 rng.normal 随机生成的,"了 0.999"只是随机数字的结果,不是模型懂了中文。真实大模型 = 同样的算式 + 在海量语料上反复"猜下一个词、错了改权重"训练出来的参数(这个过程叫训练(training);想亲手跑一次,去 LLaMA-Factory 就行)。
记忆口诀:查表领向量,加码记住序;注意力吸收语境,softmax 接龙下一句。
动手改
- 把
tokens换成["狗", "追", "球"](改vocab对应三行),看概率分布怎么洗牌 - 在 ③ 后面再加一层
H = attention(H),对比最后概率的变化 - 把
default_rng(2026)换成 2027 再跑一遍,加深"权重随机、结果各异"的印象