1. 为什么需要分词 1.1 分词的核心矛盾 自然语言是连续字符序列,模型需要离散输入单元。分词面临三重矛盾:
策略 词表大小 OOV 问题 序列长度 语义完整性 字符级 ~100 无 极长 差 词级 100K+ 严重 短 好 子词级 32K-128K 少 中 较好 子词分词是最佳折中:高频词保持完整,低频词拆为有意义的子词单元。
1.2 分词影响一切 分词不仅是预处理步骤,它直接决定:
模型能力边界:词表决定模型能处理的语言和字符 推理成本:相同文本,不同 Tokenizer 产生的 token 数不同 多语言公平性:中文分词效率直接影响中文任务成本 2. BPE(Byte Pair Encoding) 2.1 算法原理 BPE 最初是数据压缩算法,被 Sennrich 等人(2016)引入 NLP。核心思想:从字符级开始,迭代合并最高频的相邻字符对。
训练算法:
1. 将所有词拆为字符序列,词尾加 </w> "low" → ['l', 'o', 'w', '</w>'] 2. 统计相邻字符对频率 3. 合并最高频字符对为新 token 4. 重复步骤 2-3 直到达到目标词表大小 2.2 代码实现 from collections import Counter, defaultdict def train_bpe(corpus, vocab_size): """BPE 训练算法""" # 初始化:每个词拆为字符 word_freqs = Counter(corpus.split()) splits = {word: list(word) + ['</w>'] for word in word_freqs} merges = [] vocab = set(char for word in word_freqs for char in list(word) + ['</w>']) while len(vocab) < vocab_size: # 统计相邻对频率 pair_freqs = defaultdict(int) for word, freq in word_freqs.items(): split = splits[word] for i in range(len(split) - 1): pair_freqs[(split[i], split[i+1])] += freq if not pair_freqs: break # 合并最高频对 best_pair = max(pair_freqs, key=pair_freqs.get) merges.append(best_pair) new_token = best_pair[0] + best_pair[1] vocab.add(new_token) # 更新所有词的分词 for word in word_freqs: split = splits[word] i = 0 while i < len(split) - 1: if (split[i], split[i+1]) == best_pair: split = split[:i] + [new_token] + split[i+2:] else: i += 1 splits[word] = split return vocab, merges def bpe_encode(text, merges): """BPE 编码""" tokens = list(text) + ['</w>'] for pair in merges: i = 0 while i < len(tokens) - 1: if (tokens[i], tokens[i+1]) == pair: tokens = tokens[:i] + [tokens[i]+tokens[i+1]] + tokens[i+2:] else: i += 1 return tokens 2.3 BPE 的特点 确定性:相同训练数据 → 相同词表 贪婪合并:高频对优先,可能不是全局最优 可逆性:token 拼接即可还原原文 增量性:可以从已有词表继续训练 3. WordPiece 3.1 与 BPE 的区别 WordPiece(Google, 2012)用于 BERT,与 BPE 类似但有关键区别:
...