模型只懂數字
在前面的階梯裡你已經知道,神經網路本質上是一疊矩陣乘法——它吃進數字向量,再吐出數字向量。它沒有任何位置可以放下字母 *q* 或單詞 *cat*。所以任何文字系統要做的第一件事,就是把人類的文字轉換成數學能咀嚼的數字。這個轉換是所有自然語言處理樸實無華的大門,而它做得好不好,決定了後面的一切。
關鍵在於,文字是*一串離散的符號*,而不是連續的量。21.5 度的氣溫本身就是一個數字;但單詞 *however* 不是。於是我們玩一個兩步走的遊戲:先把文字切成若干單位(這就是分詞 / tokenization),再透過查一張固定的清單,把每個單位映射成一個整數 ID。這些整數本身還不帶任何含義——它們只是地址。含義是後面才掛上去的:每個 ID 會去索引一張嵌入表,而這張表是網路在訓練中學出來的。
該在哪裡切?詞、字元,以及兩者各自的麻煩
最直覺的做法,是按空格切開,把每個*單詞*當作一個詞元。早期的 NLP 正是這麼做的,它也天然契合你之前見過的那些基於計數的方法——詞袋和 TF-IDF 都假設有一份乾淨的單詞清單。但詞級分詞有兩個頑固的毛病。第一,詞表會爆炸:單是英語,把複數、時態、拼寫錯誤和人名都算上,就有數百萬種詞形。第二,在測試時你*總會*遇到清單裡從沒出現過的詞——令人頭疼的「未登錄詞」問題——而純詞級模型別無選擇,只能把它當成一個統一的「未知」符號丟掉。
另一個極端,是把*字元*當作詞元。這樣詞表就很小(幾百個符號),而且永遠不會有未登錄詞——你能拼寫出任何東西。代價是序列變得非常長,而且模型得從零重新學會:字母 *c-a-t* 往往是結伴出現的。對許多書寫系統來說還更糟:中文、日文、泰文裡根本沒有空格可切,所以「直接用詞」從一開始就不是個選項。這個領域需要一條折中的路。
留意一下,這其實是一個喬裝打扮過的權衡。詞元資訊密度高,但既脆弱又龐大;字元元穩健又緊湊,卻逼出又長又底層的序列。像去掉停用詞(那些基於計數的方法最愛丟掉的 *the*、*of*、*and*)這類預處理,對統計詞頻是有意義的,但它會實實在在地傷害需要每個詞來建模流暢語言的現代模型。最終勝出的答案是:把高頻詞整個保留,把罕見詞拆成碎片。
子詞:位元組對編碼取一個折中
佔主導地位的折中方案是子詞分詞,其中最有名的配方是位元組對編碼(BPE)。這個想法妙在簡單,借自上世紀九十年代的一個資料壓縮小技巧。先把所有文字拆成單個字元。然後反覆地找出出現頻率最高的相鄰符號對,把它合併成一個新的單一符號。這樣做上幾千次,*th*、*ing*、*tion* 這類常見片段,以及像 *the* 這樣的高頻整詞,就會自然而然地結晶成各自獨立的詞元;而一個罕見詞則被留作若干更小的碎片。
start: l o w e r _ n e w e s t _ # most frequent pair is (e, s) -> merge step1: l o w e r _ n e w es t _ # next most frequent pair is (es, t) -> merge step2: l o w e r _ n e w est _ # ... after many merges: final: low er new est # 'lower' -> [low, er] 'newest' -> [new, est]
這一個想法一舉解決了前面的兩個問題。基本上不再有未登錄詞了:最壞情況下,一個怪詞會退回到它的字元甚至原始位元組,所以*任何*字串都能被編碼。而詞表保持著固定、可控的規模——通常是 3 萬到 10 萬個詞元——這個大小你在一開始就透過「決定做多少次合併」來確定。BPE 的近親(BERT 家族用的 WordPiece,以及 Unigram/SentencePiece)在*如何*挑選碎片上有所不同,但它們共享同樣的精神:在字母和單詞之間,學出一塊中間地帶。
詞表,以及通往模型輸入的完整流水線
訓練 BPE 的產物是一份詞表:一張有序的清單,給每個詞元分配一個固定的整數 ID,外加那一串合併規則。這份詞表一旦定下來就被凍結,隨模型一起發佈——編碼和解碼都是確定性的查表,而非學習。還會留幾個特殊詞元佔據保留席位,比如用來把短序列補齊的填充詞元,以及標記一段文字開頭或結尾的標記。解碼不過是反向操作:拿到模型輸出的 ID,查出它們對應的碎片,再黏回成文字。
- 正規化:若模型需要就轉小寫,修正 unicode 的怪癖,有時去掉重音——輕量的、因模型而異的清理。
- 分詞:套用學好的合併規則,把文字切成子詞詞元。
- 映射成 ID:在詞表裡查每個詞元,得到它的整數 ID。
- 填充或截斷:把序列弄成固定長度,好讓許多樣本堆疊進一個批次。
- 嵌入:每個 ID 去索引一個學出來的向量;至此網路終於有了可以運算的真實數字。
那個「從 ID 到向量」的步驟值得停下來想想,因為它直接連向本階梯接下來的幾篇指南。從數學上看,取出嵌入表的第 *i* 列,和用一個獨熱向量去乘那張表是完全等價的——整數 ID 不過是為某一列起的一個緊湊名字。出來的那些向量,就是大名鼎鼎的詞向量;word2vec 和 GloVe 是早期獨立學習它們的方法,你接下來就會遇到。現代模型則乾脆把嵌入表和其餘一切一起聯合學出來。
為什麼這個枯燥的步驟暗中要緊
我們很容易把分詞當成管道工程,然後就略過它,但它的種種選擇會滲進每一個角落。正因為詞元不是單詞,模型在需要「字元級視力」的任務上會犯難:數一個詞裡有幾個字母、把字串倒過來、或者做那種數字被尷尬切開的算術。這些往往不是深層推理的失敗——而是分詞留下的痕跡。同理,分詞器沒在其上訓練過的語言會被切成多得多的詞元,於是它們更費錢、在上下文視窗裡塞得更少,表現也可能更差。這是一個真實、可量化的公平性問題,而不是傳言。
這裡有一份值得保持的清醒。子詞分詞是一個巧妙的工程把戲,而不是一套語言理論——它根本不知道「詞素」是什麼,它找出的那些碎片(*est*、*tion*)也只是有時才對得上真正的語言單位。研究者們不斷在問:能不能乾脆把它整個去掉,直接餵給模型原始位元組?確實有少數系統這麼做,代價是序列更長。但就目前而言,BPE 式的分詞仍是那個安靜的、近乎通用的第一步——它把你的句子,變成大語言模型真正吃進去的那一串整數。