fine_tuning_llm_grpo_trl.ipynb 逐格说明这份 notebook 是你们进入 RL 后训练的第一站。
它和前面已经做过的 course_lora_qwen_src 最大的关系是:
course_lora_qwen_src:你们做的是 SFT / LoRA 监督微调所以读它时,最应该一直问的问题是:
这一步和前面监督微调相比,究竟多了什么?
开头几格在说明:
前面 SFT 是“给模型看标准答案”。
这里 GRPO 更像是“让模型多试几次,再按奖励告诉它哪种输出更好”。
!pip install -U -q trl peft math_verifytrl
peft
math_verify
在 course_lora_qwen_src 里你们已经装过:
transformerspeft这里多出来的核心是:
trlmath_verify也就是说,从这一步开始,你已经不是单纯在做 SFT 了。
notebook_login()from huggingface_hub import notebook_login
notebook_login()登录 Hugging Face 账号。
因为后面这份 notebook 可能会:
dataset_id = 'AI-MO/NuminaMath-TIR'
train_dataset, test_dataset = load_dataset(dataset_id, split=['train[:5%]', 'test[:5%]'])load_dataset(...) 还是那套 Hugging Face datasets 流程5%,说明这是一份演示 / 教学版配置,不是正式长时间训练这和你们在 course_lora_qwen_src 里自己写:
train_file = ...
val_file = ...不一样。
前面是自己准备本地 JSON,这里是直接从 Hub 拉现成数学推理数据集。
这两格的意义非常重要:
无论是 SFT 还是 RL,第一步永远不是“先调模型”,而是“先看数据到底长什么样”。
这和你们前面做:
print(train_dataset[0])print(len(train_ds), len(val_ds))是完全同一条调试思路。
SYSTEM_PROMPT 和 make_conversation(...)这是整份 notebook 的第一个核心代码格。
SYSTEM_PROMPT它定义了模型输出的目标格式:
<think> ... </think> 里写推理过程<answer> ... </answer> 里写答案因为后面的奖励函数需要有一个明确可检查的输出格式。
如果模型乱写一通、没有边界标记,就很难自动给 reward。
make_conversation(example)这个函数把原始数学题转换成:
{
"prompt": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": example["problem"]},
]
}你们在 course_lora_qwen_src 里也写过:
messages = [
{"role":"system", ...},
{"role":"user", ...},
]这说明:
print(train_dataset[0]['prompt'])这一格的作用是确认:
map(make_conversation) 之后,数据真的变成了 prompt 列train_dataset = train_dataset.remove_columns(['messages', 'problem'])因为后面训练时,数据集里只需要:
prompt保留太多无关列,容易让数据流更混乱。
model_id = "Qwen/Qwen2-0.5B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype="auto",
device_map="auto",
)这里和 course_lora_qwen_src 很像:
AutoModelForCausalLM但少了你们前面手动写的:
dtype = ...trust_remote_code=True它更像教程里的“默认可运行版本”。
LoraConfig(...)lora_config = LoraConfig(
task_type="CAUSAL_LM",
r=8,
lora_alpha=32,
lora_dropout=0.1,
target_modules=["q_proj", "v_proj"],
)和你们前面 course_lora_qwen_src 比:
q_proj k_proj v_proj o_proj gate_proj up_proj down_projq_proj 和 v_proj因为它想先把 RL 主线跑通,不想在 LoRA 配置上把学生绕晕。
model.print_trainable_parameters()这一步和前面一样,仍然是要确认:
这是整份 notebook 最关键的“RL 和 SFT 的分界线”。
format_reward(...)pattern = r"^<think>.*?</think>\s*<answer>.*?</answer>$"检查输出有没有遵守指定格式。
因为在 RL 里,你不再只是用“标准答案 token”监督模型。
你可以直接按输出行为打分。
这里的打分逻辑是:
这就是最简单的 reward function。
accuracy_reward(...)这一格比前一个更“内容导向”。
不是只看格式,而是看答案对不对。
parse(...)
verify(...)
它说明:
reward 不一定来自人工标注,也可以来自“程序可验证的规则”。
这就是第 13 章里经常提到的:
GRPOConfig(...)这是这份 notebook 的训练配置核心。
output_dirlearning_rate=1e-5remove_unused_columns=Falsegradient_accumulation_steps=16num_train_epochs=1bf16=Truemax_completion_length=64num_generations=4num_generations=4这在 SFT 里没有。
因为 RL 这里通常会:
remove_unused_columns=False这一行特别关键。
为什么?
因为 reward function 里还要访问例如 solution 这种字段。
如果把“训练器没直接用到的列”都删掉,reward function 就拿不到标准答案了。
max_completion_length这里比 SFT 更强调“生成长度”,因为 RL 训练时真的会先生成回答,再打分。
GRPOTrainer(...)trainer = GRPOTrainer(
model=model,
reward_funcs=[format_reward, accuracy_reward],
args=training_args,
train_dataset=train_dataset
)SFTTrainer(...) 直接对照SFTTrainer:吃标准答案,做监督学习GRPOTrainer:先生成多个回答,再按 reward 优化所以这是整份 notebook 最核心的角色切换。
trainer.train()这一行还是训练入口,但你现在应该意识到:
它内部发生的事情,和前面的 trainer.train() 已经不一样了。
trainer.save_model(training_args.output_dir)
trainer.push_to_hub(dataset_name=dataset_id)这和前面 LoRA 最小实验、Chapter5 保存 adapter 的逻辑是同源的。
重新加载已经训练好的模型和 tokenizer。
generate_with_reasoning(prompt)这是推理辅助函数。
它做了这些事:
model.generate(...)因为 RL 训练不是只看训练指标,还要看:
这里是在检查:
这一步非常像你们前面在 Qwen 最小实验里做的“训练完就立即试一题”,只是现在多看了:
GRPOTrainer