一、先把第 1 讲的简化拆开
第 1 讲留了一句话没细讲:“把文字切成一个个的单元,叫 token,这里先假设一个 token 约等于一个词。”
这个简化现在要拆开了。原因很直接:第 5 讲讲过,softmax 的输出维度必须固定——一个模型的词表(vocabulary)大小从一开始就要钉死,不能训练到一半再动态扩张。这就带来一个绕不开的问题:世界上的词是无穷尽的,人会造新词、会拼错词、会把两个词粘在一起用,词表却必须是有限集合。到底该把"一串文字"切成多大的单元,才能塞进一个固定大小的词表? 这就是分词(tokenization)要回答的问题。
二、打个比方:乐高积木 vs 成套家具
假设你要用积木搭出任意你能想到的造型。
方案一:只用最小的乐高单粒积木。 好处是极其灵活——只要有足够多粒,任何造型理论上都能拼出来,不存在"这个造型没有对应积木"的问题。坏处是搭一个复杂造型要用几千粒积木,每一步操作都很琐碎。
方案二:直接买成套家具(沙发、书桌、衣柜,每一件都是一个"预制单元")。好处是搭一个客厅只需要几个"积木",非常省事。坏处是——如果你想要的造型,家具商没有预制过,你就搭不出来,只能眼睁睁看着需求落空,或者拿一个凑合的替代品硬凑。
⭐ 按字符切文本,就是方案一(乐高单粒);按整词切文本,就是方案二(成套家具)。这一讲要讲的 BPE,做的事情是:先备好最小的乐高粒,然后看数据里哪些"小拼法"总是一起出现,就提前把它们粘成一个"预制小组件",常拼的方式做成大块,不常见的留着零散——是介于两个极端之间、由数据自己决定切多大的一种折中。
三、两个极端各自的死穴
按词切:词表会爆炸,还有"没见过的词"这个硬伤
把文本按空格和标点切开,一个词当一个 token,直觉上最自然。但这条路有两个致命问题:
词表大小没有上限。 人名、地名、新造词、专业术语、拼写错误、同一个词的各种词形变化——每多收一个新词,词表就要多一个位置。英语常用词大概几万个,但算上专有名词和形态变化,实际会出现的"词"是没有上限的开放集合。
没见过的词只能报废。 训练时没收进词表的词,测试时一旦出现,只能标成一个统一的"未知词"符号(<UNK>),这个词具体是什么信息就彻底丢了。这和第 4 讲讲的 n-gram 数据稀疏问题是同一种死穴:把语言单元当成孤立的、不可再分的符号来处理,只要没见过,就束手无策。
⚠️ 对中文更麻烦的是,中文书写不像英文有空格天然分隔词与词——“按词切"这一步本身要先用分词算法猜"这几个字该不该算一个词”,猜错了就会把本来该在一起的字拆开、或者把不该在一起的字粘起来,这是一层额外的、天生带歧义的预处理。
按字符切:序列长度暴涨,组词的活全甩给了模型
另一个极端:把每一个字符都当成一个独立 token。这样词表小得多(英文只需要 26 个字母加标点,大概几十个符号),也彻底没有"没见过的词"这回事——任何字符串,拆到字符级别,天生就能被表示。
代价是序列长度会暴涨。一句话按词切可能是 10 个 token,按字符切可能变成 40 个、50 个。第 9 讲会讲到,自注意力的计算量随序列长度平方级增长——序列变长 4 倍,计算量变成 16 倍,这个代价不是线性的。
⚠️ 更隐蔽的代价是:单个字符几乎不携带语义。“苹"和"果"分开看,都不太能让人联想到"一种水果”,模型必须先花费自己的参数和层数,把这些几乎没有意义的碎片,在内部重新拼回有意义的单元——“组词"这件事没有消失,只是从"预处理阶段的人工规则"转移成了"模型内部要自己学会的隐藏任务”,白白占用了模型本该用来学更高层次规律的容量。
四、BPE:让数据自己决定切多大
**字节对编码(Byte Pair Encoding,BPE)**的思路是:从最细粒度(单个字符,或更彻底地,单个字节)出发,不断把语料里出现得最频繁的"相邻两个单元"合并成一个新的、更大的单元,重复这个过程,直到词表达到预先设定的大小为止。
算法本身只有三步,一直循环:
① 把所有训练文本切成最小单元(字符),统计当前词表
② 数一数所有"相邻两个单元"的组合,在整个语料里一共出现了多少次
③ 把出现次数最高的那一对,合并成一个新的单元,加入词表
—— 回到 ① 用新词表重新切一遍语料,继续数、继续合并
⭐ 每一轮合并,词表就多一个新符号,同时语料被重新表示得更"紧凑"一点。 停止的时机由一个预先设定好的目标词表大小决定——这是一个需要在训练前定下来的超参数。
从零手算一遍
用一个可以完整手算的小例子把整个过程走一遍。假设训练语料里只有三种"单词",各自出现的次数如下(_ 是人为加的词尾标记,用来区分"词的结尾"和"词的中间",真实 BPE 也是这么处理的):
"ab" 出现 6 次 → 切成字符:a b _
"abc" 出现 3 次 → 切成字符:a b c _
"bc" 出现 5 次 → 切成字符:b c _
第一轮:数所有相邻字符对,乘上各自单词的出现次数
来自 "ab"×6: (a,b) +6 (b,_) +6
来自 "abc"×3: (a,b) +3 (b,c) +3 (c,_) +3
来自 "bc"×5: (b,c) +5 (c,_) +5
汇总:
(a,b) = 6+3 = 9 ← 出现最多
(b,_) = 6
(b,c) = 3+5 = 8
(c,_) = 3+5 = 8
(a,b) 出现 9 次,全场最高,合并成一个新符号 “ab”,词表新增一项。
第二轮:用新词表重新切一遍语料,再数一次
"ab"×6 → [ab] _ → 相邻对:([ab],_) +6
"abc"×3 → [ab] c _ → 相邻对:([ab],c) +3 (c,_) +3
"bc"×5 → b c _ (不含"a",本轮不受影响)→ 相邻对:(b,c) +5 (c,_) +5
汇总:
([ab],_) = 6
([ab],c) = 3
(c,_) = 3+5 = 8 ← 出现最多
(b,c) = 5
(c,_) 出现 8 次最高,合并成新符号 “c_”(表示"词尾的 c"),词表再新增一项。
两轮合并之后,三个词的最终切分结果:
| 原词 | 最终 token 切分 | token 数 |
|---|---|---|
| “ab”(出现最多,6 次) | ab _ |
2 |
| “abc” | ab c_ |
2 |
| “bc” | b c_ |
2 |
⭐ 注意这里发生的事:“ab” 这个组合因为高频,被合并进了独立的大块 token;单独的 “b” 因为没有被更高频的组合选中,依然是原子字符。 词表现在是 {a, b, c, _, ab, c_} 共 6 个符号,任何由 a、b、c 组成的新字符串,哪怕从没在训练语料里出现过,也总能被这 6 个符号拼出来——最差情况下退回到最初的单字符切法,“没见过的词"这个 <UNK> 死穴,从根上被绕开了。
五、这一讲的局限:省事不等于合理,切法不等于免费
BPE 只看频率,不管语义。 合并规则完全是"哪一对出现次数最多”,不理解语法或词义——它可能把两个语义上关系不大的片段粘在一起(因为它们凑巧经常相邻),也可能把语义上紧密相关的词根拆到不同的 token 里(因为按纯频率统计,它们没有被优先合并)。这不是 bug,是这套算法从设计上就没有"理解"这回事——它和第 1 讲讲的整个范式是一致的:统计规律的产物,不是语言学规则的产物。
⚠️ “token 数量” 不等于 “字数"或"词数”,这条换算关系并不直观,而且后面好几讲都要用到这个事实:第 23 讲讲上下文长度、第 22、24 讲讲推理成本和延迟时,衡量单位几乎全部是 token 数而不是"字数"——同一句话,切分方式不同,token 数量可以差出好几倍。
⚠️ 不同语言的切分效率并不均衡。 现代大模型的 BPE 词表,是在训练语料上统计出来的,而训练语料里各语言的占比通常并不均匀。这会导致:同一个意思的句子,用不同语言表达,最终被切成的 token 数量可能相差很大——语料占比更高的语言,更容易被切成大块、高效的 token;占比低的语言,切分粒度更细碎,同样的内容要花更多 token 才能表示。这不是数学上必然的规律,是训练数据分布这个设计选择的直接后果,会实际影响到不同语言用户的有效上下文长度和使用成本。
⚠️ 关于时效性:目标词表大小(第四节说的"预设的合并停止时机")是一个要在训练前定死的超参数,今天主流模型的取值大致在几万到十几万这个量级,这个具体数字会随时间推移不断变化,不是这一讲要记住的重点。真正现代实践里更值得记住的是一个设计约束:很多面向多语言的模型不是在"字符"上跑 BPE,而是在字节(byte)上跑——因为字节的种类是固定的 256 种,不管是中文、emoji 还是任何生僻符号,天生都能被覆盖,不需要为每种语言的字符集单独设计。这叫字节级 BPE(byte-level BPE),这个"以不变应万变"的设计思路,比具体的词表大小数字更不容易过时。
六、和后面课程的关系
- 第 8 讲紧接着解决下一步:这一讲只把文本切成了离散的 token(本质上还是符号,只是比"一个词一个符号"更精细的符号),第 8 讲要讲的是怎么把每一个 token 变成第 3 讲说的那种向量。
- 第 9 讲、第 23 讲:这一讲切出来的 token 数量,直接决定了模型要处理的序列长度,而序列长度决定了自注意力的计算量(平方级增长)和上下文窗口的实际承载能力——分词切得越碎,同样一段话占用的"名额"就越多。
- 第 22、24 讲:推理阶段的延迟和成本,几乎总是按 token 数量计价、计时的——这一讲讲的切分方式,直接决定了处理同一段内容的实际开销,尤其是跨语言场景下开销并不均等。
七、本讲小结
- 词表大小必须固定(第 5 讲:softmax 输出维度不能变),但世界上的词是无限的——分词要解决的就是"怎么用有限的单元覆盖无限的文本"。
- 按词切:词表会趋于无限增长,且没见过的词只能报废成
<UNK>,信息全部丢失——和 n-gram 的数据稀疏问题(第 4 讲)是同一类死穴。 - 按字符切:序列长度暴涨,代价随第 9 讲讲的自注意力平方级放大;且单字符几乎不携带语义,“组词"的负担从预处理转移到了模型内部。
- ⭐ BPE:从最细粒度出发,反复合并语料里出现最频繁的相邻单元,直到词表达到预设大小——用数据自己的统计规律,在"细粒度、无 OOV"和"粗粒度、省 token"之间找一个折中点。本讲手算的例子完整展示了两轮合并的全过程:高频组合(“ab”)被压缩成一个 token,低频组合保持原子拆分,任何新字符串依然可以被已有词表拼出来。
- ⚠️ BPE 只按频率合并,不懂语义;token 数量和"字数/词数"不能直接换算;不同语言因为训练语料占比不均,实际的分词效率也不均衡,这是设计选择的后果,不是数学必然。
- 现代多语言模型倾向于用字节级 BPE——在固定 256 种字节上做合并,天然覆盖任何语言和符号,这个思路比具体的词表大小数字更值得记住。
思考题
- 为什么"词表大小必须固定"这条约束,直接决定了"按词切"这条路走不通(提示:回忆第 5 讲 softmax 的输出维度)?
- 在第四节的手算例子里,如果语料再多一条
"ac" 出现 10 次(拆成a c _),重新算第一轮的相邻对频率,最高频的组合还会是(a,b)吗?动手算一遍。 - BPE 合并规则是"哪对相邻单元出现最多就合并哪对”——请举一个例子(可以是中文或英文),说明这种纯频率驱动的规则,可能会把两个语义上没什么关系的片段合并到一起。
- “token 数量不等于字数”,请设想一句很短但可能被切成很多 token 的中文句子,和一句很长但可能被切成很少 token 的英文句子,说说这背后可能的原因(提示:想一想语料库里哪种语言、哪类内容出现得更频繁)。
- 字节级 BPE 用 256 种字节作为最初的最小单元,而不是某种语言的字符集——这样做为什么能让"没见过的语言/符号"也不会报废成
<UNK>(提示:任何文本在计算机里最终都是字节序列)?