My Blog
Agent 学习

Agent 学习第 4 周:语言模型与 Transformer

从 N-gram 到 BERT,再手写一个 Transformer 层,把 L×L 注意力矩阵里的每个数字算明白。

发布于 2026/9/11更新于 2026/9/11阅读约 16 分钟
只问这篇文章 →

第四周讲语言模型的演进: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:

  1. 顺序计算无法并行 —— 按时间步串行,GPU 用不满
  2. 记忆仍会衰减 —— LSTM 只是改善,超长文档照样掉效果
  3. 迁移能力有限 —— 每个下游任务都要从头训练
  4. 单向结构 —— 双向 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 到底改变了什么。

相关文章