大家在剛開始綁定 OpenAI、Anthropic 或是 Google Cloud 信用卡時,多少都有過一個想法:明明我剛才只是在對話框裡輸入了幾百個中文字,為什麼看後台帳單時,上面寫的計價單位既不是「字數」、也不是「傳輸流量(MB)」,而是一個叫 Token 的東西?甚至不知不覺,這個月的額度就被吃掉了幾十萬個 Token,到底你的錢錢都去哪了?今天就盡可能用直白的語言,認識一下這個把你錢包吃乾抹淨的少年 Token 吧!
身為軟體工程師,難免習慣電腦世界用 Byte、Int、String 說話,所以我曾經也很可愛地以為 Token 是以 Unicode 為單位(還在我同事面前吹了一頓我對 Token 的理解,現在想想還真荒謬。上次這麼尷尬大概是大學教授問我電腦用的作業系統是什麼時,我回他「什麼是作業系統?」的時候)。當你踏進大語言模型(LLM)的宇宙,你必須先認識這個宇宙的基本單位:Token。它既不是純粹的一個「字」,也不是單純的一個「字母」,更像是一個字典裡的詞,它可以是一個字,例如:「我」;它也可以是一個詞,例如:「蘋果」、「apple」。模型只能理解這個字典裡的詞,只有在這個字典裡出現的詞,對於模型而言才是有意義的。就好像你在學英文時,在一句話中看到生字,去查了字典,發現它有收錄這個字,所以你能認得;但要是遇到字典裡沒有的字呢?你可能就會試圖再把字拆成幾個你看得懂的片段(字根、字首等等)拼在一起,然後來推測意思,但當然這時理解的意思就不一定是正確的了。透過這樣的方式,你才開始有能力理解一句話。而 Token 就是這些背過或是存在字典裡的詞,也是你用來理解一句話的基本單位。
當我們談到大模型的分詞 (Tokenize) 技術時,你可能聽過這個名字:BPE(Byte-Pair Encoding)。但難道這世界上就只有一種建立分詞器 (Tokenizer) 的方法嗎?當然不是,主流分詞演算法有幾個流派:
of 和 the 在英文文章裡可能很常一起出現,但它們各自本來就是獨立常見字,硬把這兩個字黏成一個新詞 of the 毫無意義,只會浪費字典空間;相反地,New 和 York 黏在一起變成 New York(紐約),合體後的意義就大於各自單獨存在。WordPiece 只有在確認這兩個碎片 「合體後比各自存在更有價值」 時,才批准合併。那為什麼到了現代主流 LLM 全都傾向選擇 BPE 流派 呢?原因也很現實:BPE 的工程實現最乾淨穩定,而且退到底層是 256 個 UTF-8 位元組(Bytes)! 意味著不管輸入的是各國語言、罕見 emoji、還是寫錯的二進位代碼,在最差的情況下都可以退化到底層 Byte 硬拆出來,解決了 遇到「詞表外(OOV)」單字就會崩潰的問題!
難道是工程師手動幫模型 key-in 的嗎?當然不是!這本字典是模型在訓練之前,用演算法從大量文章裡「統計出來的」。非常粗淺的想像一下 BPE 演算法編字典的過程,一開始工程師會給這本字典設定一個容量上限(例如:10 萬個詞):
t 和 h 最常黏在一起,那就乾脆把 th 合併成一個新單字存進字典;接著又發現後面最常跟著 e,那就再把 the 合併進去。每次都只把「最常」相鄰出現的碎片合併成新單字,刷爆 LeetCode、聰明的你一定馬上發現這其實就是 Greedy 演算法。你肯定也想到了一個問題:「這樣不是時間複雜度可能到 $O(N^2)$ 嗎?」確實,所以也有其優化的方法,例如:我們可以透過 Max-Heap 來記錄目前出現頻率最高的 token;用 Hash Map 記錄 token 的位置在哪等等。但這就是另一個故事了,我們只需要知道,時間複雜度是可以被優化的。這就是為什麼模型的字典裡會有 "apple"、會有 "function"、會有 "import",因為在人類寫過的大量文章或程式碼中,這些詞出現的頻率實在太高了,早就收錄在字典裡了。
看到這裡可能會產生一個疑問,那為什麼不乾脆用更直覺的做法,例如:
你想得到的,歷史上的 AI 前輩們一定都想過,確實會遇到一些問題,例如:
所以,BPE 這類演算法就在這中間取了一個平衡,用有限的字典大小,盡可能涵蓋世界上的所有文本,又不會讓整句話的 Token 量太多。
稍微了解一點 Token 從哪來以及 Tokenizer 是什麼後,明天來進行個小實驗吧!(我沒想好要怎麼做就是了 XD)