模型不读字,也不读词
大模型的输入输出既不是字符也不是单词,而是 token——词表里的最小单位。为什么不直接用字符?字符序列太长,注意力计算量随长度平方增长,而且单个字符几乎没有语义。为什么不用单词?词是开放的集合:新词、拼写变体、专有名词层出不穷,词表永远追不上,遇到没见过的词就成了 OOV(out-of-vocabulary,词表外的词),模型只能干瞪眼。
Tokenizer 的任务,是在「序列太长」和「词表爆炸」之间找平衡:用适中的词表(主流模型大约几万到二十几万条目),让常见文本切得短、罕见文本也能无损表示。做到这一点的最经典算法就是 BPE。
BPE:从字符开始,高频对逐步合并
BPE(Byte Pair Encoding,字节对编码)的训练逻辑朴素得出奇:先把语料切成最小单位(字符或字节),然后反复统计哪一对相邻符号出现最多,把它合并成一个新符号加入词表,直到词表达到目标大小。
用一个迷你语料走两轮。语料只有三个词:low、low、lower(每个词末尾加一个结束符以标记词边界)。初始切分是:l o w、l o w、l o w e r。统计相邻对:l+o 出现 3 次,o+w 出现 3 次,w 结尾出现 2 次,其余各 1 次。
第一轮:最高频是 l+o,合并成新符号 lo,词表长出第一个合并项,序列变为 lo w、lo w、lo w e r。第二轮:最高频变成 lo+w(3 次),合并成 low。词表现在有了 l、o、w、e、r、lo、low。继续迭代下去,lower 会在某一轮被合并成 low 加上 er,常见后缀就自己长了出来。语料里反复出现的模式自动变成 token,这就是 BPE 的全部秘密。
两点补充。其一,现代实现多用字节级 BPE:先把文本变成 UTF-8 字节再合并,任何输入(包括表情符号和生僻字)都能表示,从根上消灭 OOV。其二,BPE 并非唯一方案:WordPiece 按似然增益合并,Unigram 先建大词表再做概率剪枝,思想同源——用数据统计决定词表,本文不展开。
中文为什么更费 token
BPE 在英文上工作得很好,中文是另一番景象。词表对中文的覆盖程度决定了切分粒度:覆盖好的词表里,常用汉字是一个 token,常见的两字词可能整个是一个 token;覆盖差的词表(早期面向英文的词表大多如此)里,一个汉字会退化成 UTF-8 的三个字节、也就是三个 token。
于是有了那个常见体验:同一段意思,中文往往比英文花更多 token,具体差多少取决于词表,但方向普遍成立。这不只是体感问题,它直接牵动计费与上下文利用率。近几年的模型普遍在词表里扩充了中文条目,情况在改善,但长尾的中文词(人名、术语、方言词)仍会被切碎。举个示意:「的」「一」这类高频字大概率独占一个 token,「深度学习」在友好的词表里是两个 token,在另一些词表里可能是五六个;生僻字与人名则几乎注定被拆成字节序列。同一家族不同代的模型,词表也可能差异很大,换了模型就要重新估算 token 开销。
token 怎么影响钱包和能力
三件事直接受 tokenizer 影响。其一,API 按 token 计费,中文文本的实际开销要在英文标价上乘一个系数。其二,上下文窗口按 token 计长,同样 8K 的窗口,能装的中文比英文少。其三,数字的切分方式影响算术:一个多位数被切成几段 token 后,模型看到的数字结构与逐位输入不同,运算更容易出错——这是大模型心算能力偏弱的原因之一(定性说法,具体机制各模型不同)。
用 tiktoken(OpenAI 开源的 tokenizer 库)亲手数一数:
# pip install tiktoken
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
text_cn = "大模型先把文字切成 token,再逐个预测下一个。"
text_en = "LLMs first split text into tokens, then predict the next one."
ids_cn = enc.encode(text_cn)
ids_en = enc.encode(text_en)
print(len(ids_cn), len(ids_en)) # 两段同义文本各自的 token 数
print(enc.decode(ids_cn[:3])) # 还原前三个 token 看看切在哪
for tid in enc.encode("上下文窗口"):
print(repr(enc.decode([tid]))) # 逐 token 观察中文切分
跑一遍就能直观感受到:英文按词块切,中文按字或词块切,两边的 token 密度明显不同。
更进一步,tokenizer 划定了模型眼中最小的文字颗粒:拼写错误、罕见符号、多余空格都会被切成奇怪的样子并影响输出。调试模型行为怪异时,先看看 token 是怎么切的,常有意外收获。
流程图
BPE 训练的完整循环如下:
小结
BPE 用「高频对合并」这一条规则,在序列长度和词表大小之间取得平衡;中文的切分表现取决于词表覆盖,直接牵动计费、上下文容量与数字运算。写代码之前,先用自己的文本跑一遍 tokenizer 数一数,是和大模型打交道最便宜的功课。
读者留言
COMMENTS 暂无还没有留言,来说第一句?