掘金 人工智能 前天 18:23
GRPO 代码实战!让大模型具备思维能力,打造你的专属DeepSeek
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了DeepSeek-R1使用的GRPO强化学习算法,并以Qwen2.5-0.5B-Instruct模型为例,通过编写奖励函数,使其具备解决数学问题的能力。文章详细介绍了GRPO算法的核心原理,包括强化学习、小组内部PK制等,并提供了代码实战,展示了如何准备环境、测试模型、准备数据集以及编写奖励函数,最终实现模型的数学推理能力提升。

💡GRPO算法是一种强化学习方法,其核心在于通过设置奖励函数进行“小组内部PK制”,让模型具备思考能力,相比传统方法节约资源。

🔑GRPO算法主要分为三步:让大模型输出多种解法、小组内比优势、触发顿悟时刻。通过奖励函数计算每个回答的相对优势值,引导模型向正确方向进化。

💻在GRPO代码实战中,奖励函数的编写至关重要,它决定了大模型向正确道路进化的方向。本文提供了多个奖励函数示例,用于评估模型输出的正确性、数字有效性以及格式规范。

📝为了进行GRPO训练,需要准备环境、下载模型、准备数据集。本例使用Qwen2.5-0.5B-Instruct模型和OpenAI/GSM8K数据集,并提供了WandB环境配置建议,方便监控训练过程。

前言

DeepSeek-R1-0528的发布再次让DeepSeek重夺开源大模型王者的宝座(DeepSeek-R1-0528详细测评可见我的文章详细DeepSeek-R1-0528模型测评报告)。作为国产模型之光,DeepSeek-R1使用的强化学习GRPO后训练方法已经成为当今大模型训练的必备流程,无论是闭源的王Gemini Pro,Claude4,还是开源的神Qwen3训练过程中,都有GRPO算法的影子。

可以说GRPO算法已经成为大语言模型获得思维能力不可或缺的关键环节,大家也不只一次在后台私信:"除了微调教程,能不能出一期GRPO算法的实战分享呢?”。

应大家要求本期分享笔者将使用通俗易懂的语言向大家介绍GRPO算法的核心原理,并通过在Qwen2.5-0.5B-Instruct模型上进行GRPO强化学习训练,让Qwen2.5-0.5B-Instruct这个小模型具备思考求解数学题的能力,大家赶快来学习吧~

一、GRPO算法核心原理

GRPO算法是DeepSeek-R1诞生思考过程用到的一种强化学习方法。DeepSeek-R1原版论文中GRPO算法的描述夹杂着大量的公式,不利于大家理解。笔者这里将不涉及任何公式,通过通俗易懂的类比讲解,确保大家看完本篇分享后将理解GRPO算法的核心原理(更推荐理解后去看看原DeepSeek-R1的论文,配合公式加深印象)。

1.1 强化学习

GRPO算法是强化学习的一种,要想了解GRPO算法必须先了解强化学习。强化学习的核心思想很简单:想象一下我们是小孩子的时候,每当我们做正确的事比如助人为乐,努力学习等,大人就会给我们奖励,当我们做错误的事比如欺骗、霸凌等,大人就会赏我们巴掌。久而久之,我们就建立了“做正确事”的人生观,在正确的认识道路上成长。

强化学习训练大模型的本质和我们成长过程是一样的。在大模型训练过程中,如果大模型回答问题是正确的,我们给大模型设置较高的奖励分数,如果大模型回答问题是错误的,那么不得分或者得负分,优化的目标就是让大模型得到更高的分数,在这种机制下大模型会不断试错直到达到我们的既定效果。

以上就是强化学习的核心机制,是不是非常好理解~

1.2 GRPO算法流程

传统的强化学习方法(例如PPO)除了被训练的大模型外,还要引入其它大模型用来评估被训练大模型的生成结果,指明被训大模型的发展方向。GRPO算法的精髓在于撤掉用于评估的大模型,通过设置合理的奖励函数进行“小组内部PK制”的方法让模型具备思考能力,相比之下节约了大量资源。

GRPO算法的主要流程分为三步:

    让大模型输出多种解法: 以一个“下蛋鸡”问题为例,大模型返回了三种回答,我们将这三种回答作为一组。
 比如提问一个问题“农场有10只鸡,5只是公鸡,3只是下蛋的母鸡,问有几只鸡不下蛋?”    大模型要针对该问题输出多个答案:    (1) 10-5-3+5=7只 是正确推理    (2) 10-5-3=2只 是接近正确答案的推理    (3) 5只公鸡 是错误的推理
    小组内比优势: GRPO算法把同一个问题的多个回答当成“一组作业”, 通过奖励函数计算每个回答的相对优势值。每个回答的相对优势值等于得分-全组平均分。大模型会选择相对优势值高的回答作为进化方向,继续看示例
“下蛋鸡”问题的三个回答:(1) 10-5-3+5 = 7 回答正确得 5分 优势值:5-(5+2+0)/3=2.7分(2) 10-5-3=2 回答近似正确得 2分 优势值:2-(5+2+0)/3=-0.3分 (3)  5只公鸡  回答错误得     0分 优势值:0-(5+2+0)/3=-2.3分  选择优势值最高的第(1)个回答作为进化方向
    触发顿悟时刻: 使用GRPO算法不断训练你提供的数据集你会发现练着练着模型突然开窍了,自己能通过不断思考写对正确答案了。这种自主延长思考、涌现推理能力的神奇现象在DeepSeek-R1论文中被称为"Aha Moment"!(也叫做 啊哈 时刻,像不像你做对数学题之后的狂喜时刻!)

二、GRPO代码实战——前期准备

想必看到这里大家已经对GRPO算法的核心原理有了大致了解,接下来我们就快速编写代码,上手实践GRPO算法!

伴随着DeepSeek R1火爆全球,GRPO算法的使用需求也是不断增加。截至目前,主流的强化学习框架均支持GRPO算法,可以快速完成GRPO算法的推理流程。

那这时大家可能会问:“既然主流框架已经支持GRPO算法,那我们要编写的代码主要作用于哪一过程呢?”

诚然目前主流框架已经实现了GRPO的算法流程,但是模型回答质量的好坏是需要我们自己编写“奖励函数”评估的。奖励函数编写的好坏是GRPO算法取得成效的关键,只有好的奖励函数才能决定大模型向正确的道路进化。

本期分享我将自行编写奖励函数搭配TRL强化学习库(Unsloth、MS-Swift训练框架均采用的强化学习库),让Qwen2.5-0.5B-Instruct小模型诞生数学推理的能力。完整的代码在github.com/TangBaron/G… 本次实验用到的数据和模型训练前后参数大家可关注我的同名微信公众号:大模型真好玩,私信GRPO代码实战免费获取。

2.1 环境准备

    本篇分享我们采用Qwen2.5-0.5B-Instruct模型为例进行GRPO强化学习训练,访问网址modelscope.cn/models/Qwen… 从ModelScope下载模型权重。大家可以直接通过浏览器将模型权重下载到本地,也可以执行如下命令下载到指定文件夹:
pip install modelscope # 安装魔搭社区依赖mkdir ./Qwen2.5-0.5B-Instruct # 新建Qwen2.5-0.5B-Instruct 目录保存模型权重modelscope download --model Qwen/Qwen2.5-0.5B-Instruct --local_dir ./Qwen2.5-0.5B-Instruct # 下载模型权重到指定目录

    本篇分享我们同样使用Anaconda管理python环境避免冲突,执行如下命令新建环境并安装GRPO的依赖库:
conda create -n grpo python=3.11 # 创建名为grpo的conda虚拟环境conda activate grpo # 激活虚拟环境pip install torch # 安装pytorch依赖pip install transformers # 安装transformers依赖用于辅助模型推理和训练pip install trl # 安装trl依赖用于执行GRPO强化学习pip install wandb # 安装wanb依赖用于监控我们的训练流程

2.2 模型GRPO之前测试

进行GRPO训练前,我们需要测试一下目前Qwen2.5-0.5B-Instruct的能力,让大家更直观对比GRPO前后的模型性能,执行如下代码:

    modelscope导入模型加载依赖库,并加载保存在本地的模型:
from modelscope import AutoModelForCausalLM, AutoTokenizerfrom datasets import load_datasetmodel_name = "./models/Qwen2.5-0.5B-Instruct" # 本地模型保存位置model = AutoModelForCausalLM.from_pretrained(    model_name,    torch_dtype="auto",    device_map="auto") # 加载模型tokenizer = AutoTokenizer.from_pretrained(model_name) # 加载模型分词器
    提出一个小学数学问题测试:“Joy 每20分钟读8页书,那么它读完120页书需要几小时?"。将提示词加入messages列表并转化为大模型提问模板。我们输入的提示词语句会被分词器tokenizer转化为词语的id列表,id列表在输入模型后不同的id转化为不同词向量。
prompt = "Joy can read 8 pages of a book in 20 minutes. How many hours will it take her to read 120 pages?"messages = [    {"role": "user", "content": prompt}]text = tokenizer.apply_chat_template(    messages,    tokenize=False,    add_generation_prompt=True) # 将提示词代入Qwen提问模板model_inputs = tokenizer([text], return_tensors="pt").to(model.device) # 模板文字经过分词器转换成向量形式后转移到GPU上print(model_inputs)

    将提问输入大模型并生成回答,回答的生成形式也是id列表
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)]print(generated_ids)

    使用分词器将回复得到的词id列表转化为文本, 回答结果如下图所示,可见Qwen2.5-0.5B-Instruct模型初始状态下并不会主动思考。
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]print(response)

2.3 准备数据集

为了提升Qwen模型数学问题思考能力,本期分享使用来自OpenAI/GSM8K数据集: hf-mirror.com/datasets/op… , 该数据集包含从小学到高中大约8000个数学问题。该数据集详细介绍可参考我上篇文章:最强大模型评测工具EvalScope——模型好不好我自己说了算!

以下是该数据集的详细情况,可以看到答案中使用####对推理过程和标准答案进行区分。

使用如下代码下载数据集,该数据集默认会从huggingface下载,如果遇到网络问题无法下载,也可以关注我的微信同名公众号:大模型真好玩, 私信 GRPO代码实战 获得完整数据集。

data = load_dataset('openai/gsm8k', 'main')print(data)

GSM8K数据集包含7473条训练问答对和1319条测试问答对,问答对由questionanswer字段构成。

2.4 WandB环境配置(建议使用)

在大规模模型训练中,我们往往需要监控和分析大量的训练数据,而WandB可以帮助我们实现这一目标。WandB的使用教程可见笔者文章可视化神器WandB,大模型训练的必备工具!, 我们只需要添加如下代码即可:

import wandbwandb.login(key="根据笔者教程注册的WandB API KEY")wandb.init(project="GRPO-test")

当然WandB的使用不是必须的,WandB只是可以方便的记录大家训练过程中的奖励得分、loss曲线等,更有助于大家分析整个训练流程。

三、 GRPO代码实战——训练流程

3.1 提示词测试

完成上述全部环境搭建、模型下载测试工作后,接下来将正式开启GRPO强化学习的流程~

    基础准备工作,导入GRPO用到的相关库:
import re # 正则表达式匹配库import torch # 导入pytorch库from datasets import load_dataset, Dataset # 导入数据集处理依赖库from transformers import AutoTokenizer, AutoModelForCausalLM # 导入模型加载库from trl import GRPOConfig, GRPOTrainer # 导入GRPO算法配置和训练类
    定义提示词模板与模型文本输出格式
SYSTEM_PROMPT = """Respond in the following format:<reasoning>...</reasoning><answer>...</answer>"""XML_COT_FORMAT = """\<reasoning>{reasoning}</reasoning><answer>{answer}</answer>"""
    定义工具函数, extract_xml_answer用于从大模型回答中提取答案, extract_hash_answer则用于从gsm8k数据集中提取答案。
# 提取answer标签之间内容def extract_xml_answer(text):    answer = text.split("<answer>")[-1]    answer = answer.split("</answer>")[0]    return answer.strip()# 提取gsm8k数据集中的答案部分def extract_hash_answer(text: str) -> str | None:    if "####" not in text:        return None    return text.split("####")[1].strip()
    加载 GSM8K 数据集并处理,修改原先数据集中的answer字段让它只包含答案,同时增加prompt字段指定系统输出格式SYSTEM_PROMPT和用户的输入问题。
def get_gsm8k_questions(split = "train"):    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()print(dataset)print(dataset[0])

可见处理后的数据集增加了prompt字段,同时answer字段也被修改

    在正式开启GRPO训练前,我们还是先测试一下模型输出。
messages = dataset[0]['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)

可见在明显向Qwen2.5-0.5B-Instruct模型提供系统返回格式提示词SYSTEM_PROMPT的情况下,模型的回答也并未诞生思考过程且没有满足格式要求。

3.2 奖励函数编写

要想模型能够按照固定思考格式回复,我们必须对它进行GRPO训练GRPO训练最重要的部分是奖励函数的编写,毕竟GRPO的基本原理是是依照获得更高奖励函数得分的方向不断强化模型的性能,让其具备思考的格式和能力。

本次GRPO训练中用到的奖励函数模型组如下:

具体的函数编写如下:

# 检查模型输出是否与正确答案匹配,并根据匹配情况返回奖励分数def correctness_reward_func(prompts, completions, answer, **kwargs):    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):    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]# 检查模型的输出是否符合严格的格式要求,包括<reasoning></reasoning>和<answer></answer>标签def strict_format_reward_func(completions, **kwargs):    """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]# 检查模型的输出是否符合稍微宽松的格式要求, <reasoning>和<answer>标签之间可以有空白字符。def soft_format_reward_func(completions, **kwargs):    """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]# 计算文本中<reasoning></reasoning>和<answer></answer>标签的出现次数,并根据它们的位置和频率分配奖励。def count_xml(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# 该函数用于计算每个模型输出的XML结构符合度,并返回奖励分数def xmlcount_reward_func(completions, **kwargs):    contents = [completion[0]["content"] for completion in completions]    return [count_xml(c) for c in contents]

奖励函数是强化学习中的反馈机制,帮助模型优化输出结果,并满足格式、正确性等多方面要求。可以说奖励函数是大模型实现GRPO的关键,大模型只有在合理的奖励函数中不断“撞墙”并向着高分值的方向逼近,才能具备思考能力!

3.3 执行GRPO过程

完成了奖励函数的编写后,终于来到GRPO算法的执行过程了。

    首先读取模型并设置模型保存地址:
model_name = "models/Qwen2.5-0.5B-Instruct"output_dir="outputs/Qwen-0.5B-GRPO"run_name="Qwen-0.5B-GRPO-gsm8k"
    然后利用GRPOConfig类创建训练参数, TRL已经将GRPO很好的封装.
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=1,    gradient_accumulation_steps=4,    num_generations=16,    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:0",    report_to="wandb" )

GRPOConfig 是TRL设计的用于GRPO强化学习和优化训练GRPO训练过程的配置类,它包含了GRPO训练的各种设置参数,这些参数的具体含义如下所示:

    加载模型和分词器,创建GRPOTrainer训练类并传入奖励函数,执行trainer.train()开启GRPO训练过程,然后就是漫长的等待过程啦~
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_tokentrainer = 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)

本次训练使用了WandB记录模型的训练流程, 我们可以在WandB中查看模型的训练状态,从中可以看到我们的奖励函数从0开始逐步升高,这表示模型已经按照我们的设定格式回答问题啦!

四、GRPO训练结果检测

GRPO训练完成后我们可以通过两种方法检测结果:

第一种方法是人工向模型提问, 检验模型输出答案和输出格式是否正确,以下是我们同样向模型提问“Joy 每20分钟读8页书,那么它读完120页书需要几小时?”的问题:

grpo_model_name = "./outputs/Qwen-0.5B-GRPO"model = AutoModelForCausalLM.from_pretrained(    grpo_model_name,    torch_dtype="auto",    device_map="auto")tokenizer = AutoTokenizer.from_pretrained(model_name)prompt = "Joy can read 8 pages of a book in 20 minutes. How many hours will it take her to read 120 pages?"messages = [    {"role": "system", "content": SYSTEM_PROMPT},    {"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=False)[0]print(response)

下面是该问题的执行结果,对比此前原始模型的输入,GRPO训练好的模型已经能够顺利执行思考过程并按照要求格式返回输出结果,多试几个例子同样会有这样的结果!GRPO果然让Qwen2.5-0.5B-Instruct模型插上了思考的翅膀!

第二种方法我们可以使用笔者文章最强大模型评测工具EvalScope中介绍的EvalScope对GSM8K的test数据集进行测试,具体的代码实现就当作小练习交给大家自行复习实现啦!笔者这边在GSM8K的test数据集上的评测结果由22.4% 提升到 48.6%, 可见GRPO不但实现了格式上的对齐,更让模型具备思考能力,在数学数据集上取得了惊人性能!

五、 总结

以上就是今天GRPO代码实战的全部分享啦!本篇分享笔者通俗易懂的讲述了GRPO核心原理,并通过TRL库和自定义奖励函数实现GRPO的完整训练流程!相信大家认真看完了本篇分享一定会对GRPO算法有更全面的了解,也具备了让大模型能够思考推理的超能力!GRPO算法是DeepSeek更是中国在世界人工智能发展历史上留下的炫丽瑰宝,应该被每一个中国人工智能爱好者掌握。

今天的分享到此结束,大家感兴趣可以关注我的掘金账号,更可关注我的同名微信公众号:大模型真好玩, 查阅更多笔者学习工作中免费的大模型经验分享和相关资料~

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

GRPO算法 强化学习 大模型 Qwen2.5 数学推理
相关文章