前言
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条测试问答对,问答对由question
和answer
字段构成。
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
是提示词模板,它是一个多行字符串,用于指定模型需要生成的结果格式,该提示词表示模型的回答中需要包含<reasoning></reasoning>
推理过程标签(也就是<think></think>
过程,这里只是更换了标签表示)。<answer></answer>
是大模型最终的回答结果。在生成式模型中,模型需要先给出推理过程,然后再给出答案。XML_COT_FORMAT
则定义了一个结构化的输出形式,这种输出形式会作为格式奖励函数的优化目标,从而让Qwen2.5-0.5B-Instruct模型生成符合XML_COT_FORMAT格式的文本。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训练中用到的奖励函数模型组如下:
- correctness_reward_func:根据正确性对答案进行奖励。int_reward_func:根据是否为数字对输出进行奖励。strict_format_reward_func:根据严格的格式要求检查并奖励。soft_format_reward_func:根据稍微宽松的格式要求检查并奖励。count_xml:计算文本中的 XML 标签结构并给予奖励。xmlcount_reward_func:为每个输出计算 XML 标签结构的符合度并返回奖励。
具体的函数编写如下:
# 检查模型输出是否与正确答案匹配,并根据匹配情况返回奖励分数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训练的各种设置参数,这些参数的具体含义如下所示:
- output_dir: 指定训练结果保存的目录(例如模型检查点、日志文件)。output_dir 变量应事先定义。run_name: 运行的名称,用于标识该训练任务的名称。learning_rate=5e-6: 学习率,决定了每次参数更新时模型更新的步长。较小的学习率意味着训练更加稳定,但可能需要更长的时间。adam_beta1=0.9 和 adam_beta2=0.99: Adam 优化器的超参数,分别对应一阶矩和二阶矩的衰减率。它们通常用于控制优化过程中的动量。weight_decay=0.1: 权重衰减,通常用于防止过拟合,是一种正则化方法。warmup_ratio=0.1: 学习率的预热比例,表示在训练的初期阶段逐步增加学习率,避免直接开始时学习率过高。lr_scheduler_type='cosine': 学习率调度类型,cosine 表示使用余弦退火(Cosine Annealing)来调整学习率,通常在训练的后期将学习率逐渐减小。logging_steps=1: 每训练 1 步就记录一次日志。这个参数控制了日志记录的频率。bf16=True: 启用 bfloat16 精度训练。bfloat16 是一种数值精度格式,通常在支持该格式的硬件(如 Google TPU)上能有效加速训练,同时减少显存占用。per_device_train_batch_size=1: 每个设备上进行的训练批次大小,即每次计算时使用的样本数。此处设置为 1,表示每个 GPU 或设备上处理 1 个样本。gradient_accumulation_steps=4: 梯度累积步数,在训练时通过累计多个小批次的梯度来模拟更大的批次大小,以节省显存。num_generations=16: 每个训练样本生成的输出数量,通常用于生成式任务(如文本生成)。max_prompt_length=256: 最大输入长度(包括问题和上下文),超过此长度的输入会被截断。max_completion_length=200: 模型生成的最大输出长度。num_train_epochs=1: 训练的总轮数。每一轮都将对整个数据集进行一次完整的训练。save_steps=100: 每 100 步保存一次模型的检查点。max_grad_norm=0.1: 梯度裁剪的最大范数,这有助于避免梯度爆炸的问题,尤其是在深度神经网络中。log_on_each_node=False: 在每个节点上是否记录日志,通常在分布式训练时使用。use_vllm=False: 是否使用 vllm 进行训练。使用vllm使模型快速生成补全结果vllm_gpu_memory_utilization=.3: 如果启用了 vllm,这是每个 GPU 的内存使用比例(通常是一个浮动值,0 到 1)。vllm_device="cuda:0": 指定要在第一个 GPU 上运行 vllm。report_to="none": 禁用训练过程中的报告工具(如 WandB)。如果设置为 "wandb",则训练过程中会记录数据并上传到 WandB。
- 加载模型和分词器,创建
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更是中国在世界人工智能发展历史上留下的炫丽瑰宝,应该被每一个中国人工智能爱好者掌握。
今天的分享到此结束,大家感兴趣可以关注我的掘金账号,更可关注我的同名微信公众号:大模型真好玩, 查阅更多笔者学习工作中免费的大模型经验分享和相关资料~