GRPO - 🎯转了码的刘公子# Summary
这里的 group,是大模型对一同一 prompt 生成的多个回答
这就像**让一个学生做同一道题多次**,然后告诉他哪些答案更好,帮助他改进!
**对齐对象**:AI 生成的组内得分最高的答案
```python
# 同一个prompt生成多个回答
prompt = "请解释什么是机器学习"
group_responses = [
"机器学习是人工智能的一个分支,通过算法让计算机从数据中学习。", # 得分:0.9
"机器学习是一种让计算机自动改进的技术。", # 得分:0.7
"机器学习让计算机能够识别模式。", # 得分:0.5
"机器学习就是让机器学习。" # 得分:0.3
]
# 对齐目标:组内得分最高的答案(0.9分的那个)
best_response = "机器学习是人工智能的一个分支,通过算法让计算机从数据中学习。"
```
## **训练过程**
```python
# 1. 计算组归一化奖励
normalized_rewards = [0.5, 0.0, -0.5, -1.0] # 相对排名
# 2. 使用GRPO-Clip损失
loss = grpo_clip_loss(policy_log_probs, old_log_probs, normalized_rewards)
# 目标:让模型学会产生更高得分的回答
```
# Cues
GRPO 主要来自两篇论文:
1. DeepSeekMath (2024)
- 标题:Group Relative Policy Optimization
- 用于数学问题求解
2. DeepSeek-R1 (2025)
- 标题:R1: A Simple and Effective Reward Function for Aligning Language Models
- 扩展应用到更广泛的任务
# Notes
**完全正确!** 你理解得很准确!它们确实是**串行执行**的,不是并行的。
## 串行执行流程
### 1. **完整的训练链路**
```python
# 步骤1: 计算组归一化奖励
normalized_rewards, raw_rewards, metadata = compute_group_normalized_rewards(
reward_fn=reward_fn,
rollout_responses=rollout_responses,
repeated_ground_truths=repeated_ground_truths,
group_size=group_size,
advantage_eps=advantage_eps,
normalize_by_std=normalize_by_std,
)
# 步骤2: 使用归一化奖励计算策略梯度损失
loss = compute_naive_policy_gradient_loss(
raw_rewards_or_advantages=normalized_rewards, # 使用步骤1的输出
policy_log_probs=policy_log_probs,
)
```
### 2. **数据流向**
```Java
原始奖励 → 组归一化 → 归一化奖励 → 策略梯度损失 → 模型更新
↓ ↓ ↓ ↓ ↓
raw_rewards → normalize → normalized_rewards → loss → gradients
```
### 3. **为什么是串行的?**
#### **逻辑依赖关系**
```python
# 必须先有归一化奖励,才能计算损失
normalized_rewards = compute_group_normalized_rewards(...) # 步骤1
loss = compute_naive_policy_gradient_loss(normalized_rewards, ...) # 步骤2
```
#### **数据依赖**
- `compute_naive_policy_gradient_loss` 的输入是 `raw_rewards_or_advantages`
- 这个输入来自 `compute_group_normalized_rewards` 的输出
- 没有步骤1的输出,步骤2就无法执行
### 4. **实际训练中的完整流程**
```python
def train_step():
# 1. 生成多个回答
responses = model.generate(prompts, num_return_sequences=group_size)
# 2. 计算原始奖励
raw_rewards = [reward_fn(response, ground_truth) for response in responses]
# 3. 组归一化奖励 ← 第一个函数
normalized_rewards = compute_group_normalized_rewards(...)
# 4. 计算策略梯度损失 ← 第二个函数
loss = compute_naive_policy_gradient_loss(normalized_rewards, ...)
# 5. 反向传播
loss.backward()
# 6. 更新模型参数
optimizer.step()
```
## 组归一化奖励
### Group 的含义
#### 1. **Group 就是同一 prompt 的多个回答**
```python
# 一个 prompt
prompt = "请解释什么是机器学习"
# 对这个 prompt 生成多个回答(group_size = 4)
responses = [
"机器学习是人工智能的一个分支,通过算法让计算机从数据中学习。",
"机器学习是一种让计算机自动改进的技术,不需要显式编程。",
"机器学习是AI的核心技术,通过训练数据来预测和决策。",
"机器学习让计算机能够识别模式并做出预测。"
]
# 这就是一个 group,包含4个回答
```
#### 2. **为什么叫 "Group"?**
在强化学习中,**Group** 指的是:
- **同一个问题**(prompt)
- **多个不同的回答**(rollout responses)
- **组成一个组**,用于比较和归一化
#### 3. **组归一化的目的**
```python
# 原始奖励(可能差异很大)
rewards = [0.85, 0.92, 0.78, 0.89] # 同一问题的4个回答
# 组内归一化后(相对表现更清晰)
normalized_rewards = [0.0, 1.0, -1.0, 0.5] # 相对排名
```
#### 4. **实际训练中的例子**
```python
def generate_responses_for_prompt(prompt, group_size=4):
"""对同一个prompt生成多个回答"""
responses = []
for _ in range(group_size):
response = model.generate(prompt) # 每次生成可能不同
responses.append(response)
return responses
# 训练数据
prompts = [
"请解释什么是机器学习",
"写一首关于春天的诗",
"如何做红烧肉"
]
# 对每个prompt生成多个回答
all_responses = []
for prompt in prompts:
group_responses = generate_responses_for_prompt(prompt, group_size=4)
all_responses.extend(group_responses) # 4个回答为一组
```
#### 5. **数据组织结构**
```python
# 完整的数据结构
rollout_responses = [
# Group 1: "请解释什么是机器学习" 的4个回答
"机器学习是人工智能的一个分支...",
"机器学习是一种让计算机自动改进的技术...",
"机器学习是AI的核心技术...",
"机器学习让计算机能够识别模式...",
# Group 2: "写一首关于春天的诗" 的4个回答
"春天来了,花儿开了...",
"春风轻抚大地...",
"万物复苏的季节...",
"春天的脚步近了...",
# Group 3: "如何做红烧肉" 的4个回答
"首先准备五花肉...",
"将肉切成块状...",
"锅中放油烧热...",
"加入调味料...",
]
# 对应的标准答案也要重复
repeated_ground_truths = [
# 每个标准答案重复4次(对应group_size)
"机器学习是人工智能的一个分支", # 重复4次
"机器学习是人工智能的一个分支",
"机器学习是人工智能的一个分支",
"机器学习是人工智能的一个分支",
"春天是一年四季中最美的季节", # 重复4次
"春天是一年四季中最美的季节",
"春天是一年四季中最美的季节",
"春天是一年四季中最美的季节",
"红烧肉的做法是...", # 重复4次
"红烧肉的做法是...",
"红烧肉的做法是...",
"红烧肉的做法是...",
]
```
### 总结
**Group = 同一 prompt 的多个回答**
- **目的**:让模型学会在同一个问题上产生更好的回答
- **方法**:通过组内比较,让模型知道哪些回答更好
- **效果**:提高模型回答的质量和一致性
这就像**让一个学生做同一道题多次**,然后告诉他哪些答案更好,帮助他改进!
这个名字由两个部分组成:
1. **GRPOTrainer**:
"GRPO"代表"Group Relative Policy Optimization",是一种基于策略优化的训练方法,用来根据生成结果的奖励对模型进行微调。这里的"Trainer"指的是使用这种方法进行训练的工具或训练器。
2. **DeepseekR1**:
"DeepseekR1"是所使用的预训练模型的名称或版本,表示这是 Deepseek 系列中的 R1 版本。
综合起来,**GRPOTrainer-DeepseekR1** 的意思就是"利用基于 Group Relative Policy Optimization 方法的训练器对 Deepseek R1 模型进行微调"。这也说明了整个项目或 Notebook 的核心内容。
下面我将以比较通俗易懂的方式,按代码块(cell)来逐行解释这份 Notebook 的主要代码内容。注意:由于代码量比较大,我会重点解释核心部分和关键行,让你对整个流程有个清晰的认识。
---
# 1. 安装依赖包
```python
!python -m pip install --no-index -v --find-links=/kaggle/input/aimo-packages/offline_packages trl --pre
```
- **解释**:这一行通过 pip 安装名为 `trl` 的包(一个用于强化学习训练语言模型的工具包)。
- 参数说明:
- `--no-index`:不从网络上查找,而是从本地指定的目录安装。
- `--find-links` 指定了离线包所在的路径。
- `--pre`:允许安装预发布版本。
接下来的几个安装命令类似,分别安装了其他依赖包(如 `levenshtein` 和 `bitsandbytes`),保证整个训练环境的依赖都就绪。
---
# 2. 设置环境变量
```python
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
os.environ["TOKENIZERS_PARALLELISM"] = "false"
```
- **解释**:
- 第一行设置使用 GPU 设备的编号为 0、1、2、3(即使用四块 GPU)。
- 第二行关闭了 Tokenizer 的并行处理,以避免可能的多线程警告或错误。
---
# 3. 导入所需模块
```python
from datasets import load_dataset, Dataset
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, PeftModel
from trl import GRPOConfig, GRPOTrainer
import datetime
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
HfArgumentParser,
AutoTokenizer,
TrainingArguments,
Trainer,
GenerationConfig,
PrinterCallback,
)
from tqdm import tqdm
import torch
import time
import transformers
import pandas as pd
import numpy as np
from Levenshtein import ratio as levenshtein_ratio
transformers.set_seed(42)
```
- **解释**:
- 导入了 Hugging Face 的数据集工具、模型和 tokenizer、以及用于 PEFT(参数高效微调)的工具。
- `trl` 包中导入了 GRPOTrainer 及相关配置,用于基于策略优化的训练。
- 导入了 `tqdm` 用于显示进度条、`torch`、`pandas`、`numpy` 等常用包。
- 最后一行设置了随机种子为 42,以确保实验可重复。
---
# 4. 配置参数类
```python
class CFG:
MAX_TRAIN = 100
MAX_TOKENS = 2048
NUM_GENERATIONS = 4
USE_PEFT = True
BATCH_SIZE = 1
MAX_STEPS = 80
BETA = 0.04
LR = 1.e-5
model_name = '/kaggle/input/deepseek-r1/transformers/deepseek-r1-distill-qwen-1.5b/1'
splitter = '<|Assistant|>'
step_count = 10
DEBUG = False
```
- **解释**:
- 定义了一个配置类 `CFG`,里面保存了训练中的各种超参数:
- `MAX_TRAIN`:用于训练的数据量(这里只取前 100 个例子)。
- `MAX_TOKENS`:每个生成样本最大的 token 数量。
- `NUM_GENERATIONS`:生成多少个答案版本(用于评估奖励)。
- `USE_PEFT`:是否使用 PEFT 技术来微调模型(参数高效微调)。
- `BATCH_SIZE`、`MAX_STEPS`、`BETA`(可能控制奖励项的权重)、学习率 `LR` 等。
- `model_name`:预训练模型所在的路径。
- `splitter`:用来分割生成文本的标记。
- `step_count`:日志打印或保存的步数间隔。
- `DEBUG`:是否处于调试模式。
---
# 5. 定义辅助函数:提取答案
```python
import re
def extract_boxed_text(text):
pattern = r'oxed{(.*?)}'
matches = re.findall(pattern, text)
if not matches:
return ""
for match in matches[::-1]:
if match != "":
return match
return ""
```
- **解释**:
- 这个函数用正则表达式从文本中提取被"oxed{...}"包围的部分(可能原本意图是提取 `\boxed{...}` 里的内容,但正则中只写了 `oxed{...}`)。
- 先搜索所有匹配项,若没有匹配则返回空字符串;否则返回最后一个非空的匹配结果。
---
# 6. 读取与处理数据集
```python
df = pd.read_parquet('/kaggle/input/math-problems-imo/math_problems.parquet')
df = df.reset_index().rename({'index': 'id'}, axis=1)
df['answer'] = df['solution'].map(extract_boxed_text)
def is_valid_answer(s):
try:
if float(s) == int(s):
i = int(s)
return 0 <= i < 1000
else:
return False
except ValueError:
return False
mask = df['answer'].map(is_valid_answer)
df = df[mask]
```
- **解释**:
- 使用 pandas 读取一个 parquet 格式的数据集,该数据集包含数学问题(以及解决方案)。
- 重置索引并将原索引重命名为 `id`。
- 对每个 `solution` 字段调用前面定义的 `extract_boxed_text` 函数,提取出答案部分,并保存在 `answer` 列中。
- 定义了 `is_valid_answer` 函数,检查提取出的答案是否为数字,并且在 0 到 1000 范围内。
- 利用这个函数过滤数据,只保留答案有效的行。
接着:
```python
df = df.iloc[:CFG.MAX_TRAIN]
```
- 只取前 100 个样本(由配置中的 `MAX_TRAIN` 控制)。
然后:
```python
dataset = Dataset.from_pandas(df)
dataset = dataset.train_test_split(test_size=0.1)
```
- 将 pandas DataFrame 转换为 Hugging Face 的 Dataset 对象,并按 90%/10% 划分训练和测试集。
---
# 7. 创建 prompt(提示)模板
```python
def create_prompt(sample):
question = sample['problem']
chat = [{"role": "system", "content": "A conversation between User and Assistant. The user asks a question, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process and answer are enclosed within <think> </think> and <answer> </answer> tags, respectively, i.e., <think> reasoning process here </think> <answer> answer here </answer>"},
{"role": "user", "content": question + " Return final answer within \\boxed{}, after taking modulo 1000."},]
sample['prompt'] = tokenizer.apply_chat_template(
conversation=chat,
tokenize=False,
add_generation_prompt=True
)
return sample
```
- **解释**:
- 该函数为每个样本创建一个对话式的提示:
- 第一条信息是系统消息,说明角色分工:用户提问,助手思考后回答,并且要求回答过程(思考部分)和最终答案分别放在 `<think>` 与 `<answer>`(在这里实际使用了 `\\boxed{}` 标记)中。
- 用户消息为实际的问题,加上一段指令"返回答案时将答案放在 \boxed{} 内,且对答案取模 1000"。
- 最后利用 tokenizer 的 `apply_chat_template` 方法将对话内容转换为模型所需的 prompt 格式,并保存在样本的 `prompt` 字段中。
---
# 8. 查看一个样本
```python
dataset['train'][0]
```
- **解释**:显示训练集中的第一个样本,以便你了解数据结构和 prompt 格式是否正确。
---
# 9. 定义奖励函数
奖励函数用于给模型生成的回答打分,主要有三种:
## a. 格式奖励函数
```python
def format_reward_func(completions, **kwargs):
"""Reward function that checks if the completion has a specific format."""
pattern = r"^<think>.*?</think>.*?oxed{(.*?)}.*?
quot;
matches = [re.match(pattern, content, re.DOTALL) for content in completions]
return [1.0 if match else 0.0 for match in matches]
```
- **解释**:
- 检查生成文本是否符合要求:即必须包含 `<think>...</think>` 部分,并且后面包含 "oxed{…}"(意图是答案被标记在某个格式中)。
- 如果符合格式,奖励为 1;否则为 0。
## b. 准确率奖励函数
```python
def accuracy_reward_func(completions, answer, **kwargs):
# Regular expression to capture content inside \boxed{}
contents = [extract_boxed_text(completion) for completion in completions]
# Reward 1 if the content is the same as the ground truth, 0 otherwise
return [1.0 if c == str(gt) else 0.0 for c, gt in zip(contents, answer)]
```
- **解释**:
- 对每个生成结果,用之前的 `extract_boxed_text` 函数提取出答案部分。
- 若提取结果与真实答案相同,则奖励 1,否则 0。
## c. 方案质量奖励函数(基于编辑距离)
```python
def levenshtein_reward_func(completions, solution, **kwargs):
res = []
for completion, sol in zip(completions, solution):
if '</think>' in completion:
t = completion.split('</think>')[-1]
res.append(levenshtein_ratio(t, sol))
else:
res.append(0.0)
return res
```
- **解释**:
- 对每个生成结果,提取 `</think>` 后面的部分(认为这是最终答案的摘要)。
- 计算这个摘要与原始解决方案之间的 Levenshtein(编辑)相似度,并用这个相似度作为奖励。
- 如果文本中没有 `</think>`,则奖励为 0。
最后,我们将三个奖励函数存入一个字典:
```python
reward_functions = {'formatting': format_reward_func, 'accuracy': accuracy_reward_func, 'solution_quality': levenshtein_reward_func}
```
---
# 10. 模型与 Tokenizer 的加载
```python
device_map = 'auto'
if CFG.USE_PEFT:
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type='nf4',
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=False,
)
original_model = AutoModelForCausalLM.from_pretrained(CFG.model_name,
device_map=device_map,
quantization_config=bnb_config,
trust_remote_code=True)
else:
original_model = AutoModelForCausalLM.from_pretrained(CFG.model_name,
device_map=device_map,
trust_remote_code=True)
```
- **解释**:
- 设置设备映射为自动选择(通常选择合适的 GPU)。
- 如果使用 PEFT:
- 定义量化配置 `bnb_config`,以 4-bit 模式加载模型(减少显存占用)。
- 从指定路径加载预训练因果语言模型(CausalLM),同时使用量化配置。
- 否则,直接加载模型(不使用 4-bit 量化)。
接着加载 Tokenizer:
```python
tokenizer = AutoTokenizer.from_pretrained(CFG.model_name, trust_remote_code=True, padding_side="left")
```
- **解释**:加载与模型对应的 Tokenizer,并设置填充方向为左边(通常用于对话生成)。
---
# 11. 给数据集创建 Prompt
```python
dataset = dataset.map(create_prompt) # 给每个样本调用 create_prompt 函数生成 prompt
```
- **解释**:对数据集中每个样本应用前面定义的 `create_prompt` 函数,从而为训练做好输入准备。
---
# 12. 定义生成函数
```python
def gen(model, text, max_tokens):
model_input = tokenizer(text, return_tensors='pt').to(model.device)
model.eval()
with torch.no_grad():
tok = model.generate(**model_input, max_new_tokens=max_tokens, pad_token_id=tokenizer.pad_token_type_id)
outputs = []
for i in range(len(tok)):
res = tokenizer.decode(tok[i], skip_special_tokens=True)
output = res.split(CFG.splitter)[-1]
outputs.append(output)
return outputs[0] if len(outputs) == 1 else outputs
```
- **解释**:
- 该函数负责用模型生成文本:
- 将输入文本转换成模型张量,并移动到模型所在设备。
- 设置模型为评估模式,并关闭梯度计算(节省内存)。
- 调用模型的 `generate` 方法,生成指定数量的新 tokens。
- 解码生成的 token 序列,按照配置中的 `splitter` 分割,取最后一部分作为生成结果。
- 如果只生成了一个结果,则返回单个文本;如果有多个,则返回列表。
---
# 13. 定义评估奖励的函数
```python
def evaluate_rewards(model, dataset, reward_functions: dict[str, callable], max_tokens: int, num_generations: int):
completions = []
other_info = []
for example in tqdm(dataset):
txt = example['prompt']
kw = {k: v for k, v in example.items() if k not in {'prompt', 'completion'}}
for _ in range(num_generations):
other_info.append(kw)
completion = gen(model, [txt]*num_generations, max_tokens)
if isinstance(completion, str):
completions.append(completion)
else:
completions += completion
kwargs = {k: [d[k] for d in other_info] for k in other_info[0].keys()}
res = {}
for nm, reward_func in reward_functions.items():
v = reward_func(completions=completions, **kwargs)
print(nm, np.mean(v))
res[nm] = np.mean(v)
return res
```
- **解释**:
- 该函数遍历数据集中的样本,对每个样本生成多个答案(`num_generations` 个)。
- 收集每个样本生成的结果(`completions`)和其他附加信息(如真实答案)。
- 然后对每个奖励函数(格式、准确率、方案质量),计算生成结果的平均奖励值并打印出来,最后返回一个包含所有奖励的字典。
---
# 14. 训练部分
## a. 设置训练参数
```python
dtstr = datetime.datetime.now().datetime.strftime('%Y%m%d%H%M%S')
output_directory = f"./DEEPSEEK-GRPO-{dtstr}"
training_args = GRPOConfig(
output_dir=output_directory,
learning_rate=CFG.LR,
per_device_train_batch_size=CFG.BATCH_SIZE,
gradient_accumulation_steps=1,
max_steps=CFG.MAX_STEPS,
max_completion_length=CFG.MAX_TOKENS, #8192
num_generations=CFG.NUM_GENERATIONS,
beta=CFG.BETA,
logging_steps=CFG.step_count,
logging_dir="./logs",
save_strategy="steps",
save_steps=CFG.step_count,
report_to="none",
overwrite_output_dir = 'True',
)
```
- **解释**:
- 生成一个基于当前时间戳的输出目录名称。
- 使用 `GRPOConfig` 定义训练时的超参数,包括学习率、批次大小、最大训练步数、生成文本的最大长度、奖励参数 beta、日志记录和保存的步数等。
## b. 配置 PEFT(LoRA)并创建 Trainer
```python
if CFG.USE_PEFT:
peft_config = LoraConfig(
r=32, #Rank
lora_alpha=32,
target_modules=['q_proj', 'k_proj', 'v_proj', 'dense'],
bias="none",
lora_dropout=0.05, # Conventional
task_type="CAUSAL_LM",
)
trainer = GRPOTrainer(
model=original_model,
reward_funcs=list(reward_functions.values()),
args=training_args,
train_dataset=dataset['train'],
peft_config=peft_config,
callbacks=[PrinterCallback()]
)
else:
trainer = GRPOTrainer(
model=original_model,
reward_funcs=list(reward_functions.values()),
args=training_args,
train_dataset=dataset['train'],
callbacks=[PrinterCallback()]
)
```
- **解释**:
- 如果使用 PEFT(CFG.USE_PEFT 为 True),就创建一个 LoRA 配置:
- 设置参数 `r`(低秩矩阵的秩)、`lora_alpha`(缩放因子)、指定要微调的模块(比如注意力中的 q、k、v 投影和全连接层 dense)。
- 指定 dropout 和任务类型为因果语言模型(CAUSAL_LM)。
- 接着利用 GRPOTrainer(策略优化的训练器)构建训练器对象,传入模型、奖励函数、训练参数、训练数据集以及回调函数(PrinterCallback 用于打印训练信息)。
## c. 开始训练
```python
trainer.train()
```
- **解释**:开始训练过程。训练过程中,模型会根据生成的回答获得奖励并更新参数。
训练结束后,会输出一系列训练指标,例如训练损失、梯度范数、奖励值以及学习率等。
---
# 15. 训练后模型的加载与评估
## a. 加载训练后的模型
```python
if CFG.USE_PEFT:
print('Loading trained model')
CHKPT = CFG.MAX_STEPS
adapter_model_name = f'{output_directory}/checkpoint-{CHKPT}/'
new_model = PeftModel.from_pretrained(original_model, adapter_model_name)
else:
new_model = original_model
```
- **解释**:
- 如果使用了 PEFT,则加载训练后保存的 LoRA 权重(adapter)。
- 否则直接使用原始模型。
## b. 使用新模型评估奖励
```python
rewards = evaluate_rewards(model=new_model, dataset=dataset['test'], reward_functions=reward_functions, max_tokens=CFG.MAX_TOKENS, num_generations=CFG.NUM_GENERATIONS)
print(rewards)
```
- **解释**:
- 在测试集上,用新模型生成答案,并计算每种奖励函数的平均分。
- 打印出"格式奖励"、"准确率奖励"和"方案质量奖励"的最终分值。
---
# 总结
整个 Notebook 的流程可以归纳为以下几个步骤:
1. **安装依赖**:确保所有必要的包(如 trl、levenshtein、bitsandbytes 等)安装正确。
2. **环境设置与导入库**:设置 GPU 使用和导入深度学习、数据处理、以及训练相关的库。
3. **数据预处理**:读取数学题数据,对答案进行提取与过滤,然后将数据转换为 Hugging Face Dataset 格式,并划分训练/测试集。
4. **生成 Prompt**:为每个问题构造对话提示,告知模型如何回答(包括"思考"和"答案"部分)。
5. **定义奖励函数**:设计三个奖励函数来评估生成回答的格式、正确性和与原解法的相似度。
6. **加载模型和 Tokenizer**:使用预训练模型(支持 4-bit 量化以节省内存)和对应的 Tokenizer。
7. **生成和评估**:定义生成函数和评估奖励的函数,用于在训练和测试阶段生成回答并打分。
8. **训练**:利用 GRPOTrainer 配合 LoRA(PEFT)进行训练,微调模型以获得更好的回答。
9. **模型加载与测试**:训练结束后加载最新的模型,评估测试集上的奖励,检查模型效果。
希望以上逐步解释能帮助你更好地理解这份代码。如果有任何不明白的地方,欢迎继续提问!