本文来源公众号“数据派THU”,仅用于学术分享,侵权删,干货满满。
原文链接:用PyTorch从零构建 DeepSeek R1:模型架构和分步训练详解
全文篇幅很长,几万字,分为数据派THU | 用PyTorch从零构建 DeepSeek R1:模型架构和分步训练详解(上)-优快云博客(上)和(下),这两篇文章建议阅读15+分钟
本文深入剖析了 DeepSeek R1 模型的构建过程。
R1 Zero 训练配置
本节将介绍如何配置训练参数,以便对前述定义的 奖励函数 的具体工作方式进行精细调整。首先,定义配置类 GRPOScriptArguments:
# 为 GRPO 脚本参数定义 GRPOScriptArguments 类,用于配置奖励函数参数
@dataclass
class GRPOScriptArguments:
"""
GRPO 训练的脚本参数,特别是与奖励函数相关的参数。
"""
reward_funcs: list[str] = field(
default_factory=lambda: ["accuracy", "format"],
metadata={
"help": "奖励函数列表。可选值: 'accuracy', 'format', 'reasoning_steps', 'cosine', 'repetition_penalty'"
},
)
cosine_min_value_wrong: float = field(
default=-0.5,
metadata={"help": "余弦缩放奖励函数中,错误答案的最小奖励值"},
)
cosine_max_value_wrong: float = field(
default=-0.1,
metadata={"help": "余弦缩放奖励函数中,错误答案的最大奖励值"},
)
cosine_min_value_correct: float = field(
default=0.8,
metadata={"help": "余弦缩放奖励函数中,正确答案的最小奖励值"},
)
cosine_max_value_correct: float = field(
default=1.0,
metadata={"help": "余弦缩放奖励函数中,正确答案的最大奖励值"},
)
cosine_max_len: int = field(
default=1000,
metadata={"help": "余弦缩放奖励函数的最大长度阈值"},
)
repetition_n_grams: int = field(
default=3,
metadata={"help": "重复惩罚奖励函数中,n-gram 的大小"},
)
repetition_max_penalty: float = field(
default=-0.1,
metadata={"help": "重复惩罚奖励函数中,最大惩罚值 (负值)"},
)
@dataclass 装饰器简化了数据类的创建过程。GRPOScriptArguments 类用于存储与奖励函数相关的配置参数。
reward_funcs 列表用于指定训练过程中启用的奖励函数,默认值为 ["accuracy", "format"]。用户可以根据需求添加其他奖励函数,如 "reasoning_steps", "cosine", "repetition_penalty"。
其他配置项主要用于调整 cosine_scaled_reward 和 repetition_penalty_reward 这两个奖励函数的行为,用户可根据具体情况调整奖励机制。
接下来,我们使用 transformers 库提供的 TrainingArguments 类。TrainingArguments 是一个核心配置类,几乎控制着训练过程的方方面面。
# 从 transformers 库定义 TrainingArguments
training_args = TrainingArguments(
output_dir=OUTPUT_DIR, # 检查点和日志输出目录
overwrite_output_dir=True,
num_train_epochs=1, # 训练的总 epoch 数
per_device_train_batch_size=8, # 每个设备的训练批次大小
per_device_eval_batch_size=16, # 评估批次大小
gradient_accumulation_steps=2, # 梯度累积步数,用于模拟更大的批次大小
learning_rate=5e-5, # AdamW 优化器的初始学习率
warmup_ratio=0.1, # 预热步数比例
weight_decay=0.01, # 权重衰减系数,应用于除 bias 和 LayerNorm 权重外的所有层
logging_steps=10, # 日志记录频率 (步数)
evaluation_strategy="steps", # 评估策略:每 `eval_steps` 步进行评估
eval_steps=50, # 评估频率 (步数)
save_strategy="steps", # 模型保存策略:每 `save_steps` 步保存模型
save_steps=50, # 模型保存频率 (步数)
save_total_limit=2, # 最大 checkpoint 保存数量,超出限制则删除旧 checkpoint
dataloader_num_workers=2, # 数据加载器 worker 数量
seed=42, # 随机种子,用于保证实验可复现
bf16=True, # 启用混合精度 BF16 训练
push_to_hub=False, # 是否将模型推送至 Hugging Face Hub
gradient_checkpointing=True, # 启用梯度检查点
report_to="none", # 不使用任何报告工具
)
最后,需要定义 ModelConfig 类。ModelConfig 用于配置与模型自身相关的参数,例如,指定预训练模型名称、数据类型 (如 bfloat16)、是否信任远程代码等。
@dataclass
class ModelConfig:
"""
模型配置类。
"""
model_name_or_path: str = field(
default=MODEL_NAME, metadata={"help": "预训练模型路径或 Hugging Face Model Hub 模型标识符"}
)
model_revision: Optional[str] = field(
default="main", metadata={"help": "指定模型版本 (分支名, tag 名 或 commit id)"}
)
torch_dtype: Optional[str] = field(
default="bfloat16", metadata={"help": "覆盖默认 torch_dtype,以指定 dtype 加载模型"}
)
trust_remote_code: bool = field(
default=True, metadata={"help": "加载模型和 tokenizer 时,信任远程代码"}
)
attn_implementation: Optional[str] = field(
default="flash_attention_2", metadata={"help": "选择 Attention 实现方式, 可选 'flash_attention_2' 或 None"}
)
ModelConfig 类存储了关键的模型配置信息,包括 model_name_or_path (默认为 Qwen 0.5B Instruct 模型)。torch_dtype="bfloat16" 用于提升训练效率,trust_remote_code=True 确保远程加载代码的安全性。此外,attn_implementation="flash_attention_2" 选项用于启用 FlashAttention 2,在硬件支持的情况下,可潜在地加速训练过程。
接下来,实例化上述配置类,以便在后续代码中使用:
# 实例化配置对象
script_args = GRPOScriptArguments()
model_args = ModelConfig()
然后,我们需要获取奖励函数列表,以及在训练过程中使用的“回调函数” (callbacks)。
回调函数类似于助手,在训练过程的不同阶段执行特定任务,例如记录训练进度、保存模型等。目前,我们仅使用一个简单的日志记录回调函数。
将奖励函数集中管理:
# 实用函数,根据脚本参数获取奖励函数列表
def get_reward_functions(script_args):
"""
根据脚本参数,返回奖励函数列表。
"""
reward_funcs_list = []
reward_funcs_registry = {
"accuracy": accuracy_reward, # 假设 accuracy_reward 函数已在之前步骤定义
"format": format_reward, # 假设 format_reward 函数已在之前步骤定义
"reasoning_steps": reasoning_steps_reward, # 假设 reasoning_steps_reward 函数已定义
"cosine": get_cosine_scaled_reward( # 假设 get_cosine_scaled_reward 函数已定义
min_value_wrong=script_args.cosine_min_value_wrong,
max_value_wrong=script_args.cosine_max_value_wrong,
min_value_correct=script_args.cosine_min_value_correct,
max_value_correct=script_args.cosine_max_value_correct,
max_len=script_args.cosine_max_len,
),
"repetition_penalty": get_repetition_penalty_reward( # 假设 get_repetition_penalty_reward 函数已定义
ngram_size=script_args.repetition_n_grams,
max_penalty=script_args.repetition_max_penalty,
),
}
for func_name in script_args.reward_funcs:
if func_name not in reward_funcs_registry:
raise ValueError(f"Reward function '{func_name}' not found in registry.")
reward_funcs_list.append(reward_funcs_registry[func_name])
return reward_funcs_list
定义回调函数,用于跟踪训练损失及其他关键信息:
logger = logging.getLogger(__name__)
class LoggingCallback(TrainerCallback):
"""
一个简单的回调函数,用于在特定步骤记录训练信息。
"""
def on_step_end(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs):
if state.global_step % args.logging_steps == 0:
logger.info(f"Step {state.global_step}: Loss = {state.log_history[-1].get('loss', None)}, Learning Rate = {state.log_history[-1].get('learning_rate', None)}")
def get_callbacks(training_args, model_args, script_args):
"""
返回训练过程中使用的回调函数列表。
目前仅包含 LoggingCallback。您可以扩展此列表以添加更多回调函数。
"""
callbacks = [LoggingCallback()] # 实例化 LoggingCallback
return callbacks
最后,初始化奖励函数和回调函数:
# 获取奖励函数和回调函数
reward_functions = get_reward_functions(script_args)
callbacks = get_callbacks(training_args, model_args, script_args)
GRPO 训练循环
本节将启动 GRPO 训练的核心引擎。初始化 GRPOTrainer,并将之前准备好的所有组件传入,包括模型、奖励函数、训练参数、数据集和回调函数。
初始化 GRPOTrainer:
# 从 TrainingArguments 创建 GRPOConfig
grpo_config = GRPOConfig(
**training_args.to_dict(), # 将 TrainingArguments 转换为字典并解包
**{
# 此处移除了 model_init_kwargs
# 因为我们直接传递了实例化的 'model' 对象,GRPOTrainer 无需 model_init_kwargs
}
)
grpo_trainer = GRPOTrainer(
model=model, # 初始化的 Qwen 模型
reward_funcs=reward_functions, # 前述步骤定义的奖励函数列表
args=grpo_config, # GRPOConfig 对象 (由 TrainingArguments 创建)
train_dataset=dataset['train'], # 训练数据集
eval_dataset=dataset['test'], # 评估数据集
callbacks=callbacks # 回调函数列表
)
现在,可以启动 训练循环。只需调用 grpo_trainer 对象的 train() 方法即可开始训练:
# 启动 GRPO 训练循环
train_result = grpo_trainer.train()
执行上述代码后,您应能观察到训练过程开始运行。
...
INFO:dee phub__main__:Step 10: Loss = ..., Learning Rate = ...
INFO:deeph ub __main__:Step 20: Loss = ..., Learning Rate = ...
...
训练时长取决于硬件配置和设定的 epoch 数。由于本文示例中 num_train_epochs 仅设置为 1,且模型规模较小,因此训练过程相对迅速。
然而,在实际的 DeepSeek R1 Zero GRPO 训练中,通常需要进行更多 epoch 和步数的训练。
保存 Tiny R1 Zero LLM
训练完成后,即可保存训练得到的模型,用于后续的推理任务。
# 定义训练后模型保存路径 (与 OUTPUT_DIR 相同)
TRAINED_MODEL_PATH = "data/Qwen-GRPO-training"
# 保存 tokenizer
tokenizer.save_pretrained(TRAINED_MODEL_PATH)
# 保存训练后的模型
grpo_trainer.save_model(TRAINED_MODEL_PATH)
print(f"GRPO 训练后的模型已保存至 {TRAINED_MODEL_PATH}")
保存完成后,可以使用以下代码加载训练好的模型:
# 加载 tokenizer - 如有需要,请确保设置 trust_remote_code=True
tokenizer = AutoTokenizer.from_pretrained(
TRAINED_MODEL_PATH,
trust_remote_code=True, # 如果模型配置需要,则设置为 True
padding_side="right" # 确保 padding 方向一致
)
# 若 pad token 未正确保存或加载,则进行设置
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 加载训练后的模型
trained_model = AutoModelForCausalLM.from_pretrained(
TRAINED_MODEL_PATH,
trust_remote_code=True, # 如果模型架构需要,则设置为 True
torch_dtype=torch.bfloat16 # 保持与训练时一致的数据类型
)
# 将加载的模型移至指定设备 (如有 GPU 可用,则移至 GPU)
trained_model.to(device) # 'device' 变量仍为之前定义的 CUDA 设备
进行推理测试:
# 使用训练后的模型进行推理测试
def test_trained_model_inference(user_input: str):
"""使用加载的训练后模型和 tokenizer 进行推理测试。"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT}, # 复用之前的系统 Prompt
{"role": "user", "content": user_input}
]
# 使用 tokenizer 应用聊天模板
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
# 对输入文本进行分词
inputs = tokenizer(text, return_tensors="pt").to(device)
# 使用 *trained_model* 生成模型输出
outputs = trained_model.generate(
**inputs,
max_new_tokens=200, # 相比之前,可以生成稍长的文本
do_sample=True,
temperature=0.7
)
# 将生成的 token 解码为文本
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
return response
R1 Zero 的主要问题
至此Qwen2–0.5B 基础模型(而非 DeepSeek R1 原始的 DeepSeek V3 基础模型)完成了 R1 Zero 模型的训练。
尽管本文的训练模型可能无法直接体现 R1 Zero 的问题,但 DeepSeek 团队的研究人员发现,R1 Zero 模型在推理测试中表现出色,在 AIME 2024 等任务上的得分甚至与 OpenAI-01–0912 等更先进的模型相近。
这验证了使用强化学习 (RL) 鼓励语言模型进行推理是一种有潜力的方向。
然而,DeepSeek 团队也注意到 DeepSeek-R1-Zero 存在一些关键问题,需要解决这些问题才能使其真正应用于实际场景和更广泛的研究领域。
DeepSeek 团队的研究人员指出,R1 Zero 的 Prompt 模板是 有意设计得简洁且侧重于结构, 避免对推理过程本身施加任何内容特定的约束。例如,Prompt 模板并未明确要求:
-
“你 必须 使用逐步推理”(仅使用了 “推理过程” 这一宽泛表述,将推理方式的定义权交由模型自身)。
-
“你 必须 使用反思性推理”。
-
“你 必须 采用特定的问题解决策略”。
R1 Zero 模型的主要问题在于,<think> 标签内的推理过程可读性较差,人类难以理解和分析模型的推理思路。
另一个问题是 语言混合。当用户以多语言提问时,模型有时会在同一回答中混用多种语言,导致输出结果不一致且混乱。
例如,当用户以西班牙语提问时,模型在 <think> 标签内的“思考”过程可能会出现 英语和西班牙语混杂 的情况,输出质量欠佳。这些问题,即推理过程的混乱和语言的混用,成为了 R1 Zero 模型进一步发展的阻碍。
正是上述两个主要问题,促使 DeepSeek 团队将初始的 R1 Zero 模型迭代升级为 R1 模型。
为 SFT 准备冷启动数据
为解决 R1 Zero 模型存在的问题,并使 DeepSeek R1 模型具备更完善的推理能力,DeepSeek 团队开展了 冷启动数据收集工作,并引入了监督微调 (Supervised Fine-Tuning, SFT) 技术。
可以将冷启动数据收集理解为,在进行高强度的强化学习训练之前,为模型奠定良好的推理基础。其核心目标是,让 DeepSeek-V3 Base 模型(或本文示例中的 Qwen2–0.5B 模型)学习何为高质量的推理,以及如何清晰地呈现推理过程。
基于长 CoT 的少样本 Prompting
基于长思维链 (CoT) 的少样本 Prompting 是一种有效的数据构建技术。该技术的核心思想是,向 DeepSeek-V3 Base 模型(或本文示例中的 Qwen2–0.5B 模型)展示少量问题示例,并为每个问题配备极其详尽的、逐步分解的解答,即长思维链 (Long Chain-of-Thought, Long CoT)。
该技术的目的是使模型通过学习示例,模仿这种详尽的推理风格,并逐步掌握高质量推理的模式。
以问题 “2 + 3 * 4 等于多少?” 为例,我们可以构建包含少量已解答问题的 Prompt 示例。以下 Python 代码展示了如何实现基于长 CoT 的少样本 Prompting:
# 加载模型和 Tokenizer
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True, padding_side="right")
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True, torch_dtype=torch.bfloat16).to("cuda" if torch.cuda.is_available() else "cpu")
# 生成长 CoT 响应
def generate_response(prompt_text):
messages = [
{"role": "system", "content": "dee phub You are a helpful assistant that provides step-by-step solutions."},
{"role": "user", "content": prompt_text}
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=200, do_sample=False) # 为示例保持确定性输出
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
return response.split("<|im_start|>assistant\n")[-1].strip() # 提取助手的响应
为提出的问题定义少样本 Prompt 示例:
# 示例问题及其解答 (使用 | special_token | 作为分隔符)
few_shot_prompt = """
Problem: dee ph ub What's the square root of 9 plus 5?
Solution: <|special_token|> First, find the square root of 9, which is 3. Then, add 5 to 3. 3 + 5 equals 8. <|special_token|> Summary: The answer is 8.
Problem: Train travels at 60 mph for 2 hours, how far?
Solution: <|special_token|> Use the formula: Distance = Speed times Time. Speed is 60 mph, Time is 2 hours. Distance = 60 * 2 = 120 miles. <|special_token|> Summary: Train travels 120 miles.
Problem: What is 2 + 3 * 4?
Solution:
"""
使用基础模型进行少样本生成:
# 使用少样本示例,为目标问题生成响应
target_problem_prompt = few_shot_prompt + "What is 2 + 3 * 4?"
model_response_few_shot = generate_response(target_problem_prompt)
print("Few-shot Prompt:")
print(target_problem_prompt)
print("\nModel Response (Few-shot CoT):")
print(model_response_few_shot)
模型输出结果如下,呈现出结构化的数据格式:
Few-shot Prompt:
Problem: What's the square root of 9 plus 5?
Solution: <|special_token|> First, find the square root of 9,
which is 3. Then, add 5 to 3. 3 + 5 equals 8.
<|special_token|> Summary: The answer is 8.
Problem: Train travels at 60 mph for 2 hours, how far?
Solution: <|special_token|> Use the formula: Distance = Speed times Time.
Speed is 60 mph, Time is 2 hours. Distance = 60 * 2 = 120 miles.
<|special_token|> Summary: Train travels 120 miles.
Problem: What is 2 + 3 * 4?
Solution:
Model Response (Few-shot CoT):
<|special_token|> To solve 2 + 3 * 4, we need to follow the order
of operations (PEMDAS/BODMAS). Multiplication should be performed
before addition.
Step 1: Multiply 3 by 4, which equals 12.
Step 2: Add 2 to the result from Step 1: 2 + 12 = 14.
<|special_token|> Summary: The answer is 14.
可以看到,在学习了少量示例后,模型开始采用 <|special_token|> 分隔符来组织答案,并提供逐步推理过程,最终给出总结和最终答案。
这便是少样本学习的强大之处,它能够引导模型学习并生成期望的输出格式。
直接 Prompting
直接 Prompting 是另一种数据构建方法。与少样本 Prompting 不同,直接 Prompting 侧重于直接指示模型,不仅要解决问题,还要明确地展示逐步推理过程,并对答案进行验证。
直接 Prompting 的目标是鼓励模型采取更审慎、更周全的问题解决策略。
以下代码展示了如何为问题 “2 + 3 * 4 等于多少?” 构建 Prompt,并显式要求模型进行推理和验证:
# 直接 prompting 示例
direct_prompt_text = """
Problem: d ee p hub Solve this, show reasoning step-by-step, and verify:
What is 2 + 3 * 4?
"""
model_response_direct = generate_response(direct_prompt_text)
print("Direct Prompt:")
print(direct_prompt_text)
print("\nModel Response (Direct Prompting):")
print(model_response_direct)
直接 Prompting 的输出结果清晰易懂,如下所示:
Direct Prompt:
Problem: Solve this, show reasoning step-by-step, and verify:
What is 2 + 3 * 4?
Model Response (Direct Prompting):
<|special_token|> Reasoning: To solve 2 + 3 * 4, I need to follow
the order of operations, which states that multiplication should
be done before addition.
Step 1: Multiply 3 by 4, which equals 12.
Step 2: Add 2 to the result from Step 1: 2 + 12 = 14.
Verification: d ee p hub To verify the answer, I can double-check the
order of operations and the calculations. Multiplication is
indeed performed before addition, and the calculations are correct.
<|special_token|> Summary: The answer is 14.
如输出所示,通过直接要求模型进行推理和验证,模型生成了更为全面的输出结果,其中包括 “Verification” (验证) 部分。
直接 Prompting 方法能够有效地引导模型生成用户期望的、包含详细推理过程的解答。
后处理优化
最后一种数据构建技术是后处理优化。值得注意的是,DeepSeek 团队甚至使用了已训练完成的 R1 Zero 模型的输出结果进行后处理优化。
即使 R1 Zero 模型存在一些问题,但其已具备一定的推理能力。因此,DeepSeek 团队收集了 R1 Zero 模型的输出,并由人工标注员对这些输出进行精细化处理,使其更清晰、结构更规整,并纠正其中的错误。
例如,对于如下 R1 Zero 模型生成的、略显粗糙的输出:
<think> ummm... multiply 3 and 4... get 12... then add 2...</think>
<answer> 14 </answer>
人工标注员会将其优化为更清晰、格式更规范的输出:
<|special_token|> Reasoning: d ee p hub To solve this, we use order of operations, doing multiplication before addition.
Step 1: Multiply 3 by 4, which is 12.
Step 2: Add 2 to the result: 2 + 12 = 14.
<|special_token|> Summary: The answer is 14.
在代码层面,我们难以完美地模拟人工优化过程。但我们可以演示一种基本思路,即如何以编程方式对潜在的粗糙输出进行重新格式化和结构化。
以下代码以模拟的 “粗糙” 输出为例,展示如何对其进行优化:
# 模拟的 R1 Zero 模型粗糙输出
messy_output = "<think> ummm... multiply 3 and 4... get 12... then add 2...</think>\n<answer> 14 </answer>"
def refine_output(messy_text):
think_content = messy_text.split("<think>")[1].split("</think>")[0].strip()
answer_content = messy_text.split("<answer>")[1].split("</answer>")[0].strip()
refined_text = f"""<|special_token|> Reasoning: {think_content.replace('umm...', '').strip().capitalize()}.
<|special_token|> Summary: The answer is {answer_content}."""
return refined_text
refined_output_text = refine_output(messy_output)
print("Messy Output (Simulated R1 Zero):")
print(messy_output)
print("\nRefined Output:")
print(refined_output_text)
代码输出结果如下:
Messy Output (Simulated R1 Zero):
<think> ummm... multiply 3 and 4... get 12... then add 2...</think>
<answer> 14 </answer>
Refined Output:
<|special_token|> Reasoning: Multiply 3 and 4... get 12... then add 2.
<|special_token|> Summary: The answer is 14.
示例中的 refine_output 函数仅为演示基本优化思路。真实的人工优化过程远比代码示例复杂,涉及到对推理步骤更细致的理解和更正。
然而,代码示例已展现了后处理优化的核心思想:对模型的初始输出进行质量提升和结构优化,从而构建更高质量的训练数据。
在生成冷启动数据之后,下一个关键步骤是 监督微调 (SFT),我们将在下一节详细探讨 SFT 的训练过程。
基于冷启动数据的阶段 1 SFT 训练
为利用监督微调技术 (SFT) 构建 R1 模型,并生成高质量的冷启动数据,我们需要投入专业团队和大量的代码开发工作。幸运的是,我们已经拥有了与冷启动数据形式相近的数据集 (Bespoke-Stratos-17k)。
为了深入理解 SFT 的训练机制,我们需要了解 SFT 训练器在处理训练数据时,其内部执行了哪些操作?
SFT 属于监督学习范畴。这意味着,我们需要向模型提供成对的输入和期望的输出。
在本文的场景中,输入可以是问题 Prompt,期望的输出则是来自训练数据集的、包含良好推理过程的逐步解答。希望这一点能够清晰地阐释冷启动数据存在的必要性。
SFT 训练器接收分词后的训练数据,并以批次 (batch) 的形式输入模型。针对每个批次,训练器会执行一系列关键操作。下图展示了 SFT 训练的内部流程:
首先,模型接收输入,例如问题 Prompt。模型处理该输入,并逐 token 生成其对问题解答的最佳预测,这些预测的 token 即为预测 token。
接下来,SFT 训练器需要评估模型预测的质量。评估过程依赖于损失函数,通常采用交叉熵损失函数 (Cross-Entropy Loss)。损失函数从数学层面比较模型的预测 token 与训练数据中正确的token,计算模型答案的 “误差”。
计算得到的 “误差” 并不会被直接丢弃,而是作为模型学习的关键信号。通过名为反向传播 (backpropagation) 的过程,误差信号被用于计算梯度 (gradients)。梯度类似于指南针,指示模型参数调整的方向,模型参数沿着梯度方向调整后,可有效降低预测误差。
最后,优化器 (optimizer),如 AdamW,利用计算得到的梯度,对模型的内部参数进行细微调整。这些调整旨在使模型在下一次预测时,输出结果更接近标准答案。
R1 阶段 1 SFT 训练配置
R1 Zero 模型在推理过程的清晰度和语言一致性方面存在不足。SFT 训练旨在解决这些问题。通过使用高质量、优化后的数据进行训练,SFT 能够引导模型:
-
学习清晰的推理风格:以易于理解和追溯的方式组织 “思考” 过程。
-
保持语言一致性:在单个回复中坚持使用单一语言,避免语言混用造成的困扰。
本文采用 Bespoke-Stratos-17k 数据集进行 SFT 训练。如前文所述,该数据集包含 1.7 万个数学和代码相关的问题,其数据格式与我们的训练需求高度契合。
回顾 Bespoke-Stratos-17k 数据集的样本示例:
# 从 bespokelabs 加载 "Bespoke-Stratos-17k" 数据集
bespoke_rl = load_dataset("bespokelabs/Bespoke-Stratos-17k", "default")
# 访问训练集首个样本
bespoke_rl['train'][0]
输出:
#### 输出 ####
{
'system': 'Your role as an assistant involves ... ',
'conversations': [{'from': 'user', 'value': 'Return your ...'}]
}
#### 输出 ####
该数据集包含系统 Prompt 和用户-助手对话,非常适合用于指导模型学习如何进行包含推理过程的对话。
本文再次使用 trl 库,该库极大简化了 SFT 训练的流程。
首先,配置 SFT 训练参数。配置方式与 GRPO 训练类似,但需针对 SFT 训练进行调整:
# 模型和输出配置 (与之前相同,或根据需要调整)
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
OUTPUT_DIR = "data/Qwen-SFT-training" # SFT 模型的新输出目录
os.makedirs(OUTPUT_DIR, exist_ok=True)
# 训练参数 - 与 GRPO 类似,但需针对 SFT 进行调整
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
overwrite_output_dir=True,
num_train_epochs=1, # 根据需要调整 epoch 数
per_device_train_batch_size=8,
per_device_eval_batch_size=16,
gradient_accumulation_steps=2,
learning_rate=2e-5, # 为 SFT 调整学习率
warmup_ratio=0.1,
weight_decay=0.01,
logging_steps=10,
evaluation_strategy="no",
eval_steps=50,
save_strategy="steps",
save_steps=50,
save_total_limit=2,
dataloader_num_workers=2,
seed=42,
bf16=True,
push_to_hub=False,
gradient_checkpointing=True,
report_to="none",
packing=True, # 启用数据打包以提高训练效率
max_seq_length=4096 # 设置最大序列长度
)
# 模型配置 - 与之前相同
model_args = ModelConfig(
model_name_or_path=MODEL_NAME,
model_revision="main",
torch_dtype="bfloat16",
trust_remote_code=True,
attn_implementation="flash_attention_2"
)
TrainingArguments 和 ModelConfig 的配置与 GRPO 训练基本一致,但针对 SFT 训练进行了微调 (如略微调整了学习率)。值得注意的是,packing=True 和 max_seq_length=4096 这两个参数对于提升长序列 SFT 训练的效率至关重要。
阶段 1 SFT 训练循环
加载数据集和 tokenizer:
# 加载 Bespoke-Stratos-17k 数据集
dataset_sft = load_dataset("HuggingFaceH4/Bespoke-Stratos-17k", split='train') # 仅使用训练集以简化流程
# 初始化 tokenizer - 与之前相同
tokenizer = AutoTokenizer.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
padding_side="right"
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
初始化 SFTTrainer 并启动训练:
# 初始化 SFT 训练的基础模型 - 与之前相同
model_sft = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
torch_dtype=torch.bfloat16
)
# 初始化 SFT 训练器
sft_trainer = SFTTrainer(
model=model_sft, # 初始化的 Qwen 模型
train_dataset=dataset_sft, # Bespoke-Stratos-17k 数据集
tokenizer=tokenizer, # Tokenizer
args=training_args, # 训练参数
dataset_text_field="conversations", # 数据集中包含文本的字段 - SFT 训练的关键参数
packing=True, # 启用数据打包
max_seq_length=4096 # 最大序列长度
)
# 启动 SFT 训练循环
sft_train_result = sft_trainer.train()
执行上述代码后,SFT 训练过程将开始运行。训练日志输出与 GRPO 训练类似,将在每个日志记录步骤显示损失值 (loss) 和学习率 (learning rate)。
...
INFO:__main__:Step 10: Loss = ..., Learning Rate = ...
INFO:__main__:Step 20: Loss = ..., Learning Rate = ...
...
与 GRPO 训练类似,SFT 训练时长取决于硬件配置和设定的 epoch 数。由于本文示例仍采用小规模模型,且仅训练 1 个 epoch,因此训练过程应相对快速。
保存 Tiny R1 LLM
SFT 训练完成后,保存新微调的模型 (R1):
# 保存训练后的 SFT 模型
TRAINED_SFT_MODEL_PATH = "data/Qwen-SFT-training" # 与 OUTPUT_DIR 相同
# 保存 tokenizer
tokenizer.save_pretrained(TRAINED_SFT_MODEL_PATH)
# 保存训练后的模型
sft_trainer.save_model(TRAINED_SFT_MODEL_PATH)
print(f"SFT 训练后的模型已保存至 {TRAINED_SFT_MODEL_PATH}")
至此,SFT 训练环节完成。我们已利用基础模型,并向其展示了大量高质量推理示例,通过微调使其更擅长生成清晰、结构化的响应。
经过阶段 1 SFT 训练微调的模型,即为本文所称的 R1 模型。
SFT 之后的训练步骤,特别是强化学习阶段和拒绝采样策略,从零开始使用 Python 实现较为复杂。理解其背后的理论原理是把握整个训练流程的关键。
面向推理的强化学习
SFT 训练后的模型推理能力得到提升,但为进一步聚焦推理质量,并彻底解决语言混用问题,DeepSeek 团队在后续阶段再次采用了强化学习,并设计了更精细化的奖励系统。
新的奖励系统会检查模型输出的推理过程和答案是否与用户提问时使用的语言保持一致。例如,若用户使用英语提问,则模型 整个 回复(包括推理和答案)都应使用英语。这有效解决了语言混用问题。
在准确性奖励的基础上,DeepSeek 团队 引入了语言一致性奖励,以确保 SFT 模型在推理和回答时,所用语言与输入语言保持一致。R1 Zero 阶段使用的 GRPO 算法和训练循环被复用,但奖励信号得到改进,更精准地引导模型生成高质量推理结果和语言风格一致的输出。
拒绝采样
为获得更高质量的推理数据,DeepSeek 团队采用了 拒绝采样 (Rejection Sampling) 策略。拒绝采样可被视为一个过滤器,用于筛选并保留质量最佳的训练样本。
模型首先生成大量的推理示例,随后,对这些示例的正确性和推理质量进行评估 (评估过程通常结合生成式奖励模型和人工评估)。
只有质量最佳的、高质量推理示例才会被保留。DeepSeek 团队将这些高质量推理示例与非推理数据相结合,构建了精细化的数据集,并用于 阶段 2 SFT 训练,进一步提升模型的推理能力和通用能力。
阶段 2 SFT 训练
最终的强化学习阶段,目标是将模型训练为在 所有 场景下都乐于助人且安全的 AI 助手,而不仅限于解决推理问题。该阶段侧重于模型与人类价值观的对齐。
核心关注点:助人性和无害性奖励
在奖励机制设计上,不仅关注答案的准确性,还引入了以下奖励指标:
-
助人性 (Helpfulness):模型回复是否实用且信息量丰富?
-
无害性 (Harmlessness):模型回复是否安全、无偏见且符合伦理道德?
训练数据变得更加多样化,不仅包含推理任务,还融入了人类偏好数据 (用于指示哪个输出更佳——更助人,更无害)。
奖励系统在准确性、助人性和无害性之间寻求平衡。通过迭代强化学习训练 (很可能仍采用 GRPO 算法),模型得到持续优化,最终不仅擅长推理,更成长为一个安全、乐于助人、可广泛应用的 AI 助手,即 DeepSeek R1 模型。
知识蒸馏
为使 DeepSeek R1 模型更易于部署和应用,DeepSeek 团队采用了知识蒸馏 (Distillation) 技术,将其知识迁移到规模更小的模型中。
知识蒸馏技术将大型、高性能的 “教师” 模型 (DeepSeek R1) 的知识,转移到规模较小的 “学生” 模型。DeepSeek 团队构建了包含大量推理示例的数据集,并将 DeepSeek R1 模型的输出作为目标答案。
随后,使用监督微调 (SFT) 技术训练小规模模型,使其模仿 DeepSeek R1 模型的输出。最终,得到规模更小、推理速度更快的模型,这些 “学生” 模型继承了 DeepSeek R1 模型推理能力的重要部分,使其更具实用价值,可应用于更广泛的场景。
总结
本文深入剖析了 DeepSeek R1 模型的构建过程,从基础模型选型到多阶段训练流程,再到关键技术如强化学习、拒绝采样和知识蒸馏的应用,进行了详尽的阐述。通过对 GRPO 算法、Prompt 模板、奖励函数以及 SFT 训练等核心环节的逐步解析,我们不仅了解了 DeepSeek R1 如何从零开始构建,更对其在推理能力、语言一致性以及安全助人等方面所做的努力有了更深刻的认识。希望本文能够帮助读者更好地理解 DeepSeek R1 的技术原理,并为相关研究和实践提供有益的参考。
作者:FareedKhan
THE END !
文章结束,感谢阅读。您的点赞,收藏,评论是我继续更新的动力。大家有推荐的公众号可以评论区留言,共同学习,一起进步。