目录
[一、核心前提:特殊 Token 在训练中的共性](#一、核心前提:特殊 Token 在训练中的共性)
[二、阶段 1:预训练阶段(Base 模型)------ 仅用 <|endoftext|>](#二、阶段 1:预训练阶段(Base 模型)—— 仅用 <|endoftext|>)
[三、阶段 2:对话微调阶段(Chat 模型)------ 核心用 <|im_start|>/<|im_end|>](#三、阶段 2:对话微调阶段(Chat 模型)—— 核心用 <|im_start|>/<|im_end|>)
[四、训练后模型对 Token 的认知变化](#四、训练后模型对 Token 的认知变化)
[多轮对话中special token的组织形式:](#多轮对话中special token的组织形式:)
你关注的这 3 个核心特殊 Token(<|endoftext|>、<|im_start|>、<|im_end|>)在 Qwen 模型预训练 和对话微调 阶段的应用逻辑完全不同,是模型能理解 "文本边界" 和 "对话结构" 的核心 ------ 预训练阶段主要用 <|endoftext|> 划分样本,微调阶段则靠 <|im_start|>/<|im_end|> 构建结构化对话数据。
一、核心前提:特殊 Token 在训练中的共性
无论预训练还是微调,这 3 个 Token 都有 2 个关键训练属性(由代码中 AddedToken 的参数决定):
special=True:训练时被标记为 "特殊 Token",不参与分词器的子词拆分,始终作为单个完整 Token 处理;normalized=False:训练时不做文本归一化(如大小写转换、空格清理),保证 Token 格式绝对固定;lstrip=False/irstrip=False:训练时不会忽略 Token 左右的空格,避免对话结构错位。
二、阶段 1:预训练阶段(Base 模型)------ 仅用 <|endoftext|>
Qwen 预训练的核心目标是学习通用文本的语义和语法,此时 <|im_start|>/<|im_end|> 尚未被引入(这两个是对话微调阶段的专属 Token),仅 <|endoftext|> 发挥作用:
- 核心作用:划分预训练样本边界
预训练数据是海量无结构文本(书籍、网页、代码等),<|endoftext|> 是样本分隔符,用于告诉模型 "这是一个样本的结束,下一个是新样本"。
- 具体应用方式
-
数据预处理 :将海量文本切分为若干 "样本片段"(长度匹配模型上下文窗口,如 2048/4096 Token),每个片段末尾拼接
<|endoftext|>; -
训练输入格式 :
plaintext
样本1文本...<|endoftext|>样本2文本...<|endoftext|>样本3文本...<|endoftext|> -
模型学习目标 :模型需要预测每个位置的下一个 Token,当预测到
<|endoftext|>时,模型知道 "当前样本结束,下一个 Token 是新样本的开头",从而避免样本间的语义干扰。
- 关键细节
- 预训练阶段,
<|im_start|>/<|im_end|>不会出现在训练数据中,模型对这两个 Token 无任何先验认知; <|endoftext|>的 Token ID 是预训练阶段固定的(如 Qwen-7B 中为 151643),训练时作为 "终止符" 参与损失计算,但不会被模型随机生成(仅作为样本边界)。
三、阶段 2:对话微调阶段(Chat 模型)------ 核心用 <|im_start|>/<|im_end|>
Qwen-Chat 模型是在 Base 模型基础上做 "对话微调",此时 <|im_start|>/<|im_end|> 成为核心,<|endoftext|> 则退为辅助角色:
- 核心目标
让模型学会 "理解多轮对话结构"------ 区分「系统指令」「用户提问」「助手回答」的角色边界,生成符合人类对话逻辑的回复。
- 具体应用方式(结构化训练数据)
微调数据会被强制构造成「<|im_start|>+角色+文本+<|im_end|>」的固定格式,每个对话轮次都是一对 <|im_start|>/<|im_end|>,示例如下:
单轮对话训练样本
<|im_start|>system
你是一个乐于助人的AI助手,回答简洁明了。<|im_end|>
<|im_start|>user
什么是Qwen模型?<|im_end|>
<|im_start|>assistant
Qwen(通义千问)是阿里云研发的大语言模型,支持多轮对话和代码生成。<|im_end|><|endoftext|>
多轮对话训练样本
<|im_start|>system
你是数学老师,解答数学问题。<|im_end|>
<|im_start|>user
1+1等于多少?<|im_end|>
<|im_start|>assistant
1+1等于2。<|im_end|>
<|im_start|>user
那2+2呢?<|im_end|>
<|im_start|>assistant
2+2等于4。<|im_end|><|endoftext|>
- 各 Token 的微调分工
| Token | 微调阶段作用 | ||||
|---|---|---|---|---|---|
| `< | im_start | >` | 标记 "某角色发言的开始",后跟角色名(system/user/assistant),让模型识别发言者身份; | ||
| `< | im_end | >` | 标记 "某角色发言的结束",与 `< | im_start | >` 成对,划分单轮发言的边界; |
| `< | endoftext | >` | 标记 "整个对话样本的结束",放在最后一轮 `< | im_end | >` 之后,终止当前样本的训练; |
- 训练过程的关键逻辑
- 输入 / 输出构造 :
- 输入:完整的结构化对话文本(含所有
<|im_start|>/<|im_end|>); - 输出:与输入一致,但仅对 "助手回答部分" 计算损失(即模型只需学习生成「
<|im_start|>assistant到<|im_end|>」之间的内容);
- 输入:完整的结构化对话文本(含所有
- 梯度更新 :模型学习 "看到
<|im_start|>user后,生成符合逻辑的<|im_start|>assistant回复",<|im_start|>/<|im_end|>作为结构标记,帮助模型定位 "该生成哪部分内容"; - 停止生成学习 :微调时模型被训练为 "生成到
<|im_end|>后停止",最终对外提供服务时,<|endoftext|>作为兜底的生成终止符。
四、训练后模型对 Token 的认知变化
- 预训练后:模型仅认识
<|endoftext|>,知道它是 "文本结束"; - 微调后:模型学会:
<|im_start|>+ 角色名 → 触发对应角色的发言逻辑;<|im_end|>→ 结束当前角色的发言,准备接收下一轮输入;<|endoftext|>→ 整个对话结束,停止生成。
五、补充:训练时的技术细节
- Token ID 固定 :这 3 个 Token 的 ID 在训练前就被硬编码到分词器中(如
<|im_start|>=151644,<|im_end|>=151645),训练过程中不会改变; - 损失屏蔽 :
<|im_start|>/<|im_end|>/<|endoftext|>作为结构标记,不参与损失计算(模型只需预测 "语义文本",无需预测这些标记); - 批量训练适配 :当批量样本长度不一致时,用
<|endoftext|>作为pad_token填充(与你之前分词器配置pad_token="<|endoftext|>"一致),避免填充 Token 干扰对话结构。
总结
这 3 个特殊 Token 在训练中的应用核心是 "分层划分边界":
- 预训练 :靠
<|endoftext|>划分 "独立文本样本",学习通用语义; - 对话微调 :靠
<|im_start|>/<|im_end|>划分 "对话角色 / 轮次",学习结构化对话逻辑,<|endoftext|>兜底标记 "对话样本结束"。
正是这种训练时的结构化设计,让 Qwen-Chat 模型能精准理解多轮对话的角色和上下文,生成符合人类交互逻辑的回复。
多轮对话中special token的组织形式:
qwen_generation_utils:
python
def make_context(
tokenizer: PreTrainedTokenizer,
query: str,
history: List[Tuple[str, str]] = None,
system: str = "",
max_window_size: int = 6144,
chat_format: str = "chatml",
):
if history is None:
history = []
if chat_format == "chatml":
im_start, im_end = "<|im_start|>", "<|im_end|>"
im_start_tokens = [tokenizer.im_start_id]
im_end_tokens = [tokenizer.im_end_id]
nl_tokens = tokenizer.encode("\n")
def _tokenize_str(role, content):
return f"{role}\n{content}", tokenizer.encode(
role, allowed_special=set()
) + nl_tokens + tokenizer.encode(content, allowed_special=set())
system_text, system_tokens_part = _tokenize_str("system", system)
system_tokens = im_start_tokens + system_tokens_part + im_end_tokens
raw_text = ""
context_tokens = []
for turn_query, turn_response in reversed(history):
query_text, query_tokens_part = _tokenize_str("user", turn_query)
query_tokens = im_start_tokens + query_tokens_part + im_end_tokens
response_text, response_tokens_part = _tokenize_str(
"assistant", turn_response
)
response_tokens = im_start_tokens + response_tokens_part + im_end_tokens
next_context_tokens = nl_tokens + query_tokens + nl_tokens + response_tokens
prev_chat = (
f"\n{im_start}{query_text}{im_end}\n{im_start}{response_text}{im_end}"
)
current_context_size = (
len(system_tokens) + len(next_context_tokens) + len(context_tokens)
)
if current_context_size < max_window_size:
context_tokens = next_context_tokens + context_tokens
raw_text = prev_chat + raw_text
else:
break
context_tokens = system_tokens + context_tokens
raw_text = f"{im_start}{system_text}{im_end}" + raw_text
context_tokens += (
nl_tokens
+ im_start_tokens
+ _tokenize_str("user", query)[1]
+ im_end_tokens
+ nl_tokens
+ im_start_tokens
+ tokenizer.encode("assistant")
+ nl_tokens
)
raw_text += f"\n{im_start}user\n{query}{im_end}\n{im_start}assistant\n"
elif chat_format == "raw":
raw_text = query
context_tokens = tokenizer.encode(raw_text)
else:
raise NotImplementedError(f"Unknown chat format {chat_format!r}")
return raw_text, context_tokens
参考:
https://blog.csdn.net/u011995719/article/details/139046487