2023 年有人在 GPT 系列模型上发现了一个经典案例:输入 SolidGoldMagikarp 这个 Reddit 用户名,模型开始胡言乱语、无视指令甚至输出错乱内容。这个词是 GPT-2 时代 BPE 词表里的一个完整 token,但它本身在训练数据里几乎不存在。这种"词表里有、世界里没有"的 token 被叫做 glitch token。它不是玄学,从训练目标推一遍就能看清机制。
成因链:梯度从哪来
词表大小 $V$,embedding 矩阵 $E\in\mathbb{R}^{V\times d}$。token $t$ 的 embedding 行 $E_t$ 唯一参与计算的环节是:
$$ x_t = E_{t}\ \text{(取出第 $t$ 行)},\qquad \ell = \mathcal{L}(x_t, \text{其余参数}) $$
对 $E_t$ 的梯度是
$$ \frac{\partial \ell}{\partial E_t} = \frac{\partial \ell}{\partial x_t}\cdot \mathbb{1}[\text{当前样本含 token } t]. $$
关键在指示函数:$E_t$ 只在包含 $t$ 的样本上收到梯度。如果 $t$ 在整个预训练语料里出现 $c_t$ 次,且 $c_t \approx 0$,那 $E_t$ 在几百万步训练里被更新的次数约等于零,参数停在初始化附近——一个范数很小、方向随机的向量。下游所有 Transformer 层从未见过这个输入,它们对 $E_t$ 的响应是完全未校准的:可能把它当成噪声放大,可能触发训练时从未出现过的激活模式。表现为 logit 异常、拒答、复读、输出乱码。
这也解释了 glitch token 的"传染性"很低:正常文本几乎不会自然切出这些 token,它们要靠对抗性拼贴(SolidGoldMagikarp 前后加空格等触发切分)才暴露。
为什么偏偏是这些 token
BPE 的合并不是按频率均匀选出的。词表构建于一个特定时间点的语料快照,某些字符串(老论坛用户名、抓取事故产生的乱码、已删除页面的残留)在快照里出现过恰好足够多次、进入了合并序列,但随即从互联网上消失。语料清洗又过滤了一遍,剩下一个"词表成员 + 语料幽灵"。RAIL 社区的后续统计显示,这类 token 在多个开源模型的词表里都能按"低频次 + 异常嵌入范数"揪出来。
诊断:频次 × 范数双筛
两条独立证据可以离线算出来:
import numpy as np
# counts[t]: token t 在语料中的出现次数;E: (V, d) embedding 矩阵
norm = np.linalg.norm(E, axis=1) # 每行范数
freq_rank = np.argsort(counts) # 频次升序
suspects = norm[freq_rank[:5000]] # 最稀有的 5000 个 token 的范数
# 训练充分的 token 范数有明确量级;未训练的贴近初始化值(如 N(0, 0.02))
# → 对 suspects 画范数直方图,左尾分离出来的就是 glitch 候选
我在 LLaMA 系词表上试过这个筛法:最稀有的一批 token 里确实存在范数明显低于主体的子群体,和已知的 glitch token 名单重合度很高。两个量都便宜——频次一次语料扫描,范数一次 norm 调用。
缓解与清理
- 训练侧:词表构建后做一次频次审计,把 $c_t$ 低于阈值的 token 移出可用集或并入 byte fallback;对保留下来的低频次 token 做 embedding 范数校准(归一到主体分布)。
- 推理侧:输入阶段用 tokenizer 的 encode–decode 往返检测可疑切分; jailbreak 研究里已有多套 glitch token 黑名单。
- 根治:byte-level BPE(GPT-2 路线)把词表锚定在 256 个字节上,任何 token 都可以分解为字节序列兜底,最差情况退化为逐字节建模,不会出现"无法解释的 token"——但 glitch token 并未消失,只是从字节层面挪到了合并单元层面,审计依然要做。
小结
glitch token 是词表快照与语料现实脱节的产物:BPE 把它造出来,语料清洗又没能把它喂饱,梯度来源的指示函数决定了它永远停在初始化。防御不需要对抗训练,词表审计 + 范数直方图两个离线检查就能覆盖大部分风险。这条线也提醒我们:tokenizer 不是中性的预处理,词表里的每一个成员都暗含着"训练时见过足够多次"的承诺。