关于deepseek训练tokenize设计的思考
文章摘要:本文探讨了对话系统中思考过程(tokenize)处理的优化方法。初始设计将思考过程(reason)错误地包含在指令(instruction)部分,导致其未被纳入训练目标。改进方案将思考过程作为助理(assistant)回复的一部分,包括在标签(labels)中以计算损失。修正后的代码结构分为三部分:1) 系统提示和用户问题(忽略损失);2) 助理完整回复(含思考过程和最终答案);3) E
·
1. 初始tokennize设计
instruction = tokenizer( <|im_start|>system\n{system_prompt}\n<|im_end|>\n<im_start>user\n{example['text']}\n<|im_end|>\n<|im_start|>assistant\n<think>\n{example['reason']}\n</think>\n",add_special_tokens=False)
response = tokenizer(f"{example['output']}", add_special_tokens=False)
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
attention_mask = (instruction["attention_mask"] + response["attention_mask"] + [1])
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
2. 重新设计
问题:
在instruction中包含了example['reason'](即思考过程),这应该属于assistant的response部分
当前的拼接方式会导致:
input_ids:将example['reason']和example['output']都放在assistant部分
labels:由于instruction部分的标签被设置为-100(忽略loss),而只有response部分的token有loss
因此,example['reason']被放在了instruction的末尾,导致它没有标签(即被忽略),而example['output']有标签
但这样处理是不对的,因为example['reason'](思考过程)也应该是assistant生成的内容的一部分,因此它应该放在response中,并且计算loss。
我们期望的流程:
用户提问(example['text'])后,assistant首先生成思考过程(<thinking>标签包裹),然后生成回复(<answer>标签包裹)
因此,整个assistant的回复应该是:
<think>\n{example['reason']}\n</think>\n{example['output']}
因此,修改思路如下:
将assistant的整个回复内容合并为一个字符串:assistant_response = f"<think>\n{example['reason']}\n</think>\n{example['output']}"
构建instruction时,只包含到assistant开始(即<|im_start|>assistant\n),不要包含assistant的任何输出。
然后,将assistant_response作为assistant的输出,并tokenize。
最后将instruction的token和assistant_response的token拼接,并设置labels:
instruction部分(包括system、user和assistant的开始标记)的token对应的label为-100
assistant_response的token对应的label为实际token
修改后代码:
# 构建instruction部分(到assistant开始标记为止)
instruction_text = (
f"<|im_start|>system\n{system_prompt}\n<|im_end|>\n"
f"<|im_start|>user\n{example['text']}\n<|im_end|>\n"
f"<|im_start|>assistant\n"
)
instruction = tokenizer(
instruction_text,
add_special_tokens=False
)
# 构建assistant的整个回复(包括<thinking>和<answer>部分)
assistant_response = f"<think>\n{example['reason']}\n</think>\n{example['output']}"
# Tokenize assistant的回复
response = tokenizer(assistant_response, add_special_tokens=False)
# 合并input_ids和attention_mask
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.eos_token_id]
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
# 设置labels:instruction部分为-100,assistant_response部分为实际token
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.eos_token_id]
# 注意:这里添加了EOS token,并且它的label也要计算loss(所以不是-100)
解释:
这样设计后,模型在训练时会学习生成assistant的整个回复(包括<thinking>和<answer>两部分)
在生成时,可以引导模型首先生成<thinking>思考过程,然后生成<answer>最终答案
注意:这里使用了tokenizer.eos_token_id作为结束符,并计算其loss。
另外,关于标签:
在训练时,我们只计算assistant生成部分的loss(即从<|im_start|>assistant后面的所有内容)
因此,instruction部分(包括system、user和assistant开始标记)都不计算loss(用-100屏蔽)
这样修改后,数据放置就正确了。
</think>
以下是修正后的代码设计,重点解决了
<think>/</think>
数据位置问题 并优化了整体逻辑:
def format_example(example):
# 重构instruction:保证system和user部分不包含assistant输出
# 注意:这里移除了对 example['reason'] 的错误包含
instruction = tokenizer(
f"<|im_start|>system\n{system_prompt}\n<|im_end|>\n"
f"<|im_start|>user\n{example['text']}\n<|im_end|>\n"
f"<|im_start|>assistant\n", # 仅包含assistant起始标记
add_special_tokens=False,
return_tensors="pt"
)
# 正确拼接assistant响应:完整包含思考+回答的完整输出
# 使用特殊标签明确区分思考/回答区间
full_response = (
f"<think>\n{example['reason']}\n</think>\n" # 思考区间
f"{example['output']}" # 最终回答
)
# Tokenize完整响应
response = tokenizer(
full_response,
add_special_tokens=False,
return_tensors="pt"
)
# 合并输入序列(添加EOS标记)
input_ids = torch.cat([
instruction.input_ids[0],
response.input_ids[0],
torch.tensor([tokenizer.eos_token_id])
])
# 创建attention mask
attention_mask = torch.ones_like(input_ids)
# 创建labels: 仅对assistant输出计算损失
labels = torch.cat([
torch.full(instruction.input_ids.shape, -100), # 忽略系统/用户部分
response.input_ids[0], # 学习整个响应
torch.tensor([tokenizer.eos_token_id]) # 学习结束符
])
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
关键修正说明:
问题诊断:
原代码错误将 example['reason'] (思考过程) 放在 instruction 中
导致思考过程被包含在输入序列但未计入训练目标
修正方案:
# 错误:思考过程放错位置 (instruction内)
f"...assistant\n<think>\n{example['reason']}..."
# 正确:思考过程作为响应的一部分
full_response = f"<think>\n{example['reason']}...{example['output']}"
损失计算优化:
labels = [
[-100] * len(instruction), # 忽略系统/用户输入
response_ids, # 学习完整响应(含思考)
[eos_token] # 学习结束符
]
保证模型学习整个
<think>...思考...</think>...输出
序列
与推理时生成流程保持一致
序列结构可视化:
[系统提示][用户问题][助理开头标记] | <think> 思考内容 </think> 最终回答 <EOS>
└─── 忽略损失 ───┘ └───────────── 计算损失的目标区域 ─────────────┘
使用验证示例:
# 样本结构
example = {
"text": "如何计算圆的面积?",
"reason": "圆面积公式为πr²,需先确定半径",
"output": "面积 = π × 半径的平方"
}
# 处理后模型学习目标:
# 输入: [系统][用户问题][assistant开始]
# 目标: <think>\n如何计算圆的面积?\n</think>\n面积=π×半径²<EOS>
# ^^^^^^^^^^^^^^^^^^^^^^^^
# 模型需要完整生成的响应部分
核心原则:在微调任务中,思考过程必须包含在模型需要生成的响应序列内,不能作为输入内容的一部分,这样才能让模型学习到"思考-回答"的完整生成逻辑。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)