
很多人一听说要在Qwen3-0.6B-Base这种0.6B参数的小模型上跑GRPO强化学习第一反应通常是这不直接做监督微调就完了吗我一开始也是这么想的直到把两套对比实验摆在一起才发现小模型其实更吃“自我探索”而不是“模仿标准答案”。这个过程中我用GRPO把基础模型的推理痕迹、格式服从度和最终答案正确率都拉起来了一个档次同时也踩了不少坑比如组内奖励全是零、模型学会刷格式骗奖励、KL散度突然爆炸这些破事。这篇文章就顺着这个实操脉络把为什么选用GRPO、训练数据怎么准备、奖励函数怎么写、超参数怎么调、训练日志怎么判断以及一套可以直接参考的代码流程完整记录下来。适合想把小模型用在数学、逻辑、计算器辅助推理这类任务上的朋友也适合准备从SFT切换到RL训练思路的人。1. 选型逻辑为什么是0.6B基础模型GRPO1.1 小模型推理能力的真实缺口在哪0.6B参数意味着模型容量非常紧张它没有足够空间去记忆海量知识能记住的只是语言模式和基本的数学运算规律。常规做法是拿一批带详细推理过程的数据做SFT让模型学会“照着推理格式生成”。SFT的问题在于它是强迫模型模仿给定输出模型在训练集覆盖范围内往往表现不错可一旦遇到训练集里没见过的问法或数字组合很容易原样套用错误路径因为它是背答案而不是在推理。我做的对照实验中同样是数学文字题集纯SFT训练后的模型对训练集内相似题目的答案正确率尚可但换一批改写过的同类型题目后正确率掉得非常明显。这个现象说明小模型在SFT框架下把推理过程学成了“条件反射”而不是真正的策略。GRPO这种强化学习方式则不一样它让模型自己生成答案从正确结果里获得奖励并在多次采样中比较哪条路径更优。模型最终学到的是对问题空间的搜索偏好而不是对训练文本的逐字记忆。还有一个现实因素小模型部署便宜、延迟低。很多端侧或业务级场景根本跑不动7B以上的模型0.6B能在普通CPU或小显存GPU上跑得动只要推理能力能拉上来它就是性价比非常高的选择。所以这个组合不是“没钱才选小模型”而是一条值得投入的实用路线。1.2 为什么GRPO比PPO更适合这种场景强化学习调语言模型最常被提到的算法是PPO但PPO有个隐藏成本它需要一个和价值网络差不多规模的Critic模型来估计每个状态的收益。对0.6B的Actor模型来说还要额外加载一个同尺寸的Value网络等于训练资源直接翻倍。而且Critic模型本身不好训奖励尺度稍有变动它就得重新适应训练稳定性很让人头疼。GRPO的全称是Group Relative Policy Optimization它的核心做法是把同一个问题采样出多个回答组成一个组然后只看这个组的内部奖励相对关系用组内平均作为基线直接算出每个回答的优势值。整个过程不需要价值网络一个策略模型跑前向采样再加反向更新就够。这样不仅省了显存还少了一个容易出问题的组件。训练成本大概只有PPO的一半多点对小规模团队或单卡实验非常友好。这个思路可以打一个比方PPO是公司请外部的绩效咨询师给每个员工定绝对KPI必须有一套完整评价体系GRPO是直接看同一个小组内部谁干得好、谁干得差谁超额完成就多奖励谁拖后腿就压一下不需要外部专家。对推理任务来说同一道题的回答之间互相比较本身就是很自然的基准这种相对评价反而比绝对价值拟合更符合任务本质。1.3 用base模型而不是instruct模型的原因很多人在选型时会问为什么不用已经做过的指令微调版本直接在Chat版本上继续强化我的经验是尽量不要。已经做过的指令版本经过大量SFT和偏好对齐之后行为已经高度固化比如回答语气、长度控制、拒绝风格等都已经定型。你再拿GRPO去优化它模型必须在“已经学会的行为”之上叠加强化信号很容易出现奖励覆盖现象——新学到的推理格式和旧的回复习惯打架输出经常性崩坏。基础模型反而是一张白纸它只学会了语言统计规律没有被强行绑定上某种回答格式。在RL训练中你给的格式奖励才能更好地塑造它。另外基础模型还没有经历过DPO对齐带来的过度平滑现象相当于还保留着更大的生成熵采样出的回答多样性更好。而GRPO非常依赖组内样本的多样性如果模型确定性太强组内8个回答几乎一样优势值估算会失效。当然用base模型也有代价初始阶段它连“以固定格式输出”都不会生成结果可能是一堆无关文本。不过这个问题可以在奖励函数里加一个格式分让模型用几十步甚至上百步学会格式规范后再慢慢把重心转移到答案正确性上。我用下来的感觉是这个代价完全值得。2. 数据和奖励设计GRPO的地基工程2.1 训练集构造与提示模板GRPO的数据和SFT不同它不要求每条数据都有标准推理过程只需要问题本身和最终正确答案。模型自己会生成推理过程GRPO根据最终答案对不对来给奖励推理过程是否清晰反而靠格式奖励间接塑造。准备数据的时候我推荐以数学文字题、计算题、逻辑推导题为主要求答案是确定性的这样奖励判断才不会有模糊空间。数据量不需要特别大我用的训练集约为一千多条每条只包含两个字段问题和答案。数量少没关系GRPO本身就是让模型对同一问题反复采样同一个问题被生成八次、更新一次信息密度很高。更重要的是问题的多样性尽量覆盖不同题型、不同数字范围、不同问法。如果训练集里都是同一类题模型很容易在一个局部最优上过拟合。提示模板一定要固定格式如下### 指令 请解决下面的问题。先给出逐步推理过程最后以单独一行输出“最终答案XXXX”。 ### 问题 {question} ### 回答用这种纯文本模板不依赖特定的对话标记因为基础模型的safetensors权重里虽然也有特殊标记但它在预训练阶段对复杂对话模板的感知强度有限固定纯文本格式反而更稳。模板写好后所有训练数据都用同一个模板套起来保证训练分布一致。评估的时候也用同样模板这样评估分数才有意义。2.2 奖励函数正确性、格式、区分度三件事奖励函数是整个GRPO训练里最核心的部分它直接决定了模型在优化什么。很多人以为奖励函数就是把“答案是否正确”变成1和0真这么写了训练大概率翻车。因为同一道题采样出的8个回答很可能全对或全错这时候组内优势全是0梯度直接消失。我的建议是把奖励拆成两部分叠加。第一部分是答案正确性占了主要权重第二部分是格式分只要模型按模板要求输出了“最终答案”并且前面有推理过程就给一个小正数格式不对给0。格式分的作用有两个一是给全错组提供至少一点区分度二是让模型先学会正确的输出结构。初始训练阶段如果8个回答答案全错格式分还能维持组内非零差异模型至少知道往“像样”的方向走。下面是我在实际训练中使用的奖励函数代码可以直接抄import re def normalize_answer(s): # 去掉各种空格、逗号、引号便于字符串比较 s re.sub(r[\s,。;:\“”‘’号元()], , s) return s.strip() def reward_func(prompts, completions, answers): rewards [] for completion, answer in zip(completions, answers): text completion.strip() lines [ln for ln in text.splitlines() if ln.strip()] # 1. 格式分必须有推理过程行且在最后给出最终答案 match re.search(r最终答案[:]\s*(.), text) format_ok 1.0 if (match and len(lines) 2) else 0.0 # 2. 正确分比对最终答案和标准答案 if match: final_ans normalize_answer(match.group(1)) correct 1.0 if final_ans normalize_answer(answer) else 0.0 else: correct 0.0 rewards.append(0.8 * correct 0.2 * format_ok) return rewards这里我把正确分设为0.8格式分设为0.2比例可以调。如果模型连格式都经常崩可以先把格式分拉高到0.4甚至0.5如果格式已经很稳定把格式分压低到0.1让正确性占主导。奖励函数写好后批量跑一遍训练集输出每条样本的奖励分布能提前看到哪些问题对模型过难、哪些答案格式容易出错。2.3 组内区分度的两个隐藏坑第一坑是“组内全对或全错”。前面提过全对或全错会导致优势无限或为零所以要保证奖励函数对同一组内的不同回答给出不同数字。哪怕只有一个回答答案正确其他全错也能算出优势。为了避免长时间全错可以在一开始故意把格式分权重提高或者把数据按难度分层。让简单题和难题交替进入批次不要让模型连续碰高难题否则整个batch的奖励都是零训练心态会崩优化方向也丢失。第二坑是答案归一化不彻底。数学题的答案格式千奇百怪“39”和“39元”在语义上等价但对字符串比较而言是两回事。我把“元”“个”“只”这些单位词在归一化时去掉后正确率判断显著改善。如果你做的是选择题建议答案直接落成“A”“B”“C”“D”这种单字符如果是填空题统一去掉空格和标点。做字符串匹配时永远要记得奖励函数是死板的它不会替你理解题目。3. GRPO的核心机制和超参数别只跑代码要看得懂它在干嘛3.1 GRPO的优化目标和计算流程GRPO的完整计算流程可以拆成四步。第一步对每个问题q采样G个回答组成一个组G通常取4到16我默认用8。第二步用奖励函数给每个回答打分得到r1到rG。第三步把组内奖励转成优势值公式是A_i (r_i - mean(r)) / std(r)也就是说组内平均分成为基线每个回答的分数减去这个基线再除以标准差得到的就是它在组内的相对优劣。第四步用类似PPO的策略梯度公式更新模型目标可以写成最大化E[ 1/G * Σ min( ratio_i * A_i, clip(ratio_i, 1-ε, 1ε) * A_i ) - β * KL(π_θ || π_ref) ]其中ratio_i是新策略对旧策略生成该回答的概率比clip把更新幅度限制在一定范围内避免一步走太远。KL项则约束新策略不要偏离初始模型太远防止模型通过生成乱码来刷奖励。注意这个流程中不需要单独训练任何价值网络。组内平均值就是天然的基线对于一个简单问题组内可能全都答对优势全为0这说明模型已经做得很好了不需要强行给个大梯度。对一个极难问题组内全错优势也为0不会让模型因为一个问题太难而受到不公正惩罚。这种相对评价机制非常贴合推理任务的场景差异。3.2 组内相对优势为什么能又快又稳语言模型强化学习最怕奖励尺度不稳定。一道题得1分另一道题得10分如果用绝对奖励模型会疯狂偏向能拿高分的问题类型忽略那些拿低分但更重要的问题。GRPO每次只用同一道题内部的相对差值计算优势完全规避了不同题目之间难度差异带来的偏差。这种设计对于数学题尤其重要因为不同问题的难度跨度极大。稳定性还来自另一层不需要价值网络意味着不需要估计绝对状态值。价值估计一旦有偏PPO的策略更新会被带偏GRPO的价值估计完全来自采样虽然方差稍大但偏差小配合8个以上采样基本够用。实际训练中我对比过同样数据下GRPO和PPO的loss曲线GRPO的收敛更顺滑reward均值总体上升而PPO经常出现奖励均值上升但生成质量反而下降的“奖励幻觉”情况。这种稳定性也带来了调参上的好处。GRPO和KL惩罚项配合得很好我基本只需要调学习率和β值两个参数其他用默认就能跑。PPO还要调价值网络的学习率、GAE系数、裁剪参数动作一大就容易把模型训飞。可以说GRPO把强化学习调语言模型的工程门槛往下拉了一大截。3.3 几个必须盯住的关键超参数以下是我在0.6B模型上调试后认为最值得关注的参数表参数推荐范围作用调参建议learning_rate1e-6 到 5e-6控制策略更新幅度小模型容量小学习率超过5e-6出现过KL失控建议从3e-6起步beta0.01 到 0.1KL惩罚系数0.04比较稳数值偏大模型探索少偏小容易偏离初始模型num_generations4 到 16每个问题采样数数据少时调到8或12试探方差更大max_completion_length512 到 1024限制回答长度数学推理一般512够用逻辑题长一点就设1024epsilon0.1 到 0.3裁剪阈值默认0.2新模型用0.1更保守temperature0.7 到 1.2采样随机性太低组内多样性差太高格式容易崩有条件的同学可以跑两三轮短实验先跑100步看KL和reward的趋势再决定正式训练步数。我记得第一次训练时直接设置了2000步结果在300步左右reward均值就开始停滞后来把学习率从1e-5降到3e-6问题才缓解。小模型对这个参数尤其敏感千万别照搬大模型的配置。4. 实操记录从安装环境到跑通训练脚本4.1 环境准备我用的环境是单张独立显卡显存24G0.6B模型训练完全够用如果你的显存只有16G也能跑但要把batch size和生成长度调低。本质上GRPO训练的开销大头在采样阶段因为每步都要为每个问题生成多条回答所以对显存的要求比对SFT高不少。安装依赖时注意版本对齐跑GRPO需要一套支持强化学习训练的开源框架组合主要以transformers和trl为主pip install transformers datasets accelerate trl wandb建议在干净的虚拟环境里装前后端版本混用很容易出问题。trl版本有变化时GRPO接口细节会跟着变装上后可以先跑一个最小demo确认GRPOTrainer可用再开始正式训练。4.2 完整训练脚本训练数据我先整理成jsonl文件一行一条{question: 一个两位数个位数字是十位的3倍且比十位数大18这个数是多少, answer: 39} {question: 把一根绳子对折两次后每段长12米原来绳子长多少米, answer: 48} {question: 一本书共有240页小明第一天看了全书的四分之一第二天看了剩下的一半第三天从第几页开始看, answer: 121}然后写训练脚本。这里用的是基于trl的GRPOTrainer实现实际使用前请比对你当前版本的具体参数名import json from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from trl import GRPOTrainer, GRPOConfig # 加载base模型和tokenizer model_name ./qwen3-0.6b-base model AutoModelForCausalLM.from_pretrained(model_name) tokenizer AutoTokenizer.from_pretrained(model_name) # 左padding很重要GRPO生成时对不同长度回答做batch padding需要token在左边 tokenizer.padding_side left if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token # 读取训练集 dataset load_dataset(json, data_files./train.jsonl, splittrain) def make_prompt(example): prompt f### 指令 请解决下面的问题。先给出逐步推理过程最后以单独一行输出“最终答案XXXX”。 ### 问题 {example[question]} ### 回答 return {prompt: prompt, answer: example[answer]} dataset dataset.map(make_prompt) train_dataset dataset.remove_columns([question]) # 训练配置 training_args GRPOConfig( output_dir./qwen3-0.6b-grpo, per_device_train_batch_size2, gradient_accumulation_steps8, num_generations8, max_prompt_length512, max_completion_length1024, learning_rate3e-6, lr_scheduler_typecosine, warmup_ratio0.03, beta0.04, logging_steps1, save_steps50, save_total_limit2, num_train_epochs1, ) trainer GRPOTrainer( modelmodel, argstraining_args, train_datasettrain_dataset, reward_funcreward_func, # 用前面写的奖励函数 ) trainer.train()这里有个容易踩的坑tokenizer.padding_side必须设为left否则我们生成的prompt长度不同padding token会出现在文本末尾而模型生成时在末尾接新文本当场就会出现大量无意义的eos token。另外batch size不要设太大因为生成的8条回答都会保存在显存里per_device_train_batch_size2意味着每步同时处理16条生成序列显存压力不小。4.3 训练日志怎么看推理能力有没有真涨训练过程中我会在日志里盯四个指标reward_mean、reward_std、kl、completion_length。刚开始的50步内reward_mean可能比较低因为模型还没有学会输出格式这时候格式分占0.2能保证reward_mean不是零。跑到100步左右reward_mean应该开始明显上升这说明模型已经掌握了“最终答案”这个格式。真正判断推理能力有没有涨核心指标不是reward_mean而是完成文本是否真的包含越来越多的正确答案。我的做法是把日志里的completion随机抽一条打印出来人工看它的推理过程。初期模型输出可能是“我不确定”“无法计算”这类废话中期会输出错误的计算过程但格式正确后期会出现正确的逐步推理。当人工抽查发现大部分回答都遵守“先推理后结论”格式时再拿一个独立评估集测最终答案正确率。注意评估集和训练集要彻底隔离最好对问题做数字改写防止模型靠记忆答题。我自己的实验结果里GRPO训练后的模型在评估集上的最终答案正确率比SFT基线明显高出一截而且推理过程的可读性强很多。另一个更微妙的信号是规规矩矩的“逐步推理”长度变长了模型不再只是给一个数字而是会写“第一步……第二部……”这种过程。这说明强化信号确实把模型推向了策略性思考方向。5. 常见问题和排坑实录5.1 先说一张速查表训练中遇到最多的问题我整理成了下面这个表排查的时候先对照看看症状常见原因应对策略reward均值全程不动组内奖励区分度过低全对或全错调整奖励权重增加格式分换更简单数据KL散度飙升学习率过大或beta过小降低学习率到1e-6调高beta到0.1模型输出全是重复片段生成温度过低或推理长度超限把temperature调到0.9以上检查max_completion_length格式学会后又崩了格式分权重下降太快保持格式分占比不低于0.15评估集正确率到一定值后过拟合训练数据太少或训练步数太长只训1个epoch提前保存评估集最优checkpoint显存爆炸采样数量和batch size叠加过大降低num_generations到4或减小batch size5.2 最值得展开的三个问题第一个是“reward涨了但推理能力没涨”。这是最典型的奖励幻觉。模型可能发现只要输出“最终答案不知道”这种固定短句就能稳定获得格式分于是它找了一条捷径。解决思路是把格式分压小、正确分权重加大同时把“不知道”“无法确定”这类无效回答明确判为0分。也可以用“如果最终答案部分包含非数字字符则正确分直接为0”的规则来封堵。第二个是“组内奖励全为0”。早期训练时模型生成的内容基本和题目无关8条采样全错格式分也是0。这种状态持续久了模型学不到东西。我的办法是把难度降低先加入一批非常简单的加法题保证模型至少能答对一部分同时给格式分一个最低温柔度比如即使格式完全不对也给0.05让组内有一点信号可学。等reward均值脱离零点后再逐渐剪掉这部分基础分。第三个是“KL散度突然崩掉”。这通常发生在学习率偏高或者训练步数太长时模型特征直接被带偏生成结果开始出现毫无逻辑的乱码。我遇到后立刻停住训练把学习率折半把beta从0.04提到0.08然后从最近一个正常checkpoint重新加载。不要心疼损失KL崩溃后继续训练只会浪费时间和显存。6. 最后想分享的几点经验如果只让说一条经验我会说GRPO训练小模型别急着上复杂数据先把奖励函数和格式信号稳住。奖励设计不好再好的算法也白搭。另一个体会是训练日志和生成样例必须人工抽查不能只看loss曲线。强化学习训练太容易跑偏机器觉得分数高了实际上只是模型发现了奖励函数的漏洞。实际操作中还有一个特别实用的小技巧在每个训练stage保存的checkpoint里各自跑一次独立评估集的生成抽样对比“答案正确率”和“格式服从度”。这样就能精确知道强化学习到底在提升哪个维度。训练结束后把最后几个checkpoint都跑一遍评估选最优的一个用于后续SFT或部署不要默认取最后一个因为最后几步往往已经开始过拟合了。这个路线后续还能继续扩展比如在GRPO训练得到的模型基础上再用少量SFT数据做一轮整理把输出格式打磨得更干净效果往往比其中一个单独训练要稳定。小模型推理能力不是一锤子买卖GRPO只是把模型的潜力激发出来后面的工程化调优还需要慢慢磨。