一、先把第 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 种字节上做合并,天然覆盖任何语言和符号,这个思路比具体的词表大小数字更值得记住。

思考题

  1. 为什么"词表大小必须固定"这条约束,直接决定了"按词切"这条路走不通(提示:回忆第 5 讲 softmax 的输出维度)?
  2. 在第四节的手算例子里,如果语料再多一条 "ac" 出现 10 次(拆成 a c _),重新算第一轮的相邻对频率,最高频的组合还会是 (a,b) 吗?动手算一遍。
  3. BPE 合并规则是"哪对相邻单元出现最多就合并哪对”——请举一个例子(可以是中文或英文),说明这种纯频率驱动的规则,可能会把两个语义上没什么关系的片段合并到一起。
  4. “token 数量不等于字数”,请设想一句很短但可能被切成很多 token 的中文句子,和一句很长但可能被切成很少 token 的英文句子,说说这背后可能的原因(提示:想一想语料库里哪种语言、哪类内容出现得更频繁)。
  5. 字节级 BPE 用 256 种字节作为最初的最小单元,而不是某种语言的字符集——这样做为什么能让"没见过的语言/符号"也不会报废成 <UNK>(提示:任何文本在计算机里最终都是字节序列)?