永远喜欢幼刀 2025年12月12日 23:00
筆記一 —— Tokenizer
我打算先從Tokenizer開始,因爲這比直接去生啃《Attention Is All You Need》等論文要簡單。
任何語句在LLM中都會變成一個個Token,token 可能是字符、字节或子词,而Token都有一個獨有的ID,這個ID就是由Tokenizer來的。
现代LLM还可能使用WordPiece、SentencePiece等变体,但暫時只用BPE舉例。
BPE(Byte-pair encoding)是由Philip Gage在1994年提出的算法,原本是用於數據壓縮的,但是其變種被用於LLM。
接下來開始直接寫代碼(僞代碼)吧,我不太會講,但是代碼應該會比較直觀。
(由於不能開頭直接寫空格所以只能用|了)
{ Key, Id } Vocab
int WordSeq[]
int[2] PairFreq[]
Function BPE_Block(char* buffer):
| for c in buffer:
| | if c not in Vocab:
| | | add_to_vocab(c)
| | add_to_wordseq(get_id(c))
| for w in WordSeq[0..len-1]:
| | inc_pair(w, w+1)
|
| move_last_word_to_first()
| free_wordseq_without_first()
Function BPE():
| while Vocab.size < target and still_has_pairs():
| | for block in ctx:
| | | BPE_Block(block)
| | add_to_vocab(get_key(get_most_pair()[0])+get_key(get_most_pair()[1]))
| | free_pairfreq()
我的思路是按塊處理語料,然後多次處理語料,處理完一次語料合并一次token,也就是每轮重新遍历语料块统计pair,更新vocab,這樣可以在有限内存中處理大語料了。
運行的效果大致是這樣的:
aaabdaaabac
1:vocab:{a:0, b:1, d:2, c:3}
2:vocab:{a:0, b:1, d:2, c:3, aa:4}
3:vocab:{a:0, b:1, d:2, c:3, aa:4, aaa:5}
4:vocab:{a:0, b:1, d:2, c:3, aa:4, aaa:5, aaab:6}
...
這會產生字詞,且大多數情況會有意義,例如playing、played可能會變成pl、ay、play、ing、ed。
BPE算法的好處就是可以生成不在語料中的單詞,加入語料中沒有unplayable,但是有un、play、able,模型就能生成unplayable。
還有一個好處就是可以很好的處理多語言,將上述僞代碼中的c以utf-8的字符為最小單位則就可以。
至於實際C代碼等我寫好再以照片的形式貼上來吧。
(但願有人一起討論)