探秘Transformer系列之(6)--- token
探秘Transformer系列之(6)--- token
0x00 概述
語言是人類特有的概念。作為一個抽象符號,人是可以理解每個語言單詞的意義的,但是現(xiàn)在的NLP語言模型無法直接的從感知中抽象出每個語言符號的意義。為了讓模型能夠理解自然語言文本,需要把文本轉(zhuǎn)換為模型可以理解的表征,比如整數(shù)、浮點數(shù)或者向量。
從前面章節(jié)我們可以知道,Transformer接受的是高維向量(word embedding),而從文本到向量的轉(zhuǎn)換分為兩個階段:分詞和embedding化,分別產(chǎn)出token和word embedding。在構(gòu)建大模型的過程中,token 分詞與word embedding扮演著舉足輕重的角色。它們不僅是模型理解文本語言的基礎(chǔ),還深刻影響著模型的性能與精度。本篇會介紹如何做好單詞到數(shù)字的映射,下一篇介紹如何從數(shù)字轉(zhuǎn)換到embedding。
以機(jī)器翻譯(中譯英)為例,我們來概述下一段輸入文本在轉(zhuǎn)化為embedding之前都經(jīng)歷了什么(以及開發(fā)者做了哪些準(zhǔn)備),具體流程如下:
-
建立中文詞表和英文詞表。此步驟會把語料庫中的文字去重、然后排序之后就可以得到詞匯表。每個字符在詞表中有唯一序號。
-
整理文本。此步驟會把段落等裁剪成短文本。
-
分詞。將連續(xù)的文本拆分/轉(zhuǎn)換成一個個獨(dú)立的最小語義單元。生成的每個單元稱為token。具體又包括如下幾步:
- 規(guī)范化(Normalization)。對文本進(jìn)行必要的清理工作,例如清理特殊符號、格式轉(zhuǎn)換、過濾停用詞、Unicode標(biāo)準(zhǔn)化等)。
- 預(yù)分詞(Pre-tokenization)。將輸入拆分為單詞。
- 模型處理。把預(yù)分詞器得到的單詞送進(jìn)分詞模型進(jìn)行分詞,得到token序列。
- 后處理(Post-processing)。添加分詞器的特殊token,生成注意力掩碼等。
此時token僅僅是字符串,還不能直接用于模型的計算。
-
索引化。通過詞表將token轉(zhuǎn)換成計算機(jī)更容易處理的數(shù)字信息。每個token都會去詞表中查詢,得到該token在詞表中的序號,這個序號就是每個token對應(yīng)的one-hot向量。比如我們可以得到“Hello how are U today”這句話的序號編碼為[5, 17, 7, 12, 15]。
總之,經(jīng)過上述操作之后,每個詞匯都被表示為一個整數(shù),一串文本就變成了一串整數(shù)組成的向量。然后這個整數(shù)向量列表會被轉(zhuǎn)換為更具表達(dá)能力的向量形式,以便能夠捕獲詞匯之間的語義關(guān)系和特征信息。上面流程在下圖中也有對應(yīng)的展示。

0x01 基礎(chǔ)概念
1.1 分詞
顧名思義,分詞是把一句話分成一個個詞。比如按標(biāo)點符號分詞 ,或者按語法規(guī)則來分詞。Transformer接受的處理是向量,而用戶的輸入是文本句子,因此需要首先對句子進(jìn)行分詞或者說符號化(tokenize),然后才能生成向量。分詞會將輸入文本拆分為模型或機(jī)器可以處理的小單位,保證各個單位擁有相對完整和獨(dú)立的語義,并且給每個單元分配一個唯一的標(biāo)識符,這個標(biāo)識符我們稱之為token(標(biāo)識符/詞元)。比如對于句子“我吃了一個蘋果”,分詞產(chǎn)生的結(jié)果就是一個列表["我","吃了","一個","蘋果"],列表每個元素就是一個token。
另外,直接輸入完整字符會導(dǎo)致信息丟失或復(fù)雜性增加,而分詞能保持語義、減少稀疏性,最終提高模型的訓(xùn)練效率和預(yù)測準(zhǔn)確性。
1.2 token
token(詞元):token是分詞的結(jié)果,也就是最小語義單位,是模型有效理解和管理的單元。token既可以是一個單詞、一個漢字,也可能是一個表示空白字符、未知字符、句首字符等特殊字符。
1.3 tokenizer
tokenizer(分詞器)的作用就是把原始數(shù)據(jù)集中的語料按照一定的規(guī)則分開,找出具備語義的字符串轉(zhuǎn)換成token,然后把每個token映射到一個整數(shù)上。
分詞的本質(zhì)其實就是一個字符到數(shù)字的映射,因此tokenizer 需要維護(hù)一個類似字典的模塊或者功能,該功能或者模塊有如下常見方式:基于詞典的分詞方式、基于統(tǒng)計的分詞方式和基于深度學(xué)習(xí)的分詞方式。目前LLM主要是基于詞表的分詞方式。
1.4 詞表
詞表是指LLM能夠理解和識別的唯一單詞或token的集合,是一個由token與數(shù)字組成的一個字典(hashmap),用來定義token與整數(shù)之間的映射關(guān)系。該整數(shù)就是此token在詞表中的唯一序號,整數(shù)的最大范圍是詞表大小。
詞表的基本思想是基于詞典匹配,即將待分詞的中文文本根據(jù)一定規(guī)則切分和調(diào)整,然后跟詞典中的詞語進(jìn)行匹配,匹配成功則按照詞典的詞進(jìn)行分詞,匹配失敗則調(diào)整或重新選擇。在訓(xùn)練模型之前是需要構(gòu)建好詞表的。
下圖是哈佛詞表的樣例。

1.6 分詞流程
分詞流程主要分為如下幾個階段:標(biāo)準(zhǔn)化、預(yù)分詞、模型處理和后處理。我們一一介紹。
規(guī)范化
規(guī)范化是指對文本進(jìn)行標(biāo)準(zhǔn)化處理,主要包括以下幾個方面:
- 文本清洗。比如去除無用字符(比如特殊字符、非打印字符等)和額外空白(多余的空格、制表符、換行符等),只保留對分詞和模型訓(xùn)練有意義的內(nèi)容。
- 標(biāo)準(zhǔn)化寫法。比如統(tǒng)一大小寫和數(shù)字標(biāo)準(zhǔn)化(將所有數(shù)字替換為一個占位符或特定的標(biāo)記,以減少模型需要處理的變量數(shù)量)。
- 編碼一致性。比如確保文本采用統(tǒng)一的字符編碼,處理或轉(zhuǎn)換特殊字符和符號。
- 語言規(guī)范化。比如詞形還原(Lemmatization)和詞干提?。⊿temming)。
示例如下:
from tokenizers import normalizers
from tokenizers.normalizers import NFD, StripAccents
normalizer = normalizers.Sequence([NFD(), StripAccents()])
normalizer.normalize_str("Héllò h?w are ü?")
預(yù)分詞
預(yù)分詞是指在將文本分割成 token 之前的預(yù)處理步驟。具體是基于一些簡單的規(guī)則(如空格和標(biāo)點)進(jìn)行初步的文本分割,得到更小的單元(比如單詞)。對于英文就是按照空格進(jìn)行切分,把文本拆分成單詞,最終的token將是這些單詞的一部分。中文因為沒有空格分割,所以一般不需要此步驟。
比如,為了優(yōu)化多語言的壓縮效率,DeepSeek-V3 對預(yù)分詞器(Pretokenizer)和訓(xùn)練數(shù)據(jù)進(jìn)行了專門的調(diào)整。與 DeepSeek-V2 相比,新的預(yù)分詞器引入了將標(biāo)點符號和換行符組合成新 token 的機(jī)制。這種方法可以提高壓縮率,但也可能在處理不帶換行符的多行輸入時引入 token 邊界偏差(Token Boundary Bias)。為了減輕這種偏差,DeepSeek-V3 在訓(xùn)練過程中以一定概率隨機(jī)地將這些組合 token 拆分開來,從而讓模型能夠適應(yīng)更多樣化的輸入形式,提升了模型的魯棒性。
模型處理
把預(yù)分詞器得到的單詞送進(jìn)分詞模型或者依據(jù)詞匯表進(jìn)行分詞。具體是在Pre-tokenization的基礎(chǔ)上,根據(jù)選定的模型或算法(BPE,WordPiece,Unigram或SentencePiece等)進(jìn)行更細(xì)致的處理,包括通過大量文本數(shù)據(jù),根據(jù)算法規(guī)則生成詞表, 然后依據(jù)詞表,將文本拆分為Token。
后處理
后處理階段是對編碼后的文本進(jìn)行一些額外的處理步驟,以確保編碼后的文本符合特定模型的輸入要求。后處理主要包括:
- 序列填充與截斷:為保證輸入序列的長度一致,對過長的序列進(jìn)行截斷,對過短的序列進(jìn)行填充。
- 特殊Token添加:根據(jù)模型需求,在序列的適當(dāng)位置添加特殊Token(如[CLS], [SEP])。
- 構(gòu)建注意力掩碼:對于需要的模型,構(gòu)建注意力掩碼以區(qū)分實際Token和填充Token。
0x02 詞表
詞表是指LLM能夠理解和識別的唯一單詞或token的集合。我們接下來先使用哈佛代碼來看看如何構(gòu)建、使用詞表。此流程比較復(fù)雜,因此繪制下圖進(jìn)行輔助分析。

2.1 構(gòu)建詞表
前文簡述了如何加載詞表,這里看看如何構(gòu)建詞表。build_vocabulary()函數(shù)會從數(shù)據(jù)集中通過迭代器來讀取數(shù)據(jù),最終返回英語詞典和德語詞典,兩個詞典都是Vocab對象。
def build_vocabulary(spacy_de, spacy_en):
# 德語分詞方法
def tokenize_de(text):
return tokenize(text, spacy_de)
# 英語分詞方法
def tokenize_en(text):
return tokenize(text, spacy_en)
# 調(diào)用datasets.Multi30k得到三個Iterator
print("Building German Vocabulary ...")
train, val, test = datasets.Multi30k(language_pair=("de", "en"))
# 調(diào)用PyTorch函數(shù)build_vocab_from_iterator()來構(gòu)建德語詞典
vocab_src = build_vocab_from_iterator(
# 使用分詞器從三個Iterator之中獲取token
yield_tokens(train + val + test, tokenize_de, index=0),
min_freq=2,
specials=["<s>", "</s>", "<blank>", "<unk>"],
)
print("Building English Vocabulary ...")
train, val, test = datasets.Multi30k(language_pair=("de", "en"))
vocab_tgt = build_vocab_from_iterator(
yield_tokens(train + val + test, tokenize_en, index=1),
min_freq=2,
specials=["<s>", "</s>", "<blank>", "<unk>"],
)
# 設(shè)置缺省index為"<unk>",分詞器無法識別的單詞會被歸為`<unk>`
vocab_src.set_default_index(vocab_src["<unk>"])
vocab_tgt.set_default_index(vocab_tgt["<unk>"])
return vocab_src, vocab_tgt # 返回德語詞典和英語詞典
tokenize_en()函數(shù)是英語分詞函數(shù),其會調(diào)用傳入的分詞器參數(shù)對語句進(jìn)行分詞。tokenize_de()函數(shù)是德語分詞函數(shù),與tokenize_en()類似。
def tokenize(text, tokenizer):
"""
功能:調(diào)用分詞模型tokenizer對text進(jìn)行分詞
示例:tokenize("How are you", spacy_en)
返回:['How', 'are', 'you']
"""
return [tok.text for tok in tokenizer.tokenizer(text)]
build_vocabulary()函數(shù)中使用了build_vocab_from_iterator()函數(shù),該函數(shù)的作用是:
- 統(tǒng)計數(shù)據(jù)集中詞語的頻率。
- 依據(jù)頻率對詞語進(jìn)行排序。
- 如果有限定max_tokens,則需要將特殊單詞排除。
- 按照詞語頻率將詞語寫入詞匯表。
- 返回一個Vocab對象。
具體代碼如下。
def build_vocab_from_iterator(
iterator: Iterable, # 迭代器,里面是分好的詞
min_freq: int = 1, # 當(dāng)某個單詞出現(xiàn)的頻率大于min_freq,才會被加入詞典
specials: Optional[List[str]] = None, # 一個包含特殊標(biāo)記(special tokens)的列表。這些特殊標(biāo)記在構(gòu)建詞匯表時會被添加到詞匯表中,例如`<unk>`(未知單詞)、`<pad>`(填充)、`<bos>`(句子開始)和`<eos>`(句子結(jié)束)。
special_first: bool = True, # 如果special_first為True,則特殊單詞會加入到詞典最前面specials
max_tokens: Optional[int] = None,
) -> Vocab:
"""
Build a Vocab from an iterator.
Args:
iterator: Iterator used to build Vocab. Must yield list or iterator of tokens.
min_freq: The minimum frequency needed to include a token in the vocabulary.
specials: Special symbols to add. The order of supplied tokens will be preserved.
special_first: Indicates whether to insert symbols at the beginning or at the end.
max_tokens: If provided, creates the vocab from the `max_tokens - len(specials)` most frequent tokens.
Returns:
torchtext.vocab.Vocab: A `Vocab` object
Examples:
>>> #generating vocab from text file
>>> import io
>>> from torchtext.vocab import build_vocab_from_iterator
>>> def yield_tokens(file_path):
>>> with io.open(file_path, encoding = 'utf-8') as f:
>>> for line in f:
>>> yield line.strip().split()
>>> vocab = build_vocab_from_iterator(yield_tokens(file_path), specials=["<unk>"])
"""
# counter表示詞匯表中的單詞及其頻次信息。其中鍵是詞匯表中的單詞,值是對應(yīng)的單詞頻次統(tǒng)計。
counter = Counter()
for tokens in iterator
counter.update(tokens)
specials = specials or []
# First sort by descending frequency, then lexicographically
sorted_by_freq_tuples = sorted(counter.items(), key=lambda x: (-x[1], x[0]))
"""
實際變量打印如下:
[('a', 32828), ('.', 28591), ('A', 18068), ('in', 15365), ('the', 10268), ('on', 8311), ('is', 7796), ('and', 7647), ('man', 7628), ('of', 7106), ('with', 6370), (',', 4102), ('woman', 3970), ('are', 3862), ('to', 3243), ('Two', 3233), ('at', 3011), ('wearing', 2706), ('people', 2641), ('shirt', 2405), ('white', 2292), ('young', 2147), ('black', 2063), ('his', 2041), ('an', 2023), ('while', 2003), ('blue', 1944), ('red', 1799), ('sitting', 1799), ('girl', 1722), ('dog', 1706), ('boy', 1690), ('men', 1675), ('standing', 1672),...
"""
if max_tokens is None:
ordered_dict = OrderedDict(sorted_by_freq_tuples)
else:
ordered_dict = OrderedDict(sorted_by_freq_tuples[: max_tokens - len(specials)])
word_vocab = vocab(ordered_dict, min_freq=min_freq, specials=specials, special_first=special_first)
return word_vocab
構(gòu)建詞匯表需要對數(shù)據(jù)集進(jìn)行分詞。yield_tokens()函數(shù)完成了這個功能。
def yield_tokens(data_iter, tokenizer, index):
"""
從iterator之中獲取句子,調(diào)用分詞器進(jìn)行分詞,返回一個token列表。
比如,從data_iter之中得到了from_to_tuple為:
('Zwei junge wei?e M?nner sind im Freien in der N?he vieler Büsche.', 'Two young, White males are outside near many bushes.')
如果index為0,則說明對德語進(jìn)行分詞,返回德語token列表,如果index為1,則說明對英語進(jìn)行分詞,返回英語token列表
"""
for from_to_tuple in data_iter:
yield tokenizer(from_to_tuple[index])
2.2 使用詞表
詞典如何使用?在collate_batch()函數(shù)中會把詞表傳入進(jìn)去,我們進(jìn)入此函數(shù)來繼續(xù)深挖。collate_batch()函數(shù)是DataLoader類的collate_fn (Callable, optional)參數(shù),其作用是將一個樣本列表組合成一個張量的mini-batch。DataLoader內(nèi)部會將”句子對的列表“傳給collate_batch()函數(shù)來處理,然后把輸入的batch發(fā)給模型。
def collate_batch(
batch, # 句子對的列表。比如[(源句子1, 目標(biāo)句子1),(源句子2, 目標(biāo)句子2),.....],列表大小為batch size
src_pipeline, # 德語分詞功能,即spacy_de的封裝器
tgt_pipeline, # 英語分詞功能,即spacy_en的封裝器
src_vocab, # 德語詞典,Vocab對象
tgt_vocab, # 英語詞典,Vocab對象
device,
max_padding=128, # 句子最大長度
pad_id=2,
):
# <bos>和<eos>在詞典中的index
bs_id = torch.tensor([0], device=device) # <s> token id
eos_id = torch.tensor([1], device=device) # </s> token id
for (_src, _tgt) in batch: # 遍歷句子對列表
# 首先調(diào)用src_vocab(src_pipeline(_src))對源句子處理,具體是利用分詞器src_pipeline和詞表src_vocab把句子轉(zhuǎn)換為詞表index的序列;其次調(diào)用torch.cat在句子前面加上<bos>,句子后面加上<eos>。
processed_src = torch.cat(
[
bs_id,
torch.tensor(
src_vocab(src_pipeline(_src)), # 這里調(diào)用詞表
dtype=torch.int64,
device=device,
),
eos_id,
],
0,
)
# 首先調(diào)用tgt_vocab(tgt_pipeline(_tgt))對源句子處理,具體是利用分詞器tgt_pipeline和詞表tgt_vocab把句子轉(zhuǎn)換為詞表index的序列;其次調(diào)用torch.cat在句子前面加上<bos>,句子后面加上<eos>。
processed_tgt = torch.cat(
[
bs_id,
torch.tensor(
tgt_vocab(tgt_pipeline(_tgt)), # 這里調(diào)用詞表
dtype=torch.int64,
device=device,
),
eos_id,
],
0,
)
collate_batch()函數(shù)實際上調(diào)用到了Vocab對象的forward()函數(shù),該函數(shù)返回token列表所對應(yīng)的index列表,這樣就可以句子轉(zhuǎn)換為詞表索引的序列。
class Vocab(nn.Module):
@torch.jit.export
def forward(self, tokens: List[str]) -> List[int]:
r"""Calls the `lookup_indices` method
Args:
tokens: a list of tokens used to lookup their corresponding `indices`.
Returns:
The indices associated with a list of `tokens`.
"""
"""
從下面的數(shù)據(jù)結(jié)構(gòu)之中,可以管窺。
vocab = {Vocab: 8185} <torchtext._torchtext.Vocab object at 0x0000021A26983DF0>
0000 = {str} '<s>'
0001 = {str} '</s>'
0002 = {str} '<blank>'
0003 = {str} '<unk>'
0004 = {str} '.'
0005 = {str} 'Ein'
0006 = {str} 'einem'
"""
return self.vocab.lookup_indices(tokens)
2.3 詞表大小
在自然語言處理模型中,確定合適的詞匯表大小是一個關(guān)鍵步驟,它直接影響模型的性能、效率以及適應(yīng)性。事實上,較新的SLM(Small Language Model)詞匯表通常超過50,000個單詞或token。下圖給出了從2022年到2024年間,SLM詞匯表的變化趨勢??梢钥吹?,在總體趨勢上,詞表大小呈現(xiàn)增長趨勢。詞匯表的擴(kuò)大使模型能夠處理更廣泛的語言,并提供更準(zhǔn)確和更全面的響應(yīng)。

下圖則給出了部分LLM的詞表大小。

任務(wù)相關(guān)
理想的詞匯表應(yīng)該在保證模型性能和效率的同時,滿足特定任務(wù)和數(shù)據(jù)集的需求。不同的自然語言處理任務(wù)可能需要不同大小的詞匯表。例如,精細(xì)的文本生成任務(wù)可能需要較大的詞匯量以覆蓋更多細(xì)節(jié),而一些分類任務(wù)則可能只需較小的詞匯表即可達(dá)到較高性能。這都需要在設(shè)置詞匯表時考慮,比如。
-
領(lǐng)域特定詞匯:如果特定領(lǐng)域包含大量專業(yè)術(shù)語和行話,而這些詞匯在通用詞表中可能不存在或不夠豐富,那么詞表擴(kuò)增通常是有必要的。這有助于模型更準(zhǔn)確地理解和生成領(lǐng)域相關(guān)的文本,從而提升在特定任務(wù)上的性能。另外,在專業(yè)領(lǐng)域,某些詞匯可能有特定的含義,擴(kuò)增詞表有助于減少模型在理解這些詞匯時的歧義,并提高對領(lǐng)域文本的理解和生成能力。
-
數(shù)據(jù)集中文本的復(fù)雜性和多樣性也影響詞匯表的設(shè)置,豐富多變的數(shù)據(jù)集可能需要更大的詞匯量來捕獲文本的多樣性。不同語言的結(jié)構(gòu)差異意味著對詞匯表的需求也不同。例如,拼接語(如德語)可能需要更大的詞匯量來覆蓋其豐富的復(fù)合詞形態(tài)。
-
分詞策略非常重要,不同的分詞方式會導(dǎo)致模型對數(shù)值的理解差異。比如那個著名問題:embedding模型是否可以判斷9.11 比 9.9哪一個更大?研究人員給出了各種猜測,包括預(yù)訓(xùn)練數(shù)據(jù)的構(gòu)成和模型架構(gòu)本身。而詞匯表的設(shè)計就會直接影響數(shù)字的表示。例如,如果詞匯表只包括 0-9 的數(shù)字,那么 11 可能被分為單獨(dú)的 1 和 1,而非作為整體的 11。
另外,論文 Rethinking LLM Language Adaptation: A Case Study on Chinese Mixtral指出,雖然擴(kuò)展詞表能夠顯著提升目標(biāo)語言的編解碼效率,但并不意味著一定會提升下游任務(wù)效果,甚至可能會對下游任務(wù)效果產(chǎn)生負(fù)面影響。而且,詞表擴(kuò)增可能會增加模型的計算和存儲成本。
因此,在實際操作中,可以通過分析數(shù)據(jù)集的特點,針對性地擴(kuò)充詞表,從而提高模型的性能。有時候,簡單的詞表截斷或者使用基于規(guī)則的方法來處理領(lǐng)域特定詞匯也可以取得不錯的效果。最佳的詞表擴(kuò)增策略會因特定任務(wù)和領(lǐng)域的需求而不同,建議根據(jù)具體情況進(jìn)行評估和實驗??偟膩碚f,領(lǐng)域模型詞表擴(kuò)增是一個值得考慮的策略,它可以在保持模型通用能力的同時,提升模型在特定領(lǐng)域的性能。但是,是否擴(kuò)增詞表應(yīng)該基于對領(lǐng)域數(shù)據(jù)的詳細(xì)分析和對模型性能需求的準(zhǔn)確理解。
優(yōu)勢
較大的詞匯表可以提高模型覆蓋不同詞匯和表達(dá)的能力,有助于模型更好地理解和生成文本;論文"Scaling Laws with Vocabulary: Larger Models Deserve Larger Vocabularies"就探討了大型語言模型(LLMs)的詞表大小對模型性能的影響,其結(jié)論是大模型的詞表大小同樣適用于Scaling Law,并且強(qiáng)調(diào)了在設(shè)計和訓(xùn)練 LLMs 時,需要綜合考慮模型參數(shù)、訓(xùn)練數(shù)據(jù)和詞表大小。關(guān)于詞表大小 V 對語言模型的性能的影響,論文的具體結(jié)論如下:
- 增加詞表大小可以提高標(biāo)記化分詞的效率,也就是用更短的詞元去表示文本,從而提高模型性能。
- 更大的模型應(yīng)該配備更大的詞表。因為隨著模型計算量的增加,更大的詞表大小增強(qiáng)了模型理解更多樣化文本的能力,也可以讓模型表達(dá)更復(fù)雜的語言模式。
- 當(dāng)詞表大小達(dá)到一定程度之后,逐漸增加詞表大小所帶來的分詞效率收益會逐漸減少,且可能導(dǎo)致詞表有關(guān)參數(shù)的欠擬合,特別是針對低頻詞的詞表征。
綜上所述,詞表大小對模型擴(kuò)展至關(guān)重要。
論文也提出3 種預(yù)測最優(yōu)詞表大小的方法 (基于 FLOPs 的、基于導(dǎo)數(shù)的和基于損失函數(shù)參數(shù)擬合的估計方法),并且列出了當(dāng)前主流的大型語言模型(LLMs)的詞表參數(shù)和預(yù)測最優(yōu)詞表參數(shù)的關(guān)系。下圖這張表列出了對于不同大小模型理論上最優(yōu)的詞表大小。

當(dāng)前大多數(shù) LLMs 的詞表參數(shù)由于詞表尺寸小于預(yù)測的最優(yōu)值而處于次優(yōu)狀態(tài)。 例如下圖所示,預(yù)測 Llama2-70B 的最優(yōu)詞表大小應(yīng)該是至少 216K,遠(yuǎn)大于其實際的 32K。近來,社區(qū)開始轉(zhuǎn)向更大的詞匯量,例如Llama3的詞匯量從Llama2的32K增加到128K。然而,擴(kuò)展數(shù)據(jù)仍然是最關(guān)鍵的部分,解決數(shù)據(jù)稀缺問題應(yīng)該是未來工作的重點。

該論文還發(fā)現(xiàn),在給定算力的情況下,最優(yōu)的詞表大小是有上限的。通過在不同 FLOPs 預(yù)算下訓(xùn)練 3B 參數(shù)的模型驗證了這些預(yù)測,發(fā)現(xiàn)僅僅把原始詞表的大小替換成預(yù)測的最優(yōu)詞表大小,就可以提高模型在多個下游任務(wù)的性能。
劣勢
然而過大的詞表也有問題:
- 過大的詞匯表可能導(dǎo)致某些token訓(xùn)練不足。因為大詞匯表意味著更稀疏的token分布(詞嵌入空間的稀疏性問題)和更細(xì)粒度的token切分,這必然會導(dǎo)致更多低頻token和無意義的token殘片,進(jìn)而導(dǎo)致模型在某些較少見的Token上的訓(xùn)練不足(under-trained),難以有效學(xué)習(xí)到這些Token的表示,影響其泛化能力。論文"Fishing for Magikarp: Automatically Detecting Under-trained Tokens in Large Language Models"對LLM中訓(xùn)練不足的token(這些“訓(xùn)練不足”的token會導(dǎo)致模型產(chǎn)生異常輸出)進(jìn)行了檢測,發(fā)現(xiàn)訓(xùn)練不足能讓大模型“發(fā)瘋”的token在這些大模型上普遍存在。而詞匯表較大的模型,“訓(xùn)練不足”token的數(shù)量也會明顯增多。因此該論文提出優(yōu)化詞匯表結(jié)構(gòu)和tokenizer算法是解決token訓(xùn)練不足問題的關(guān)鍵。
- 詞匯表的大小也會影響模型的處理速度。在資源有限的環(huán)境下,較大的詞匯表意味著模型需要更多的計算資源來處理存儲分詞嵌入,也會在生成輸出時產(chǎn)生計算負(fù)擔(dān),從而減慢處理速度,導(dǎo)致訓(xùn)練和推理過程變得低效。
如上所述,在實際應(yīng)用中,可能需要通過實驗和調(diào)整來找到最適合特定模型和任務(wù)的詞匯表大小。
0x03 Tokenizer
tokenizer總體上做兩件事情:
- 分詞。tokenizer將字符串切分為子詞(sub-word)token,然后將token映射到數(shù)字(詞表中的序號)。
- 切分成token是依據(jù)詞表進(jìn)行匹配的過程,如果詞表中可以查到對應(yīng)的token,就輸出token,否則輸出某個表示token不存在的特殊符號。
- 從字串映射到數(shù)字的過程被稱為tokenizer的編碼過程,從數(shù)字映射回字串稱為tokenizer的解碼過程。
- 擴(kuò)展詞表。某些tokenizer會把訓(xùn)練語料出現(xiàn)的且詞匯表中本來沒有的token或者特殊字符加入詞表。當(dāng)然用戶也可以手動添加這些token。
理想的tokenizer應(yīng)該具備如下基本特性:
- 高壓縮率。可以使用更少的token表示更多的數(shù)據(jù)。
- 訓(xùn)練友好:可以在合理時間內(nèi)完成訓(xùn)練。
- 語言無關(guān):訓(xùn)練和分詞都和某種語言特性無關(guān)。
- 無損壓縮:分詞結(jié)果應(yīng)該可以無損還原為輸入。
在LLM時代,如何設(shè)計一個兼顧通用且高效推理的Tokenizer是非常重要的事情。
3.1 分詞粒度
按切分文本的顆粒度,分詞通常分成三大類:按單詞粒度(word base)來分,按字符粒度(character base)來分和按子詞粒度來分(subword tokenization)。以英文文本“Today is sunday”為例,三種方法切分結(jié)果如下
| 顆粒度 | 切割方式 | 分詞結(jié)果 |
|---|---|---|
| 單詞 | 單詞級別分詞,英文天然可以根據(jù)空格或者標(biāo)點來分割出單詞 | [today, is, sunday, .] |
| 字符 | 字符級別分詞,以單個字符作為最小顆粒度,會窮舉所有出現(xiàn)的字符,所以是最完整的。 | [t, o, d,a,y,i, s, s,u,n,d,a,y,.] |
| 子詞 | 介于word和character之間,將word拆分為子串。子詞分詞法有很多不同方法,例如BPE,WordPiece,Unigram等等。 | [to, day,is , s,un,day, .] |
按單詞粒度
詞(Word)在NLP任務(wù)中是最常見的基礎(chǔ)單元。傳統(tǒng)構(gòu)造詞表的方法會先對各個句子進(jìn)行分詞,然后再統(tǒng)計并選出頻數(shù)最高的前N個詞組成詞表。每個詞都分配一個ID。
優(yōu)點:
- 容易保持語義。如同人類閱讀一樣,用詞作為切分粒度,可以很好地保留詞的邊界信息和完整語義。
- 容易切分句子。詞是最自然的語言單元,對于英文這種存在空格的語言天然就容易切分。
劣勢:
- 詞表過大。因為羅列出單詞的所有組合明顯比窮舉出所有字符更加困難,所以按單詞粒度來切分所構(gòu)造的詞典太過龐大,嚴(yán)重影響計算效率和消耗內(nèi)存。
- 容量有限。出于計算效率的考慮,通常N的選取無法包含訓(xùn)練集中的所有詞。這會導(dǎo)致OOV(Out Of Vocabulary, OOV)問題。而且,這種詞表長尾效應(yīng)嚴(yán)重(存在大量低頻詞語占據(jù)詞表空間),低頻詞無法得到充分訓(xùn)練,導(dǎo)致模型無法充分理解這些詞的語義。
- 難以處理語義關(guān)系。比如無法處理單詞的形態(tài)、詞綴、單復(fù)數(shù)等語義關(guān)系和泛化性。
按字符粒度
此分詞法會將單詞拆分為單個字符和特殊符號。
優(yōu)點:
- 字符數(shù)量少,5000多個中文常用字基本能組合出所有文本序列,不會產(chǎn)生OOV問題。
劣勢:
- 丟失了邊界信息導(dǎo)致無法承載單詞級別的豐富語義,增加了建模難度。比如每個字符對應(yīng)的嵌入向量會承載太多語義信息,模型很難學(xué)習(xí)到詞與詞、句子與句子之間的關(guān)系。
- 序列長度增大,增加了文本表征的成本。比如推理和訓(xùn)練都會帶來更多的計算成本,且訓(xùn)練難以收斂。
另外,雖然按字符分詞對于中文比較合理,但是在中文里,某些詞語(或者成語)才是表達(dá)語義的最小單元,站在單字視角很難獲得一個有價值的語義信息。
按子詞粒度
按子詞粒度分詞(subword tokenization)是上面兩種方法的平衡。該方法會把一個詞切成更小的一塊一塊的子詞,得到一種劃分粒度介于詞與字符之間的中間粒度表示,或者說,這些 token 可能是完整的單詞、也可能是一個單詞的一部分,這使得模型能夠在處理未知詞時仍然具有很好的泛化能力,提供了良好的靈活性和擴(kuò)展性。
它的處理原則是,常用詞應(yīng)該保持原狀,生僻詞應(yīng)該拆分成子詞以共享token壓縮空間。這樣可以較好的平衡了詞匯量、語義獨(dú)立性和語義表達(dá)能力。從而通過一個有限的詞表來解決所有單詞的分詞問題,同時盡可能將結(jié)果中 token 的數(shù)目降到最低。
比如 friendly and lovely,從詞粒度切分,會得到friendly / and / lovely,加上常見的friend和love,詞典中會得到5個詞。從子詞粒度切分,會得到friend / ly / and / love / ly,詞表中只包含4個詞。
而且,subword劃分出來的詞片段可以用來組成更大的詞。有點類似英語中的詞根詞綴拼詞法。比如可以將”looking”劃分為”look”和”ing”兩個子詞,而劃分出來的"look",”ing”又能夠用來構(gòu)造其它詞,如"look"和"ed"子詞可組成單詞"looked",因而按子詞粒度分詞方法能夠降低詞典的大小,同時對相近詞能更好地處理。
比如下面就是一個可能的詞典形式,這里的##代表這個詞應(yīng)該緊跟在前面的那個詞之后來組成一個完整的詞。
// 動詞
look
see
speck
...
// 形容詞
big
small
smart
...
// 表示時態(tài)的后綴
##ed
##ied
##ing
##s
##ies
// 表示比較級和最高級后綴
##er
##est
...
Subword 方法是目前LLM的主流切詞粒度,有三種主流算法,分別是:Byte Pair Encoding (BPE)、WordPiece 和 Unigram Language Model。
如何選擇
選擇 Tokenization(分詞)方法通常取決于多個因素,需要綜合考慮,并根據(jù)實際應(yīng)用場景做出最合適的決策。常見的因素如下:
- 任務(wù)需求。不同的任務(wù)可能需要不同的分詞粒度。
- .語言特性。不同語言的結(jié)構(gòu)決定了適合它們的分詞策略。
- 模型要求。具體到每一種模型,它們可能會根據(jù)自身的設(shè)計目標(biāo)和優(yōu)化方向選擇最適合的分詞工具。比如某些特定的分詞方法可以幫助模型更好地理解和處理未見過的詞匯。
- 上下文相關(guān)性。某些 Tokenization 方法能夠保留上下文信息,這對于理解語義非常重要。
- 計算效率。在處理大規(guī)模數(shù)據(jù)集時,分詞的速度和內(nèi)存消耗也是重要的考量因素。
- 可解釋性。在某些應(yīng)用場景中,分詞結(jié)果的可解釋性也是一個重要因素。
3.2 常見tokenizer
下表整理了部分LLM使用的tokenizer。
| LM | Tokenizer |
|---|---|
| BERT | Word-Piece |
| DistilBERT | Word-Piece |
| ALBERT | Sentence-Piece |
| RoBERTa | BPE |
| GPT-2 | BPE |
| GPT-3 | BPE |
| GPT-3.5 (ChatGPT) | BPE |
| GPT-4 | BPE |
| T5 | Sentence-Piece |
| Flan T5 | Sentence-Piece |
| XLNet | Sentence-Piece |
| BART | Word-Piece |
| Llama 1 | Sentence-Piece |
| Llama 2 | Sentence-Piece |
| Llama 3 | Tiktokenizer |
| MiniMax-01 | BPE |
| DeepSeek-V3 | BPE |
3.3 Llama3示例
因為哈佛代碼中tokenizer部分比較簡單,所以我們用Llama3中的代碼作為示例來進(jìn)行分析。
定義
Tokenizer類封裝了Tiktoken tokenizer。
class Tokenizer:
"""
Tokenizing and encoding/decoding text using the Tiktoken tokenizer.
"""
special_tokens: Dict[str, int]
num_reserved_special_tokens = 256
pat_str = r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+" # noqa: E501
def __init__(self, model_path: str):
"""
Initializes the Tokenizer with a Tiktoken model.
Args:
model_path (str): The path to the Tiktoken model file.
"""
mergeable_ranks = load_tiktoken_bpe(model_path)
num_base_tokens = len(mergeable_ranks)
special_tokens = [
"<|begin_of_text|>",
"<|end_of_text|>",
"<|reserved_special_token_0|>",
"<|reserved_special_token_1|>",
"<|reserved_special_token_2|>",
"<|reserved_special_token_3|>",
"<|start_header_id|>",
"<|end_header_id|>",
"<|reserved_special_token_4|>",
"<|eot_id|>", # end of turn
] + [
f"<|reserved_special_token_{i}|>"
for i in range(5, self.num_reserved_special_tokens - 5)
]
self.special_tokens = {
token: num_base_tokens + i for i, token in enumerate(special_tokens)
}
self.model = tiktoken.Encoding(
name=Path(model_path).name,
pat_str=self.pat_str,
mergeable_ranks=mergeable_ranks,
special_tokens=self.special_tokens,
)
self.n_words: int = self.model.n_vocab
# BOS / EOS token IDs
self.bos_id: int = self.special_tokens["<|begin_of_text|>"]
self.eos_id: int = self.special_tokens["<|end_of_text|>"]
self.pad_id: int = -1
self.stop_tokens = {
self.special_tokens["<|end_of_text|>"],
self.special_tokens["<|eot_id|>"],
}
編碼
編碼過程的代碼如下。
def encode(
self,
s: str,
*,
bos: bool,
eos: bool,
allowed_special: Union[Literal["all"], AbstractSet[str]] = set(),
disallowed_special: Union[Literal["all"], Collection[str]] = (),
) -> List[int]:
"""
Encodes a string into a list of token IDs.
Args:
s (str): The input string to be encoded.
bos (bool): Whether to prepend the beginning-of-sequence token.
eos (bool): Whether to append the end-of-sequence token.
allowed_tokens ("all"|set[str]): allowed special tokens in string
disallowed_tokens ("all"|set[str]): special tokens that raise an error when in string
Returns:
list[int]: A list of token IDs.
By default, setting disallowed_special=() encodes a string by ignoring
special tokens. Specifically:
- Setting `disallowed_special` to () will cause all text corresponding
to special tokens to be encoded as natural text (insteading of raising
an error).
- Setting `allowed_special` to "all" will treat all text corresponding
to special tokens to be encoded as special tokens.
"""
# The tiktoken tokenizer can handle <=400k chars without
# pyo3_runtime.PanicException.
TIKTOKEN_MAX_ENCODE_CHARS = 400_000
MAX_NO_WHITESPACES_CHARS = 25_000
substrs = (
substr
for i in range(0, len(s), TIKTOKEN_MAX_ENCODE_CHARS)
for substr in self._split_whitespaces_or_nonwhitespaces(
s[i : i + TIKTOKEN_MAX_ENCODE_CHARS], MAX_NO_WHITESPACES_CHARS
)
)
t: List[int] = []
for substr in substrs:
t.extend(
self.model.encode(
substr,
allowed_special=allowed_special,
disallowed_special=disallowed_special,
)
)
if bos:
t.insert(0, self.bos_id)
if eos:
t.append(self.eos_id)
return t
解碼
解碼過程如下所示。
def decode(self, t: Sequence[int]) -> str:
"""
Decodes a list of token IDs into a string.
Args:
t (List[int]): The list of token IDs to be decoded.
Returns:
str: The decoded string.
"""
# Typecast is safe here. Tiktoken doesn't do anything list-related with the sequence.
return self.model.decode(cast(List[int], t))
0x04 BPE
BPE是在2015年由Google在論文[1508.07909] Neural Machine Translation of Rare Words with Subword Units中提出的一種按子詞粒度分詞方法。
4.1 思路
BPE(Byte Pair Encoding)全稱為字節(jié)對編碼,最早其實是一種數(shù)據(jù)壓縮算法,來自于一篇發(fā)表于1994年的論文:“A new algorithm for data compression”。比如有一段數(shù)據(jù)是“cddcdycdyc”,其中相鄰字母對的組合中"cd"出現(xiàn)最多,是3次。因此可以使用一個字母X去代替"cd",把數(shù)據(jù)壓縮成:"XdXyXyc"。以此類推,下一個'Xy'繼續(xù)被替換成Y,數(shù)據(jù)變成:"XdYYc"。因為沒有出現(xiàn)多次的字節(jié)對,所以"XdYYc"不能被進(jìn)一步壓縮。反向執(zhí)行以上過程即可將壓縮的編碼復(fù)原。
Google把這種方法引入到了NLP領(lǐng)域。該方案的思路是:從一個基礎(chǔ)小詞表開始,通過統(tǒng)計文本中字符對的頻率進(jìn)而不斷合并文本數(shù)據(jù)中最頻繁出現(xiàn)的字符或字符序列(高頻的連續(xù)token對),以此來產(chǎn)生新的token。BPE 可以確保最常見的詞在token列表中表示為單個token,而罕見的詞被分解為兩個或多個subword tokens,這樣可以自適應(yīng)地動態(tài)構(gòu)建詞匯表,并且可以很好地處理未知詞匯。
4.2 算法
訓(xùn)練tokenizer與訓(xùn)練模型不同。模型訓(xùn)練使用隨機(jī)梯度下降,是自然隨機(jī)化的過程。而訓(xùn)練tokenizer是一個統(tǒng)計過程,是確定性的。這種算法總是在給定語料庫中最適合選擇的子詞,這意味著在同一語料庫上使用相同的算法進(jìn)行訓(xùn)練時,總是得到相同的結(jié)果。另外,因為存在大量if-else的分支,所以tokenizer很難并行,使用GPU訓(xùn)練的利用率很低。
為了以最有效的方式構(gòu)建語料庫,BPE遵循一種貪婪的策略來盡可能取得最優(yōu)的解決方案。算法先將每個文本詞(Word)拆分成 Char粒度的字母序列,然后從字符級別開始,每一步都將出現(xiàn)頻數(shù)最大的一對“相鄰token”合并為該數(shù)據(jù)中沒有出現(xiàn)過的一個“新token”。這樣可以逐漸構(gòu)建出更長的詞匯或短語表示。這個過程反復(fù)迭代直到達(dá)到預(yù)設(shè)的詞匯表大小或合并次數(shù)為止。后期使用該方案時需要使用一個合并(merge)表來重建原始數(shù)據(jù)。貪心算法可能不一定是全局最優(yōu)、頻數(shù)也不一定是最好的合并指標(biāo),但不可否認(rèn)BPE是一個性能非常高效的Tokenizer算法、同時它能很方便將Token總數(shù)量控制在一個手動設(shè)置的數(shù)目內(nèi)。
BPE算法的主要步驟如下:
- 準(zhǔn)備足夠大的訓(xùn)練語料;設(shè)定最大分詞詞典數(shù)量。
- 準(zhǔn)備基礎(chǔ)詞表,比如英文中26個字母加上各種符號;
- 初始化:初始化一個詞典?;诨A(chǔ)詞表將將語料中所有文本切成最小單元(單個字符形式)加入詞典,并且將特殊字符也加入詞典。
- 統(tǒng)計共現(xiàn)頻率:對已經(jīng)切為字符的語料,全局統(tǒng)計所有相鄰字符對的共現(xiàn)頻率。
- 合并操作:選擇最頻繁出現(xiàn)的字符對,將它們合并為一個整體(當(dāng)作一個詞),將這個整體添加到詞典,并且在語料中將這個字符對也同步全部替換為這個新的整體。
- 重復(fù):重復(fù)上述過程,直到達(dá)到預(yù)設(shè)的合并次數(shù)或詞匯表大小,或下一個最高頻的字節(jié)對的頻率為1。
- 輸出:輸出為運(yùn)行算法得到的subword詞表。后續(xù)的任務(wù)都以這個分詞詞典來切詞和編碼。

我們再看看詞表大小。單看每一輪操作完,詞表的數(shù)量可能增大,也可能減少。但整體上,隨著合并的次數(shù)增加,詞表總數(shù)通常先增大,后逐會減少到趨于一個穩(wěn)定值。比如"loved","loving","loves"這三個單詞。其實本身的語義都是“愛”的意思,但是如果我們以單詞為單位,那它們就算不一樣的詞。在英語中不同后綴的詞非常的多,就會使得詞表變的很大,訓(xùn)練速度變慢,訓(xùn)練的效果也不是太好。BPE算法通過訓(xùn)練,能夠把上面的3個單詞拆分成"lov”,“ed”,“ing”,"es"幾部分,這樣可以把詞的本身的意思和時態(tài)分開,有效的減少了詞表的數(shù)量。
4.3 示例剖析
接下來我們用一個實例來進(jìn)行剖析。假設(shè)我們有一個語料庫,在對預(yù)語料進(jìn)行預(yù)分詞(pre-tokenization)之后,我們得到了一個原始詞集合,其中包含如下單詞:old, older, highest, 和 lowest。
統(tǒng)計頻率
我們首先計算這些詞在語料庫中的出現(xiàn)頻率。假設(shè)這些詞出現(xiàn)的頻率如下:
{"low": 5, "lowest": 2, "new": 6, "widest": 3}
初始分割
本階段的subword的粒度是字符,因此我們先將這些單詞分割成單個字符,并在每個單詞的末尾添加一個特殊的結(jié)束標(biāo)記“”來表示終止符。同時我們也標(biāo)記出該單詞出現(xiàn)的次數(shù)。例如," low"的頻率為5,那么我們將其改寫為" l o w </ w>":5。此時的子詞頻數(shù)表如下:
{'l o w </w>': 5, 'l o w e s t </w>': 2, 'n e w </w>': 6, 'w i d e s t </w>': 3}
終止符""的意義在于,它表示subword是詞后綴,這樣可以標(biāo)識單詞邊界,能夠讓算法知道每個單詞的結(jié)束位置(因為我們統(tǒng)計相鄰字符對時不能把分別位于兩個單詞中的字符對算進(jìn)去),這有助于算法查看每個字符并找到頻率最高的字符配對。稍后我們將看到“”也能被算作字符對的一部分。
另外,終止符也有助于算法理解“star”和“highest”等詞之間的區(qū)別。 這兩個詞都有一個共同的“st”,但一個詞在結(jié)尾有一個“st”,一個在開頭有一個“st”,二者意義截然不同。因此,像“st”和“st”這樣的token需要被不同地處理。 如果算法看到token “st”,它就會知道它更可能是“highest”這個詞的token,而不是“star”的。
對于英文,我們可以直接簡單地使用空格加一些標(biāo)點符號來分詞;中文可以使用jieba分詞或者直接按照字來進(jìn)行分詞。
構(gòu)建初始詞表
接下來我們構(gòu)建基礎(chǔ)詞表(base vocab) 并開始學(xué)習(xí)合并規(guī)則(merge rules)。對于英語來說,我們選擇字母來構(gòu)成基礎(chǔ)詞表。以下是初始狀態(tài)下的所有子詞,以列表形式表示,一共10個子詞,都是字母。
['l', 'o', 'w', '</w>', 'e', 's', 't', 'n', 'i', 'd']
注:這個基礎(chǔ)詞表就是我們詞表的初始狀態(tài)。雖然看起來好像使用26個英文字母就可以表示所有單詞,壓縮率很高,但是這樣的子詞基本上無法表示相應(yīng)的含義。所以,我們要使用 BPE 算法進(jìn)行迭代,讓這些初始子詞將逐漸被合并成更長,更有語義的子詞。我們會不斷構(gòu)建新詞,加進(jìn)去,直到達(dá)到我們理想的詞表規(guī)模。
循環(huán)迭代學(xué)習(xí)結(jié)合規(guī)則
BPE 算法的下一步是尋找最頻繁的字符對,合并它們,并一次又一次地執(zhí)行相同的迭代,直到達(dá)到我們預(yù)先設(shè)置的token數(shù)限制或迭代次數(shù)限制。
- 合并字符可以讓你用最少的token來表示語料庫,這也是 BPE 算法的主要目標(biāo),即數(shù)據(jù)的壓縮。
- 為了合并,BPE 尋找最常出現(xiàn)的字節(jié)對。然后將最常見的字節(jié)對合并成一個token,并將它們添加到token列表中,并重新計算每個token出現(xiàn)的頻率。因為頻率計數(shù)將在每個合并步驟后發(fā)生變化。
- 我們將繼續(xù)執(zhí)行此合并步驟,直到達(dá)到預(yù)先設(shè)置的token數(shù)限制或迭代限制。
每次迭代都做同樣事情,我們來具體看看。
第一次迭代
統(tǒng)計字符對頻率
首先根據(jù)基礎(chǔ)詞表,我們可以對原始的詞集合進(jìn)行細(xì)粒度分詞,并看到基礎(chǔ)詞的詞頻,得到如下。
('l', 'o'): 7, ('o', 'w'): 7, ('w', '</w>'): 11, ('w', 'e'): 2, ('e', 's'): 5, ('s', 't'): 5, ('t', '</w>'): 5, ('n', 'e'): 6, ('e', 'w'): 6, ('w', 'i'): 3, ('i', 'd'): 3, ('d', 'e'): 3
合并最高頻字符對
接下來我們會選擇最高頻的字符對進(jìn)行合并。例如,現(xiàn)在的最高頻率是('w', ''),我們將其合并為一個新的子詞。具體分為三步。
首先我們把'w'加進(jìn)子詞表,現(xiàn)在我們新的的子詞表如下:注意,此處去除了('w', '')內(nèi)部的空格:
['l', 'o', 'w</w>', 'e', 's', 't', 'n', 'i', 'd','w','</w>]
其次,要更新詞頻統(tǒng)計表:
('l', 'o'): 7, ('o', 'w</w>'): 5, ('o', 'w'): 2, ('w', 'e'): 2, ('e', 's'): 5, ('s', 't'): 5, ('t', '</w>'): 5, ('n', 'e'): 6, ('e', 'w</w>'): 6, ('w', 'i'): 3, ('i', 'd'): 3, ('d', 'e'): 3
最后,把詞表中的w進(jìn)行合并(消除中間的空格)。
{'l o w</w>': 5, 'l o w e s t </w>': 2, 'n e w</w>': 6, 'w i d e s t </w>': 3}
可以看出,相比上次,詞表里多了一個新的子詞'w'。每次合并后詞表可能出現(xiàn)3種變化:
- +1,表明加入合并后的新字詞,同時原來的2個子詞還保留(2個字詞不是完全同時連續(xù)出現(xiàn))。
- +0,表明加入合并后的新字詞,同時原來的2個子詞中一個保留,一個被消解(一個字詞完全隨著另一個字詞的出現(xiàn)而緊跟著出現(xiàn))。
- -1,表明加入合并后的新字詞,同時原來的2個子詞都被消解(2個字詞同時連續(xù)出現(xiàn))。
隨著合并的次數(shù)增加,詞表通常先增加后減小。因為原有詞表中的單詞逐步被合并,所以把原有單詞從詞表中驅(qū)除。在實踐中需要仔細(xì)設(shè)置迭代次數(shù)。迭代次數(shù)太小,大部分還是字母,沒什么意義;迭代次數(shù)多,又重新變回了原來那幾個詞。所以詞表大小要取一個合適的中間值。
然而現(xiàn)在詞表還是太大,我們需要進(jìn)入第二次迭代。
第二次迭代
我們再次統(tǒng)計更新后的字符對頻率:
{('l', 'o'): 7, ('o', 'w</w>'): 5, ('o', 'w'): 2, ('w', 'e'): 2, ('e', 's'): 5, ('s', 't'): 5, ('t', '</w>'): 5, ('n', 'e'): 6, ('e', 'w</w>'): 6, ('w', 'i'): 3, ('i', 'd'): 3, ('d', 'e'): 3})
這時,最高頻的字符對是('l', 'o')。因此,我們將這一對字符合并為新的子詞 lo。
更新后的子詞表如下:
['lo', 'w</w>', 'e', 's', 't', 'n', 'i', 'd', 'w','</w>']
更新后的頻數(shù)表如下:
{('l', 'o'): 7, ('o', 'w</w>'): 5, ('o', 'w'): 2, ('w', 'e'): 2, ('e', 's'): 5, ('s', 't'): 5, ('t', '</w>'): 5, ('n', 'e'): 6, ('e', 'w</w>'): 6, ('w', 'i'): 3, ('i', 'd'): 3, ('d', 'e'): 3}
更新后的詞表如下。
{'lo w</w>': 5, 'lo w e s t </w>': 2, 'n e w</w>': 6, 'w i d e s t </w>': 3}
后續(xù)迭代
我們繼續(xù)重復(fù)以上步驟,直到達(dá)到預(yù)設(shè)的詞表規(guī)?;蛘邼M足迭代條件,或者下一個最高頻的字符對出現(xiàn)頻率為 1。最終得到的子詞詞匯表如下:
['est</w>', 'new</w>', 'low</w>', 'wid', 'lo', 'w']
我們從最初的10個字母成功得到了6個子詞,這個子詞也被我們稱為token。我們也看到了在編碼的過程中,輸入的句子被打散變?yōu)橐粋€個token的過程。
注意,在上述算法執(zhí)行后,如果句子中仍然有子字符串沒被替換但所有subword都已迭代完畢,則將剩余的子詞替換為特殊token,如
小結(jié)
我們用下圖來把迭代流程做一下梳理。我們最初的token列表為['l', 'o', 'w', '', 'e', 's', 't', 'n', 'i', 'd'],一共10個token。現(xiàn)在token列表為['lo', 'new', 'w', 'wid', 'est', 'low'],一共6個token,這說明token列表被有效壓縮了。例子中我們的語料庫很小,現(xiàn)實中的語料庫會大很多,我們能通過更多的迭代次數(shù)將token列表縮小更多。

4.4 使用
編碼
BPE 的編碼過程是將單詞分割成詞表中的token的過程。在得到Subword詞表后,針對每一個單詞,我們可以采用如下的方式來進(jìn)行編碼:
- 子詞排序。將詞典中的所有子詞按照長度由大到小進(jìn)行排序,因為我們接下來會優(yōu)先匹配最長的 token,然后迭代到最短的token。
- 匹配子字串。對于單詞w,我們依次遍歷排好序的詞典。查看當(dāng)前子詞是否是w的子字符串,如果是,則使用當(dāng)前子詞替換w中的子字符串。并對w剩余的字符串繼續(xù)匹配。
- 重復(fù)直到無法匹配。最終我們將遍歷所有tokens,并且token的子字符串將被替換為我們子詞列表中已經(jīng)存在的子詞。
- 處理未匹配的子字符串。如果遍歷完字典后,仍然有子字符串沒有匹配但所有token都已迭代完畢,則將剩余字符串替換為特殊符號(如”
”)輸出。 - 單詞的表示即為上述所有輸出子詞。
基本流程如下。
for subword in subwords:
for word in words:
# 執(zhí)行替換操作
比如,我們使用上面生成的子詞表['lo', 'new', 'w', 'wid', 'est', 'low']來對一個新造的單詞 "wwidlow"進(jìn)行編碼,得到['w','wid', 'low']??梢院苤庇^的看出,一個從沒有出現(xiàn)過的新單詞被編碼成為詞表里的token,而這是傳統(tǒng)分詞方法做不到的。
解碼
解碼過程是編碼的逆過程,如果相鄰子詞間沒有中止符,則將兩子詞直接拼接,否則兩子詞之間添加分隔符,這樣可以恢復(fù)原始單詞。也可以看到,在拼接時,"< /w >"的作用就是其可以隔離開不同的單詞。舉例如下。
# 編碼序列
[“the</w>”, “high”, “est</w>”, “moun”, “tain</w>”]
# 解碼序列
“the</w> highest</w> mountain</w>”
4.5 MINBPE
我們接下來用MINBPE來看看具體實現(xiàn)。
基礎(chǔ)函數(shù)
get_stats()和merge()函數(shù)是編碼時使用到的基礎(chǔ)函數(shù)。
# 統(tǒng)計字頻
def get_stats(ids, counts=None):
"""
Given a list of integers, return a dictionary of counts of consecutive pairs
Example: [1, 2, 3, 1, 2] -> {(1, 2): 2, (2, 3): 1, (3, 1): 1}
Optionally allows to update an existing dictionary of counts
"""
counts = {} if counts is None else counts
for pair in zip(ids, ids[1:]): # iterate consecutive elements
counts[pair] = counts.get(pair, 0) + 1
return counts
# 合并子詞
def merge(ids, pair, idx):
"""
In the list of integers (ids), replace all consecutive occurrences
of pair with the new integer token idx
Example: ids=[1, 2, 3, 1, 2], pair=(1, 2), idx=4 -> [4, 3, 4]
"""
newids = []
i = 0
while i < len(ids):
# if not at the very last position AND the pair matches, replace it
if ids[i] == pair[0] and i < len(ids) - 1 and ids[i+1] == pair[1]:
newids.append(idx)
i += 2
else:
newids.append(ids[i])
i += 1
return newids
Tokenizer
Tokenizer是基類,提供了分詞器所需的基礎(chǔ)能力。
class Tokenizer:
"""Base class for Tokenizers"""
def __init__(self):
# default: vocab size of 256 (all bytes), no merges, no patterns
self.merges = {} # (int, int) -> int 對哪兩個進(jìn)行合并,以及合并之后對應(yīng)的索引
self.pattern = "" # str
self.special_tokens = {} # str -> int, e.g. {'<|endoftext|>': 100257}
# 詞典
self.vocab = self._build_vocab() # int -> bytes
def train(self, text, vocab_size, verbose=False):
# Tokenizer can train a vocabulary of size vocab_size from text
raise NotImplementedError
def encode(self, text):
# Tokenizer can encode a string into a list of integers
raise NotImplementedError
def decode(self, ids):
# Tokenizer can decode a list of integers into a string
raise NotImplementedError
def _build_vocab(self):
# vocab is simply and deterministically derived from merges
# 單字節(jié)token
vocab = {idx: bytes([idx]) for idx in range(256)}
# 字節(jié)對token
for (p0, p1), idx in self.merges.items():
vocab[idx] = vocab[p0] + vocab[p1]
# 特殊token
for special, idx in self.special_tokens.items():
vocab[idx] = special.encode("utf-8")
return vocab
def save(self, file_prefix):
"""
Saves two files: file_prefix.vocab and file_prefix.model
This is inspired (but not equivalent to!) sentencepiece's model saving:
- model file is the critical one, intended for load()
- vocab file is just a pretty printed version for human inspection only
"""
# write the model: to be used in load() later
model_file = file_prefix + ".model"
with open(model_file, 'w') as f:
# write the version, pattern and merges, that's all that's needed
f.write("minbpe v1\n")
f.write(f"{self.pattern}\n")
# write the special tokens, first the number of them, then each one
f.write(f"{len(self.special_tokens)}\n")
for special, idx in self.special_tokens.items():
f.write(f"{special} {idx}\n")
# the merges dict
for idx1, idx2 in self.merges:
f.write(f"{idx1} {idx2}\n")
# write the vocab: for the human to look at
vocab_file = file_prefix + ".vocab"
inverted_merges = {idx: pair for pair, idx in self.merges.items()}
with open(vocab_file, "w", encoding="utf-8") as f:
for idx, token in self.vocab.items():
# note: many tokens may be partial utf-8 sequences
# and cannot be decoded into valid strings. Here we're using
# errors='replace' to replace them with the replacement char ?.
# this also means that we couldn't possibly use .vocab in load()
# because decoding in this way is a lossy operation!
s = render_token(token)
# find the children of this token, if any
if idx in inverted_merges:
# if this token has children, render it nicely as a merge
idx0, idx1 = inverted_merges[idx]
s0 = render_token(self.vocab[idx0])
s1 = render_token(self.vocab[idx1])
f.write(f"[{s0}][{s1}] -> [{s}] {idx}\n")
else:
# otherwise this is leaf token, just print it
# (this should just be the first 256 tokens, the bytes)
f.write(f"[{s}] {idx}\n")
def load(self, model_file):
"""Inverse of save() but only for the model file"""
assert model_file.endswith(".model")
# read the model file
merges = {}
special_tokens = {}
idx = 256
with open(model_file, 'r', encoding="utf-8") as f:
# read the version
version = f.readline().strip()
assert version == "minbpe v1"
# read the pattern
self.pattern = f.readline().strip()
# read the special tokens
num_special = int(f.readline().strip())
for _ in range(num_special):
special, special_idx = f.readline().strip().split()
special_tokens[special] = int(special_idx)
# read the merges
for line in f:
idx1, idx2 = map(int, line.split())
merges[(idx1, idx2)] = idx
idx += 1
self.merges = merges
self.special_tokens = special_tokens
self.vocab = self._build_vocab()
BPE Tokenizer
BasicTokenizer是BPE算法的實現(xiàn)。
class BasicTokenizer(Tokenizer):
def __init__(self):
super().__init__()
def train(self, text, vocab_size, verbose=False):
assert vocab_size >= 256
num_merges = vocab_size - 256
# input text preprocessing
# 得到原始字節(jié)
text_bytes = text.encode("utf-8") # raw bytes
ids = list(text_bytes) # list of integers in range 0..255
# iteratively merge the most common pairs to create new tokens
# merges用來決定把哪些單字節(jié)合并成一個token,并且標(biāo)記一個索引
merges = {} # (int, int) -> int
# 前256個單字節(jié)token
vocab = {idx: bytes([idx]) for idx in range(256)} # int -> bytes
# 擴(kuò)充詞典
for i in range(num_merges):
# count up the number of times every consecutive pair appears
# 計算每個相鄰對出現(xiàn)的次數(shù)
stats = get_stats(ids)
# find the pair with the highest count
# 找到出現(xiàn)最多的字節(jié)對,將其構(gòu)建成為一個新的token
pair = max(stats, key=stats.get)
# mint a new token: assign it the next available id
# 給這個新token設(shè)置對應(yīng)的索引
idx = 256 + i
# replace all occurrences of pair in ids with idx
# 對ids進(jìn)行更新,即把ids中出現(xiàn)的pair替換成idx
ids = merge(ids, pair, idx)
# save the merge
merges[pair] = idx
# 更新詞匯表,新idx對應(yīng)的token就是原來詞匯表中兩個對應(yīng)字節(jié)的拼接
vocab[idx] = vocab[pair[0]] + vocab[pair[1]]
# prints
if verbose:
print(f"merge {i+1}/{num_merges}: {pair} -> {idx} ({vocab[idx]}) had {stats[pair]} occurrences")
# save class variables
self.merges = merges # used in encode()
self.vocab = vocab # used in decode()
def decode(self, ids):
# 從整數(shù)列表還原成字節(jié)字符串
# given ids (list of integers), return Python string
text_bytes = b"".join(self.vocab[idx] for idx in ids)
text = text_bytes.decode("utf-8", errors="replace")
return text
# 編碼函數(shù)
def encode(self, text):
# given a string text, return the token ids
text_bytes = text.encode("utf-8") # raw bytes
# 把字節(jié)變成整型數(shù)(0~255)的列表。中文可能對應(yīng)多個字節(jié)
ids = list(text_bytes) # list of integers in range 0..255
while len(ids) >= 2: # 遍歷ids
# find the pair with the lowest merge index
# 計算相鄰字節(jié)的頻數(shù)
stats = get_stats(ids)
# 對于stats的每個key,調(diào)用merge函數(shù),得到最小值
pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))
# subtle: if there are no more merges available, the key will
# result in an inf for every single pair, and the min will be
# just the first pair in the list, arbitrarily
# we can detect this terminating case by a membership check
# 排名最短的pair都不在merge之中,說明沒有需要合并的pair,就不需要再編碼了
if pair not in self.merges:
break # nothing else can be merged anymore
# otherwise let's merge the best pair (lowest merge index)
idx = self.merges[pair]
# 合并
ids = merge(ids, pair, idx)
# ids中所有可以合并的pair都被替代了,得到了新的ids
return ids
4.5 優(yōu)劣
優(yōu)點
BPE的優(yōu)點如下:
-
更小的詞表。BPE 可以生成一個更小的詞表。這不僅節(jié)省存儲空間,還提高了計算效率。
-
更好的泛化能力。BPE的詞匯表包含了從單個字符到較長的子詞單位,模型可以利用這些單位來表示任何輸入文本。這種方法尤其適用于處理大量的未知詞匯或拼寫錯誤的情況,因為它允許模型通過組合已知的部分來推測新的詞匯。例如,遇到一個新詞,我們可以用現(xiàn)有的子詞來表示它,而不需要為每個新詞創(chuàng)建一個新的詞條。
-
可以有效地平衡詞典大小和編碼步驟數(shù)。
劣勢
BPE的劣勢是:
- 基于貪婪算法和確定的符號替換導(dǎo)致BPE不能提供帶概率的多個分詞結(jié)果。
- 解碼的時候會面臨歧義問題。比如對于同一個句子, 例如Hello world,可能會有不同的Subword序列(Hell/o/ word和H/ello/ world)。不同的Subword序列會產(chǎn)生完全不同的id序列表示,這種歧義可能在解碼階段無法解決。導(dǎo)致在翻譯任務(wù)中,不同的id序列可能翻譯出不同的句子。
- 在訓(xùn)練任務(wù)中,如果能對不同的Subword進(jìn)行訓(xùn)練的話,將增加模型的健壯性,能夠容忍更多的噪聲,而BPE的貪心算法無法對隨機(jī)分布進(jìn)行學(xué)習(xí)。
另外,“9.9 和 9.11 到底哪個大?”這個問題,也可以從tokenizer角度來給出解釋。比如不同tokenizer 對于處理數(shù)字方法有所不同,這導(dǎo)致對語言模型中的算術(shù)性能有顯著影響。
- GPT-2 論文使用BPE將頻繁出現(xiàn)的子詞合并為單個單元,直到詞匯量達(dá)到目標(biāo)大小。然而,這種做法生成的詞匯表在很大程度上取決于輸入到 tokenizer 中的訓(xùn)練數(shù)據(jù),從而導(dǎo)致了在數(shù)字編碼方式上的不一致性。例如,在訓(xùn)練數(shù)據(jù)中常見的數(shù)字(例如 1-100、1943 年這樣的表示)很可能被表示為單個 token,而較少見到的數(shù)字則被拆分成多個 token。
- Llama 和 Llama 2 使用 SentencePiece的 BPE 實現(xiàn)對數(shù)字進(jìn)行了顯著的調(diào)整:它們將所有數(shù)字拆分為單個數(shù)字。這意味著只有 10 個唯一 token(0-9)來表示任何數(shù)字,從而簡化了 LLM 的數(shù)字表示。
- DeepSeek-V2則有一個類似的單位數(shù)(single-digit)的 tokenizer 。
- Llama 3 采用了不同的方法來處理數(shù)字,將它們 tokenizing 為三位數(shù)。因此,從 1 到 999 的數(shù)字每個數(shù)都有唯一的 token。
- 后來又出現(xiàn)了從右到左(R2L)的分詞方法,該方法以三個字符為一組,從文本的末尾開始向開頭處理。
我們可以根據(jù)問題類型優(yōu)化 tokenization 策略,從而提高 LLM 在數(shù)學(xué)任務(wù)上的表現(xiàn)。
0x05 其它算法
前面提到過,常用的subword分詞算法有如下三種:BPE、WordPiece和Unigram。本小節(jié)我們來看看WordPiece和Unigram,以及其它的子詞分類算法。
5.1 WordPiece
WordPiece算法出自論文“JAPANESE AND KOREAN VOICE SEARCH”,用于解決日語和韓語的語音問題。這種算法的字面理解是把word拆成一片一片,可以看作是BPE的變種。與BPE相比,WordPiece可以更有效地處理詞匯的變體和未知詞匯。
思想
WordPiece與BPE類似,走的是合并的思路。即從一個基礎(chǔ)小詞表出發(fā),每次詞表中選出兩個Subword合并成新的Subword,通過不斷合并來產(chǎn)生最終的詞表。主要的差別在于,BPE按頻率來選擇合并的token對,而wordpiece基于語言模型似然概率的最大值來相鄰子詞合并,它不僅計算這些組合的頻率,還考慮了合并后帶來的概率增益。也可以這樣理解,wordpiece按token間的互信息進(jìn)行合并。即如果 P(ed) 的概率比P(e) + P(d)單獨(dú)出現(xiàn)的概率更大,WordPiece就會把他們合并放入詞表。
注:互信息,在分詞領(lǐng)域有時也被稱為凝固度、內(nèi)聚度,可以反映一個詞內(nèi)部的兩個部分結(jié)合的緊密程度?;バ畔⒃酱?,兩個子詞在語言模型上就擁有越強(qiáng)的關(guān)聯(lián)性。
算法
WordPiece引入了一個假設(shè):所有subword的出現(xiàn)都是獨(dú)立的,并且subword序列由subword出現(xiàn)概率的乘積產(chǎn)生。WordPiece的算法如下:
- 準(zhǔn)備足夠大的訓(xùn)練語料,確定期望的子詞表大小。
- 準(zhǔn)備基礎(chǔ)詞表,比如英文中26個字母加上各種符號。
- 基于基礎(chǔ)詞表將語料拆分為最小單元。
- 基于第3步數(shù)據(jù)訓(xùn)練語言模型(比如unigram語言模型)。
- 從所有可能的token對中選擇加入語言模型后,能最大程度地增加訓(xùn)練數(shù)據(jù)概率的token對作為新的子詞。
- 重復(fù)第5步直到達(dá)到第2步設(shè)定的subword詞表大小或概率增量低于某一閾值。
算法的輸出是子詞表。
優(yōu)勢與劣勢
-
優(yōu)勢:可以較好的平衡詞表大小和OOV問題;
-
劣勢:可能會產(chǎn)生一些不太合理的子詞或者說錯誤的切分;對拼寫錯誤非常敏感;對前綴的支持不夠好;
5.2 UniLM
ULM出自論文 "Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates"。ULM 算法考慮了句子的不同分詞可能,因而能夠輸出帶概率的多個子詞分段。
Unigram 與 WordPiece 的相同點是:同樣使用語言模型來挑選子詞,即Unigram 也使用概率統(tǒng)計的方式來預(yù)測每個單詞作為獨(dú)立單元出現(xiàn)的概率,并基于這個概率來進(jìn)行分詞。這個過程中,某些詞可能會被拆分成更小的單元,以便模型可以更靈活地處理語言中的變化和新詞。
Unigram 與 WordPiece 的最大區(qū)別是:WordPiece 算法的詞表大小是從小到大變化。UniLM 的詞庫則是從大到小變化,可以看成是WordPiece算法在執(zhí)行過程中進(jìn)行反向操作。Unigram 先初始化一個大詞表,之后每一步根據(jù)評估準(zhǔn)則不斷丟棄詞表中的子詞(根據(jù)評估不斷刪除排序靠后的Subword),直到滿足限定條件。由于每次保留、刪除的是一批Subword,因此,Unigram 算法復(fù)雜度比WordPiece(每次合并一個)要低。
算法
- 準(zhǔn)備足夠大的訓(xùn)練語料,確定期望的子詞表大小。
- 準(zhǔn)備基礎(chǔ)詞表:初始化一個很大的詞表,比如所有字符+高頻ngram,也可以通過BPE算法初始化。
- 針對當(dāng)前詞表,用語言模型來估計每個子詞在語料上的概率。ULM算法考慮了句子的不同分詞可能,因而能夠輸出帶概率的多個分詞結(jié)果。
- 計算刪除每個subword后對總loss的影響,作為該子詞的得分。
- 將子詞按照Score大小進(jìn)行排序,保留前X%的Subword??梢?,ULM會傾向于保留那些以較高頻率出現(xiàn)在很多句子的分詞結(jié)果中的子詞,因為這些子詞如果被刪除,其損失會很大。
- 重復(fù)步驟3到5,直到詞表大小減少到設(shè)定值,或第5步的結(jié)果不再變化。
算法的輸出是的subword詞表。
優(yōu)勢與劣勢
優(yōu)勢:
- 使用的訓(xùn)練算法可以利用所有可能的分詞結(jié)果,這是通過data sampling算法實現(xiàn)的;
- 提出一種基于語言模型的分詞算法,這種語言模型可以給多種分詞結(jié)果賦予概率,從而可以學(xué)到其中的噪聲;
- 能夠自動適應(yīng)不同語言的特性,使得模型在處理多語言文本時更加高效,它在GPT-1中被使用。
劣勢:
- 效果與初始詞表息息相關(guān),初始的大詞表要足夠好,比如可以通過BPE來初始化;
- 略顯復(fù)雜。
比對
下面的表格和圖例給出了三種分詞方法的對比。
| 名稱 | BPE | WordPiece | Unigram |
|---|---|---|---|
| 選擇子詞方法 | 出現(xiàn)頻率 | 互信息(使用語言模型) | 互信息(使用語言模型) |
| 操作 | 合并子詞 | 合并子詞 | 刪除使得最大似然概率減小最小的子詞 |
| 詞表變化 | 逐步變大 | 逐步變大 | 逐步減小 |

5.3 BBPE
論文"Neural Machine Translation with Byte-Level Subwords"在基于BPE基礎(chǔ)上提出了一種新的subword算法,將BPE的思想從字符級別擴(kuò)展到子節(jié)級別,故稱之為BBPE,即Byte-level BPE。
動機(jī)
幾乎所有現(xiàn)有的機(jī)器翻譯模型都建立在基于字符的詞匯表之上:characters, subwords or words(只是字符的粒度不同)。 對于英文、拉美體系的語言來說使用BPE分詞足以在可接受的詞表大小下解決OOV的問題,然而,對于噪聲文本或字符豐富的語言(如日語和中文),其稀有字符可能會不必要地占用詞匯表并限制其緊湊性。用字節(jié)級別表示文本并使用 256 字節(jié)集作為詞匯表是解決此問題的潛在方法。 然而,高昂的計算成本阻礙了它在實踐中的廣泛部署或使用。 因此,論文作者提出了字節(jié)級子詞BBPE,它比字符詞匯表更緊湊,沒有詞匯表外的標(biāo)記,但比僅使用純字節(jié)更有效。
思想
BBPE是從UTF8編碼入手的。相比ASCII只能覆蓋英文中字符,UTF-8編碼創(chuàng)建的本身就是為了通用的將世界上不同的語言字符盡可能全部用一套編碼進(jìn)行編號,相比之下,UTF-32對于每個字符都采用4位字節(jié)(byte)則過于冗長。改進(jìn)的UTF-8編碼是一個變長的編碼,有1~4個范圍的字節(jié)(bytes)長度。對于不同語言中字符可以采用不同長度的字節(jié)編碼。
BBPE從原理上和BPE類似,也是選取出現(xiàn)頻數(shù)最高的字符對進(jìn)行合并。最主要區(qū)別是BPE基于char粒度去執(zhí)行合并的過程生成詞表,而BBPE是先通過UTF-8的編碼方式將任意字符轉(zhuǎn)化為長度1到4個字節(jié),1個字節(jié)有256種表示,然后以字節(jié)為顆粒度進(jìn)行聚合,其他流程和BPE是一樣的。
優(yōu)劣
優(yōu)點如下:
- 效果與BPE相當(dāng),其大小僅為 BPE 的 1/8。針對稀有字符,BBPE不會為其分配專門的token id,而是使用字節(jié)級別來編碼來解決OOV的問題,一定程度上控制了詞表大小和解決了稀疏字符難以訓(xùn)練的問題。
- 可以跨語言共用詞表。BBPE 可以最大限度地共享多種語言的詞匯并實現(xiàn)更好的翻譯質(zhì)量。
- 任意語種都可以被編碼到字節(jié)進(jìn)行表示,而UTF-8編碼可以在不同語言之間具有一定互通性,這樣底層字節(jié)層面的共享就可能帶來知識遷移。
缺點如下:
- 編碼序列時,長度可能會略長于BPE,計算成本更高。比如單個中文字符被切割為多個字節(jié)表示,導(dǎo)致表征的成本上升。
- 由于字節(jié)層面比字符粒度更低一層,也會導(dǎo)致在解碼的過程中對于某個字節(jié)不確定是來自某個Character還是單獨(dú)的Character中從而導(dǎo)致歧義。這個時候可能需要借助上下文的信息和一些動態(tài)規(guī)劃的算法來進(jìn)行解碼。
0x06 發(fā)展
我們接下來看看和token相關(guān)的一些有特色或者較新的論文。
6.1 Better Than Tokens
傳統(tǒng)的語言模型依賴于 tokenizer 來預(yù)處理數(shù)據(jù),但 tokenization 有其固有的局限性,包括固定的詞匯表、處理多語言或噪聲數(shù)據(jù)的效率低下,以及由壓縮啟發(fā)式方法引入的偏見。論文"Byte Latent Transformer: Patches Scale Better Than Tokens"提出的字節(jié)潛在 Transformer(Byte Latent Transformer,簡稱 BLT)挑戰(zhàn)了這種常規(guī)做法。BLT 通過直接建模原始字節(jié)流,將它們根據(jù)熵動態(tài)分組為patch(片段/補(bǔ)?。┮詫崿F(xiàn)高效計算。這種無需 tokenizer 的方法代表了語言建模的重大轉(zhuǎn)變。
主要貢獻(xiàn)
論文的主要貢獻(xiàn)是:
-
動態(tài)patch劃分:BLT通過基于熵的patch劃分方法,動態(tài)地將字節(jié)分組為patch,從而在數(shù)據(jù)復(fù)雜性較高的地方分配更多的計算資源。
-
擴(kuò)展研究:本文首次對字節(jié)級模型進(jìn)行了FLOPs控制的擴(kuò)展研究,展示了BLT在8B參數(shù)和4T訓(xùn)練字節(jié)的規(guī)模上,能夠與基于token的模型(如Llama 3)相匹配,并且在推理效率上具有顯著優(yōu)勢。
-
推理效率提升:BLT可以在保持相同推理FLOPs預(yù)算的情況下,同時增加模型大小和patch大小,從而實現(xiàn)更高的擴(kuò)展效率。
-
魯棒性提升:BLT在處理噪聲輸入和字符級任務(wù)(如拼寫檢查、音素轉(zhuǎn)錄等)上表現(xiàn)出色,顯示出對子詞結(jié)構(gòu)和字符級信息的更好理解。
-
由于在數(shù)據(jù)可預(yù)測時動態(tài)選擇長patch,BLT使訓(xùn)練和推理效率都得到提升,同時在推理和長尾數(shù)據(jù)泛化方面也取得了定性改進(jìn)。
-
字節(jié)化預(yù)訓(xùn)練模型:論文還探討了通過初始化預(yù)訓(xùn)練的基于token的模型(如Llama 3)的全局Transformer參數(shù),快速訓(xùn)練BLT模型的方法,展示了其在減少訓(xùn)練FLOPs方面的潛力。
動機(jī)
現(xiàn)有的LLM幾乎完全端到端訓(xùn)練,除了token化——這是一個將字節(jié)分組為靜態(tài)token集的啟發(fā)式預(yù)處理步驟。token化之所以重要,是因為直接在字節(jié)維度上來著眼,會導(dǎo)致序列長度較長。從而導(dǎo)致LLM在大規(guī)模訓(xùn)練上成本過高,而使用token可以避免這個問題。這種偏重于如何壓縮字符串的token化方式會導(dǎo)致一些缺點,如領(lǐng)域/模態(tài)敏感性、對輸入噪聲的敏感性、缺乏字法知識等。之前的研究通過采用更高效的自注意力機(jī)制或無注意力架構(gòu)來緩解這些問題。然而,這主要有助于訓(xùn)練小模型。在大模型上訓(xùn)練時,Transformer的主要計算成本并非注意力機(jī)制,而是主要由運(yùn)行在每個字節(jié)上的大型FFN來主導(dǎo)。
基于token化的LLM為每個token分配相同的計算量,以效率換取性能。但是token是通過壓縮啟發(fā)式方法生成的,這些啟發(fā)式方法并不總是與預(yù)測的復(fù)雜性相關(guān)。而BLT論文作者認(rèn)為,模型應(yīng)該動態(tài)分配計算資源,以滿足實際需求。例如,預(yù)測大多數(shù)單詞的結(jié)尾不需要大型Transformer,因為這些是相對簡單、低熵的決策,而選擇新句子的第一個單詞則更為困難。
patch指的是沒有固定詞匯表的動態(tài)分組序列。patch和token之間的一個關(guān)鍵區(qū)別是,使用token時,模型無法直接訪問底層字節(jié)特征。為了高效分配計算資源,論文作者提出了一種動態(tài)、可學(xué)習(xí)的方法,將字節(jié)分組為patch,并引入了一種新的混合了字節(jié)和patch信息的模型架構(gòu)。具體來說是如下幾點:
- 動態(tài)學(xué)習(xí)。與傳統(tǒng)的基于token的模型不同,BLT沒有固定的patch詞匯表,而是從原始字節(jié)數(shù)據(jù)中直接學(xué)習(xí),這樣避免了靜態(tài)詞匯表的限制,并能更好地處理多樣化和帶噪聲的輸入。
- 基于熵的 Patch:根據(jù)信息復(fù)雜度動態(tài)地將字節(jié)分組為 Patch,從而動態(tài)分配計算資源。BLT根據(jù)下一個字節(jié)預(yù)測的熵對數(shù)據(jù)進(jìn)行分段,創(chuàng)建信息密度相對均勻的上下文化字節(jié)分組。即對高熵區(qū)域(復(fù)雜輸入)分配更多的計算資源,在低熵區(qū)域節(jié)省資源。
- 引入了一種新的模型架構(gòu),通過輕量級的編碼器和解碼器模塊將任意字節(jié)組分組為潛在的patch表示?;旌狭俗止?jié)和patch信息。
在標(biāo)準(zhǔn)LLM中,增加詞匯表大小意味著平均token更大,因此模型步驟更少,但最終投影層的輸出維度也更大。這種權(quán)衡限制了基于token化的方法在token大小和推理成本上實現(xiàn)顯著提升。BLT對基于token化模型的關(guān)鍵改進(jìn)就是重新定義了詞匯表大小和計算之間的權(quán)衡。在生成時,BLT需要決定當(dāng)前字節(jié)序列的步驟是否處于patch邊界,因為這決定了是否通過潛在Transformer來調(diào)用更多計算。形式上,patch方案\(f_p\)需要滿足增量patch化的屬性:
比如,BPE就不是增量patch化方案,因為相同的前綴可以根據(jù)延續(xù)序列以不同方式來token化,因此不滿足上述屬性。
因為接下來要涉及到熵的概念,所以我們要先拿出來說一下。信息熵用來衡量系統(tǒng)不確定性或隨機(jī)性,這里指大腦關(guān)于世界的內(nèi)部模型的不確定性。大腦的目標(biāo)是將其內(nèi)部模型與感官輸入之間的預(yù)測誤差最小化,減少信息熵是減少預(yù)測誤差的一種方法。通過減少信息熵,大腦可以對世界做出更準(zhǔn)確的預(yù)測,這等于是使系統(tǒng)的自由能最小化。預(yù)訓(xùn)練 pre-train 階段,優(yōu)化目標(biāo)是最小化交叉熵(cross entropy), 對于GPT 自回歸語言模型而言,是看能否正確預(yù)測到下一個單詞。這里的交叉熵就是信息熵。
Patch化
patch函數(shù)\(f_p\)將長度為n的字節(jié)序列\(x=\{x_i, | i=1,...,n\}\)分段為長度為m<n的patch序列\(p=\{p_j, | j=1,...,m\}\),具體方式是將\(x_i\)映射到集合{0,1},其中1表示新patch的開始。這樣使得BLT可以依據(jù)上下文動態(tài)分配資源。patch的平均大小是使用給定patch函數(shù)在訓(xùn)練和推理期間處理數(shù)據(jù)的主要因素。論文使用的三種patch函數(shù)如下:
- 每patch固定字節(jié)數(shù)。最直接的字節(jié)分組方法是固定大小的patch。固定跨步易于實現(xiàn)訓(xùn)練和推理,提供了一種改變平均patch大小的簡單機(jī)制,因此易于控制FLOP成本。然而,這種patch函數(shù)存在顯著的缺點。首先,計算資源沒有動態(tài)分配到最需要的地方:如果僅預(yù)測代碼中的空白字符就可能會浪費(fèi)一個Transformer步驟,而導(dǎo)致沒有為信息密集的字節(jié)(如數(shù)學(xué)符號)分配到足夠的計算資源。其次,這導(dǎo)致相似字節(jié)序列的不一致和非上下文patch化,例如同一個單詞被用不同的方式進(jìn)行分割。
- 空白patch。Slagle提出了一種簡單而有效的改進(jìn),即在任何空白字節(jié)后創(chuàng)建新patch,這些空白字節(jié)是許多語言中語言單元的自然邊界。在空白patch化中,一個潛在Transformer步驟(即更多的FLOP)被分配來建模每個單詞。這確保了單詞在序列中以相同方式patch化,并為通常跟隨空白的困難預(yù)測分配FLOP。例如,預(yù)測問題“誰創(chuàng)作了《魔笛》?”的第一個字節(jié)比預(yù)測“M”之后的字節(jié)要困難得多,因為第一個字符顯著減少了可能的選擇,使得完成“莫扎特”相對容易預(yù)測。然而,空白patch化無法優(yōu)雅地處理所有語言和領(lǐng)域,最重要的是無法改變patch大小。
- 使用小字節(jié)語言模型的動態(tài)熵patch。這種方法使用熵估計來推導(dǎo)patch邊界,即采用數(shù)據(jù)驅(qū)動的方法來識別高不確定性的下一個字節(jié)預(yù)測。作者訓(xùn)練了一個小字節(jié)級自回歸語言模型,在字節(jié)詞匯表V上的LM分布\(p_e\)下計算下一個字節(jié)的熵。如果下一個字節(jié)的熵大,就說明是一個新patch的開始。

上圖展示了用不同的方式對字節(jié)進(jìn)行分組,每種方案會導(dǎo)致不同的patch數(shù)量。由于每個patch都通過一個大的Transformer步驟進(jìn)行處理,因此patch的數(shù)量直接決定了計算開銷(以FLOPs計)的主要部分。這些方案通過以下方式將字節(jié)分組為patch:
- 每四個字節(jié)進(jìn)行跨步分組,如MegaByte。
- 使用字節(jié)對編碼(BPE)進(jìn)行token化。
- 基于熵的patch劃分。
- 基于空白字節(jié)來劃分patch。
- 使用具有2字節(jié)上下文的小型CNN字節(jié)級模型對熵進(jìn)行預(yù)測,然后基于熵來劃分patch。
BLT架構(gòu)
BLT 由一個對 patch 表征進(jìn)行操作的大型全局自回歸語言模型以及兩個較小的局部模型組成。這兩個較小的局部模型將字節(jié)序列編碼為 patch,并將 patch 表征解碼回字節(jié)。

上圖中,BLT由三個模塊組成:一個輕量級的局部編碼器,用于將輸入字節(jié)編碼為patch表示;一個計算量較大的潛在Transformer會處理patch表示;以及一個輕量級的局部解碼器,用于解碼下一個patch的字節(jié)。BLT結(jié)合了字節(jié)n-gram嵌入和交叉注意力機(jī)制的優(yōu)點,這樣可以最大化潛在Transformer與字節(jié)級模塊之間的信息流動。與固定詞匯表的token化不同,BLT動態(tài)地將字節(jié)分組為patch,同時保留了對字節(jié)級信息的訪問。
潛在全局Transformer模型
潛在全局 Transformer 是一個具有 \(l_G\) 層的自回歸 transformer 模型 G,它將一系列潛在輸入 patch 表征 $p_j $映射到一系列輸出 patch 表征 \(o_j\)。論文使用下標(biāo) j 表示 patch,使用下標(biāo) i 表示字節(jié)。全局模型使用塊因果注意力掩碼。
**局部編碼器 **
局部編碼器模型(用 ε 表示)是一種基于 Transformer 的輕量級模型,具有 $ l_ε \ll l_g$ 層,其主要作用是將輸入字節(jié)序列 \(b_i\) 映射為表達(dá)性 patch 表征$ p_j$。此處與 Transformer 架構(gòu)的主要區(qū)別是在每個 Transformer 層之后添加了一個交叉注意力層,其功能是將字節(jié)表征池化為 patch 表征。其具體操作如下:
首先,使用\(R^{256 \times h_ε}\) 矩陣把輸入字節(jié)序列 \(b_i\) 表示為嵌入$ x_i$ 。這些嵌入可以選擇以散列嵌入的形式來添加附加信息。然后,一系列交替的 Transformer 層和交叉注意力層將這些表征轉(zhuǎn)換為patch 表征 \(p_i\),這些patch將由全局 transformer G 處理。這些Transformer 層使用局部塊因果注意力掩碼;每個字節(jié)都關(guān)注前面字節(jié)的固定窗口,該窗口通常可以跨越動態(tài) patch 邊界,但不能跨越文檔邊界。
局部解碼器
與局部編碼器類似,局部解碼器 D 是一個基于 transformer 的輕量級模型,具有$ l_d \ll l_g$ 層,它將全局 patch 表征序列 \(o_j\) 解碼為原始字節(jié) \(y_i\) 。因為局部解碼器根據(jù)解碼的字節(jié)來預(yù)測原始字節(jié)序列,因此需要將局部編碼器為字節(jié)序列生成的隱藏表征輸入給局部解碼器。在解碼器交叉注意力中,query和key/value的角色互換,即字節(jié)表示現(xiàn)在是query,patch表示是key/vale。
交互
下圖給出了幾個模塊之間的交互關(guān)系。局部編碼器使用一個交叉注意力模塊將字節(jié)表示編碼為patch表示,其中patch表示作為查詢,字節(jié)表示作為鍵/值,局部解碼器使用類似的模塊,但角色相反,即字節(jié)表示是查詢,patch表示是鍵/值。此處交叉注意力k = 2。

6.2 Tokenformer
論文"TOKENFORMER: RETHINKING TRANSFORMER SCALING WITH TOKENIZED MODEL PARAMETERS"主要探討了一種革新性的基于參數(shù)token化的高效可擴(kuò)展的Transformer架構(gòu)設(shè)計方案,該方案通過參數(shù)token化實現(xiàn)了模型的高效擴(kuò)展和計算優(yōu)化。
研究團(tuán)隊引入了 TokenFormer來統(tǒng)一 Token-Token 和 Token-Parameters Interaction 的計算。其 Token-Parameter attention 具有靈活性,并能夠處理可變數(shù)量的參數(shù),從而本質(zhì)上最大化了 Transformer 的靈活性,增強(qiáng)了模型的可擴(kuò)展性。
主要貢獻(xiàn)
Tokenformer消除了在增加模型規(guī)模時需要從頭開始重新訓(xùn)練模型的需求,大大降低了成本。論文中提出的關(guān)鍵創(chuàng)新包括:
- 完全基于注意力的架構(gòu)設(shè)計。該設(shè)計不僅用于token之間的交互,還用于token和模型參數(shù)之間的交互,提供了更大的架構(gòu)靈活性。
- 參數(shù)token化方法。該方法將模型參數(shù)視為可學(xué)習(xí)的token,使用交叉注意力機(jī)制管理交互,同時支持動態(tài)參數(shù)擴(kuò)展。
動機(jī)
論文的研究團(tuán)隊觀察到,雖然Transformer架構(gòu)在多個領(lǐng)域取得了巨大成功,但其可擴(kuò)展性受到了嚴(yán)重限制,主要是因為在token-parameter交互計算方面采用了固定的線性投影方法。這種線性投影設(shè)計限制了模型的靈活性和可擴(kuò)展性。因為這些投影層的參數(shù)大小是固定的,所以當(dāng)需要增加模型規(guī)模時無法重用以前的小規(guī)模模型。而必須改變這些線性投影層的維度,這就需要重新訓(xùn)練整個模型,導(dǎo)致極大的計算開銷。
為了克服這一挑戰(zhàn),論文作者提出了Tokenformer,這是一種新的完全基于注意力的更靈活的架構(gòu),包括token-參數(shù)交互,支持逐步擴(kuò)展模型參數(shù)量等,從而大大降低了訓(xùn)練大型Tokenformer架構(gòu)的總體成本。
架構(gòu)
對比
下圖給出了傳統(tǒng)Transformer和Tokenformer之間的區(qū)別。對于vanilla Transformer,輸入首先通過線性投影塊來計算注意力塊的輸入,即Q、K和V矩陣。這個階段涉及模型參數(shù)和輸入token之間的交互,使用線性投影進(jìn)行計算。然后,自注意力組件允許輸入token之間相互交互,通過注意力塊進(jìn)行計算。最后,前饋網(wǎng)絡(luò)(FFN)產(chǎn)生下一層的輸出,此處同樣表示使用線性投影計算的token和參數(shù)之間的交互。
Tokenformer則不同。為了計算自注意力塊的輸入(Q、K和V矩陣),輸入token被送入一個稱為token-參數(shù)注意力的新組件,在這里除了輸入token外,還傳入了參數(shù)。輸入token代表查詢部分,參數(shù)代表token-參數(shù)注意力塊的鍵和值部分。然后是和vanilla Transformer相同的自注意力組件。最后為了準(zhǔn)備下一層的輸出,論文用另一個token-參數(shù)注意力塊替代了FFN,這個token-參數(shù)注意力塊的query來自自注意力塊的輸出,Key和value則用新的參數(shù)組件中獲取。

論文中的詳細(xì)架構(gòu)圖展示了Tokenformer的完整設(shè)計。Tokenformer是一個完全由注意力驅(qū)動的架構(gòu),具有一個新的token參數(shù)注意力(Pattention)層。Pattention使用一組可學(xué)習(xí)的token來表示模型參數(shù),這些可學(xué)習(xí)的token可以和輸入token進(jìn)行注意力計算。
在架構(gòu)圖的右下方,我們可以看到,當(dāng)想要通過添加新參數(shù)來增量增加模型規(guī)模時,我們基本上是通過在每個Pattention塊的鍵和值矩陣中添加更多的參數(shù)token行來擴(kuò)展現(xiàn)有的鍵值參數(shù)集,同時保留已訓(xùn)練的參數(shù)token。從實驗結(jié)果中可以看到,相比從頭開始訓(xùn)練,規(guī)模增加的模型訓(xùn)練速度要快得多。

TokenFormer 提供一種新的看待模型的視角,即網(wǎng)絡(luò)的計算就是一些 Tokens 相互任意交互?;谶@些 Tokens (比如 data token, parameter token, memory token)和 attention 機(jī)制可以靈活地構(gòu)造任意的網(wǎng)絡(luò)結(jié)構(gòu)。因此,該團(tuán)隊希望 TokenFormer 可以作為一種通用的網(wǎng)絡(luò)結(jié)構(gòu)。

Pattention機(jī)制
Tokenformer 的核心創(chuàng)新是 Token-Parameter Attention(Pattention) Layer,研究團(tuán)隊使用 Pattention Layer 替換掉標(biāo)準(zhǔn) Transformer 中的所有的線性投影層。Pattention使用一組可訓(xùn)練的 tokens 作為模型參數(shù),并通過交叉注意力來管理 Input Token 與這些 Parameter Tokens 之間的交互。
這樣,Pattention層引入了一個額外的維度——參數(shù)token的數(shù)量——它獨(dú)立于輸入和輸出通道維度運(yùn)行。這種解耦使輸入數(shù)據(jù)能夠與可變數(shù)量的參數(shù)動態(tài)交互,通過重用預(yù)訓(xùn)練的模型提供增量模型縮放所需的靈活性。

上圖給出了標(biāo)準(zhǔn)注意力和Pattention的對比。具體來說,Pattention就是讓 input data 作為 query,同時引入了兩組具有 n 個可學(xué)習(xí)的 Tokens:代表 key,\(V_p\)表示 value。圖上A是從\(X\cdot K^{\top}_P\)得到的分?jǐn)?shù)。Θ 是改進(jìn)的 softmax,為了防止梯度 exponential 帶來的梯度問題:τ 是標(biāo)量scale factor,缺省設(shè)置為\(\sqrt n\)。f() 是任意非線性函數(shù),默認(rèn)使用 gelu。
通過這種方式,Pattention 層引入了一個額外的維度 —Parameter Token 的數(shù)量,這一維度獨(dú)立于輸入和輸出維度。此解耦方式使得輸入數(shù)據(jù)可以與可變數(shù)量的參數(shù)(variable number of parameters)進(jìn)行交互,提供了增量模型擴(kuò)展所需的靈活性。因此,訓(xùn)練更大的模型大大加快了速度,同時實現(xiàn)了與從頭開始訓(xùn)練的 Transformer 相當(dāng)?shù)男阅堋?/p>
論文對比了標(biāo)準(zhǔn)注意力機(jī)制和新提出的Pattention機(jī)制,這種新的注意力機(jī)制設(shè)計具有以下優(yōu)勢:更好的梯度穩(wěn)定性;支持動態(tài)參數(shù)擴(kuò)展;保持輸出分布的連續(xù)性。
FFN的革新
在Tokenformer中,傳統(tǒng)Transformer中的前饋網(wǎng)絡(luò)被替換為兩個連續(xù)的pattention塊,然后通過殘差連接與輸入token合并,這樣可以支持模型參數(shù)的動態(tài)擴(kuò)展。
復(fù)用
從TokenFormer 靈活的性質(zhì),我們可以延伸出很多應(yīng)用。這里以增量式 model scaling 為例。由于Pattention層的多功能設(shè)計,它非常適合沿參數(shù)軸(parameter axis)進(jìn)行大規(guī)模模型訓(xùn)練,這允許通過重用較小的預(yù)訓(xùn)練對應(yīng)模型的參數(shù)來增量開發(fā)較大的模型。假設(shè)已經(jīng)訓(xùn)練好了一個 TokenFormer,其 key parameters 和 value parameters 計為 \(K_P^{old}\)和 \(V_P^{old}\)。如下圖所示,我們將加入新的重新初始化的 key-value parameter pairs,計為 \(K_P^{new}\)和 \(V_P^{new}\),進(jìn)而和原有參數(shù)一起組合成新的 key-value 集。然后使用 pattention layer,讓 input data 與 Parameter tokens 進(jìn)行交互。直觀的理解就是每個 Key-Value 代表一種學(xué)好的 pattern,其組成一個巨大的知識庫。incremental scaling 就是在原有的知識庫上進(jìn)一步拓展訓(xùn)練。
這種縮放方案允許在不改變輸入或輸出維度的情況下集成任意數(shù)量的參數(shù)。如圖3所示,這種方法顯著提高了更大規(guī)模模型的訓(xùn)練效率,而不會降低性能。重要的是,通過將Knew P初始化為零,類似于LoRA技術(shù),該模型可以完美地從預(yù)訓(xùn)練階段恢復(fù)模型狀態(tài),而不會丟失所學(xué)的知識,從而促進(jìn)更快的收斂并加速整體縮放過程。

總結(jié)
vanilla Transformer 模型通常將處理單個 Token 所需的計算分為兩個部分:與其他 Token 的交互(Token-Token Interaction)和涉及模型參數(shù)的計算(Token-Parameter Interaction)。Token-Parameter 計算主要依賴于固定的 linear projection,大大限制 model size 的 scaling。Scaling model 是通常改變模型結(jié)構(gòu),往往需要從頭訓(xùn)練整個模型,帶來了過多的資源消耗,使其越來越不切實際。
TokenFormer 則打破了原有人們區(qū)別看待 data 和 model 的觀念,使用 token 這一概念來建模所有的計算。即,不僅像原始 Transformer 一樣將輸入數(shù)據(jù)進(jìn)行 token 化,將模型參數(shù)也視為一種 token,而且將 attention 機(jī)制拓展到 Token 和模型參數(shù)的交互中,把計算統(tǒng)一為各種不同的 token (比如data tokens and parameter tokens) 之間通過注意力機(jī)制來進(jìn)行交互。這大大增強(qiáng)了 Token-Parameter 交互的靈活性,從而能夠基于訓(xùn)好的模型上增量的拓展新的更大的模型,從而顯著降低了訓(xùn)練負(fù)擔(dān)。
6.3 LCM
論文"The Future of AI: Exploring the Potential of Large Concept Models"提出了大型概念模型(Large Concept Models, LCMs)。這篇論文不僅是對現(xiàn)有大語言模型(LLMs)局限性的深刻反思,更是對AI未來發(fā)展路徑的前瞻性探索。
問題
LLM的Token粒度其實并不是一個好的表達(dá)語義的方式,基于Token的學(xué)習(xí)方式對于學(xué)習(xí)語義來說效率也比較低。 這種“逐詞預(yù)測”的模式,雖然在很多任務(wù)上取得了成功,但在處理長文本和復(fù)雜概念時,容易出現(xiàn)“只見樹木,不見森林”的問題。
因為人腦并不在單詞層面運(yùn)作。人的思維明顯是分層的。網(wǎng)上一個非常恰當(dāng)?shù)睦邮牵耗悴⒉皇腔趯W(xué)習(xí)在每個路口如何打方向盤,來學(xué)習(xí)如何從北京開到廣州的。這里的每個路口如何打方向盤就是一個過于細(xì)粒度的單元,也就是對應(yīng)到這里的Token。
之前隨著推理硬件性能的提升和各種優(yōu)化方式的出現(xiàn),看起來token粒度過小對于性能的影響已經(jīng)不那么大了。然而近期隨著推理期計算等新事物的實現(xiàn),強(qiáng)化學(xué)習(xí)在AI各種領(lǐng)域中愈發(fā)重要。這需要模型具備語義空間中的能力,而不僅僅是在token序列空間視角中獲取到的能力。因此需要尋找一個更好的與Token不同的更接近語義粒度的建模方式。
動機(jī)
受人類構(gòu)思交流的高層級思路啟發(fā),Meta AI研究員提出全新語言建模新范式LCM(大概念模型),解耦語言表示與推理。簡而言之,LCM將token拋棄,轉(zhuǎn)而采用更高級別的「概念」在「句子嵌入空間」對推理(reasoning)進(jìn)行建模,直接操作高層級顯式語義表示信息,徹底讓推理擺脫語言和模態(tài)制約。新系統(tǒng)將不再單純基于下一個token預(yù)測,而是像嬰兒和小動物那樣通過觀察和互動來理解世界。
為什么需要「概念」?這是因為現(xiàn)有的LLM都缺少人類智能的一個重要的特點:在多級別抽象上顯式的推理和規(guī)劃。比如在解決一項復(fù)雜的任務(wù)或撰寫一份長篇文檔時,人類通常采用自上而下的流程:首先在較高的層次上規(guī)劃整體結(jié)構(gòu),然后逐步在較低的抽象層次上添加細(xì)節(jié)。具有顯式的分層結(jié)構(gòu)模型更適合創(chuàng)建長篇輸出。而現(xiàn)在市面上的語言模型,比如大家熟悉的GPT,雖然能寫詩、寫代碼、聊天,但它們本質(zhì)上還是一個字一個字地“猜”出來的。想象一下,就像一個只會背誦但不懂意思的鸚鵡,雖然能流利地說話,但缺乏真正的理解。
LCM的出現(xiàn),就是要打破這個局面。LCMs不再執(zhí)著于“下一個詞是什么?”,而是思考“這句話、這段話、乃至整篇文章的核心概念是什么?” 這說明AI的“思維”模式正經(jīng)歷著從“詞語”到“概念”的質(zhì)的飛躍。
思路
論文將抽象層次限制為2種:子詞token(subword token)和概念。而所謂的「概念」被定義為整體的不可分的「抽象原子見解」。在現(xiàn)實中,一個概念往往對應(yīng)于文本文檔中的一個句子,或者等效的語音片段。論文作者認(rèn)為,與單詞相比,句子才是實現(xiàn)語言獨(dú)立性的恰當(dāng)?shù)膯卧?。這與當(dāng)前基于token的LLMs技術(shù)形成了鮮明對比。
LCM的核心在于它不再執(zhí)著于預(yù)測下一個詞,而是在更高的語義層級——“概念”上進(jìn)行思考。它把句子看作一個概念單元,并用一種叫做SONAR的句子嵌入技術(shù)來表示這些概念。這意味著LCM處理的不再是單個的詞語,不再像傳統(tǒng)語言模型那樣逐詞預(yù)測,而是考慮整句話的含義。在句子表征空間中進(jìn)行建模。這意味著,LCM將句子視為一個概念單元,并利用句子嵌入(sentence embeddings)來表示這些概念。LCM的目標(biāo)是預(yù)測下一個句子的嵌入向量,也就是下一個“概念”。這種方法能夠更好地捕捉文本的整體語義結(jié)構(gòu),使模型能夠在更高的抽象層面上進(jìn)行推理。
例如在句子:
Tim 并不擅長運(yùn)動,他認(rèn)為如果參加一項運(yùn)動就會有所改變,他嘗試加入幾個團(tuán)隊,但沒有一個團(tuán)隊錄取他。
不同的概念將是:
Tim 并不擅長運(yùn)動。
他認(rèn)為如果參加一項運(yùn)動就會有所改變。
他嘗試加入幾個團(tuán)隊。
但沒有一個團(tuán)隊錄取他。
我們可以看到每個概念都代表了句子的一個想法。
新方法將與token級別的處理不同,更靠近在抽象空間的(分層)推理。上下文在LCM所設(shè)計的抽象空間內(nèi)表達(dá),但抽象空間與語言或模態(tài)無關(guān)。也就是說在純粹的語義層面對基本推理過程進(jìn)行建模,而不是對推理在特定語言中的實例建模。具體而言,只需要固定長度的句子嵌入空間的編碼器和解碼器,就可以構(gòu)造LCM,處理流程非常簡單:
- 首先將輸入內(nèi)容分割成句子,然后用編碼器對每個句子進(jìn)行編碼,以獲得概念序列,即句子嵌入。
- 然后,大概念模型(LCM)對概念序列進(jìn)行處理,在輸出端生成新的概念序列。
- 最后,解碼器將生成的概念解碼為子詞(subword)序列。
總體架構(gòu)
訓(xùn)練大概念模型需要基于句子嵌入空間的解碼器和編碼器來訓(xùn)練一個新的嵌入空間,針對推理架構(gòu)進(jìn)行優(yōu)化。此論文使用其開源的SONAR作為句子嵌入的解碼器和編碼器?;蛘哒f,LCM的核心組件是句子嵌入模型SONAR。SONAR是一個強(qiáng)大的多語言、多模態(tài)句子表征模型,支持超過200種語言和語音輸入。LCM在SONAR嵌入空間中進(jìn)行操作,這意味著LCM的輸入和輸出都是SONAR嵌入向量,而不是離散的詞語。這種基于連續(xù)向量空間的建模方式,為LCM帶來了諸多優(yōu)勢。
通過SONAR,LCMs能夠在概念層面進(jìn)行推理,而不僅僅是進(jìn)行詞語的排列組合。例如,當(dāng)LCMs處理“全球變暖導(dǎo)致海平面上升”這個句子時,它不僅理解了每個單詞的含義,更重要的是,它理解了“全球變暖”、“海平面上升”這兩個概念,以及它們之間的因果關(guān)系。

左:概念嵌入空間中推理的可視化(摘要任務(wù))。右:大型概念模型(LCM)的基本架構(gòu)。
SONAR解碼器和編碼器(圖中藍(lán)色部分)是固定的,不用訓(xùn)練。LCM(圖中綠色部分)輸出的概念可以解碼為其他語言或模態(tài),而不必從頭執(zhí)行整個推理過程。同樣, 某個特定的推理操作,如歸納總結(jié),可以在任何語言或模態(tài)的輸入上以零樣本(zero-shot)模式進(jìn)行。因為推理只需操作概念。
總之,LCM既不掌握輸入語言或模態(tài)的信息,也不以特定語言或模態(tài)生成輸出。
細(xì)節(jié)
SONAR嵌入空間
SONAR文本嵌入空間使用編碼器/解碼器架構(gòu)進(jìn)行訓(xùn)練,以固定大小的瓶頸代替交叉注意力,如下圖。

為了探索在SONAR空間中進(jìn)行語言建模的最佳實踐,Meta AI的研究人員設(shè)計了多種LCM架構(gòu)變體。
Base-LCM
下個概念預(yù)測(next concept prediction)的基線架構(gòu)是Base-LCM,這是一個基于Transformer解碼器的基礎(chǔ)模型。它將前一個句子的SONAR嵌入作為輸入(先行概念),并預(yù)測下一個句子的嵌入(概念)。這種架構(gòu)簡單直接,易于理解和實現(xiàn)。
如下圖所示,Base-LCM配備了「PostNet」和「PreNet」。PreNet對輸入的SONAR嵌入進(jìn)行歸一化處理,并將它們映射到模型的隱藏維度。
Base-LCM在半監(jiān)督任務(wù)上學(xué)習(xí), 模型會預(yù)測下一個概念,通過優(yōu)化預(yù)測的下一個概念與真實的下一個概念的距離來優(yōu)化參數(shù),也就是通過MSE回歸來優(yōu)化參數(shù)。

基于擴(kuò)散的LCM(Diffusion-based LCM)
基于擴(kuò)散的LCM是一種生成式潛變量模型,它能學(xué)習(xí)一個模型分布\(p_θ\) ,用于逼近數(shù)據(jù)分布q。與基礎(chǔ)LCM相似,可以將擴(kuò)散LCM建模視為自動回歸模型,每次在文檔中生成一個概念。
具體而言, 在序列的位置n上,模型以之前全部的概念為條件,來預(yù)測在此處某概念的概率。
單塔擴(kuò)散LCM(One-Tower Diffusion LCM)
該模型引入了擴(kuò)散模型(Diffusion Model)的思想,通過逐步添加噪聲,然后去噪的方式來生成下一個句子的嵌入。這種方法可以生成更具多樣性和創(chuàng)造性的文本。

如上圖左,單塔擴(kuò)散LCM由一個Transformer主干組成,其任務(wù)是在給定句子嵌入和噪音輸入的條件下預(yù)測下一個句子嵌入 。
雙塔擴(kuò)散LCM(Two-Tower Diffusion-LCM)
如上圖右側(cè),該模型將編碼器和解碼器分離,編碼器負(fù)責(zé)處理上下文信息,解碼器負(fù)責(zé)生成下一個句子的嵌入。這種架構(gòu)更類似于傳統(tǒng)的序列到序列模型,可以更好地捕捉長距離依賴關(guān)系。
第一個模型,即上下文標(biāo)注模型,將上下文向量作為輸入,并對其進(jìn)行因果編碼。然后,上下文分析器的輸出結(jié)果會被輸入第二個模型,即去噪器(denoiser)。去噪器通過迭代去噪潛高斯隱變量來預(yù)測下一個句子嵌入 。
Quant-LCM
為了提高計算效率,該模型對SONAR空間進(jìn)行量化,將連續(xù)的嵌入向量轉(zhuǎn)換為離散的碼本。這種方法可以在不損失太多性能的情況下顯著降低計算成本。
在圖像或語音生成領(lǐng)域,目前有兩種處理連續(xù)數(shù)據(jù)生成的主要方法:一種是擴(kuò)散建模,另一種是先對數(shù)據(jù)進(jìn)行學(xué)習(xí)量化,然后再在這些離散單元的基礎(chǔ)上建模。此外,文本模態(tài)仍然是離散的,盡管處理的是SONAR空間中的連續(xù)表示,但全部可能的文本句子(少于給定字符數(shù))都是SONAR空間中的點云,而不是真正的連續(xù)分布。這些考慮因素促使作者探索對SONAR表示進(jìn)行量化,然后在這些離散單元上建模,以解決下一個句子預(yù)測任務(wù)。最后,采用這種方法可以自然地使用溫度、top-p或top-k采樣,以控制下一句話表示采樣的隨機(jī)性和多樣性水平。
6.4 動作Tokenizer
論文"FAST: Efficient Action Tokenization for Vision-Language-Action Models"提出了一種高效的機(jī)器人動作Tokenization方法,能把動作像語言一樣,用離散Token表示。這可以讓機(jī)器人技術(shù)能夠與自回歸Transformer訓(xùn)練流程無縫銜接,提升了從大規(guī)?;ヂ?lián)網(wǎng)數(shù)據(jù)預(yù)訓(xùn)練的遷移能力,增強(qiáng)了機(jī)器人執(zhí)行語言指令的能力。
FAST使用了一種基于離散余弦變換(DCT)的壓縮算法,來提高VLA模型的訓(xùn)練速度。DCT是一種頻域變換,因簡潔和計算高效,常用于壓縮算法,如JPEG圖像壓縮、MP3音頻的編解碼。
FAST首先對輸入的動作進(jìn)行歸一化,然后對每個動作維度分別應(yīng)用離散余弦變換(DCT),最后用BPE來壓縮DCT矩陣。將DCT和字節(jié)對編碼(BPE)結(jié)合,就能把原始動作塊壓縮成數(shù)量少但更密集的動作Token。

通常每個動作塊包含30-60個Token,和以前的動作Tokenization方法相比,壓縮率提高了10倍。

上圖給出了FAST動作token化的流水線概述。給定一個歸一化的動作塊,我們應(yīng)用離散余弦變換(DCT)將信號轉(zhuǎn)換為頻域。然后,我們對DCT系數(shù)進(jìn)行量化,并使用字節(jié)對編碼(BPE)將每維DCT系數(shù)的展平序列壓縮為最終的動作標(biāo)記序列。詳細(xì)說明見第V-B節(jié)。
0xFF 參考
Byte Latent Transformer: Patches Scale BetterThan Tokens——字節(jié)潛在Transformer:patch比token更高效 Together_CZ
Byte Pair Encoding and WordPiece Model詳解 Yuki
BytePiece:更純粹、更高壓縮率的Tokenizer 蘇劍林
FAST: Efficient Action Tokenization for Vision-Language-Action Models
https://huggingface.co/learn/nlp-course/chapter6/6?fw=pt#implementing-wordpiece
Huggingface詳細(xì)教程之Tokenizer庫 基本粒子
JAPANESE AND KOREAN VOICE SEARCH
Large Concept Models: Language Modeling in a Sentence Representation Space
LLM時代Transformer中的Positional Encoding
LLM還沒研究透,LCM又來了 Alex [算法狗]
Luke:深入理解NLP Subword算法:BPE、WordPiece、ULM Luke
Neural Machine Translation of Rare Words with Subword Units
Neural Machine Translation of Rare Words with Subword Units
Neural Machine Translation with Byte-Level Subwords
NLP 中的Tokenizer:BPE、BBPE、WordPiece、UniLM 理論
NLP-Tokenizer-BPE算法原理及代碼實現(xiàn) 愛喝熱水的lucky
NLP三大Subword模型詳解:BPE、WordPiece、ULM
NLP中的Tokenization
NLP分詞模型:BPE、WordPiece、ULM、SentencePiece
Rethinking LLM Language Adaptation: A Case Study on Chinese Mixtral
Robin3D: Improving 3D Large Language Model via Robust Instruction Tunin
Robin3D: Improving 3D Large Language Model via Robust Instruction Tuning
Scaling Laws with Vocabulary: Larger Models Deserve Larger Vocabularies
TokenFormer: Rethinking Transformer Scaling with Tokenized Model Parameters
Tokenization不存在了?Meta最新研究,無需Tokenizer的架構(gòu)來了 [PaperWeekly]
Tokenization,再見!Meta提出大概念模型LCM,1B模型干翻70B? 新智元
UC伯克利等提出具身智能「動作Tokenizer」,效率飆升5倍! 新智元
【OpenLLM 008】大模型基礎(chǔ)組件之分詞器-萬字長文全面解讀LLM中的分詞算法與分詞器(tokenization & tokenizers):BPE/WordPiece/ULM & beyond
【OpenLLM 008】大模型基礎(chǔ)組件之分詞器-萬字長文全面解讀LLM中的分詞算法與分詞器(tokenization & tokenizers):BPE/WordPiece/ULM & beyond OpenLLMAI
從2019年到現(xiàn)在,是時候重新審視Tokenization了 機(jī)器之心
大模型中的分詞器tokenizer
大模型中的分詞器tokenizer:BPE、WordPiece、Unigram LM、SentencePiece
智能連接:碳原子與Token 伍鵬 [AI的無限游戲]
機(jī)器如何認(rèn)識文本 ?NLP中的Tokenization方法總結(jié)
深入理解NLP Subword算法:BPE、WordPiece、ULM
理解NLP最重要的編碼方式 — Byte Pair Encoding (BPE),這一篇就夠了
https://arxiv.org/pdf/2412.08821
https://github.com/karpathy/minbpe
https://huggingface.co/learn/nlp-course/chapter6/5%3Ffw%3Dpt)
Jordan Hoffmann, Sebastian Borgeaud, Arthur Mensch, Elena Buchatskaya, Trevor Cai, Eliza Rutherford, Diego de Las Casas, Lisa Anne Hendricks, Johannes Welbl, Aidan Clark, et al. 2022. Training compute-optimal large language models. arXiv preprint arXiv:2203.15556.
Kudo, Taku. "Subword regularization: Improving neural network translation models with multiple subword candidates." arXiv preprint arXiv:1804.10959 (2018)
Neural Machine Translation of Rare Words with Subword Units Rico Sennrich, Barry Haddow, Alexandra Birch
浙公網(wǎng)安備 33010602011771號