Tokenizer & 样本构造:模型到底读到了什么?
本篇是「小模型训练系列」第 6 篇:从 token 切分、上下文窗口、label shift、loss mask 与对话样本格式出发,解释模型真正读到的不是文本,而是 token 序列与训练目标。
正在沿着「小模型训练系列」系统学习,希望把概念、工程和真实判断串起来的读者。
上一篇讲数据工程时,我们把训练数据比作一张地图。
地图决定模型要往哪里走。
但地图还不能直接塞进模型脑子里。文章、对话、代码、表格、换行、标点、角色标记,全部都要先经过一台看不见的“剪辑机”。
这台剪辑机会把人类世界里的文字,切成一格一格的 token,再排成一条胶片轨道。
模型真正看到的,不是“你好”“请解释”“这是一段答案”。
它看到的是:
[128000, 9906, 11, 220, 19182, 88923, ...]
像不像一部动画片的底片?
银幕上是人物、对白、情绪和剧情。底片上却是一格一格的编号。训练模型也是这样:读者看到文章,标注员看到问答,训练程序看到 token id、attention mask 和 labels。
所以这一篇要拆的是训练入口处最容易被低估、也最容易出事故的一层:
模型到底读到了什么?
📌 本篇聚焦的问题
- Tokenizer 为什么不是简单的“分词器”
- token 切分如何改变模型看到的信息颗粒度
- 上下文窗口为什么不是“能放多少字”的仓库
- 一条聊天记录如何变成
input_ids、attention_mask和labels - label shift 为什么是下一词预测的关键机关
- loss mask 为什么决定模型到底学谁、不学谁
- prompt 部分到底该不该参与 Loss
- 样本构造里最常见的训练事故长什么样
这一篇会稍微工程一点,但不会把读者丢进代码森林。
可以把它想象成一集训练片场的动画:Tokenizer 是剪辑师,样本构造是分镜师,loss mask 是导演手里的计分板。
模型坐在放映厅里,一帧一帧看过去,然后学会下一帧应该是什么。
🎞️ 1. 模型读的不是文字,而是 token id
人类看到这句话:
小模型训练不是背书,而是在目标函数里被塑形。
模型不会直接理解这一整句话。它会先经过 Tokenizer。
Tokenizer 会做两件事:
- 把文本切成 token
- 把每个 token 映射成词表里的数字编号
可以简化成这样:
flowchart LR
A[原始文本] --> B[Tokenizer 切分]
B --> C[token 序列]
C --> D[token id]
D --> E[Embedding 查表]
E --> F[模型内部向量]
也就是说,文字不是直接进入模型的。
真正进入模型第一层的,是 token id 查出来的 embedding 向量。
这就解释了一个重要事实:
Tokenizer 是文本世界和向量世界之间的入口。入口怎么切,模型就怎么读。
它不是一个无关紧要的预处理工具,而是在决定模型看到世界的最小颗粒。
🧩 2. Token 不是字,也不一定是词
很多人会把 token 理解成“词”。这个说法方便,但不准确。
token 可能是一个字:
我
也可能是一个词:
training
也可能是一个词的一部分:
train
ing
还可能是空格、换行、标点,甚至某种特殊标记:
\n
<|user|>
<|assistant|>
<eos>
在多语言场景里,这件事会更明显。
同样一句话,在不同 tokenizer 里,可能被切成完全不同的形状。中文可能按字、词片段或混合方式切;英文可能按词根、后缀、空格和标点切;代码里的缩进、括号、引号,也可能成为非常重要的 token。
这不是细枝末节。
因为模型不是在“阅读自然语言”,而是在处理 token 序列。
如果某个领域词总是被切得很碎,模型学习它的成本就更高。
如果某种格式在 tokenizer 里很稳定,模型就更容易形成模式。
如果训练和推理用的 tokenizer 不一致,模型甚至会像拿错了字幕版本一样,台词对不上画面。
🔍 3. Tokenizer 会改变信息的颗粒度
想象一个动画片场。
一句台词本来是:
低成本微调
剪辑师可能把它切成三格:
低成本 / 微 / 调
也可能切成四格:
低 / 成本 / 微调
也可能切成更多碎片。
表面上意思没变,但模型看到的训练路径变了。
每一个 token 都是一次预测位置。序列越碎,模型要走的步子越多;某个概念被拆得越零散,模型越需要从更多上下文里重新拼回它。
这就是为什么 tokenizer 会影响:
| 影响位置 | 具体表现 |
|---|---|
| 序列长度 | 同样内容切得越碎,占用 token 越多 |
| 训练成本 | token 越多,计算和显存压力越高 |
| 概念学习 | 专有词被拆太碎,模型更难稳定记住 |
| 格式学习 | 角色标记、换行、代码缩进会影响输出习惯 |
| 上下文容量 | token 变多后,能装进去的原文变少 |
所以,“这个模型支持 8k 上下文”并不等于“能放 8000 个汉字”。
它的意思是:最多能放 8000 个 token。
而 token 和字数之间没有一个永远固定的换算关系。
🪟 4. 上下文窗口不是仓库,而是一条胶片轨道
很多人听到上下文窗口,会把它想成一个仓库:
这个模型有 32k,上下文更大,能塞更多资料。
这个理解有一半对。
窗口确实限制了最多能放多少 token,但它不是一个安静堆东西的仓库,更像一条正在播放的胶片轨道。
模型沿着这条轨道,从左往右看。
[system] [user] [assistant] [user] [assistant] ... [当前要预测的位置]
当前位置前面的 token,都会成为预测下一个 token 的条件。
这带来几个现实问题:
- 不是塞进去就一定被充分利用
- 长上下文会增加计算成本
- 重要信息放在不同位置,效果可能不一样
- 截断策略会决定哪些信息被保留,哪些被切掉
- 训练时见过的长度分布,会影响推理时的稳定性
上下文窗口不是“越大越自动聪明”。
它更像舞台长度变长了:能站更多角色,但导演、灯光、走位也更难控制。
🎬 5. 一条聊天记录如何变成训练样本
现在进入片场。
假设有一条最普通的聊天记录:
用户:什么是 Loss?
助手:Loss 是模型预测和目标答案之间的差距,用来指导参数更新。
人类看起来很自然。
但训练程序不能直接吃这段文字。它要把这条对话变成一个固定格式的样本。
通常会经历四步:
flowchart TB
A[原始聊天记录] --> B[套入聊天模板]
B --> C[Tokenizer 编码]
C --> D[构造 input_ids / attention_mask]
D --> E[构造 labels / loss mask]
套入模板后,可能变成类似这样:
<bos>
<|user|>
什么是 Loss?
<|assistant|>
Loss 是模型预测和目标答案之间的差距,用来指导参数更新。
<eos>
然后 Tokenizer 把它变成一串 token id:
input_ids:
[BOS, USER, 21603, 3922, 11, ASSISTANT, 8210, 318, ... , EOS]
还会有 attention mask:
attention_mask:
[1, 1, 1, 1, 1, 1, 1, 1, ... , 1]
如果有 padding,还会出现:
attention_mask:
[1, 1, 1, 1, 1, 1, 0, 0, 0]
其中 1 表示真实内容,0 表示填充出来的空位,不应该被模型当成有效上下文。
但真正决定训练目标的,是 labels。
🧠 6. Labels 不是答案原文,而是“下一格该是什么”
语言模型的核心训练方式通常是下一 token 预测。
它不是一次性猜完整答案,而是在每个位置猜下一格。
比如:
今 天 天 气 很 好
训练时更像这样:
| 当前看到 | 要预测 |
|---|---|
| 今 | 天 |
| 今 天 | 天 |
| 今 天 天 | 气 |
| 今 天 天 气 | 很 |
| 今 天 天 气 很 | 好 |
这就是 causal language model 的基本节奏。
模型像一个逐帧播放的动画师:每看完前面的画面,就要猜下一帧是什么。
所以 labels 往往和 input_ids 很像,但在训练内部会做一个位移。
input_ids: [A, B, C, D, E]
labels: [A, B, C, D, E]
训练时实际比较:
模型在 A 后预测 B
模型在 B 后预测 C
模型在 C 后预测 D
模型在 D 后预测 E
这个“错一格就全片跑偏”的机关,就是 label shift。
如果 shift 逻辑错了,模型可能在错误的位置被惩罚。它看起来还在训练,Loss 也可能在下降,但学到的东西会变形。
这类事故特别隐蔽,因为训练日志不会大喊:“你的电影字幕提前了一秒。”
它只会安静地给出一个数字,然后让模型往奇怪的方向收敛。
🎯 7. Loss mask:导演说哪里算分,哪里不算分
现在来到最关键的一幕。
同一条聊天样本里,哪些 token 应该参与 Loss?
还是刚才这条:
<|user|>
什么是 Loss?
<|assistant|>
Loss 是模型预测和目标答案之间的差距,用来指导参数更新。
如果目标是训练一个助手,通常希望模型学会回答,而不是学会复读用户的问题。
那么一个常见做法是:
用户部分:不计算 Loss
助手部分:计算 Loss
在 labels 里,经常会用一个特殊值,比如 -100,表示这个位置不参与 Loss。
概念上可以写成:
tokens:
[USER, 什么, 是, Loss, ASSISTANT, Loss, 是, 模型, 预测, ...]
labels:
[-100, -100, -100, -100, -100, Loss, 是, 模型, 预测, ...]
这就是 loss mask。
它像导演在片场举起计分板:
这段只是铺垫,不算分。
从这里开始,是演员正式表演,算分。
模型会在算分的位置被奖励或惩罚,在不算分的位置只把它们当作上下文。
所以 loss mask 决定的不是小细节,而是训练责任如何分配。
🧭 8. Prompt 部分到底该不该参与 Loss?
这是一个非常容易争论的问题。
答案不是永远“该”或“不该”,而是看训练目标。
如果是在预训练阶段,目标是让模型学习通用文本分布,那么很多连续文本都会参与下一 token 预测。模型要学习语言、知识、风格、结构。
如果是在指令微调阶段,目标通常是:
给定 system / user / context,让模型学会生成 assistant 答案。
这时 prompt 更像题目,assistant answer 更像标准答案。
题目本身通常不应该被当成模型要生成的目标。
所以常见策略是:
| 场景 | Prompt 是否算 Loss | 原因 |
|---|---|---|
| 通用预训练 | 通常会算 | 学习连续文本分布 |
| 指令微调 SFT | 通常不算用户部分 | 让模型学习回答,而不是复读问题 |
| 多轮对话微调 | 常只算 assistant 部分 | 保持角色边界 |
| 格式续写任务 | 可能部分计算 | 取决于训练目标 |
| 特定模板学习 | 可能计算模板后的目标段 | 让模型稳定输出格式 |
一句话:
Prompt 是否参与 Loss,不是道德问题,而是目标函数设计问题。
如果目标是让模型学会“看到问题后回答”,就不要把问题本身也当成答案来惩罚。
如果目标是让模型续写某种完整文本分布,prompt 和 answer 的边界就没那么绝对。
训练不是让模型“理解你的意图”,而是把意图翻译成一个可计算的目标。
loss mask 就是这份翻译里最锋利的一支笔。
🚧 9. 样本构造事故剧场
这一层最有意思,也最危险。
因为很多训练事故不是模型结构错了,而是样本构造这场动画片剪坏了。
事故一:模型学会复读用户
如果用户问题也参与 Loss,模型可能会把用户内容也当成要生成的目标。
结果就是:
用户:帮我总结这段文字
模型:帮我总结这段文字……
它不是故意敷衍,而是在训练时被教过:用户说的话也是“正确输出”的一部分。
事故二:模型停不下来
如果训练样本没有稳定的结束标记,比如 EOS,模型可能不知道什么时候该收尾。
它会像一集没有片尾曲的动画,一直往下演。
表现为:
回答已经完整了,但模型继续补充、重复、绕圈。
事故三:角色边界混乱
如果 chat template 不稳定,有时写 User:,有时写 <|user|>,有时又没有角色标记,模型就很难学会谁在说话。
上线后可能出现:
用户:给我一个建议
模型:用户:当然可以。助手:我建议……
这不是模型调皮,是片场的分镜牌乱了。
事故四:Padding 被算进 Loss
批量训练时,不同样本长度不一样。为了拼成 batch,短样本会被补齐。
如果 padding 位置没有正确 mask 掉,模型会被迫学习“空白”。
这会把训练信号稀释掉,甚至制造奇怪的输出偏好。
事故五:截断切掉了答案
如果样本太长,超过最大长度,就要截断。
最糟糕的情况是:题目保留了,答案被切掉了;或者答案只剩半截,还被当成完整目标训练。
模型会学到一种很危险的模式:
复杂问题 → 半截答案
如果评估时只看 Loss,甚至不一定马上发现。
因为它确实在认真学习,只是学习对象已经坏了。
🧪 10. 一个训练样本的“论文级”拆解
把所有东西合在一起,一条样本可以拆成五层:
flowchart TB
A[人类语义层: 问题与答案] --> B[模板层: system / user / assistant]
B --> C[Token 层: token 切分]
C --> D[张量层: input_ids / attention_mask]
D --> E[目标层: labels / loss mask]
E --> F[优化层: Loss / 梯度 / 参数更新]
这五层对应五个判断:
| 层级 | 要问的问题 |
|---|---|
| 人类语义层 | 这条样本是否真的高质量 |
| 模板层 | 角色边界是否清楚 |
| Token 层 | 切分后是否过长、过碎、异常 |
| 张量层 | padding、attention、长度是否正确 |
| 目标层 | 哪些位置参与 Loss,是否符合训练目标 |
真正严肃的训练工程,不会只问“数据集有多少条”。
它会问:
每一条数据最后变成了什么训练目标?
因为模型不会对原始文本负责。
模型只对它实际看到的 token、实际比较的 labels、实际计算的 Loss 负责。
🧱 11. 样本构造的三个核心原则
第一,模板要稳定
同一种任务,最好使用一致的 chat template。
角色标记、换行位置、结束符、system prompt 的位置,都应该尽量稳定。
稳定模板不是为了好看,而是为了让模型把格式当成可靠信号。
第二,目标要干净
不要让模型在不该学习的位置计算 Loss。
指令微调时,通常重点是 assistant 输出。用户问题、系统提示、检索上下文,多数时候应该作为条件存在,而不是作为生成目标。
第三,截断要可控
长样本不是不能用,但要知道它怎么被截断。
尤其要避免:
- 只保留问题,切掉答案
- 切掉关键角色标记
- 切掉 EOS
- 把一个多轮对话从中间切开,却还当成完整样本
- 训练和推理的模板不一致
样本构造最怕“看起来差不多”。
模型看到的不是差不多,它看到的是严格的一串 token。
🧾 12. 训练前的样本构造检查表
如果要真的微调一个小模型,训练前至少做一次这张表。
| 检查项 | 为什么重要 |
|---|---|
| Tokenizer 是否和底座模型一致 | 不一致可能导致整套输入空间错位 |
| Chat template 是否固定 | 决定角色边界和对话格式 |
| System / user / assistant 是否清楚 | 防止模型混淆谁在说话 |
| 是否添加 EOS | 影响模型是否学会停止 |
| Prompt 是否 mask | 决定模型学回答还是学复读 |
| Padding 是否 mask | 防止空白 token 污染 Loss |
| Label shift 是否正确 | 防止预测目标错位 |
| 最大长度是否合理 | 防止有效答案被截断 |
| 截断策略是否检查过样例 | 防止半截样本进入训练 |
| 训练和推理模板是否一致 | 防止上线时“字幕版本”不同 |
这张表看起来普通,但它能拦住很多训练事故。
训练系统里最贵的错误,不一定是报错。
有时报错反而是好事,因为它及时拦住了你。
更可怕的是:程序不报错,Loss 还在下降,模型也产出了东西,但你训练出来的是一个被错误目标塑形的版本。
🌱 13. 小模型为什么更需要认真做样本构造
大模型容量大,见过的分布广,有时能靠底座能力把一些脏格式扛过去。
小模型不一样。
小模型容量更紧,容错空间更少。每一条高质量样本都很宝贵,每一种格式混乱也更容易留下痕迹。
在小模型训练里,样本构造像给一间小房子布置家具。
空间大时,沙发歪一点还能忍;空间小时,桌子放错位置,门都打不开。
所以小模型微调的关键不是“把数据塞进去”,而是:
让每一个 token 都尽量有意义。
让每一次 Loss 都尽量算在该算的位置。
让每一次梯度更新都尽量朝着目标能力移动。
这也是小模型训练最有趣的地方。
它不只是堆算力,而是在做一种非常精细的能力编排。
🧠 14. 这一篇的核心结论
- 模型不直接读文字,它读 token id 对应的向量序列
- Tokenizer 决定文本进入模型时的颗粒度
- 上下文窗口是 token 序列长度,不是字数仓库
- 聊天记录必须先变成模板化样本,才能进入训练
- Label shift 决定模型在每个位置预测哪一个 token
- Loss mask 决定哪些位置负责学习,哪些位置只做上下文
- Prompt 是否参与 Loss,取决于训练目标
- 样本构造错误会让模型稳定地学到错误行为
- 小模型更依赖干净、稳定、目标明确的样本格式
如果把上一篇的数据工程看成“选择什么剧情”,那么这一篇讲的就是“剧情怎样被剪成模型能学习的分镜”。
剧情再好,如果剪辑错位、字幕错位、计分位置错位,最后训练出来的也会是一部奇怪的片子。
所以训练入口处的真正问题不是:
数据有没有放进去?
而是:
模型实际看到了什么,又被要求在哪些地方学会什么?
📌 下一篇预告
《LoRA / QLoRA:为什么低成本微调可行?》
下一篇会进入很多人真正关心的部分:显卡不大、预算有限、模型又想变成自己的,怎么办?
会拆开这些问题:
- 全参微调为什么贵
- LoRA 为什么只训练一小部分参数也能改变模型行为
- rank、alpha、dropout 到底在调什么
- QLoRA 为什么能把显存压力继续压低
- 显存账本应该怎么算,哪些地方最容易爆
- 小模型微调时,怎样在成本、效果和稳定性之间做选择
核心观点是:低成本微调不是“少训练一点”,而是把可训练能力放在更聪明的位置上。
🗺️ 后续章节路线
| 顺序 | 章节方向 | 要解决的问题 |
|---|---|---|
| 第 7 篇 | LoRA / QLoRA:为什么低成本微调可行? | 全参训练、参数高效微调、rank、adapter 与显存账本 |
| 第 8 篇 | 评估体系:模型到底有没有真的变好? | training loss、validation loss、任务评估、回归集与人工验收 |
| 第 9 篇 | RAG vs 微调:知识应该放进检索,还是写进参数? | 知识更新频率、事实性、成本、召回边界与能力边界 |
| 第 10 篇 | 推理与部署:模型能力如何稳定跑起来? | 量化、KV Cache、吞吐、并发、延迟与部署成本 |
| 第 11 篇 | 工具调用与 Agent:模型如何从会说话变成会做事? | Function Calling、工具参数、权限边界、日志与人工确认 |
| 第 12 篇 | Memory 与工作流:模型如何承接长期任务? | 对话记忆、任务状态、上下文压缩、恢复现场与流程编排 |
| 第 13 篇 | 行业模型与业务闭环:训练如何进入真实场景? | 领域数据、流程反馈、人工审核、持续评估与灰度改进 |
| 第 14 篇 | 系列总结:从数据到 Agent 的小模型工程地图 | 把训练、微调、评估、RAG、部署和 Agent 串成完整路线 |
到这里,小模型训练系列已经走过了一个完整入口:
从数据如何决定方向,到 token 如何切分世界,再到样本如何定义训练目标。
后面会继续往工程实践里走:怎么低成本改模型、怎么评估是否真的变好、怎么部署到真实场景里稳定运行。
持续学习,持续记录,持续筛选
如果这篇对你有用,可以继续沿着主题读下去。
福星家和会长期记录 AI 技术、生活成长、真实好物与正向文化观察。产品体验、人物故事、教育内容、生活方式、文化作品和内容共创都欢迎邮件沟通,合作内容会清楚标注。