#从“说人话”到 GPT-2:基于第一性原理的语言模型演进
系列导读:本系列旨在从第一性原理与底层张量算子出发,记录从零构建现代大语言模型(LLM)直至稀疏混合专家模型(Sparse MoE)的完整过程。第一部分参考 Andrej Karpathy 的 Neural Networks: Zero to Hero 教程,复盘从最朴素的统计查表(Bigram)演进到 2019 年 GPT-2 架构的底层逻辑。
#1. 起点:如果要设计一套“说人话”的算法,怎么写?
抛开所有深度学习术语,假设现在要从零设计一套能自动生成人类语言的算法,最符合直觉的第一次迭代是什么?
收集海量文本,统计每个字后面接什么字的概率最高,然后按概率采样拼装出完整回答。
例如,扫描完语料库后,统计发现“我”这个字后面接“喜”、“是”、“和”、“椅”的概率分布如下:
- $P(\text{喜} \mid \text{我}) = 0.001$
- $P(\text{是} \mid \text{我}) = 0.0008$
- $P(\text{和} \mid \text{我}) = 0.00076$
- $P(\text{椅} \mid \text{我}) = 3 \times 10^{-9}$
在一次采样生成中,模型输出“我喜”的概率就远高于“我椅”。这就是 Bigram(二元语法模型):每次预测下一个字时,仅依赖当前这 1 个字。如果词表大小(Vocabulary Size)为 $\vert{}V\vert{}$,那么整个 Bigram 模型的参数本质上就是一张 $\vert{}V\vert{} \times \vert{}V\vert{}$ 的二维转移概率查找表(Lookup Table)。
Bigram 具备最基础的局部短语拼接能力。如果“喜”后面接“欢”的概率也很高,某次采样中模型就能拼出“我喜欢”这样合理的词组。但因为它的视野只有 1 个字(一阶马尔可夫假设),它极大概率会生成局部两两合理、整体却完全语无伦次的序列——比如 “大家人们”(“大家”合理,“家人”合理,“人们”也合理,但拼在一起毫无逻辑)。
#2. 扩大视野与 N-gram 的三堵死墙
为了更好地控制输出、消除“大家人们”式的胡言乱语,最自然的第二次迭代是增加预测时的背景信息(Context):
不再只看前 1 个字,而是用前 2 个字(或更多)做前缀。例如当输入前缀是“我喜”时,第 3 个字是“欢”的条件概率 $P(\text{欢} \mid \text{我}, \text{喜})$ 会大幅上升,而“庆”的概率 $P(\text{庆} \mid \text{我}, \text{喜})$ 则会骤降(尽管在 Bigram 里“喜庆”是个高频词)。
这就是 N-gram 模型。每次预测下一个字时所参考的前缀长度($n-1$ 个字),就是模型的上下文窗口大小(Context Size)。在数学上,它试图逼近序列的联合概率链式法则:
$$P(x_1, x_2, \dots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_{t-(n-1)}, \dots, x_{t-1})$$
在一定范围内,随着 $n$ 的增加,模型的输出质量会显著变好。但这种基于“离散符号统计查表”的方法很快就会撞上三堵死墙,无法继续扩展(Scale up):
- 维度灾难(状态表指数爆炸):
查表法需要存储所有可能前缀的组合概率。英文字母只有 26 个,但汉字或子词(Subword)词表 $\vert{}V\vert{}$ 动辄数万。假设 $\vert{}V\vert{} = 10,000$:
- Bigram ($n=2$) 需要存储 $10^4 \times 10^4 = 10^8$ 个浮点数;
- 4-gram ($n=4$) 需要 $10^{16}$ 个条目(~40PB);
- 如果要把上下文窗口扩大到现代 LLM 常见的 $T = 4096$,表格状态数将达到 $10000^{4096}$.。
- 语义孤岛(零泛化能力): 在离散查表体系下,每个字只是一个孤立的整数编号(ID)。即便语料库里出现过一万次“我喜欢吃苹果”,只要从未出现过“我爱吃梨”这个精确组合,模型对后者的预测概率就是绝对的 0(未平滑的情况下)。查表法无法共享“喜欢/爱”、“苹果/梨”之间的语义相似性。
- 信息信噪比极低(无效上下文稀释): 当上下文拉长到几百个字时,并不是前文里的每一个字都对预测下一个字有信息增益。N-gram 强行将前 $n-1$ 个字焊死为一个刚性整体作为查表索引,哪怕中间夹杂了一个无关紧要的语气词,也会导致整个前缀匹配失败。
#3. 范式跃迁:从离散查表到 Transformer
既要把上下文窗口(Context Size)扩大到数千乃至数万字,又要保证参数量不随窗口指数爆炸,同时还得从长文中只筛选出对当前预测真正有用的信息——怎么解决?
从 N-gram 到 GPT-2(Transformer Decoder),核心完成了以下四步架构跃迁:
#第一步:用连续词向量(Embedding)消灭查表爆炸
放弃直接用离散的字 ID 去建多维概率表,改为给词表中的每个字分配一个固定维度(例如 $d = 768$)的可学习向量,即 Token Embedding。
在连续向量空间中,语义相近的词(“苹果”与“梨”)向量夹角很近。预测下一个字的过程,从“去指数级大表里查精确前缀”,变成了“将固定维度的向量送入神经网络做矩阵乘法”。模型参数量不再随上下文长度 $T$ 指数增长。
#第二步:引入 QKV 自注意力机制(Self-Attention)过滤有用信息
假设当前上下文有 $t$ 个字,对应向量序列 $x_1, x_2, \dots, x_t$。预测第 $t+1$ 个字时,如何把前 $t$ 个字的信息汇总过来?
- 粗糙做法(等权平均 / Bag-of-Words):直接把前 $t$ 个向量加起来除以 $t$,即 $y_t = \frac{1}{t}\sum_{i=1}^t x_i$。结果是关键线索被大量废话稀释成一团均值浆糊。
- Transformer 做法(QKV 动态加权路由):让模型自己学会“挑重点”。为此,Transformer 用三个独立的权重矩阵 $W_q, W_k, W_v \in \mathbb{R}^{d \times d_k}$,给每个字衍生出三个向量角色:
- Query ($q_t = x_t W_q$):我要找什么。代表当前位置 $t$ 为了预测下一个字,正在向上下文检索什么特征(例如:当前是个及物动词,正在前文寻找主语和修饰副词)。
- Key ($k_i = x_i W_k$):我有什么。代表历史位置 $i$ ($i \le t$) 的字对外展示的特征标签(例如:“我是一个名词”、“我是一个时间状语”)。
- Value ($v_i = x_i W_v$):我实际传递的内容。把原始向量 $x_i$ 经过一次线性投影转换为 $v_i$,解耦“寻址匹配”与“内容传输”,增加特征变换与计算表达空间。
当预测位置 $t$ 的输出时,拿当前的检索请求 $q_t$ 去和前面所有位置($1 \dots t$)的标签 $k_i$ 做内积(Dot Product)。内积越大,说明历史位置 $i$ 的信息对当前预测越重要。除以缩放因子 $\sqrt{d_k}$(防止内积过大导致 Softmax 梯度消失)并归一化后,就得到了每个历史字的权重,最后对 $v_i$ 加权求和:
$$\text{Attention}(Q, K, V) = \text{Softmax}\left(\frac{Q K^T + M}{\sqrt{d_k}}\right) V$$
(注:$M$ 为上三角全为 $-\infty$ 的因果掩码矩阵 Causal Mask,确保第 $t$ 个字只能看到 $1 \dots t$ 的历史,绝不能偷看未来的答案。)
#第三步:注入位置信息(Positional Encoding)
纯向量内积 $q_t \cdot k_i$ 存在一个盲区:它只关心内容匹配度,完全无视距离与顺序(置换不变性)。如果不加干预,“我打你”和“你打我”中的前文向量集合在 Attention 看来完全等价。
而在自然语言中,每个前面的字根据其相对距离和语序位置,对当前预测有着截然不同的重要性。因此,GPT-2 在输入层引入了一张可学习的位置编码表(Position Embedding,$W_{pe} \in \mathbb{R}^{T_{\max} \times d}$),在进入 Attention 前将字向量与位置向量直接相加:
$$x_t = \text{TokenEmb}(\text{idx}_t) + \text{PosEmb}(t)$$
#第四步:通信与思考的交替堆叠(Attention + MLP)
Self-Attention 解决了跨位置的上下文信息收集(Communication),但收集来的信息只是历史向量 $V$ 的线性加权组合。为了对收集到的上下文特征进行深度非线性加工,GPT-2 在每个 Attention 层后面接了一个前馈神经网络(MLP / FFN,负责在单个字内部做特征变换 Computation),并配合残差连接(Residual Connection)与层归一化(LayerNorm),堆叠 $L$ 层后输出最终预测概率。
#4. 代码映射:三代架构的核心张量流对比
回顾整个演进链路,我们实际上完成了三次特征提取器的升级(完整可运行源码见下方 Colab):
- Bigram 模型 (Colab):One-Hot 乘以 $\vert{}V\vert{} \times \vert{}V\vert{}$ 权重矩阵(等价于直接按索引查表),视野 $T=1$。
- MLP 模型 / Bengio 2003 (Colab):引入 Embedding 表
C消灭词表维度爆炸,但用硬拼接view(-1, T * d)处理上下文。虽然将block_size从 3 提高到 8 能短期提升性能,但首层权重W1的参数量随窗口 $T$ 线性膨胀,且不同位置之间无法共享特征权重,依然无法扩展到长文本。 - GPT-2 / Simple LLM (Colab):用
TokenEmb + PosEmb配合MultiHeadAttention + FeedForward动态聚合任意长度 $T \le T_{\max}$ 的上下文。
#5. 小结:为什么 2019 年的 GPT-2 还不够好?
从 Bigram 到 GPT2,我们通过 Embedding + 位置编码 + QKV 自注意力 + MLP 彻底解决了 N-gram 的维度灾难与上下文信息筛选问题。这也是 Karpathy build-nanogpt 教程的终点。
然而,站在今天工业界大模型(如 Llama 3、Qwen、DeepSeek-V3)的视角审视上面这段 2019 年的 GPT-2 代码,它内部依然潜藏着四个严重的数学与硬件效率瓶颈:
| 模块 | GPT-2 (2019) 的设计缺陷 | 现代架构演进方向(后续章节拆解) |
|---|---|---|
| 1. 位置编码 | 绝对位置查表 self.wpe(pos):写死了最大长度 block_size,超出长度直接报错,且破坏了相对距离的平移不变性。 | RoPE(旋转位置编码):在 Q 和 K 的复平面内积中直接注入相对距离旋转角(详见 第四篇)。 |
| 2. 归一化层 | LayerNorm 每次都要计算均值 $\mu$ 并中心化,且线性层带有冗余 bias,浪费显存带宽。 | RMSNorm + Bias-Free:砍掉均值计算与偏置项,减少访存同步开销。 |
| 3. 推理效率 | 每次生成新字都要把历史所有字的 QKV 从头算一遍($\mathcal{O}(T^2)$ 冗余计算),且多头注意力(MHA)显存占用巨大。 | KV Cache + GQA(分组查询注意力):缓存历史 KV 并跨头共享,打穿推理显存墙。 |
| 4. 参数效率 | MLP 层占了全模型 2/3 的参数量,且每生成一个字都必须 100% 激活所有参数(Dense 强绑定)。 | SwiGLU + Sparse MoE(稀疏混合专家):将巨型 MLP 拆分为数十个细粒度专家,每次仅通过 Router 激活极小部分参数。 |