分词处理

预计学习时间:30分钟

分词处理是将文本转换为大语言模型可处理的token序列的关键步骤,直接影响模型的理解能力和效率。

分词的重要性

分词是模型与文本数据交互的桥梁:

  • 输入转换:将人类可读文本转为模型可处理的数值向量
  • 词表决定:分词方式和词表大小决定模型的词汇理解能力
  • 效率影响:分词粒度影响序列长度,进而影响计算效率
  • 语言适应:不同语言需要不同的分词策略

主流分词算法

基于规则的分词

最简单的分词方式,通常用于英语等以空格为自然分隔的语言:

# 简单的基于空格的分词
def simple_tokenize(text):
    return text.split()

tokens = simple_tokenize("Hello world!")  # ['Hello', 'world!']

基于子词的分词

现代大语言模型普遍采用的方法,平衡词汇表大小和表达能力:

from transformers import AutoTokenizer

# 加载BERT分词器(WordPiece算法)
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
tokens = tokenizer.tokenize("Hello world!")
print(tokens)  # ['hello', 'world', '!']

# 加载GPT-2分词器(BPE算法)
tokenizer = AutoTokenizer.from_pretrained("gpt2")
tokens = tokenizer.tokenize("Hello world!")
print(tokens)  # ['Hello', 'Ġworld', '!']

字节对编码(BPE)

BPE是当前最流行的分词算法之一,从字符级别开始,逐步合并常见对:

BPE算法通过统计字节对出现频率,以贪心方式构建词表,能够有效处理未见词汇。

算法步骤:

  1. 将所有单词分解为字符序列
  2. 统计相邻字符对频率
  3. 合并出现最频繁的字符对
  4. 重复步骤2-3直到达到目标词表大小

BPE算法示意图

WordPiece

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

# WordPiece示例(使用HuggingFace的BERT分词器)
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")

# 分词示例
text = "The transformer architecture is powerful"
tokens = tokenizer.tokenize(text)
print(tokens)  # ['the', 'transform', '##er', 'architecture', 'is', 'powerful']

# 注意"##"前缀表示子词片段

SentencePiece

SentencePiece是一个不基于预分词的模型,尤其适合中日韩等无明显分词边界的语言:

import sentencepiece as spm

# 训练SentencePiece模型
spm.SentencePieceTrainer.train(
    input='corpus.txt',
    model_prefix='spm_model',
    vocab_size=8000,
    model_type='unigram'  # 可选 'bpe', 'unigram', 'char' 或 'word'
)

# 加载模型
sp = spm.SentencePieceProcessor()
sp.load('spm_model.model')

# 分词
tokens = sp.encode_as_pieces("这是一个中文句子")
print(tokens)  # ['▁这是', '一个', '中文', '句子']

# 注意"▁"表示单词边界

Token级别的影响

不同的分词粒度影响模型处理文本的方式:

分词级别优点缺点代表模型
字符级词表小,无OOV问题序列长,计算量大早期CNN模型
单词级语义直观,序列短词表大,OOV问题严重Word2Vec
子词级平衡序列长度与词表大小可能分割语义单元BERT, GPT系列
混合级灵活处理不同语言实现复杂T5, XLM-R

分词过程中的特殊处理

特殊标记

分词器通常添加特殊标记用于标记序列边界或特殊含义:

from transformers import AutoTokenizer

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

# 特殊标记的处理
encoded = tokenizer(
    "Hello world!", 
    add_special_tokens=True,  # 添加特殊标记
    return_tensors="pt"
)

print(tokenizer.decode(encoded['input_ids'][0]))
# 输出:[CLS] hello world! [SEP]

常见特殊标记:

  • [CLS]:分类标记,通常位于序列开始
  • [SEP]:分隔标记,用于分隔不同文本片段
  • [MASK]:掩码标记,用于掩码语言模型预训练
  • [PAD]:填充标记,用于批处理时序列对齐
  • [BOS]/[EOS]:序列开始/结束标记

处理未知词

处理词表外的词(OOV)是分词的重要挑战:

# 处理OOV示例
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

# 使用UNK标记处理未知词
text_with_rare_word = "This is a supercalifragilisticexpialidocious day!"
tokens = tokenizer.tokenize(text_with_rare_word)
print(tokens)  
# 输出可能包含 [UNK] 标记或将长词拆分为子词

分词与上下文关系

现代分词器处理上下文信息的方式:

# GPT-2分词器示例 - 展示空格的处理
tokenizer = AutoTokenizer.from_pretrained("gpt2")

text1 = "learning"
text2 = " learning"  # 注意前面有空格

tokens1 = tokenizer.tokenize(text1)
tokens2 = tokenizer.tokenize(text2)

print(tokens1)  # ['learning']
print(tokens2)  # ['Ġlearning']  - 'Ġ'前缀表示单词前有空格

多语言分词挑战

不同语言的分词处理需要不同策略:

# 多语言分词示例
tokenizer = AutoTokenizer.from_pretrained("xlm-roberta-base")

english_text = "Hello world!"
chinese_text = "你好世界!"
japanese_text = "こんにちは世界!"

print(tokenizer.tokenize(english_text))
print(tokenizer.tokenize(chinese_text))
print(tokenizer.tokenize(japanese_text))

多语言分词挑战:

  • 中日韩语言:无明显空格分隔
  • 形态丰富语言:如芬兰语、土耳其语的词形变化多
  • 书写系统不同:从右到左书写语言如阿拉伯语
  • 字符集差异:非ASCII字符的处理

实践中的分词处理

分词器训练

为特定域或语言训练自定义分词器:

from tokenizers import ByteLevelBPETokenizer

# 训练自定义分词器
tokenizer = ByteLevelBPETokenizer()

# 从语料库训练
tokenizer.train(
    files=["corpus.txt"],
    vocab_size=30000,
    min_frequency=2,
    special_tokens=["<s>", "<pad>", "</s>", "<unk>", "<mask>"]
)

# 保存分词器
tokenizer.save_model("custom_tokenizer")

处理长序列

处理超出模型最大长度的文本:

# 处理长文本的策略
long_text = "..." # 一段很长的文本

# 策略1:截断
encoded_truncated = tokenizer(
    long_text,
    max_length=512,
    truncation=True,
    return_tensors="pt"
)

# 策略2:分块处理
def process_long_text(text, tokenizer, chunk_size=512, overlap=50):
    tokens = tokenizer.tokenize(text)
    chunks = []
    
    for i in range(0, len(tokens), chunk_size - overlap):
        chunk = tokens[i:i + chunk_size]
        if len(chunk) > chunk_size / 2:  # 确保最后一个块不会太小
            chunks.append(chunk)
    
    return [tokenizer.convert_tokens_to_ids(chunk) for chunk in chunks]

分词效率优化

大规模数据处理需要高效分词策略:

# 批量分词提高效率
def batch_tokenize(texts, tokenizer, batch_size=1000):
    results = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        encoded = tokenizer(
            batch, 
            padding=True, 
            truncation=True,
            return_tensors="pt"
        )
        results.append(encoded)
    return results

# 并行分词
from concurrent.futures import ProcessPoolExecutor

def parallel_tokenize(texts, tokenizer, n_workers=4):
    chunks = [texts[i::n_workers] for i in range(n_workers)]
    
    with ProcessPoolExecutor(max_workers=n_workers) as executor:
        results = list(executor.map(
            lambda chunk: [tokenizer(text) for text in chunk],
            chunks
        ))
    
    # 合并结果
    flat_results = []
    for res_list in results:
        flat_results.extend(res_list)
    
    return flat_results

小结

分词是连接人类语言和模型理解的桥梁,对模型效果有显著影响:

  1. 选择合适算法:根据任务和语言特点选择分词算法
  2. 词表大小平衡:平衡表达能力和计算效率
  3. 特殊场景处理:针对长文本、多语言等场景优化
  4. 效率优化:大规模处理时考虑批处理和并行化

通过本章学习,我们已完成了数据准备的全部关键环节。下一步将探讨如何利用准备好的数据进行模型训练。