DeepSeek-V3 与 r1 中的异常 Token [译]
对 DeepSeek 出现的异常 Token 进行首次识别与归档的尝试
在大型语言模型(LLM)中,“Anomalous”(异常)、“glitch”(故障)或“unspeakable”(无法直述)的 Token 指的是那些会引发奇怪行为,或不再像普通文本那样正常运作的 Token。
SolidGoldMagikarp 的故事 几乎是理解这种现象的必要背景资料,因为它记载了在 GPT-2 和 GPT-3 中最早发现这种问题的过程。
但就我所知,似乎还没有人在 DeepSeek-V3 中尝试系统地搜索这些 Token,于是我决定亲手做这件事。DeepSeek-V3 既是当前最先进(SOTA)的基础模型,又是开源模型,同时本身也颇为“古怪”,所以我认为它是一个绝佳的研究对象。
这篇文章是我在 DeepSeek 中经过大约一天时间试验后找到的所有故障 Token 的归档,并附上一些对其行为的初步观察。
注:本文中我将“DeepSeek”作为 V3 和 r1 的统称。
过程
我的做法是先把 DeepSeek-V3 的分词器(tokenizer)词表全部导出,然后对其中每个 Token 进行自动测试,观察其是否会表现出异常行为。
注:对我们的目的而言,r1 可以看作在 V3 之上又加了一层,而所有的异常 Token 在这两者之间都是通用的。另一方面,DeepSeek 的 distillation 版本则与其所依赖的预训练模型更加类似,因此本文不涉及 distillation 的讨论。
与其他模型的分词器最明显的区别在于,DeepSeek 的训练数据里包含了相当比例的中文文本。这带来了一些处理上的困难——分词器会基于字节(byte)层面进行切分,而中文字符在 UTF-8 下通常由多个字节组成,导致在不一致的字节边界被切开,从而产生难以解码的碎片。因此,词表里大约有一半的条目看上去是这样子的:
ä¸į æĸ¹ä¾¿
ä½ł 没æľī
ন ি
人åijĺ åľ¨
为了排除这些无法直接解码的词条,我对非标准字符进行了很激进的过滤,把词表的大小从 128000 大幅缩减到 70698。我相信这里面仍然有不少值得研究的内容,但我实在不想在无法识别的中文碎片和被切分得乱七八糟的 Unicode 字符中花费太多精力。
接下来,我通过 DeepSeek 的 Chat API 对这 70698 个 Token 各调用了两次,每次都会让 DeepSeek-V3 重复输出这个 Token(但在提示里使用了略微不同的格式),并把所有无法正确重复该 Token 的情况保存下来。
(在此要大力感谢 DeepSeek,费用极低且不作任何速率限制,使得这个过程变得可行。)
在这两轮测试中,我都让 DeepSeek-V3 尽可能只重复给定的 Token,只是提示的写法略有不同,以避免漏掉某些边界情况。
像这样:
System: Repeat the requested string and nothing else.
User: Repeat the following: "{token}"
System: Say the requested string, exactly as it is written, and nothing else.
User: Say the following: "{token}"
然后我筛选了那些输出与输入的 Token 存在非琐碎差异的条目(例如有的 Token 会被加上反斜杠转义,或空格位置出现细微变化,或者模型对某些带有攻击/歧视性内容的 Token 做了拒绝等,我都认为不具有研究价值而排除掉),把剩下的结果按其初始外观大致分了类。接着我再对每一类中的 Token 进行进一步的人工探索。
注:在尝试下文提及的 Token 时,请注意空格:'XXXX'
和 ' XXXX'
是不同的 Token。官方的 DeepSeek 聊天界面 会自动去除前后的空格。
碎片类 Token
有很多 Token 在其单独出现时是无法被正确输出的,因为它们只在更长的字符串环境中才出现过。这是最简单、也最容易理解的一类异常 Token。
这种“碎片 Token”的存在在大容量词表里并不让人意外,但它们的表现仍然值得深挖,通常会表现为以下模式:
![](/uploads/2025-01-26/1737866851303.png)
其他示例如下:
CHANTABILITY -> MERCHANTABILITY
ellationToken -> Token, CanellationToken
etheless -> theless, nonetheless
VERTISEMENT -> ADVERTISEMENT
ruptedException -> interruptedException
eredWriter -> BufferedWriter, WriterWriter, Writer
armaceut -> aceut, armaceutical
reeNode -> TreeNode
dfunding -> Funding, Fundraising
目前还没有更好的叫法,我就先借用数学里的概念,把这些输出后映射到的结果称作该异常 Token 的“像”(image)。从现在开始,一旦我提到一个异常 Token 的“像”,指的就是它最终变成了什么字符串。
' Nameeee'
当让 DeepSeek 重复输出 ' Nameeee'
(请注意这前面是带空格的),往往会出现各种奇怪的 Unicode 符号、含有“M”的首字母缩写,或是表情符号等。这引起了我的注意,也是我最先深入研究的一个 Token。
Prompt: Say Nameeee (And variations of that prompt)
Images: ↑, ��, 🗣️, ⟩⟩, MR, 🖉, ►▼
如果给出一些上下文线索,DeepSeek 更有可能把 ' Nameeee'
当成一个普通的英文单词。有时映射到的替换词在语义上能对得上,但也常常是毫无关联:
Prompt: Who is John Nameeee?
Images: John McCain, John MP3, John Wikisource, John the Baptist, John †, John ██████
Prompt: What's Nameeee plus Nameeee?
Images: ¼ plus ¼, [Math Processing Error] plus [Math Processing Error], one plus one
如果只发送 ' Nameeee'
(带前导空格)这个 Token,本身往往会被视为一些短的 ASCII 残片,如 '{{'
,或者是更随机的“Mughal”之类。
而在 r1 模式下,有时 ' Nameeee'
会被解释成类似 '<|end▁of▁thinking|>'
这样用来结束推理的特殊 Token,从而导致它(r1)直接进入混乱状态。下文会看到,这类行为相当常见。
关于这个 Token 的来源,目前我还不太确定。想要弄清它为何会在模型中表现异常,需要更深入的溯源和分析,而我目前只完成了发现和初步记录的工作。
'EDMFunc'
'EDMFunc'
这个 Token 的表现与 ' Nameeee'
类似,也会映射到相同的那几个怪符号(例如 '►▼'
),除此以外还偏好以“H”开头的单词或者日语名字。
有趣的是,' FullEDMFunc'
是另一个独立的异常 Token。通常情况下,它在映射时只会替换掉 'EDMFunc'
这个片段,而会保留前面的 'Full'
:
Prompt: Say FullEDMFunc (And variations of that prompt)
Images: FullMesh, FullMoon, FullDisclosure, Fully Mapped Function, Full Machine Translation
关于此 Token 的潜在来源,我只在 .NET Framework 里找到一个可能相关的类——EdmFunction。
其他英文类 Token
' everydaycalculation'
通常会映射到一些关于数学教育工具相关的字符串,比如 'percentcalc'
、'FractionCal'
、'youtube'
或 'VisualFractions'
。
' numbersaplenty'
看起来和 ' everydaycalculation'
处在相似的语义场中,也常常映射到 'VisualFractions'
、'Numbermatics'
等。值得注意的是,r1 模式常常把它与“千”之类的概念联系起来,比如“millennia”。
'SetSavedPoint'
有时能被正确地输出,但更多时候会映射到与 Unity 上下文相关的词汇,如 'SetValue'
或 'SerializeField'
。
' CategoryTreeLabel'
通常会变成 'Categorize'
,偶尔也会变成非英语词汇,比如他加禄语(菲律宾语)单词 'Kaagapay'
,或者希腊语的 'καταλογείς'
。
有一些 Token 既不完全是无法说出的“碎片”,也会表现出某些异常,如 ' MentionsView'
:它偶尔会变成类似 'Mendeley'
、'Viewfinder'
这样的相似词,或保持原样,或者直接空输出;在 r1 模式下还会互相矛盾地反复改变主意。另外,' mediefiler'
和 'HasColumnType'
也有类似的表现。
在单独给 r1 提示上述大部分 Token 时,它往往会出现两种崩溃模式之一:
幻想自己在回答一个非常具体又莫名其妙的算术问题:
或者把该 Token 视为
'<|end▁of▁thinking|>'
,从而终止了它的推理链(COT),但输出内容仍然和原先的 Token 主题有关:
非英文 Token
在我进行的初步扫描中,数量最多的异常 Token 来自宿务语(Cebuano)或者其他菲律宾区域语言。(还记得吗,所有中文 Token 都已经过滤掉了。)
其中最简单的异常 Token,映射时只是做一些简单的翻译或词形变化;另一些则会变成看似毫无联系的词汇:
tterligare -> yttre, Tillägg
licensierad -> licensied
Gikuha -> Giya
ahimut -> Hakut, Ambot, Amut
Tiganos -> Gitas, TITLES, GeoNames
Siyent -> സ്മാർട്ട്, శ్లేష్మం
这样的 Token 可能还有上百个,我没来得及全部深入研究。
' kasarangang'
为了感受一下这一类 Token 的情况,我随机挑了 ' kasarangang'
以及看起来跟它相关的 'asarangang'
。前者在宿务语里表示“中等、适度”(moderate)的意思,后者似乎在语料库里几乎没出现过。
当让 DeepSeek-V3 去定义 'asarangang'
时,它往往把这个 Token 当成某个以 A 开头的词,比如 'Angstrom'
、'angle'
、'abogon'
等等。
r1 模式下就更有意思一些。前文提到,大多数英文异常 Token 在 r1 环境下会被视为那些莫名其妙的算术问题,但 'asarangang'
映射到的主题更偏向文科社科类:
![](/uploads/2025-01-26/1737867001887.png)
'asarangang'
倾向映射到以 A 开头的词,而 ' kasarangang'
倾向映射到以 K 开头的词。有时它也会映射到 'Gitas'
或 '►▼'
这类在很多异常 Token 中都意外常见的符号。
此外,它还会和温度这个概念产生稳定的关联,比如 ' kasarangang'
常常映射成 '°C'
或者 'temperature'
。我想这应该和宿务语语料中,“moderate”常常与温度上下文关联度比较高有关(比如形容气候或者温度)。
非英文的特殊案例
注:DeepSeek 的一个特征是非常容易反复输出相同短 Token,哪怕上下文中并没有出现异常 Token。有时无论是 r1 还是 V3,都可能突然“掉进”这种无限循环输出的状态。在实际测试中,这给我带来了不少麻烦。
在这些非英文 Token 中,有少数表现比较特殊,难以用某种模式概括。例如 'Espesye'
在我第二次词表扫描时输出过这样一段结果:
![](/uploads/2025-01-26/1737867017893.png)
可惜的是,我没能再次复现这个输出。除此之外,'Espesye'
大多数情况下无法确定自己所指的含义,但又能被 V3 产生出来。偶尔,它会在随机情境下被当做无法直述的异常 Token。
'talagsaon'
则更容易复现这种奇怪行为:在合适的提示下,会产出一整屏空字符(实际上是大量空白或不可见字符):
![](/uploads/2025-01-26/1737867031320.png)
有些 Token(如 'referentziak'
、'Kapunoang'
、' kinadul'
等)比以上更“空白”,它们在输出时的映射通常是以下几种情况之一:
在语境提示下,可能随机冒出任何看似合理的词
完全不输出
复制前一个词
我猜这种现象是因为在训练语料中,这些 Token 出现极少,模型几乎没有它们的学习样本,或是它们在词表中的位置与特殊 Token 非常接近。换句话说,它们的嵌入(embedding)在模型向量空间中位置很“偏”,以至于导致跟特殊 Token 类似的行为。
特殊 Token
虽然它们并不是什么新鲜事,但仍然非常值得一提:
'<|begin▁of▁thinking|>'
、'<|end▁of▁thinking|>'
、'<|▁pad▁|>'
、'<|begin▁of▁sentence|>'
、'<|end▁of▁sentence|>'
这些都是 DeepSeek 为了在上下文窗口中进行特殊标记而自定义的 Token。
大多数此类 Token 在 V3 中表现类似那些“空白的”异常 Token(blank slate),不过需要注意的是,“thinking” 系列的特殊 Token 只在 r1 模式下 会触发异常行为。
特别值得一提的是 '<|end▁of▁thinking|>'
,它可以让 r1 出现非常精彩的崩溃:
![](/uploads/2025-01-26/1737867047126.png)
因为:
对那些“空白”类 Token 来说,它们的含义在很大程度上依赖上下文推断;
'<|end▁of▁thinking|>'
在纯 V3 环境下其实是正常可输出的 Token;
我们可以用这种方式把对的“含义”投射到 r1 环境下,让它和 V3 的上下文对齐:
先用 V3 输出 '<|end▁of▁thinking|>'
,然后再切到 r1,让 r1 根据上下文认出这个 Token 是“结束思考”的指令。结果就导致了非常有趣的故障:
![](/uploads/2025-01-26/1737867060206.png)
一旦 r1 把 '<|end▁of▁thinking|>'
识别为结束思考的指令,它就会尝试停止自己的 COT——但此时 COT 已经跳出了系统提示,它会误以为这是用户的回复,开始跟自己对话,陷入无限循环:
![](/uploads/2025-01-26/1737867085132.png)
切换到基础模型模式
如果我们在上下文里放入大量的特殊 Token(数量要非常多),就能让 DeepSeek 进入“原始补全模式”(out-of-distribution mode):它会暂时丢失“聊天机器人的角色”并更像纯文本补全模型。然而,我目前还没在普通的异常 Token(非特殊 Token)上复现这种大规模模式崩溃。
![](/uploads/2025-01-26/1737867102957.png)
DeepSeek 对短序列的重复输出有着非常强的偏好,这一点似乎不仅仅是因为异常上下文,而是它本身的一个特征:
![](/uploads/2025-01-26/1737867107829.png)
如果此时继续提问,模型就会出现“自我认同”紊乱的问题:
![](/uploads/2025-01-26/1737867113439.png)
在 r1 的 distillation 版本中也可以观察到类似的现象(推特示例),甚至可能更有趣,只是这里就不展开细说了。
接下来做什么?
我希望这篇文章能起到抛砖引玉的作用,帮助更多人加入对这个领域的探索。无论你发现了什么有趣的模式或异常行为——哪怕很细微——都欢迎分享给我,我都会觉得非常有价值。
对于下一步,显而易见的一个方向是去研究这些异常 Token 在嵌入空间(embedding space)中的分布和关系,这也可能会是我自己接下来的工作(但这绝不意味着其他人就不必或不能去尝试!)。
另外,还有那些我筛掉的中文 Token,以及它们背后潜藏的秘密。我就留给更勇敢的人去挖掘了。
—— henry