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

article/2025/7/4 16:58:49

基础分词(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 retext = "Hello, world! This is a test."
tokens = re.findall(r'\w+|[^\w\s]', text)
print(f"Tokens: {tokens}")

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

import retext = "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_tokenizenltk.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_tokenizenltk.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 tiktokenencoding = 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 BpeTrainerds = 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 WordPieceTrainerds = 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 SentencePieceUnigramTokenizerds = 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。

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


http://www.hkcw.cn/article/GffGWLRbjt.shtml

相关文章

Deepseek给出的8255显示例程

#include <stdio.h> #include <conio.h> #include <dos.h>// 定义8255端口地址 (根据原理图译码确定) #define PORT_8255_A 0x8000 // PA端口地址 #define PORT_8255_B 0x8001 // PB端口地址 #define PORT_8255_C 0x8002 // PC端口地址 #define PORT_8255…

计算机组成原理——CPU的功能和基本结构

5.1 CPU的功能和基本结构 整理自beokayy课程视频 1.CPU的组成 程序计数器&#xff08;PC&#xff09;&#xff1a; 存放即将执行指令的地址。顺序执行时&#xff0c;PC“1”形成下条指令地址。在有的机器中&#xff0c;PC本身具有“1”计数功能&#xff0c;也有的机器借助运算…

LINUX62软链接;核心目录;错题:rpm -qa |grep<包名> 、rpm -ql<包名>;rm -r rm -rf;合并 cat

硬链接 软链接 软链接 [rootcode axel-2.4]# which axel /usr/bin/which: no axel in (/sbin:/bin:/usr/sbin:/usr/bin) [rootcode axel-2.4]# ln -s /opt/axel/bin/axel /usr/bin [rootcode axel-2.4]# axel https://mirrors.aliyun.com/centos-stream/ 初始化下载: https:/…

[Java恶补day13] 53. 最大子数组和

休息了一天&#xff0c;开始补上&#xff01; 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组是数组中的一个连续部分。 示例 1&#xff1a; 输入&#xff1a;nums …

C++哈希表:冲突解决与高效查找

引入 通过CSTL库中的unordered_map和unordered_set的学习&#xff0c;我们还需要其底层结构是什么&#xff0c;如何实现的&#xff0c;本节重点讲解哈希 哈希概念 顺序结构以及平衡树中&#xff0c;元素关键码与其存储位置之间没有对应关系&#xff0c;因此在查找一个元素是…

【科研绘图系列】R语言绘制论文比较图(comparison plot)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理画图1画图2画图3系统信息介绍 这篇文章详细介绍了如何使用R语言进行工作流中不同步骤的比较分析,包括数据预处理、特征选择、模型训练和结果可…

录屏不再难,从功能到体验深度测评

在日常工作和学习中&#xff0c;录屏是一项非常常见的需求&#xff0c;比如制作教程、记录操作过程、录制线上会议等。市面上虽然有不少录屏工具&#xff0c;但要么功能受限&#xff0c;要么广告繁多&#xff0c;甚至需要付费解锁高级功能&#xff0c;使用起来并不方便。 这款…

2023年12月6级第一套第一篇

在这里才找完题干&#xff0c;所以答案在下一句虽然不重要&#xff0c;不用看&#xff0c;重要的是但是 A&#xff1a;critic在虽然部分&#xff0c;不重要&#xff0c; c选项前面的部分也在虽然部分&#xff0c;不重要定位的下一句就是答案&#xff0c;还有冒号&#xff0c;看…

Linux --TCP协议实现简单的网络通信(中英翻译)

一、什么是TCP协议 1.1 、TCP是传输层的协议&#xff0c;TCP需要连接&#xff0c;TCP是一种可靠性传输协议&#xff0c;TCP是面向字节流的传输协议&#xff1b; 二、TCPserver端的搭建 2.1、我们最终好实现的效果是 客户端在任何时候都能连接到服务端&#xff0c;然后向服务…

多群组部署

相关概念 星形拓扑和并行多组 如下图&#xff0c;星形组网拓扑和并行多组组网拓扑是区块链应用中使用较广泛的两种组网方式。 星形拓扑&#xff1a;中心机构节点同时属于多个群组&#xff0c;运行多家机构应用&#xff0c;其他每家机构属于不同群组&#xff0c;运行各自应用…

unidbg patch 初探 微博deviceId 案例

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向过程 看了b站迷人瑞信那个由于是…

如何自定义WordPress主题(5个分步教程)

如果您已经安装了一个 WordPress 主题&#xff0c;但它不太适合您&#xff0c;您可能会感到沮丧。在定制 WordPress 主题方面&#xff0c;您有很多选择。 挑战在于找到正确的方法。 在本篇文章中&#xff0c;我将引导您了解自定义 WordPress 主题的各种选项&#xff0c;帮助您…

【兽医处方专用软件】佳易王兽医电子处方软件:高效智能的宠物诊疗管理方案

一、软件概述与核心优势 &#xff08;一&#xff09;试用版获取方式 资源下载路径&#xff1a;进入博主头像主页第一篇文章末尾&#xff0c;点击卡片按钮&#xff1b;或访问左上角博客主页&#xff0c;通过右侧按钮获取详细资料。 说明&#xff1a;下载文件为压缩包&#xff…

【nssctf第三题】[NSSCTF 2022 Spring Recruit]easy C

这是题目&#xff0c;下载附件打开是个C文件 #include <stdio.h> #include <string.h>int main(){char a[]"wwwwwww";char b[]"dvxbQd";//try to find out the flagprintf("please input flag:");scanf(" %s",&a);if…

DAY41 CNN

可以看到即使在深度神经网络情况下&#xff0c;准确率仍旧较差&#xff0c;这是因为特征没有被有效提取----真正重要的是特征的提取和加工过程。MLP把所有的像素全部展平了&#xff08;这是全局的信息&#xff09;&#xff0c;无法布置到局部的信息&#xff0c;所以引入了卷积神…

助力活力生活的饮食营养指南

日常生活中&#xff0c;想要维持良好的身体状态&#xff0c;合理的营养补充至关重要。对于易受身体变化困扰的人群来说&#xff0c;更需要从饮食中摄取充足养分。​ 蛋白质是身体的重要 “建筑材料”&#xff0c;鱼肉、鸡肉、豆类制品富含优质蛋白&#xff0c;易于消化吸收&am…

CA-Net复现

复现结果–Dice&#xff1a;90.093802&#xff0c;Jaccard&#xff1a;82.077802&#xff0c;95HD&#xff1a;6.89387267&#xff0c;ASD&#xff1a;1.76263258&#xff0c;与原文一致 感想 第16篇完全复现的论文

【具身智能】【机械臂】各类机械臂对比

选购指标 选购指标 说明机械-负载1w以内通常200g负载&#xff08;一袋酸奶&#xff09;&#xff0c;1w-5w 1kg负载&#xff08;1L饮料&#xff09;&#xff0c;5w 3kg负载机械-精度越贵精度越高机械-夹爪是否支持更换夹爪等&#xff0c;能否支持力控夹爪机械-AGV扩展 …

云服务器无法远程连接怎么办?

当云服务器无法远程连接&#xff08;比如 SSH、RDP 连接不上&#xff09;时&#xff0c;可以按照以下步骤逐一排查和解决&#xff1a; ✅ 一、检查网络连通性 &#x1f539; 1. 确认公网 IP 是否正确 登录云服务商后台查看分配给服务器的公网 IP&#xff0c;确保你连接的目标地…

PolyGen:一个用于 3D 网格的自回归生成模型 论文阅读

[2002.10880] PolyGen&#xff1a;一个用于 3D 网格的自回归生成模型 --- [2002.10880] PolyGen: An Autoregressive Generative Model of 3D Meshes 图 2&#xff1a;PolyGen 首先生成网格顶点&#xff08;左侧&#xff09;&#xff0c;然后基于这些顶点生成网格面&#xff0…