00 背景知识速成(看代码之前,先花 20 分钟读懂这篇)

这篇不是讲解某个文件,而是把读这些代码之前必须具备的背景知识集中讲一遍。整份 InstructGPT 项目反复用到这些概念,先打通它们,后面的逐行笔记会顺很多。

如果你是零基础,务必按顺序读这篇;如果只是忘了某个概念,可以直接跳到对应小节。


目录

  1. 数据是怎么进模型的:字符 → token → id → 向量
  2. 神经网络的"最小零件":nn.Linear、激活函数、Dropout
  3. 概率与损失:logits、softmax、交叉熵、log 概率、KL 散度
  4. "训练"到底在做什么:前向、反向、梯度下降、epoch/batch/step
  5. 张量操作速查:广播、view/reshape、transpose、gather、flatten
  6. 强化学习一分钟:状态、动作、奖励、策略、价值、优势

1. 数据是怎么进模型的:字符 → token → id → 向量

1.1 为什么机器不能直接读文字

计算机只能处理数字。"你好世界"这串字,模型是不认识的。所以第一步是把文字切成小块、编上号,再变成向量。

1.2 tokenizer(分词器)干的事

tokenizer = "文字 → 编号"的转换器。它做的事:

"这本书真是好看"  --分词-->  ["这","本","书","真","是","好","看"]  --查词表-->  [6821, 1726, 1210, 2682, 1878, 2619, 3554]
  • token(词元):被切出来的最小单位。中文里通常一个"字"或一个"词"就是一个 token。
  • 词表(vocab):所有 token 的清单。本项目词表大小 = 21128,每个 token 有个唯一编号 id(0~21127)。
  • id(编号):token 在词表中的位置。tokenizer.encode("这本书") 返回的就是一串 id。

1.3 embedding(嵌入)干的事

id 是整数,但模型需要的是向量。Embedding 就是一张"查表字典":

id=6821  →  查表  →  一个 768 维的向量(一堆小数)
  • nn.Embedding(vocab_size, emb_dim) 内部是一张形状 (21128, 768) 的大表。
  • id 是几,就取第几行。取出来这行向量就是该词的"意思"(语义相近的词,向量也相近)。
  • 这张表是训练学出来的——一开始是随机数,训练后越来越好。

对比 one-hot 编码:也可以把 id 编成一个只有一位是 1 的 21128 维向量(one-hot),但那又稀疏又浪费,而且看不出词与词的相似关系。Embedding 表等于把 21128 维压缩到 768 维,还能学出语义。

1.4 一句话总结数据流

文字 --tokenizer--> token id 列表 --embedding--> (B, T, 768) 向量
        [6821, 1726, ...]                每个 token 一个 768 维向量
  • B:batch,一次喂几条样本。
  • T:seq_len,一条样本里多少个 token。
  • 形状 (B, T, 768) 就是"整个模型世界的主语"——几乎所有操作都在这个形状上进进出出。

2. 神经网络的"最小零件"

2.1 nn.Linear(线性层):一句乘法 + 加法

nn.Linear(in_features, out_features)

数学定义:y = xWᵀ + b

  • 输入 x:(..., in_features)
  • 权重 W:(out_features, in_features)
  • 偏置 b:(out_features,)
  • 输出 y:(..., out_features)

通俗理解:给输入的每个分量配一个"权重",加权求和,再加个偏置,得到新的一组数。它只能学"线性关系",所以后面必须接激活函数。

2.2 激活函数:让网络"能弯曲"

如果全是线性层,那多少层叠加都等于一层(线性函数套线性函数还是线性),学不了复杂东西。激活函数(如 GELU、ReLU、sigmoid)是非线性的,让每一层的输出"扭曲"一下,网络才有表达复杂规律的能力。

线性层 --GELU--> 非线性 --线性层--> 非线性 ... 

2.3 Dropout:随机"丢"一部分,防过拟合

训练时,Dropout 随机把一部分神经元的输出置 0(比如 10%),剩下的值放大一点。效果:

  • 模型不能依赖某个特定的神经元,被迫学得更"平均"、更鲁棒;
  • 这就是为什么训练和推理要切换模式model.train() 开 dropout、model.eval() 关 dropout)——推理时不需要随机丢。

3. 概率与损失

3.1 logits(打分)是什么

模型最后一层输出的原始分数,可以理解成"模型认为每个词该被选中的热度"。

  • 形状:(B, T, 21128)——每个位置对词表 21128 个词各打一个分。
  • 分数是任意实数,可正可负可大。它还不是概率

3.2 softmax:把分数变成概率

想让分数之和等于 1(才能当概率用),就用 softmax:

probs[i] = exp(z[i]) / Σⱼ exp(z[j])
  • exp 是指数函数 ,把任意数变成正数;
  • 除以总和,保证所有词的概率加起来 = 1;
  • 分数越高,概率越大(但不是线性:高分差距会被放大)。

数值稳定性技巧(代码里反复出现):

z = z - z.max(dim=-1, keepdim=True)   # 先减每行最大值
probs = softmax(z)                     # 结果一样,但不会溢出

为什么?如果 z 里有很大的数(比如 1000),exp(1000) 直接是 inf,算炸。先减去最大值,最大的变成 0,exp(0)=1,安全。(softmax 对每个元素同时加/减同一个常数,结果不变,数学上等价。)

3.3 交叉熵损失(cross entropy):衡量"猜得有多不准"

一句话:模型给"正确答案"的概率越低,损失越大。

loss = -log(P(正确答案))
  • 模型猜"正确词"的概率是 0.9 → -log(0.9) ≈ 0.105,很小,损失小 ✓
  • 模型猜"正确词"的概率是 0.0001 → -log(0.0001) ≈ 9.2,很大,损失大 ✗
  • 训练目标就是把这个损失降到最低,也就是让"正确词的概率"越来越大。

为什么要 log? 概率是 0~1 的乘数,直接最小化损失时梯度很小;取 -log 后,概率越小损失增长越猛,梯度更有"推动力"。另外 log 把连乘变成连加,方便数学推导。

代码里的标准用法(PyTorch):

F.cross_entropy(logits, labels)
# logits: (N, C),C 是类别数
# labels: (N,),每个位置是"正确答案的类别编号"
# 它内部自动做 softmax,所以传 logits(分数)而不是概率

注意F.cross_entropy 内部已经带 softmax 了,所以传原始 logits 而不是 softmax 后的概率(传概率反而错)。

3.4 log 概率与 KL 散度(RLHF 里反复出现)

  • log 概率log P(x)。P(x) 是 0~1,所以 log P(x) 是负数(或 0)。log 概率越大 = 概率越大。
  • KL 散度(Kullback-Leibler divergence):度量"一个分布 P 偏离另一个分布 Q 有多远"。值越大越偏离,等于 0 表示完全相同。
  • 在 RLHF 里,我们不想让新策略偏离参考模型太多,就在奖励里加一项"偏离惩罚":-β·(log π_θ − log π_ref)log 相减正好是逐 token 的 KL 贡献——这就是 3-PPO.py 里 rewards = -beta * kl 的由来。

4. "训练"到底在做什么

4.1 一次训练的完整流程(背下来)

1. 前向传播:喂数据 → 模型算出预测 → 算出损失(衡量错多少)
2. 反向传播:loss.backward() → 用链式法则算出每个参数的梯度(该往哪改)
3. 参数更新:optimizer.step() → 让每个参数沿"减小损失"的方向走一小步
4. 清梯度:optimizer.zero_grad() → 把上一步的梯度清零,防止累加

每一步对应一行代码:

optimizer.zero_grad()   # 清梯度
loss.backward()         # 反传
optimizer.step()        # 更新

4.2 梯度下降与学习率

  • 梯度:损失函数对每个参数的偏导,告诉它"往哪个方向改、损失会变小"。
  • 更新公式θ = θ − lr × 梯度。学习率 lr每步走多远
    • lr 太大 → 步子太大,容易"跳过"最优点,甚至发散;
    • lr 太小 → 走得慢,训练半天不收敛。
  • 本项目:预训练 5e-4,SFT 5e-5,PPO 1e-5——越到后面越小,因为越到后期越要精细微调。

4.3 epoch / batch / step 是什么

含义 例子
样本(sample) 一条数据 一条评论
batch(批) 一次喂给模型的样本集合 8 条评论
step / iteration(步) 处理完一个 batch 并更新一次参数 1 次 optimizer.step()
epoch(轮) 把整个数据集完整过一遍 全部评论过 1 遍 = 1 epoch

关系:1 epoch = 数据条数 ÷ batch_size 个 step。本项目 SFT:NUM_EPOCHS=1 表示只把数据过一遍(防止灾难性遗忘)。

4.4 train() 和 eval() 的区别

  • model.train():训练模式,Dropout 开启
  • model.eval():评估模式,Dropout 关闭,且不更新任何参数。
  • 忘记切换是新手常见 bug:评估时忘了 eval(),结果每次输出随机抖动。

5. 张量操作速查(读代码必备)

操作 干什么 例子
x.shape 看形状 (2, 4, 768)
.unsqueeze(0) 加一个维度 (4,) → (1, 4)(补 batch 维)
.squeeze(0) 去掉一个维度 (1, 4) → (4,)
.view(a,b,c) 重排形状(总元素数不变) (2,4,768) → (2,4,12,64)
.reshape(a,b,c) 同 view,更宽容(必要时复制) 同上
.transpose(1,2) 交换两个维度的顺序 (2,4,12,64) → (2,12,4,64)
.flatten(0,1) 把一段维度压平成一维 (8,256,21128) → (2048,21128)
.cat([a,b], dim=1) 拼接 (1,5)+(1,1) → (1,6)
.gather(dim, index) 按索引取数 从词表维取出"实际 token"的分数
.item() 单元素张量 → Python 数 取 loss 值
.numel() 元素总数 8×256=2048

广播(broadcasting):形状对不上时自动"拉齐"

(4, 768)  +  (2, 4, 768)  =  (2, 4, 768)
  • 规则:从右往左对齐,维度能配得上(相等或一个为 1)就自动扩展。
  • 位置嵌入 (T, 768) 和词嵌入 (B, T, 768) 相加,靠的就是广播——(T,768) 被自动"复制"到每个 batch。
  • 维数对不上会直接报错,这是最常见的报错来源之一。

view 为什么老报错"requires contiguous"

[:, :-1, :] 这类切片产生的结果内存可能不连续.view() 要求连续内存,所以报错;.reshape() 不报(必要时自动复制)。所以代码里错位后用 .contiguous() 或直接用 view 之前 contiguous() 一下。


6. 强化学习一分钟

6.1 四件套

术语 通俗理解 本项目里
状态 S “现在写到哪了” 已生成的 token 序列
动作 a “下一步选什么” 选下一个 token
策略 π(a|S) “在这种情况下选各种动作的概率” GPT-2 输出的概率分布
奖励 R “这一步做得好不好” KL 惩罚 + RM 打分

6.2 强化学习和监督学习的本质区别

  • 监督学习:有"标准答案"(标签),照答案学。比如 SFT 直接告诉模型"下一个词该是 X"。
  • 强化学习没有标准答案,只有事后打分。模型自己试、被打分、再改进。就像学骑自行车,没人告诉你"往左 3 度",只有"摔了/没摔"。

6.3 为什么 RL 训练不稳定?

  • 监督学习:一批数据固定,梯度是"确定性"的。
  • 强化学习:数据是策略自己采样的——策略一变,下一批数据的分布就变(“自己给自己出题”)。这就是 on-policy(在轨)的含义:数据必须由当前策略产生。
  • 数据分布一直在变 + 奖励有噪声 → 训练容易来回震荡。所以 PPO 才要:用旧策略的数据、限制一次更新幅度(裁剪)、用价值网络当基线降方差。

6.4 策略梯度的一行直觉

想让高分动作更常出现:
  梯度 ∝ A_t × ∇log π_θ(a_t|s_t)
        ↑        ↑
     "这步多好"  "让这步更容易出现的改动方向"
  • 优势 A_t > 0(这步比预期好)→ 往"提高该动作概率"的方向更新;
  • A_t < 0 → 往"降低该动作概率"的方向更新。

读完这篇,你应该能回答

  1. tokenizer 把文字变成了什么?为什么还要 embedding?
  2. F.cross_entropy 为什么传 logits 而不传概率?
  3. 一次训练为什么要有 zero_grad → backward → step 三步?
  4. model.train()model.eval() 差在哪?
  5. 强化学习和监督学习最本质的区别是什么?

答不上来的话,回去再看对应小节;全答上来,就可以开始读 gpt_model.md 了。

更多推荐