news 2026/9/14 15:01:24

ms-swift GRPO 进阶:REAL 损失——把奖励当作标签的组内分类视角

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ms-swift GRPO 进阶:REAL 损失——把奖励当作标签的组内分类视角

ms-swift GRPO 进阶:REAL 损失——把奖励当作标签的组内分类视角

【免费下载链接】swiftUse PEFT or Full-parameter to CPT/SFT/DPO/GRPO 600+ LLMs (Qwen3.6, DeepSeek-V4, GLM-5.1, InternLM3, Llama4, ...) and 300+ MLLMs (Qwen3-VL, Qwen3-Omni, InternVL3.5, Ovis2.5, GLM4.5v, Gemma4, Llava, Phi4, ...) (AAAI 2025).项目地址: https://gitcode.com/GitHub_Trending/swift1/swift

在 RLVR(可验证奖励强化学习)训练中,GRPO 的逐 token 梯度权重既可能"给错了对象",也可能被少数过度自信的负样本"劫持"。本文围绕 ms-swift 中loss_type=real的进阶研究实现,讲清楚 REAL(Rewards as Labels)如何用组内分类替代 advantage 估计来重构 GRPO 优化目标:先剖析 GRPO 梯度中"梯度错配(Gradient Misassignment)"与"梯度主导(Gradient Domination)"两个问题的数学来源,再给出 REAL 的分类 logit、损失函数与有界梯度权重的推导,最后结合仓库源码逐行印证其实现、参数配置(--loss_type real/--real_tau)、官方训练脚本 real.sh 以及必须满足的per_device_train_batch_size可被num_generations整除的约束条件。读完本文,你可以直接在 ms-swift 的swift rlhf流程中启用 REAL 训练,并理解其在源码层面与教师蒸馏、RLSD 等特性不兼容的原因。

背景与动机:GRPO 目标函数的两处"梯度病"

GRPO 的目标函数与逐 token 梯度权重

GRPO 的优化目标为:

$$ J_{\mathrm{GRPO}}(\theta)=\mathbb{E}{q,o\sim\pi{\mathrm{od}}(\cdot|q)}\left[\frac{1}{|o|}\sum_{t=1}^{|o|}\left(\min\left(\rho_tA_t,\mathrm{clip}(\rho_t,1-\epsilon,1+\epsilon)A_t\right)\right)\right] $$

其中:

  • $\rho_t = \frac{\pi_\theta(o_t|q)}{\pi_{\mathrm{old}}(o_t|q)}$ 为概率比(probability ratio);
  • $A_t$ 为优势函数(advantage function)。

对应的梯度为:

$$ \nabla_{\theta} J_{\mathrm{GRPO}} = \mathbb { E } \left[ \frac { 1 } { | o | } \sum _ { t = 1 } ^ { | o | } \mathbb { I } _ { \mathrm { clip } } \cdot A _ { t } e ^ { s _ { t } } \nabla _ { \theta } \log \pi _ { \theta } \left( o _ { t } | q \right) \right] $$

其中:

  • $s_t = \log \frac{\pi_\theta(o_t|q)}{\pi_{\mathrm{old}}(o_t|q)}$ 为相对对数概率(relative log-probability);
  • $\mathbb{I}_{\mathrm{clip}}$ 为裁剪指示函数。

因此 GRPO 中每个 token 的梯度权重(取绝对值)为:

$$ |\mathcal{W}{\mathrm{GRPO}}|=\left{ \begin{array} {ll}\left|A\cdot e^s\right|, & \mathrm{if~}\mathbb{I}{\mathrm{clip}}=1, \ 0, & \text{otherwise.} \end{array}\right. $$

可以看到,梯度权重由 advantage 幅度 $A$ 与指数项 $e^s$ 共同决定,后者正是问题的根源。

问题一:正样本的梯度错配(Gradient Misassignment)

正样本($A>0$)而言,随着相对对数概率 $s$ 减小,梯度幅值 $|A\cdot e^s|$ 也随之减小。这与直觉相悖:模型当前把握较小($s$ 低)但作答正确的 token,本应获得更大的学习信号;而 GRPO 反而把权重给了模型已经很自信的 token,导致"欠训练"的 token 学习信号不足——这就是梯度错配。

问题二:负样本的梯度主导(Gradient Domination)

负样本($A<0$)而言,随着 $s$ 减小,$|A\cdot e^s|$ 并不减小而是指数级增大(此时 $s$ 越负说明旧策略下该 token 概率被当前策略压得越低,对应 $e^s$ 越大)。结果是:组内少数几个"过度自信的坏 token"主导了整个梯度,淹没同组其他负样本的信号;且该权重没有上界,可能导致不稳定、过大的参数更新。

从 ms-swift 源码结构看,GRPO 的 token 级裁剪权重正体现在 grpo_trainer.py 中coef_1/coef_2advantages的乘积项上,即 $\rho \cdot A$ 的经典 PPO-clip 形式,与上述公式一一对应。

针对上述两个问题,REAL 的思路是:不再估计 advantage 再逐 token 加权,而是把奖励直接当作分类标签,做组内(in-group)分类训练

REAL 的数学形式化:从 advantage 估计到组内分类

分类 logit:序列级相对对数概率

REAL 为每个样本 $k$ 定义如下分类 logit:

$$ \bar{s}^k=\frac{1}{|o^k|}\sum_{t=1}^{|o^k|}\left(\log\frac{\pi_\theta(o_t^k\mid q)}{\pi_{\mathrm{old}}(o_t^k\mid q)}\right) $$

即:该样本在当前策略下相对旧策略的逐 token 对数概率差在序列上的平均值。它的符号具有明确语义:

  • $\bar{s}^k > 0$:该样本在当前策略下比在旧策略下更可能 → 模型倾向于提升(promote)该样本;
  • $\bar{s}^k < 0$:该样本在当前策略下更不可能 → 模型倾向于抑制(suppress)该样本。

REAL 损失函数

$$ \mathcal{L}{REAL}=\log\left(1+\sum{\mathcal{O}+}e^{-\bar{s}^i/\tau}\right)+\log\left(1+\sum{\mathcal{O}_-}e^{\bar{s}^j/\tau}\right) $$

其中 $\mathcal{O}+$、$\mathcal{O}-$ 分别为组内奖励为正/负的样本集合,$\tau$ 为温度参数。两项都是 softplus/logsumexp 型结构:正样本项希望把 $\bar{s}^i$ 推正,负样本项希望把 $\bar{s}^j$ 推负,而常数 1 提供了"无样本时损失为 0"的自然基线。

梯度性质:单调且有界

REAL 的梯度权重绝对值为:

$$ |\mathcal{W}{\mathrm{REAL}}|= \begin{cases} \frac{1}{\tau}\frac{1}{1+C{+}e^{\bar{s}^{k}/\tau}}, & r=1 \ \ \frac{1}{\tau}\frac{1}{1+C_{-}e^{-\bar{s}^{k}/\tau}}, & r=0 \end{cases} $$

($r\in{0,1}$ 为样本奖励标签,$C_\pm$ 为组内同类样本数相关的常数。)由此带来两点关键改进,正好对应 GRPO 的两个病灶:

  • 有界性:权重幅值上界为 $1/\tau$,负样本不再能产生指数级放大的梯度,消除了 Gradient Domination;
  • 单调性:对正样本,$\bar{s}$ 越负(模型当前越不自信但答案正确),权重越大——学习信号被正确地分配给欠训练样本,解决了 Gradient Misassignment。

ms-swift 的参数文档 args_mixin.py 对该性质的描述与推导一致:"REAL induces monotonic and bounded gradient weighting with magnitude upper-bounded by 1/tau"。

参数配置

参数类型默认值说明
--loss_typestr-设置为real
--real_taufloat0.5温度参数,控制分类决策边界的锐度(决策边界越锐,权重区分度越强;同时决定权重上界 $1/\tau$)

对应源码定义见 args_mixin.py:

# REAL https://arxiv.org/abs/2602.05630 real_tau: float = 0.5

除这两个显式参数外,启用 REAL 还会触发一些隐式约束,源码中已做强制处理:

  • 奖励归一化被强制关闭。在 rlhf_args.py 中,当loss_type == 'real'时,scale_rewards会被强制置为'none'并打印警告,因为 REAL 的分组正/负划分直接使用奖励的原始符号(见后文group_rewards > 0的判断),组内标准化会破坏该语义;
  • 与教师蒸馏(OPD-RL)互斥real损失将 advantage 归约为序列级标量,与教师逐 token 的 signed log-ratio 冲突,rlhf_args.py 与 grpo_trainer.py 均会在参数校验和训练入口处直接抛出ValueError
  • 与 RLSD 优势重加权互斥advantage_reweight=rlsd同样不支持loss_type=real,原因见 rlhf_args.py。

源码实现走读:REAL 损失在 grpo_trainer.py 中的落地

以下按 grpo_trainer.py 的执行顺序,逐段对照上面的数学公式。

第一步:计算序列级 logit $\bar{s}^k$(L1096-1102)

global_scores = (log_ratio * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) group_scores = global_scores.view(-1, self.num_generations) seq_advantages = (advantages * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) group_rewards = seq_advantages.view(-1, self.num_generations)

log_ratio = per_token_logps - old_per_token_logps即逐 token 的 $\log(\pi_\theta/\pi_{\mathrm{old}})$,按completion_mask做有效 token 平均后得到 $\bar{s}^k$;随后通过view(-1, num_generations)把展平的 batch 重新切回"每个 prompt 一组"的形状——这正是组内分类的前提,也解释了后文"batch size 必须可被num_generations整除"的约束从何而来。

第二步:按奖励符号划分正负标签,并过滤无效组(L1104-1109)

pos_mask = (group_rewards > 0) neg_mask = (group_rewards <= 0) valid_mask = (pos_mask.sum(dim=1) != 0) & (neg_mask.sum(dim=1) != 0) if not valid_mask.any(): loss = torch.tensor(0., device=global_scores.device) * global_scores.mean()

组内必须同时存在正、负样本才参与损失计算(单侧组没有对比信号);若当前 batch 所有组都无效,则损失置 0(保留计算图以便 backward)。

第三步:用 logsumexp 实现 $\log(1+\sum e^{\cdot/\tau})$(L1111-1126)

scaled_scores = batch_scores / self.real_tau zeros = torch.zeros(batch_scores.size(0), 1, device=batch_scores.device, dtype=batch_scores.dtype) # Negative Loss: log(1 + sum(e^{S_neg})) neg_input = scaled_scores.masked_fill(~batch_neg_mask, float('-inf')) neg_loss = torch.logsumexp(torch.cat([neg_input, zeros], dim=1), dim=1) # Positive Loss: log(1 + sum(e^{-S_pos})) pos_input = (-scaled_scores).masked_fill(~batch_pos_mask, float('-inf')) pos_loss = torch.logsumexp(torch.cat([pos_input, zeros], dim=1), dim=1) loss = (neg_loss + pos_loss).sum() / group_rewards.size(0)

这里用一个巧妙的数值技巧实现公式中的常数项 1:把每行拼一个 0 元素再取logsumexp,$e^0=1$ 即成为和式中的"1";非目标类别用-inf掩掉($e^{-\infty}=0$),使 logsumexp 只累加对应类别。负样本项为 $\log(1+\sum_{\mathcal O_-} e^{\bar s^j/\tau})$,正样本项取 $-\bar s^i/\tau$ 后同理,最终按组数group_rewards.size(0))归一。

第四步:KL 正则独立叠加(L1128-1130)

if self.beta != 0.0: kl_loss = (per_token_kl * completion_mask).sum() / completion_mask.sum().clamp(min=1.0) loss = loss + kl_loss * self.beta

注意 REAL 与标准 GRPO 的一个重要结构差异:由于分类损失是序列级的,token 级策略损失在 grpo_trainer.py 中被直接置零(per_token_loss = torch.zeros_like(per_token_logps)),KL 项不再混入 token 级 loss,而是单独以序列级 token 平均的形式、乘以beta叠加到总损失上。这与 PPO/GRPO 分支中per_token_loss + self.beta * per_token_kl的写法不同,阅读指标日志时需注意kl的含义是叠加项而非权重内嵌项。

另外,REAL 分支不统计任何裁剪指标(grpo_trainer.py 中elif self.loss_type in ['sapo', 'real']: pass),因为分类损失本身没有 clip 机制——这也是"用有界权重替代裁剪"这一设计思想在工程上的自然体现。

训练脚本参考

仓库提供的完整参考脚本为 examples/train/grpo/internal/real.sh,采用 vLLM server 模式 rollout + 双卡全参训练 Qwen3-1.7B 的数学任务,核心片段如下(完整脚本含 rollout 服务启动部分):

CUDA_VISIBLE_DEVICES=2 \ swift rollout \ --model Qwen/Qwen3-1.7B NPROC_PER_NODE=2 \ CUDA_VISIBLE_DEVICES=0,1 \ swift rlhf \ --rlhf_type grpo \ --model Qwen/Qwen3-1.7B \ --dataset 'AI-MO/NuminaMath-TIR#5000' \ --enable_thinking false \ --reward_funcs accuracy \ --use_vllm true \ --vllm_mode server \ --vllm_server_host 127.0.0.1 \ --vllm_server_port 8000 \ --tuner_type full \ --torch_dtype bfloat16 \ --load_from_cache_file true \ --max_completion_length 4096 \ --num_train_epochs 1 \ --per_device_train_batch_size 8 \ --learning_rate 2e-6 \ --gradient_accumulation_steps 1 \ --save_total_limit 2 \ --save_steps 500 \ --logging_steps 1 \ --warmup_ratio 0.05 \ --dataloader_num_workers 4 \ --num_generations 8 \ --temperature 0.6 \ --system """You are a helpful math assistant. Solve the problem step by step and put your final answer within \\boxed{}.""" \ --log_completions true \ --num_iterations 1 \ --beta 0.001 \ --loss_type real \ --deepspeed zero2

几个与 REAL 直接相关的取值值得注意:

  • --loss_type real是开关,配合默认--real_tau 0.5(脚本未显式设置即使用默认值);
  • --num_generations 8--per_device_train_batch_size 8恰好整除,满足下述组完整性要求(该脚本中--tuner_type full与多卡并行下,每个训练 batch 内的样本数需能按 8 个一组完整切分);
  • --beta 0.001保留了一个较小的 KL 正则,对应源码中叠加的beta * kl_loss项;
  • --reward_funcs accuracy提供环境奖励:REAL 的正负标签划分依赖奖励符号,因此必须配置 reward function(对数学题即为答案正确性)。

重要注意事项与适用限制

batch 必须包含完整组

配置训练参数时必须保证:

  • per_device_train_batch_size能被num_generations整除。

这是组内分类正确性的硬性前提:源码中global_scores.view(-1, self.num_generations)依赖"展平后的样本数 = 组数 × 每组生成数"才能正确还原组结构(grpo_trainer.py)。若不能整除,组边界会被错位切分,正负标签与 logit 的对应关系全部错乱。

单侧组被跳过

如前述实现细节,组内奖励全正或全负时该组不贡献损失(valid_mask过滤)。当奖励信号稀疏、大量组只出现单一符号时,有效梯度会比标准 GRPO 少,此时可考虑调高num_generations增大组内方差,或检查奖励函数区分度。

不兼容特性速查

从源码校验逻辑(rlhf_args.py、rlhf_args.py、grpo_trainer.py)确认,loss_type=real与以下特性互斥,配置时会被直接拒绝:

  • --teacher_model/--teacher_model_server(OPD-RL 教师蒸馏);
  • --advantage_reweight rlsd
  • 任何依赖奖励组内归一化的设置(scale_rewards强制为'none')。

小结

REAL 用"奖励即标签"的组内二分类替换了 GRPO 的 advantage 估计与逐 token 加权:分类 logit $\bar{s}^k$ 只依赖新旧策略的序列级对数概率差,损失函数 $\log(1+\sum e^{\pm\bar s/\tau})$ 的梯度权重单调且上界为 $1/\tau$,从数学上同时消除了正样本梯度错配与负样本梯度主导。在 ms-swift 中,只需在swift rlhf --rlhf_type grpo基础上追加--loss_type real(可选--real_tau,默认 0.5),并注意 batch 组完整性与上述互斥约束,即可复现 examples/train/grpo/internal/real.sh 所示的完整训练流程;实现细节可进一步对照 swift/rlhf_trainers/grpo_trainer.py 与 swift/rlhf_trainers/args_mixin.py 深入阅读。

【免费下载链接】swiftUse PEFT or Full-parameter to CPT/SFT/DPO/GRPO 600+ LLMs (Qwen3.6, DeepSeek-V4, GLM-5.1, InternLM3, Llama4, ...) and 300+ MLLMs (Qwen3-VL, Qwen3-Omni, InternVL3.5, Ovis2.5, GLM4.5v, Gemma4, Llava, Phi4, ...) (AAAI 2025).项目地址: https://gitcode.com/GitHub_Trending/swift1/swift

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 15:00:42

LLMFit 实战:单卡消费级显卡的大模型轻量化微调全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:59:24

AI 编程提示词全攻略:10 个高效模板与实战解析

不知道你有没有过这种经历——同样是打开 Cursor 或者 Claude&#xff0c;别人一个小时搞定一个带数据库的完整功能&#xff0c;你折腾一下午还在跟 AI 来回拉扯&#xff1a;"不是这个意思""我说的是那个需求""你怎么又改了别的地方"。我早期用 …

作者头像 李华
网站建设 2026/9/14 14:55:50

编程题自动判分:基于加权Levenshtein距离的工程化实现

简介&#xff1a;这是一套基于SSM框架&#xff08;SpringSpringMVCMyBatis&#xff09;、JSP前端与MySQL数据库开发的在线考试系统&#xff0c;核心亮点在于集成Levenshtein Distance&#xff08;LD&#xff09;算法实现编程题自动判分&#xff0c;有效解决传统考试系统对代码类…

作者头像 李华