第四周讲语言模型的演进:N-gram → 神经网络语言模型 → BERT,最后深入 Transformer 内部。
这周的作业是手写一个 Transformer 层(week4/task/Transformer层实现.py)。写完之后对 Attention 的理解和只看图完全不是一回事——尤其是那个 L×L 的注意力矩阵,自己算一遍才知道每个数字什么意思。
一、语言模型是什么
一句话:对文字序列赋予概率的数学模型,衡量「成句概率」。
P(w₁, w₂, …, wₙ) = ?
核心能力是下一词预测。应用场景其实早就在我们身边了:搜索补全、输入法联想、语音识别、拼写纠错、机器翻译。
二、N-gram:数数就完事了
三步推导
① 链式规则(精确分解)
P(w₁…wₙ) = ∏ P(wᵢ | w₁…wᵢ₋₁)
数学上无损,但条件越往后越长,历史组合数爆炸,语料里根本统计不到。
② 马尔可夫假设(N-gram 的立身之本)
P(wᵢ | w₁…wᵢ₋₁) ≈ P(wᵢ | wᵢ₋ₙ₊₁…wᵢ₋₁)
只保留前 N−1 个词做条件。这一步是用精度换可行性。
③ 频率估计(本质就是数数)
P(wᵢ | ctx) = C(ctx, wᵢ) / C(ctx)
零概率问题
链式乘法里只要一项为 0,整句概率就归零。语料里没见过的搭配就直接判死刑,这显然不合理。
拉普拉斯平滑的做法很粗暴——分子加 1,分母加词表大小:
P(wᵢ|ctx) = (C(ctx,wᵢ) + 1) / (C(ctx) + V)
四大局限
| 局限 | 说明 |
|---|---|
| 数据稀疏 | N 越大组合指数增长,大量 N-gram 概率为 0 |
| 长距离依赖失效 | 窗口固定为 N−1,一超就没法建模 |
| 语义泛化弱 | 纯字符串匹配,「手机」和「电话」毫无关系 |
| 存储开销 | O(Vᴺ),随词表指数增长 |
第三条是最本质的——N-gram 完全不懂语义,这正是词向量要解决的问题。
三、神经网络语言模型
Bengio 在 2003 年的思路:把每个词映射成连续稠密向量,语义相近的词在空间里距离近。
P(wₜ | ctx) = f(e(wₜ₋₁), e(wₜ₋₂), …)
RNN 语言模型进一步用隐状态把历史一路带下去,相比 N-gram 有三个优势:无固定窗口 / 同一套权重处理所有时刻 / 词向量带来语义泛化。
困惑度 Perplexity
语言模型的核心指标:
PPL = exp( −(1/N)·Σ log P(wᵢ|ctx) )
越低越好。 直觉上可以理解成「模型在每一步平均纠结于多少个候选词」。
但 RNN 也撑不住了
四个瓶颈直接催生了 Transformer:
- 顺序计算无法并行 —— 按时间步串行,GPU 用不满
- 记忆仍会衰减 —— LSTM 只是改善,超长文档照样掉效果
- 迁移能力有限 —— 每个下游任务都要从头训练
- 单向结构 —— 双向 LSTM 是高代价的折衷
第一条是致命的。在「算力可以堆」的时代,不能并行就等于出局。
四、BERT:预训练 + 微调
范式转变
大规模语料自监督预训练(不需标注)
↓
通用语言知识模型
↓
少量标注数据微调
这个思路是从 CV 借来的——ImageNet 上大规模预训练,再迁移到各类视觉任务。
BERT 之前 NLP 的困境是:每个任务从头训练、词向量静态不随上下文变、任务间知识无法复用。
两个预训练任务
MLM(掩码语言模型):随机遮盖 15% 的 token 让模型还原。这 15% 内部再分:
80% → [MASK]
10% → 随机词
10% → 保持原词
为什么不全用 [MASK]?因为微调时输入里没有 [MASK],全用会造成预训练和微调的输入分布不一致。
NSP(下一句预测):判断句子 B 是否为 A 的真实下一句,50% 正例 + 50% 随机负例。
总损失是两者相加。
Embedding 是三种向量相加
e = e_token + e_position + e_segment
- Token Embedding —— WordPiece 词表约 30K
- Position Embedding —— 可学习的位置编码(注意:不是 sin/cos,那是原始 Transformer 论文的做法)
- Segment Embedding —— 区分句子 A/B,服务于 NSP
五、Transformer 内部:这周的重点
单个 Block 的结构
输入
↓
Multi-Head Self-Attention
↓
Add & LayerNorm ← 残差① z = LayerNorm(x + MHA(x))
↓
Feed-Forward Network
↓
Add & LayerNorm ← 残差② out = LayerNorm(z + FFN(z))
↓
输出
Self-Attention:QKV 到底是什么
三个矩阵都是对同一份输入做独立线性变换得到的:
Q = XW_Q 当前词「想问什么」
K = XW_K 别的词「能提供什么」
V = XW_V 别的词「实际包含什么」
Attention(Q,K,V) = softmax( QKᵀ / √dₖ ) · V
三步:算相关度 → Softmax 归一化 → 用权重对 V 加权求和。
为什么要除以 √dₖ? 防止点积数值过大,把 Softmax 推进饱和区导致梯度消失。维度越高点积越大,所以要按维度开方来缩放。
张量维度:手写时最容易卡住的地方
以 BERT-base、序列长 L=128 为例:
输入 X L × d_model = 128 × 768
d_k = 768/12 = 64
Q/K/V L × d_k = 128 × 64
Q × Kᵀ L × L = 128 × 128 ← 注意力矩阵
Weights × V L × d_k = 128 × 64
关键点:进出形状一致,所以才能安全地堆 12 层、24 层。
L×L 矩阵怎么读
这是我这周最大的收获,写代码之前一直没真正理解:
Scores[i][j] = Token i 对 Token j 的相关性得分
任意两个 token 都算一次,所以是全局注意力。然后逐行做 Softmax:
Weights[i] = softmax(Scores[i]) 这一行之和 = 1.0
Weights[i][j] = Token i 从 Token j 借取信息的比例
看这个矩阵时:对角线偏大说明模型在关注自身;某行某列偏大说明这两个词强相关。
多头:在不同子空间并行地看
单个头只能关注一种关系。多头让模型同时看多种:
头 1 → 句法依存(主谓宾)
头 2 → 语义相似(近义词)
头 3 → 位置与指代
每个头独立计算,最后拼接再线性投影。
FFN 与残差
FFN(x) = GELU(xW₁ + b₁)W₂ + b₂ 768 → 3072 → 768
先升维再降回,对每个位置独立作用。
残差连接让梯度可以经跳连直接反传,层归一化缓解分布漂移——这两个加上 FFN,BERT 才敢堆 12/24 层。
六、动手:手写 Transformer 层
作业是自己实现一个 Transformer 层。写的过程中卡住过三次:
① QKV 的形状。 多头要先把 d_model 切成 h × d_k,做完注意力再拼回去。这个 reshape 和 transpose 的顺序错了不会报错,但结果全是错的。
② Mask 加在哪一步。 必须在 softmax 之前把被屏蔽位置置为 −∞。加在之后就没意义了——softmax 已经把权重分配出去了。
③ 残差连接的对象。 LayerNorm(x + MHA(x)) 里的 x 是这一层的输入,不是 MHA 的输出。看图时容易看漏那根跳线。
手写一遍最大的价值是:Attention 不再是一个黑盒方块,而是三次矩阵乘法加一次 softmax。 后面理解 Causal Mask、KV Cache 这些都建立在这个基础上。
这周的收获
1. 演进是被问题推着走的。
N-gram 不懂语义 → 词向量
RNN 不能并行 → Transformer
每个任务都要重训 → 预训练 + 微调
每一步都是在解决上一步过不去的坎,不是凭空冒出来的新点子。
2. 「不能并行」是 RNN 出局的真正原因。
不是效果不好,是在算力可以无限堆的时代,串行结构拿不到红利。这个视角对理解后面的技术选型很有帮助。
3. 注意力矩阵是可读的。
Weights[i][j] = Token i 从 Token j 借取信息的比例 —— 想通这句话之后,很多关于 Attention 的讨论(比如注意力可视化、注意力头在学什么)就都能看懂了。
下周进入大语言模型:BERT 为什么做不了生成、Causal Mask、SFT,以及 ChatGPT 到底改变了什么。