文件名 MM模型无法输出`嘉祺`的解析.md

MM模型无法输出`嘉祺`的解析

本文目录

正文

原文知乎链接: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。