【大模型系列篇】GRPO算法实战-复现Qwen2.5-1.5B-Instruct的DeepSeek-R1顿悟时刻

一、“Aha Moment” - 顿悟时刻

本文将快速上手实践DeepSeek提出的GRPO算法,并手动复现DeepSeek R1论文中的模型顿悟时刻,即通过GRPO训练,让模型诞生思考链。这也是整个DeepSeek R1模型训练的至关重要的环节,也是DeepSeek R1模型为大模型技术做出的卓越贡献。

不同于传统的强化学习训练算法,或者常用的PPO算法,GRPO算法更加省时高效,通过暴力枚举策略、以及自我策略对比的方法快速提升模型在推理问题上制定策略的能力。这也是目前强化学习领域、被验证的最有效的提升大模型推理能力的方法。

        本次采用最基础的trl库,并围绕Qwen2.5-1.5B-instruct模型的GRPO强化学习训练,并复现DeepSeek R1模型训练过程中的aha时刻,从此诞生思考过程。

  • 模型下载

魔搭社区模型权重下载地址:https://modelscope.cn/models/Qwen/Qwen2.5-1.5B-Instruct

pip install modelscope
mkdir ./Qwen2.5-1.5B-Instruct 
modelscope download --model Qwen/Qwen2.5-1.5B-Instruct --local_dir ./Qwen2.5-1.5B-Instruct
  • 准备数据集

数据集选自OpenAI/GSM8K数据集:https://huggingface.co/datasets/openai/gsm8k

OpenAI GSM8K 数据集 是一个广泛用于评估推理和数学能力的多样化数学题目数据集。该数据集包含大约 8,000 个数学问题,涵盖了从小学到高中的各种数学领域,包括算术、代数、几何等。GSM8K 旨在挑战大型语言模型在没有显式提示的情况下,解决更复杂的数学推理问题。数据集中的问题通常需要模型进行多步推理,远超基本的算术计算,因此它被广泛用于测试模型在理解和处理数字推理的能力。

GSM8K 数据集的设计基于自然语言形式,呈现为问题-解答对的形式,且包含了问题的详细解析步骤。该数据集被广泛应用于模型的 零样本推理少样本学习 任务,是当前研究中用于验证语言模型推理能力的重要基准之一。

二、训练前调用

from modelscope import AutoModelForCausalLM, AutoTokenizer

model_name = "./models/Qwen2.5-1.5B-Instruct"

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

prompt = "John buys 2 packs of gum and 3 candy bars.  Each stick of gum cost half as much as the candy bar.  If the candy bar cost $1.5 each, how much did he pay in total?"
messages = [
    {"role": "user", "content": prompt}
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)

generated_ids = model.generate(
    **model_inputs,
    max_new_tokens=512
)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)

三、GRPO复现aha时刻

完整代码

import re
import torch
from datasets import load_dataset, Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM
from trl import GRPOConfig, GRPOTrainer

SYSTEM_PROMPT = """
Respond in the following format:
<reasoning>
...
</reasoning>
<answer>
...
</answer>
"""

XML_COT_FORMAT = """\
<reasoning>
{reasoning}
</reasoning>
<answer>
{answer}
</answer>
"""

def extract_xml_answer(text: str) -> str:
    answer = text.split("<answer>")[-1]
    answer = answer.split("</answer>")[0]
    return answer.strip()


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


# uncomment middle messages for 1-shot prompting
def get_gsm8k_questions(split = "train") -> Dataset:
    data = load_dataset('openai/gsm8k', 'main')[split] # type: ignore
    data = data.map(lambda x: { # type: ignore
        'prompt': [
            {'role': 'system', 'content': SYSTEM_PROMPT},
            {'role': 'user', 'content': x['question']}
        ],
        'answer': extract_hash_answer(x['answer'])
    }) # type: ignore
    return data # type: ignore

# Reward functions
def correctness_reward_func(prompts, completions, answer, **kwargs) -> list[float]:
    responses = [completion[0]['content'] for completion in completions]
    q = prompts[0][-1]['content']
    extracted_responses = [extract_xml_answer(r) for r in responses]
    print('-'*20, f"Question:\n{q}", f"\nAnswer:\n{answer[0]}", f"\nResponse:\n{responses[0]}", f"\nExtracted:\n{extracted_responses[0]}")
    return [2.0 if r == a else 0.0 for r, a in zip(extracted_responses, answer)]

def int_reward_func(completions, **kwargs) -> list[float]:
    responses = [completion[0]['content'] for completion in completions]
    extracted_responses = [extract_xml_answer(r) for r in responses]
    return [0.5 if r.isdigit() else 0.0 for r in extracted_responses]


def strict_format_reward_func(completions, **kwargs) -> list[float]:
    """Reward function that checks if the completion has a specific format."""
    pattern = r"^<reasoning>\n.*?\n</reasoning>\n<answer>\n.*?\n</answer>\n$"
    responses = [completion[0]["content"] for completion in completions]
    matches = [re.match(pattern, r) for r in responses]
    return [0.5 if match else 0.0 for match in matches]


def soft_format_reward_func(completions, **kwargs) -> list[float]:
    """Reward function that checks if the completion has a specific format."""
    pattern = r"<reasoning>.*?</reasoning>\s*<answer>.*?</answer>"
    responses = [completion[0]["content"] for completion in completions]
    matches = [re.match(pattern, r) for r in responses]
    return [0.5 if match else 0.0 for match in matches]

def count_xml(text) -> float:
    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

def xmlcount_reward_func(completions, **kwargs) -> list[float]:
    contents = [completion[0]["content"] for completion in completions]
    return [count_xml(c) for c in contents]

model_name = "models/Qwen2.5-1.5B-Instruct"

output_dir="models/Qwen2.5-1.5B-GRPO"
run_name="Qwen2.5-1.5B-GRPO-gsm8k"


training_args = GRPOConfig(
    output_dir=output_dir,
    run_name=run_name,
    learning_rate=5e-6,
    adam_beta1 = 0.9,
    adam_beta2 = 0.99,
    weight_decay = 0.1,
    warmup_ratio = 0.1,
    lr_scheduler_type='cosine',
    logging_steps=1,
    bf16=True,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    num_generations=4,
    max_prompt_length=256,
    max_completion_length=200,
    num_train_epochs=1,
    save_steps=100,
    max_grad_norm=0.1,
    log_on_each_node=False,
    use_vllm=False,
    vllm_gpu_memory_utilization=.3,
    vllm_device="cuda",
    report_to="none"
)

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map=None
).to("cuda")

tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

dataset = get_gsm8k_questions()

trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    reward_funcs=[
        xmlcount_reward_func,
        soft_format_reward_func,
        strict_format_reward_func,
        int_reward_func,
        correctness_reward_func],
    args=training_args,
    train_dataset=dataset,
)

trainer.train()

trainer.save_model(output_dir)
nohup env HF_ENDPOINT=https://hf-mirror.com  python rl_grpo.py > ./grpo.log 2>&1 &

我们在两卡3090上,GRPO训练过程需要约40G显存,并运行13个小时左右。 

从训练日志中我们发现,前一个问题还没有思考过程,后一个问题诞生了思考过程。

代码解析

定义提示词模板
  • SYSTEM_PROMPT = """..."""这是一个多行字符串,定义了一个系统提示prompt的格式。
    • "<reasoning>""</reasoning>":这部分表示推理过程的地方。推理过程可能包括模型分析问题、提出假设、推理等逻辑推导步骤。
    • "<answer>""</answer>":这部分表示最终的回答或结论的地方。根据推理结果,模型将给出答案。
模型文本输出格式模板
  • XML_COT_FORMAT = """..."""这个字符串定义了一个 XML 风格的格式,并允许动态插入 reasoninganswer 的内容。
    • "<reasoning>{reasoning}</reasoning>":这里 {reasoning} 是一个占位符,表示实际的推理过程会被插入到该位置。
    • "<answer>{answer}</answer>":类似地,{answer} 是占位符,用于插入最终答案。
数据格式处理函数组
  • extract_xml_answer 用于从 XML 风格的文本中提取 <answer> 标签之间的内容。
  • extract_hash_answer 用于从文本中提取 #### 后的内容,若没有找到 ####,返回 None
  • get_gsm8k_questions 加载 GSM8K 数据集,并将问题与系统提示结合,格式化后返回。
奖励函数组
  • correctness_reward_func:根据正确性对答案进行奖励。
  • int_reward_func:根据是否为数字对输出进行奖励。
  • strict_format_reward_func:根据严格的格式要求检查并奖励。
  • soft_format_reward_func:根据稍微宽松的格式要求检查并奖励。
  • count_xml:计算文本中的 XML 标签结构并给予奖励。
  • xmlcount_reward_func:为每个输出计算 XML 标签结构的符合度并返回奖励。

四、训练后调用

from modelscope import AutoModelForCausalLM, AutoTokenizer

model_name = "./models/Qwen2.5-1.5B-GRPO"

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

prompt = "John buys 2 packs of gum and 3 candy bars.  Each stick of gum cost half as much as the candy bar.  If the candy bar cost $1.5 each, how much did he pay in total?"
messages = [
    {"role": "user", "content": prompt}
]

text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)

generated_ids = model.generate(
    **model_inputs,
    max_new_tokens=512
)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)

<think>嗯,用户问的是Qwen2.5-1.5B-InstructQwen2.5-1.5B的区别。首先,我需要确定这两个模型都是来自阿里的千问系列。根据之前的知识,通常模型名称中的"Instruct"可能表示该版本是针对指令跟随或对话优化的。比如,像其他模型如LLaMA的Instruct版本就是经过微调,更适合对话交互。 接下来,我需要确认这两个模型的基础架构是否相同。用户提到的都是1.5B参数,所以参数规模应该是一样的。那区别可能在于训练数据或微调方法。通常,基础模型(不带Instruct)可能是在大规模通用语料上预训练的,而Instruct版本可能在预训练后进行了进一步的指令微调,使用更多的对话或任务导向的数据,使其更擅长理解和响应人类指令。 另外,应用场景的不同也很重要。基础模型可能更适合作为通用模型,用于文本生成、摘要等任务,而Instruct版本则更适用于聊天机器人、问答系统等需要交互的场景。用户可能想知道选择哪一个更适合他们的项目需求。 还需要考虑技术细节,比如是否使用了不同的微调技术,比如监督微调(SFT)或者基于人类反馈的强化学习(RLHF)。不过对于1.5B的模型,可能主要是SFT。另外,可能的输出格式不同,Instruct版本可能更严格遵守指令,生成结构化的回答,避免冗余信息。 用户可能的背景是开发者或者研究人员,想要选择合适的模型进行部署或实验。他们可能关心性能差异、响应质量、适用任务类型等。需要明确说明两者的差异,帮助用户做出决策。 最后,要确保回答准确,如果有不确定的地方,建议查阅官方文档或论文。但根据常见的命名惯例和模型发布模式,上述分析应该是合理的。</think>Qwen2.5-1.5B 和 Qwen2.5-1.5B-Instruct 是阿里云推出的同一参数规模(1.5B)大语言模型的两个不同版本,主要区别体现在**训练目标**和**适用场景**上: --- ### 1. **基础模型:Qwen2.5-1.5B** - **定位**:通用语言模型(Base Model) - **训练目标**: 基于大规模无监督文本进行预训练,主要学习语言的统计规律和通用知识,目标是生成连贯的文本。 - **特点**: - 擅长文本生成(如续写故事、生成文章)。 - 支持通过提示词(prompt)调整输出,但需用户设计合适的指令。 - 更适合需要灵活控制生成内容的研究或开发场景。 - **局限性**: 对复杂指令的理解和直接对话能力较弱,需额外微调才能适配对话任务。 --- ### 2. **优化版本:Qwen2.5-1.5B-Instruct** - **定位**:指令微调模型(Instruction-tuned Model) - **训练目标**: 在基础模型上,通过**监督微调(SFT)**对大量指令-答案数据进行训练,学习如何理解人类指令并生成符合要求的回答。 - **特点**: - 擅长对话、问答、任务型交互(如写邮件、写代码、推理等)。 - 输出更简洁、结构化,直接响应用户需求。 - 内置对齐优化,安全性更高(减少有害输出)。 - **适用场景**: 直接部署为对话助手、API服务或需要自然交互的应用。 --- ### **简单总结** | 特性 | Qwen2.5-1.5B | Qwen2.5-1.5B-Instruct | |--------------------|-----------------------|------------------------| | **核心能力** | 文本生成、知识存储 | 指令理解、任务交互 | | **使用难度** | 需设计复杂prompt | 直接输入自然语言指令 | | **适用场景** | 研究、定制化开发 | 对话机器人、API服务 | --- ### **如何选择?** - 若需**直接对话交互**或**快速部署应用**,选 `Instruct` 版本。 - 若需**深度定制模型行为**或**研究底层生成能力**,选基础模型。 建议参考阿里云官方文档或通过实际测试(如HuggingFace平台)进一步验证模型表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值