TellWorldAI 世界档案
问档案English

首页›实践›实践

实践

Qwen3 GRPO 强化学习微调

用 Unsloth 对 Qwen3-8B 做 GRPO 强化学习微调:先 SFT 打底,再设计奖励函数训练,并用 SwanLab 记录实验。读完能看懂每一步的作用。

这篇适合谁

适合已经会用 Hugging Face 训练大模型、想尝试强化学习(RL)微调推理模型的人。读完你应该能理解 GRPO 的基本流程,用 Unsloth 加载 Qwen3-8B 并挂上 LoRA,写出奖励函数,跑通一次 GRPO 训练,并用 SwanLab 记录结果。

强化学习和 GRPO 是什么

强化学习解决的是序贯决策问题,智能体通过与环境互动、根据奖励不断调整策略[1]。GRPO(Group Relative Policy Optimization,组相对策略优化)可以看作 PPO 的一种变体或改进,目标是提高训练效率和稳定性[1]。它尝试去掉或简化价值模型(Critic),靠组内的相对比较来估计每个输出的优势[1]。

GRPO 的流程大致是:对同一个 prompt 生成多组回答,用正确性检查或奖励函数给每个回答打分,算出组内平均分,再把每个回答和平均分比较,强化得分更高的回答[1]。规则化的奖励函数或验证器可以代替 LLM 奖励模型,从而节省训练成本和资源[1]。DeepSeek 的研究人员在用纯强化学习训练 R1-Zero 时,观察到模型无需人工指导,就学会了重新评估自己的方法并延长思考[1]。

为什么选 Unsloth

常见的 RL 框架有 TRL(Hugging Face 开发)、VeRL(字节跳动开发)和 Unsloth[1]。VeRL 面向分布式训练,两张卡及以上用起来才顺手;Ray 的学习成本对新手偏高[1]。Unsloth 则可以在单卡上以很低的资源占用对 8B 模型做 RL[1]。

Unsloth 称,它能把 Llama-3、Mistral、Phi-4 和 Gemma 的微调速度提高到 2 倍,内存占用降低 70%,且准确率没有下降[1]。对 Qwen3(8B),它称微调速度快 2 倍、显存减少 70%,并且支持的上下文长度是使用 Flash Attention 2 的环境的 8 倍[1]。项目地址是 docs.unsloth.ai 和 GitHub 上的 unslothai/unsloth[1][2]。

环境和依赖

本文的测试环境是单张 A100,显存 80 GB[1]。安装命令如下[1]:

pip install unsloth==2025.3.19 vllm==0.8.2

加载模型并挂上 LoRA

用 FastLanguageModel.from_pretrained 加载 Qwen/Qwen3-8B。max_seq_length 设为 2048,fast_inference=True 表示启用 vLLM 推理;显存不够时,把 gpu_memory_utilization 调低[1]:

from unsloth import FastLanguageModel
import torch

max_seq_length = 2048  # 最大序列长度,可以增加以支持更长的推理轨迹
lora_rank = 32         # LoRA 的秩,秩越大模型可能越智能,但训练和推理速度会变慢

# 从预训练模型加载模型和分词器
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="Qwen/Qwen3-8B",  # 要加载的预训练模型名称(官网版本/离线版本/unsloth版本均可)
    max_seq_length=max_seq_length,        # 设置模型的最大序列长度
    load_in_4bit=False,                   # 是否以4位加载模型,对于LoRA 16位训练,设置为False
    fast_inference=True,                  # 是否启用 vLLM 快速推理
    max_lora_rank=lora_rank,              # 设置 LoRA 的最大秩
    gpu_memory_utilization=0.7,           # GPU显存使用率,如果显存不足 (OOM),可以降低此值
)

LoRA 通过 get_peft_model 添加,示例中秩为 32,lora_alpha 设为秩的两倍[1]:

# 为模型添加 PEFT (Parameter-Efficient Fine-Tuning) 配置,这里使用 LoRA
model = FastLanguageModel.get_peft_model(
    model,
    r=lora_rank,  # LoRA 的秩 (r),选择任何大于0的数字!建议值为 8, 16, 32, 64, 128
    target_modules=[  # 需要应用LoRA的模块名称列表
        "q_proj", "k_proj", "v_proj", "o_proj",  # 注意力机制中的查询、键、值、输出投影
        "gate_proj", "up_proj", "down_proj",     # 前馈网络中的门控、上行和下行投影
    ],
    lora_alpha=lora_rank * 2,  # LoRA 的 alpha 参数,设置为秩的2倍可以加速训练
    use_gradient_checkpointing="unsloth",  # 是否使用梯度检查点技术,"unsloth" 表示使用其优化版本以减少显存使用
    random_state=3407,                   # 随机种子,用于确保结果的可复现性
)

设置推理格式

训练前要让模型学会固定的输出格式。示例定义了四个标签:<start_working_out> 和 <end_working_out> 包住推理过程,<SOLUTION> 和 </SOLUTION> 包住最终答案[1]:

reasoning_start = "<start_working_out>"     # 用于包裹模型的“思考过程”开始部分
reasoning_end   = "<end_working_out>"       # 用于包裹模型的“思考过程”结束部分
solution_start  = "<SOLUTION>"              # 用于包裹最终解答开始部分
solution_end    = "</SOLUTION>"             # 用于包裹最终解答结束部分

系统提示词告诉模型先写推理过程,再给出答案[1]:

system_prompt = \
f"""You are given a problem.
Think about the problem and provide your working out.
Place it between {reasoning_start} and {reasoning_end}.
Then, provide your solution between {solution_start}{solution_end}"""

先做 SFT,再做 GRPO

数据集的准备分两步。SFT 阶段用 unsloth/OpenMathReasoning-mini 的 cot 切分,只保留预期答案为数字的样本[1];随后只保留 token 长度在 90% 分位数以内的样本[1]。SFT 用 SFTTrainer,训练 2 个 epoch,学习率 2e-4,每张卡的 batch size 为 1[1]。

GRPO 阶段换成 open-r1/DAPO-Math-17k-Processed 的 en 配置、train 切分[1]。

设计奖励函数

GRPO 的效果很大程度取决于奖励函数。示例用了四个:

  • match_format_exactly:回答完整符合推理和答案的格式时得 3.0 分[1]。
def match_format_exactly(completions, **kwargs):
    scores = []  # 用于保存每个 completion 的得分

    for completion in completions:
        score = 0
        response = completion[0]["content"]  # 获取模型输出内容(假设为 messages 列表中的第一个 assistant 回复)

        # 如果输出内容能成功匹配指定格式(即包含完整 <start_working_out>...<SOLUTION>... 标签结构)
        if match_format.search(response) is not None:
            score += 3.0  # 匹配成功得 3 分(用于奖励格式正确的输出)

        scores.append(score)  # 保存该条 completion 的得分

    return scores  # 返回所有 completion 的格式匹配得分列表
  • match_format_approximately:三个结束或答案标签各出现一次,每个加 0.5 分;出现次数不对则每个扣 1.0 分[1]。
  • check_answer:答案与参考答案完全一致得 5.0 分,去掉空格后一致得 3.5 分,数值误差在 10% 以内得 2.0 分,在 20% 以内得 1.5 分,其余扣 2.5 分;无法解析为数字扣 4.5 分,没有提取到答案扣 2.0 分[1]。
  • check_numbers:提取出的数字与答案完全相等得 3.5 分,否则扣 1.5 分;解析失败记 0 分,没提取到数字扣 2.5 分[1]。

GRPO 训练配置

示例中每个 prompt 生成 4 个回答,num_generations 越大效果可能越好,但显存占用也越高;显存不够时应减小它[1]。采样温度为 1.0,学习率 5e-6,max_steps 为 100,适合调试[1]:

training_args = GRPOConfig(
    vllm_sampling_params = vllm_sampling_params,  # 用于生成 completion 的采样策略
    temperature = 1.0,               # 生成的多样性控制(通常设为 0.7 ~ 1.0)
    learning_rate = 5e-6,            # 训练的学习率(较小以保证稳定收敛)
    weight_decay = 0.01,             # 权重衰减,用于防止过拟合
    warmup_ratio = 0.1,              # warmup 步数占总训练步数的比例(通常为 0.05 ~ 0.1)
    lr_scheduler_type = "linear",    # 学习率调度方式为线性下降
    optim = "adamw_8bit",            # 使用 bitsandbytes 的 8bit AdamW 优化器(省显存)

    logging_steps = 1,               # 每一步打印一次日志(适合 debug)
    per_device_train_batch_size = 1, # 每张 GPU 的 batch size
    gradient_accumulation_steps = 1, # 梯度累积步数(设为 4 可等效 batch size=4)

    num_generations = 4,             # 每个 prompt 生成多少个 response(越多越好,但显存消耗更大)
    max_prompt_length = max_prompt_length,         # 提示 token 最大长度(前面计算得出)
    max_completion_length = max_completion_length, # 回答 token 最大长度(确保总长不超过模型限制)

    # num_train_epochs = 1,          # 可选参数:训练轮次,设置后可按 epoch 控制训练终止
    max_steps = 100,                 # 最大训练步数(适合调试用,正式训练时可加大)
    save_steps = 100,                # 每 100 步保存一次模型(训练短时可以不存)

    report_to = "none",              # 不上传训练日志(可设置为 "wandb" 使用可视化工具)
    output_dir = "outputs",          # 模型输出路径

训练器把四个奖励函数一起交给 GRPOTrainer[1]:

trainer = GRPOTrainer(
    model = model,  # 需要训练的语言模型(必须支持 causal LM 格式,如 GPT、LLaMA 等)

    processing_class = tokenizer,  # 用于生成 prompt、解码 response 的 tokenizer(需支持 chat_template)

    reward_funcs = [  # 自定义奖励函数列表,用于计算每条样本的得分
        match_format_exactly,       # 检查是否严格符合 <start_working_out>...<SOLUTION> 格式,匹配得分高
        match_format_approximately, # 检查是否大致有格式标签,宽松评分
        check_answer,               # 与参考答案对比,进行精确匹配、模糊匹配、近似数值匹配
        check_numbers,              # 提取数值进行比较(用于数学题)
    ],

    args = training_args,  # 训练参数配置(使用前面定义好的 GRPOConfig 对象)

    train_dataset = dataset,  # 实际用于训练的数据集

    # 可选:如启用训练 + 验证评估,可替换为如下配置
    # train_dataset = new_dataset["train"],
    # eval_dataset = new_dataset["test"],
)

保存和测试

训练结束后,用 model.save_lora("grpo_saved_lora") 保存 LoRA 权重,并检查保存的 safetensors 文件中没有全零的张量[1]:

model.save_lora("grpo_saved_lora")

合并保存、只存 LoRA 以及 GGUF 格式(如 Q8_0、f16、q4_k_m)的代码都列在示例里,但都包在 if False: 中,默认不执行,需要时改成 True[1]。

用 SwanLab 记录实验

SwanLab 是开源的训练记录工具,提供训练可视化、自动日志、超参数记录、实验对比和多人协作等功能[1][3]。使用前先到 swanlab.cn 注册账号,然后在 SFT 和 GRPO 的配置里把 report_to 改为 "swanlab"[1]:

        report_to = "swanlab", # 这里改成swanlab

下一步

相关论文可以读 DeepSeek-R1 的论文,地址是 arxiv.org/abs/2501.12948[1]。Unsloth 的参数和更多训练方式见官方文档 docs.unsloth.ai。

本文改编自 Datawhale 开源项目 self-llm 中的教程《Qwen3-8B GRPO微调及通过swanlab可视化》[1],原文以 Apache-2.0 许可证发布。

来源

  1. 1
    Qwen3-8B GRPO微调及通过swanlab可视化Datawhale self-llm (Apache-2.0)
  2. 2
    unsloth (GitHub)Unsloth (GitHub)
  3. 3
    SwanLab (GitHub)SwanLab (GitHub)

引用本页: Qwen3 GRPO 强化学习微调. TellWorld, https://www.tellworld.net/zh/practice/practice-qwen3-grpo. 更新于 2026-11-14

相关动态