基于Qwen模型在SwanLab上复现DeepSeek-R1-Zero

简介

本文旨在对deepseek-r1-zero进行复现实验,详细介绍了从r1原理到代码实现,再到结果观测的整个过程。在实验中,采用了基石智算平台来实现GRPO(基于PPO的优化算法),并通过SwanLab监控实验过程,确保实验的每个阶段都能精确跟踪与调试。通过这一系列的实验步骤,本文希望帮助用户更好地理解基石智算平台的使用方式,并深入掌握GRPO的实现方法。希望读者在实验过程中能够加深对相关技术的理解,并能灵活应用于实际项目中。

在这里插入图片描述

链接资料

作者信息:情感机器实验室研究员-李馨雨 邮箱:wind.340171@gmail.com

模型地址:huggingface社区|魔搭社区

数据集地址:huggingface-MATH23K

可视化工具SwanLab项目地址:SwanLab结果可视化

友情链接

基石智算平台链接:

AI计算平台(用于模型训练部署等任务):AI计算平台

大模型推理服务平台(调用模型API,该平台提供了DeepSeek-R1以及蒸馏版本的API,可以轻松实现R1的本地调用):大模型推理服务平台


SwanLab官方文档:

用户指南,可以快速上手SwanLab: 快速开始 | SwanLab官方文档

应用案例:入门实验 | SwanLab官方文档

DeepSeek-R1原理

论文标题:DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning

论文链接:论文链接

代码地址:github链接

下面是论文里从DeepSeek-V3到DeepSeek-R1的流程图表示

本文仅考虑从DeepSeek-V3—>DeepSeek-R1-Zero的复现过程,基于Qwen2.5-7B-Instruct模型实现。因此下面仅介绍R1-Zero的复现过程。

在这里插入图片描述

📝流程图解析:

从图中可以看到从V3到R1的过程中总共分为三步,每一行为一步骤

1、第一步是V3到R1-Zero的生成过程,该过程只用到GRPO算法,用于激发V3的思考推理能力,在
没有用到SFT数据进行微调的前提下,就能够显著提高模型的性能,虽然Zero还不具备完整的泛化能力,但是DeepSeek为我们提供了有效的思路。

2、第二步笔者理解是从V3到R1的中间过程,该过程最开始使用Zero生成的数据来对V3进行微调,该步骤成为冷启动,那为什么在RL有如此强大性能的情况下仍然选择SFT呢?
因为冷启动为模型提供了高质量的初始策略,避免从零开始训练的低效和不稳定性,稳定提高模型性能。

随后继续使用GRPO来提高模型性能,该步骤相比于Zero生成过程中的GRPO添加了对回答语言的奖励函数,防止出现语言混乱的情况,该过程后,也就是图中的V3’'相比于Zero生成的结果会更加稳定准确,
但是仍然缺乏泛化能力,对于数学问题的思考推理能力较强,而其他方面仍有提高空间。

3、因此第三步是从V3’'模型生成部分高质量数据,以及额外的包含CoT数据、无推理数据等高质量数据总共80W条对V3直接进行SFT,最后通过GRPO强化学习设置奖励函数避免模型生成有危害性的知识等,最终获得R1模型。

R1模型具备比Zero更强、泛化能力更好、推理思考能力更强的能力。

若希望激活较小规模的大语言模型的推理能力,使其具备类似R1的推理思考能力,可以单独采用第一步的GRPO强化学习方法。

具体过程我们可以从图中看到,官方使用V3对格式(format)以及答案(answer)设置奖励函数进行强化训练,使用GRPO策略,该策略是PPO的优化方案,能够显著的减少显存占用,提高训练效率,训练完毕后的模型能够获得类似于zero的思考能力,显著提高了模型的性能,但是结果仍会有些错误,并且强化学习后的结果毕竟不如SFT(监督微调)的结果稳定,所以R1仍然需要一些SFT来提高模型稳定性。

GRPO原理:

群体相对策略优化 (GRPO,Group Relative Policy Optimization) 是一种强化学习 (RL) 算法,专门用于增强大型语言模型 (LLM) 中的推理能力。与严重依赖外部评估模型(价值函数)指导学习的传统 RL 方法不同,GRPO 通过评估彼此相关的响应组来优化模型。这种方法可以提高训练效率,使 GRPO 成为需要复杂问题解决和长链思维的推理任务的理想选择。

GRPO 的本质思路:通过在同一个问题上生成多条回答,把它们彼此之间做“相对比较”,来代替传统 PPO 中的“价值模型”

传统的强化学习算法(如Proximal Policy Optimization,PPO)在应用于LLMs的推理任务时面临着重大挑战:

1、依赖批评者模型:
PPO需要一个独立的批评者模型来评估每个回答的价值,这使内存和计算需求增加了一倍。
训练批评者模型非常复杂且容易出错,尤其是在需要对主观或细微差别进行评价的任务中。

2、高昂的计算成本:
强化学习流程通常需要大量计算资源来迭代评估和优化回答。
将这些方法扩展到更大的LLMs会进一步加剧成本。

3、可扩展性问题:
绝对奖励评估难以应对多样化任务,使得跨推理领域的泛化变得困难。

GRPO如何应对这些挑战:

1、无批评者优化: GRPO通过比较组内回答,消除了对批评者模型的需求,显著降低了计算开销。

2、相对评估: GRPO不依赖外部评价者,而是利用组内动态来评估每个回答在同一批次中的相对表现。

3、高效训练: 通过专注于组内优势,GRPO简化了奖励估计流程,使其对大型模型的训练更快且更具可扩展性。

下图是PPO与GRPO的对比,GRPO放弃了价值模型,从分组得分中估计,显著减少了训练资源

在这里插入图片描述

看到一位作者的看法,把GRPO比作老师给学生上课,老师让一组学生解决一个问题。
老师没有单独为每个学生打分,而是让学生在组内比较彼此的答案。表现更好的学生会得到鼓励,而其他人则从错误中学习。随着时间的推移,整个组会逐渐提高,变得更准确和一致。GRPO 将这一原理应用于训练AI模型,使其能够高效地学习。

实验工具选择

在这里插入图片描述

本次实验我们使用基石智算平台和SwanLab工具来进行实验,其中基石智算平台主要实现强化学习阶段,SwanLab工具主要实现模型训练过程的观测以及GPU情况监测。

在这里插入图片描述

基石智算是一个为各行业提供面向人工智能场景的资源与服务的平台。它提供GPU云服务、AI训练集群、并行文件存储、镜像仓库等AI专用服务,确保资源的高效利用和成本优化。基石智算还具备分布式调度与管理能力,能够自动分配和管理算力资源,大幅缩短任务执行时间,提高工作效率。此外,基石智算平台支持一键启动、一键部署以及在线微调,助力用户打造专属AI应用,加速AI应用落地,提升业务智能化水平。

在这里插入图片描述

SwanLab是一款开源、轻量级的AI实验跟踪工具,提供了一个跟踪、比较、和协作实验的平台,旨在加速AI研发团队的研发效率。它提供了友好的API和漂亮的界面,结合了超参数跟踪、指标记录、在线协作、实验链接分享、实时消息通知等功能,让您可以快速跟踪ML实验、可视化过程、分享给同伴。SwanLab还支持云端和离线使用,支持远程查看训练过程,比如可以在手机上看远程服务器上跑的训练。此外,SwanLab自动记录训练过程中的日志、硬件环境、Python库以及GPU、NPU等硬件信息。

在这里插入图片描述

实验代码

1、环境搭建

由于本次实验使用青云的基石智算平台,因此环境的搭建需要按照步骤来进行,这样能够节省查阅文档学习的时间,轻松上手。

操作流程可以大致按照下面的流程图来进行,具体的步骤我会在下面详细介绍:

在这里插入图片描述

我们需要按照流程图中的主要流程来操作,而且最好是先设置存储与数据服务,此步骤是开启一个数据存放地址,可以在该文件夹中上传本地数据,而后续创建容器实例的时候会要求设置存储与数据服务,如果没有提前设置,那么申请下来的服务器就没有该地址,需要重新申请服务器来设置。
在存储与数据服务以及容器实例创建好后就可以直接在快捷IDE界面编辑训练代码并且训练。

step1:创建存储与数据服务

在这里插入图片描述

step2:创建容器实例

在这里插入图片描述

step3:配置环境
在这里插入图片描述

环境设置如下:

pip install transformers==4.48.1

pip install peft==0.14.0

由于我使用的是pytorch基础镜像,其中pytorch版本有点低,所以我重新安装了2.4.0版本的pytorch,需要注意:该服务器CUDA等配置可能不是很完整,因此为了节省时间,我们直接安装CPU版本,如果有条件可以安装支持CUDA的PyTorch版本。

pytorch安装地址:pytorch-old-versions

conda install pytorch2.4.0 torchvision0.19.0 torchaudio==2.4.0 -c pytorch

pip install datasets

pip install accelerate>=0.26.0

pip install trl

pip install -U swanlab


⚠️注意:

DeepSpeed 是一个由微软开发的高性能深度学习优化库,专注于大规模模型训练和推理。它通过ZeRO(Zero Redundancy Optimizer技术显著减少显存占用,支持千亿参数模型的训练。DeepSpeed 还提供了混合精度训练、梯度累积、模型并行等功能,能够大幅提升训练速度和效率。

在安装这个库的时候会发现下面的错误:

在这里插入图片描述

这代表CUDA没有正常安装,比如看下有没有正常安装的话用下面的代码:

nvcc -V

在这里插入图片描述

如果出现这行信息的话表示CUDA正常安装,如果是下面的情况就是CUDA没装好,需要重新安装下:

在这里插入图片描述

可以根据显卡类型以及操作系统版本信息来确定安装的CUDA版本,操作系统版本信息需要在命令行输入下面的命令查看:

uname -m

输出是x86_64,这里笔者安装12.4版本CUDA,安装CUDA的链接:CUDA各个版本,直接依次执行下面的命令即可:

在这里插入图片描述

执行完命令后还要设置环境变量,修改路径,首先查看cuda是否已经安装

which nvcc

会出现下面的信息:

/usr/local/cuda/bin/nvcc

如果没有这个信息的话,可以查看/usr/local/cuda/bin/nvcc该文件夹知否存在即可,一般情况下,执行上述cuda安装代码后nvcc都是存在的。

然后手动添加nvcc到PATH环境变量中,在命令行执行下面的命令:

nano ~/.bashrc

如果nano命令不存在,可以安装下:

apt install nano

在文章末尾添加下面的命令:

export CUDA_HOME=/usr/local/cuda
export PATH=/usr/local/cuda/bin:$PATH

保存并退出编辑器,然后运行以下命令使配置生效:

source ~/.bashrc

最后执行nvcc -V,应该就正常显示cuda信息了,那么接下来安装deepspeed就能正常

pip3 install deepspeed

2、数据预处理

本次实验使用一个490k条数据的Countdown数据集来进行实验,内容如下图所示:

在这里插入图片描述

该数据集仅有两项,一个是target结果数据,一个是nums组合数据,我们的目的是为了让模型思考如何从nums经过+、-、*、/计算得到target,为了让模型更好的激活思考能力,我们需要对其设置提示词模板,最重要让模型回答成如下模样:

<think>:
让我们来思考下,……
</think>

<answer>
……
</answer>

同时,由于每个模型都有对应的训练格式模板,比如Qwen的模板在其权重文件中的tokenizer_config.json文件里,具体例子如下:

"chat_template": "{%- if tools %}\n    {{- '<|im_start|>system\\n' }}\n    {%- if messages[0]['role'] == 'system' %}\n        {{- messages[0]['content'] }}\n    {%- else %}\n        {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n    {%- endif %}\n    {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n    {%- for tool in tools %}\n        {{- \"\\n\" }}\n        {{- tool | tojson }}\n    {%- endfor %}\n    {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n    {%- if messages[0]['role'] == 'system' %}\n        {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n    {%- else %}\n        {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n    {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n    {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n        {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n    {%- elif message.role == \"assistant\" %}\n        {{- '<|im_start|>' + message.role }}\n        {%- if message.content %}\n            {{- '\\n' + message.content }}\n        {%- endif %}\n        {%- for tool_call in message.tool_calls %}\n            {%- if tool_call.function is defined %}\n                {%- set tool_call = tool_call.function %}\n            {%- endif %}\n            {{- '\\n<tool_call>\\n{\"name\": \"' }}\n            {{- tool_call.name }}\n            {{- '\", \"arguments\": ' }}\n            {{- tool_call.arguments | tojson }}\n            {{- '}\\n</tool_call>' }}\n        {%- endfor %}\n        {{- '<|im_end|>\\n' }}\n    {%- elif message.role == \"tool\" %}\n        {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n            {{- '<|im_start|>user' }}\n        {%- endif %}\n        {{- '\\n<tool_response>\\n' }}\n        {{- message.content }}\n        {{- '\\n</tool_response>' }}\n        {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n            {{- '<|im_end|>\\n' }}\n        {%- endif %}\n    {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n    {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",

这是一个Jinja2 模板。Jinja2 是一个流行的模板引擎,常用于 Python Web 应用中,但它也可以在其他环境中使用。举一个例子:

<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>
<|im_start|>user
使用给定的数字 [10, 3, 6],创建一个等于 7 的方程。你可以使用基本算术运算(+、-、*、/)一次或多次,但每个数字只能使用一次。在 <think> </think> 标签中展示你的思考过程,并在 <answer> </answer> 标签中返回最终方程,例如 <answer> (1 + 2) / 3 </answer>。在 <think> 标签中逐步思考。<|im_end|>
<|im_start|>assistant
让我们逐步解决这个问题。
<think>

当然也可以利用tokenizer.apply_chat_template自动根据模型的格式模板进行内容整理,具体如下述代码所示,将数据集转换为R1 Countdown提示词格式:

### 模仿R1的prompt格式来处理数据集,使得GRPO的时候的数据集是可以有思考过程
def generate_r1_prompt(question:str,target:str):
    """
    激活qwen模型的思考过程
    :param question:数据集的question,给qwen让他自己思考去
    :param target:数据集的ans
    :return:
    """
    r1_prefix = [
        {
            "role":"user",
            "content":f"现在有一个数学问题,内容是:{question},答案是{target},你需要根据问题思考其推理过程,使得最终能够得到正确答案,在<think>和</think>标签中展示你的思考过程,并在<answer>和</answer>标签中返回最终答案,比如<answer>19</answer>。在<think>标签后逐步思考。"
        },
        {
            "role":"assistant",
            "content":"让我们逐步解决这个问题。\n<think>"
        }
    ]
    # apply_chat_template是应用qwen模型文件中tokenizer_config.json文件中chat_template提示词模板来生成回答。
    return {"prompt": tokenizer.apply_chat_template(r1_prefix, tokenize=False, continue_final_message=True),
            "question":question,
            "target": target}
            
### 将数据集转换为R1 Countdown提示词格式,在这里我们会把prompt转换为Qwen2的提示词模版,让它以更熟悉的方式来接收提示词,并且我们把让我们逐步解决这个问题。\n<think>作为模型输出的开头,让它接着续写。用 Python字典的方式返回样本,这样trl会在调用奖励函数的时候,帮我们把键名设为为对应的参数;另外,trl会把模型的多个输出设为completions。
def train_dataset_process(train_data_path:str):
    dataset = read_jsonl_to_dataset(train_data_path)
    dataset = dataset.map(lambda x: generate_r1_prompt(x["sni_text"], x["ans"]))

    train_test_split = dataset.train_test_split(test_size=0.1)

    train_dataset = train_test_split["train"]
    test_dataset = train_test_split["test"]

    return {
        "train_dataset":train_dataset,
        "test_dataset":test_dataset
    }
    

❗注意: generate_r1_prompt中最终需要return包含数据提问,以及数据集对应的答案answer,map方法会帮我们把实际的question和answer填入到prompt里

3、设置奖励函数

在强化学习中,奖励函数是指导智能体(agent)在环境中如何行动的核心信号。奖励提供了对智能体行为的即时反馈,用于评估某个动作在某一状态下的好坏,从而影响其未来的决策。通过不断地试错和调整,智能体学习到在不同状态下选择能获得高奖励的行为策略。奖励的主要功能是引导智能体朝着最大化长期回报的目标去优化策略。正向奖励(正数)鼓励行为,负向奖励(负数)抑制行为。奖励用于更新智能体的策略或值函数,策略的优化通常基于累计奖励(Return),即智能体从当前状态到未来一段时间内获得的总奖励。

本次实验我们仅对输出格式format以及最终答案answer设置奖励函数,训练过程会不断修正格式输出以及答案输出。

format奖励函数

### 格式奖励函数
def format_reward_func(completions, **kwargs):
    """
    格式奖励函数,检查模型输出格式是否匹配: <think>...</think><answer>...</answer>

    参数:
        completions (list[str]): 生成的输出
    返回:
        list[float]: 奖励分数
    """
    # 初始化奖励列表
    rewards = []
    # 遍历生成的输出
    for completion in completions:
        try:
            # 在生成的输出前添加<think>标签,便于后续正则表达式匹配
            completion = "<think>" + completion

            if random.random() < 0.1:  # 1% 的概率将生成输出写入文件
                # 创建生成输出目录(如果不存在)
                os.makedirs("completion_samples", exist_ok=True)
                log_file = os.path.join("completion_samples", "completion_samples.txt")
                with open(log_file, "a") as f:
                    f.write(f"\n\n==============\n")
                    f.write(completion)  # 写入生成的输出

            # 定义正则表达式模式,用于匹配 <think> 和 <answer> 标签
            regex = r"^<think>([^<]*(?:<(?!/?think>)[^<]*)*)<\/think>\n<answer>([\s\S]*?)<\/answer>$"
            match = re.search(regex, completion, re.DOTALL)  # 使用正则表达式进行匹配

            if match is None or len(match.groups()) != 2:
                rewards.append(0.0)  # 如果格式不正确,奖励为 0
            else:
                rewards.append(1.0)  # 如果格式正确,奖励为 1
        except Exception:
            rewards.append(0.0)  # 如果发生异常,奖励为 0

    return rewards

answer奖励函数

### 答案奖励函数
def equation_reward_func(completions, target, nums, **kwargs):
    """
    方程奖励函数,检查计算结果是否正确,数字是否符合使用要求(每个数字只用一次,只使用所提供的数字)

    参数:
        completions (list[str]): 生成的输出
        target (list[str]): 预期的答案
        nums (list[str]): 可用的数字

    返回:
        list[float]: 奖励分数
    """
    # 初始化奖励列表
    rewards = []
    # 遍历生成的输出、预期的答案和可用的数字
    for completion, gt, numbers in zip(completions, target, nums):
        try:
            # 在生成的输出前添加 <think> 标签,便于后续正则表达式匹配
            completion = "<think>" + completion
            # 定义正则表达式模式,用于匹配 <answer> 标签
            match = re.search(r"<answer>(.*?)<\/answer>", completion)
            if match is None:
                rewards.append(0.0)  # 如果没有匹配到 <answer> 标签,奖励为 0
                continue
            equation = match.group(1).strip()  # 提取 <answer> 标签中的内容
            # 提取方程中的所有数字
            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)  # 如果方程包含不允许的字符,奖励为 0
                continue

            # 计算方程的结果
            result = eval(equation, {"__builtins__": None}, {})
            # 检查方程是否正确且与预期答案匹配(误差小于 1e-5)
            if abs(float(result) - float(gt)) < 1e-5:
                rewards.append(1.0)  # 如果正确,奖励为 1

                # 10% 的概率将成功的样本写入文件
                if random.random() < 0.10:
                    # 创建生成输出目录(如果不存在)
                    os.makedirs("completion_samples", exist_ok=True)
                    log_file = os.path.join(
                        "completion_samples", "success_completion_samples.txt"
                    )
                    with open(log_file, "a") as f:
                        f.write(f"\n\n==============\n")
                        f.write(completion)  # 写入生成的输出
            else:
                rewards.append(0.0)  # 如果不正确,奖励为 0
        except Exception:
            rewards.append(0.0)  # 如果评估失败,奖励为 0

    return rewards

补充: 也可以设置思考长度以及语言一致性奖励函数来提高模型性能

4、设置模型参数

# 模型参数设置
model_config = ModelConfig(
    model_name_or_path=model_path,
    torch_dtype="bfloat16",
    # attn_implementation="flash_attention_2",
    use_peft=True,
    load_in_4bit=True
)

attn_implementation:使用 flash_attention_2 可以优化显存使用和加速计算,尤其是在处理大规模模型时。若启用,它会减少内存占用并加速训练过程,尤其在使用多GPU时效果显著。未启用时,可能会牺牲性能和显存效率,影响训练速度。

5、设置训练参数

# 训练参数
training_args = GRPOConfig(
    output_dir="/root/test/outputs",
    learning_rate=5e-7,
    lr_scheduler_type="cosine",
    logging_steps=2,
    max_steps=200,
    per_device_train_batch_size=1,
    gradient_checkpointing=False,
    gradient_accumulation_steps=8,
    gradient_checkpointing_kwargs={"use_reentrant": False},
    bf16=True,
    save_steps=50,
    # GRPO参数设置
    max_prompt_length=256,
    max_completion_length=1024,
    num_generations=2,
    beta=0.001,
    # vllm加速
    use_vllm=False
    # vllm_device="npu:7"
    vllm_device="cuda:1"
    vllm_gpu_memory_utilization=0.8
)

其中vLLM 是一个用于加速推理的库,能在 GPU 上优化内存使用和计算性能。启用 use_vllm=True 后,它可以在推理阶段通过高效的内存管理和多设备并行来加速计算,特别是在处理大型语言模型时。它还能通过 vllm_device 参数指定加速设备,例如 cuda:1,提升训练和推理速度,减少显存占用。
这里之所以是false是因为我申请的服务器只有两块卡,使用vllm的时候一块卡训练,一块卡用来推理,而vllm一般在多块卡的时候,比如5、6块卡以上的时候才能体现出加速效果,而本次实验使用的是4090,只有242GB显存,很容易炸显存,如果卡比较多的话推荐vllm。

⚠️注意:

我们使用的是trl的库来使用GRPO,目前有个小bug,就是gradient_checkpointing和vllm要同时true或者同时false,否则就会报错,而这两个参数都有降低显存占用,提高训练推理速度的功能,因此如何设置可以交给各位炼丹师自行选择。

6、设置SwanLab可视化训练工具参数

SwanLab是一款完全开源免费的机器学习日志跟踪与实验管理工具,为人工智能研究者打造。有以下特点:
1、基于一个名为swanlab的python库
2、可以帮助您在机器学习实验中记录超参数、训练日志和可视化结果
3、能够自动记录logging、系统硬件、环境配置(如用了什么型号的显卡、Python版本是多少等等)
4、同时可以完全离线运行,在完全内网环境下也可使用

如果想要快速入门,请参考以下文档链接:

用户指南,可以快速上手SwanLab: 快速开始 | SwanLab官方文档

应用案例:入门实验 | SwanLab官方文档

代码设置如下:

## swanlab参数配置
swanlab_callback = SwanLabCallback(
    workspace=None, # 项目不公开
    project="DeepSeek-R1-zero",  # 项目名称
    experiment_name="4090-grpo",  # 实验名称
)

7、训练并保存模型

# 训练器配置
trainer = GRPOTrainer(
    model=model_config.model_name_or_path,
    reward_funcs=[format_reward_func,answer_reward_func],
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=test_dataset,
    peft_config=get_peft_config(model_config),
    callbacks=[swanlab_callback]
)

trainer.train()
trainer.save_model(training_args.output_dir)

全过程代码

为了便于管理和配置分布式训练环境、强化学习(RL)训练的超参数,以及定义主训练函数 main,我们建议采用 YAML 格式的脚本文件来系统化地记录和维护这些关键参数,同时使用 Python 文件来实现 main 函数。

root/project/

├──data/

│ └──zouxuhong___countdown-tasks-3to4

├──models/

│ ├──.___temp

│ ├──Qwen

│ │ └──Qwen2___5-3B-Instruct

├──config/

│ ├──2rtx4090.yaml

│ └──grpo-qwen-2.5-3b-deepseek-r1-zero-countdown.yaml

├──train_r1_grpo.py

└──train_r1_grpo.sh

1、Accelerate 配置文件,用于分布式训练(两张卡)。新建deepspeed_zero3.yaml,填入以下内容并保存

一般来说,这个文件内容不需要修改,如果有定制需求,请不要使用这个文件,运行accelerate config自行设定。

compute_environment: LOCAL_MACHINE
debug: false
deepspeed_config:
  gradient_accumulation_steps: 8
  gradient_clipping: 1.0
  offload_optimizer_device: cpu
  offload_param_device: cpu
  zero3_init_flag: false
  zero3_save_16bit_model: true
  zero_stage: 3
distributed_type: DEEPSPEED
downcast_bf16: 'no'
enable_cpu_affinity: false
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 2
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false

⚠️注意:

由于本次实验资源有限,因此训练优化器还有模型参数部分会转移到CPU上进行计算,以减少显存压力,修改的参数是offload_optimizer_device和offload_param_device

  1. offload_optimizer_device: cpu

    作用:将优化器状态(如动量、梯度等)卸载到 CPU 上。

    具体内容:

     优化器状态通常占用大量显存,尤其是在使用 Adam 优化器时。
    
     将这些状态卸载到 CPU 上可以显著减少 GPU 显存占用,从而支持更大的模型或更大的批量大小。
    
  2. offload_param_device: cpu

    作用:将模型参数卸载到 CPU 上。

    具体内容:

    模型参数是训练过程中占用显存的主要部分。
    
    将这些参数卸载到 CPU 上可以进一步减少 GPU 显存占用,但会增加 CPU 和 GPU 之间的数据传输开销。
    

2、设定训练的超参数。新建grpo-qwen-2.5-3b-deepseek-r1-zero-countdown.yaml填入以下内容,并根据实际情况修改

# Model arguments
model_name_or_path: /root/epfs/ascend_r1_turtorial/models/Qwen/Qwen2___5-3B-Instruct
model_revision: main
torch_dtype: bfloat16
# attn_implementation: flash_attention_2
bf16: true
tf32: false
output_dir: /root/epfs/ascend_r1_turtorial/output

# Dataset arguments
dataset_id_or_path: /root/epfs/zouxuhong___countdown-tasks-3to4

# Lora Arguments
# No LoRA is used here

# Training arguments
max_steps: 450
per_device_train_batch_size: 1
gradient_accumulation_steps: 8
gradient_checkpointing: false
gradient_checkpointing_kwargs:
  use_reentrant: false
learning_rate: 5.0e-7 # 1.0e-6 as in the deepseek math paper 5-e7 from https://hijkzzz.notion.site/unraveling-rlhf-and-its-variants-engineering-insights#147d9a33ecc9806090f3d5c749d31f05
lr_scheduler_type: cosine
warmup_ratio: 0.03
# GRPO specific parameters
beta: 0.001 # 0.04 as in the deepseek math paper 0.001 from https://hijkzzz.notion.site/unraveling-rlhf-and-its-variants-engineering-insights#147d9a33ecc9806090f3d5c749d31f05
max_prompt_length: 256
max_completion_length: 1024
num_generations: 2
use_vllm: false
# vllm_device: "npu:7"
vllm_device: "cuda:1"
vllm_gpu_memory_utilization: 0.8

# Logging arguments
logging_strategy: steps
logging_steps: 1
save_strategy: "steps"
save_steps: 100
save_total_limit: 1
seed: 2025

# Swanlab 训练流程记录参数
swanlab: true # 是否开启 Swanlab 
workspace: none
project: Try_r1
experiment_name: qingyun-4090-jupyter

3、设置训练函数

import logging
import os
import random
import re
from dataclasses import dataclass
from datetime import datetime
from typing import List

from datasets import load_dataset
from swanlab.integration.transformers import SwanLabCallback
import torch
from transformers import AutoTokenizer
from transformers.trainer_utils import get_last_checkpoint
from trl import GRPOConfig, GRPOTrainer, ModelConfig, TrlParser


################################################
# 自定义参数类
################################################

@dataclass
class DatasetArguments:
    """数据集参数的数据类"""

    # 数据集 ID 或路径
    dataset_id_or_path: str = "Jiayi-Pan/Countdown-Tasks-3to4"
    # 数据集拆分
    dataset_splits: str = "train"
    # 分词器名称或路径
    tokenizer_name_or_path: str = None

@dataclass
class SwanlabArguments:
    """SwanLab参数的数据类"""

    # 是否使用 SwanLab
    swanlab: bool
    # SwanLab 用户名
    workspace: str
    # SwanLab 的项目名
    project: str
    # SwanLab 的实验名
    experiment_name: str

################################################
# 设置日志记录
################################################

# 配置日志记录器
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(
    logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)  # 设置日志格式

logger.addHandler(handler)

################################################
# 定义奖励函数
################################################

def format_reward_func(completions, **kwargs):
    """
    格式奖励函数,检查模型输出格式是否匹配: <think>...</think><answer>...</answer>

    参数:
        completions (list[str]): 生成的输出
    返回:
        list[float]: 奖励分数
    """
    # 初始化奖励列表
    rewards = []
    # 遍历生成的输出
    for completion in completions:
        try:
            # 在生成的输出前添加<think>标签,便于后续正则表达式匹配
            completion = "<think>" + completion

            if random.random() < 0.1:  # 1% 的概率将生成输出写入文件
                # 创建生成输出目录(如果不存在)
                os.makedirs("completion_samples", exist_ok=True)
                log_file = os.path.join("completion_samples", "completion_samples.txt")
                with open(log_file, "a") as f:
                    f.write(f"\n\n==============\n")
                    f.write(completion)  # 写入生成的输出

            # 定义正则表达式模式,用于匹配 <think> 和 <answer> 标签
            regex = r"^<think>([^<]*(?:<(?!/?think>)[^<]*)*)<\/think>\n<answer>([\s\S]*?)<\/answer>$"
            match = re.search(regex, completion, re.DOTALL)  # 使用正则表达式进行匹配

            if match is None or len(match.groups()) != 2:
                rewards.append(0.0)  # 如果格式不正确,奖励为 0
            else:
                rewards.append(1.0)  # 如果格式正确,奖励为 1
        except Exception:
            rewards.append(0.0)  # 如果发生异常,奖励为 0

    return rewards

def equation_reward_func(completions, target, nums, **kwargs):
    """
    方程奖励函数,检查计算结果是否正确,数字是否符合使用要求(每个数字只用一次,只使用所提供的数字)

    参数:
        completions (list[str]): 生成的输出
        target (list[str]): 预期的答案
        nums (list[str]): 可用的数字

    返回:
        list[float]: 奖励分数
    """
    # 初始化奖励列表
    rewards = []
    # 遍历生成的输出、预期的答案和可用的数字
    for completion, gt, numbers in zip(completions, target, nums):
        try:
            # 在生成的输出前添加 <think> 标签,便于后续正则表达式匹配
            completion = "<think>" + completion
            # 定义正则表达式模式,用于匹配 <answer> 标签
            match = re.search(r"<answer>(.*?)<\/answer>", completion)
            if match is None:
                rewards.append(0.0)  # 如果没有匹配到 <answer> 标签,奖励为 0
                continue
            equation = match.group(1).strip()  # 提取 <answer> 标签中的内容
            # 提取方程中的所有数字
            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)  # 如果方程包含不允许的字符,奖励为 0
                continue

            # 计算方程的结果
            result = eval(equation, {"__builtins__": None}, {})
            # 检查方程是否正确且与预期答案匹配(误差小于 1e-5)
            if abs(float(result) - float(gt)) < 1e-5:
                rewards.append(1.0)  # 如果正确,奖励为 1

                # 10% 的概率将成功的样本写入文件
                if random.random() < 0.10:
                    # 创建生成输出目录(如果不存在)
                    os.makedirs("completion_samples", exist_ok=True)
                    log_file = os.path.join(
                        "completion_samples", "success_completion_samples.txt"
                    )
                    with open(log_file, "a") as f:
                        f.write(f"\n\n==============\n")
                        f.write(completion)  # 写入生成的输出
            else:
                rewards.append(0.0)  # 如果不正确,奖励为 0
        except Exception:
            rewards.append(0.0)  # 如果评估失败,奖励为 0

    return rewards

################################################
# 断点续训处理
################################################

def get_checkpoint(training_args: GRPOConfig):
    """
    获取最后一个检查点

    参数:
        training_args (GRPOConfig): 训练参数
    返回:
        str: 最后一个检查点的路径,如果没有检查点,则返回 None
    """
    last_checkpoint = None
    if os.path.isdir(training_args.output_dir):  # 如果输出目录存在
        # 获取最后一个检查点
        last_checkpoint = get_last_checkpoint(training_args.output_dir)
    return last_checkpoint

################################################
# 基于trl实现GRPO训练过程
################################################
def grpo_function(
    model_args: ModelConfig,
    dataset_args: DatasetArguments,
    training_args: GRPOConfig,
    callbacks: List,
):
    # 记录模型参数
    logger.info(f"Model parameters {model_args}")
    # 记录训练/评估参数
    logger.info(f"Training/evaluation parameters {training_args}")

    ################################################
    # 处理数据
    ################################################

    # 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(
        (
            # 如果有指定分词器,则使用指定的分词器,否则使用模型名称
            dataset_args.tokenizer_name_or_path
            if dataset_args.tokenizer_name_or_path
            else model_args.model_name_or_path
        ),
        revision=model_args.model_revision,  # 使用指定的模型版本
        trust_remote_code=model_args.trust_remote_code,  # 允许使用远程代码
    )
    # 如果分词器没有填充标记,则使用结束标记作为填充标记
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # 加载数据集
    dataset = load_dataset(
        dataset_args.dataset_id_or_path, split=dataset_args.dataset_splits
    )
    # 随机选择 50K 个样本,看你喜好定数字,但是数据集有 409K 个样本
    dataset = dataset.shuffle(seed=training_args.seed).select(range(50000))

    def generate_r1_prompt(numbers, target):
        """
        生成 R1 Countdown 游戏提示词

        参数:
            numbers (list[int]): 数字列表
            target (int): 目标值
        返回:
            dict: 生成的一个数据样本
        """
        # 定义提示词前缀
        r1_prefix = [
            {
                "role": "user",
                "content": f"使用给定的数字 {numbers},创建一个等于 {target} 的方程。你可以使用基本算术运算(+、-、*、/)一次或多次,但每个数字只能使用一次。在 <think> </think> 标签中展示你的思考过程,并在 <answer> </answer> 标签中返回最终方程,例如 <answer> (1 + 2) / 3 </answer>。在 <think> 标签中逐步思考。",
            },
            {
                "role": "assistant",
                "content": "让我们逐步解决这个问题。\n<think>",  # 结尾使用 `<think>` 促使模型开始思考
            },
        ]

        return {
            "prompt": tokenizer.apply_chat_template(
                r1_prefix, tokenize=False, continue_final_message=True
            ),  # 提示词,continue_final_message=True 表示将提示词中的最后一个消息继续到最终的输出中
            "target": target,
            "nums": numbers,
        }

    # 将数据集转换为 R1 Countdown 游戏提示词
    dataset = dataset.map(lambda x: generate_r1_prompt(x["nums"], x["target"]))
    # 将数据集拆分为训练集和测试集,拆分比例为 9:1
    train_test_split = dataset.train_test_split(test_size=0.1)
    train_dataset = train_test_split["train"]  # 获取训练集
    test_dataset = train_test_split["test"]  # 获取测试集

    # 参考自 huggingface/open-r1, 把attn_implementation(是否使用flash_attention)等参数传入模型初始化参数
    logger.info("*** Initializing model kwargs ***")
    torch_dtype = (
        model_args.torch_dtype if model_args.torch_dtype in ["auto", None] else getattr(torch, model_args.torch_dtype)
    )
    model_kwargs = dict(
        revision=model_args.model_revision,
        trust_remote_code=model_args.trust_remote_code,
        attn_implementation=model_args.attn_implementation,
        torch_dtype=torch_dtype,
        use_cache=False if training_args.gradient_checkpointing else True,
    )
    training_args.model_init_kwargs = model_kwargs


    ################################################
    # 设置 GRPOTrainer
    ################################################
    trainer = GRPOTrainer(
        model=model_args.model_name_or_path,  # 模型名称或路径
        # 奖励函数列表,用于计算奖励分数
        reward_funcs=[
            format_reward_func,  # 格式奖励函数
            equation_reward_func,  # 方程奖励函数
        ],
        args=training_args,
        train_dataset=train_dataset,
        eval_dataset=test_dataset,
        callbacks=callbacks,
    )

    last_checkpoint = get_checkpoint(training_args)  # 检查最后一个检查点
    # 如果检测到检查点且指定从检查点恢复训练,则记录信息
    if last_checkpoint is not None and training_args.resume_from_checkpoint is None:
        logger.info(f"Checkpoint detected, resuming training at {last_checkpoint}.")

    logger.info(
        f'*** Starting training {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} for {training_args.num_train_epochs} epochs***'
    )

    ################################################
    # 训练模型
    ################################################
    train_result = trainer.train(resume_from_checkpoint=last_checkpoint)

    ################################################
    # 保存训练结果
    ################################################

    # 记录和保存指标
    metrics = train_result.metrics
    metrics["train_samples"] = len(train_dataset)
    trainer.log_metrics("train", metrics)
    trainer.save_metrics("train", metrics)
    trainer.save_state()

    logger.info("*** Training complete ***")

    # 保存模型和分词器
    logger.info("*** Save model ***")
    trainer.model.config.use_cache = True
    trainer.save_model(training_args.output_dir)
    logger.info(f"Model saved to {training_args.output_dir}")
    training_args.distributed_state.wait_for_everyone()  # 等待所有进程加载
    tokenizer.save_pretrained(training_args.output_dir)
    logger.info(f"Tokenizer saved to {training_args.output_dir}")

    logger.info("*** Training complete! ***")

def main():
    """主函数,用于执行主训练循环"""
    # 解析命令行参数和配置文件
    parser = TrlParser((ModelConfig, DatasetArguments, GRPOConfig, SwanlabArguments))
    model_args, dataset_args, training_args, swanlab_args = (
        parser.parse_args_and_config()
    )

    # 如果使用 SwanLab,则创建 SwanLab 回调对象,用于训练信息记录
    if swanlab_args.swanlab:
        swanlab_callback = SwanLabCallback(
            project=swanlab_args.project,
            experiment_name=swanlab_args.experiment_name,
        )
        callbacks = [swanlab_callback]
    else:
        callbacks = None

    # 运行主训练循环
    grpo_function(model_args, dataset_args, training_args, callbacks=callbacks)

if __name__ == "__main__":
    main()

4、设置分布式训练脚本

accelerate launch \
    --num_processes 2 \
    --config_file config/2rtx4090.yaml \
    train_r1_grpo.py \
    --config config/grpo-qwen-2.5-3b-deepseek-r1-zero-countdown.yaml

5、启动训练

在命令行输入下面的内容:

bash train_r1_grpo.sh

训练后模型部署和推理

保存下来的仅仅是模型的权重信息以及配置文件等,是不能直接使用的,需要与原模型进行合并操作,代码如下:

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import os
import shutil

# 保证原始模型的各个文件不遗漏保存到merge_path中
def copy_files_not_in_B(A_path, B_path):
    if not os.path.exists(A_path):
        raise FileNotFoundError(f"The directory {A_path} does not exist.")
    if not os.path.exists(B_path):
        os.makedirs(B_path)

    # 获取路径A中所有非权重文件
    files_in_A = os.listdir(A_path)
    files_in_A = set([file for file in files_in_A if not (".bin" in file or "safetensors" in file)])

    files_in_B = set(os.listdir(B_path))

    # 找到所有A中存在但B中不存在的文件
    files_to_copy = files_in_A - files_in_B

    # 将文件或文件夹复制到B路径下
    for file in files_to_copy:
        src_path = os.path.join(A_path, file)
        dst_path = os.path.join(B_path, file)

        if os.path.isdir(src_path):
            # 复制目录及其内容
            shutil.copytree(src_path, dst_path)
        else:
            # 复制文件
            shutil.copy2(src_path, dst_path)

def merge_lora_to_base_model(adapter_name_or_path,save_path,model_name_or_path="Qwen/Qwen2-0.5B"):
    # 如果文件夹不存在,就创建
    if not os.path.exists(save_path):
        os.makedirs(save_path)
    tokenizer = AutoTokenizer.from_pretrained(model_name_or_path,trust_remote_code=True,)

    model = AutoModelForCausalLM.from_pretrained(
        model_name_or_path,
        trust_remote_code=True,
        low_cpu_mem_usage=True,
        torch_dtype=torch.float16,
        device_map="auto"
    )
    # 加载保存的 Adapter
    model = PeftModel.from_pretrained(model, adapter_name_or_path, device_map="auto",trust_remote_code=True)
    # 将 Adapter 合并到基础模型中
    merged_model = model.merge_and_unload()  # PEFT 的方法将 Adapter 权重合并到基础模型
    # 保存合并后的模型
    tokenizer.save_pretrained(save_path)
    merged_model.save_pretrained(save_path, safe_serialization=False)
    copy_files_not_in_B(model_name_or_path, save_path)
    print(f"合并后的模型已保存至: {save_path}")


if __name__ == '__main__':
    adapter_name_or_path="你的生成的模型的文件夹"
    save_path = "保存模型的地址"
    merge_lora_to_base_model(adapter_name_or_path=adapter_name_or_path,save_path=save_path)

运行上述代码后,会得到最终合并后的模型,我们用该模型进行推理测试,测试代码如下:

from transformers import AutoModelForCausalLM,AutoTokenizer
import torch

MODEL_NAME_OR_PATH = "output/qwen-grpo"
PROMPT="""使用给定的数字 [80, 9, 18],创建一个等于 53 的方程。你可以使用基本算术运算(+、-、*、/)一次或多次,
          但每个数字只能使用一次。在 <think> </think> 标签中展示你的思考过程,并在 <answer> </answer> 标签中返回最终方程,
          例如 <answer> (1 + 2) / 3 </answer>。在 <think> 标签中逐步思考。让我们逐步解决这个问题。\n<think>"""



model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME_OR_PATH,
    torch_dtype="auto",
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME_OR_PATH)

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": PROMPT}
]
text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)

generated_ids = model.generate(
    model_inputs.input_ids,
    max_new_tokens=512,
    top_p=0.95,
    temperature=0.7,
)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)

基石智算平台使用细节

1、创建容器实例后关机会自动在自定义镜像仓库生成自定义镜像
在这里插入图片描述

如果需要新开启一些容器实例的话,可以使用自定义镜像中你保存的镜像,无论是下载的文件还是模型、数据集都会保留在新的容器实例里。

2、可以调用API实现大模型本地使用

在这里插入图片描述

友情链接:调用API的文档教程:VScode上调用API教程

3、容器实例里使用模型可以自己下载

青云AI计算平台自己已经提供了一部分的大模型权重,但是可能比较老,如果想要使用比较新的模型的话可以自己编写脚本然后下载模型:

在这里插入图片描述

这里提供一些下载模型的脚本代码,以供参考:

# 注意:HF下载不了,可以使用魔搭社区&魔乐社区
# 魔搭社区下载模型:
from modelscope import snapshot_download
model_dir = snapshot_download("Qwen/Qwen2.5-3B-Instruct",cache_dir="./models")

# 魔乐社区下载模型:
from openmind_hub import snapshot_download
snapshot_download(repo_id="PyTorch-NPU/Qwen2.5_3B_Instruct", token="your_token", repo_type="model",cache_dir="./models")

实验结果演示

基石智算平台监控

基石智算提供了非常便捷的GPU以及CPU的使用情况,我们可以随时观测内存以及显存的占用情况。

在这里插入图片描述

本次实验我们只使用两块卡的服务器,正在工作的是5,6号卡,因为使用了3B模型进行分布式训练,因此显存占用比较多,每块卡基本都达到了90%(24GB)

在这里插入图片描述

而由于我们将一些计算从GPU转移到了CPU上,因此对CPU的利用率也比较高。

在这里插入图片描述

SwanLab工具观测实验过程

而更多的细节我们可以使用swanlab工具来观测,既可以看到模型训练的时候loss、reward分数、lr等参数的变化,也可以直接查看GPU、NPU等加速卡的情况:

在这里插入图片描述

通过SwanLab图表(下图)可以清楚的观测到模型的训练状况,本教程的训练日志可在下方链接处访问,下面解释一下几个关键图表的含义:

  • train_loss 模型训练的损失值(由于trl框架的实现,此处loss并非越低越好,而是随着训练的继续逐渐增长)。

  • format_reward_func 格式奖励,此数值越高表示模型越能够准确遵守<think>...<\think>\n<answer>...<\answer>的正确输出格式。

  • equation_reward_func 结果正确奖励,此数值越高表示模型最终作答的公式正确率越高。

  • train/completion_length 输出文本长度,此图表表示了随着训练模型生成的think+answer的最终长度变化

在这里插入图片描述

本次实验的结果已开源在SwanLab图表当中,链接:https://swanlab.cn/@LiXinYu/Try_r1/

模型推理结果以及分析

1、假设我们设置nums=[10,3,6],而target设置为1,那么结果如下

首先我们使用Qwen2.5-3B-instruct模型尝试下:

首先,我们需要找到一种方式来使这些数字组合成 1。考虑到题目要求我们只使用一次每个数字,我们可以考虑使用除法和减法。10 - 3 = 7,然后将 7 除以 6 得到约 1.1666...,这显然不是理想的解决方案,因为我们要得到一个精确为 1 的结果。

接下来,考虑到 10 和 6 之间的差值较大,直接相减或除可能无法得到整数 1,那么我们尝试将 10 分解为更小的部分,也许可以通过乘法或者除法来实现。如果我们将 10 除以 6,我们得到大约 1.6666...,还是不能得到整数 1。那么,我们尝试另一种方法,即通过将 10 减去某个数,然后用这个数除以另一个数,使得结果为 1。考虑到 10 - 3 = 7,我们尝试将 7 除以 6,得到 1.1666...,仍然不理想。最后,我们注意到 10 - 3 - 6 = 1,所以我们可以构建这样的方程:(10 - 3 - 6) / 1 = 1。这样就得到了一个精确的结果。 </think>

<answer>(10 - 3 - 6) / 1</answer>

生成的时间有点长,但是看答案还是非常正确的,这也是我们使用3B-instruct模型的原因,因为这样在RL的过程中就能够得到更准确的推理过程,因为“智商”足够,更小的模型就不一定能达到效果,比如我们举一个0.5B的模型的例子:

首先,我们需要找到一个能加起来等于1的数。我们可以尝试将所有数字相加:
10 + 3 + 6 = 19
但是这个结果大于1,所以我们需要减去1,得到2。
</think>
接下来,我们将2乘以3,得到6。
所以,最终的方程是:(10 + 3 + 6) * 3 = 1
这样我们就得到了一个等于1的等式。

可以看到相比于3B,得到的答案和我们希望的相差甚远


然后我们使用3B-grpo模型尝试,

首先,我们需要找到一个方程,使得使用数字 10, 3, 和 6 一次且仅一次的结果为 1。我们可以尝试不同的组合。假设我们尝试 10 - 3 - 6,结果是 1。这个组合满足条件。因此,最终方程是 10 - 3 - 6 = 1。 </think>
<answer> (10 - 3 - 6)</answer>

看样子没有变化,可能因为题目简单,本身不用RL都能算出来,所以差别不大。

2、假设我们设置稍微难点nums=[50,26,89,72,3],而target设置为65,那么结果如下

我们使用Qwen2.5-3B-instruct模型测试下:

首先,我们需要找到一个组合,使得使用给定的数字 [50, 26, 89, 72, 3] 和基本算术运算(+、-、*、/)一次或多次后得到 65。我们可以尝试不同的组合。假设我们尝试 89 - 50 + 26 - 72 + 3。这样操作后的结果是:89 - 50 = 39,39 + 26 = 65,65 - 72 = -7,-7 + 3 = -4。显然这个组合不正确。接下来尝试其他组合。假设我们尝试 89 + 26 - 50 - 72 + 3,这样操作后的结果是:89 + 26 = 115,115 - 50 = 65,65 - 72 = -7,-7 + 3 = -4。仍然不正确。最后尝试 89 + 26 + 72 - 50 - 3,这样操作后的结果是:89 + 26 = 115,115 + 72 = 187,187 - 50 = 137,137 - 3 = 134。显然这个组合也不正确。我们需要继续尝试其他组合。假设我们尝试 89 - 50 - 26 + 72 + 3,这样操作后的结果是:89 - 50 = 39,39 - 26 = 13,13 + 72 = 85,85 + 3 = 88。显然这个组合也不正确。直到最后,我们发现 89 + 26 - 50 + 72 - 3 等于 135 - 50 + 72 - 3 = 135 - 50 = 85,85 - 72 = 13,13 + 3 = 16。我们已经没有其他可能的组合了。</think>
<answer> (89 + 26 + 72 - 50 + 3)</answer>

可以看到格式正确,但是答案和预想中的相差甚远,89+26+72-50+3=140。

然后使用训练好的3B-instruct-grpo模型得到如下结果:

首先,我们需要找到一种方法来组合这些数字以达到65的结果。注意到65接近于89,而89减去剩下的三个数(50, 26, 72, 3)中的任何一个都太远了。这意味着我们可以考虑将89与较小的数相加或者将较大的数相减。

接下来,我们可以尝试将89和3相加,因为89 - 3 = 86,这非常接近我们的目标值65。为了使结果为65,我们可以从86中减去一个较小的数。考虑到我们还有50和26这两个数,我们可以选择减去26,因为这样可以确保结果更接近65。因此,最终的方程将是:89 - 26 + 3 = 65。</think>

<answer>89 - 26 + 3</answer>

可以看到结果已经很相近了,也可以看出在推理层面确实有所提高

3、使用其他的数据来进行测试

我们使用Qwen2.5-3B-instruct模型测试下:

当使用原始

我们需要使用给定的数字 25, 46, 和 91,来构造一个等于3的表达式。首先考虑最直接的方法:将三个数相加或相减。然而,无论我们如何组合这三个数字,它们的总和或差都不会等于3。接下来考虑乘除法,尝试找到一种方式使结果为3。

考虑到乘法的结果通常比除法更接近于3,我们可以先尝试将较大的数字91与较小的数字25或46相乘。然而,即使尝试了91 * 25 或 91 * 46,结果都大于3。因此,我们需要寻找其他组合方法。注意到91 - 46 = 45,而45 - 25 = 20,这些都不符合要求。但是如果我们尝试91 - (25 * 46),这样可以得到91 - 1150,这显然也不正确。现在让我们重新审视一下乘法,尝试不同的组合,比如91 / 46 或 91 / 25,看是否能得到一个更接近3的结果。尝试91 / 46得到约2. 478,仍然不是3。尝试91 / 25得到3. 64,这还是不能满足条件。看来我们需要重新考虑,或许我们可以尝试将两个数字进行某种运算后,再与第三个数字结合。考虑到25和46的差值是21,我们可以尝试用这个差值与91做某种运算。如果91 - 21 = 70,那么剩下的就是1了,这似乎不太可能。如果尝试91 - (25 + 46) = 91 - 71 = 20,也显然不合适。这时,我们注意到91 - (25 * 46 / 1) = 91 - 115 = -24,仍然不是3。现在让我们考虑尝试91 - (25 * 46 / 91) = 91 - (1150 / 91)。这个表达式会得到一个近似值,但是不是整数。实际上,我们可能需要将25和46中的一个小数部分与91结合。考虑到25和46都是整数,我们可能需要尝试一个稍微复杂的方法,即利用它们

可以看到没有强化学习过的模型并不能每次或者大部分时候具备准确的遵循指令和推理思考的能力,很有可能因为模型的一些特性回答不准确或者有额外的输出,

然后使用训练好的3B-instruct-grpo模型得到如下结果:

首先,我们需要找到一个方程,使得使用数字 25, 46, 和 91 只一次,结果为 3。我们可以尝试不同的组合。假设我们尝试 (91 - 46) / 25,这样得到的结果是 (45) / 25 = 1.8,这不是 3。再试另一种组合 (91 - 25) / 46,这样得到的结果是 66 / 46 ≈ 1.43,也不是 3。最后,尝试 (46 - 25) / 91,这样得到的结果是 21 / 91 = 0.23,仍然不是 3。我们需要重新考虑其他可能性。现在尝试 91 - (46 + 25),这样得到的结果是 91 - 71 = 20,也不对。继续尝试 (91 + 25) / 46,这样得到的结果是 116 / 46 ≈ 2.55,还是不行。最后,尝试 91 - (46 * 25) / 100,虽然这个表达式看起来复杂,但实际上是 91 - 11.5 = 79.5,依然不对。看来需要更简单的组合,再试一次 (91 - 46) / 25,结果是 1.8,太小了。最后,发现 46 - (91 - 25) 是 46 - 66 = -20,也是错误的。重新审视 (91 - 25) / 46 = 66 / 46 ≈ 1.43。唯一正确的组合是 91 - (46 + 25) = 91 - 71 = 20,没有合适的答案。重新考虑 (46 - 25) / 91,是 21 / 91 = 0.23。正确答案应该是 91 - (46 + 25) / 100 = 91 - 11.5 = 79.5,错误再次出现。最后,尝试 (46 + 25) / (91 - 1) 是 71 / 90 ≈ 0.789,还是不正确。最终,正确的组合是 (91 - 46) / 25 = 1.8,错误再次出现。经过仔细检查,没有合适的简单组合。所以正确答案可能是 91 - (46 + 25) / 100 = 91 - 11.5 = 79.5,但没有合适答案。</think>
<answer> (91 - 46 - 25) / 100 </answer>

可以看到虽然格式正确,但是答案确实是有问题,也就是具备思考能力,但是能力还没有完全展现。

💡总结:

1、在选择复现Zero的时候,需要注意,GRPO更多的是激活模型思考的能力,如果本身模型思考能力不强,那么也没办法提高性能。

2、GRPO确实能够提升模型自我反思并且推理的能力,并且在训练的时候并不需要大量高质量SFT数据,那么对于资源有限的团队,强化学习是很便捷的提高模型性能的手段。

✨✨✨至此,您已完成全部的教程✨✨✨

参考链接:

  • https://github.com/philschmid/deep-learning-pytorch-huggingface
  • https://github.com/Jiayi-Pan/TinyZero
  • https://github.com/datawhalechina/unlock-deepseek
  • https://arxiv.org/pdf/2501.12948?
  • https://github.com/deepseek-ai/DeepSeek-R1
  • https://arxiv.org/pdf/2402.03300
  • https://zhuanlan.zhihu.com/p/21952581194
  • https://github.com/huggingface/open-r1?tab=readme-ov-file#grpo
  • https://zhuanlan.zhihu.com/p/21062322587
  • https://cloud.tencent.com/developer/article/2495699
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐