分词处理
预计学习时间: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算法通过统计字节对出现频率,以贪心方式构建词表,能够有效处理未见词汇。
算法步骤:
- 将所有单词分解为字符序列
- 统计相邻字符对频率
- 合并出现最频繁的字符对
- 重复步骤2-3直到达到目标词表大小
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
小结
分词是连接人类语言和模型理解的桥梁,对模型效果有显著影响:
- 选择合适算法:根据任务和语言特点选择分词算法
- 词表大小平衡:平衡表达能力和计算效率
- 特殊场景处理:针对长文本、多语言等场景优化
- 效率优化:大规模处理时考虑批处理和并行化
通过本章学习,我们已完成了数据准备的全部关键环节。下一步将探讨如何利用准备好的数据进行模型训练。