【精选优质专栏推荐】
- 《AI 技术前沿》 —— 紧跟 AI 最新趋势与应用
- 《网络安全新手快速入门(附漏洞挖掘案例)》 —— 零基础安全入门必看
- 《BurpSuite 入门教程(附实战图文)》 —— 渗透测试必备工具详解
- 《网安渗透工具使用教程(全)》 —— 一站式工具手册
- 《CTF 新手入门实战教程》 —— 从题目讲解到实战技巧
- 《前后端项目开发(新手必知必会)》 —— 实战驱动快速上手
每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。
文章目录

前言
分词是自然语言处理(NLP)中的关键预处理步骤,它将原始文本转换为语言模型可以处理的“词元”(tokens)。现代语言模型使用复杂的分词算法来应对人类语言的多样性和复杂性。
本文将介绍现代大型语言模型(LLM)中常用的分词算法、它们的实现方法,以及如何使用这些算法。
原始分词(Naive Tokenization)
最简单的分词方法是基于空格将文本拆分为词元(tokens)。这种方法在许多 NLP 任务中都很常见。
text = "Hello, world! This is a test."
tokens = text.split()
print(f"Tokens: {tokens}")
输出结果为:
Tokens: ['Hello,', 'world!', 'This', 'is', 'a', 'test.']

虽然这种方法简单且高效,但也存在一些显著的局限性。首先,需要注意模型在处理文本时必须依赖词汇表(vocabulary,即所有可能词元的集合)。在使用这种原始分词方法时,词汇表由训练语料中的所有单词构成。模型训练过程中会基于训练数据构建词汇表,但在实际应用中,若遇到词汇表中未收录的单词,模型要么无法处理,要么只能以特殊的“未知(unknown)”词元替代。
另一个问题在于原始分词对标点符号和特殊字符的处理效果不佳。例如,“world!” 可能被视为一个完整的词元,而在另一句话中,“world” 又会作为独立的词元出现,从而导致同一个单词在词汇表中对应多个不同的词元。类似地,大写字母、连字符等情况也会引发同类问题。
为什么按空格分词?
在英文中,空格是分隔单词的方式,而单词是语言的基本单位。如果按字节分词,会得到无意义的字母组合,使模型难以理解文本含义。同样,按句子分词也不理想,因为句子的数量远大于单词数量。在句子级别训练模型需要成比例更多的训练数据。
单词是最优的分词单位吗?
理想情况下,我们希望将文本拆分为最小的有意义单位。在德语中,空格分词并不理想,因为存在大量复合词。即便在英文中,前缀和后缀也能改变词义。例如,“unhappy” 应被理解为 “un-” + “happy”。
因此,我们需要一种更好的分词方法。
词干提取(Stemming)与词形还原(Lemmatization)
通过使用更复杂的分词算法,可以构建更合理的词汇表。
例如,下面的正则表达式可以将文本拆分为单词、标点符号和数字:
import re
text = "Hello, world! This is a test."
tokens = re.findall(r'\w+|[^\w\s]', text)
print(f"Tokens: {tokens}")
输出结果为:
Tokens: ['Hello', ',', 'world', '!', 'This', 'is', 'a', 'test', '.']

为了进一步缩小词汇表,可以将所有文本转换为小写:
tokens = re.findall(r'\w+|[^\w\s]', text.lower())
print(f"Tokens: {tokens}")
输出结果为:
Tokens: ['hello', ',', 'world', '!', 'this', 'is', 'a', 'test', '.']

然而,这仍然无法解决单词变形的问题。
词干提取(Stemming)和词形还原(Lemmatization)是将单词还原到其词根形式的两种方法。
- 词干提取是一种较为激进的方法,它通过规则去掉前缀和后缀。
- 词形还原则更温和,通过词典将单词还原为基础形式。两者都是针对特定语言的,但词干提取可能产生无效单词。
在英文中,常用的 Porter 词干算法可以通过 nltk 实现:
import nltk
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
# 如果未下载所需资源
nltk.download('punkt')
text = "These models may become unstable quickly if not initialized."
stemmer = PorterStemmer()
words = word_tokenize(text)
stemmed_words = [stemmer.stem(word) for word in words]
print(stemmed_words)
输出结果为:
['these', 'model', 'may', 'becom', 'unstabl', 'quickli', 'if', 'not', 'initi', '.']
可以看到,“unstabl” 并不是一个有效单词。
相比之下,词形还原更温和,几乎总能生成有效单词。使用 nltk 进行词形还原的示例:
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
# 如果未下载所需资源
nltk.download('wordnet')
nltk.download('punkt')
text = "These models may become unstable quickly if not initialized."
lemmatizer = WordNetLemmatizer()
words = word_tokenize(text)
lemmatized_words = [lemmatizer.lemmatize(word) for word in words]
print(lemmatized_words)
输出结果为:
['These', 'model', 'may', 'become', 'unstable', 'quickly', 'if', 'not', 'initialized', '.']
在这两种方法中,首先需要对文本进行分词,然后再使用词干器或词形还原器进行转换。这一归一化步骤可以生成更加一致的词汇表,但分词的根本问题(如对子词的识别)仍然没有解决。
字节对编码(Byte-Pair Encoding, BPE)
字节对编码(BPE)是现代语言模型中最广泛使用的分词算法之一。它最初是作为一种文本压缩算法提出的,后来被引入机器翻译,并最终被 GPT 模型采用。
BPE 的工作方式是:在训练数据中迭代合并出现频率最高的相邻字符或 token 对。
该算法从一个仅包含单个字符的词表开始,逐步将出现频率最高的相邻字符对合并为新的 token。这个过程不断重复,直到达到所需的词表大小。对于英文文本,可以仅以字母和少量标点作为初始字符集,起始词表非常小,然后逐步将常见的字母组合加入词表。最终得到的词表既包含单个字符,也包含常见的子词单元。
BPE 的训练依赖于特定的数据,因此其具体的分词方式取决于训练语料。因此,在项目中使用时,需要保存并加载 BPE 分词器模型。
BPE 并未严格规定如何定义一个词。例如,“pre-trained” 这样的带连字符的词,可以被视为一个词,也可以被分成两个词,这由“预分词器”(pre-tokenizer)决定。在最简单的形式下,预分词器通过空格来切分单词。
许多 Transformer 模型都使用 BPE,包括 GPT、BART 和 RoBERTa,我们可以直接使用它们训练好的 BPE 分词器。
以下示例展示了如何使用 Hugging Face Transformers 库中的 BPE 分词器:
from transformers import GPT2Tokenizer
# 加载 GPT-2 分词器(使用 BPE)
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
# 对文本进行分词
text = "Pre-trained models are available."
tokens = tokenizer.encode(text)
print(f"Token IDs: {tokens}")
print(f"Tokens: {tokenizer.convert_ids_to_tokens(tokens)}")
print(f"Decoded: {tokenizer.decode(tokens)}")
其输出为:
Token IDs: [6719, 12, 35311, 4981, 389, 1695, 13]
Tokens: ['Pre', '-', 'trained', 'Ġmodels', 'Ġare', 'Ġavailable', '.']
Decoded: Pre-trained models are available.
可以看到,分词器使用特殊符号 “Ġ” 来表示单词之间的空格。这是 BPE 用于标识词边界的特殊 token。需要注意的是,BPE 不会进行词干提取(stemming)或词形还原(lemmatization),例如 “models” 保持原样,而不会被转化为 “model”。
除了 Hugging Face 的分词器,另一种方案是OpenAI 的 tiktoken 库。
示例如下:
import tiktoken
encoding = tiktoken.get_encoding("cl100k_base")
text = "Pre-trained models are available."
tokens = encoding.encode(text)
print(f"Token IDs: {tokens}")
print(f"Tokens: {[encoding.decode_single_token_bytes(t) for t in tokens]}")
print(f"Decoded: {encoding.decode(tokens)}")
要训练你自己的 BPE 分词器,最简单的方法是使用 Hugging Face 的 Tokenizers 库。
示例如下:
from datasets import load_dataset
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.pre_tokenizers import Whitespace
from tokenizers.trainers import BpeTrainer
ds = load_dataset("Salesforce/wikitext", "wikitext-103-raw-v1")
print(ds)
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace()
trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
print(tokenizer)
tokenizer.train_from_iterator(ds["train"]["text"], trainer)
print(tokenizer)
tokenizer.save("my-tokenizer.json")
# 重新加载训练好的分词器
tokenizer = Tokenizer.from_file("my-tokenizer.json")
运行后,我们会看到:
DatasetDict({
test: Dataset({
features: ['text'],
num_rows: 4358
})
train: Dataset({
features: ['text'],
num_rows: 1801350
})
validation: Dataset({
features: ['text'],
num_rows: 3760
})
})
Tokenizer(version="1.0", truncation=None, padding=None, added_tokens=[], normalizer=None,
pre_tokenizer=Whitespace(), post_processor=None, decoder=None, model=BPE(..., vocab={}, merges=[]))
[00:00:04] Pre-processing sequences ███████████████████████████ 0 / 0
[00:00:00] Tokenize words ███████████████████████████ 608587 / 608587
[00:00:00] Count pairs ███████████████████████████ 608587 / 608587
[00:00:02] Compute merges ███████████████████████████ 25018 / 25018
Tokenizer(version="1.0", truncation=None, padding=None, added_tokens=[
{"id":0, "content":"[UNK]", ...},
{"id":1, "content":"[CLS]", ...},
{"id":2, "content":"[SEP]", ...},
{"id":3, "content":"[PAD]", ...},
{"id":4, "content":"[MASK]", ...}],
...
model=BPE(..., vocab={"[UNK]":0, "[CLS]":1, "[SEP]":2, "[PAD]":3, "[MASK]":4, ...},
merges=[("t", "h"), ("i", "n"), ("e", "r"), ("a", "n"), ("th", "e"), ...]))
BpeTrainer 对象提供了更多参数来控制训练过程。在上面的例子中,我们使用 Hugging Face 的 datasets 库加载了一个数据集,并在其中的文本数据上训练了分词器。
每个数据集的结构都不同,这个数据集包含 “test”、“train” 和 “validation” 三个分片,每个分片都有一个名为 “text” 的特征字段,里面存放字符串。我们使用 ds[“train”][“text”] 来训练分词器,并由训练器自动找到合并规则,直到达到所需的词表大小。
可以看到,分词器在训练前后处于不同状态:经过训练后,从语料中学习到的词元会被加入词汇表,并与相应的 token ID 建立映射关系。
BPE 分词器的一个关键优势是它能够通过将未知词分解为已知的子词单元来处理未登录词(OOV)。
WordPiece
WordPiece 是 Google 在 2016 年提出的一种常用分词算法,被广泛应用于 BERT 及其变体中。它同样属于子词分词(subword tokenization)算法。
让我们看看它是如何对一句话进行分词的:
from transformers import BertTokenizer
# 从 BERT 加载 WordPiece 分词器
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
# 对文本进行分词
text = "These models are usually initialized with Gaussian random values."
tokens = tokenizer.encode(text)
print(f"Token IDs: {tokens}")
print(f"Tokens: {tokenizer.convert_ids_to_tokens(tokens)}")
print(f"Decoded: {tokenizer.decode(tokens)}")
运行结果如下:
Token IDs: [101, 2122, 4275, 2024, 2788, 3988, 3550, 2007, 11721, 17854, 2937, 6721, 5300, 1012, 102]
Tokens: ['[CLS]', 'these', 'models', 'are', 'usually', 'initial', '##ized', 'with', 'ga', '##uss', '##ian', 'random', 'values', '.', '[SEP]']
Decoded: [CLS] these models are usually initialized with gaussian random values. [SEP]
从结果可以看到,分词器将 “initialized” 拆分成了 “initial” 和 “##ized”。其中 ## 前缀表示该词片段是前一个词的一部分;如果一个词没有 ## 前缀,则默认它的前面有一个空格。
这个结果中还包含了一些 BERT 特定的设计:
- 在这个 BERT 模型里,所有文本会被转成小写,分词器会自动完成这一处理。
- BERT 假设所有输入序列以 [CLS] 开头,以 [SEP] 结束,因此分词器会自动加上这些特殊标记。
- 这些并不是 WordPiece 算法本身的要求,所以在其他模型中可能不会出现。
WordPiece 与 BPE(Byte Pair Encoding) 很相似:
- 两者都从所有字符开始,并逐步合并生成新的词表单元。
- BPE 每次合并出现频率最高的词元对;
- WordPiece 使用一个最大化似然的评分公式来决定合并。
- 核心区别是:BPE 可能会把常见词也拆成子词,而 WordPiece 通常会将常见词保持为完整词元。
使用 Hugging Face 的 tokenizers 库训练一个 WordPiece 分词器的方式与 BPE 类似,可以通过 WordPieceTrainer 来实现。
例如:
from datasets import load_dataset
from tokenizers import Tokenizer
from tokenizers.models import WordPiece
from tokenizers.pre_tokenizers import Whitespace
from tokenizers.trainers import WordPieceTrainer
ds = load_dataset("Salesforce/wikitext", "wikitext-103-raw-v1")
tokenizer = Tokenizer(WordPiece(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace()
trainer = WordPieceTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
tokenizer.train_from_iterator(ds["train"]["text"], trainer)
tokenizer.save("my-tokenizer.json")
SentencePiece 与 Unigram
BPE 和 WordPiece 属于自底向上(bottom-up)的构建方式,它们从完整的字符集出发,通过不断合并生成新的词表单元。另一种思路是自顶向下(top-down):从训练语料中的所有词开始,逐步裁剪词表,直到达到目标大小。
Unigram 便是这种方法的代表。在训练 Unigram 分词器的过程中,每一步都会根据对数似然评分(log-likelihood score)移除部分词汇项。与 BPE 和 WordPiece 不同,Unigram 分词器并非基于规则,而是建立在统计模型之上。它会为每个词元保存似然值,并据此决定新文本的分词方式。
虽然 Unigram 可以独立使用,但更常见的应用形式是作为 SentencePiece 的一部分。
SentencePiece 是一种语言无关(language-neutral)的分词算法,不需要对输入文本进行预分词,在多语言场景下尤其适用。例如,英语通过空格区分单词,而中文则没有空格。SentencePiece 会将输入视为连续的 Unicode 字符流,再结合 BPE 或 Unigram 来完成分词构建。
下面展示如何在 Hugging Face Transformers 库中使用 SentencePiece 分词器:
from transformers import T5Tokenizer
# 加载 T5 分词器(使用 SentencePiece + Unigram)
tokenizer = T5Tokenizer.from_pretrained("t5-small")
# 对文本进行分词
text = "SentencePiece is a subword tokenizer used in models such as XLNet and T5."
tokens = tokenizer.encode(text)
print(f"Token IDs: {tokens}")
print(f"Tokens: {tokenizer.convert_ids_to_tokens(tokens)}")
print(f"Decoded: {tokenizer.decode(tokens)}")
输出结果如下:
Token IDs: [4892, 17, 1433, 345, 23, 15, 565, 19, 3, 9, 769, 6051, 14145, 8585, 261, 16, 2250, 224, 38, 3, 4, 434, 9688, 11, 332, 9125, 1]
Tokens: ['▁Sen', 't', 'ence', 'P', 'i', 'e', 'ce', '▁is', '▁', 'a', '▁sub', 'word', '▁token', 'izer', '▁used', '▁in', '▁models', '▁such', '▁as', '▁', 'X', 'L', 'Net', '▁and', '▁T', '5.', '']
Decoded: SentencePiece is a subword tokenizer used in models such as XLNet and T5.
与 WordPiece 类似,SentencePiece 也会使用一个特殊前缀来区分词和子词。这里使用的是下划线符号▁来标记词边界。
使用 Hugging Face 的 tokenizers 库训练 SentencePiece 分词器也很简单,例如:
from datasets import load_dataset
from tokenizers import SentencePieceUnigramTokenizer
ds = load_dataset("Salesforce/wikitext", "wikitext-103-raw-v1")
tokenizer = SentencePieceUnigramTokenizer()
tokenizer.train_from_iterator(ds["train"]["text"])
tokenizer.save("my-tokenizer.json")
我们也可以直接使用 Google 的 sentencepiece 库来完成相同的任务。
总结
在本文中,我们探讨了现代语言模型中使用的不同分词算法。主要内容包括:
-
BPE
广泛应用于 GPT 模型,通过合并高频相邻子词对来构建词表; -
WordPiece
用于 BERT 模型,通过最大化训练数据的似然来选择子词; -
SentencePiece
更加灵活,无需预分词即可处理多种语言; -
现代分词器功能
支持特殊符号(special tokens)、截断(truncation)和填充(padding)。
理解这些分词算法对于使用现代语言模型以及高效预处理文本数据至关重要。
1870

被折叠的 条评论
为什么被折叠?



