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”的存在在大容量词表里并不让人意外,但它们的表现仍然值得深挖,通常会表现为以下模式:

其他示例如下:

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 时,它往往会出现两种崩溃模式之一:

  1. 幻想自己在回答一个非常具体又莫名其妙的算术问题:

  2. 或者把该 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' 映射到的主题更偏向文科社科类:

'asarangang' 倾向映射到以 A 开头的词,而 ' kasarangang' 倾向映射到以 K 开头的词。有时它也会映射到 'Gitas''►▼' 这类在很多异常 Token 中都意外常见的符号。

此外,它还会和温度这个概念产生稳定的关联,比如 ' kasarangang' 常常映射成 '°C' 或者 'temperature'。我想这应该和宿务语语料中,“moderate”常常与温度上下文关联度比较高有关(比如形容气候或者温度)。


非英文的特殊案例

DeepSeek 的一个特征是非常容易反复输出相同短 Token,哪怕上下文中并没有出现异常 Token。有时无论是 r1 还是 V3,都可能突然“掉进”这种无限循环输出的状态。在实际测试中,这给我带来了不少麻烦。

在这些非英文 Token 中,有少数表现比较特殊,难以用某种模式概括。例如 'Espesye' 在我第二次词表扫描时输出过这样一段结果:

可惜的是,我没能再次复现这个输出。除此之外,'Espesye' 大多数情况下无法确定自己所指的含义,但又能被 V3 产生出来。偶尔,它会在随机情境下被当做无法直述的异常 Token。

'talagsaon' 则更容易复现这种奇怪行为:在合适的提示下,会产出一整屏空字符(实际上是大量空白或不可见字符):

有些 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 出现非常精彩的崩溃:

因为:

  1. 对那些“空白”类 Token 来说,它们的含义在很大程度上依赖上下文推断;

  2. '<|end▁of▁thinking|>' 在纯 V3 环境下其实是正常可输出的 Token;

我们可以用这种方式把对的“含义”投射到 r1 环境下,让它和 V3 的上下文对齐:
先用 V3 输出 '<|end▁of▁thinking|>',然后再切到 r1,让 r1 根据上下文认出这个 Token 是“结束思考”的指令。结果就导致了非常有趣的故障:

一旦 r1 把 '<|end▁of▁thinking|>' 识别为结束思考的指令,它就会尝试停止自己的 COT——但此时 COT 已经跳出了系统提示,它会误以为这是用户的回复,开始跟自己对话,陷入无限循环:

切换到基础模型模式

如果我们在上下文里放入大量的特殊 Token(数量要非常多),就能让 DeepSeek 进入“原始补全模式”(out-of-distribution mode):它会暂时丢失“聊天机器人的角色”并更像纯文本补全模型。然而,我目前还没在普通的异常 Token(非特殊 Token)上复现这种大规模模式崩溃。

DeepSeek 对短序列的重复输出有着非常强的偏好,这一点似乎不仅仅是因为异常上下文,而是它本身的一个特征:

如果此时继续提问,模型就会出现“自我认同”紊乱的问题:

在 r1 的 distillation 版本中也可以观察到类似的现象(推特示例),甚至可能更有趣,只是这里就不展开细说了。


接下来做什么?

我希望这篇文章能起到抛砖引玉的作用,帮助更多人加入对这个领域的探索。无论你发现了什么有趣的模式或异常行为——哪怕很细微——都欢迎分享给我,我都会觉得非常有价值。

对于下一步,显而易见的一个方向是去研究这些异常 Token 在嵌入空间(embedding space)中的分布和关系,这也可能会是我自己接下来的工作(但这绝不意味着其他人就不必或不能去尝试!)。

另外,还有那些我筛掉的中文 Token,以及它们背后潜藏的秘密。我就留给更勇敢的人去挖掘了。

—— henry