首页
学习
活动
专区
圈层
工具
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

现代语言模型中的分词算法全解:从基础到高级

基础分词(Naive Tokenization)

最简单的分词方式是基于空格将文本拆分为单词。这是许多自然语言处理(NLP)任务中常用的一种分词方法。

text = "Hello, world! This is a test."

tokens = text.split()

print(f"Tokens: {tokens}")

输出结果为:

Tokens: ['Hello,', 'world!', 'This', 'is', 'a', 'test.']

虽然这种方法简单且快速,但存在诸多局限。例如,模型处理文本时需要了解其词汇表——即所有可能的分词集合。采用这种朴素分词,词汇表就是训练数据中出现的所有单词。当模型投入实际应用时,可能遇到词汇表外的新词,这时模型无法处理这些词,或者只能用特殊的“未知”标记替代。

此外,朴素分词对标点和特殊字符的处理也很糟糕。例如,“world!” 会视为一个整体分词,而在另一句中,“world” 可能是单独的分词,这样就会为本质相同的词在词汇表中分配两个不同的标记。类似问题还出现在大小写和连字符的处理上。

为什么要用空格分词?

在英语中,空格用于分隔单词,而单词是语言的基本单位。如果按字节分词,会得到毫无意义的字母序列,模型难以理解文本含义。同理,按句子分词也不可行,因为句子的数量比单词多得多,训练模型理解句子层面的文本需要成倍增加的数据量。

然而,单词真的是最佳分词单位吗?理想情况下,我们希望将文本拆解为最小的有意义单位。例如在德语中,由于大量复合词,基于空格的分词效果很差。即使在英语中,前缀和后缀也往往与其他单词组合表达特定含义,比如 “unhappy” 应该理解为 “un-” + “happy”。

因此,我们需要更好的分词方法。

词干提取与词形还原(Stemming and Lemmatization)

通过实现更复杂的分词算法,可以构建更优的词汇表。例如,以下正则表达式可将文本分割为单词、标点和数字:

import re

text = "Hello, world! This is a test."

tokens = re.findall(r'\w+|[^\w\s]', text)

print(f"Tokens: {tokens}")

为了进一步减少词汇表大小,可以将所有内容转为小写:

import re

text = "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_tab')

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” 并不是有效单词,但这是 Porter 算法的结果。

词形还原的效果更佳,基本总能得到有效单词。用 nltk 实现如下:

import nltk

from nltk.stem import WordNetLemmatizer

from nltk.tokenize import word_tokenize

nltk.download('wordnet')

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 的核心思想是:在训练数据中,反复合并出现频率最高的相邻字符或分词对。

该算法以单个字符为初始词表,不断将最常见的相邻字符对合并为新分词。这个过程持续进行,直到达到期望的词表大小。对于英文文本,你可以仅以字母和少量标点作为初始字符集,然后逐步将常见字母组合引入词表。最终,词表既包含单个字符,也包含常见的子词单元。

BPE 需要在特定数据集上训练,因此其分词方式取决于训练数据。因此,你需要保存并加载 BPE 分词器模型,以便在项目中使用。

BPE 并未规定“单词”如何定义。例如,带连字符的“pre-trained”可以被视为一个单词,也可以拆为两个,这取决于“预分词器”(pre-tokenizer),最简单的形式就是按空格切分。

许多 Transformer 模型都采用 BPE,包括 GPT、BART 和 RoBERTa。你可以直接使用它们训练好的 BPE 分词器。例如调用 Hugging Face Transformers 库如下:

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 用于标识词边界的特殊符号。注意,词语并未被词干提取或词形还原——“models” 保持原样。

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", ..., model=BPE(..., vocab={"[UNK]":0, "[CLS]":1, "[SEP]":2, "[PAD]":3, "[MASK]":4, ...}, merges=[("t", "h"), ("i", "n"), ("e", "r"), ...]))

BpeTrainer对象支持更多训练参数。上述示例中,我们用 Hugging Face 的 datasets 库加载数据集,并在“train”数据上训练分词器。每个数据集结构略有不同——本例包含“test”、“train”和“validation”三部分,每部分有一个名为“text”的字段。我们用ds["train"]["text"]训练,训练器会自动合并直到达到目标词表大小。

训练前后分词器的状态明显不同——训练后,词表中新增了从训练数据中学习到的分词及其 ID。

BPE 分词器最大的优势之一,就是能将未登录词(未知词)拆分为已知的子词单元进行处理。

WordPiece

WordPiece 是 Google 于 2016 年提出的著名分词算法,被 BERT 及其变体广泛采用。它同样是一种子词分词算法。让我们看一个分词示例:

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 类似,都从所有字符出发,合并部分字符生成新的分词。区别在于:

BPE 总是合并出现频率最高的分词对;

WordPiece 则采用最大化似然的得分公式。

BPE 可能会将常见单词拆为子词,而 WordPiece 通常会保留常见单词为单一分词。

用 Hugging Face Tokenizers 训练 WordPiece 分词器的过程与 BPE 类似。例如:

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 都是自底向上的分词算法:它们从所有字符出发,通过合并得到新的词汇单元。与之相对,也可以采用自顶向下的方法,从训练数据的所有单词出发,不断修剪词表直至达到目标大小。

Unigram就是这样一种算法。训练 Unigram 分词器时,每一步都会根据对数似然分数移除一部分词汇项。与 BPE 和 WordPiece 不同,训练好的 Unigram 分词器不是基于规则的,而是基于统计概率。它会保存每个分词的概率,在分词新文本时据此决策。

虽然理论上 Unigram 可以独立存在,但它最常见的实现形式是作为 SentencePiece 框架的一部分。

SentencePiece是一种语言中立的分词算法,无需对输入文本进行预分词(如按空格切分)。这对于多语言场景尤其有用,例如英语使用空格分词,而中文没有空格。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 用特殊的前缀字符(下划线“_”或“▁”)来区分词与词内子词。

训练 SentencePiece 分词器也同样简单,下面是使用 Hugging Face Tokenizers 库进行训练的例子:

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 库实现同样的功能。

延伸阅读

如需了解更多,推荐以下资料:

The Porter Stemming Algorithm

BPE 论文:Neural Machine Translation of Rare Words with Subword Units

WordPiece 论文:Google’s Neural Machine Translation System: Bridging the Gap between Human and Machine Translation

Fast WordPiece Tokenization

Unigram 论文:Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates

SentencePiece 论文:A simple and language independent subword tokenizer and detokenizer for Neural Text Processing

Hugging Face Tokenizers 文档

Google SentencePiece 项目

总结

本文介绍了现代语言模型中常用的分词算法:

BPE

:GPT 等模型广泛采用,通过合并高频相邻对实现子词分词。

WordPiece

:BERT 模型采用,通过最大化训练数据似然分数来合并子词单元。

SentencePiece

:更灵活,可无预分词直接处理多语言文本,底层可选用 BPE 或 Unigram。

现代分词器还包含特殊分词、截断、填充等重要功能。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OdVKOcxBxykUJ9oJC3XrBdag0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券