ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

训练目标与损失函数全解析:从预训练到 DPO、PPO、GRPO 的损失体系指南

2026/9/15 18:30:19 拓冰建站 浏览量
训练目标与损失函数全解析:从预训练到 DPO、PPO、GRPO 的损失体系指南 训练目标与损失函数全解析从预训练到 DPO、PPO、GRPO 的损失体系指南【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch本文是 train-llm-from-scratch 项目中 LLM Foundations 系列教程的第四篇核心讲解目标函数Objectives这一主题从解码器模型最基础的**下一个 token 预测next-token prediction**出发依次推导交叉熵损失、困惑度Perplexity、SFT 掩码损失再到 DPO/PPO/GRPO 三类偏好与强化学习目标的数学形式并逐一对照仓库源码中的真实实现。读完本文你将掌握贯穿整个训练流水线的损失函数全谱系理解每一阶段训练奖励什么行为的数学本质并能在仓库代码中精确定位对应的实现与调用链。从架构到目标模型能算什么 vs. 训练要奖励什么在深入任何训练脚本之前需要先建立一个核心区分这也是 objectives.md 开篇的观点**架构architecture**决定模型能计算什么——多少层 Transformer、多大嵌入维度、多长上下文**目标objective**决定训练奖励什么行为——模型朝哪个方向更新参数。对于本项目采用的 decoder-only 语言模型基础目标就是下一个 token 预测给定已出现的 token 序列最大化真实下一个 token 的概率。这个朴素目标之所以能催生出大量有用行为是因为预测下一个 token这个任务本身迫使模型把语法、事实、格式、风格乃至推理痕迹压缩进权重之中——foundations 导读 中把完整训练故事概括为raw text → tokenizer → token ids → decoder-only Transformer → next-token cross-entropy → base checkpoint → SFT → preference optimization → RL / verifier optimization → inference and chat整条流水线共享同一个骨干网络各阶段的变化几乎只体现在数据和损失函数上——这正是本仓库最重要的设计思想也是我们下面逐一拆解损失函数的原因。从 logits 到概率softmax 与逐位置交叉熵模型输出什么在位置 (t)模型经过最后一个线性层lm_head输出词汇表上的 logits 向量[ z_t \in \mathbb{R}^{V} ]其中 (V) 是词汇表大小。Softmax 把 logits 归一化为一个概率分布[ p_\theta(x_{t1}i \mid x_{\leq t}) \frac{\exp(z_{t,i})}{\sum_{j1}^{V}\exp(z_{t,j})} ]在仓库中Transformer类的前向路径src/models/transformer.py先由嵌入层加 Transformer 块得到隐藏状态再经self.lm_head nn.Linear(n_embed, vocab_size)投影出(B, T, V)形状的 logits。其中词汇表大小由 configs/base.json 中的vocab_size: 50304决定。逐位置交叉熵训练时每个位置的目标是一个整数 token id (y_t)。该位置的交叉熵就是真实 token 的负对数概率[ \ell_t -\log p_\theta(y_t \mid x_{\leq t}) ]对整个 batch(B) 个序列每序列 (T) 个位置取平均得到语言建模损失[ \mathcal{L}{\text{LM}} -\frac{1}{BT}\sum{b1}^{B}\sum_{t1}^{T} \log p_\theta(y_{b,t} \mid x_{b,\leq t}) ]对应到 Transformer.forward 中的实现logits, loss model(idx, targets) flat_logits logits.reshape(B * T, C) # 源码中为 reshape 而非 view targets targets.reshape(B * T).long() loss F.cross_entropy(flat_logits, targets)值得注意的是transformer.py 特意使用了reshape而非view源码注释说明了原因targets 来自 batch 张量的非连续切片view()在 CPU 上会抛错跨设备.to(cuda)拷贝恰好使其变得连续因此这个 bug 只在 CPU 运行时暴露——这是把论文公式落地为工程时一个典型的细节坑。The shift预测下一个 token 的对齐技巧模型输入与目标的错位是理解所有损失函数的前提[ x [t_0,t_1,\ldots,t_{T-1}], \qquad y [t_1,t_2,\ldots,t_T] ]即模型用位置 (t) 的输入去预测位置 (t1) 的 token。预训练路径的forward通过F.cross_entropy一次性计算所有位置上述代码shift 隐含在对同一序列整体计算交叉熵之中。而在 SFT 路径中shift 被显式写出因为需要配合掩码见下文logits logits[:, :-1, :] targets tokens[:, 1:] mask loss_mask[:, 1:].to(logits.dtype)这段代码来自 src/post_training/sft.py 的sft_loss函数裁剪掉最后一个位置的 logits、丢弃第一个目标 token同时把掩码同步平移一个位置。Perplexity交叉熵的指数形式困惑度Perplexity是交叉熵的指数化[ \text{PPL} \exp(\mathcal{L}) ]如果模型对真实下一个 token 给出高概率则损失下降、困惑度也下降。三种典型读法损失接近 (\log(V))模型接近均匀随机猜测损失越低模型把概率集中在合理的下一个 token 上验证损失比训练损失更重要它反映泛化能力。对本仓库vocab_size 50304见 configs/base.json[ \log(V) \approx 10.83 ]因此一个未训练模型的语言建模损失通常从接近 10.83 起步——这是判断训练是否正常启动的常用基准。仓库中的 loss_curve.png 记录了实际训练过程中损失曲线的演化可用于对照这一理论起点。SFT 掩码损失只学回答不学提问监督微调SFT仍然使用下一个 token 交叉熵但只有助手的回复 token 计入损失[ \mathcal{L}{\text{SFT}} \frac{\sum{b,t} m_{b,t},\ell_{b,t}} {\sum_{b,t} m_{b,t}} ]其中 (m_{b,t}1) 表示该位置是助手 token(0) 表示是 prompt token。这一区分的意义在于模型应当学会如何回答问题而不是预测 prompt 本身。src/post_training/sft.py 的完整实现def sft_loss(logits, tokens, loss_mask): # Predict token t1 from position t (same shift the base model uses). logits logits[:, :-1, :] targets tokens[:, 1:] mask loss_mask[:, 1:].to(logits.dtype) V logits.size(-1) ce F.cross_entropy( logits.reshape(-1, V).float(), targets.reshape(-1).long(), reductionnone, ) ce ce.view(targets.shape) * mask return ce.sum() / mask.sum().clamp(min1.0)几个工程要点使用reductionnone先得到逐位置损失再手动乘掩码、按掩码和做平均clamp(min1.0)防止掩码全零时除零掩码由 src/post_training/chat_template.py 的encode_chat生成该模块的 docstring 明确指出掩码 1 表示助手完成 token0 表示 prompt/角色标记同文件还提供了pack_examples序列打包工具把多个(ids, loss_mask)示例拼接后切成context_length的固定行长示例以 EOT 结尾EOT 充当打包分隔符尾部不足一行的 token 直接丢弃src/post_training/sft.py。调用链印证scripts/train_sft.py第 27 行导入sft_loss并在训练循环中直接调用scripts/train_sft.py。SFT 的超参如lr: 1e-05、epochs: 3见 configs/sft.json。序列对数概率偏好优化与 RL 的共享原语预训练和 SFT 只需要单 token 的交叉熵但偏好优化DPO和强化学习PPO/GRPO需要整条回复的对数概率。对于 prompt (p) 之后的回复 token (a_1,\ldots,a_L)[ \log \pi_\theta(a \mid p) \sum_{t1}^{L} \log \pi_\theta(a_t \mid p, a_{t}) ]仓库在 src/post_training/rollout.py 中实现sequence_logprobs它先调用compute_logprobs做 teacher-forced 重算同样遵循logits[:, t]预测sequences[:, t1]的 shift 约定rollout.py再用response_mask对回答位置求和def sequence_logprobs(model, sequences, response_mask, *, temperature1.0, requires_gradTrue): lp, mask compute_logprobs(model, sequences, response_mask, temperaturetemperature, requires_gradrequires_grad) m mask.to(lp.dtype) return (lp * m).sum(dim-1), m.sum(dim-1)该原语被多处复用从源码确认其调用方包括DPO/ORPO/KTOscripts/train_dpo.py 导入sequence_logprobs计算 chosen/rejected 两侧的序列对数概率PPO 策略比率ppo_policy_loss中ratio torch.exp(new_logp - old_logp)GRPO 策略比率grpo_loss中同样的 ratio 计算KL 度量对冻结的 reference 模型做同样的对数概率计算后求差。rollout.py的模块 docstring 还澄清了两个实现决策其一这些函数故意写成自由函数而非Transformer的方法因为 PPO/GRPO 需要对四套参数可训练策略、冻结 reference、旧策略快照、actor-critic 包装运行完全相同概率数学自由函数组合性更好其二对数概率始终在 fp32 下计算logits.float()即使在 bf16 autocast 下因为 PPO/GRPO/DPO 要做对数概率相减bf16 舍入误差有害rollout.py。DPO 目标相对参考模型的偏好差距DPODirect Preference Optimization使用偏好对chosen 回复 (y_w) 与 rejected 回复 (y_l)。它把策略与冻结的参考模型做对比[ \Delta_\pi \log \pi_\theta(y_w \mid x) - \log \pi_\theta(y_l \mid x) ][ \Delta_{\text{ref}} \log \pi_{\text{ref}}(y_w \mid x) - \log \pi_{\text{ref}}(y_l \mid x) ][ \mathcal{L}{\text{DPO}} -\log \sigma\left(\beta(\Delta\pi - \Delta_{\text{ref}})\right) ]src/post_training/dpo.py 的实现几乎与公式一一对应pi_logratios policy_chosen_logps - policy_rejected_logps ref_logratios ref_chosen_logps - ref_rejected_logps logits pi_logratios - ref_logratios loss -F.logsigmoid(beta * logits).mean()直觉解读让 chosen 比 rejected 更可能但以参考模型为基线衡量变化量从而约束策略不至于无约束漂移。该函数还额外返回解耦的诊断量chosen_reward beta * (policy_chosen_logps - ref_chosen_logps).detach()隐式奖励供日志监控并提供了implicit_accuracy用于统计隐式奖励偏好 chosen 的比例dpo.py。值得补充的是dpo.py 还实现了两个同族的变体ORPOorpo_loss无参考模型把 chosen 上的 SFT NLL 与一个 odds-ratio 偏好项合并L NLL(chosen) lambda * -log sigmoid(log_odds_chosen - log_odds_rejected)其中log_odds mean_logp - log(1 - exp(mean_logp))并使用数值稳定的_log1mexp辅助函数KTOkto_loss基于每个样本 desirable/undesirable 信号从 batch 平均对数比率估算detach 的参考 KL 基线。配置上loss_type: dpo、beta: 0.1、orpo_lambda: 1.0均可在 configs/dpo.json 中切换训练脚本 scripts/train_dpo.py 按cfg.loss_type分发到对应损失函数。PPO 目标带裁剪的策略梯度PPO 先采样回复、打分再用新旧动作概率之比更新策略[ r_t(\theta) \frac{\pi_\theta(a_t \mid s_t)} {\pi_{\text{old}}(a_t \mid s_t)} \exp(\log \pi_\theta - \log \pi_{\text{old}}) ]裁剪后的策略目标为[ \mathcal{L}_{\text{PPO}} -\mathbb{E}_t \left[ \min \left( r_t(\theta) A_t, \text{clip}(r_t(\theta),1-\epsilon,1\epsilon) A_t \right) \right] ]src/post_training/ppo.py 的实现ratio torch.exp(new_logp - old_logp) surr1 ratio * advantages surr2 torch.clamp(ratio, 1.0 - clip, 1.0 clip) * advantages loss -masked_mean(torch.min(surr1, surr2), mask)裁剪的作用是防止一次更新偏离采样策略太远。ppo_policy_loss还返回clip_fraction((ratio - 1.0).abs() clip)的掩码均值作为更新健康度指标。ppo.py模块还包含 PPO 完整更新所需的其他可复用件ppo.pycompute_gae在 response token 上做广义优势估计delta rewards gamma * values_next * nonterminal - values并正确处理最后一个回复 token 之后不再 bootstrap的终止语义whiten对优势做零均值、单位方差的掩码归一化ppo_value_loss裁剪的价值函数损失0.5 * max(unclipped, clipped)平方误差approx_klold_logp - new_logp的掩码均值作为 PPO 健康指标。调用链scripts/train_ppo.py第 28 行导入这些函数scripts/train_ppo.py并在微批循环中调用ppo_policy_loss。PPO 依赖的 value head 见 src/post_training/value_head.py。GRPO 目标组内相对优势免去价值网络GRPO 不需要学习价值函数。对每个 prompt 采样一组G 个回复然后在组内归一化奖励[ A_i \frac{r_i - \text{mean}(r_1,\ldots,r_G)} {\text{std}(r_1,\ldots,r_G)\epsilon} ]这个优势的含义是这条回答相对同一 prompt 的兄弟回答是更好还是更差src/post_training/grpo.py 的实现r rewards.view(-1, group_size) adv (r - r.mean(1, keepdimTrue)) / (r.std(1, keepdimTrue) eps)GRPO 的更新在 grpo.py 的grpo_loss中token 级裁剪代理目标加上逐 token 的 KL 惩罚使用 Schulman 的 k3 无偏非负估计器exp(diff) - diff - 1.0并返回kl与clipfrac统计量。调用方为 scripts/train_grpo.py组内优势由group_advantages计算。GRPO 特别适用于可验证奖励verifiable reward的推理任务因为它移除了 PPO 的价值头和 critic 训练循环——仓库中对应的验证器实现在 src/post_training/rewards/verifiers.py。奖励模型的损失Bradley-Terry 成对偏好虽然原文档的目标对比表格中列入了 reward model但其数学形式值得单独确认。仓库的奖励模型在 Transformer 骨干上加一个标量奖励头按 InstructGPT 惯例从最后一个真实 token 的隐藏状态读取序列奖励src/post_training/reward_model.py并用 Bradley-Terry 成对损失训练[ \mathcal{L}{\text{RM}} -\log \sigma(r{\text{chosen}} - r_{\text{rejected}}) ]实现在 src/post_training/reward_train.pydef bradley_terry_loss(chosen_rewards, rejected_rewards): return -F.logsigmoid(chosen_rewards - rejected_rewards).mean()同模块还提供preference_accuracychosen 得分更高的比例作为头号评估指标与reward_marginchosen 与 rejected 的得分差距作为训练诊断量。训练好的奖励模型为 PPO 提供标量奖励信号。各阶段目标对比总表下表综合 objectives.md 的原始表格与仓库源码确认阶段数据主要信号学到什么仓库实现预训练原始 token 流next-token 交叉熵语言建模transformer.pySFTprompt/回答示例掩码 next-token 交叉熵指令跟随与格式sft.py奖励模型chosen/rejected 对Bradley-Terry 偏好损失标量偏好打分reward_train.pyDPOchosen/rejected 对序列对数概率偏好损失无 rollout 的偏好对齐dpo.pyPPO采样回复 奖励裁剪策略梯度KL 约束下的奖励追寻ppo.pyGRPO分组采样回复 验证器组相对裁剪策略梯度无 critic 的验证器驱动推理grpo.py小结与下一步目标函数给出了方向预训练用逐位置交叉熵学语言建模SFT 用掩码交叉熵学指令跟随奖励模型用 Bradley-Terry 学偏好打分DPO 用相对参考模型的序列对数概率差距做对齐PPO/GRPO 则用裁剪的组相对策略梯度在奖励信号下寻优。它们的共同点是共享同一个 Transformer 骨干变化的只是数据形态与损失函数。损失有了方向优化决定模型能否稳定地追随这个方向。推荐继续阅读本系列的下一篇优化与训练系统Optimization Training Systems其中讲解 AdamW、线性 warmup 余弦学习率衰减、梯度累积、梯度裁剪、bf16 autocast 与多卡训练它们与本文的损失共同构成完整可运行的训练循环。【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考