00 背景知识速成-----全流程实现chatgpt2(预训练->sft->ppo)

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 有个唯一编号 id0~21127。id编号token 在词表中的位置。tokenizer.encode(这本书)返回的就是一串 id。1.3 embedding嵌入干的事id 是整数但模型需要的是向量。Embedding 就是一张查表字典id6821 → 查表 → 一个 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 维向量Bbatch一次喂几条样本。Tseq_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才能当概率用就用 softmaxprobs[i] exp(z[i]) / Σⱼ exp(z[j])exp是指数函数eˣ把任意数变成正数除以总和保证所有词的概率加起来 1分数越高概率越大但不是线性高分差距会被放大。数值稳定性技巧代码里反复出现z z - z.max(dim-1, keepdimTrue) # 先减每行最大值 probs softmax(z) # 结果一样但不会溢出为什么如果 z 里有很大的数比如 1000exp(1000)直接是inf算炸。先减去最大值最大的变成 0exp(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 把连乘变成连加方便数学推导。代码里的标准用法PyTorchF.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-4SFT5e-5PPO1e-5——越到后面越小因为越到后期越要精细微调。4.3 epoch / batch / step 是什么词含义例子样本sample一条数据一条评论batch批一次喂给模型的样本集合8 条评论step / iteration步处理完一个 batch 并更新一次参数1 次optimizer.step()epoch轮把整个数据集完整过一遍全部评论过 1 遍 1 epoch关系1 epoch 数据条数 ÷ batch_size 个 step。本项目 SFTNUM_EPOCHS1表示只把数据过一遍防止灾难性遗忘。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], dim1)拼接(1,5)(1,1) → (1,6).gather(dim, index)按索引取数从词表维取出实际 token的分数.item()单元素张量 → Python 数取 loss 值.numel()元素总数8×2562048广播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 把文字变成了什么为什么还要 embeddingF.cross_entropy为什么传 logits 而不传概率一次训练为什么要有zero_grad → backward → step三步model.train()和model.eval()差在哪强化学习和监督学习最本质的区别是什么答不上来的话回去再看对应小节全答上来就可以开始读gpt_model.md了。