00 背景知识速成-----全流程实现chatgpt2(预训练->sft->ppo)
00 背景知识速成(看代码之前,先花 20 分钟读懂这篇)
这篇不是讲解某个文件,而是把读这些代码之前必须具备的背景知识集中讲一遍。整份 InstructGPT 项目反复用到这些概念,先打通它们,后面的逐行笔记会顺很多。
如果你是零基础,务必按顺序读这篇;如果只是忘了某个概念,可以直接跳到对应小节。
目录
- 数据是怎么进模型的:字符 → token → id → 向量
- 神经网络的"最小零件":nn.Linear、激活函数、Dropout
- 概率与损失:logits、softmax、交叉熵、log 概率、KL 散度
- "训练"到底在做什么:前向、反向、梯度下降、epoch/batch/step
- 张量操作速查:广播、view/reshape、transpose、gather、flatten
- 强化学习一分钟:状态、动作、奖励、策略、价值、优势
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是指数函数eˣ,把任意数变成正数;- 除以总和,保证所有词的概率加起来 = 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,SFT5e-5,PPO1e-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 → 往"降低该动作概率"的方向更新。
读完这篇,你应该能回答
- tokenizer 把文字变成了什么?为什么还要 embedding?
F.cross_entropy为什么传 logits 而不传概率?- 一次训练为什么要有
zero_grad → backward → step三步? model.train()和model.eval()差在哪?- 强化学习和监督学习最本质的区别是什么?
答不上来的话,回去再看对应小节;全答上来,就可以开始读 gpt_model.md 了。
更多推荐



所有评论(0)