mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2618 字
7 分钟
BPE 与 SentencePiece:LLM 分词器原理
2025-01-18

分词(Tokenization)是大语言模型处理文本的第一步,也是最容易被忽视的关键环节。将原始文本转换为 Token 序列的策略,直接影响模型的词汇覆盖率、多语言支持能力和推理效率。2016 年 Sennrich 等人提出的 BPE(Byte-Pair Encoding)和 2018 年 Kudo 等人提出的 SentencePiece,奠定了现代 LLM 分词器的基础。GPT-4 使用 BPE(cl100k_base),BERT 使用 WordPiece,LLaMA 使用 SentencePiece——不同的分词策略对模型性能有着深远影响。

理解分词器是理解 LLM 的第一步。一个不好的分词器会让模型”看”不到正确的语言单位,从而限制其理解和生成能力。

本文要点#

  • 分词问题的三种粒度:词级、字符级、子词级
  • BPE(Byte-Pair Encoding)算法的详细原理与实现
  • WordPiece(BERT 使用)的核心机制
  • Unigram Language Model 与 SentencePiece
  • 主流 LLM 的分词器对比:GPT、BERT、LLaMA、Qwen
  • OOV 处理、多语言支持和压缩率分析
  • tiktoken、sentencepiece、tokenizers 库的代码示例
  • 词汇表大小与序列长度的权衡

一、分词问题的本质#

0.1 三种分词粒度#

graph TD A["原始文本:<br/>I love machine learning"] --> B["词级分词<br/>(Word-Level)"] A --> C["字符级分词<br/>(Character-Level)"] A --> D["子词级分词<br/>(Subword-Level)"] B --> B1["['I', 'love', 'machine', 'learning']<br/>词汇表大、OOV 严重"] C --> C1["['I', ' ', 'l', 'o', 'v', 'e', ...]<br/>无 OOV、序列过长"] D --> D1["['I', 'love', 'mach', 'ine', 'learn', 'ing']<br/>平衡:无 OOV + 合理长度"] style D fill:#4CAF50,color:#fff style D1 fill:#4CAF50,color:#fff

0.2 各粒度的优缺点#

粒度优点缺点代表模型
词级直观、语义完整OOV 问题严重、词汇表巨大早期 NMT
字符级无 OOV、极小词汇表序列过长、语义信息弱CharRNN
子词级平衡 OOV 和序列长度需要预训练分词器GPT、BERT、LLaMA

子词级分词的核心思想是:高频词保留为完整词,低频词拆分为有意义的子词单元。

二、BPE:字节对编码#

0.3 算法原理#

BPE(Byte-Pair Encoding)最初是一种数据压缩算法,2016 年被 Sennrich 等人引入 NLP 领域用于分词。其核心思想是:迭代地合并最频繁的字符对,构建子词词汇表。

0.4 算法步骤#

flowchart TD A["初始化:将每个单词拆分为字符序列"] --> B["统计所有相邻字符对的频率"] B --> C["合并频率最高的字符对"] C --> D{"达到目标词汇表大小?"} D --> |"否"| B D --> |"是"| E["得到 BPE 词汇表"] style E fill:#4CAF50,color:#fff

0.5 详细示例#

以训练语料 low lower lowest 为例:

Step 1: 初始化(字符级)

l o w </w>
l o w e r </w>
l o w e s t </w>

Step 2: 统计字符对频率

(l, o): 3 次
(o, w): 3 次
(w, e): 2 次
(e, r): 1 次
(e, s): 1 次
(s, t): 1 次

Step 3: 合并最高频对 (l, o) → lo

lo w </w>
lo w e r </w>
lo w e s t </w>

Step 4: 合并 (lo, w) → low

low </w>
low e r </w>
low e s t </w>

Step 5: 合并 (w, e) → we…(继续直到目标词汇表大小)

0.6 编码新文本#

训练好 BPE 后,对新文本编码就是按照学到的合并规则,从最优先级最高的合并开始应用:

def bpe_encode(token, merges):
"""将一个单词用 BPE 编码"""
word = list(token) + ['</w>']
while len(word) > 1:
# 找到优先级最高的合并对
pairs = [(word[i], word[i+1]) for i in range(len(word)-1)]
best_pair = min(pairs, key=lambda p: merges.get(p, float('inf')))
if best_pair not in merges:
break # 没有更多合并规则可用
# 应用合并
new_word = []
i = 0
while i < len(word):
if i < len(word) - 1 and (word[i], word[i+1]) == best_pair:
new_word.append(word[i] + word[i+1])
i += 2
else:
new_word.append(word[i])
i += 1
word = new_word
return word
# 示例
merges = {('l', 'o'): 0, ('lo', 'w'): 1, ...}
print(bpe_encode("lowest", merges))
# 输出: ['low', 'est</w>']

0.7 Byte-level BPE#

现代 LLM(如 GPT-2/3/4)使用 Byte-level BPE,将字节(而非字符)作为基本单元:

  • 优点:天然支持所有语言和特殊字符,无需预处理
  • 词汇表:256 个字节 + 学到的合并规则
  • 无 OOV:任何文本都可以用 UTF-8 字节表示
# Byte-level BPE 的基本单元是字节
text = "你好世界" # 中文
bytes_repr = text.encode('utf-8')
# b'\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c'
# 每个中文字符 = 3 个字节

三、WordPiece:BERT 的分词器#

WordPiece 是 Google 为 BERT 开发的子词分词算法,与 BPE 类似但有重要区别。

0.8 核心区别#

BPE 选择频率最高的字符对合并,而 WordPiece 选择似然增益最大的字符对合并:

# BPE: 选择频率最高的对
best_pair = max(pairs, key=lambda p: count[p])
# WordPiece: 选择似然增益最大的对
# Score = P(AB) / (P(A) * P(B))
best_pair = max(pairs, key=lambda p: likelihood_gain(p))

0.9 特殊标记#

WordPiece 使用 ## 前缀表示子词的续接部分:

输入: "unbelievable"
分词: ["un", "##believe", "##able"]
输入: "helloworld"
分词: ["hello", "##world"]

## 前缀让模型知道这个子词是前一个子词的延续,而不是独立的词。

四、Unigram Language Model#

Unigram LM 是 SentencePiece 支持的另一种子词分词方法,与 BPE 的”自底向上合并”相反,Unigram LM 是”自顶向下裁剪”。

0.10 算法原理#

  1. 初始化:从一个超大词汇表开始(如所有字符 + 常见子串)
  2. 计算每个子词的概率:基于 Unigram 语言模型
  3. 计算每个子词的重要性:移除后对整体似然的损失
  4. 裁剪:移除最不重要的子词(保留前 V 个)
  5. 迭代:重复 2-4 直到词汇表达到目标大小
graph TD A["超大初始词汇表<br/>(所有字符+常见子串)"] --> B["训练 Unigram LM<br/>估计每个子词概率"] B --> C["计算每个子词的重要性"] C --> D["移除最不重要的子词"] D --> E{"达到目标词汇表大小?"} E --> |"否"| B E --> |"是"| F["最终词汇表"] style F fill:#4CAF50,color:#fff

0.11 关键优势#

Unigram LM 的一个重要特性是:一个文本可以有多种分词方式,模型通过概率选择最优的:

# "invisible" 的可能分词
# 方案1: ["in", "vis", "ible"] P = 0.3
# 方案2: ["invis", "ible"] P = 0.5
# 方案3: ["in", "visible"] P = 0.2
# 选择概率最高的方案2

这种概率性分词在处理多语言文本时特别有用。

五、SentencePiece#

SentencePiece 是 Google 开源的分词工具库,支持 BPE 和 Unigram LM 两种算法。

0.12 核心特性#

  1. 语言无关:不依赖预分词,直接处理原始文本
  2. 空格作为普通字符:将空格编码为特殊标记 (U+2581)
  3. 可逆转换:分词结果可以无损还原为原始文本
  4. 子词正则化:训练时随机采样不同分词方案,提高鲁棒性
import sentencepiece as spm
# 训练 SentencePiece 模型
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='mymodel',
vocab_size=32000,
model_type='unigram', # 或 'bpe'
)
# 使用模型
sp = spm.SentencePieceProcessor()
sp.load('mymodel.model')
# 编码
tokens = sp.encode("Hello, how are you?", out_type=str)
# ['▁Hello', ',', '▁how', '▁are', '▁you', '?']
ids = sp.encode("Hello, how are you?")
# [1234, 56, 789, 345, 678, 90]
# 解码(可逆)
text = sp.decode(ids)
# "Hello, how are you?"

六、主流 LLM 分词器对比#

模型分词算法词汇表大小特殊设计空格处理
GPT-2Byte-level BPE50,257字节级基础保留空格
GPT-3/4Byte-level BPE (cl100k_base)100,256更大多词汇表保留空格
BERTWordPiece30,000## 前缀预分词
LLaMASentencePiece (BPE)32,000字节回退▁ 标记
LLaMA-2/3SentencePiece (BPE)32,000同上▁ 标记
Qwentiktoken (BPE)151,643超大多语言词汇保留空格
ChatGLMBPE130,528中英双语优化特殊处理
MistralSentencePiece (BPE)32,000标准配置▁ 标记

0.13 压缩率对比#

压缩率指每个 Token 平均能表示多少字符。压缩率越高,模型处理相同文本所需的 Token 越少:

graph LR subgraph "压缩率对比(英文)" GPT4["GPT-4<br/>~4 字符/Token"] BERT["BERT<br/>~3 字符/Token"] LLAMA["LLaMA<br/>~3.5 字符/Token"] Qwen["Qwen<br/>~4.2 字符/Token"] end subgraph "压缩率对比(中文)" GPT4_ZH["GPT-4<br/>~1.5 字符/Token"] LLAMA_ZH["LLaMA<br/>~1.2 字符/Token"] Qwen_ZH["Qwen<br/>~2.0 字符/Token"] end style Qwen fill:#4CAF50,color:#fff style Qwen_ZH fill:#4CAF50,color:#fff

中文压缩率的差异尤为显著:LLaMA 将每个中文字符编码为约 2-3 个 Token(因为 SentencePiece 对中文的词频统计不够),而 Qwen 的中文优化分词器将每个中文字符编码为约 1-1.5 个 Token。

0.14 词汇表大小的影响#

词汇表大小优点缺点
小(30K)嵌入矩阵小、内存低压缩率低、序列长
中(50-100K)平衡
大(150K+)压缩率高、序列短嵌入矩阵大、内存高

七、代码示例#

0.15 tiktoken(GPT-4 使用)#

import tiktoken
# GPT-4 使用的分词器
enc = tiktoken.get_encoding("cl100k_base")
# 编码
tokens = enc.encode("Hello, how are you?")
print(tokens) # [9906, 11, 1268, 527, 499, 30]
# 解码
text = enc.decode(tokens)
print(text) # "Hello, how are you?"
# 统计 Token 数量
def count_tokens(text, model="gpt-4"):
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))
print(count_tokens("这是一段中文文本"))

0.16 sentencepiece(LLaMA 使用)#

import sentencepiece as spm
# 加载 LLaMA 分词器
sp = spm.SentencePieceProcessor()
sp.load("llama_tokenizer.model")
# 编码
tokens = sp.encode("Hello, world!")
print(tokens) # [1, 15043, 29892, 1917, 29901]
# 获取子词文本
pieces = sp.encode("Hello, world!", out_type=str)
print(pieces) # ['▁Hello', ',', '▁world', '!']
# 解码
text = sp.decode(tokens)
print(text) # "Hello, world!"
# 词汇表大小
print(f"Vocab size: {sp.get_piece_size()}")

0.17 tokenizers(HuggingFace)#

from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
# 从零训练 BPE 分词器
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace()
trainer = BpeTrainer(vocab_size=30000, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
# 训练
tokenizer.train(files=["corpus.txt"], trainer=trainer)
# 使用
output = tokenizer.encode("Hello, how are you?")
print(output.tokens) # ['Hello', ',', 'how', 'are', 'you', '?']
print(output.ids) # [234, 56, 789, 345, 678, 90]

八、分词器对模型的影响#

0.18 推理成本#

分词器直接影响推理成本,因为大多数 API 按 Token 计费:

同一个句子 "这是一段关于大语言模型的中文文本"
- GPT-4 (cl100k_base): ~18 Tokens × $0.03/1K = $0.00054
- LLaMA (SentencePiece): ~25 Tokens × 更低单价
- Qwen (优化中文): ~12 Tokens × 最低单价
→ 分词效率直接影响使用成本

0.19 上下文利用率#

分词效率决定了有效上下文长度:

128K Token 上下文窗口
- 英文(GPT-4): ~100K 单词 ≈ 1 本书
- 中文(LLaMA): ~50K 字符 ≈ 半本书(因为压缩率低)
- 中文(Qwen): ~80K 字符 ≈ 接近 1 本书(因为压缩率高)

0.20 多语言公平性#

不同分词器对非英语语言的处理效率差异巨大:

语言GPT-4 Token 效率LLaMA Token 效率Qwen Token 效率
英语基准 1.0×约 0.9×约 1.0×
中文约 0.4×约 0.3×约 0.8×
日语约 0.4×约 0.3×约 0.6×
韩语约 0.4×约 0.3×约 0.6×
代码约 0.7×约 0.6×约 0.8×

这意味着用 GPT-4 处理中文时,相同信息量所需的 Token 数是英语的 2.5 倍,直接导致更高的成本和更低的上下文利用率。

常见问题 FAQ#

0.1 Q1: 为什么 LLaMA 的中文分词效率这么低?#

LLaMA 的 SentencePiece 模型主要在英语数据上训练,词汇表中中文子词极少。大部分中文字符被编码为 UTF-8 字节(每个中文字符 3 字节),再通过字节级合并组合,导致一个汉字需要 2-3 个 Token。

0.2 Q2: BPE 和 WordPiece 哪个更好?#

在现代 LLM 中,BPE 更流行。BPE 的优势是训练简单、可解释性强,且 Byte-level BPE 天然支持多语言。WordPiece 主要被 BERT 系列模型使用,其似然增益的合并策略在词汇表较小时可能更有优势。

0.3 Q3: 如何选择词汇表大小?#

对于通用模型,32K-100K 是常见范围。选择时需要平衡:

  • 太小:压缩率低,序列过长,影响训练效率
  • 太大:嵌入矩阵占用内存大,训练和推理成本高
  • 多语言模型通常需要更大的词汇表(100K+)以覆盖各语言

0.4 Q4: 分词器可以跨模型使用吗?#

理论上可以,但不推荐。不同模型的分词器与其嵌入层紧密耦合。使用错误的分词器会导致 Token ID 与嵌入向量不匹配,产生无意义的输出。唯一合理的跨模型分词器使用场景是训练新模型时继承已有分词器。

0.5 Q5: 如何评估一个分词器的好坏?#

主要指标:

  1. 压缩率:每 Token 平均字符数(越高越好)
  2. OOV 率:未登录词比例(应接近 0)
  3. 语言公平性:不同语言的压缩率差异(越小越好)
  4. 可逆性:能否无损还原原始文本

0.6 Q6: 什么是分词器的”毒性”问题?#

分词器的毒性指某些 Token 可能被用于绕过模型安全过滤。例如,一个包含有害子串的 Token 可能被模型正确处理但绕过了简单的关键词过滤。现代分词器通过仔细设计词汇表和过滤训练数据来缓解这个问题。

小结#

分词器是 LLM 的基础组件,直接影响模型的效率、成本和多语言能力:

  1. BPE:通过迭代合并最频繁字符对构建子词词汇表,GPT 系列使用 Byte-level BPE
  2. WordPiece:通过似然增益选择合并对,BERT 使用 ## 前缀标记续接
  3. Unigram LM:通过自顶向下裁剪构建词汇表,SentencePiece 支持此算法
  4. SentencePiece:语言无关的分词工具库,被 LLaMA 等模型采用
  5. 多语言支持:不同分词器对非英语语言的处理效率差异巨大
  6. 实际影响:分词效率直接影响推理成本、上下文利用率和多语言公平性

理解分词器是深入理解 LLM 的第一步。选择合适的分词器需要根据目标语言、模型规模和应用场景综合考量。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

BPE 与 SentencePiece:LLM 分词器原理
https://blog.souloss.com/posts/machine-learning/llm-paper-history/bpe-and-sentencepiece-tokenizer/
作者
Souloss
发布于
2025-01-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时