DeepSeek r1 模型惊艳亮相后,其创新及模型实力获得了众多称赞和好评,但同时也因一些数据问题被质疑其涉嫌抄袭OpenAI 模型。
见前文:
OpenAI 称有证据证明DeepSeek违规使用其模型!
Anthropic CEO:DeepSeek 为落后的追随者,需进一步加强中国芯片管制!
微软正调查DeepSeek是否不当获取OpenAI数据!
Stable Diffusion 创始人:DeepSeek 没有抄袭!
甚至,意大利监管机构也以数据保护为由,对DeepSeek进行了封禁下架!
不过吐槽归吐槽,抹黑归抹黑,那只是少数人,且多为利益相关方。在此不作过多讨论,但能看到的是,已经有无数的AI 实验室在复刻r1,同时有众多播客、沙龙、话题都是针对DeepSeek,可谓是热火朝天。
就在刚刚,Hugging Face的技术负责人Philipp Schmid在他最新的博客文章中介绍了Mini R1。这篇文章通过详细讲解,帮助读者了解并能够重现一些小例子。
详情请查看他的博客 Mini Deepseek R1: https://www.philschmid.de/mini-deepseek-r1
下为译文:
Mini-R1: 重现 Deepseek R1 的 “Aha Moment” —— 强化学习教程
作者:Philipp Schmid @HuggingFace
Deepseek R1 的发布震惊了整个行业,为什么呢?
因为 DeepSeek-R1 是一个开源模型,在复杂推理任务上与 OpenAI 的 o1 相抗衡,采用了 Group Relative Policy Optimization(GRPO)和以强化学习为核心的多阶段训练方法。除了发布该模型,他们还发布了一篇研究论文,详细说明了他们是如何实现这一切的。
在这篇论文[1]中,他们描述了在使用纯强化学习训练模型时的一个“aha moment”(顿悟时刻)。在这一阶段,DeepSeek-R1-Zero(DeepSeek-R1 的首次测试版本)学会了通过重新评估最初的方法,来分配更多的思考时间,而无需任何人工反馈或数据描述如何进行。这一现象被称为“aha moment”,正如他们所说:
这一行为不仅证明了模型推理能力的增长,也是强化学习如何引导意外而复杂的结果的一个迷人例子。
在这篇博文中,我们将通过使用 GRPO 和倒计时游戏,重现 DeepSeek-R1 的这个“小 aha moment”。我们将训练一个开源模型,使用强化学习教它如何自我验证和搜索,从而解决倒计时游戏。倒计时游戏是一个数字谜题,玩家需要使用一组随机抽取的数字和基本算术运算(加、减、乘、除),尽可能接近目标数字。
目标数字:952
可用数字:25, 50, 75, 100, 3, 6
(100 × (3 × 3)) + (50 + 6 / 3) = 952
本博文包括一个交互式代码,你可以在 Jupyter Notebook 中运行,学习如何使用 GRPO 和 Q-Lora 来训练模型。这是学习如何使用 TRL 和 GRPO 的一个好方法,但它非常慢,需要大量计算资源。此外,我还提供了一个脚本[2]和如何在带有多个 GPU 或 SLURM 集群的节点上运行训练的说明。
-
设置开发环境[3] -
从倒计时游戏生成带有推理前缀的训练样本[4] -
使用 GRPO 训练模型(教育部分)[5] -
使用 Deepspeed 和 vLLM 的 GRPO 分布式训练示例[6] -
结果和训练观察[7]
注意:本博文灵感来源于Jiayi Pan[8],他最初探索了这个想法并通过一个小模型验证了这一点。
在开始之前,我们先来了解一下Group Relative Policy Optimization (GRPO)[9]是如何工作的。
Group Relative Policy Optimization(GRPO)
Group Relative Policy Optimization(GRPO)是一种强化学习算法,用于提高大语言模型(LLM)的推理能力。
它在DeepSeekMath[10]论文中首次提出,主要用于数学推理任务。GRPO 修改了传统的 Proximal Policy Optimization(PPO)算法,去除了值函数模型的需求。相反,它通过从组分数估计基线,从而减少了内存使用和计算开销。现在,GRPO 也被 Qwen 团队使用,可以与基于规则或二进制的奖励,以及通用奖励模型一起使用,以提高模型的帮助能力。
GRPO 的工作流程
-
采样:使用当前策略生成每个提示的多个输出。 -
奖励评分:使用奖励函数对每个生成的输出进行评分,可以是基于规则或基于结果的奖励。 -
优势计算:生成的输出的平均奖励作为基线,每个解的优势相对于该基线进行计算。奖励在一个组内进行归一化。 -
策略优化:策略会尽力最大化 GRPO 目标,该目标包括计算出的优势和 KL 散度项。这与 PPO 在奖励中实现 KL 项的方式不同。
1. 设置开发环境
我们的第一步是安装 Hugging Face 库、PyTorch、vllm、trl、transformers 和 datasets。如果你还没听说过 trl,不用担心。它是一个基于 transformers 和 datasets 的新库,使得微调、强化学习和对齐开源 LLM 更加容易。
# 安装 Pytorch 和其他库,确保与 GPU 驱动版本匹配
%pip install "torch==2.5.1" tensorboard "setuptools<71.0.0" --index-url https://download.pytorch.org/whl/cu121
# 安装 flash-attn
%pip install flash-attn
# 安装 Hugging Face 库
%pip install --upgrade \
"transformers==4.48.1" \
"datasets==3.1.0" \
"accelerate==1.3.0" \
"hf-transfer==0.1.9" \
"deepspeed==0.15.4" \
"trl==0.14.0"
# 安装 vLLM
%pip install "vllm==0.7.0"
## 重要:如果你想运行交互式单元格,还需要安装以下库:
# 但在安装之前请阅读博文,因为这些库可能会与分布式训练的库发生冲突。
# %pip install "peft==0.14.0" "bitsandbytes==0.45.0"
注意:你可能需要重启内核,以便使用更新后的包。
我们将使用Hugging Face Hub[11]作为远程模型版本控制服务。这意味着我们将在训练期间自动将模型、日志和信息推送到 Hub。你必须在Hugging Face[12]注册账户。注册后,我们将使用 huggingface_hub
包中的 login
工具登录账户并将访问令牌存储在磁盘上。
from huggingface_hub import login
login(token="", add_to_git_credential=True) # 在此处添加你的令牌
2. 从倒计时游戏生成带有推理前缀的训练样本
我们将使用Jiayi-Pan/Countdown-Tasks-3to4[13]数据集,该数据集包含 3 到 4 个数字和对应的解决方案。
作为模型,我们将使用Qwen/Qwen2.5-3B-Instruct[14],这是一个具有 30 亿参数的指令调优模型。由于它已经遵循提示格式,因此展示“aha moment”会更容易。但你也可以使用 Qwen 的基础版本或其他模型。Jiayi-Pan[15] 发现模型需要具备一定的质量才能学习推理过程,起始参数量需大于 1.5B。
from transformers import AutoTokenizer
from datasets import load_dataset
# 从 Hugging Face Hub 加载数据集
dataset_id = "Jiayi-Pan/Countdown-Tasks-3to4"
dataset = load_dataset(dataset_id, split="train")
# 随机选择 50k 样本
dataset = dataset.shuffle(seed=42).select(range(50000))
# 从 Hugging Face Hub 加载 tokenizer,用于格式化数据集为 "r1" 提示
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B-Instruct")
# 生成 r1 提示,带有推理前缀
def generate_r1_prompt(numbers, target):
r1_prefix = [{
"role": "system",
"content": "你是一个有帮助的助手。你首先思考推理过程,然后给用户提供答案。"
},
{
"role": "user",
"content": f"使用数字 {numbers} 来接近目标数字 {target}。"
}]
return r1_prefix
# 创建训练样本
train_sample = []
for sample in dataset:
train_sample.append(generate_r1_prompt(sample["numbers"], sample["target"]))
3. 使用GRPO训练模型(教育部分)
注意: 第3部分展示了如何使用TRL和GRPO的基础知识。如果你想运行交互式代码单元,必须安装bitsandbytes
和peft
,因为它们是Trainer
类的必备依赖。此部分主要用于教育目的。
TRL通过专门的GRPOTrainer[16]支持基于偏好数据对大语言模型(LLM)进行对齐,相关内容可以参考DeepSeekMath: 推动开放语言模型数学推理极限[17]。GRPOTrainer
是transformers
库中Trainer
的子类,支持所有相同的功能,包括日志记录、检查点、分布式训练和参数高效微调(PEFT)。
GRPOTrainer
支持通用的结果奖励模型(ORM)和自定义奖励函数,可以用来实现基于规则的奖励模型。在DeepSeek R1论文中,他们实现了基于规则的奖励模型,用于验证生成解答的正确性。在我们的示例中,我们将采用类似的方式,创建两个奖励函数:
-
格式奖励:检查生成的格式是否正确 <think> [thinking] </think><answer> [answer] </answer>
-
准确性奖励:从 <answer>
标签中提取方程式,并与目标进行对比,确保每个数字都仅使用一次。
注意: 在我们的示例中,正确的<answer>
标签应包含方程式,例如:<answer> 55 + 36 - 7 - 19 </answer>
。
示例代码
import re
def format_reward_func(completions, target, **kwargs):
"""
格式检查:<think>...</think><answer>...</answer>
参数:
completions (list[str]): 生成的输出
target (list[str]): 期望的答案
返回:
list[float]: 奖励分数
"""
rewards = []
for completion, gt in zip(completions, target):
try:
# 由于<think>已作为提示的一部分并预填充,因此这里人为添加<think>
completion = "<think>" + completion
# 检查格式是否正确
regex = r"^<think>([^<]*(?:<(?!/?think>)[^<]*)*)<\/think>\n<answer>([\s\S]*?)<\/answer>$"
match = re.search(regex, completion, re.DOTALL)
# 如果格式不正确,奖励为0
if match is None or len(match.groups()) != 2:
rewards.append(0.0)
else:
rewards.append(1.0)
except Exception:
rewards.append(0.0)
return rewards
def equation_reward_func(completions, target, nums, **kwargs):
"""
基于以下因素评估生成结果:
2. 答案的数学正确性
参数:
completions (list[str]): 生成的输出
target (list[str]): 期望的答案
nums (list[str]): 可用的数字
返回:
list[float]: 奖励分数
"""
rewards = []
for completion, gt, numbers in zip(completions, target, nums):
try:
# 由于<think>已作为提示的一部分并预填充,因此这里人为添加<think>
completion = "<think>" + completion
# 检查<answer>标签格式
match = re.search(r"<answer>(.*?)<\/answer>", completion)
if match is None:
rewards.append(0.0)
continue
# 提取“答案”部分
equation = match.group(1).strip()
# 提取方程式中的所有数字
used_numbers = [int(n) for n in re.findall(r'\d+', equation)]
# 检查所有数字是否都使用了一次
if sorted(used_numbers) != sorted(numbers):
rewards.append(0.0)
continue
# 定义一个只允许数字、运算符、括号和空格的正则表达式模式
allowed_pattern = r'^[\d+\-*/().\s]+$'
if not re.match(allowed_pattern, equation):
rewards.append(0.0)
continue
# 在受限的全局和局部环境中评估方程式
result = eval(equation, {"__builtins__": None}, {})
# 检查方程式是否正确,并与真实值匹配
if abs(float(result) - float(gt)) < 1e-5:
rewards.append(1.0)
else:
rewards.append(0.0)
except Exception:
# 如果评估失败,奖励为0
rewards.append(0.0)
return rewards
测试我们的奖励函数
注意: 示例中没有任何一个生成的答案以<think>
开始,因为我们已经在提示中人为地添加了它。
correct_sample_1 = """我们需要使用19、36、55和7这四个数字,
在不重复的情况下,通过基本的算术运算找到等于65的方程式。一种可能的组合是:55 + 36 - 19 + 7... </think>
<answer> 55 + 36 - 7 - 19 </answer>"""
correct_sample_2 = """ ... </think>
<answer> 55 + 36 - 7 - 19 </answer>"""
wrong_format = """用户:使用数字[19, 36, 55, 7],创建一个等于65的方程式。"""
wrong_format_2 = """为了找到等于79的方程式,我开始将88和95相加:
95 + 88 = 183
然后,减去104得到79:
183 - 104 = 79
<think> 183 - 104 = 79 </think><think> 183 - 104 = 79 </think><answer> 183 - 104 = 79 </answer>"""
wrong_result = """ ... </think>
<answer> 55 + 36 - 7 - 18 </answer>"""
test_rewards = format_reward_func(completions=[correct_sample_1, correct_sample_2, wrong_format, wrong_format_2, wrong_result], target=["65", "65", "65", "65", "65"], nums=[[19, 36, 55, 7]] * 5)
assert test_rewards == [1.0, 1.0, 0.0, 0.0, 1.0], "奖励函数未正常工作"
test_rewards = equation_reward_func(completions=[correct_sample_1, correct_sample_2, wrong_format, wrong_format_2, wrong_result], target=["65", "65", "65", "65", "65"], nums=[[19, 36, 55, 7]] * 5)
assert test_rewards == [1.0, 1.0, 0.0, 0.0, 0.0], "奖励函数未正常工作"
定义训练参数并开始训练
from trl import GRPOConfig, GRPOTrainer, get_peft_config, ModelConfig
# 我们将作为策略使用的模型
model_config = ModelConfig(
model_name_or_path="Qwen/Qwen2.5-3B-Instruct",
torch_dtype="bfloat16",
attn_implementation="flash_attention_2",
use_peft=True,
load_in_4bit=True,
)
# 超参数配置
training_args = GRPOConfig(
output_dir="qwen-r1-aha-moment",
learning_rate=5e-7,
lr_scheduler_type="cosine",
logging_steps=10,
max_steps=100,
per_device_train_batch_size=1,
gradient_accumulation_steps=1,
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
bf16=True,
# GRPO特定参数
max_prompt_length=256,
max_completion_length=1024, # 最大生成输出长度
num_generations=2,
beta=0.001,
)
trainer = GRPOTrainer(
model=model_config.model_name_or_path,
reward_funcs=[format_reward_func, equation_reward_func],
args=training_args,
train_dataset=train_dataset,
eval_dataset=test_dataset,
peft_config=get_peft_config(model_config),
)
我们可以通过调用train
方法开始训练:
# 开始训练并将模型上传到Hub
trainer.train()
# 保存模型
trainer.save_model(training_args.output_dir)
注意: 强化训练非常缓慢且计算密集。使用1xL4、Q-LoRA、批量大小为1且每个样本只有2个生成的情况下,每个步骤的运行时间超过20分钟。
4. 使用Deepspeed和vLLM进行GRPO分布式训练示例
单步训练超过20分钟,仅2个生成的样本是不可行的。我们需要扩展训练规模。Hugging Face TRL 已支持通过Deepspeed进行分布式训练,并使用vLLM进行更快的生成。我准备了一个run_r1_grpo.py[18]脚本和一个receipes/grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml[19]配置文件来运行训练。
这个配置已经在4x H100 80GB的节点上经过验证,在这种硬件环境下,单步训练大约需要45-60秒,因为我们可以利用vLLM进行生成,使用DeepSpeed进行分布式训练。因此,我们需要确保正确设置num_processes
为GPU的数量减去1,最后一个GPU将用于vLLM进行生成。如果你使用更多GPU,需要在配置文件中将vllm_device
设置为最后一个GPU的索引,例如,如果你有8个GPU,应该设置vllm_device=7
,并将num_processes
设置为7。
执行训练的命令如下:
accelerate launch --num_processes 3 --config_file configs/accelerate_configs/deepspeed_zero3.yaml scripts/run_r1_grpo.py --config receipes/grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml
通过优化的分布式训练,使用4x H100 80GB的硬件,且每个样本有8个生成,单步训练大约需要45-60秒。完整的训练(450步)大约需要6小时。
5. 结果与训练观察
该脚本将随机生成的完成任务保存到 completion_samples
文件夹中,供您查看模型的进展。它包括 completion_samples.txt
和 success_completion_samples.txt
。其中,completion_samples.txt
包含所有生成的答案,而 success_completion_samples.txt
仅包括正确解答了方程的样本。下面列出了训练过程中一些有趣的观察,展示了模型性能随时间变化的情况,以及 TensorBoard 日志和成功推理的样本。
带有每25步检查点的模型可以在 philschmid/qwen-2.5-3b-r1-countdown[20] 中找到。
超参数
实验开始时,我使用了来自 DeepSeekMath[21] 论文的超参数,学习率设为 1e-6,beta(KL系数)设为 0.04。这个配置在大约150步后导致了不稳定的训练过程。之后我进行了小规模的调参,将学习率降到 5e-7,将 beta 降到 0.001,这一调整基于 OpenRLHF[22] 的测试结果。我没有测试将 num_generations
从 8 增加到 64 会对训练有何影响。64 是 DeepSeekMath 论文中使用的生成值。所有其他参数可以在 grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml[23] 配置文件中找到。
训练观察:
-
在约 50 步时,模型已学会了正确的格式 <think>...</think>\n<answer>...</answer>
。 -
在 100 步时,解决方程的成功率约为 25%。此时,模型开始用语言进行“推理”,具体见下文的示例。 -
到了 200 步时,性能似乎趋于收敛,成功率约为 40%。模型开始学习一种新的“格式”,它以类似编程方式的方式解决方程,通过尝试不同的组合并检查结果,详见“200 到 450 步之间的成功推理样本”。 -
在 450 步时,模型的成功率达到了 50%。尽管性能仍在缓慢提升,但模型保持了从第 200 步开始的新格式。
我有三种可能的假设,解释模型为什么从“语言推理”转变为“程序化执行”:
-
Qwen 2.5 3B 模型不够强大,或者模型过小,DeepSeek 提到需要一个非常强大的基础模型。 -
奖励函数定义不足,可能模型通过奖励机制采用了“作弊”方式来解决方程。我们可以尝试强制其使用文字,例如设置数字与文字频率条件(我们对 DeepSeek 中的奖励函数了解较少)。 -
仅在 Countdown 游戏任务上训练 可能迫使模型自然而然地学习最有效的解题方法,因为没有其他格式的需求。 -
训练时间不够长,在 R1 论文中,模型在 8000 步之后仍能看到明显的训练效果。
TensorBoard 结果
200 步之前的成功推理样本
<think>首先,我需要思考一下我们手头的数字:14, 70 和 80。为了得到 85,我觉得 80 是一个不错的起点,因为它接近 85。如果我减去或加上 80,似乎很难得到 85。接下来,70 似乎是一个不错的中间值,尤其是与 14 一起使用时。
现在,我可以尝试使用加法、减法、乘法和除法的不同组合。如果我计算 80 - 70 = 10,但这不是我们需要的。因此,我需要找到一种方法来使用 14。如果我从 80 中减去 14,得到 66,但仍然不是 85。如果我从 70 中减去 80,结果也是 10,依然无法找到解决办法。
乘法可能会生成大于目标 85 的数字。让我们考虑除法:如果我将 70 除以 14,得到 5。然后如果将这个结果加到 80,得到 85。这种组合似乎有希望:80 - (70 / 14) = 80 - 5 = 75,这还不对。我需要调整一下方法:我们可以使用除法,然后加法。如果我计算 80 / (70 / 14) → 80 / 5 = 16,然后 80 - 16 = 64,仍然不正确。让我简化一下:80 / (70 / 14) → 80 / 5 = 16,然后 80 - 16 依然得到 64。让我再改进一下:80 - (70 / 14) → 80 - 5 = 75,但我需要 85。所以,我将以稍微不同的顺序插入数字:得到 80 + (70 / 14) = 80 + 5 = 85</think>
<answer> 80 + (70 / 14) </answer>
200 到 450 步之间的成功推理样本
<think>我们需要用数字 39, 16 和 55 进行基本的算术运算,得到 78。让我们尝试不同的组合:
- 55 + 39 - 16 = 84 - 16 = 68(太低)
- 55 + 16 - 39 = 71 - 39 = 32(太低)
- 55 - 16 + 39 = 39 + 39 = 78(这成立)
所以,方程是 55 - 16 + 39 = 78。</think>
<answer> 55 - 16 + 39 </answer>
结论
DeepSeek R1 的发布和其研究论文可能成为开放科学和开源开发的分水岭。在 DeepSeek 发布后的短短一周内,我们便成功地复现了一个简单版本的 R1 学习“推理”,通过 GRPO 和 Countdown 游戏来实现。尽管我们的实现集中于特定任务,而非通用推理,并且收敛到一个非常具体的“推理”格式,但它展示了这一方法的有效性。
在我们的迷你 R1 实验中,我们使用了 GRPO,并且使用了两个基于规则的奖励,但仍然需要显著的计算资源:4 台 H100 GPU 持续运行了 6 小时,仅完成了 450 步的训练,模型为 3B 参数。这给了我们一个关于强化学习规模化所需计算需求的初步认识。DeepSeek 运行了一个 671B 的模型,训练超过 8000 步,可能进行了许多实验。
展望 2025年,显然我们正处于更大进展的边缘。强化学习将变得更加易用,更多的研究人员和开发者将探索其潜力,但与以往一样,仍需要大量的计算资源,相较于监督微调仍然有不小的差距。
我对2025年充满期待。如果你有任何问题或想法,随时可以联系我。
相关资料
论文: https://arxiv.org/abs/2501.12948
[2]脚本: https://github.com/philschmid/deep-learning-pytorch-huggingface/blob/main/training/scripts/run_r1_grpo.py
[3]设置开发环境: https://www.philschmid.de/mini-deepseek-r1#1-setup-the-development-environment
[4]从倒计时游戏生成带有推理前缀的训练样本: https://www.philschmid.de/mini-deepseek-r1#2-generate-training-samples-with-reasoning-prefix-from-the-countdown-game
[5]使用 GRPO 训练模型(教育部分): https://www.philschmid.de/mini-deepseek-r1#3-train-the-model-using-grpo-educational-part
[6]使用 Deepspeed 和 vLLM 的 GRPO 分布式训练示例: https://www.philschmid.de/mini-deepseek-r1#4-distributed-training-example-for-grpo-using-deepspeed-and-vllm
[7]结果和训练观察: https://www.philschmid.de/mini-deepseek-r1#5-results-and-training-observations
[8]Jiayi Pan: https://x.com/jiayi_pirate/status/1882839370505621655
[9]Group Relative Policy Optimization (GRPO): https://arxiv.org/abs/2402.03300
[10]DeepSeekMath: https://arxiv.org/abs/2402.03300
[11]Hugging Face Hub: https://huggingface.co/models
[12]Hugging Face: https://huggingface.co/join
[13]Jiayi-Pan/Countdown-Tasks-3to4: https://huggingface.co/datasets/Jiayi-Pan/Countdown-Tasks-3to4
[14]Qwen/Qwen2.5-3B-Instruct: https://huggingface.co/Qwen/Qwen2.5-3B-Instruct
[15]Jiayi-Pan: https://x.com/jiayi_pirate/status/1882839487417561307
[16]GRPOTrainer: https://huggingface.co/docs/trl/main/en/grpo_trainer
[17]DeepSeekMath: 推动开放语言模型数学推理极限: https://arxiv.org/abs/2402.03300
[18]run_r1_grpo.py: https://github.com/philschmid/deep-learning-pytorch-huggingface/blob/main/training/scripts/run_r1_grpo.py
[19]receipes/grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml: https://github.com/philschmid/deep-learning-pytorch-huggingface/blob/main/training/receipes/grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml
[20]philschmid/qwen-2.5-3b-r1-countdown: https://huggingface.co/philschmid/qwen-2.5-3b-r1-countdown
[21]DeepSeekMath: https://arxiv.org/abs/2402.03300
[22]OpenRLHF: https://hijkzzz.notion.site/unraveling-rlhf-and-its-variants-engineering-insights#147d9a33ecc9806090f3d5c749d31f05
[23]grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml: https://github.com/philschmid/deep-learning-pytorch-huggingface/blob/main/training/receipes/grpo-qwen-2.5-3b-deepseek-r1-countdown.yaml
(文:AGI Hunt)