Tokenizer
·
一、什么是Tokenizer?为什么需要它?
1.1 基本概念
原始文本(人类能读):"我爱吃北京烤鸭"
↓ Tokenizer
Token序列(模型能处理):["我","爱","吃","北京","烤鸭"]
↓ 转成数字ID
ID序列:[101, 2769, 4263, 1166, 1266, 2110, 102]
↓ Embedding
向量矩阵(模型真正的输入)
Tokenizer就像翻译官兼速记员,把人类的语言(文字)先切割成有意义的片段(Token),再把每个片段翻译成模型能理解的数字编号(ID)。
1.2 什么是Token?
Token = 模型处理文本的最小单元
可以是:
- 一个词: "烤鸭" → 1个Token
- 一个字符: "鸭" → 1个Token
- 一个子词: "play"+"ing" → 2个Token
- 一个字节: 某个Unicode字符的字节表示
不同的Tokenizer,切割方式不同!
1.3 三类Tokenizer的演进
文本粒度从粗到细:
Word-based Character-based Subword-based
(词级别) (字符级别) (子词级别)
↓ ↓ ↓
词表太大 序列太长 两者折中!✓
信息丰富 信息稀疏 主流方案
Tokenizer的作用:
- 将文本序列转化为数字序列(token编号),作为transformer的输入
- 是训练和微调LLM必不可少的一部分
二、Word-based Tokenizer(基于词的分词)
2.1 基本原理
将文本按照词语边界切割:
英文(按空格切割):
"I love roast duck"
→ ["I", "love", "roast", "duck"]
中文(按词语切割):
"我爱吃北京烤鸭"
→ ["我", "爱吃", "北京", "烤鸭"]
2.2 词表的构建
收集大量文本,提取所有出现过的词:
词表示例:
ID 0 → [UNK](未知词)
ID 1 → [PAD](填充符)
ID 2 → "the"
ID 3 → "I"
ID 4 → "love"
ID 5 → "duck"
...
ID 50000 → "烤鸭"
词表大小:通常 50,000 ~ 100,000 个词
2.3 核心缺陷
缺陷一:相同意思的词被拆分为不同Token
以下这些词,语义相似,但被视为完全不同的Token:
"run" → ID 1024
"runs" → ID 3721 ← 明明是同一个词的变形!
"running" → ID 8832 ← 完全不同的三个ID
"ran" → ID 2156
模型需要分别学习这四个词的含义,
而无法知道它们其实共享同一个词根!
缺陷二:词表过于巨大
问题的连锁反应:
词表大(10万个词)
↓
Embedding矩阵巨大:
10万词 × 768维 = 7680万个参数!
↓
空间复杂度爆炸 ❌(存储消耗大)
时间复杂度爆炸 ❌(训练速度慢)
↓
还有大量词是低频词,学习效果差
缺陷三:未登录词(OOV问题)
训练时没见过的词:
训练词表里有:"iPhone"
但没有:"iPhone15"
处理"iPhone15"时:
→ 不在词表中!→ 只能用[UNK]替代
→ 信息完全丢失 ❌
新词层出不穷,Word-based完全无法应对!
三、Character-based Tokenizer(基于字符的分词)
3.1 基本原理
将文本切割为最小的字符单元:
英文(按字母切割):
"duck" → ["d", "u", "c", "k"]
中文(按单字切割):
"烤鸭" → ["烤", "鸭"]
3.2 词表结构
词表非常小,只需覆盖基本字符:
英文词表(约100~200个字符):
a, b, c, ..., z ← 26个字母
A, B, C, ..., Z ← 26个大写字母
0, 1, 2, ..., 9 ← 数字
!, ?, ., ,, ... ← 标点符号
中文词表(约数千个常用字):
我、你、他、的、是...
对比Word-based:
Word-based词表:50,000+
Character-based词表:几百~几千
词表缩小了100倍!✓
3.3 核心缺陷
缺陷一:信息量极低,性能差
"duck"被拆成 ["d", "u", "c", "k"]
单个字母本身没有任何语义!
模型需要从零学习:
"d"+"u"+"c"+"k"组合在一起才是"鸭子"
就像要你从26个字母重新学习英语一样困难!
模型性能大幅下降 ❌
缺陷二:Token序列过长
Word-based:
"I love eating roast duck in Beijing"
→ 7个Token
Character-based:
"I love eating roast duck in Beijing"
→ 35个Token(包括空格)
序列长度增加5倍!
后果:
① Transformer的计算复杂度是O(n²)
序列长度5倍 → 计算量25倍!❌
② 长距离依赖更难学习
③ 训练时间大幅增加
四、Subword-based Tokenizer(基于子词的分词)
4.1 核心思想
聪明的折中方案:
高频词 → 保持为整体Token(不再细分)
"the", "I", "is" → 各自1个Token
低频词 → 拆分为高频子词
"unhappiness" → ["un", "happiness"]
或 ["un", "happy", "ness"]
未登录词 → 拆分为字符或字节
"iPhone15" → ["i", "Phone", "15"]
原则:
✅ 常见的不拆,保留语义完整性
✅ 罕见的拆开,避免OOV问题
✅ 子词之间共享信息("play","plays","playing"共享"play")
4.2 BPE:字节对编码(Byte Pair Encoding)
4.2.1 算法原理
BPE是目前最广泛使用的Subword方法,GPT系列都在使用
核心思想:
从字符级词表出发,反复合并“最常出现的相邻字符对,直到达到设定的词表大小!
4.2.2 详细步骤图解
假设我们有以下语料库(corpus):
语料库:
"low" 出现5次
"lower" 出现2次
"newest"出现6次
"widest"出现3次
第0步:初始化(Character-based词表)
将每个词拆成字符,并加上词尾标记</w>:
"low" → l o w </w> (频次5)
"lower" → l o w e r </w> (频次2)
"newest" → n e w e s t </w> (频次6)
"widest" → w i d e s t </w> (频次3)
初始词表:{l, o, w, e, r, n, s, t, i, d, </w>}
共11个字符
第1步:统计相邻Token对的词频
统计所有相邻字符对出现的次数:
(e, s):出现在newest(6次) + widest(3次) = 9次 ← 最高!
(s, t):出现在newest(6次) + widest(3次) = 9次
(e, s)先出现,选择合并(e, s)
第2步:合并最高频的Token对
将"e"+"s"合并为新Token "es":
"low" → l o w </w> (频次5)
"lower" → l o w e r </w> (频次2)
"newest" → n e w es t </w> (频次6) ← es合并
"widest" → w i d es t </w> (频次3) ← es合并
更新词表:{l, o, w, e, r, n, es, t, i, d, </w>}
新增了"es"!
第3步:继续迭代
第2轮:最高频对 = (es, t) 出现9次
合并后:
"newest" → n e w est </w> (频次6)
"widest" → w i d est </w> (频次3)
词表新增:"est"
第3轮:最高频对 = (l, o) 出现7次
合并后:
"low" → lo w </w> (频次5)
"lower" → lo w e r </w> (频次2)
词表新增:"lo"
第4轮:最高频对 = (lo, w) 出现7次
合并后:
"low" → low </w> (频次5)
"lower" → low e r </w> (频次2)
词表新增:"low"
...持续迭代,直到达到设定的词表大小...
最终词表效果:
最终词表(部分):
字符级:l, o, w, e, r, n, s, t, i, d, </w>
子词级:es, est, lo, low, new, newest, ...
测试未见过的词 "lowest":
→ 在词表中找不到整词
→ 拆分:low + est + </w>
→ [low][est][</w>] ← 成功分解,无OOV问题 ✓
对比Word-based:
"lowest" → [UNK] ← 完全丢失信息 ✗
4.2.3 BPE的完整算法流程图
输入:语料库corpus,目标词表大小V
① 初始化词表 = 字符级词表
↓
② 将语料库中每个词拆成字符序列
↓
③ 循环(直到词表大小达到V):
┌─────────────────────────────────┐
│ a. 统计所有相邻Token对的出现频次 │
│ b. 找出频次最高的Token对(x, y) │
│ c. 将(x, y)合并为新Token "xy" │
│ d. 更新语料库中所有(x,y)为"xy" │
│ e. 将"xy"加入词表 │
└─────────────────────────────────┘
↓
④ 输出最终词表
4.2.4 BPE的优缺点
优点:
✅ 词表大小可控(预先设定)
✅ 有效解决OOV问题
✅ 高频词保持完整,低频词拆分为子词
✅ 子词之间可以共享参数(play/plays/playing共享"play")
缺点:
❌ 初始词表基于字符,对非拉丁语系(如中文)效率低
❌ 同一个词可能有多种分割方式
❌ 纯贪心合并,不一定是全局最优
4.3 Byte-level BPE(BBPE) :字节级BPE
4.3.1 为什么需要BBPE?
BPE的问题——初始词表太大:
普通BPE初始词表:
英文:26个字母 + 标点 ≈ 几百个字符
中文:数千个常用汉字!
日文:平假名 + 片假名 + 汉字 ≈ 几千个字符
Emoji: 😀😂🎉... 数百个
多语言混合时:
初始词表 = 所有语言字符的并集 → 巨大!❌
4.3.2 BBPE的核心思路
关键洞察:
所有字符(不管哪种语言)最终都是由字节组成的!
Unicode编码 → UTF-8字节表示:
"A" (ASCII) → 1个字节:0x41
"中" (中文) → 3个字节:0xE4 0xB8 0xAD
"😀" (Emoji) → 4个字节:0xF0 0x9F 0x98 0x80
统一用字节作为基础Token:
初始词表 = 256个字节(0x00 ~ 0xFF)
不管什么语言,都只需要256个基础Token!✓
4.3.3 BBPE vs BPE对比
BPE: BBPE:
初始词表:字符 初始词表:字节(固定256个)
"中" → 1个字符token "中" → 3个字节token
"A" → 1个字符token "A" → 1个字节token
多语言支持:
BPE:需要把所有语言的字符加入初始词表(可能数万个)
BBPE:固定256个字节,天然支持所有语言 ✓
OOV问题:
BPE:遇到罕见字符可能OOV
BBPE:理论上不存在OOV(任何字符都能用字节表示)✓
4.4 WordPiece Tokenization
4.4.1 基本介绍
WordPiece是BERT使用的分词方法,与BPE思路相似,但合并规则不同
BPE vs WordPiece 的核心区别:
BPE合并标准:
→ 选择出现频次最高的相邻Token对合并
WordPiece合并标准:
→ 选择合并后能最大化语言模型似然概率的Token对合并
通俗理解:
BPE:合并出现最多的 (看频率)
WordPiece:合并最有意义的 (看语言学价值)
4.4.2 WordPiece的合并规则详解
WordPiece的评分公式:
score(A, B) = 频率(AB) / (频率(A) × 频率(B))
含义:
- 如果AB经常一起出现(频率(AB)大)→ 分数高 → 值得合并
- 但如果A和B各自也很常见 → 分母大 → 分数降低
直觉解释:
AB一起出现,但A和B单独出现不多 → 说明AB组合有独特含义 → 应该合并!
AB一起出现,但A和B单独出现也很多 → 说明只是偶然相邻 → 不应该合并!
4.4.3 WordPiece的标记方式
WordPiece用"##"标记子词(不是词的开头):
"unhappiness" 可能被分词为:
["un", "##happy", "##ness"]
↑ ↑ ↑
词开头 子词 子词
(##表示我不是词的开头)
对比BPE:
BPE:["un", "happiness"] 或 ["un", "happy", "ness"]
(没有##标记,顺序拼接)
WordPiece用##明确标注了词内部的边界信息!
4.5 Unigram Tokenization
4.5.1 与BPE的根本区别
BPE和WordPiece的方向:
从小到大(Bottom-up)
字符 → 逐步合并 → 子词 → 词
Unigram的方向:
从大到小(Top-down)
大词表 → 逐步删减 → 最优词表 ✓
4.5.2 算法详细步骤
第一步:初始化超大词表
包含所有可能的子词:
从语料库中提取所有可能的子词:
- 所有单字符:a, b, c, d, ...
- 所有二字符子串:ab, bc, cd, ...
- 所有三字符子串:abc, bcd, ...
- 所有完整词:apple, book, running, ...
初始词表:可能有几十万个条目
(远超最终目标词表大小)
第二步:Unigram语言模型假设
假设:每个词独立出现(Unigram = 一元语言模型)
整个词的概率 = 各子词概率的乘积:
"unhappy" 的一种分割:["un", "happy"]
P("un", "happy") = P("un") × P("happy")
"unhappy" 的另一种分割:["u", "n", "h", "a", "p", "p", "y"]
P = P("u") × P("n") × P("h") × ... × P("y")
选择概率最大的分割方式!
(通常较少的、较长的子词概率更高)
第三步:计算每个词的最优分割
对语料库中的每个词,找概率最大的分割:
"playing":
方案1: ["play", "ing"] → P = 0.05 × 0.08 = 0.004 ← 最大!
方案2: ["play", "i", "n", "g"] → P = 0.05 × 0.01 × 0.02 × 0.03 = 0.0000003
方案3: ["p", "l", "a", "y", "ing"] → P更小
选择方案1:["play", "ing"] ✓
(通常使用动态规划Viterbi算法高效计算)
第四步:迭代删除子词
目标:把大词表压缩到目标大小
每轮:
① 尝试删除词表中每一个子词
② 计算删除后,整个语料库的"损失"
(负对数似然 Negative Log Likelihood)
③ 删除那个导致损失增加最小的子词
(损失增加最小 = 这个子词最不重要)
④ 重复,直到词表大小达到目标
为什么是"负对数似然损失"?
P最大 ↔ log(P)最大 ↔ -log(P)最小
删除某子词后,-log(P)增加越少
→ 说明这个子词越不重要
→ 优先删除它!
直观理解:
删除某子词的后果评估:
情况A:删除"playing"(低频完整词)
→ 可以用["play","ing"]替代
→ 替代方案概率高,损失小 → 可以删除
情况B:删除"the"(超高频词)
→ 只能用["t","h","e"]替代
→ 替代方案概率极低,损失大 → 不能删除!
Unigram会优先删除"可被替代"的子词!
第五步:批量删除
每轮不只删1个,而是删除最不重要的p%(通常10%~20%):
当前词表:100,000个子词
每轮删除10%:
第1轮:100,000 → 90,000
第2轮:90,000 → 81,000
第3轮:81,000 → 72,900
...
直到达到目标大小(如32,000)
4.5.3 Unigram的特点
优点:
✅ 可以给一个词提供多种分割方案(概率分布)
✅ 分割结果有理论基础(最大似然)
✅ 在某些语言(如日语)上效果更好
✅ 可以计算每种分割的概率,用于数据增强
缺点:
❌ 算法复杂,计算量大
❌ 需要EM算法估计子词概率
❌ 训练速度慢于BPE
4.6 SentencePiece:跨语言统一分词框架
4.6.1 SentencePiece是什么?
SentencePiece不是一种新算法,
而是一个支持多种算法的统一框架!
支持的算法:
✅ BBPE(Byte-level BPE)
✅ Unigram
代表模型:
- LLaMA(使用SentencePiece + BPE)
- T5(使用SentencePiece + Unigram)
- ALBERT(使用SentencePiece + Unigram)
4.6.2 SentencePiece的核心创新
创新一:直接处理原始文本
传统Tokenizer的流程:
文本 → 预处理(分词、去空格)→ 子词分割 → Token
SentencePiece的流程:
文本 → 直接子词分割 → Token
优点:
✅ 不依赖任何语言相关的预处理
✅ 空格也是Token的一部分(用"▁"表示)
✅ 真正的语言无关(Language-agnostic)
例子:
"Hello World" → ["▁Hello", "▁World"]
(▁表示词前有空格,保留了空格信息)
创新二:统一的多语言处理
传统方案:
中文:需要专用中文分词工具
英文:按空格分割
日文:需要专用日文分词工具
每种语言需要不同的处理方式 ❌
SentencePiece:
统一用字节/字符序列处理
所有语言用同一套流程!✓
结合BBPE:
所有字符转字节(256种)→ BPE合并
天然支持任何语言 ✓
五、实战:在代码中使用Tokenizer
5.1 使用HuggingFace Transformers库
from transformers import AutoTokenizer
# 加载预训练的Tokenizer
# "uer/roberta-base-finetuned-dianping-chinese" 是中文情感分析模型
tokenizer = AutoTokenizer.from_pretrained(
"uer/roberta-base-finetuned-dianping-chinese"
)
# 待处理的文本
sen = "弱小的我也有大梦想!"
# 进行分词(padding填充到max_length=15)
inputs = tokenizer(sen, padding="max_length", max_length=15)
print(inputs)
5.2 输出结果分析
# 输出:
{
"input_ids": [101, 2483, 2207, 4638, 2769, 738, 3300,
1920, 3457, 2682, 106, 102, 0, 0, 0],
"token_type_ids": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"attention_mask": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]
}
- input_ids(Token ID序列)
[101, 2483, 2207, 4638, 2769, 738, 3300, 1920, 3457, 2682, 106, 102, 0, 0, 0]
可视化对应关系:
ID Token 说明
101 [CLS] ← BERT的句子开始标记
2483 弱 ← "弱小"的"弱"
2207 小 ← "弱小"的"小"
4638 的 ← "的"
2769 我 ← "我"
738 也 ← "也"
3300 有 ← "有"
1920 大 ← "大梦想"的"大"
3457 梦 ← "梦"
2682 想 ← "想"
106 ! ← 感叹号
102 [SEP] ← BERT的句子结束标记
0 [PAD] ← 填充符(凑足max_length=15)
0 [PAD] ← 填充符
0 [PAD] ← 填充符
原文11个字符 + [CLS] + [SEP] = 13个有效Token
再加3个[PAD]填充 = 15个Token ✓
- token_type_ids(句子类型标识)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
↑ 全部是0
含义:标记每个Token属于第几个句子
单句任务:全部为0
句子对任务(如问答、自然语言推断):
句子A的Token → 0
[SEP]分隔符 → 0
句子B的Token → 1
例子(句子对):
"今天天气好" + "适合出门"
token_type_ids:
[0, 0, 0, 0, 0, 0, | 1, 1, 1, 1, 1]
←── 句子A ──→ ←── 句子B ──→
3.attention_mask(注意力掩码)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]
↑_________________________真实Token_____↑ ↑__填充__↑
值为1:真实有效的Token,模型应该关注 ✓
值为0:填充符[PAD],模型应该忽略 ✗
可视化:
位置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Token: CLS 弱 小 的 我 也 有 大 梦 想 ! SEP PAD PAD PAD
mask: 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0
Self-Attention计算时:
mask=0的位置会被加上-inf → Softmax后变成0 → 被完全忽略
这样填充符就不会影响模型对真实内容的理解!
5.3 常用Tokenizer操作
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
"uer/roberta-base-finetuned-dianping-chinese"
)
# ① 基本分词
text = "弱小的我也有大梦想!"
tokens = tokenizer.tokenize(text)
print(tokens)
# ['弱', '小', '的', '我', '也', '有', '大', '梦', '想', '!']
# ② 转换为ID
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
# [2483, 2207, 4638, 2769, 738, 3300, 1920, 3457, 2682, 106]
# ③ ID转回Token(解码)
decoded = tokenizer.decode(ids)
print(decoded)
# "弱小的我也有大梦想!"
# ④ 批量处理(多个句子)
sentences = ["弱小的我也有大梦想!", "今天天气真好!"]
batch_inputs = tokenizer(
sentences,
padding=True, # 自动填充到最长句子
truncation=True, # 超长截断
max_length=20, # 最大长度
return_tensors="pt" # 返回PyTorch张量
)
print(batch_inputs['input_ids'].shape) # torch.Size([2, 20])
# ⑤ 查看特殊Token
print(tokenizer.special_tokens_map)
# {'unk_token': '[UNK]', 'sep_token': '[SEP]',
# 'pad_token': '[PAD]', 'cls_token': '[CLS]',
# 'mask_token': '[MASK]'}
# ⑥ 查看词表大小
print(tokenizer.vocab_size)
# 21128(BERT中文词表大小)
六、特殊Token详解
[CLS] (Classification Token)
├── 位置:句子开头
├── 作用:聚合整个句子的信息,用于分类任务
└── 例:[CLS] 今天 天气 真好 [SEP] → 取[CLS]的向量做情感分类
[SEP] (Separator Token)
├── 位置:句子结尾,或两句话之间
├── 作用:标记句子边界
└── 例:[CLS] 问题 [SEP] 答案 [SEP]
[PAD] (Padding Token)
├── 位置:句子末尾填充
├── 作用:让批次中所有句子长度一致
└── attention_mask=0,模型忽略它
[UNK] (Unknown Token)
├── 位置:遇到词表外的词时
├── 作用:替代未登录词
└── 注意:子词方法基本消除了[UNK]
[MASK] (Mask Token) ← BERT特有
├── 位置:随机替换某些Token(训练时)
├── 作用:让模型学习预测被遮盖的词
└── 例:今天 [MASK] 真好 → 模型预测[MASK]是"天气"
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)