昨天文章最後留了一個問題「當一段長文字送進分詞器時,底層怎麼決定在哪裡切一刀當作一個 Token 的,哪裡該合併?」今天我們來把 Token 出航前的最後半哩路走完吧,來看看它是怎麼轉成模型看得懂的向量,進到模型的世界裡。
在推論階段,BPE 會沿用訓練時學到的合併規則與優先順序。其中一種高效率的實作方式,就是透過優先佇列(Priority Queue / Max Heap)管理目前可以合併的相鄰碎片:
"lowest",先拆成最小字元 ['l', 'o', 'w', 'e', 's', 't']。e + s -> es(字典有收,丟進 Heap)o + w -> ow(字典沒收,直接忽略)e 和 s 黏成 es)。['l', 'o', 'w', 'es', 't'],這時檢查新產生的左右鄰居(如 es + t -> est),字典有收就再丟進 Heap。est、黏成 lo、黏成 low。['low', 'est'],兩者合體的 lowest 在字典裡查不到,Heap 沒有其他候選人可拿時,迴圈立刻停止!最後輸出切分結果:['low', 'est']。
碎片切好後,查字典對應編號:low 是 1042,est 是 8007,字串就此轉換成一串整數陣列:[1042, 8007]。
此時,分詞器的工作正式結束,接下來交棒給神經網路來處理。
💡 題外話:為什麼神經網路不能直接拿整數計算?
Token ID 只是個「代號」,沒有連續數值的意義。如果直接丟進神經網路算矩陣乘法,模型會誤以為編號 200 的「香蕉」是編號 100 的「蘋果」兩倍大。所以,代號還需要再換一次裝。
為了避免模型依賴 Token ID 的順序性來進行訓練,所以額外建立了一個 Embedding Matrix 來表示字典裡的 Tokens,當中每一列代表字典裡一種 Token 的特徵向量,所以這張表的大小通常會是 [字典大小, 隱藏層維度](例如 [32000, 4096],表示字典大小有 32000 種 Token,每種 Token 都以一個 4096 維的向量表示)。而所謂的 Embedding Lookup,底層其實就是單純的陣列索引(Array Indexing):
token_id = 1042
token_vector = embedding_matrix[token_id]
拿出來的這條向量,才是模型真正用來理解語意的單位。當中可以特別注意,這個 Embedding Matrix 在模型訓練的階段,是會被一起訓練的。一開始裡面的數值通常是隨機填入的小浮點數,隨著訓練迭代,裡面的數值會跟著反向傳播(Backpropagation)進行更新。訓練結束後,在 4,096 維的空間裡,意思相近的詞(如「蘋果」與「香蕉」)距離較容易會比較近。也就是因為這樣,模型得以在幾何空間中重新建立每種 Token 之間的關係,進而與原本離散的 Token ID 解耦(Decouple)。
回答一個常見疑問:「如果覺得某個開源模型的 Tokenizer 切中文不好用,能自己換一個給它嗎?」答案是:不是不行,但不能在保留原模型權重,又不做配套調整的情況下,任意換用不相容的 Tokenizer。
因為在訓練階段中,模型使用的這套字典裡,假設第 1042 列向量已經被調整為代表「蘋果」,第 8007 列固定代表「飛機」, Embedding Matrix 也是透過這本字典來學習的,所以整張矩陣跟那本字典是嚴格一對一綁定的。除非能在確保原本 Token 跟 Embedding 關係沒變的情況下更新 Tokenizer,否則,如果換了新 Tokenizer,字典裡變成「飛機」的編號是 1042,那就會發生:
1042。1042 列,拿到的卻是「蘋果」的特徵。這就是為什麼 Tokenizer 與模型權重無法拆分。只要字典變動或是直接更換字典,模型就會拿到錯誤的 Embedding,就可能對不上原本的語意了。這也是為什麼開源模型釋出時,一定會附帶 tokenizer.model 或分詞配置檔案。
下一篇,我們要正式踏入 LLM 內部,看看模型是怎麼透過這些 Embedding 向量預測下一個 Token 的。明天見~
src/bpe_model.cc)