技术博客

特殊 token 的设计:BOS/EOS/PAD 如何影响生成

第 42 / 147 篇 · 2025-08-19 · ai · tokenizer特殊token训练

词表里有一类 token 不代表任何文本片段:<|begin_of_text|>、<|end_of_text|>、<|pad|>。它们数量不多,却控制着模型行为的关键节点——生成从哪开始、什么时候停、批内不等长序列怎么对齐。设计得不好,轻则浪费词表,重则引入训练隐患。本文把这几个 id 的设计意图和坑一次讲清。

三个角色,三种机制

BOS(begin of sequence)回答"序列从哪开始"。自回归模型生成第一个 token 时,上下文必须非空(注意力至少要有 query 位置),BOS 就是那个确定性的起点;训练时每个样本前都拼上它,模型学到"P(首个内容 token | BOS)"。它还有一个少有人提的作用:提供稳定的注意力锚点——softmax 需要一个总得有点概率的地方,BOS 作为全序列最稳定的早期 token,天然承接了这部分质量(第四章讲 attention sink 时会看到这正是 attention sink 的主要来源)。

EOS(end of sequence)回答"什么时候停"。它不只是"文档结束"的记号,而是生成循环的停机信号:采样循环每步检查输出是否为 EOS,是则停。它的训练信号来自交叉熵——语料里每个文档结尾的下一个 token 就是 EOS,模型学到 $P(\text{EOS}\mid\text{文档结尾上下文})$。这里有个微妙的设计选择:EOS 同时承担"文档边界"和"回答结束"两个语义。通用预训练里它只见过前者,指令微调时必须让模型把"用户问题回答完了"也映射到这个 token 上,否则会出现"答完不停、继续自问自答"——这是早期开源模型常见的失控模式。

PAD(padding)与生成无关,纯粹是批处理的工程产物。batch 内序列不等长,短序列尾部要补齐到最长长度;PAD token 填充这些位置。它的关键约束是必须被屏蔽:注意力掩码把 PAD 位置屏蔽掉(否则短序列会"看见"填充垃圾),损失掩码把 PAD 位置的标签置为 $-100$(PyTorch 交叉熵的忽略索引),让填充位置不参与梯度。

与训练机制的交互

一个训练样本的完整布局长这样:输入序列是 $[\text{BOS}]\ t_1\ t_2\ \dots\ t_n\ [\text{EOS}]\ [\text{PAD}]^m$,注意力掩码在真实部分为 1、PAD 部分为 0,标签整体右移一位后,PAD 对应的标签位置置 $-100$。EOS 在标签侧出现在最后一个真实位置——这个位置恰恰承担着"教模型停机"的全部梯度,如果语料截断策略把长文档拦腰切断,丢失的就是这个监督信号,模型在该停的上下文学不会停。

BOS 还有个版本差异值得留意。LLaMA 系训练时强制前置 BOS(add_bos_token=True),而 GPT-2 系不设 BOS,直接以第一个内容 token 起步——GPT-2 的预切分正则保证空白已编码进 token,不需要额外起点。两套约定互不兼容:同一个模型在 transformers 里用 add_bos_token=False 加载,首 token 分布会整体偏移。排查"同模型不同框架输出不同"的问题时,这是第一嫌疑。

一个批内样本的完整布局:token、注意力掩码、损失掩码三层对齐 输入序列 BOS t1 t2 t3 EOS PAD 注意力掩码 1 1 1 1 1 0 损失标签 t1 t2 t3 EOS -100 -100 三个关键位置:BOS 提供起点锚点;EOS 携带停机梯度;PAD 被双层掩码隔离 截断长文档 = 砍掉 EOS 监督信号,模型在该停的上下文学不会停

设计事故集锦

特殊 token 混进训练文本。 <|endoftext|> 这类字符串如果原样出现在预训练语料里(网页上讨论 GPT 的文章很多),BPE 会把它当普通文本切开,但如果它恰好整体匹配特殊 token 的表示(取决于训练管线的 tokenizer 配置),这个位置的嵌入会拿到梯度——而特殊 token 的嵌入在预训练里极少被更新,随机初始化遇上梯度可能直接数值发散。多数管线会对特殊 token 做文本级过滤,但漏网的不罕见。

词表尾部预留的沉睡条目。 cl200k 这类词表在 BOS/EOS 之外还预留几十个 reserved_special_token(Llama 3 同样)。它们从训练第一天起就占位词表,embedding 从未被更新过。为它们付 $2Vd$ 里的行数是必要的(未来加特殊 token 不用重训嵌入矩阵),但绝对不能让它们出现在任何训练的输入里——一次意外触发就是一次数值事故。

EOS 与截断的语义混淆。 前文提过:packing 的文档边界用 BOS/EOS 分隔时,模型学到"EOS 后大概率接 BOS";而 streaming 生成里 EOS 后是用户的新请求。两种统计的冲突不致命,但会让停机概率的校准偏保守或偏激进。一些训练框架用独立的 <|end_of_document|> 与 <|end_of_turn|> 把两个语义拆开,代价是多占词表行、多一组监督信号——这是设计上的正交性换成本。

Chat 模板与 tokenizer 的纠缠。 现代对话模型把 <|im_start|>user\n... 这类模板当特殊 token 处理。它们既是特殊 token(整段匹配、不可再分),又在模板里携带角色语义。模板改动(比如换行风格)不改 tokenizer 也能生效,但会改变特殊 token 周围的上下文分布,微调数据必须和新模板对拍——用旧模板数据微调新模板模型,是开源社区最常见的静默退化源之一。

小结

BOS/EOS/PAD 三者分别锚定起点、终点和批处理对齐,看起来是词表的边角料,实际牵着三条训练命脉:首 token 分布、停机监督的完整性、批内梯度纯度。设计 checklist 很短:EOS 监督信号是否被截断策略保护、特殊 token 是否被文本过滤、BOS 约定是否跨框架一致、预留条目是否隔离训练。这几条查完,特殊 token 就从隐患清单上划掉了。