首页 / 文章

GRPO微调详解:教会本地大语言模型进行推理

GRPO放弃PPO的批评机制,通过群体比较进行学习。详细介绍五个核心量、奖励函数以及一段简短的Unsloth和TRL训练代码示例。

发布者:REACTAPP.TOP阅读约 11 分钟2471 词

来源: Creating Reasoning Models with GRPO Finetuning

像DeepSeek-R1这类注重推理能力的模型吸引了大量关注,现在许多人希望在自己的小型模型中也实现类似的功能。通常被认为相关的技术是GRPO微调。监督式微调(SFT)旨在让模型模仿示例中的模式;而群体相对策略优化(GRPO)则利用强化学习促使模型得出可行的答案,这更接近于教会它如何思考而非仅仅如何表达。

本文将阐述GRPO在RLHF方法中的定位、其五个核心参数的含义,以及这些参数如何转化为几十行训练代码。

GRPO在偏好调优方法中的位置

RLHF,即基于人类反馈的强化学习,更适合作为一个总称。PPO、GRPO和DPO都是不同的优化器,但目标一致:让模型符合人们的偏好。

用烹饪作类比有助于理解。RLHF就像通过品尝和调整来改进菜肴。PPO则是一种较为传统且资源消耗较大的方法,需要另一个模型(评估模型)以及大量的GPU内存。GRPO则是更新且更高效的方法,它省去了评估模型,通过相互比较答案来节省内存。

底层的强化学习循环

一般的强化学习是在循环中运行的:智能体观察状态、选择动作,环境随之变化,随后会有奖励出现并进入新的状态。当PPO(近端策略优化)被应用于语言模型时,语言模型便成了智能体,整个系统由三部分组成:

  1. 生成策略,即当前正在训练的模型
  2. 参考策略,为保持冻结状态的原始模型
  3. 价值模型,用于估算平均奖励值

奖励模型会对输出结果进行评分,训练的目标就是提升这一评分。

PPO采用演员-评论家设计模式,这意味着需要训练两个独立的网络。演员即LLM本身,而评论家则是规模相近的价值网络,用于预测某个响应应获得的奖励。使用这两种网络会使训练时的内存需求大致增加一倍。

GRPO没有评论家模块。由于省去了这一部分,其内存使用量大约减少一半。在基准测试中,GRPO会为同一提示生成多组答案,对每组答案进行评分,并以该组的平均分作为参考标准,来判断哪些答案优于其他答案。

DeepSeek在2024年的DeepSeekMath论文中提出了GRPO方法,该技术后来被应用于DeepSeek-R1模型中。

GRPO背后的五个核心概念

这一方法基于五个核心理念,每个理念都旨在阐明其衡量内容以及设计初衷。

1. 策略比率:模型的观点发生了多大变化?

该比率通过比较更新后模型给出某个答案的概率与之前模型给出的概率来计算:即针对提示x的新响应y概率除以旧概率。

抛开符号不谈,它实际上是在询问模型在当前训练步骤之后产生该答案的概率相比之前增加了还是减少了多少。

可以想象一个学生经过一夜复习前的状态与复习后的状态——有些答案现在看起来更显而易见,而有些则不然。该比率正是反映了这种信心程度的变化。

  • 数值大于1表示模型对该答案的信心比之前更高。
  • 数值小于1表示信心下降,模型正在逐渐偏离该答案。
  • 数值恰好为1表示此次更新对该答案没有产生任何影响。

这为何重要?在传统的强化学习中,人们可能会认为高奖励的答案应该获得更高的概率权重。但这样做存在风险,因为过度调整可能导致模型出现剧烈波动,仅仅因为某个答案运气好就放弃原有的良好行为模式。该比例起到了速度限制的作用,GRPO通过限制大幅波动,使模型能够逐步变化而非出现异常行为。

2. 平均奖励:这批数据的平均得分是多少?

平均值是指G个答案的奖励值之和除以G。G代表针对一个问题所抽取的响应数量,每个响应都会从奖励模型中获得一个得分。

假设你让模型解决一道数学题,并随机抽取五个答案作为样本。其中一个完全正确(奖励为1.0),另一个部分正确(奖励为0.5),还有一个明显错误(奖励为0.0)。平均值能反映模型当前解决该问题的水平。

正是这一机制让GRPO显得十分巧妙。PPO需要一个评估器,即另一个专门负责预测预期奖励的网络,而训练该网络既耗时又占用大量内存,还容易出错。GRPO则认为既然可以直接计算平均值,何必再去预测呢?通过随机抽取一组样本并计算实际奖励的平均值,就能免费获得基准结果,既不需要额外的网络,也不会浪费内存。方法名称中的“group”指的就是这一组样本。

3. 标准差:分数的分布范围有多广?

标准差用于衡量每个奖励值与平均值的偏离程度,其计算方式为平均平方差的平方根。如果平均值代表典型表现,那么标准差则反映一致性。

较高的标准差意味着响应结果差异极大,有的非常好,有的非常差。较低的标准差则表示模型每次都能生成质量相近的答案。

以两名学生为例:学生A的成绩分别为90、10、95、5和88,平均分为57.6,成绩波动极大,一致性极差;而学生B的成绩分别为58、56、60、57和59,平均分为58,成绩波动很小,表现始终处于中等水平。

在GRPO中,标准差起到归一化作用。当响应结果差异较大时,奖励值之间的差距包含有效信息,因此需要更大的更新幅度;而当模型已经表现稳定时,较小的奖励差距可能只是噪声,此时更新时应更加谨慎。

4. 相对群体优势:该答案是否优于其他答案?

这是该方法的核心。答案i的优势值为其奖励减去群体均值,再除以群体的标准差。这样就能将原始奖励转化为用于驱动学习的相对分数。

减法部分用来表示该答案比平均水平好多少或差多少。除法则是通过标准差进行缩放,从而使不同问题及不同训练步骤中的分数具有可比性。

想象一下在一场难度极高的考试中,全班平均分为100分中的40分。如果有学生得了60分,从绝对分数来看属于中等水平,但相对于全班而言则比平均分高20分,这已经相当出色了。这一优势机制的原理也是如此,它不考虑奖励的绝对值,而是判断该答案是否优于模型通常针对同一提示生成的答案。

有三个特性使得这一方法十分有用。

  1. 无需评价函数。PPO的优势在于依赖评价函数来估算预期奖励,而GRPO则用群体内的经验平均值取代这种猜测。
  2. 自我校准。如果奖励模型存在偏差,导致所有事物的评分都过高或过低,相对排序机制就能抵消这种偏差,只有群体内的排名才具有意义。
  3. 稳定性。由于进行了归一化处理,学习速度会随之调整:当模型处于广泛探索阶段时,更新幅度较大;而当模型逐渐收敛时,更新幅度则会变小。

5. KL惩罚:保持自身的独特性

这就是安全制动机制。虽然GRPO会促使模型生成高奖励的答案,但KL项则起到相反的作用,防止模型变成另一个人格。它将当前模型与冻结的参考模型进行比较,该参考模型通常是强化学习开始之前的基础模型。KL散度用于衡量两个概率分布之间的差异程度。文中给出的公式是一种常用的基于参考概率与当前概率比值来估算每个标记的散度值的方法。

可以把基础模型想象成一位博学多才的通才,能够写诗、调试代码以及总结新闻。现在我们通过强化学习让它变成数学天才。如果没有KL项,它可能会过度专注于数学方面的奖励,以至于忘记其他所有能力,甚至会对无关的提示生成带有数学风格的胡言乱语。

KL值较低意味着模型仍保持其原始特性;KL值较高则表示模型已经偏离太多,可能是过度拟合了奖励模型,或是丧失了通用能力。

奖励模型并不完美,且可能被利用。没有KL项的模型可能会发现,即使没有任何实际作用,使用“让我一步步思考”作为开头也能获得额外奖励。这种对参考答案的依赖实际上属于一种奖励操纵手段。在实践中,KL项会以较小的权重加入损失函数中,大约在0.01到0.04之间。它的存在并非为了阻碍进展,而是为了保持进展的平衡。

用通俗语言解释完整更新过程

  1. 针对一个问题生成一组答案。
  2. 用奖励模型为每个答案打分。
  3. 计算这些分数的平均值和标准差。
  4. 确定每个答案的优劣势,即它比平均值高或低多少。
  • 查看策略比率,了解每个答案的置信度发生了多大变化。
  • 限制大幅波动,防止模型出现剧烈震荡。
  • 通过KL项对偏离参考值的情形进行惩罚。
  • 更新模型,使其更倾向于选择具有较高相对优势的答案。
  • 最终得到的模型能够通过推理来学习,而非单纯记忆正确答案,而是找出那些能持续超越其平均水平的思维模式。

    从方程式到 trainer.train()

    在介绍了相关数学原理后,下面是实现这些原理的代码。该代码使用Unsloth加载基于vLLM后端的4位Llama 3.2 3B Instruct模型,附加LoRA适配器,通过系统提示强制要求使用特定推理格式,并从GSM8K数据集中构建训练集。选择vLLM是因为GRPO需要快速为每个提示生成大量完整内容。

    from unsloth import FastLanguageModel
    from datasets import load_dataset
    import re
    
    # 1. Load 4-bit model with vLLM backend for fast group generation
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name="unsloth/Llama-3.2-3B-Instruct",
        max_seq_length=1024,
        load_in_4bit=True,
        fast_inference=True,
        max_lora_rank=32,
        gpu_memory_utilization=0.6,
    )
    
    # 2. Attach LoRA adapters
    model = FastLanguageModel.get_peft_model(
        model,
        r=32,
        target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
        lora_alpha=64,
        use_gradient_checkpointing="unsloth",
    )
    
    # 3. Enforcing reasoning format - GRPO needs structure to reward
    SYSTEM_PROMPT = "Respond in the following format:\n<reasoning>\n...\n</reasoning>\n<answer>\n...\n</answer>"
    
    # 4. Load GSM8K and wrap prompts
    dataset = load_dataset("openai/gsm8k", "main")["train"]
    dataset = dataset.map(lambda x: {
        "prompt": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": x["question"]}
        ],
        "answer": x["answer"].split("####")[1].strip()
    })
    #vLLM is used in GRPO because GRPO requires the AI to generate a ma
    

    奖励函数

    奖励设计是GRPO微调中最棘手的部分。如果设计不当,模型会学会利用评分规则来投机取巧,而非进行合理推理。各个个体的得分会被汇总为原始奖励,进而用于计算优势值。下面列出了五种函数,每种都有其存在的理由。

    1. correctness_reward是主要信号。它会提取答案标签内的文本,若与真实答案一致则给予2.0分,否则为0.0分。该权重被设定得较大,以确保正确性始终比格式美观更重要。
    2. int_reward起到安全保障的作用。在训练初期几乎所有结果都是错误的,因此只要输出有效数字就会给予0.5分,这样就能在模型开始解决问题之前持续提供一定的学习信号。
  • soft_format_reward和strict_format_reward会分别以0.5的分数奖励采用“先推理再作答”标签结构的输出。前者能容忍布局上的差异,而后者则要求元素位置完全精确。二者共同作用,使输出从杂乱变得整洁。
  • xmlcount_reward会为每个位于正确位置的标签给予0.125的分数,若出现重复标签或答案后有多余文字则会扣分。这些虽是细微的调整,但累积起来效果显著。
  • 单独来看,每个功能都很简单。综合起来,正确性是核心,格式规范起到引导作用,细节问题则由相应机制自行处理。

    def extract_xml_answer(text: str) -> str:
        return text.split("<answer>")[-1].split("</answer>")[0].strip()
    
    # Primary signal: is the final answer correct?
    def correctness_reward(prompts, completions, answer, **kwargs):
        responses = [c[0]["content"] for c in completions]
        extracted = [extract_xml_answer(r) for r in responses]
        return [2.0 if r == a else 0.0 for r, a in zip(extracted, answer)]
    
    # Dense shaping rewards - keep learning signal alive even when answers are wrong
    def int_reward(completions, **kwargs):
        return [0.5 if extract_xml_answer(c[0]["content"]).isdigit() else 0.0 for c in completions]
    
    def strict_format_reward(completions, **kwargs):
        pattern = r"^<reasoning>\n.*?\n</reasoning>\n<answer>\n.*?\n</answer>\nquot;
        return [0.5 if re.match(pattern, c[0]["content"]) else 0.0 for c in completions]
    
    def soft_format_reward(completions, **kwargs):
        pattern = r"<reasoning>.*?</reasoning>\s*<answer>.*?</answer>"
        return [0.5 if re.match(pattern, c[0]["content"]) else 0.0 for c in completions]
    
    def xmlcount_reward(completions, **kwargs):
        def score(text):
            count = 0.0
            if text.count("<reasoning>\n") == 1: count += 0.125
            if text.count("\n</reasoning>\n") == 1: count += 0.125
            if text.count("\n<answer>\n") == 1:
                count += 0.125
                count -= len(text.split("\n</answer>\n")[-1]) * 0.001
            if text.count("\n</answer>") == 1:
                count += 0.125
                count -= (len(text.split("\n</answer>")[-1]) - 1) * 0.001
            return count
        return [score(c[0]["content"]) for c in completions]pyt
    

    教练

    from trl import GRPOConfig, GRPOTrainer
    
    trainer = GRPOTrainer(
        model=model,
        processing_class=tokenizer,
        reward_funcs=[
            xmlcount_reward,      # tag quality
            soft_format_reward,   # structure (forgiving)
            strict_format_reward, # structure (exact)
            int_reward,           # partial credit
            correctness_reward,   # ground-truth match
        ],
        args=GRPOConfig(
            learning_rate=5e-6,
            per_device_train_batch_size=1,
            gradient_accumulation_steps=1,
            num_generations=8,          # G = group size
            max_prompt_length=256,
            max_completion_length=200,
            max_steps=250,
            warmup_ratio=0.1,
            max_grad_norm=0.1,
            output_dir="outputs",
        ),
        train_dataset=dataset,
    )
    
    trainer.train()
    

    在每一步中,GRPOTrainer都会执行上述五种计算。

    为何DPO在推理方面存在困难

    DPO之所以高效,是因为它完全避免了强化学习。它将问题转化为分类任务:给定一个提示和两个答案,学会偏好其中更好的那个。当偏好标签准确时,这种方法效果很好。

    然而,推理实际上并非偏好问题,而是一个搜索问题。

    假设你希望模型证明一个几何定理。DPO需要人类或更强大的模型来判定两种证明中哪种更好。但如果两者都是错误的呢?如果正确的证明需要12步才能完成且从未被采样到呢?DPO只能从给定的配对数据中学习,无法发现新的推理策略。

    GRPO会为同一提示生成多组答案,对每组进行评分并找出高于平均分的答案。这意味着它会在不同思路中探索,可能会发现某种此前无人采用过的推理路径,给其较高评分并加以强化。

    简而言之,DPO的运行成本更低,但会限制模型的表现仅取决于其偏好数据的质量;而GRPO虽然每步成本更高,却能超越人工标注的数据范围,实现真正新颖的推理。

    总结

    GRPO通过让模型学习哪些自身答案优于其平均表现来教会它推理。去除评价机制可以减少内存占用,同时保留学习信号;再结合合理的奖励函数与群体采样方法,就能在单块GPU上提升本地模型的推理能力。只需对期望的行为给予奖励,让模型保持稳定,并持续关注KL值即可。