Tokenization深入解析
Tokenization(分词)是大型语言模型处理文本的基础环节,它将原始文本转换为模型可处理的token序列。本质上,Tokenization是一种有损压缩过程——将无限可能的字符串映射到有限的整数索引空间。这一过程直接影响模型的训练效率、推理速度以及最终的语言理解能力。1
1. Tokenization基础
1.1 为什么需要Tokenization
语言模型本质上是定义在离散符号序列上的概率分布。直接以字符为单位建模存在以下问题:
序列长度爆炸:以中文字符为例,常用汉字约3500个,但一个句子可能包含数十甚至数百个字符。若以UTF-8编码的字节作为基本单位,一个汉字需要3-4个字节,导致序列长度进一步膨胀。
语义碎片化:单个字符(如”的”、“了”)通常语义信息不足,而传统基于词的分词又面临严重的OOV(Out-of-Vocabulary,未登录词)问题。
训练效率低下:字符级模型的embedding矩阵相对较小,但自注意力计算的序列长度极长,导致的注意力复杂度成为瓶颈。
Tokenization通过将文本切分为具有语义完整性的subword单元,在序列长度和词汇量之间取得平衡。
1.2 Token vs Character vs Subword
| 分词粒度 | 词汇表大小 | 序列长度 | OOV处理 | 典型应用 |
|---|---|---|---|---|
| Character | ~100-256 | 最长 | 无OOV问题 | 字节级模型 |
| Word | ~10K-100K | 最短 | 全部OOV | 传统NLP |
| Subword | ~8K-100K | 中等 | 子词拼接 | 现代LLM |
Character级分词的优缺点:
- 优点:词汇表极小,无OOV问题,对拼写错误鲁棒
- 缺点:序列长度长,语义信息碎片化,需要更大的上下文来理解词义
Word级分词的优缺点:
- 优点:序列短,语义完整直观
- 缺点:词汇表庞大,未登录词处理困难,多语言支持复杂
Subword级分词的核心理念是将罕见词分解为常见子词,同时保持高频词的完整性:
# 示例:不同粒度的分词结果
text = "unfriendly"
character_level = ['u', 'n', 'f', 'r', 'i', 'e', 'n', 'd', 'l', 'y'] # 10 tokens
word_level = ['unfriendly'] # 1 token (可能OOV)
subword_level_bpe = ['un', 'friendly'] # 2 tokens
subword_level_wp = ['un', 'friend', 'ly'] # 3 tokens1.3 Tokenization在LLM中的重要性
Tokenization对LLM的影响体现在多个层面:
计算成本:Token数量直接决定了自注意力计算的序列长度。对于Transformer架构,复杂度为,其中为序列长度,为隐藏维度。相同的文本内容,不同的Tokenizer可能导致2-3倍的token数量差异。
信息密度:好的分词策略应使每个token携带较高的信息量。信息论视角下,我们希望最小化token序列的熵,同时保持语义完整性。
多语言能力:对于多语言模型,Tokenizer需要公平地处理不同语言。某些语言的词可能天然更长(如德语复合词),导致token消耗不均。
1.4 Token数量与计算成本
在商业LLM服务中,Token数量直接与成本挂钩。以OpenAI的定价为例,输入和输出的token分开计费。理解token的计算方式对于成本优化至关重要。
Token估算经验法则(针对英文):
- 1 token ≈ 4个字符
- 1 token ≈ 0.75个单词
- 1页纸 ≈ 500-800 tokens
中文Token消耗:中文的token效率通常低于英文,因为中文字符在UTF-8编码下占3-4字节,且常用tokenization方法会将汉字与subword混合处理。
# 使用tiktoken估算token数量(GPT系列Tokenizer)
import tiktoken
enc = tiktoken.get_encoding("cl100k_base") # GPT-4/ChatGPT使用
english_text = "The quick brown fox jumps over the lazy dog"
chinese_text = "快速brown狐狸jumps过lazy狗"
english_tokens = enc.encode(english_text)
chinese_tokens = enc.encode(chinese_text)
print(f"英文: {len(english_tokens)} tokens") # 输出: 9 tokens
print(f"中文: {len(chinese_tokens)} tokens") # 输出: 13 tokens2. BPE (Byte Pair Encoding)
2.1 算法原理:频率编码
BPE最初是一种数据压缩算法,由Gage在1994年提出。2其核心思想是迭代地合并最频繁相邻的符号对。在NLP应用中,BPE被adapted用于subword分词。
形式化定义:给定训练语料,BPE通过以下步骤构建词汇表:
- 将文本切分为最小单元(字符或字节)
- 统计所有相邻符号对的频率
- 合并频率最高的符号对,加入词汇表
- 重复步骤2-3,直至达到预设词汇表大小
2.2 训练过程详解
初始化阶段:每个字符被视为一个独立的token。设字符集合为,初始词汇表。
# BPE训练算法伪代码
def train_bpe(corpus, vocab_size):
# Step 1: 初始化词汇表为所有字符
vocab = set(all_characters)
# Step 2: 统计字符对频率
pairs = get_pair_frequencies(corpus)
while len(vocab) < vocab_size:
# 找到最频繁的字符对
most_frequent_pair = argmax(pairs)
# 合并该字符对
new_token = concatenate(most_frequent_pair)
vocab.add(new_token)
# 更新语料中的所有出现
corpus = merge_all_instances(most_frequent_pair, new_token)
# 重新统计字符对频率
pairs = get_pair_frequencies(corpus)
return vocab合并操作示例:
初始文本: aaabdaaabac
字符频率: {'a': 6, 'b': 2, 'd': 1}
字符对频率: {'aa': 3, 'ab': 2, 'ba': 1, 'ad': 1, 'da': 1, 'ac': 1}
第1轮合并: 'aa' → 'Z'
语料变为: ZabdaZabac
字符对频率更新: {'Za': 1, 'ab': 2, 'bd': 1, 'da': 1, 'Zb': 1, 'ac': 1}
第2轮合并: 'ab' → 'X'
语料变为: ZXdaZXac
...
2.3 编码/解码算法
编码(Tokenization):
BPE编码采用贪婪+最长匹配策略,从左到右扫描文本,每次尽可能匹配最长的已知token。
def encode_bpe(text, vocab, merges):
"""
text: 输入文本
vocab: 基础词汇表(字符级)
merges: 按优先级排序的合并操作列表
"""
# 初始化:每个字符作为独立的token
tokens = list(text)
# 迭代应用合并操作
while True:
# 找到第一个可以合并的位置
merge_found = False
for i in range(len(tokens) - 1):
pair = (tokens[i], tokens[i+1])
if pair in merges:
# 合并该对
tokens = tokens[:i] + [merges[pair]] + tokens[i+2:]
merge_found = True
break
if not merge_found:
break
return tokens解码(Detokenization):解码相对简单,直接拼接所有token即可。
2.4 GPT-2的BPE实现
GPT-2采用的BPE实现有几点值得注意的改进:3
基于字节的BPE:GPT-2不使用Unicode字符作为基本单元,而是使用UTF-8字节序列。这避免了不同语言字符集不一致的问题,实现了真正的language-agnostic分词。
词汇表结构:
- 基础词汇:256个字节
- 合并操作:50,000次
- 总词汇表大小:50,257
# GPT-2 BPE实现核心逻辑
class BPEEncoder:
def __init__(self, vocab, merges):
self.byte_encoder = bytes_to_unicode() # 256 bytes → 256 visible chars
self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
self.stokens = vocab # 基础token(可见字符)
self.bpe_ranks = dict(zip(merges, range(len(merges))))
def encode(self, text):
# 1. Unicode归一化
text = unicodedata.normalize("NFKC", text)
# 2. UTF-8编码 → 字节序列 → 可见字符映射
tokens = [self.byte_encoder[b] for b in text.encode('utf-8')]
# 3. BPE合并
while True:
pairs = get_pairs(tokens)
bigram = min(pairs, key=lambda p: self.bpe_ranks.get(p, float('inf')))
if bigram not in self.bpe_ranks:
break
first, second = bigram
new_tokens = []
i = 0
while i < len(tokens):
try:
j = tokens.index(first, i)
new_tokens.extend(tokens[i:j])
break
except ValueError:
new_tokens.extend(tokens[i:])
i = len(tokens)
break
else:
new_tokens = tokens
tokens = merge_sequence(new_tokens, first, second)
return tokensGPT-2的特殊Token:
| Token | Token ID | 用途 |
|---|---|---|
| `< | endoftext | >` |
| `< | start | >` |
3. WordPiece
3.1 原理:基于似然的分词
WordPiece由Schuster和Nakajima于2012年提出,最初用于日语和韩语的语音处理。4与BPE的频率驱动不同,WordPiece采用似然最大化原则构建词汇表。
核心思想:给定语料,WordPiece选择能够使语言模型似然最大化的合并操作。
评分函数:
或者使用基于语言模型的评分:
选择使语言模型困惑度提升最大的字符对进行合并。
3.2 与BPE的区别
| 特性 | BPE | WordPiece |
|---|---|---|
| 选择标准 | 字符对频率 | 语言模型似然 |
| 合并方向 | 频率优先 | 似然提升优先 |
| 编码方式 | 贪婪最长匹配 | 动态规划最优路径 |
| OOV处理 | 递归分解为子词 | 同样递归分解 |
| 典型应用 | GPT系列 | BERT系列 |
编码差异示例:
# 给定词汇表:["un", "##friend", "##ly", "unfriendly"]
# 注意:##前缀表示该token必须接在其他token后面
# BPE编码(贪婪)
bpe_encode("unfriendly")
# 贪婪匹配: "un" + "friend" + "ly" → 可能需要回溯
# WordPiece编码(动态规划)
wordpiece_encode("unfriendly")
# 动态规划找最优路径:
# "un" → 得分最高
# "friend" → 需要检查 "##friend"
# "ly" → 需要检查 "##ly"
# 完整序列: ["un", "##friend", "##ly"]3.3 BERT的WordPiece实现
BERT使用WordPiece作为tokenizer,其词汇表规模为30,000。5
BERT Tokenizer特点:
-
特殊Token:
[CLS]:分类任务专用,位于序列开头[SEP]:句子分隔符,用于成对任务[PAD]:填充符,批处理时统一长度[UNK]:未登录词标记[MASK]:掩码标记(预训练时使用)
-
子词标记规范:
- 独立词:
"hello"→"hello" - 词内子词:
"unning"→"un" "##ning"
- 独立词:
-
句子对处理:
# BERT输入格式
[CLS] + tokens_a + [SEP] + tokens_b + [SEP] + [PAD] * n
↓
Token IDs: [101, 1996, 2003, ...]
Segment IDs: [0, 0, 0, ..., 1, 1, 1, ...]
Attention Mask: [1, 1, 1, ..., 1, 1, 1, 0, 0, ...]3.4 词汇表构建
WordPiece词汇表构建是一个迭代优化过程:
def train_wordpiece(corpus, vocab_size, min_frequency=2):
"""
基于语言模型的WordPiece训练
"""
# Step 1: 初始化单字符词汇表
vocab = set(all_characters)
# Step 2: 迭代增加词汇表
while len(vocab) < vocab_size:
# 计算所有可能的字符对的语言模型得分
candidates = []
for word in corpus:
for i in range(len(word) - 1):
pair = (word[i], word[i+1])
# 计算该合并的语言模型收益
score = calculate_lm_score(pair, corpus, vocab)
candidates.append((pair, score))
# 选择得分最高的合并
best_pair = max(candidates, key=lambda x: x[1])[0]
# 添加到词汇表并更新语料
vocab.add(best_pair)
corpus = merge_corpus(corpus, best_pair)
if len(vocab) >= vocab_size:
break
return vocab4. SentencePiece
4.1 特性:无监督、Language-independent
SentencePiece由Google于2019年发布,是一种更加通用的分词工具。6其核心设计理念是完全无监督,不依赖语言特定的预处理。
与传统Tokenizer的关键区别:
| 特性 | 传统Tokenizer | SentencePiece |
|---|---|---|
| 预处理 | 需要分词/ normalization | 无需预处理 |
| 空格处理 | 依赖显式空格标记 | 将空格编码为特殊字符 |
| 训练数据 | 需要干净文本 | 可处理原始文本 |
| 语言依赖 | 高度依赖语言规则 | Language-independent |
4.2 Unigram模型支持
SentencePiece支持多种分词模型,其中Unigram Language Model是最常用的选项。
Unigram模型原理:假设每个token的生成是独立的,文本概率为各token概率的乘积。
训练目标:最大化似然,同时正则化词汇表大小。
其中是训练语料中的句子,是词汇表大小,是正则化系数。
词汇表剪枝:Unigram模型可以反向使用——先训练大词汇表,再逐步剪枝不重要的token:
def train_unigram(corpus, target_vocab_size):
# Step 1: 初始化(使用所有可能的substrings)
vocab = generate_all_substrings(corpus)
# Step 2: EM算法训练Unigram模型
probs = em_training(corpus, vocab)
# Step 3: 迭代剪枝直到达到目标大小
while len(vocab) > target_vocab_size:
# 找到对似然贡献最小的token
least_useful = find_least_useful_token(vocab, probs, corpus)
vocab.remove(least_useful)
probs = recalculate_probs(corpus, vocab)
return vocab, probs4.3 动态规划解码
SentencePiece使用动态规划(Viterbi算法)找到最优的分词路径:
def decode_sentencepiece(text, vocab, probs):
"""
使用Viterbi算法进行最优分词
"""
n = len(text)
# dp[i] = 从位置i到结尾的最小负对数概率
dp = [float('inf')] * (n + 1)
dp[n] = 0 # 终止状态
# 记录回溯指针
backptr = [-1] * n
for i in range(n - 1, -1, -1):
for length in range(1, n - i + 1):
substr = text[i:i+length]
if substr in vocab:
prob = probs[substr]
new_score = dp[i + length] - log(prob)
if new_score < dp[i]:
dp[i] = new_score
backptr[i] = i + length
# 回溯找到最优分词
tokens = []
pos = 0
while pos < n:
next_pos = backptr[pos]
tokens.append(text[pos:next_pos])
pos = next_pos
return tokens4.4 多语言支持
SentencePiece在多语言场景下表现出色,主要得益于以下设计:
空格编码:将空格表示为_(可配置),而非依赖显式空格字符。这使得无需语言特定的tokenization即可处理各种语言的文本。
# SentencePiece处理流程
text = "Hello 世界 🌍"
# 预处理:将空格替换为特殊字符
text = "Hello▁世界▁🌍"
# 分词:可能在字节级别处理emoji等复杂字符
# 输出:['Hello', '▁', '世', '界', '▁', '🗺', '🌍']字节级别Fallback:对于词汇表中不存在的字符,SentencePiece可以退回到字节级别表示,确保任意输入都可以处理。
LLaMA的SentencePiece应用:
LLaMA系列模型使用基于SentencePiece的Tokenizer,其特点包括:7
| 模型 | 词汇表大小 | 分词算法 |
|---|---|---|
| LLaMA 1 | 32,000 | SentencePiece |
| LLaMA 2 | 32,000 | SentencePiece |
| LLaMA 3 | 128,256 | Tiktoken (BPE) |
5. Tokenizer对比分析
5.1 分词粒度对比
不同Tokenizer的分词粒度直接影响模型的token效率和信息密度:
| Tokenizer | 典型词汇表 | 分词粒度 | 语言处理 |
|---|---|---|---|
| GPT-2 BPE | 50,257 | Byte-level subword | 语言无关 |
| BERT WordPiece | 30,522 | Subword | 需要预处理 |
| LLaMA SentencePiece | 32,000 | SentencePiece | 语言无关 |
| Claude | ~100,000 | - | 高度优化 |
| GPT-4 | ~100,000 | - | 高度优化 |
分词粒度对信息密度的影响:
假设英文平均单词长度为4.5字符,token效率(字符/token)如下:
- BPE (GPT-2):约3.8字符/token
- WordPiece (BERT):约4.2字符/token
- 理想情况:约4字符/token(1 token ≈ 4 chars)
5.2 词汇表大小影响
词汇表大小是Tokenizer设计中的关键超参数,需要在多个因素间权衡:
优点:
- 减少序列长度,降低计算成本
- 减少OOV问题
- 提高信息密度
缺点:
- Embedding矩阵增大()
- 稀疏的长尾token难以有效学习
- 内存占用增加
# 词汇表大小对Embedding参数量的影响
def embedding_size(vocab_size, hidden_dim):
"""计算Embedding层参数量"""
return vocab_size * hidden_dim
# 典型Transformer配置
hidden_dim = 4096
for vocab_size in [8000, 32000, 50000, 100000]:
params = embedding_size(vocab_size, hidden_dim)
print(f"Vocab: {vocab_size:>6} → Embedding: {params:,} params ({params*4/1024/1024:.1f} MB)")输出:
Vocab: 8000 → Embedding: 32,768,000 params (125.0 MB)
Vocab: 32000 → Embedding: 131,072,000 params (500.0 MB)
Vocab: 50000 → Embedding: 204,800,000 params (781.3 MB)
Vocab: 100000 → Embedding: 409,600,000 params (1.5 GB)
5.3 训练数据敏感性
Tokenizer对训练数据的分布高度敏感,这一特性在多语言场景下尤为重要。
单语言训练的问题:
如果Tokenizer仅在英语数据上训练,对中文文本的分词效率会显著下降:
# 不同语言的分词效率对比
texts = {
"English": "The quick brown fox jumps over the lazy dog",
"中文": "快速棕色狐狸跳过懒惰的狗",
"日本語": "素早い茶色の狐が怠けた犬を飛び越える",
"العربية": "الثعلب البني السريع يقفز فوق الكلب الكسول"
}
# 使用GPT-2 tokenizer
for lang, text in texts.items():
tokens = enc.encode(text)
chars = len(text)
print(f"{lang:>10}: {chars} chars → {len(tokens)} tokens (效率: {chars/len(tokens):.2f} chars/token)")典型输出:
English: 44 chars → 9 tokens (效率: 4.89 chars/token)
中文: 21 chars → 21 tokens (效率: 1.00 chars/token)
日本語: 32 chars → 64 tokens (效率: 0.50 chars/token)
العربية: 40 chars → 20 tokens (效率: 2.00 chars/token)
5.4 OOV处理策略
OOV(Out-of-Vocabulary)问题是所有基于固定词汇表的Tokenizer面临的挑战。
| 策略 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
[UNK]替换 | 直接替换为未知标记 | 简单 | 信息丢失 |
| 子词分解 | 递归分解为子词 | 保留部分信息 | 可能语义碎片化 |
| 字节Fallback | 退回到字节级别 | 保证可处理性 | 序列长度增加 |
| 字符级Fallback | 退回到字符级别 | 无OOV | 语义丢失 |
现代LLM的OOV处理:
def smart_tokenize(text, tokenizer, oov_strategy='hybrid'):
"""
智能OOV处理
"""
if oov_strategy == 'hybrid':
# 尝试完整token匹配
full_tokens = tokenizer.encode(text)
# 检测OOV token
oov_positions = [i for i, t in enumerate(full_tokens)
if t == tokenizer.unk_token_id]
# 对OOV位置进行子词分解
result = []
for i, token_id in enumerate(full_tokens):
if token_id != tokenizer.unk_token_id:
result.append(token_id)
else:
# 子词或字节级分解
sub_tokens = subword_fallback(text, tokenizer)
result.extend(sub_tokens)
return result6. Tokenizer对模型的影响
6.1 压缩率与效率
Tokenization本质上是文本到整数的映射,其压缩效率直接影响模型的计算效率和信息容量。
压缩比定义:
典型压缩比:
- Character级:~1 byte/char → 约1 token/char
- Word级:~5 chars/word → 约0.2 token/char
- Subword级:~4 chars/token → 约0.25 token/char
信息论视角:
英文文本的信息熵约为1.0-1.5 bits/character,而UTF-8编码使用8 bits/character。这说明存在显著的冗余,Subword分词可以在保留语义的同时提高信息密度。
6.2 推理速度影响
Token数量直接影响推理阶段的延迟和吞吐量:
KV Cache压力:对于自回归模型,每个新token都需要访问所有历史token的Key和Value。token数量越多,KV Cache越大,内存带宽压力越高。
# KV Cache内存估算
def kv_cache_memory(num_layers, seq_len, num_heads, head_dim, batch_size, precision=2):
"""
计算KV Cache的内存占用
"""
bytes_per_element = precision # float16=2, float32=4
kv_elements = 2 * num_layers * seq_len * num_heads * head_dim * batch_size
memory_bytes = kv_elements * bytes_per_element
return memory_bytes / (1024 ** 3) # GB
# LLaMA 7B配置
config = {
"num_layers": 32,
"num_heads": 32,
"head_dim": 128,
"batch_size": 1
}
for seq_len in [512, 2048, 8192, 32768]:
memory = kv_cache_memory(**config, seq_len=seq_len)
print(f"Seq Len: {seq_len:>5} → KV Cache: {memory:.2f} GB")输出:
Seq Len: 512 → KV Cache: 0.06 GB
Seq Len: 2048 → KV Cache: 0.25 GB
Seq Len: 8192 → KV Cache: 1.00 GB
Seq Len: 32768 → KV Cache: 4.00 GB
6.3 对齐问题(分词不对齐)
Tokenization引入的一个微妙但重要的问题是token边界与原始文本边界不对齐。这在需要精确定位文本位置的任务(如问答、信息抽取)中尤为关键。
问题示例:
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
text = "Hello, 世界!"
tokens = enc.encode(text)
# 不同tokenize方式的字符偏移
char_offsets = enc.decode_to_offsets(text)
print("Token → Byte Offset → Character Offset")
for i, (token, byte_start, byte_end) in enumerate(char_offsets):
char_start = len(text[:byte_start].encode('utf-8')) # 简化计算
char_text = text[byte_start:byte_end]
print(f" {i:2d}: {token:>6} → [{byte_start:3d}:{byte_end:3d}] → '{char_text}'")解决方案:
- 返回偏移映射:大多数Tokenizer支持返回token到原始文本的映射
- 特殊标记处理:确保特殊token(如
<|endoftext|>)的正确处理 - 字节级对齐:使用字节级模型实现精确对齐
6.4 多语言Tokenizer设计
设计多语言Tokenizer需要解决语言公平性和效率平衡的问题。
语言公平性指标:
其中是语言数量,和分别是第种语言的字符数和token数。
多语言Tokenizer设计策略:
| 策略 | 方法 | 优缺点 |
|---|---|---|
| 统一词汇表 | 所有语言共享词汇表 | 简单,但效率不均 |
| 语言特定词汇 | 每种语言独立Tokenizer | 效率高,但需要路由 |
| 分层词汇表 | 共享基础词+语言特定词 | 平衡效率和公平性 |
| 数据驱动 | 按比例采样各语言训练 | 自动优化分布 |
LLaMA 3的Tokenizer改进:
LLaMA 3将词汇表扩展到128K tokens,显著提高了多语言tokenization效率:
# LLaMA 3词汇表设计分析
# 128K词汇表中包含:
# - 基础ASCII/Latin字符
# - 扩展的Unicode字符覆盖
# - 更多的多语言子词单元
# - 保留空间用于特定应用扩展7. 实践指南
7.1 选择Tokenizer的考量
选择Tokenizer时需要综合考虑以下因素:
应用场景:
| 场景 | 推荐Tokenizer | 理由 |
|---|---|---|
| 英文为主的LLM | GPT-2 BPE / Tiktoken | 成熟、高效 |
| 多语言模型 | SentencePiece | 语言无关 |
| BERT系列 | WordPiece | 预训练权重兼容 |
| 代码处理 | Byte-level BPE | 无特殊字符问题 |
| 中文任务 | SentencePiece + 中文优化 | 中文友好 |
效率 vs 公平性:
- 如果应用以英文为主,优先选择英文效率高的Tokenizer
- 如果是多语言应用,需要测试各语言的分词效率
- 使用信息论指标评估Tokenizer的压缩效率
7.2 自定义Tokenizer训练
使用tokenizers库训练自定义BPE Tokenizer:
from tokenizers import Tokenizer, trainers, pre_tokenizers, decoders, models
def train_custom_tokenizer(corpus_path, vocab_size=30000, min_frequency=2):
"""
训练自定义BPE Tokenizer
"""
# Step 1: 初始化BPE模型
tokenizer = Tokenizer(models.BPE(unk_token="[UNK]"))
# Step 2: 配置预处理器
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
# Step 3: 配置后处理器
tokenizer.post_processor = processors.ByteLevel(trim_offsets=True)
# Step 4: 配置解码器
tokenizer.decoder = decoders.ByteLevel()
# Step 5: 训练
trainer = trainers.BpeTrainer(
vocab_size=vocab_size,
min_frequency=min_frequency,
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]
)
tokenizer.train(files=[corpus_path], trainer=trainer)
return tokenizer
# 使用示例
tokenizer = train_custom_tokenizer("./data/train.txt", vocab_size=50000)
tokenizer.save("./tokenizer.json")
# 编码测试
encoded = tokenizer.encode("Hello, world!")
print(f"Tokens: {encoded.tokens}")
print(f"IDs: {encoded.ids}")7.3 Tokenizer版本管理
在实际项目中,Tokenizer版本管理至关重要,因为训练和推理必须使用完全相同的Tokenizer。
最佳实践:
import json
from pathlib import Path
class TokenizerManager:
def __init__(self, tokenizer_dir):
self.tokenizer_dir = Path(tokenizer_dir)
self.tokenizer = None
def save_with_version(self, tokenizer, version):
"""保存Tokenizer及其版本信息"""
save_path = self.tokenizer_dir / f"tokenizer_v{version}"
save_path.mkdir(parents=True, exist_ok=True)
tokenizer.save(str(save_path / "tokenizer.json"))
# 保存版本元数据
metadata = {
"version": version,
"vocab_size": tokenizer.get_vocab_size(),
"model_type": "BPE",
"special_tokens": tokenizer.tokenizer.token_to_id
}
with open(save_path / "metadata.json", "w") as f:
json.dump(metadata, f, indent=2)
def load_with_version(self, version):
"""加载指定版本的Tokenizer"""
save_path = self.tokenizer_dir / f"tokenizer_v{version}"
self.tokenizer = Tokenizer.from_file(str(save_path / "tokenizer.json"))
with open(save_path / "metadata.json") as f:
metadata = json.load(f)
return self.tokenizer, metadata7.4 特殊Token处理
常见特殊Token:
| Token | 用途 | 位置 |
|---|---|---|
[CLS] | 分类特征 | 序列开头 |
[SEP] | 句子分隔 | 句子边界 |
[PAD] | 填充 | 批处理对齐 |
[MASK] | 掩码预测 | MLM预训练 |
[UNK] | 未知词 | OOV替代 |
<|endoftext|> | EOS标记 | 序列结尾 |
<|start|> | BOS标记 | 序列开头 |
ChatML格式示例:
# ChatML格式的Tokenization
def format_chatml(messages, tokenizer):
"""
使用ChatML格式处理对话
"""
special_tokens = {
"system": "<|im_start|>system\n",
"user": "<|im_start|>user\n",
"assistant": "<|im_start|>assistant\n",
"end": "<|im_end|>\n"
}
text = ""
for msg in messages:
role = msg["role"]
content = msg["content"]
text += special_tokens[role] + content + "\n" + special_tokens["end"]
# 编码时需要使用对应的tokenizer
return tokenizer.encode(text, add_special_tokens=False)多模态Token对齐:
对于包含文本和图像的多模态模型,需要特别注意Token边界与图像patch的对齐:
class MultimodalTokenizer:
def __init__(self, text_tokenizer, image_tokenizer):
self.text_tokenizer = text_tokenizer
self.image_tokenizer = image_tokenizer
def encode_multimodal(self, text, image):
# 文本编码
text_tokens = self.text_tokenizer.encode(text)
text_len = len(text_tokens)
# 图像编码
image_patches = self.image_tokenizer.encode(image)
image_len = len(image_patches)
# 构建多模态输入
# [TEXT] tokens ... [IMAGE] patches ...
multimodal_tokens = (
[self.special_ids["image_start"]] +
image_patches +
[self.special_ids["image_end"]] +
text_tokens
)
return multimodal_tokens8. 总结与展望
Tokenization作为LLM处理文本的第一道工序,其重要性往往被低估。不同的Tokenizer选择会对模型的训练效率、推理速度、多语言能力以及最终性能产生深远影响。
核心要点回顾:
- BPE适合追求效率的英文场景,GPT系列是其典型应用
- WordPiece通过语言模型似然优化,适合BERT系列的双向编码
- SentencePiece的语言无关性使其成为多语言模型的首选
- 词汇表大小需要在计算效率和公平性之间权衡
- Tokenizer版本管理是生产环境的必要实践
未来趋势:
- 更大词汇表:LLaMA 3的128K词汇表预示了趋势
- 字节级模型:直接操作字节可能进一步提升泛化能力
- 自适应Tokenization:根据内容动态调整分词粒度
- 语义感知的Tokenization:结合语义信息指导分词决策
参考资料
Footnotes
-
Manning, C. D., & Schütze, H. (1999). Foundations of Statistical Natural Language Processing. MIT Press. ↩
-
Gage, P. (1994). A new algorithm for data compression. The C Users Journal, 12(2), 23-38. ↩
-
Radford, A., et al. (2019). Language Models are Unsupervised Multitask Learners. OpenAI Technical Report. ↩
-
Schuster, M., & Nakajima, K. (2012). Japanese and Korean voice search. 2012 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). ↩
-
Devlin, J., et al. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL-HLT. ↩
-
Kudo, T., & Richardson, J. (2018). SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. EMNLP. ↩
-
Touvron, H., et al. (2023). LLaMA: Open and Efficient Foundation Language Models. Meta AI Research. ↩