推理模型的训练:从原理到实践

我们采用了一个微小的 0.5B 参数模型,在我们的家庭实验室中向它投入了一些 GRPO,并设法教会它一些相当不错的推理技能。

推理模型的训练:从原理到实践

推理模型并不完全是新事物,但 DeepSeek 的 R1 模型的发布在开源社区中引起了极大的兴奋。在阅读 DeepSeek-R1 论文后,我对他们的后训练技术特别感兴趣,据报道,该技术可以显着提高性能。

传统的后训练方法是监督微调 (SFT)。在 SFT 中,为模型提供提示和期望的输出,以了解用户偏好、结构对齐、安全协议等。本质上,SFT 展示了开发人员或用户对模型的期望,并引导它与这些示例保持一致。

既然已经提到了后训练,那么在深入研究 R1 的后训练方法之前,值得简要讨论一下预训练,因为两者是相关的。训练大型语言模型 (LLM) 的初始步骤之一是因果语言建模。在此阶段,模型通过关注前面的标记来学习预测序列中的下一个标记。此过程产生“基础”模型,这些模型以半监督的方式在大量公共文本数据上进行训练,没有明确的标签,以预测后续标记。说到公共文本数据,我们即将耗尽它(在某种程度上,在这种情况下忽略其他模式)。这种稀缺性是研究人员专注于 LLM 后训练阶段的主要驱动力之一。

回到后训练,存在许多技术,例如 SFT、PPO 和 DPO 等。虽然某些方法在特定领域表现更好,但它们通常会限制指令对齐的创作自由,或者在长时间运行中变得过于昂贵。相反,基础模型没有这些限制,但不可预测、难以指导,并且经常产生人类难以理解的输出。

R1 的作者发现了一种两全其美的方法:将原始模型与指令对齐,同时保留一定程度的创作自由。我不会在这里详细介绍整个过程,但从根本上讲,它们允许基础模型以相当大的自由度“推理”,同时设定专注于准确性和格式的简单目标。为了实现这一点,他们采用了强化学习,实施了一种称为“GRPO”(组相对策略优化)的优化技术。 GRPO 消除了对批评模型(传统上与策略模型大小相同并用于 PPO/DPO 等方法)的需求,而是使用聚合组分数计算基线。

将这种方法直接应用于基础模型产生了强大的推理能力,从而产生了 R1 模型的第一个版本:DeepSeek-R1-Zero。尽管该模型提高了基础模型的推理能力,但其输出通常难以被人类阅读或解释。如前所述,SFT 方法有助于模型遵循人类的偏好和可读性。然而,我们还讨论了 SFT 如何扼杀创作自由和泛化,如论文“SFT 记忆,RL 泛化:基础模型后训练的比较研究”中所述。

来源

因此,作者在强化学习步骤之前使用了一个小型、高质量的数据集来“冷启动”他们的模型。这种方法在著名的 DeepSeek-R1 模型的创建中发挥了重要作用。在下一章中,我们将探讨其中一些技术的实现,以实现类似的结果。

1、强制推理

在他们的报告中,DeepSeek 提到了一组新的蒸馏模型。这些明显较小的模型使用 SFT 的蒸馏技术进行训练,利用了 DeepSeek-R1 的输出。值得注意的是,这些提炼模型没有经过任何强化学习步骤,这是一个有趣的观点,也是我们值得探索的领域。因此,如果我们不提炼模型,而是直接在原始模型上实现 GRPO,会怎么样?

我们可以继续使用基础模型或指导模型,假设指导模型已经“冷启动”。鉴于这是在我配备 RTX 3090 的本地游戏装备上进行的实验,我选择了 Qwen2.5–0.5B-Instruct 模型作为开始。对于 LLM 来说,这是一个参数数量非常少的模型,虽然它的局限性尚不确定,但值得尝试。

2、设置奖励

在原始论文中,作者使用了一个具有 671B 个参数的大型混合专家 (MoE) 模型。这种相当大的模型规模使他们能够为模型提供相当大的自由度,促进自由、稳健性和创造力。然而,就我而言,使用 0.5B 参数模型(是的,你没看错!),我感觉有必要实施更严格的规则来有效地指导模型。原始的 DeepSeek-R1 模型采用了两种主要的奖励来建模奖励:准确性和格式。这两个奖励旨在评估最终响应的正确性以及格式是否包含  和  标签内的推理步骤。

训练奖励图表 

在我更受限制的场景中,我加入了一套更广泛的规则来奖励或惩罚模型:

  • 答案是否正确?
  • 它是否遵循建议的格式?
  • 它是否包含推理标签?
  • 如果存在推理标签,思考过程的长度是多少?
  • 它是否包含验证标签?
  • 如果存在验证标签,自我批评的长度是多少?
  • 它是否使用“啊哈!”时刻标记?

我认识到其中一些奖励是主观的,可能不具有广泛的普遍性。但是,正如所提到的,我相信其中几个对于较小的模型和促进更快的训练是必不可少的。为代码中真正的任意奖励点做好准备!此外,考虑我们正在使用的数据集也很重要,因为其中一些规则是根据其特定特征量身定制的。数据集是 GSM8K(小学数学 8K),它包含语言多样的小学数学应用题。该数据集专门用于支持需要多步推理的基本数学问题的问答任务。因此,其中一些规则是特定于此数据集的任务,可能无法很好地概括。不过,请允许我解释一下这些奖励选择背后的一些理由。

  • 对于准确性奖励,我们将提取 <answer></answer> 标签之间的最终答案作为预测,并使用基本事实答案检查它们的准确性。
  • 格式化部分是检查推理、批评和答案部分的标签是否遵循一般结构。

这两个与原始论文相似,但其余的取决于你的任务和创造力。

为了鼓励更长的思考过程,我计算了推理和验证标签之间的单词数量,并将其作为奖励添加到一定范围内。(以防止模型利用无用的长答案)

原始作品提到“啊哈!”时刻在推理过程中自然发生。虽然更大的模型和更长的训练程序也有可能出现这种情况,但在我的场景中,我设置了一个俗气的游戏规则来强制这一时刻。

在最近的一篇论文 《s1:简单的测试时间缩放中》,作者在模型准备结束预测序列时插入了一个“等待”标记,以迫使它们重新思考。这听起来很像“啊哈!”时刻,所以我就把这个想法插入到我的奖励函数中。

来源

基本上,如果有一个“啊哈!”时刻,比如“等待,但或重新思考”,然后是最少的字符,我会设置巨大的奖励来鼓励这种思考,并将其设置为系统提示。

3、剧透警告!

以下是训练集预测的一个例子:

好吧,看看这个输出,似乎模型正在尝试遵循我建立的规则。尽管它利用了一些方面(这是预料之中的),但它似乎是一个有前途的推理模型,并带有一个自我发起的“等等,让我们重新思考”的时刻。

4、代码时间

好的,我认为我们已经涵盖了足够的理论背景。现在,让我们深入研究实际的代码实现。我们将首先为几个关键组件创建实用函数:设置系统提示、从数据集中提取答案、提取指定标签之间的内容以及加载 GSM8K 数据集本身。

import re
import torch
from datasets import load_dataset, Dataset
from trl import GRPOConfig, GRPOTrainer

from nltk.tokenize import word_tokenize

SYSTEM_PROMPT = """
Respond in the following format:
<reasoning>
[Break and reason your answer here]
</reasoning>
<validate>
[Criticize your reason and think about it. If you see somethin start with 'wait']
</validate>
<answer>
[Final integer answer justified by validate]
</answer>
"""

def extract_hash_answer(text: str) -> str | None:
    if "####" not in text:
        return None
    return text.split("####")[1].strip()

def extract_xml_answer(response, tag="answer"):
    import re
    match = re.search(rf'<{tag}>(.*?)</{tag}>', response, re.DOTALL)
    if match:
        return match.group(1).strip()
    return ""

def get_gsm8k_questions(split = "train") -> Dataset:
    data = load_dataset('openai/gsm8k', 'main')[split]
    data = data.map(lambda x: {
        'prompt': [
            {'role': 'system', 'content': SYSTEM_PROMPT},
            {'role': 'user', 'content': x['question']}
        ],
        'answer': extract_hash_answer(x['answer'])
    })
    return data

dataset = get_gsm8k_questions()

接下来,我们将实现最重要的部分之一:奖励函数。我根据要点创建了它。我在上一章中提到过,但你可以随意修改或编写自己的代码。

def custom_reward_func(prompts, completions, answer, min_reasoning_length=10, **kwargs) -> list[float]:
    responses = [completion[0]['content'] for completion in completions]
    q = prompts[0][-1]['content']
    extracted_responses_answer = [extract_xml_answer(r, tag="answer") for r in responses]
    extracted_responses_reasoning = [extract_xml_answer(r, tag="reasoning") for r in responses]
    extracted_responses_validate = [extract_xml_answer(r, tag="validate") for r in responses]

    rewards = []
    for original_response, extracted_answer, extracted_reasoning, extracted_validate in zip(
        responses, extracted_responses_answer, extracted_responses_reasoning, extracted_responses_validate
    ):
        is_correct = (extracted_answer == answer[0])
        is_int = extracted_answer.isdigit()
        has_answer_tags = "<answer>" in original_response and "</answer>" in original_response
        has_reasoning_tags = "<reasoning>" in original_response and "</reasoning>" in original_response
        has_validate_tags = "<validate>" in original_response and "</validate>" in original_response
        reasoning_length = len(word_tokenize(extracted_reasoning.lower()))
        validate_length = len(word_tokenize(extracted_validate.lower()))

        reward = 0.0
        reasoning_reward = 0.0

        if is_correct:
            reward += 5.0
        if is_int:
            reward += 0.5

        if has_validate_tags:
            reward *= 1.25
            if validate_length >= 5:
                min_validate_length = 5
                max_validate_length = 256
                max_validate_bonus = 3.0
                if validate_length >= min_validate_length:
                    if validate_length >= max_validate_length:
                        validate_bonus = max_validate_bonus
                    else:
                        validate_bonus = ((validate_length - min_validate_length) / (max_validate_length - min_validate_length)) * max_validate_bonus
                else:
                    validate_bonus = 0.0
            else:
                validate_bonus = 0.0
        else:
            validate_bonus = 0.0

        if has_reasoning_tags:
            reward *= 1.25
            if reasoning_length >= 5:
                min_scaling_length = 5
                max_scaling_length = 1024
                max_scaling_bonus = 10
                if reasoning_length <= min_scaling_length:
                    reasoning_reward = 0.0
                elif reasoning_length >= max_scaling_length:
                    reasoning_reward = 5.0
                else:
                    reasoning_reward = ((reasoning_length - min_scaling_length) / (max_scaling_length - min_scaling_length)) * max_scaling_bonus
            else:
                reasoning_reward = 0.0
        else:
            reasoning_reward = 0.0

        total_reward = reward + reasoning_reward + validate_bonus

        if has_validate_tags:
            validate_lower = extracted_validate.lower()
            if re.search(r"(wait|but|rethink)(?=.{20,})", validate_lower, re.DOTALL):
                total_reward *= 10.0

        rewards.append(total_reward)

    return rewards

好的,我们完成了实用函数和奖励函数。现在,让我们进入训练脚本。为了在有限的计算能力下进行高效、快速的训练,我们将使用一些库和技术,例如 Unsloth、trl、vLLM 和 LoRA 等。

我不会在本文中深入介绍这些内容,但会提到它们在此流程中的作用。

基本上,Unsloth 允许优化的模型以更小的内存占用和更快的迭代进行训练。

from unsloth import FastLanguageModel, PatchFastRL
PatchFastRL("GRPO", FastLanguageModel)


max_seq_length = 1024
lora_rank = 32

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen2.5-0.5B-Instruct-unsloth-bnb-4bit",
    max_seq_length = max_seq_length,
    load_in_4bit = False,
    fast_inference = True, # Enable vLLM fast inference
    max_lora_rank = lora_rank,
    gpu_memory_utilization = 0.8,
)

model = FastLanguageModel.get_peft_model(
    model,
    r = lora_rank,
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"],
    lora_alpha = lora_rank*2,
    use_gradient_checkpointing = "unsloth",
    random_state = 1907,
)

通过应用 LoRA,我们正在训练一小部分模型参数。然后我们设置 trl 训练参数和训练器本身。

output_dir="outputs/Qwen-.5B-GRPO"

training_args = GRPOConfig(
    output_dir=output_dir,
    run_name=run_name,
    learning_rate=3e-5,
    adam_beta1 = 0.9,
    adam_beta2 = 0.99,
    weight_decay = 0.1,
    warmup_steps=5,
    lr_scheduler_type='cosine',
    logging_steps=1,
    bf16=True,
    optim="paged_adamw_8bit",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=32,
    num_generations=2,
    max_prompt_length=256,
    max_completion_length=512,
    num_train_epochs=1,
    save_steps=100,
    max_grad_norm=1.0,
    report_to="none",
    log_on_each_node=False,
    use_vllm=True,
)

trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    reward_funcs=[custom_reward_func],
    args=training_args,
    train_dataset=dataset,
)
trainer.train()

让奇迹发生吧!这些超参数可能远非最佳(其他组件也是如此),但我相信开源社区很快就会找到更好的方法。(我目前的设置是基于 GRPO Llama-1B 构建的)。

完成长度图表

如上所示,该模型通过延长其推理和验证步骤不断增加其输出。但在某个时候,它停止了增长,原因如下:

  • 缺乏 max_sequence 限制:随着推理步骤变长,模型开始削减最终答案,因此无法获得重要的准确性奖励。(我在训练器中设置的限制)
  • 缺乏模型能力:这听起来可能像是一个借口,但对于 0.5B 参数模型来说,这确实很重要。
  • 奖励函数推理长度的上限。

5、评估时间!

如果你认为我会在不评估测试集结果的情况下得出结论,那你就错了,哈哈!幸运的是,GSM8K 包含一个测试拆分,使我们能够评估结果并进行比较。首先,让我们在原始指令模型上使用相同的系统提示来建立基线结果。我将采样设置为 False,以确保两组预测的结果都是确定性的。

原始模型的准确度得分:0.0887035633055345

嗯……这不是很好……但是,重要的是要考虑到模型没有明确训练为仅提供整数答案,除了系统提示所暗示的内容。因此,有时即使格式不完美,预测中也可能存在正确答案。不过,我们可以观察到模型试图遵循指令,但并没有完全理解它们的含义,或者只是重复提示中的一次性示例……

原始模型预测

现在,让我们使用相同的设置测试 GRPO 调整模型:

调整模型的准确度得分:0.6527672479150872

哇!对于这么小的模型,这比我预期的要好。我甚至不得不仔细检查是否存在潜在的数据泄漏。即使没有泄漏,我们也可能已经微调了这个模型以过度拟合这种特定类型的任务。因此,我们本质上拥有的不是通用模型,而是专门的 GSM8K 计算器模型。但这仍然是一个重大的进步,为许多专门的模型训练工作(或通用大模型)打开了大门,这也是我非常感兴趣的另一个话题……

微调模型预测

6、结束语

所以,就是这样!我们采用了一个微小的 0.5B 参数模型,在我们的家庭实验室(或者,你知道,游戏装备!)中向它投入了一些 GRPO,并设法教会它一些相当不错的推理技能,至少对于数学问题来说是这样。谁会想到呢,对吧?虽然我们的小 Qwen-GRPO 不会很快取代 DeepSeek-R1,但我们看到的准确度的飞跃确实令人兴奋。它证明,即使资源有限,我们也可以开始使用这些先进的技术,并突破小型模型的极限。

整个实验确实强调了一个关键点:推理模型不仅适用于拥有无限计算能力的大型实验室。你可以在家里摆弄这些东西,学到很多东西,甚至得到令人惊讶的好结果。当然,还有很长的路要走——泛化、更强大的奖励,甚至可能弄清楚如何让那个“啊哈!”时刻不那么俗气——但这只是一个开始。开源社区充满了各种想法,我迫不及待地想看看我们能一起构建什么令人惊叹的推理模型,一砖一瓦,或者一行一行地编写代码!在家推理的未来?它看起来出奇地光明,而我,就我而言,已经系好安全带,准备好踏上旅程了!


原文链接:Reasoning Models at Home

汇智网翻译整理,转载请标明出处