语言模型与分词
文本是连续的字符流,但语言模型处理的是离散的符号序列。将连续文本切分为离散符号的过程称为分词(Tokenization),它是语言模型与原始文本之间的桥梁。分词的质量直接影响模型的能力,切分太粗则词表庞大且无法处理新词,切分太细则序列过长且语义碎片化。
对分词的研究历史可以追溯到 20 世纪 90 年代。1994 年,美国软件工程师菲利普·盖奇(Philip Gage)在文章《A New Algorithm for Data Compression》中首次提出了 BPE(Byte Pair Encoding)算法,开创了以子词分词替代词级分词的时代。在此之前,词级分词的机器翻译系统面临严重的未登录词问题(Out-of-Vocabulary,OOV),训练中未见过的词无法被正确处理。将 BPE 应用于分词改变了这一局面,它让模型能够通过组合有限的子词来表示任意文本,成为现代 LLM 分词的基础。
在本章介绍分词算法及下一章实际训练一个大语言模型之前,我们需要先理解语言模型是什么、它要解决什么问题。我们将从语言模型的历史脉络出发,追溯从统计方法到神经方法的演进,然后详细剖析现代 LLM 使用的分词算法,最后探讨词表设计的权衡考量。
语言模型简史
语言模型(Language Model)定义了自然语言序列上的概率分布。给定一个文本序列 ,语言模型为该序列赋予概率 ,衡量其在自然语言中出现的可能性。通过链式法则,这个联合概率可以分解为逐步预测的条件概率:
因此,建模 (即根据前文预测下一个词)成为语言模型最常见的训练目标,GPT、Qwen、DeepSeek、Claude 等自回归模型均是以此方式训练的。不过,这并非语言模型的唯一定义方式,BERT 等掩码语言模型通过预测被遮蔽的词来建模 ,而语音识别等经典应用中,语言模型的用途是给候选序列打分,而非逐词预测。无论采用哪种形式,语言模型都需要设计者对语言本身有深刻的理解。要准确估计序列概率,模型必须掌握语法、语义、常识、世界知识,甚至推理能力。
N-Gram 语言模型
在深度学习兴起之前,N-Gram 语言模型是语言建模的主流方法,由克劳德·香农(Claude Shannon)在 1948 年的信息论研究中为其奠定基础。它的核心是马尔可夫假设:下一个词只依赖于前面的 个词,而非整个历史。设 是 时刻之前的所有词(完整历史), 则表示前面的 个词(最近的历史),要预测的下一个词 满足条件概率:
即以全部历史( 到 )为前提,与以前 个词为前提( 到 ),下一个词 出现的概率是大致相等的。拿 Bigram 为例(),下一个词只依赖于前一个词,根据条件概率的定义得:
这样直接从训练语料中计算 这个词对出现的次数,再除以 出现的次数,就能得到下一个词 的概率值。以下的代码实现了一个简单的 Bigram 语言模型,演示了 N-Gram 模型的基本工作原理。从代码的运行结果可见,对于训练语料中出现过的词对,模型可以给出合理的概率估计,但对于未见过的词对(如"爱 广州"),模型输出的该词对的概率为零。
# N-Gram 语言模型演示
from collections import defaultdict
class BigramModel:
"""简单的 Bigram 语言模型"""
def __init__(self):
self.bigram_counts = defaultdict(lambda: defaultdict(int))
self.unigram_counts = defaultdict(int)
self.vocab = set()
def train(self, sentences):
"""从句子列表中训练模型"""
for sentence in sentences:
tokens = ['<s>'] + sentence.split() + ['</s>']
for i in range(len(tokens) - 1):
w1, w2 = tokens[i], tokens[i + 1]
self.bigram_counts[w1][w2] += 1
self.unigram_counts[w1] += 1
self.vocab.add(w1)
self.vocab.add(w2)
def probability(self, w1, w2):
"""计算 P(w2 | w1)"""
if self.unigram_counts[w1] == 0:
return 0
return self.bigram_counts[w1][w2] / self.unigram_counts[w1]
def sentence_probability(self, sentence):
"""计算句子的概率"""
tokens = ['<s>'] + sentence.split() + ['</s>']
prob = 1.0
for i in range(len(tokens) - 1):
p = self.probability(tokens[i], tokens[i + 1])
if p == 0:
return 0 # 遇到未见过的情况
prob *= p
return prob
# 训练语料
corpus = [
"我 爱 北京",
"我 爱 上海",
"北京 是 首都",
"上海 是 城市",
"我 爱 中国"
]
model = BigramModel()
model.train(corpus)
# 测试
print("词汇表:", model.vocab)
print("\n条件概率 P(爱|我):", model.probability("我", "爱"))
print("条件概率 P(北京|爱):", model.probability("爱", "北京"))
print("条件概率 P(上海|爱):", model.probability("爱", "上海"))
# 计算句子概率
test_sentence = "我 爱 北京"
print(f"\n句子 '{test_sentence}' 的概率:", model.sentence_probability(test_sentence))
test_sentence2 = "我 爱 广州"
print(f"句子 '{test_sentence2}' 的概率:", model.sentence_probability(test_sentence2))
N-Gram 模型足够简单、直观,但它存在稀疏性问题和无法捕捉长距离依赖两个根本缺陷,最终催生了神经语言模型的诞生。
稀疏性问题:就是指上面代码所暴露的问题,即使训练语料规模再大,也不可能覆盖所有可能的词组合。当遇到训练中未出现过的组合时,模型输出概率为零,导致整个句子的概率为零。以 Bigram 为例,假设词汇表大小为 ,可能的词对数量为 。即使词汇表只有 10000 个词,词对数量就达到 1 亿。
无法捕捉长距离依赖:N-Gram 模型只看前面 个词,无法捕捉更远距离的依赖关系。考虑这个例子:"生长在北京的我对___很熟悉"。要预测空白处的词(如"天安门"),就需要理解句子开头的"北京"与空白处的关系。但在 N-Gram 模型中,如果 ,模型最远只能看到"我对___",完全无法利用前面的信息。增大 可以部分缓解这个问题,但又会加剧稀疏性, 越大,可能的 N-Gram 组合越多,训练语料越难以覆盖。实践中,N-Gram 模型通常最多只使用 而已。
神经语言模型
2003 年,加拿大计算机科学家约书亚·本吉奥(Yoshua Bengio)在论文《A Neural Probabilistic Language Model》中首次提出了神经语言模型,开创了语言建模的新范式。这项工作后来获得了 2018 年的图灵奖,本吉奥与辛顿(Geoffrey Hinton)、杨立昆(Yann LeCun)因深度学习的开创性贡献共同被誉为深度学习的"三巨头"。
神经语言模型比较好地解决了稀疏性问题,它将每个词表示为一个分布式向量(Distributed Representation),而非离散的符号。词向量捕捉词之间的语义相似性,使得模型可以泛化到训练中未见过但语义相似的词组合。譬如模型训练中从未见过"猫吃鱼",但如果见过"狗吃肉",模型依然可以通过词向量的相似性推断出"猫吃鱼"也是合理的。此外,比起 N-Gram 的离散概率分布,神经语言模型的分布是平滑的,这是因为神经网络的输出是 Softmax,对所有词都给出非零概率,不存在概率绝对为零的问题,也就避免了由于一个罕见词组合导致一整句话的概率变成零。
对于无法捕捉长距离依赖的问题,最早由本吉奥提出的神经语言模型使用的是前馈神经网络,输入窗口大小固定,效果虽然比 N-Gram 只支持不超过 5 个词的序列好点,但依然是捉襟见肘,对生产实践谈不上有多少实用性。后来,循环神经网络(RNN)、长短期记忆网络(LSTM)、序列映射(Seq2Seq)这些架构进一步提升了神经语言模型的能力,直到 Transformer 架构出现,长距离依赖的问题才被彻底解决,现代大语言模型的时代终于开启。
自回归语言模型
从最早的 N-Gram 模型到今天的 Transformer 模型,"根据前文预测下一个词"这条线索贯穿始终,以这个预测下一个词为目标的语言模型有一个正式名称:自回归语言模型(Autoregressive Language Model),也称因果语言模型(Causal Language Model, CLM),是神经语言模型中最大的一个分支。CLM 的形式化定义是给定一个文本序列 ,语言模型将其分解为条件概率的乘积:
以上定义表明,CLM 视角下一句话出现的概率,等于从第一个词开始,逐个词往后猜,每次都根据前面已经出现的所有词来猜下一个词,把这些猜对的可能性乘在一起。譬如"今天天气真好"这句话,模型先算"今天"之后出现"天气"的概率,再算"今天天气"之后出现"真"的概率,再算"今天天气真"之后出现"好"的概率,把这些条件概率乘起来,就得到了整句话的概率。这就是自回归这个名字的由来,每一步的预测都依赖前面所有步的输出,逐步回归地生成文本。CLM 训练目标是最大化训练语料中所有序列的似然:
这个训练目标的含义是拿大量真实文本当参考答案,让模型反复练习给定前文后猜测下一个词这项技能,训练过程中不断调整参数,使得模型对真实文本中每个位置下一个词的预测越来越准。目标函数取对数是一个工程技巧,目的是把连乘变成连加,既避免了大量小概率相乘导致数值下溢,也方便求导优化。
分词算法
语言模型的输入必须是离散的符号,原始文本是连续的字符流,需要切分为有限词汇表中的符号。将文本字符流转化为模型可以处理的离散符号序列的组件称为分词器(Tokenizer)。现代 LLM 实现分词器的主流分词算法有 BPE、WordPiece 和 Unigram 等,选择不同的分词器,很大程度上决定了模型的这几个设计决策:
- 词表大小:词汇表越大,覆盖率越高,但模型参数也就越多,因为模型输出层是 的矩阵。
- 序列长度:分词切分越细,相同字符流产生的序列越长,模型计算成本越高。序列长度对复杂度是 的注意力机制来说压力是比较大的。
- 未登录词处理:无论词表多大,总会遇到训练中未见过的词。好的分词方案应该能处理这种情况。
分词按粒度可分为词级、子词级和字符级分词三种。最符合人类直觉的分词方案是词级分词(Word-level Tokenization),将文本按词切分,每个词作为词表中的一个符号。譬如原始文本是"我爱北京天安门",词级分词的结果就是["我", "爱", "北京", "天安门"]。词级分词从人类视角看起来既直观又合理,但从前面三个设计决策来看,词级分词至少有其中两个是难以解决的:
- 词表大小:英语起码有数十万词,中文词汇更是难以穷尽。一个覆盖广泛的词表可能需要数百万条目,这将会导致模型输出层参数量极其巨大。
- 未登录词问题:无论词表多大,总会遇到新词,譬如人名、地名、专业术语、网络新词等。词级分词对这些词束手无策,只能用
<UNK>符号替代,丢失所有语义信息。
因此,现代 LLM 的选择是三种粒度中最复杂的子词分词(Subword-level Tokenization),这是一种词级和字符级之间的折中方案,子词分词让常用词保持完整,罕见词就拆分为有意义的子词单元,这跟学生按词根词缀背诵英文单词很像,譬如单词 "unhappiness" 可以拆分为 ["un", "happiness"] 或 ["un", "happy", "ness"],这样即使模型从未见过 "unhappiness" 这个词,也能通过子词的组合理解其含义。
相对词级分词来说,子词分词的优势显著。它只要固定大小的词表,就能做到几乎无限的覆盖率,通过组合有限的子词表示出任意的文本。子词通常有语义含义(如前缀、后缀、词根),模型可以泛化到新词,从而解决未登录词问题。在后面介绍词表设计权衡中会给出主流 LLM 的词表大小,通常都在 3 万至 10 万之间,远小于词级分词。
BPE
字节对编码(Byte Pair Encoding,BPE)最初是一种数据压缩算法,由美国软件工程师菲利普·盖奇(Philip Gage)在 1994 年发表于文章《A New Algorithm for Data Compression》中。盖奇的原始思路是设计一种算法找出文本中出现最频繁的相邻字节对,用一个不在原文中出现的字节来替换它,重复这一过程就能逐步压缩文本。2015 年,英国爱丁堡大学的菲利普·森里奇(Philipp Sennrich)在论文《Neural Machine Translation of Rare Words with Subword Units》中将 BPE 改造为子词分词算法,将压缩文本的目标更换为构建子词词表,让机器翻译模型能够通过组合有限的子词来表示任意文本,解决了长期困扰 NLP 的未登录词问题。GPT-2、GPT-3、RoBERTa 等模型使用的都是 BPE 分词算法。
BPE 算法的假设是语言中的符号组合并非均匀分布的,某些字符对经常一起出现,如英文的 "th"、"er"、"in",某些子串反复构成词的词根词缀,如 "ing"、"tion"、"ment"。如果把这些高频组合当作整体来处理,既能缩短序列长度,又能让词表保持适中的规模。BPE 正是利用了这种分布的不均匀性,通过数据驱动的方式自动发现值得合并的符号对。从压缩到分词,差别只在于压缩算法关心的是替换后文本是否变短了,而分词算法关心的是合并后词表能否覆盖新词。两者完全可以共享同一套迭代合并机制,只是评价标准不同而已。
BPE 算法的流程可以分为训练(从语料中学习合并规则)和分词(将规则应用于新文本)两个阶段。训练阶段包含以下几个步骤:
初始化词表:将训练语料中每个词拆分为字符序列,并在词尾添加特殊的结束符号
</w>,用于区分词内子词和词尾子词。同时统计每个词的出现频率。假设语料中 "low" 出现 5 次、"lower" 出现 2 次,初始化后得到:l o w </w>: 5l o w e r </w>: 2
此时词表就是所有出现过的字符的集合:
{l, o, w, e, r, </w>}。统计相邻符号对频率:遍历词频表中的每个词,按词频加权统计所有相邻符号对的出现次数。对于以上面的例子,符号对
(l, o)出现 7 次,(o, w)出现 7 次,(w, </w>)出现 5 次,(w, e)出现 2 次,(e, r)出现 2 次,(r, </w>)出现 2 次。合并最频繁的符号对:找出频率最高的相邻对,将其合并为一个新的符号,加入词表,并记录这条合并规则。上面
(l, o)和(o, w)并列最高频,假设先合并(l, o),词表新增lo,词频表更新为:lo w </w>: 5lo w e r </w>: 2
重复迭代:重新统计相邻符号对频率,继续合并,直到词表达到目标大小或达到预设的合并次数。每次合并都会产生一条新规则,规则按合并顺序排列,形成有序的规则列表。
分词阶段则严格按训练时学到的合并顺序,依次尝试对输入文本应用每条合并规则。分词过程的顺序很重要,先学到的规则应用于更高频的模式,确保分词结果与训练时一致。如果训练时先合并了 (l, o) 再合并 (lo, w),那么对 "lower" 分词时,会先得到 ["lo", "w", "e", "r"],再得到 ["low", "e", "r"]。下面的代码实现了一个简化的 BPE 算法,演示了训练阶段和分词阶段的完整流程。
# BPE 算法演示
from collections import defaultdict
class SimpleBPE:
"""简化的 BPE 算法演示"""
def __init__(self, num_merges=10):
self.num_merges = num_merges
self.merges = [] # 合并规则
def train(self, corpus):
"""从语料库训练 BPE"""
# 统计词频
word_freqs = defaultdict(int)
for word in corpus.split():
word_freqs[' '.join(list(word))] += 1
print("初始词频统计:")
for word, freq in sorted(word_freqs.items(), key=lambda x: -x[1])[:10]:
print(f" {word}: {freq}")
# 迭代合并
for i in range(self.num_merges):
# 统计相邻符号对频率
pairs = defaultdict(int)
for word, freq in word_freqs.items():
symbols = word.split()
for j in range(len(symbols) - 1):
pairs[(symbols[j], symbols[j + 1])] += freq
if not pairs:
break
# 找出最频繁的对
best_pair = max(pairs, key=pairs.get)
print(f"\n迭代 {i+1}: 合并 {best_pair}(频率: {pairs[best_pair]})")
# 合并
new_symbol = ''.join(best_pair)
self.merges.append(best_pair)
# 更新词频表
new_word_freqs = {}
for word, freq in word_freqs.items():
new_word = word.replace(' '.join(best_pair), new_symbol)
new_word_freqs[new_word] = freq
word_freqs = new_word_freqs
print("\n最终词表:")
vocab = set()
for word in word_freqs.keys():
vocab.update(word.split())
print(f" {sorted(vocab)}")
def tokenize(self, word):
"""对单个词进行分词"""
symbols = list(word)
for pair in self.merges:
i = 0
while i < len(symbols) - 1:
if symbols[i] == pair[0] and symbols[i + 1] == pair[1]:
symbols = symbols[:i] + [''.join(pair)] + symbols[i + 2:]
else:
i += 1
return symbols
# 训练语料(简化示例)
corpus = "low low low low low lower lower newest newest newest newest newest newest wider wider wider new new"
print("=== BPE 训练过程 ===\n")
bpe = SimpleBPE(num_merges=10)
bpe.train(corpus)
print("\n=== 分词测试 ===")
test_words = ["low", "lower", "newest", "wider", "newer"]
for word in test_words:
tokens = bpe.tokenize(word)
print(f"'{word}' -> {tokens}")
BPE 的优势是算法本身逻辑清晰,只需统计频率和迭代合并,实现成本很低。同时它完全是数据驱动的,不依赖任何语言特定的规则或词典,同一种算法可以适用于任何语言。在处理未见过的词时,BPE 天然具备 OOV 免疫能力,任何新词都可以回退到字符级拆分,不会出现无法编码的情况。此外,通过调整合并次数可以灵活控制词表大小,从而在序列长度和模型参数之间取得平衡。
不过 BPE 也有它的局限。BPE 的合并遵循贪心策略,每一步只看当前最高频的符号对,不考虑合并后对后续选择的影响。一个高频但语义价值不大的合并可能挤占更有价值的合并机会,譬如在英文语料中 (t, h) 的频率极高,BPE 会优先合并它,但 "th" 本身不是一个独立的语义单元,而某些频率稍低但语义更完整的子词(如 "ment"、"tion")反而被延迟合并。
另一个问题是分词结果是确定且唯一的,给定训练好的合并规则,每个词只有一种分词方式,模型在训练时始终看到相同的子词切分,无法学习到同一词的不同切分可能带来的语义关联。此外,BPE 的合并规则完全取决于训练语料的频率统计,对语料分布敏感。如果语料中英文占比过高,中文的高频子词可能得不到足够的合并机会,导致中文被过度切分为单字,这也是多语言模型在词表设计时需要特别关注的问题。
字节级 BPE
经典 BPE 以字符为初始单位构建词表,然而什么是"字符"(Character)?ASCII 字符集只有 128 个符号,远远无法覆盖全球的文字系统。Unicode 标准定义了超过 14 万个字符,涵盖了中文、阿拉伯文、 emoji 等,如果以 Unicode 字符为初始词表,基础词表就已经非常庞大,而且 Unicode 字符的编码长度不统一(1 到 4 个字节),这会给 BPE 的合并过程带来不必要的额外复杂性。
GPT-2 开始采用了一个更优雅的方案:字节级 BPE(Byte-level BPE)。它不再以 Unicode 字符为初始单位,而是以 UTF-8 编码的字节为初始单位。UTF-8 是一种变长编码,ASCII 字符占 1 个字节,常用中文占 3 个字节,emoji 占 4 个字节。以字节为初始词表,基础词表大小固定为 256(一个字节有 256 种可能的取值),无论输入是什么语言、什么符号,都能被编码为字节序列,然后 BPE 从这个统一的字节序列上开始合并。这个方案有许多好处:
- 无需
<UNK>符号:任何文本都能被编码为字节序列,不存在无法处理的字符。 - 跨语言一致性:所有语言在字节层面站在同一起跑线上,BPE 按频率合并时会自然地给每种语言分配合理的子词条目,而非被 Unicode 字符表的大小差异所干扰。
- 基础词表紧凑:256 个基础 token 远小于 Unicode 字符表的规模,留出更多合并空间给有语义价值的子词。
注意,字节级 BPE 和字符级分词(Character-level Tokenization)是两个容易混淆的概念。字符级分词将每个 Unicode 字符直接作为一个 token,中文的"学习"被切分为["学", "习"]两个 token。字节级 BPE 则先将字符编码为字节,再通过合并规则组合成子词,中文的"学习"可能被合并为["学习"]一个 token,也可能拆为["学", "习"],取决于语料中该词的频率。字节级 BPE 本质上仍然是子词分词的一种实现算法,只是在更细的字节粒度上构建合并规则,兼具了字符级分词的覆盖能力和子词分词的语义完整性。GPT-2 之后,字节级 BPE 成为现代 LLM 的主流选择,ChatGLM、DeepSeek、Llama、Qwen 等模型均采用字节级 BPE 构建分词器。
Unigram
BPE 的分词结果是确定且唯一的,模型训练时始终看到相同的子词切分。Unigram 是为了弥补这一不足而提出的另一种子词分词方法,由工藤拓(Taku Kudo)在 2018 年的论文《Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates》中提出。与 BPE 自底向上逐步合并的思路相反,Unigram 采用自顶向下逐步删除的策略,先构造一个巨大的候选子词集(包含语料中所有可能的子串),然后不断删除对训练语料似然贡献最小的子词,直到词表缩减到目标大小。Unigram 是一个定义在子词上的概率模型,给定子词集 ,每个子词 的概率由其在语料中的相对频率决定:
一个词 的分词方式通常不止一种,Unigram 将词的概率定义为所有可能分词方式的概率之和:
其中 是词 的所有可能分词方式集合。在每次迭代中,Unigram 计算删除每个子词后训练语料似然的下降量,移除下降量最小(即对似然贡献最小)的子词,重复这一过程直到词表缩减到预设大小。这种基于似然的剪枝策略使得 Unigram 保留了那些对编码训练语料最有价值的子词,而非仅仅保留频率最高的。
Unigram 还有一个独特的优势是支持子词正则化(Subword Regularization)。由于一个词存在多种概率不同的分词方式,训练时可以从中随机采样而非总是选择概率最高的那种,这使得模型在训练过程中能看到同一词的不同子词组合,从而增强训练的鲁棒性。
词表设计权衡
选择分词算法后,下一步是确定词表大小,词表大小是模型训练的关键设计决策,直接影响模型的效率和能力。较大或较小词表各有优劣,需要在多个因素之间权衡取舍。词表越大,子词表示越完整,序列越短,计算效率越高;但输出层参数随之增多,低频子词表示学习可能不充分。词表越小,模型参数越少,高频子词学习更充分;但序列更长,计算成本变高,语义可能碎片化。下表给出了现代部分开源模型的词表大小:
| 模型 | 词表大小 | 来源 | 国家 |
|---|---|---|---|
| Yi | 64,000 | 01万物 | 中国 |
| Qwen2 | 151,643 | 阿里云 | 中国 |
| DeepSeek-V3 | 129,280 | DeepSeek | 中国 |
| ChatGLM | 151,329 | 智谱AI | 中国 |
| Mistral | 32,000 | Mistral AI | 法国 |
| Llama 3 | 128,000 | Meta | 美国 |
词表设计还必须考虑语言本身,对于混合语言,多语言模型要平衡不同语言的覆盖。譬如英语属于拼音文字,词由空格自然分隔,子词通常是词根、词缀,词表大小适中即可覆盖。中文属于表意文字,词之间无空格分隔,字本身有语义,譬如网上有个名梗叫做"南京市长江大桥",它就可以分词为"南京市/长江/大桥"或"南京市长/江大桥",因此中文 LLM 往往需要更大的词表。
本章小结
语言模型的核心任务是估计文本序列的概率分布。从 N-Gram 到神经网络,建模方式经历了从计数统计到连续表示的范式转换,但"根据前文预测下一个词"这条主线始终未变。N-Gram 模型受限于马尔可夫假设,既无法处理训练中未出现的词组合,也无法捕捉长距离依赖,而神经语言模型通过词向量的分布式表示天然解决了稀疏性问题,并逐步发展出以 GPT 系列为代表的自回归模型和以 BERT 为代表的掩码语言模型两条主要分支。
训练 Transformer 架构语言模型的前置基础已就绪,下一节我们将从零开始训练一个小规模但可应用的语言模型。
练习题
BPE 分词时,合并规则的顺序至关重要。假设训练时依次学到了两条规则:规则 1 合并
(e, r),规则 2 合并(er, </w>)。现在对单词 "lower" 进行分词,初始字符序列为l o w e r。请写出分词过程中每条规则应用后的中间结果,并说明如果交换两条规则的顺序(先应用规则 2 再应用规则 1),分词结果会发生什么变化。参考答案
原始顺序(规则 1 → 规则 2):
- 初始:
['l', 'o', 'w', 'e', 'r'] - 应用规则 1,合并
(e, r)→er:['l', 'o', 'w', 'er'] - 应用规则 2,合并
(er, </w>)→er</w>:['l', 'o', 'w'](er在词尾,与</w>合并,但 "lower" 的字符序列中原本没有</w>,此处仅为说明合并机制。实际上,分词时初始序列是否包含</w>取决于实现。本例中如果不加</w>,规则 2 不匹配,最终结果为['l', 'o', 'w', 'er'])
更准确的演示(加入
</w>):- 初始:
['l', 'o', 'w', 'e', 'r', '</w>'] - 应用规则 1,合并
(e, r)→er:['l', 'o', 'w', 'er', '</w>'] - 应用规则 2,合并
(er, '</w>')→er</w>:['l', 'o', 'w', 'er</w>']
交换顺序(规则 2 → 规则 1):
- 初始:
['l', 'o', 'w', 'e', 'r', '</w>'] - 先应用规则 2,寻找
(er, '</w>')对。当前序列中e和r仍是独立符号,不存在er这个符号,所以规则 2 不匹配,跳过。 - 再应用规则 1,合并
(e, r)→er:['l', 'o', 'w', 'er', '</w>']
最终结果为
['l', 'o', 'w', 'er', '</w>'],与原始顺序的结果['l', 'o', 'w', 'er</w>']不同。关键结论:BPE 分词必须严格按照训练时学到的合并顺序应用规则。先学到的规则对应更高频的模式,优先应用才能确保分词结果与训练时一致。交换顺序可能导致某些规则无法匹配,产生不同的分词结果,进而导致模型在推理时看到的子词序列与训练时不一致,影响生成质量。
- 初始:
Unigram 分词模型中,假设词表 ,各子词概率为 ,,,。对于输入词 "abc",列出所有可能的分词方式,计算每种分词方式的概率,并找出概率最高的分词结果。
参考答案
列出所有分词方式:
"abc" 的所有可能分词方式:
["a", "b", "c"]["ab", "c"]
注意:
["a", "bc"]不合法,因为 "bc" 不在词表中;["abc"]也不合法,"abc" 不在词表中。计算每种分词的概率:
Unigram 模型假设各子词独立,分词概率为各子词概率的乘积。
词的概率(所有分词方式概率之和):
概率最高的分词结果:
["ab", "c"](概率 0.04),远高于["a", "b", "c"]的 0.006。延伸讨论:Unigram 的子词正则化正是利用了这种多分词方式的特性。训练时,不是每次都选择概率最高的
["ab", "c"],而是按概率比例采样:约 87% 的时间选["ab", "c"],约 13% 的时间选["a", "b", "c"]。这让模型在训练中看到同一词的不同子词组合,增强对分词变体的鲁棒性。而 BPE 只能产生唯一的分词结果,缺乏这种正则化能力。下表列出了几个开源模型的词表大小。假设模型隐藏维度 ,计算每个模型输出层的参数量(输出层为 的矩阵)。如果将 Qwen2 的词表从 151,643 缩减到 32,000(与 Mistral 相同),输出层能节省多少参数?这些节省的参数占 Qwen2 原始输出层参数的百分之几?结合序列长度的变化,讨论词表缩小可能带来的负面影响。
模型 词表大小 Yi 64,000 Qwen2 151,643 Mistral 32,000 Llama 3 128,000 参考答案
各模型输出层参数量(,):
模型 词表大小 输出层参数量 Yi 64,000 (约 2.62 亿) Qwen2 151,643 (约 6.21 亿) Mistral 32,000 (约 1.31 亿) Llama 3 128,000 (约 5.24 亿) Qwen2 词表缩减的参数节省:
原始输出层参数:
缩减后输出层参数:
节省参数:(约 4.9 亿)
占比:
负面影响分析:
词表从 151,643 缩减到 32,000,最直接的后果是中文的编码效率大幅下降。Qwen2 使用大词表的一个重要原因是中文需要更多的子词条目来覆盖常用词组和短语。词表缩减后,大量原本有独立子词的中文词组将被拆分为更细粒度的单字甚至字节,导致:
- 序列长度增加:同样的文本,分词后产生更多的 token,序列变长。由于 Transformer 注意力计算复杂度为 ,序列长度翻倍意味着注意力计算量变为 4 倍,训练和推理的成本显著上升。
- 语义碎片化:原本作为一个整体编码的词组(如"机器学习")被拆分为单个字符,模型需要在更长的上下文中重新学习字符之间的组合关系,增加了学习难度。
- 低频子词表示不充分:词表缩小时,保留下来的子词更多是高频通用子词,领域专用词汇和低频子词的训练样本不足,表示质量下降。
这就是词表设计的核心权衡:大词表以更多参数为代价换取更短的序列和更完整的语义单元,小词表节省参数但增加序列长度和语义碎片化风险。Qwen2 选择 15 万词表,正是为了在中文编码效率上取得优势,尽管付出了近 5 亿输出层参数的代价。
