正文
原文知乎链接:https://www.zhihu.com/question/2017049686331127666
GPT解析:
第一个问题: 解析这篇文章提出的问题、解决思路、排查思路、问题原因等。 通俗易懂的语言解释。
这篇知乎讨论的是一个很有意思的大模型问题:MiniMax M2.5 不是完全不知道“马嘉祺”是谁,而是模型知道相关信息,却说不出“马嘉祺”这个名字本身。知乎问题标题就是“为什么 MiniMax 大模型无法识别马嘉祺是谁?”,其中一篇回答总结说,模型会回避输出“嘉祺”这两个字,像是这个名字“不能说”。
一句话解释
模型的“理解能力”没坏,坏的是“把某个词说出口”的能力。
可以类比成:
一个人知道某位明星是谁,也知道他的经历、组合、身份,但一到说名字时就卡壳,嘴边那个词怎么也蹦不出来。
技术上说,就是:输入侧还能理解“嘉祺”,但输出侧 lm_head 里“嘉祺”这个 token 的生成概率出了问题。
1. 文章提出的问题是什么?
问题现象是:用户问 MiniMax M2.5 “时代少年团队长是谁”,模型能回答出很多相关信息,但就是无法稳定输出“马嘉祺”三个字,甚至会编出“马嘉轩”“马丝祺”等错误名字。转载的技术文章也提到,模型并不是完全不知道马嘉祺,而是“知道但就是说不出”。
这和普通幻觉不太一样。
普通幻觉是:
模型不知道,于是乱编。
这个问题更像:
模型知道,但最后生成文字时,那个词选不出来。
所以这篇文章真正讨论的不是娱乐圈知识问题,而是一个大模型训练问题:后训练阶段可能让某些低频 token 的生成能力退化。
2. 先理解几个关键概念
Tokenizer:分词器
大模型不是直接按“字”理解文本,而是先把文本切成 token。
比如“马嘉祺”不是一定被切成:
马 / 嘉 / 祺
MiniMax 的分词结果里,“马嘉祺”被切成:
马 / 嘉祺
也就是说,“嘉祺”被当成了一个独立 token。MiniMax 排查中确认,“马嘉祺”被切成 ['马','嘉祺'],而“嘉祺”是一个独立 token。
Embedding:输入侧理解能力
Embedding 可以理解为:
模型看到一个 token 后,把它转换成一个向量,也就是机器能理解的“语义坐标”。
如果 embedding 没问题,说明模型“看见”这个词时还能理解它。
lm_head:输出侧说话能力
lm_head 可以理解为:
模型最后决定“下一个词该说什么”的出口。
它会给词表里的每个 token 打分,哪个分高,就更可能输出哪个。
所以:
Embedding 负责“看懂”
lm_head 负责“说出”
这次问题就是:Embedding 基本正常,但 lm_head 出问题了。
3. 排查思路是怎样的?
第一步:确认是不是 tokenizer 切错了
很多人第一反应会怀疑分词器:
是不是“马嘉祺”被错误切分了?
是不是编码和解码不一致?
是不是 token ID 对不上?
MiniMax 检查后发现,分词器没有明显问题。“嘉祺”确实是独立 token,encode/decode 也正常。技术文章中提到,“马嘉祺”能被切成“马”和“嘉祺”,而且 decode 回来也是正常的“马嘉祺”。
所以第一条路被排除了:
不是 tokenizer 把字切坏了。
第二步:确认是不是预训练阶段没学会
接下来要判断:
模型是不是在预训练时就没见过“嘉祺”?
如果预训练阶段没见过,那模型不认识也正常。
但排查发现不是这样。MiniMax 检查“嘉祺”的 embedding 分布和语义近邻,发现它附近是“亚轩”“千玺”“祺”“耀文”“王一博”“肖战”等明星或人名相关 token,这说明预训练阶段模型其实已经把“嘉祺”放在了合理的人名语义空间里。
所以第二条路也被排除了:
不是预训练没学会。
第三步:对比 Base 模型和 SFT 后模型
这是关键一步。
MiniMax 用早期 Base 模型做测试,发现 Base 模型能正常输出“马嘉祺”;但是经过 SFT 后训练之后,模型反而回避这个词。知乎回答也提到,预训练后的 base 模型可以输出“嘉祺”,问题定位到了后训练阶段。
这说明问题发生在:
预训练之后,SFT 后训练过程中。
也就是:
模型原本会说,后来被训练“训歪了”。
第四步:检查 SFT 数据里“嘉祺”出现了多少次
MiniMax 统计后发现,SFT 后训练数据中包含“嘉祺”的样本不足 5 条。
这就很关键了。
预训练阶段,模型看过海量互联网文本,可能见过很多“马嘉祺”。
但 SFT 阶段用的是更干净、更偏对话、更偏指令的数据。在这些数据里,“嘉祺”几乎没有作为目标答案出现。
于是问题来了:
模型在后训练时,几乎没有练习过“输出嘉祺”。
不是没见过,而是后训练阶段没“复习”。
第五步:比较输入侧 embedding 和输出侧 lm_head
MiniMax 进一步比较了两个地方:
输入侧:vocab embedding
输出侧:lm_head
结果是:
vocab embedding 几乎没变
lm_head 变化非常明显
也就是说,模型看到“嘉祺”时仍然能理解,但要生成“嘉祺”时,输出层对应的向量位置已经漂移了。钛媒体转载中解释,MiniMax 计算了 SFT 前后每个 token 在 lm_head 中的向量变化,发现“嘉祺”的变化幅度在整个词表中排名靠前。
这就是“知道但说不出”的核心证据。
4. 真正的问题原因是什么?
核心原因可以概括为:
“嘉祺”是一个低频 token。
它在预训练阶段学会了。
但在 SFT 后训练阶段几乎没出现。
于是它在输出层 lm_head 中发生了语义漂移。
最终模型无法稳定生成这个 token。
更通俗地说:
“嘉祺”原来在模型脑子里的位置是“中文人名/明星名”。
但是 SFT 时,它几乎没被训练到。
周围大量高频 token,比如工具调用标记、代码符号、特殊符号不断被训练。
这些 token 把输出空间搅动了。
“嘉祺”这个没人管的低频 token 被挤歪了。
最后模型想说它时,找不到正确出口,只能说成别的相似词。
文章中也提到,SFT 后“嘉祺”的 lm_head 邻居从中文人名,变成了人名、工具标记、乱码、特殊 token 混杂在一起。
5. 为什么模型还能理解,却说不出来?
因为“理解”和“生成”不是完全同一个部位负责。
可以这样理解:
输入侧 embedding:负责识别这个词是谁
输出侧 lm_head:负责把这个词说出来
这次是:
输入侧没坏,所以模型知道马嘉祺是谁。
输出侧坏了,所以模型说不出“嘉祺”。
这也是这篇文章最重要的启发之一:
大模型不是一个完整均匀的“知识库”。
它的“知道”和“能准确说出来”之间,可能存在裂缝。
新浪转载文章也总结说,MiniMax M2.5 仍然掌握相关知识,但问题更像是生成阶段某个 token 无法被稳定采样出来。
6. 这是不是个孤立问题?
不是。
MiniMax 后续扫描了整个约 20 万 token 的词表,发现约 4.9% 的 token 出现显著退化。退化 token 包括特殊标记、LaTeX/维基源码标记、中文 SEO 垃圾词、日文口语和博客模板表达等。
其中日语 token 退化尤其明显。文章提到,29.7% 的日语 token 发生显著退化,远高于韩语、俄语、中文和英文。
这说明“马嘉祺”只是一个被用户发现的典型案例,背后其实是更系统的:
长尾 token / 小语种 token / 低频 token 在后训练阶段退化
7. 解决思路是什么?
解决思路很朴素:
让每个 token 在 SFT 阶段都至少被练到。
MiniMax 的做法是构造一份“全词表覆盖合成数据”。
具体做法大概是:
1. 把词表里的 200064 个 token 分批;
2. 每批大约 8000 个 token;
3. 构造一个简单任务:请重复以上内容;
4. answer 原样复制这些 token;
5. 总共构造约 500 条合成数据;
6. 保证每个 token 至少作为生成目标出现 20 次。
新浪转载文章也提到,这批合成数据约占总 SFT 数据量 1%,目标是确保每个 token 至少作为生成目标出现 20 次。
通俗讲就是:
给模型发一本“词表默写本”,让它把每个词都抄几遍。
不是为了教新知识,而是防止它把某些低频词的“说法”忘掉。
8. 修复效果怎么样?
根据转载的 MiniMax 技术内容,加入全词表覆盖合成数据后:
日语回答中混入俄文字符的比例从 47% 降到 1%;
此前无法输出的部分词汇被修复;
全词表 20 万 token 的 lm_head 稳定度明显提升。
AITNT 转载文章中给出的数据是:日语回答混入俄文字符比例从 47% 降至 1%,全词表 20 万 token 的 lm_head cosine similarity 从最低 0.329 提升至全部高于 0.97。
这说明问题不是靠硬编码“马嘉祺”三个字修好的,而是通过提升整个词表的后训练覆盖度,系统性缓解了低频 token 退化。
9. 文章里的进一步思考
知乎回答里还提出了一些延伸猜测,比如“嘉祺”这个 token 可能漂移到了特殊 token 附近,或者被内容安全相关 token 空间影响,所以模型表现得像在刻意回避。但回答作者也明确说这些是猜测,无法通过 API 拿到 lm_head 向量来确认。
所以这里要区分:
已确认结论:
SFT 数据中“嘉祺”覆盖极低,lm_head 发生明显漂移。
推测部分:
它是否漂移到了特殊 token、内容安全 token 或其他禁区附近。
不能把作者的猜测当作官方结论。
10. 对我们做模型训练有什么启发?
这篇文章的价值不只是解释一个热梗,而是提醒我们:后训练数据质量不能只看任务覆盖,还要看 token 覆盖。
以前我们做 SFT,经常关注:
有没有问答数据
有没有业务场景
有没有安全数据
有没有工具调用数据
有没有多轮对话
但这篇文章提醒我们还要关注:
低频 token 有没有作为 target 出现
专有名词有没有被模型练习输出
小语种 token 有没有覆盖
特殊符号、代码符号、工具标记有没有挤压普通 token
lm_head 是否发生异常漂移
尤其是企业场景中,比如你做金融、证券、知识库、交换机型号、路由器命令、内部产品名,可能也会遇到类似问题:
模型知道这个型号是什么意思,
但生成时总把型号写错、漏字、换成相似型号。
这不一定是 RAG 没召回,也不一定是模型没知识,可能是:
领域专有 token 在后训练中没有被充分作为输出目标训练。
最后总结
这篇文章讲的是:
MiniMax M2.5 无法稳定输出“马嘉祺”,不是因为模型完全不知道这个人,
而是因为“嘉祺”这个低频 token 在 SFT 后训练阶段几乎没有被作为生成目标训练,
导致输出层 lm_head 中该 token 的向量发生漂移。
输入侧 embedding 没坏,所以模型还能理解;
输出侧 lm_head 漂了,所以模型说不出来。
解决方法是构造覆盖全词表的合成重复数据,
让每个 token 在后训练阶段至少被练到,
从而防止低频 token 的生成能力退化。
用最通俗的话说就是:
模型不是“不认识马嘉祺”,而是“提笔忘字”。
解决办法也很像人类学习:
忘了就补抄,低频词也要定期复习。
- 这里的lm_head 和 vocab Embedding,分别指的是模型架构的哪个部分?
Thought for 10s
在 Decoder-only 大模型里,可以把一次生成理解成这条链路:
token_id
↓
vocab embedding / token embedding
↓
Transformer 多层注意力 + MLP
↓
最后一层 hidden state
↓
lm_head
↓
每个 token 的概率分布
↓
采样 / 贪心选择下一个 token
第二个问题: 这里的lm_head 和 vocab Embedding,分别指的是模型架构的哪个部分?
1. vocab Embedding 是什么?
vocab Embedding,也叫 token embedding、input embedding。
它位于模型最前面,负责把离散的 token id 转成连续向量。
例如:
"马嘉祺" → tokenizer → ["马", "嘉祺"]
假设:
"马" 的 token id = 123
"嘉祺" 的 token id = 45678
模型不能直接理解 45678 这个整数,所以需要查 embedding 表:
embedding_table[45678] = 一个 hidden_size 维向量
如果 hidden_size 是 4096,那么这个向量就是:
嘉祺 → [0.12, -0.03, 0.88, ..., 0.21]
这个向量会作为 Transformer 的输入。
所以 vocab Embedding 负责的是:
模型看到某个 token 时,怎么理解它
也就是:
输入侧理解能力
2. lm_head 是什么?
lm_head 是模型最后的输出层。
它位于 Transformer 主体之后,负责把 hidden state 转成词表上每个 token 的分数。
假设模型经过多层 Transformer 计算后,最后得到当前位置的 hidden state:
h = [0.31, -0.42, 0.15, ..., 0.09]
这个 hidden state 还不是文字,它只是一个语义向量。
lm_head 会把它映射成整个词表的 logits:
logits = lm_head(h)
如果词表大小是 200000,那么输出就是:
[
token_0 的分数,
token_1 的分数,
token_2 的分数,
...
token_199999 的分数
]
然后 softmax 变成概率:
"马" 0.01
"嘉祺" 0.000001
"亚轩" 0.08
"千玺" 0.05
...
如果“嘉祺”这个 token 在 lm_head 里的分数很低,模型就很难生成它。
所以 lm_head 负责的是:
模型想输出某个 token 时,能不能把它说出来
也就是:
输出侧生成能力
3. 两者在模型架构中的位置
可以这样看:
输入文本
↓
Tokenizer
↓
token ids
↓
【vocab Embedding】
↓
Transformer Block 1
↓
Transformer Block 2
↓
...
↓
Transformer Block N
↓
hidden state
↓
【lm_head】
↓
logits
↓
softmax
↓
下一个 token
对应到 PyTorch 里,大概是这种结构:
class DecoderOnlyLLM(nn.Module):
def __init__(self, vocab_size, hidden_size):
super().__init__()
# 输入侧:把 token id 转成向量
self.embed_tokens = nn.Embedding(vocab_size, hidden_size)
# 中间主体:Transformer 层
self.layers = nn.ModuleList([
TransformerBlock(hidden_size)
for _ in range(num_layers)
])
# 输出侧:把 hidden state 转成词表概率
self.lm_head = nn.Linear(hidden_size, vocab_size, bias=False)
def forward(self, input_ids):
# input_ids: [batch_size, seq_len]
hidden_states = self.embed_tokens(input_ids)
# hidden_states: [batch_size, seq_len, hidden_size]
for layer in self.layers:
hidden_states = layer(hidden_states)
logits = self.lm_head(hidden_states)
# logits: [batch_size, seq_len, vocab_size]
return logits
4. 用“马嘉祺”这个例子解释
假设用户问:
时代少年团队长是谁?
模型内部可能已经通过 Transformer 推理出答案应该是:
马嘉祺
但是最后生成时,需要一个 token 一个 token 输出。
可能先输出:
马
接下来要输出:
嘉祺
这时模型会用 lm_head 给所有 token 打分。
如果 lm_head 中“嘉祺”这个 token 的输出向量漂移了,导致它分数很低,那么模型就可能输出别的相似 token,比如:
亚轩
嘉轩
丝祺
祺
这就出现了:
模型知道答案相关信息,但说不出准确名字
5. 为什么说 embedding 没坏,lm_head 坏了?
因为两者作用不同。
| 部分 | 位置 | 作用 | 影响 |
|---|---|---|---|
| vocab Embedding | 模型最前面 | 把 token id 转成向量 | 影响模型能不能“看懂”这个词 |
| Transformer Blocks | 模型中间 | 做语义理解、推理、上下文建模 | 影响模型怎么思考 |
| lm_head | 模型最后面 | 把 hidden state 转成 token 概率 | 影响模型能不能“说出”这个词 |
如果 vocab Embedding 正常,说明模型看到“嘉祺”时还能理解它。
如果 lm_head 异常,说明模型想输出“嘉祺”时,这个 token 的概率打不上去。
所以会出现:
输入时能理解,
输出时说不出。
6. embedding 和 lm_head 的参数形状
假设:
vocab_size = 200000
hidden_size = 4096
那么:
vocab Embedding
形状通常是:
[200000, 4096]
意思是:
每个 token 有一个 4096 维向量
例如:
embedding["嘉祺"] = 第 45678 行向量
lm_head
形状通常是:
[4096, 200000]
或者在 PyTorch 线性层里存成:
[200000, 4096]
它的作用是:
hidden state → 每个 token 的分数
例如:
logit["嘉祺"] = hidden_state 和 lm_head["嘉祺"] 做相似度/线性投影
如果 lm_head["嘉祺"] 这个方向漂了,模型就很难输出“嘉祺”。
7. 有些模型会共享 embedding 和 lm_head
需要注意一点:有些模型会做 weight tying,也就是:
lm_head 权重 = vocab embedding 权重
这样可以减少参数量,也能让输入和输出空间保持一致。
但不是所有模型都这么做。
如果一个模型的 embedding 和 lm_head 是分开的,那么就可能出现:
vocab embedding 没怎么变
lm_head 变化很大
这也就能解释为什么:
模型还能理解某个 token,
但输出这个 token 的能力退化。
8. 最通俗的类比
可以把模型想象成人。
vocab Embedding 是“认字能力”
比如你看到:
嘉祺
你知道这是一个人名,也知道它和“马嘉祺”“时代少年团”有关。
这说明你的“认字系统”没问题。
lm_head 是“写字/说话能力”
当别人问你:
时代少年团队长是谁?
你脑子里知道答案,但一开口却说成:
马嘉轩
马嘉其
马亚轩
这说明不是你完全不认识,而是“说出口”这个环节出问题。
总结
vocab Embedding:
模型最前面的输入词向量表。
负责把 token id 转成语义向量。
影响模型能不能看懂某个 token。
lm_head:
模型最后面的输出投影层。
负责把 hidden state 转成每个 token 的生成概率。
影响模型能不能说出某个 token。
所以这篇文章里的核心意思是:
“嘉祺”这个 token 的输入 embedding 还正常,
所以模型看到它时大概能理解。
但是 “嘉祺” 在 lm_head 里的输出方向漂移了,
所以模型生成时很难把这个 token 选出来。
一句话概括:
vocab Embedding 负责“看懂词”,lm_head 负责“说出词”。
- 为什么 嘉祺 单独作为一个token?这是由什么决定的?什么情况下 嘉和祺分开?如果token是嘉 和 祺,是不是就不会有这个问题了?
Thought for 9s
“嘉祺”单独作为一个 token,不是模型临时决定的,而是由这个模型使用的 Tokenizer 词表和分词算法 决定的。
可以先记住一句话:
某个词会不会成为单独 token,主要取决于它在训练 tokenizer 时的语料中是否足够常见,以及 tokenizer 算法最终有没有把它合并进词表。
第三个问题: 为什么 嘉祺 单独作为一个token?这是由什么决定的?什么情况下 嘉和祺分开?如果token是嘉 和 祺,是不是就不会有这个问题了?
1. 为什么“嘉祺”会单独作为一个 token?
大模型的 tokenizer 会先在大量文本上训练一个“词表”。
词表里可能有:
嘉
祺
嘉祺
马
马嘉祺
时代少年团
也可能只有:
嘉
祺
马
如果 tokenizer 训练时发现“嘉祺”这个组合在语料中经常一起出现,比如大量娱乐新闻、粉丝内容、人物介绍里都有“马嘉祺”,那么算法就可能认为:
嘉 + 祺 经常一起出现,不如合并成一个 token:嘉祺
于是词表中就会出现一个独立 token:
嘉祺
所以“马嘉祺”可能被切成:
马 / 嘉祺
而不是:
马 / 嘉 / 祺
2. 这是由什么决定的?
主要由三件事决定。
第一:训练 tokenizer 的语料
如果语料中“嘉祺”出现很多次,成为单独 token 的概率就更高。
比如这些文本很多:
马嘉祺是时代少年团队长
马嘉祺参加节目
马嘉祺演唱会
那么 tokenizer 在统计时会发现:
嘉 和 祺 经常连续出现
于是就可能合并。
如果训练语料中很少出现“嘉祺”,那它大概率不会成为单独 token。
第二:分词算法
常见 tokenizer 算法有:
BPE
WordPiece
SentencePiece / Unigram
虽然细节不同,但大方向类似:
高频组合更容易成为一个 token,低频组合更可能被拆开。
BPE 的直观逻辑就是:
先从字符开始:
嘉 / 祺
如果 嘉 + 祺 经常一起出现:
合并成 嘉祺
经过很多轮合并之后,词表里就会有大量常见片段。
第三:词表大小
词表不是无限大的。
假设词表大小是 20 万,算法会把它认为“最有价值”的 20 万个 token 留下来。
“有价值”通常意味着:
出现频率较高
能减少文本长度
对压缩语料有帮助
如果“嘉祺”在语料中比较常见,它就可能被保留。
如果词表比较小,很多词就会被拆得更细。
3. 什么情况下“嘉”和“祺”会分开?
如果词表里没有“嘉祺”这个 token,就只能拆开。
比如词表是:
嘉
祺
马
没有:
嘉祺
那么“马嘉祺”就可能被切成:
马 / 嘉 / 祺
还有一种情况是,虽然词表里有“嘉祺”,但具体分词算法没有选择它。不过多数 BPE 类 tokenizer 会倾向于使用已经合并好的较长 token,所以只要词表里有“嘉祺”,通常就会用它。
不同模型的 tokenizer 不一样,所以同一个词在不同模型中可能切法不同:
模型 A:马 / 嘉祺
模型 B:马 / 嘉 / 祺
模型 C:马嘉祺
这都正常。
4. 如果 token 是“嘉”和“祺”,是不是就不会有这个问题?
大概率会缓解,但不能说一定完全不会。
这次 MiniMax 的问题核心是:
“嘉祺”这个完整 token 在 lm_head 中退化了,
导致模型很难直接输出这个 token。
如果 tokenizer 把它拆成:
嘉 / 祺
那么模型生成“马嘉祺”时就不是一次性生成“嘉祺”,而是分两步:
第 1 步:生成 马
第 2 步:生成 嘉
第 3 步:生成 祺
这时“嘉”和“祺”都是单字 token,一般出现频率比“嘉祺”高,训练中被覆盖得更多,lm_head 不容易严重漂移。
所以从这个角度看,拆成单字确实可能减少这种问题。
5. 但是拆成“嘉”和“祺”也不是万能的
因为模型还要学会:
马 后面应该接 嘉
嘉 后面应该接 祺
如果模型本身对“马嘉祺”这个人名记忆不稳定,还是可能输出成:
马嘉轩
马嘉琪
马嘉其
马亚轩
也就是说:
单 token 问题:嘉祺这个整体 token 说不出来
多 token 问题:每一步都可能选错
拆成“嘉 / 祺”可以避免“嘉祺这个 token 单点故障”,但不能保证模型一定知道完整名字。
6. 为什么不都拆成单字,避免这种问题?
因为 token 太细会带来其他问题。
比如:
马嘉祺是时代少年团队长
如果按较大 token 切,可能是:
马 / 嘉祺 / 是 / 时代少年团 / 队长
如果按单字切,可能是:
马 / 嘉 / 祺 / 是 / 时 / 代 / 少 / 年 / 团 / 队 / 长
单字切分会导致序列变长。
序列变长会带来几个问题:
推理更慢
显存占用更高
上下文长度浪费更多
模型要学更多步的依赖关系
所以 tokenizer 设计本质上是在平衡:
token 太粗:低频 token 可能退化
token 太细:序列太长,效率下降
7. 用一个例子理解
假设词表里有“嘉祺”:
输入:马嘉祺
切分:马 / 嘉祺
模型输出时:
先输出 马
再输出 嘉祺
如果“嘉祺”这个 token 的 lm_head 坏了,模型就卡住。
如果词表里没有“嘉祺”:
输入:马嘉祺
切分:马 / 嘉 / 祺
模型输出时:
先输出 马
再输出 嘉
再输出 祺
这时不会依赖“嘉祺”这个完整 token,但模型需要连续两步都生成对。
8. 所以这次问题的关键不是“嘉祺不该作为 token”
更准确地说,问题不是 tokenizer 把“嘉祺”合成一个 token 就一定错了。
真正的问题是:
既然 tokenizer 里有“嘉祺”这个 token,
后训练阶段就必须保证它有足够的输出训练覆盖。
否则就会出现:
输入时能识别这个 token,
输出时不会说这个 token。
也就是:
embedding 还正常,
lm_head 漂了。
总结
“嘉祺”单独作为 token,是 tokenizer 训练时根据语料频率、分词算法、词表大小决定的。
如果“嘉祺”在 tokenizer 训练语料中经常出现,就可能被合并成一个 token。
如果词表里没有“嘉祺”,或者算法没有选择它,就会拆成“嘉 / 祺”。
如果拆成“嘉 / 祺”,这次“嘉祺 token 输出退化”的问题大概率会缓解,因为单字 token 更常见、更稳定。
但拆成单字也不是绝对无问题,因为模型仍然要学会连续输出“嘉”和“祺”,否则还是可能生成错别字或相似名字。
一句话概括:
“嘉祺”成为一个 token,是 tokenizer 为了压缩高频文本做出的选择;问题不在于它成为 token,而在于后训练时这个低频输出 token 没有被充分复习,导致模型想说却说不出来。
第四个问题:模型将 输入内容,转为向量供模型内部计算。是通过input Embedding层。 也就是说,会有一个词表专门提供了 “嘉琪”-“token_id”-“嘉琪的向量化” 这样一个表吗? 这个表会持续更新训练吗?token_id的作用是什么?对嘉琪向量化还是 tokenid
对,你理解的方向基本正确。但要稍微纠正一个点:
不是模型里有一张 "嘉祺" → token_id → 向量化结果 的完整三列表,而是分成两部分。
可以理解为两张东西:
第一部分:Tokenizer 词表
负责:文本 token → token_id
第二部分:Embedding 权重矩阵
负责:token_id → 向量
1. 模型里真的有“嘉祺”这条记录吗?
如果 tokenizer 词表里有“嘉祺”这个 token,那么确实会有类似这样的映射:
"嘉祺" → token_id = 45678
这属于 tokenizer 词表。
然后模型的 embedding 层里会有一个大矩阵:
embedding_matrix[token_id] → 向量
比如:
embedding_matrix[45678] → [0.12, -0.03, 0.88, ..., 0.21]
所以完整流程是:
"嘉祺"
↓ tokenizer
token_id = 45678
↓ input embedding 查表
嘉祺对应的向量
2. 这个“表”长什么样?
假设模型词表大小是 200000,hidden_size 是 4096,那么 input embedding 矩阵就是:
[200000, 4096]
意思是:
一共有 200000 个 token
每个 token 对应一个 4096 维向量
可以类比成:
| token | token_id | embedding 向量 |
|---|---|---|
| 马 | 123 | 第 123 行向量 |
| 嘉祺 | 45678 | 第 45678 行向量 |
| 时代少年团 | 89123 | 第 89123 行向量 |
但在真实模型里,embedding 层通常不会保存 "嘉祺" 这个字符串,只保存数字矩阵。
字符串到 token_id 的映射由 tokenizer 管。
模型主体只认识:
45678
然后用这个 id 去 embedding 矩阵中取第 45678 行。
3. token_id 的作用是什么?
token_id 的本质是:索引编号。
它不是语义本身,也不是向量本身,只是一个“查表用的编号”。
比如:
"嘉祺" → token_id = 45678
意思不是 45678 有什么特殊语义,而是:
去 embedding 矩阵的第 45678 行,把那一行向量取出来。
所以 token_id 的作用是:
把文本世界连接到向量世界。
流程如下:
文字 token:嘉祺
↓
token_id:45678
↓
embedding_matrix[45678]
↓
向量:[0.12, -0.03, 0.88, ...]
4. 是对“嘉祺”向量化,还是对 token_id 向量化?
严格来说,是:
对 token_id 做 embedding 查表。
不是直接对“嘉祺”这个字符串做数学计算。
也就是说,模型不会直接拿 "嘉祺" 这两个字去算向量,而是先变成 token_id:
"嘉祺" → 45678
然后查表:
embedding_matrix[45678]
所以更准确的说法是:
字符串先被 tokenizer 编码成 token_id,
token_id 再通过 embedding 层映射成向量。
5. 这个 embedding 表会持续更新训练吗?
分情况。
预训练阶段:会更新
在大模型预训练时,embedding 矩阵是模型参数的一部分,会随着训练不断更新。
比如一开始:
embedding_matrix[45678] = 随机初始化向量
训练中模型不断看到类似文本:
马嘉祺是时代少年团队长
嘉祺参加节目
粉丝喜欢马嘉祺
于是 "嘉祺" 对应的向量会慢慢被训练到一个合理位置,和人名、明星、时代少年团相关 token 更接近。
SFT 阶段:通常也会更新
SFT,也就是监督微调阶段,如果没有冻结 embedding,那么 embedding 矩阵也会继续被训练。
但是很多时候,embedding 的变化没有 lm_head 那么明显。
这篇文章里提到的问题就是:
vocab embedding 变化不大
lm_head 变化很大
所以模型还能“看懂”嘉祺,但不容易“说出”嘉祺。
LoRA 微调阶段:通常不更新
如果你用 LoRA 微调模型,默认情况下通常只训练一小部分 adapter 参数,不训练原始 embedding。
也就是说:
tokenizer 不变
token_id 不变
embedding 矩阵一般也不变
除非你特意设置让 embedding 参与训练。
6. token_id 会变吗?
一般不会。
对于一个已经训练好的模型:
"嘉祺" → 45678
这个映射通常是固定的。
原因很简单:如果 token_id 变了,整个 embedding 矩阵就乱了。
比如训练时:
45678 = 嘉祺
结果你后面改成:
45678 = 苹果
那模型拿第 45678 行向量时,本来是“嘉祺”的语义,现在却被解释成“苹果”,整个模型会混乱。
所以:
tokenizer 词表和 token_id 映射,通常在模型训练前确定好。
训练过程中一般不变。
7. embedding 向量会变吗?
会。
区别是:
token_id 通常固定
embedding 向量可以训练更新
举个例子:
"嘉祺" → token_id = 45678
这个编号不变。
但是第 45678 行的向量会在训练中变化:
训练前:
embedding_matrix[45678] = 随机向量
预训练后:
embedding_matrix[45678] = 人名/明星相关语义向量
SFT 后:
embedding_matrix[45678] = 可能略微变化
所以:
token_id 是固定编号
embedding 是可训练参数
8. 那 lm_head 又是另一张表吗?
可以这么理解。
模型有输入侧 embedding:
token_id → 输入向量
也有输出侧 lm_head:
hidden state → 每个 token 的输出分数
如果词表大小是 200000,hidden_size 是 4096:
input embedding: [200000, 4096]
lm_head: [4096, 200000]
或者 PyTorch 里 lm_head 权重经常存成:
[200000, 4096]
每个 token 在 lm_head 里也对应一个输出方向。
例如:
lm_head["嘉祺"] 对应第 45678 个输出方向
生成时模型会计算:
当前 hidden state 和 "嘉祺" 输出方向的匹配程度
匹配程度高,就容易输出“嘉祺”。
匹配程度低,就不容易输出。
9. 最完整的流程
假设输入是:
马嘉祺是谁?
模型处理流程是:
1. Tokenizer 分词
"马嘉祺是谁?" → ["马", "嘉祺", "是谁", "?"]
2. 转 token_id
["马", "嘉祺", "是谁", "?"] → [123, 45678, 8123, 32]
3. Input embedding 查表
123 → 向量
45678 → 向量
8123 → 向量
32 → 向量
4. Transformer 内部计算
多层 attention + MLP
5. lm_head 输出
hidden state → 所有 token 的 logits
6. 采样下一个 token
选择概率最高或采样到的 token_id
7. tokenizer decode
token_id → 文字
10. 回到“嘉祺”这个问题
它的问题大概是:
输入侧:
"嘉祺" → token_id → embedding 向量
这个过程基本正常,所以模型能理解。
输出侧:
hidden state → lm_head → "嘉祺" 的生成分数
这个过程出了问题,所以模型说不出来。
也就是说:
tokenizer 认识“嘉祺”
embedding 也还能表达“嘉祺”
但是 lm_head 里“嘉祺”的输出方向漂移了
总结
1. tokenizer 词表负责:
"嘉祺" → token_id
2. input embedding 层负责:
token_id → 向量
3. token_id 本身不是向量,只是查 embedding 表的编号。
4. 训练过程中:
token_id 通常不变;
embedding 向量会更新;
lm_head 权重也会更新。
5. 模型不是直接对“嘉祺”字符串向量化,
而是先把“嘉祺”转成 token_id,
再用 token_id 查 embedding 矩阵得到向量。
一句话概括:
token_id 是编号,embedding 是编号对应的向量;模型训练时通常不改编号,但会改编号对应的向量。
- 那最初每个token的向量是怎么来的? 这个向量化的结果,最终会在整个模型架构中进行怎样的计算?
Thought for 12s
最初每个 token 的向量,一开始通常是随机初始化出来的。
但重点是:
随机初始化只是起点,真正有语义的向量,是在预训练过程中慢慢学出来的。
第五个问题:那最初每个token的向量是怎么来的? 这个向量化的结果,最终会在整个模型架构中进行怎样的计算?
1. 最初每个 token 的向量怎么来的?
假设模型词表大小是:
vocab_size = 200000
隐藏层维度是:
hidden_size = 4096
那么模型会有一个 embedding 矩阵:
E.shape = [200000, 4096]
意思是:
200000 个 token
每个 token 对应一个 4096 维向量
例如:
token_id = 45678
那么:
E[45678]
就是这个 token 的输入向量。
最开始训练模型时,这个矩阵通常是随机初始化的,比如:
E[45678] = [0.0021, -0.0183, 0.0045, ..., 0.0098]
这个时候它还没有语义。
也就是说,最初的 "嘉祺" 向量并不天然表示“人名”“明星”“时代少年团”,它只是一个随机数向量。
2. 那它怎么变得有语义?
靠预训练。
大模型预训练的核心任务通常是:
根据前面的 token,预测下一个 token。
比如训练语料里有:
马嘉祺是时代少年团队长。
tokenizer 可能切成:
马 / 嘉祺 / 是 / 时代少年团 / 队长 / 。
模型训练时会做类似这样的预测:
看到:马
预测:嘉祺
看到:马 / 嘉祺
预测:是
看到:马 / 嘉祺 / 是
预测:时代少年团
如果模型预测错了,就会产生 loss。
然后通过反向传播更新模型参数,其中就包括:
input embedding 矩阵
Transformer 参数
lm_head 参数
所以 "嘉祺" 的向量会在大量训练中逐渐调整。
如果它经常出现在这些上下文中:
马嘉祺
时代少年团
队长
歌手
演员
明星
丁程鑫
宋亚轩
那么它的 embedding 最终就会被训练到和这些语义相关的位置附近。
3. token 向量不是人工设计的
这一点很重要。
不是工程师手工写:
嘉祺 = [明星, 人名, 男歌手, 时代少年团]
而是模型自己从海量语料中学出来的。
更准确地说:
最初是随机向量;
训练过程中通过预测下一个 token 的任务,不断调整;
最后形成带有语义结构的向量空间。
所以一开始:
嘉祺、苹果、Redis、Transformer
这些 token 的向量都是随机的。
训练之后,模型会慢慢学到:
嘉祺 更接近 人名、明星、亚轩、千玺
苹果 可能接近 水果、手机、iPhone
Redis 接近 缓存、数据库、key-value
Transformer 接近 attention、模型、深度学习
4. token_id 和向量之间的关系
还是以 "嘉祺" 为例。
完整流程是:
"嘉祺"
↓ tokenizer
token_id = 45678
↓ embedding 查表
E[45678] = 4096 维向量
这里:
token_id 只是编号
embedding 向量才是参与计算的数值
token_id 本身不参与神经网络计算,它只是用来查表。
就像:
身份证号不是人本身,
只是用来找到这个人的编号。
token_id 也是这样:
45678 不是“嘉祺”的语义,
只是用来找到“嘉祺”向量的索引。
5. 向量化结果进入模型后怎么计算?
假设输入是:
马嘉祺是谁?
tokenizer 后变成:
["马", "嘉祺", "是谁", "?"]
再转成 token_id:
[123, 45678, 8123, 32]
然后通过 embedding 层查表:
E[123] → "马" 的向量
E[45678] → "嘉祺" 的向量
E[8123] → "是谁" 的向量
E[32] → "?" 的向量
得到一个矩阵:
X.shape = [seq_len, hidden_size]
比如:
X.shape = [4, 4096]
意思是:
4 个 token
每个 token 是 4096 维向量
这个 X 就是 Transformer 的输入。
6. Transformer 内部会做什么?
大体分三类计算:
1. 加入位置信息
2. Self-Attention 计算上下文关系
3. MLP 做非线性语义变换
6.1 位置信息:知道 token 的顺序
模型不能只知道有哪些 token,还要知道顺序。
比如:
马嘉祺喜欢音乐
音乐喜欢马嘉祺
token 一样,但顺序不同,意思完全不同。
所以模型需要位置信息。
不同模型实现不同,有些是位置向量,有些是 RoPE 旋转位置编码。
可以通俗理解为:
给每个 token 的向量加上“它在第几个位置”的信息。
6.2 Self-Attention:让每个 token 看上下文
Self-Attention 是 Transformer 的核心。
它的作用是:
让每个 token 根据上下文更新自己的表示。
比如输入:
马 / 嘉祺 / 是 / 谁 / ?
一开始,"嘉祺" 的向量只是它自己的静态向量。
经过 attention 后,它会吸收上下文信息。
比如:
"嘉祺" 看到前面的 "马"
"谁" 看到前面的 "马 / 嘉祺"
"?" 看到整个问题
于是每个位置的向量会从“孤立 token 向量”变成“带上下文的语义向量”。
可以把 attention 简化理解成:
每个 token 都去问:
我应该重点关注前面哪些 token?
例如在句子:
马嘉祺是时代少年团队长。
当模型处理 "队长" 时,它可能会重点关注:
马
嘉祺
时代少年团
当模型要预测下一个 token 时,它会利用这些上下文关系。
6.3. embedding 怎么变成 Q、K、V?
进入 Transformer 后,会有三组可训练矩阵:
W_Q
W_K
W_V
它们分别负责把输入向量变成:
Q:Query,查询向量
K:Key,键向量
V:Value,值向量
公式是:
Q = X W_Q
K = X W_K
V = X W_V
6.4 MLP:做更复杂的语义变换
Attention 主要负责 token 之间的信息交互。
MLP 负责对每个位置的向量做进一步加工。
可以理解为:
Attention:负责看上下文
MLP:负责思考、抽象、变换
例如模型可能在中间层逐渐形成这些信息:
马 + 嘉祺 → 一个具体人名
马嘉祺 + 时代少年团 → 成员关系
马嘉祺 + 队长 → 身份关系
当然,真实模型不是用这么清晰的符号存储,而是在高维向量里表达这些关系。
7. 每一层都会把向量变得更“上下文化”
一开始:
E[45678] = "嘉祺" 的静态 token 向量
经过第一层 Transformer 后:
h1 = 结合了一点上下文的 "嘉祺" 表示
经过第十层:
h10 = 更强的上下文语义表示
经过最后一层:
hN = 当前上下文下,用于预测下一个 token 的表示
所以要注意:
embedding 向量只是输入起点;
Transformer 每一层都会产生新的 hidden state;
最终用于生成的是最后一层 hidden state,不是原始 embedding 向量。
8. 最后怎么生成下一个 token?
经过多层 Transformer 后,模型得到最后一个位置的 hidden state。
比如用户输入:
时代少年团队长是
模型最后一个位置的 hidden state 可能已经表达出:
下一个 token 应该是某个人名
而且更可能是“马”
然后交给 lm_head:
hidden state → lm_head → vocab_size 个分数
如果词表大小是 200000,那么 lm_head 会输出:
200000 个 token 的分数
例如:
马 12.8
丁 8.1
宋 7.9
王 5.2
苹果 -3.4
Redis -5.8
分数经过 softmax 变成概率:
马 0.72
丁 0.08
宋 0.06
王 0.02
...
然后模型选择或采样一个 token:
马
接着继续下一轮生成。
当上下文变成:
时代少年团队长是马
模型再预测下一个 token。
如果 tokenizer 中有 "嘉祺" 这个 token,它需要在下一步生成:
嘉祺
这时 lm_head 会给 "嘉祺" 这个 token 打分。
如果 "嘉祺" 的输出方向正常,它就能生成:
马嘉祺
如果 "嘉祺" 的 lm_head 输出方向漂了,它就可能生成其他错误 token。
9. 整个计算链路可以这样看
文本
↓
Tokenizer
↓
token_id 序列
↓
Input Embedding 查表
↓
token 向量序列
↓
位置编码 / RoPE
↓
多层 Transformer Block
- Self-Attention
- MLP
- 残差连接
- Norm
↓
最后一层 hidden state
↓
lm_head
↓
每个 token 的 logits 分数
↓
softmax
↓
下一个 token 概率
↓
采样 / 贪心解码
↓
输出 token_id
↓
Tokenizer decode
↓
输出文字
10. 训练时 embedding 怎么被更新?
训练时,模型会预测下一个 token。
假设真实答案是:
嘉祺
但是模型预测成了:
嘉轩
那么 loss 会变大。
反向传播时,模型会调整参数,让下次更容易预测出 "嘉祺"。
被调整的参数包括:
input embedding
attention 层参数
MLP 层参数
lm_head 参数
但不是所有 token 的 embedding 都会被同等更新。
通常只有本 batch 中出现过的 token,它们的 embedding 行会直接收到更新。
例如这批数据中出现了:
马
嘉祺
时代少年团
队长
那么这些 token 对应的 embedding 行会被更新。
如果某个 token 在训练数据里很少出现,比如 "嘉祺" 在 SFT 数据中几乎不出现,那么它在 SFT 阶段就很少被继续更新。
11. 推理时 embedding 还会更新吗?
不会。
推理时,比如你问模型问题:
马嘉祺是谁?
模型只做前向计算:
查 embedding → Transformer 计算 → lm_head 输出
不会更新参数。
也就是说,推理时:
embedding 矩阵固定
lm_head 固定
Transformer 参数固定
只有训练时才会更新。
总结
最初每个 token 的向量通常是随机初始化的。
训练开始时,这些向量没有明确语义;
预训练过程中,模型通过“预测下一个 token”的任务不断调整 embedding、Transformer、lm_head 等参数;
训练完成后,相似语义、相似上下文的 token 向量会逐渐形成有规律的空间结构。
输入时,文本先被 tokenizer 转成 token_id;
token_id 再去 embedding 矩阵中查表,得到向量;
这些向量进入 Transformer,经过 attention、MLP、残差、归一化等多层计算;
最后得到 hidden state;
hidden state 经过 lm_head,变成词表中每个 token 的分数;
再通过 softmax 得到概率,选择下一个 token 输出。
一句话概括:
token 向量一开始是随机的,语义是在预训练中学出来的;embedding 是入口,Transformer 负责上下文计算,lm_head 负责把计算结果翻译回 token。