兩個糟糕的極端
想像你必須自己挑詞彙表。選項一:一個字一個 token。乾淨,但光是英文就有上百萬個單字、名字和錯字——你的清單會大到誇張,而且使用者第一次寫出 unfriending 或 *Llama-3* 時,你根本沒有對應的 token。選項二:一個字元一個 token。現在清單很小(只有字母和符號),但 「internationalization」 會變成 20 個 token,序列變得很長,而長序列對模型來說既慢又貴。
子詞分詞(subword tokenization)剛好穿過針眼:常見的整字保留成單一 token(the、cat),但罕見或新出現的字就拆成可重複利用的片段(token + ization、Llama + *-* + *3*)。常見的東西很短;而且沒有任何字是真正「不認識」的,因為最壞情況下都能退回到字元或位元組。這正是每個主流大型語言模型都採用的方案。
流程图:句子被切成子词,每个子词映射成整数编号,每个编号再映射成嵌入向量。
BPE:用「合併」學出詞彙表
最常見的做法是 位元組對編碼(Byte-Pair Encoding, BPE)。你不用手寫詞彙表——而是從一大堆文字裡學出來。一開始把每個字都拆成單一字元,然後不斷找出出現最頻繁的相鄰字元對,把它黏成一個新的 token。每一次黏合都被記成一條合併規則(merge rule),這些規則照順序排成的清單就是合併表(merges table)。
- 起點:「lower」這個字是字元 l · o · w · e · r。在語料中,字元對 (e, r) 非常常見。
- 合併最頻繁的字元對 (e, r) → 「er」。記下這條規則。現在「lower」= l · o · w · er。
- 下一個最頻繁的字元對可能是 (l, o) → 「lo」。記下它。「lower」= lo · w · er。
- 持續做上千次合併,直到達到你設定的目標詞彙表大小。被記下來的這些合併,就是分詞器本身。
BPE 的每一步都合并语料中出现最频繁的那一对相邻符号。
在推論(inference)時,你對新文字依照同樣的順序做同樣的合併。因為 er 和 lo 現在是單一 token,一個分詞器從沒見過的字——比方說 lowering——依然能乾淨地切成已知的片段。這就是全部的魔法:一份有限的清單,卻有無限的覆蓋力。
WordPiece 與 Unigram:另外兩種方言
BPE 不是唯一的方案。WordPiece(BERT 使用)和 BPE 非常像,但它合併的不是最頻繁的字元對,而是最能提升訓練文字機率的那一對——一個稍微更偏統計的標準。它有個著名的標記方式:用 `##` 標出「接續」的片段,所以 playing 會變成 `play` + `##ing`。
WordPiece 改为合并似然比最高的那一对,而不只是最频繁的那一对。
Unigram的做法剛好相反。它一開始就有一份龐大的候選詞彙表,然後逐步刪減,不斷丟掉那些「拿掉後對模型機率傷害最小」的 token,直到達到目標大小。在推論時它可以考慮一個字的好幾種切法,挑出機率最高的那一種。BPE 由小往大堆,Unigram 由大往小砍——通往「實用子詞片段」這個共同目標的不同路徑。
位元組與 SentencePiece:永不卡關
如果輸入裡有表情符號、罕見漢字,或一個分詞器從沒學過的符號怎麼辦?位元組層級分詞(byte-level tokenization)一勞永逸地解決這件事:在學合併之前,先把文字當成它原始的 UTF-8 位元組(bytes)(256 種可能值)。既然任何字元都是某一串位元組,那就真的沒有任何輸入是分詞器無法表示的——不存在真正「不認識」的 token。現代 GPT 風格的分詞器就是位元組層級的 BPE,正是為了這份穩健。
SentencePiece是很受歡迎的工具包(Llama、T5 與許多多語模型使用),它用一種與語言無關的方式把 BPE 或 Unigram 打包起來。它的關鍵想法是:把輸入當成包含空格在內的原始字元串流——它把空格本身編碼成一個看得見的符號 `▁`——所以它對英文、日文或泰文都一視同仁,而這些語言對「什麼算一個詞」根本沒有共識。完全不需要針對特定語言先做斷詞。
選擇詞彙表大小
你跑多少次合併,決定了最終的詞彙表大小(vocabulary size),這是一個實實在在的取捨。較大的詞彙表代表每個 token 涵蓋更多文字,所以序列較短、模型用更少步驟就能處理完一段提示——但嵌入表(embedding table)(下一篇會講)會跟著變大,吃掉記憶體。較小的詞彙表精簡,卻把文字切成更多、更細的片段,讓序列變長。實務上的分詞器通常落在三萬二到二十五萬六千之間,作為其目標語言的甜蜜點。