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 tokens

1.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 tokens

2. BPE (Byte Pair Encoding)

2.1 算法原理:频率编码

BPE最初是一种数据压缩算法,由Gage在1994年提出。2其核心思想是迭代地合并最频繁相邻的符号对。在NLP应用中,BPE被adapted用于subword分词。

形式化定义:给定训练语料,BPE通过以下步骤构建词汇表:

  1. 将文本切分为最小单元(字符或字节)
  2. 统计所有相邻符号对的频率
  3. 合并频率最高的符号对,加入词汇表
  4. 重复步骤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 tokens

GPT-2的特殊Token

TokenToken ID用途
`<endoftext>`
`<start>`

3. WordPiece

3.1 原理:基于似然的分词

WordPiece由Schuster和Nakajima于2012年提出,最初用于日语和韩语的语音处理。4与BPE的频率驱动不同,WordPiece采用似然最大化原则构建词汇表。

核心思想:给定语料,WordPiece选择能够使语言模型似然最大化的合并操作。

评分函数

或者使用基于语言模型的评分:

选择使语言模型困惑度提升最大的字符对进行合并。

3.2 与BPE的区别

特性BPEWordPiece
选择标准字符对频率语言模型似然
合并方向频率优先似然提升优先
编码方式贪婪最长匹配动态规划最优路径
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特点

  1. 特殊Token

    • [CLS]:分类任务专用,位于序列开头
    • [SEP]:句子分隔符,用于成对任务
    • [PAD]:填充符,批处理时统一长度
    • [UNK]:未登录词标记
    • [MASK]:掩码标记(预训练时使用)
  2. 子词标记规范

    • 独立词:"hello""hello"
    • 词内子词:"unning""un" "##ning"
  3. 句子对处理

# 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 vocab

4. SentencePiece

4.1 特性:无监督、Language-independent

SentencePiece由Google于2019年发布,是一种更加通用的分词工具。6其核心设计理念是完全无监督,不依赖语言特定的预处理。

与传统Tokenizer的关键区别

特性传统TokenizerSentencePiece
预处理需要分词/ 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, probs

4.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 tokens

4.4 多语言支持

SentencePiece在多语言场景下表现出色,主要得益于以下设计:

空格编码:将空格表示为_(可配置),而非依赖显式空格字符。这使得无需语言特定的tokenization即可处理各种语言的文本。

# SentencePiece处理流程
text = "Hello 世界 🌍"
 
# 预处理:将空格替换为特殊字符
text = "Hello▁世界▁🌍"
 
# 分词:可能在字节级别处理emoji等复杂字符
# 输出:['Hello', '▁', '世', '界', '▁', '🗺', '🌍']

字节级别Fallback:对于词汇表中不存在的字符,SentencePiece可以退回到字节级别表示,确保任意输入都可以处理。

LLaMA的SentencePiece应用

LLaMA系列模型使用基于SentencePiece的Tokenizer,其特点包括:7

模型词汇表大小分词算法
LLaMA 132,000SentencePiece
LLaMA 232,000SentencePiece
LLaMA 3128,256Tiktoken (BPE)

5. Tokenizer对比分析

5.1 分词粒度对比

不同Tokenizer的分词粒度直接影响模型的token效率和信息密度:

Tokenizer典型词汇表分词粒度语言处理
GPT-2 BPE50,257Byte-level subword语言无关
BERT WordPiece30,522Subword需要预处理
LLaMA SentencePiece32,000SentencePiece语言无关
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 result

6. 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}'")

解决方案

  1. 返回偏移映射:大多数Tokenizer支持返回token到原始文本的映射
  2. 特殊标记处理:确保特殊token(如<|endoftext|>)的正确处理
  3. 字节级对齐:使用字节级模型实现精确对齐

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理由
英文为主的LLMGPT-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, metadata

7.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_tokens

8. 总结与展望

Tokenization作为LLM处理文本的第一道工序,其重要性往往被低估。不同的Tokenizer选择会对模型的训练效率、推理速度、多语言能力以及最终性能产生深远影响。

核心要点回顾

  1. BPE适合追求效率的英文场景,GPT系列是其典型应用
  2. WordPiece通过语言模型似然优化,适合BERT系列的双向编码
  3. SentencePiece的语言无关性使其成为多语言模型的首选
  4. 词汇表大小需要在计算效率和公平性之间权衡
  5. Tokenizer版本管理是生产环境的必要实践

未来趋势

  • 更大词汇表:LLaMA 3的128K词汇表预示了趋势
  • 字节级模型:直接操作字节可能进一步提升泛化能力
  • 自适应Tokenization:根据内容动态调整分词粒度
  • 语义感知的Tokenization:结合语义信息指导分词决策

参考资料

Footnotes

  1. Manning, C. D., & Schütze, H. (1999). Foundations of Statistical Natural Language Processing. MIT Press.

  2. Gage, P. (1994). A new algorithm for data compression. The C Users Journal, 12(2), 23-38.

  3. Radford, A., et al. (2019). Language Models are Unsupervised Multitask Learners. OpenAI Technical Report.

  4. Schuster, M., & Nakajima, K. (2012). Japanese and Korean voice search. 2012 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP).

  5. Devlin, J., et al. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL-HLT.

  6. Kudo, T., & Richardson, J. (2018). SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. EMNLP.

  7. Touvron, H., et al. (2023). LLaMA: Open and Efficient Foundation Language Models. Meta AI Research.