首页 > 人工智能 > 自然语言 > GRPO如何通过可验证奖励训练小型语言模型
语言模型可以写出“让我再检查一下”,但仍然算错乘法。它甚至可以写出“等等,让我重新考虑”,然后得出另一个错误答案。这两句话都不是思考的证据。如果目标是解决问题,只有最后的数字才算数。
DeepSeek 的 R1-Zero 让没有初步监督微调阶段的强化学习受到了相当多的关注。其研究人员报告了一些行为,例如重新审视某种方法,以及在困难问题上花费更多 token。相比之下,DeepSeek-R1 使用了更广泛的训练流程,包括冷启动数据,因此我们不应将这两个训练过程视为可以互换。
我对 Group Relative Policy Optimization,也就是我们大多数人所熟知的 GRPO,最感兴趣的地方在于,基本设置所需的反馈竟然如此之少。
模型可以针对一个问题生成多次尝试,为每次尝试获得一个分数,并利用这些差异来调整其行为。我们甚至可以只检查最终答案,而不必写出希望它模仿的解法。结合减少训练内存的技术,这使得更小规模的推理实验更容易开展。
这样一来,在选择模型或 GPU(图形处理器) 之前,有两件事值得理解:这些分数如何指导更新,以及当评分规则奖励了错误的东西时会发生什么。
一个简单的算术例子有助于解释这两点。考虑对同一个问题的四种可能回答,以及奖励函数会如何为每一种打分。
考虑这个问题:
一家商店有 6 个盒子,每个盒子里有 8 件物品。它卖出了 6 件物品。还剩多少件物品?
答案是 42。一个 Python 表达式可以验证它:
boxes, items_per_box, sold = 6, 8, 6 expected_count = boxes * items_per_box - sold assert expected_count == 42
现在,这个简单的检查给了我们一些有价值的东西:一种独立地为最终答案打分的方法。我们不需要另一个语言模型来判断这个回答听起来是否有说服力。
在训练数据集中,问题被送入模型,而预期答案则保留在验证器(verifier)中。模型生成一个回答,验证器对其打分,训练算法利用该分数来调整模型的行为。
因此,我们可以使用问答对而不必提供详细的解题过程作为监督目标。这正是带可验证奖励的强化学习(reinforcement learning with verifiable rewards)的吸引力所在:反馈可以来自对结果的检查。
对于真实的库存系统而言,普通的算术运算仍然是合理的解决方案。语言模型会是一个昂贵且带有主观意见的计算器。在这里,这个例子让学习机制易于观察。
这也暴露了我们验证器的不足之处,因为检查最终整数是否正确并不能告诉我们其背后的推理是否合理。
GRPO 在 DeepSeekMath 工作中被提出,作为传统基于 PPO 的训练方法内存需求的一种替代方案。
常见的 PPO actor-critic 实现会与策略一起训练一个价值估计器,通常称为 critic。该估计器为评判某个动作的结果提供了一个基线。
GRPO 从针对同一提示(prompt)采样的一组回答中获取其基线。它不是让一个单独的 critic 来估计模型应该表现得多好,而是比较多次尝试实际表现如何。
假设模型产生了以下四个答案:
| 尝试 | 最终答案 | 正确性奖励 | | --- | --- | --- | | 1 | 42 | 1 | | 2 | 40 | 0 | | 3 | 42 | 1 | | 4 | 48 | 0 |
该组的平均奖励为 0.5。尝试 1 和 3 的表现优于平均水平;尝试 2 和 4 的表现则更差。
原始的结果奖励公式对这一比较进行了标准化:
这里,( r i ) (r_i) ( r i ) 是一次尝试的奖励,( r ˉ ) (\bar{r}) ( r ˉ ) 是组均值,( σ r ) (\sigma_r) ( σ r ) 是该组奖励的标准差。小的 ( ϵ ) (\epsilon) ( ϵ ) 为分母提供数值保护。这个量被称为优势(advantage)。
对于这些二元奖励,我们可以显式处理没有变化的组,否则除以标准差:
from statistics import mean, pstdev rewards = [1.0, 0.0, 1.0, 0.0] group_mean = mean(rewards) group_std = pstdev(rewards) # Equal rewards give us nothing to rank within this group. if group_std == 0: advantages = [0.0] * len(rewards) else: advantages = [(r - group_mean) / group_std for r in rewards] print(advantages) # [1.0, -1.0, 1.0, -1.0]
这个例子假设一个非空组具有有限的二元奖励。它使用总体标准差,并在非零分支中省略 epsilon,因此它说明的是比较过程,而不是精确复现训练器的归一化。库实现可能使用不同的估计器或归一化选项,从而改变数值尺度,但不改变在组内比较各次尝试的基本思想。
优化器使用这些优势来调整采样响应的概率,相对于低奖励尝试更偏好高奖励尝试。在基于结果的训练中,该信号作用于整个响应;它并不能识别导致成功的具体算术步骤。
这为我们提供了一种比较各次尝试的方法,但老实说,仍然存在一个问题:这些分数如何改变模型。
实际训练涉及 token 概率以及控制模型如何变化的策略目标。裁剪(clipping),以及在某些配置中,对偏离参考策略的惩罚,有助于调节这些更新。
上面的 Python 计算展示了我们如何得到优势;优化器处理接下来发生的事情。
如果一组中的每个答案都获得相同的奖励,无论是全为零还是全为一,减去均值后每次尝试的优势都为零:奖励没有差异来告诉模型应该偏好哪次尝试。这使得任务难度成为一个实际考虑因素。
如果模型从未成功,稀疏的正确性奖励可能提供不了多少有用方向。如果它已经持续成功,该任务提供的改进空间就很小。组内必须足够频繁地包含有信息量的差异,学习才能受益。
假设只要响应中出现字符串 42,我们就给予奖励。那将接受:
我最初考虑了 42,但我的最终答案是 40。
验证器在训练开始之前就会出错。
在本示例中,要求只有一个最终答案块,且其后没有文本。然后提取其整数值:
import re
# Keep integer conversion bounded; 12 digits is ample for this toy task.
FINAL_INTEGER = re.compile(
r"\s*([+-]?[0-9]{1,12})\s*\s*\Z"
)
def extract_final_count(response: str) -> int | None:
# Don't let a response submit several answers and win on the last one.
if response.count("") != 1 or response.count("") != 1:
return None
match = FINAL_INTEGER.search(response)
if match is None:
return None
return int(match.group(1))
def correctness_reward(response: str, expected_count: int) -> float:
count = extract_final_count(response)
if count is None:
return 0.0
return float(count == expected_count)
assert correctness_reward("48 - 6 = 42.42", 42) == 1.0
assert correctness_reward("I considered 42.40", 42) == 0.0
assert correctness_reward("4042", 42) == 0.0
assert correctness_reward("0", 0) == 1.0解析器接受周围的空白字符、符号和前导零。对于无效答案,它返回 None,从而将有效的零与解析失败区分开来。
它拒绝重复的答案块、尾随文本、小数和逗号分隔的数字。这适用于这个构造的整数任务,但对于通用的数学答案检查器来说过于狭窄。
分数是一或零,但其背后的决策并不那么整洁。我们的解析器拒绝 42.0,即使它表示相同的数量。
不同的任务可能需要单位,或接受等值分数。
这些决策属于验证器,因为它们决定了训练将偏好哪些响应。
我们的简单奖励结合了两件事:算术正确和遵守输出格式。模型可以仅通过学习所需的标签来提高分数,即使其算术几乎没有变化,这就是为什么评估应单独跟踪格式错误的响应。
验证器故意不为更长的答案或诸如“等等”和“让我重新考虑”之类的短语给予额外加分。奖励这些特征会鼓励模型使用它们,而无法证明它们能改善问题解决。
GRPO 本身不需要确定性规则。TRL 支持奖励模型以及自定义奖励函数。
根据我的经验,当任务允许时,规则很有吸引力,因为它们使评分过程可检查。对于开放式解释,正确性较难压缩为可靠的自动检查。
我会在花时间配置训练之前先解决这些决策。否则,就很难判断奖励上升意味着模型解决了更多问题,还是仅仅更擅长满足检查器。
对我来说,实际问题是这能在本地机器上容纳多少。
移除学习到的 critic 可以减少内存使用,当我们同时限制哪些参数被更新并更高效地存储模型权重时,本地训练变得更加可控。
LoRA 使用低秩矩阵表示权重更新,减少了需要梯度和优化器状态的参数数量。
QLoRA 将适配器训练与四位骨干网络以及额外的内存节省技术相结合。两者都没有消除在训练期间生成响应或处理响应的成本。
Unsloth 将这些想法带入实际工作流程,包括基于 GRPO 的强化学习。其强化学习文档还涵盖了与 vLLM 的集成以进行 rollout 生成。
这些部分是互补的:GRPO 是学习算法,适配器定义了可训练参数,而软件栈决定了操作运行的效率。
像 Qwen2.5-1.5B-Instruct 这样的小模型说明了人们可能开始探索的规模。其模型卡列出了大约 15.4 亿个参数和 Apache 2.0 许可证。由于它经过指令微调,它已经携带了在任何新的 GRPO 运行之前学到的行为。任何改进都可以轻松地在此基础上构建。
我不会仅凭参数数量来判断模型是否能装在我的 GPU 上。以每个参数四位计算,15 亿个参数大约占用 0.75 GB,但这在理想化计算中只涵盖了权重。
GPU 还需要空间用于量化元数据、保持更高精度的组件、适配器、激活、生成缓存和临时缓冲区。
GRPO 还会为每个提示采样多个响应。更长的响应和更多的并发生成会增加工作负载。
使用相同模型的两个实验可能有非常不同的内存需求,因为它们的 rollout 设置不同。
因此,当你看到已发布的 VRAM 数值时,请检查它指的是哪个模型和训练设置,包括软件版本。规模相近的模型在同一块 GPU 上可能需要更多内存,而硬件兼容性取决于你使用的工具。Unsloth 安装指南是检查你的配置是否受支持的有用参考。
当回答变得更长并包含自我纠正时,推理是一个有吸引力的解释。但它并不是唯一可能的解释。
考察 R1-Zero 风格训练的研究发现,一些与“顿悟时刻”相关的行为在初始模型中就已经存在。
同一项工作还识别出了 GRPO 原始公式中的偏差,包括对回答长度的影响,并提出了 Dr. GRPO 来解决这些问题。
这改变了我们在一次训练运行中应该关注什么。模型可能是在进行更有用的计算、学习一种偏好的回答风格,或者对某种优化偏差做出反应。仅凭几个令人印象深刻的例子不足以区分这些可能性。
这也解释了为什么两个都标为“GRPO”的教程可能使用不同的设置。
损失归一化、奖励缩放和参考策略惩罚都是重要的实现选择。比较时应该记录它们,而不是假设算法名称已经完全描述了实验。
对于一次奖励有所提升的训练运行,第一个站得住脚的观察相当具体:在该任务和推理预算下,模型变得更可能产生验证器接受的答案。
这种提升能否在新问题上持续有效,仍然需要我们进一步测试。
我希望看到的比较是:原始模型和训练后的版本回答相同的未见问题,使用相同的指令、采样设置和 token 预算,这样我们才能判断训练是否真正带来了差异。
对于算术示例,衡量所有测试问题的准确率,并单独跟踪格式错误,将其计为总体评分中的失败。
这有助于区分是算术能力更好,还是对答案格式的遵循更好。别忘了,响应长度也很重要,因为需要两倍输出的改进会带来不同的成本。
结论应贴近实际测试的内容。
在新库存问题上获得更高的准确率是有用的证据,表明模型在该任务上有所改进,但这并不能证明其具备通用推理能力。
相比那些听起来像在更努力思考的解释,模型答对更多题目会让我更信服。
GRPO 使语言模型强化学习的一部分变得更简单。它通过比较对同一提示的响应来推导学习信号,并避免单独训练一个评论者模型。
适配器训练和量化可以减少其他部分的内存负担,使更小规模的实验更容易开展。
困难的判断在于决定这些实验应该奖励什么。
评分应反映你真正关心的行为,而留出评估则检查模型能否做到不仅仅是满足评分规则。
对于算术,验证器可以是一次计算。
对于生成的程序,验证器可能是在隔离环境中执行的测试。
然后,对于开放式解释,构建一个可信的检查器可能是项目中最大的部分。
在选择模型或 GPU 之前,我会写下一个问题、一个可接受的答案,以及一个验证器应当拒绝的看似合理的回答。
这个小练习会暴露出训练配置无法修复的假设。
GRPO 为这些判断提供了一种影响模型的方式。结果是否有用仍然取决于我们选择奖励什么,以及之后我们检查了什么。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Towards Data Science
原创地址:https://towardsdatascience.com/how-grpo-trains-small-language-models-with-verifiable-rewards/
原文作者:Benjamin Nweke;如有侵权,请联系管理员删除。