fine_tuning_llm_grpo_trl.ipynb 逐格说明

这份 notebook 是你们进入 RL 后训练的第一站。
它和前面已经做过的 course_lora_qwen_src 最大的关系是:

所以读它时,最应该一直问的问题是:

这一步和前面监督微调相比,究竟多了什么?

Cells 1-3:标题和背景

开头几格在说明:

编程小白先抓一句话

前面 SFT 是“给模型看标准答案”。
这里 GRPO 更像是“让模型多试几次,再按奖励告诉它哪种输出更好”。

Cell 4:安装依赖

!pip install  -U -q trl peft math_verify

每个包大概在做什么

和前面最小实验的对应

course_lora_qwen_src 里你们已经装过:

这里多出来的核心是:

也就是说,从这一步开始,你已经不是单纯在做 SFT 了。

Cell 6:notebook_login()

from huggingface_hub import notebook_login
notebook_login()

它在做什么

登录 Hugging Face 账号。

为什么这里要登录

因为后面这份 notebook 可能会:

Cell 8:加载数据集

dataset_id = 'AI-MO/NuminaMath-TIR'
train_dataset, test_dataset = load_dataset(dataset_id, split=['train[:5%]', 'test[:5%]'])

这里最值得初学者看什么

和前面课程对照

这和你们在 course_lora_qwen_src 里自己写:

train_file = ...
val_file = ...

不一样。
前面是自己准备本地 JSON,这里是直接从 Hub 拉现成数学推理数据集。

Cells 10-12:检查数据结构

这两格的意义非常重要:

为什么这一步不能省

无论是 SFT 还是 RL,第一步永远不是“先调模型”,而是“先看数据到底长什么样”。

这和你们前面做:

是完全同一条调试思路。

Cell 14:SYSTEM_PROMPTmake_conversation(...)

这是整份 notebook 的第一个核心代码格。

SYSTEM_PROMPT

它定义了模型输出的目标格式:

为什么要这么做

因为后面的奖励函数需要有一个明确可检查的输出格式。

如果模型乱写一通、没有边界标记,就很难自动给 reward。

make_conversation(example)

这个函数把原始数学题转换成:

{
  "prompt": [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": example["problem"]},
  ]
}

和前面 Qwen 最小实验的对应

你们在 course_lora_qwen_src 里也写过:

messages = [
  {"role":"system", ...},
  {"role":"user", ...},
]

这说明:

Cell 16:打印一个 prompt

print(train_dataset[0]['prompt'])

这一格的作用是确认:

Cell 18:删掉多余列

train_dataset = train_dataset.remove_columns(['messages', 'problem'])

为什么删列

因为后面训练时,数据集里只需要:

保留太多无关列,容易让数据流更混乱。

Cells 22-24:加载模型和 LoRA

Cell 22:加载 baseline model

model_id = "Qwen/Qwen2-0.5B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    torch_dtype="auto",
    device_map="auto",
)

和前面最小实验的对照

这里和 course_lora_qwen_src 很像:

但少了你们前面手动写的:

它更像教程里的“默认可运行版本”。

Cell 24: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 比:

为什么教程会这样简化

因为它想先把 RL 主线跑通,不想在 LoRA 配置上把学生绕晕。

model.print_trainable_parameters()

这一步和前面一样,仍然是要确认:

Cells 26-28:奖励函数

这是整份 notebook 最关键的“RL 和 SFT 的分界线”。

Cell 26:format_reward(...)

pattern = r"^<think>.*?</think>\s*<answer>.*?</answer>$"

它在做什么

检查输出有没有遵守指定格式。

为什么这就是 reward

因为在 RL 里,你不再只是用“标准答案 token”监督模型。
你可以直接按输出行为打分。

这里的打分逻辑是:

这就是最简单的 reward function。

Cell 28:accuracy_reward(...)

这一格比前一个更“内容导向”。

它在做什么

不是只看格式,而是看答案对不对。

关键函数

为什么这一格很重要

它说明:

reward 不一定来自人工标注,也可以来自“程序可验证的规则”。

这就是第 13 章里经常提到的:

Cell 30:GRPOConfig(...)

这是这份 notebook 的训练配置核心。

重点参数

这些参数和前面 SFT 的差别

num_generations=4

这在 SFT 里没有。
因为 RL 这里通常会:

remove_unused_columns=False

这一行特别关键。

为什么?

因为 reward function 里还要访问例如 solution 这种字段。
如果把“训练器没直接用到的列”都删掉,reward function 就拿不到标准答案了。

max_completion_length

这里比 SFT 更强调“生成长度”,因为 RL 训练时真的会先生成回答,再打分。

Cell 33:GRPOTrainer(...)

trainer = GRPOTrainer(
    model=model,
    reward_funcs=[format_reward, accuracy_reward],
    args=training_args,
    train_dataset=train_dataset
)

这一格可以和 Chapter5 的 SFTTrainer(...) 直接对照

所以这是整份 notebook 最核心的角色切换。

Cell 35:trainer.train()

这一行还是训练入口,但你现在应该意识到:

它内部发生的事情,和前面的 trainer.train() 已经不一样了。

前面 SFT 训练时

这里 GRPO 训练时

Cell 37:保存和 push

trainer.save_model(training_args.output_dir)
trainer.push_to_hub(dataset_name=dataset_id)

这和前面 LoRA 最小实验、Chapter5 保存 adapter 的逻辑是同源的。

Cells 41-51:加载训练后模型并推理

Cell 41

重新加载已经训练好的模型和 tokenizer。

Cell 45:generate_with_reasoning(prompt)

这是推理辅助函数。

它做了这些事:

  1. 把 prompt 里的消息拼成字符串
  2. tokenizer 编码
  3. model.generate(...)
  4. 统计推理时间
  5. 统计生成 token 数

为什么这一步重要

因为 RL 训练不是只看训练指标,还要看:

Cells 47-51:查看推理效果

这里是在检查:

对零基础学生的意义

这一步非常像你们前面在 Qwen 最小实验里做的“训练完就立即试一题”,只是现在多看了:

给零基础学生的最短总结

  1. 这份 notebook 仍然以 chat prompt + LoRA 为基础
  2. 和 SFT 真正不同的地方,是多了 reward function 和 GRPOTrainer
  3. reward 可以同时看“格式对不对”和“答案对不对”
  4. RL 后训练的目标,是让模型输出更符合我们定义的好行为