深浅色
你数了十七个字,它收了你二十四个的钱。这笔账差在哪。
像给一整箱衣服分格子
分词(tokenization)——把文本切成词元的步骤。词元(token)——模型眼里的最小处理单位。
打比方:你要把一整箱衣服塞进一排固定大小的格子,格子上限 5 件。你得决定哪几件衣服绑成一捆。绑法不是唯一的,有人按「上衣一捆、裤子一捆」,有人按「主人的习惯捆」。塞不进去的要么硬塞,要么拆散重捆。
模型遇到中文,面临的是同一件事。词表(vocabulary)——模型提前认得的那一捆捆的清单——是死的、有限的,你训练它的时候定死了。遇到没见过的词,它只能拆成已知的碎片拼。
这个拆法有个副作用:同一个汉字,在不同上下文里可能被切成不同数量的词元。 「天气」里的「天」可能单独成一个词元;但「今天」的「今」在某些词表里会跟「天」绑在一起。中文没有空格,模型猜的就是这个绑法。
我自己数错过的那次
我用 OpenAI 公开的 tiktoken 库跑过一句话。原文是:
今天天气不错,我们去公园散步吧。
十六个字符。在 cl100k_base 这套词表下,切成 19 个词元;在更新的 o200k_base 下,切成 12 个。词表大小也不一样,前者 100277 条,后者 200019 条。
同一个人,同一天,同一句话,两个数字。
更狠的是 cl100k_base 那 19 个词元里,有几个是我肉眼看不出边界的——解码出来是一串替换符号,因为「气」和「园」这种字的字节被从中间劈开了。UTF-8 里一个汉字占三个字节,词表不够大的时候,模型会拿半截字节去凑数。
为什么这么设计
早年是按空格切词。中文和日语根本没有空格,这套直接崩。
后来的路子叫子词分词——不追求切出完整的词,只求切出「常用的小块」。最常见的一种是字节对编码(Byte-Pair Encoding)——反复扫描语料,把出现次数最多的相邻字符对合并成一个新词元,一直合并到词表满了为止。效果是高频组合会自然长成一个词元,低频的、生僻的就碎着留。
SentencePiece 是 Google 2018 年开源的一套实现,特点是可以直接从没分过词的原始句子开始训练,不要求上游先切好。T5 用的就是它。
代价
好处是词表小、句子短、算得快。坏处也有:
- 同样的意思,长度会不一样。 你用「优化」和「改进」描述同一件事,模型可能数出不同数量的词元,收费和速度都跟着变。
- 多语言会互相挤。 词表就那么大,中文和西班牙文分同一份。分给谁多一点,是训练时就定死的。
- 拆碎的片段很怪。 有些词元是半个汉字、三个标点连在一起。你在计费明细里看到它们,不是在骗你。
我自己判断一个项目的中文成本,从来不看字数,看词元。差着两三倍是常事。
延伸阅读
SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing(Taku Kudo、John Richardson,2018,arXiv 预印本)—— Google 那两位写的。核心是「不需要预先分词,直接从原始句子训练子词模型」,顺手把反向拼接也做了。开源在 Apache 2 协议下,好几家用它做自己的分词器。arXiv: 1808.06226
Byte Pair Encoding is Suboptimal for Language Model Pretraining(Kaj Bostrom、Greg Durrett,2020,arXiv 预印本)—— 这篇有点冒犯性。两个人用完全相同的模型架构、只换分词方法(字节对编码 vs unigram),在英语和日语上做对比,结论是 unigram 打平或更好。理由是字节对编码那种「贪心地合并最常出现的对」的做法,会切出一些跟语言结构对不上的碎片。arXiv: 2004.03720
动手试试
装个 tiktoken,五秒钟看到真实的切法:
bash
pip install tiktokenpython
import tiktoken
s = "今天天气不错,我们去公园散步吧。"
for name in ["cl100k_base", "o200k_base"]:
enc = tiktoken.get_encoding(name)
ids = enc.encode(s)
print(name, "词表", enc.n_vocab, "词元数", len(ids))
print([enc.decode([i]) for i in ids])把 s 换成你自己的一段话,换成英文再跑一次,你会看到英文那边整齐得多。装 tiktoken 需要联网下载词表文件,第一次会慢一点。
下一站
字进来了,也被切开了。现在问题来了:模型处理「散步」这个词的时候,是怎么知道前面有「公园」、后面有「吧」和句号的。