← 返回首页
🧠

Tokenization:文本分词技术详解

📂 llm ⏱ 3 min 472 words

--- title: "Tokenization:文本分词技术详解" description: "深入理解BPE、WordPiece、SentencePiece等分词算法及其在LLM中的应用" tags: ["Tokenization", "BPE", "分词", "NLP"] category: "llm" icon: "🧠"

Tokenization:文本分词技术详解

为什么需要Tokenization

大语言模型无法直接处理原始文本,需要将文本转换为数字表示。Tokenization(分词)就是将文本切分成更小单元(token)的过程。

不同的分词策略会显著影响模型的性能和效率:

# 不同分词器对同一文本的处理差异
text = "人工智能正在改变世界"

# 字符级分词
char_tokens = list(text)  # ['人', '工', '智', '能', '正', '在', '改', '变', '世', '界']

# 词级分词(假设)
word_tokens = ['人工智能', '正在', '改变', '世界']

# 子词级分词(BPE)
# 可能是: ['人工', '智能', '正在', '改变', '世界']

主流分词算法

1. BPE(Byte Pair Encoding)

BPE 是目前最流行的分词算法,GPT 系列和许多其他模型都使用它。

算法原理

  1. 初始化:将文本拆分为单个字符
  2. 统计:计算相邻字符对的频率
  3. 合并:将最频繁的字符对合并为新token
  4. 重复:直到达到预设的词汇表大小
def simple_bpe(text, num_merges=10):
    """简化的BPE算法演示"""
    # 初始化为字符级
    tokens = list(text)
    vocab = set(tokens)
    
    for i in range(num_merges):
        # 统计相邻对频率
        pairs = {}
        for j in range(len(tokens) - 1):
            pair = (tokens[j], tokens[j + 1])
            pairs[pair] = pairs.get(pair, 0) + 1
        
        if not pairs:
            break
        
        # 找到最频繁的对
        best_pair = max(pairs, key=pairs.get)
        new_token = best_pair[0] + best_pair[1]
        
        # 合并
        new_tokens = []
        j = 0
        while j < len(tokens):
            if j < len(tokens) - 1 and (tokens[j], tokens[j+1]) == best_pair:
                new_tokens.append(new_token)
                j += 2
            else:
                new_tokens.append(tokens[j])
                j += 1
        
        tokens = new_tokens
        vocab.add(new_token)
        print(f"合并 {i+1}: {best_pair} -> {new_token}")
    
    return tokens, vocab

# 示例
text = "aabbaaabba"
tokens, vocab = simple_bpe(text, num_merges=3)
print(f"最终tokens: {tokens}")
print(f"词汇表: {vocab}")

2. WordPiece

WordPiece 是 BERT 使用的分词算法,与 BPE 类似,但使用不同的合并策略。

# WordPiece的合并策略
# BPE: 选择频率最高的对
# WordPiece: 选择使语言模型似然最大化的对

# 使用HuggingFace的WordPiece分词器
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')

text = "自然语言处理是人工智能的重要分支"
tokens = tokenizer.tokenize(text)
print(tokens)
# ['自', '然', '语', '言', '处', '理', '是', '人', '工', '智', '能', '的', '重', '要', '分', '支']

3. SentencePiece

SentencePiece 是一个独立于语言的分词工具,特别适合处理中文、日文等无空格分隔的语言。

import sentencepiece as spm

# 训练SentencePiece模型
# spm.SentencePieceTrainer.train(
#     input='corpus.txt',
#     model_prefix='my_tokenizer',
#     vocab_size=32000,
#     model_type='bpe'
# )

# 加载并使用
sp = spm.SentencePieceProcessor(model_file='my_tokenizer.model')
tokens = sp.encode("你好世界", out_type=str)
print(tokens)  # ['▁你好', '世界']

实际使用示例

使用HuggingFace Tokenizers

from transformers import AutoTokenizer

# 加载不同模型的分词器
models = {
    'GPT-2': 'gpt2',
    'BERT': 'bert-base-uncased',
    'T5': 't5-small',
    'LLaMA': 'meta-llama/Llama-2-7b-hf'
}

text = "Large language models are transforming AI"

for name, model_name in models.items():
    try:
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        tokens = tokenizer.tokenize(text)
        print(f"{name}: {len(tokens)} tokens")
        print(f"  Tokens: {tokens[:10]}...")
    except:
        print(f"{name}: 无法加载")

分词与反分词

from transformers import GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained('gpt2')

# 编码
text = "Hello, how are you?"
encoded = tokenizer(text)
print(f"Token IDs: {encoded['input_ids']}")
# [15496, 11, 703, 389, 345, 30]

# 解码
decoded = tokenizer.decode(encoded['input_ids'])
print(f"Decoded: {decoded}")
# "Hello, how are you?"

特殊Token处理

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')

text = "Hello, [MASK] are you?"

# 添加特殊token
inputs = tokenizer(
    text,
    padding='max_length',
    max_length=20,
    truncation=True,
    return_tensors='pt'
)

print(f"Input IDs: {inputs['input_ids']}")
print(f"Attention Mask: {inputs['attention_mask']}")
print(f"Token Type IDs: {inputs['token_type_ids']}")

中文分词的挑战

中文分词有其特殊性,因为中文没有天然的空格分隔:

# 中文分词的不同粒度
text = "北京大学计算机科学技术研究所"

# 字符级
char_tokens = list(text)

# 基于词典的分词
import jieba
word_tokens = list(jieba.cut(text))
# ['北京大学', '计算机', '科学', '技术', '研究所']

# BPE分词(可能的结果)
# ['北京', '大学', '计算', '机科', '学技', '术研', '究所']

词汇表大小的选择

词汇表大小是重要的超参数,需要在以下因素间权衡:

# 词汇表大小的影响
vocab_sizes = [32000, 50000, 100000]

for vocab_size in vocab_sizes:
    # 假设平均每个token覆盖的字符数
    avg_chars_per_token = 3.5  # 中文大约2-4个字符
    # 编码1000字符需要的token数
    tokens_needed = 1000 / avg_chars_per_token
    # 模型嵌入层参数量
    embedding_params = vocab_size * 4096  # 假设hidden_size=4096
    
    print(f"Vocab {vocab_size}: ~{tokens_needed:.0f} tokens/1000chars, "
          f"embedding params: {embedding_params/1e6:.1f}M")

优化分词效率

# 批量分词优化
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained('gpt2')

texts = ["Hello world"] * 1000

# 批量编码比逐条编码快得多
import time

start = time.time()
batch_encoded = tokenizer(texts, padding=True, truncation=True, max_length=512)
batch_time = time.time() - start

print(f"批量编码1000条: {batch_time:.3f}秒")
print(f"平均速度: {len(texts)/batch_time:.0f}条/秒")

总结

Tokenization 是大语言模型处理文本的第一步,不同的分词算法会显著影响模型的性能。理解 BPE、WordPiece、SentencePiece 等算法的原理,对于选择合适的分词器和优化模型表现至关重要。在实际应用中,通常直接使用预训练模型配套的分词器即可。