当前位置: 首页 > ops >正文

分词器详解(一)

文章目录

  • 🌟 第0层:极简版(30秒理解)
    • 核心公式
    • 生活比喻
  • 📚 第1层:基础概念(5分钟理解)
    • 1. 分词器基础
      • 1.1 分词器的核心作用
      • 1.2 主流分词算法对比
    • 2. 基础实现
      • 2.1 BPE实现原理
      • 2.2 特殊标记处理
      • 2.3 和 huggingface的Tokenizer库做对比
      • 2.4 WordPiece分词器
        • 2.4.1 WordPiece的工作原理
      • 2.5 SentencePiece
        • 2.5.1 SentencePiece的概述
        • 2.5.2 SentencePiece的工作原理
    • 3. 基础应用
      • 3.1 选择指南

🌟 第0层:极简版(30秒理解)

一句话核心分词器是语言模型的"词汇翻译器"——BPE是"高频字符对合并器",WordPiece是"最大似然子词分割器",SentencePiece是"语言无关编码器"优秀的分词器能使模型性能提升5-15%,训练速度提高10-20%,特别在低资源语言上效果更显著

核心公式

最优词汇表 = argmin(α·表示效率 + β·词汇量 - γ·训练稳定性)
分词效率 = f(词汇表大小, 语言特性, 模型架构)
  • 关键特性:分词直接影响模型表示能力、训练效率和泛化性能
  • 优势:处理未登录词,平衡词汇量与表示能力
  • 挑战:语言特性差异,特殊领域适应,长尾分布处理

生活比喻

想象图书馆编目系统:

  • 传统分词:按整本书分类 → 词汇量爆炸
  • BPE:按高频词组创建新类别 → 高效但可能碎片化
  • SentencePiece:按字符组合创建类别 → 语言无关但可能过细
  • 理想分词:既识别完整概念又处理新词 → 平衡效率与表达

💡 记住这个数字5-15% → 10-20% → 30-50%
优秀分词器可使模型性能提升5-15%,训练速度提高10-20%,在低资源语言上效果提升30-50%

📚 第1层:基础概念(5分钟理解)

1. 分词器基础

1.1 分词器的核心作用

核心功能

  • 文本→ID转换:将原始文本映射为模型可处理的整数序列
  • 未知词处理:通过子词机制处理未见词汇
  • 语言表示:影响模型对语言结构的理解
  • 效率优化:控制序列长度和计算复杂度

关键组件

  • 词汇表(Vocabulary):词项到ID的映射
  • 分词算法:将文本拆分为词项的规则
  • 特殊标记:处理特殊场景的标记
  • 后处理:添加位置信息、截断和填充

1.2 主流分词算法对比

算法代表模型核心思想优点局限
BPEGPT系列高频字符对合并处理未登录词好可能产生不自然分割
WordPieceBERT最大似然子词分割适合英文任务严重依赖空格
SentencePieceT5, XLNet无空格分词语言无关训练较慢
Unigram LM部分SentencePiece概率模型可逆转换计算复杂

关键区别

  • BPE:贪心合并高频对
  • WordPiece:基于似然选择最佳分割
  • SentencePiece:不依赖空格,统一处理所有语言

2. 基础实现

在代码的同级目录新建corpus.txt文件,向里面写一个文字。

2.1 BPE实现原理

Byte-Pair Encoding(BPE)最初用于数据压缩,后来被广泛应用于自然语言处理中的子词分词任务。它通过迭代合并最频繁的字符对或子词对来构建词汇表,是GPT、RoBERTa等现代语言模型的关键分词技术。

BPE的训练过程如下:

初始化词汇表:从训练数据中提取所有唯一字符。例如,对于语料库[“dog” (8次), “log” (6次), “lot” (10次), “dot” (3次), “dogs” (4次)],初始词汇表为[“d”, “o”, “g”, “l”, “t”, “s”]。

统计字符对频率:遍历语料库,计算连续字符对的频率。例如,在"dog"中,字符对为(“d”, “o”)和(“o”, “g”)。

合并最频繁的字符对:选择频率最高的字符对,合并为新token。

更新词汇表:将新token加入词汇表,重新分词语料库,重复上述过程,直到达到目标词汇表大小。

示例:BPE训练
语料库:[“dog” (8次), “log” (6次), “lot” (10次), “dot” (3次), “dogs” (4次)]

步骤0:初始化

词汇表:[“d”, “o”, “g”, “l”, “t”, “s”]
语料库分词:

  • dog: [“d”, “o”, “g”]
  • log: [“l”, “o”, “g”]
  • lot: [“l”, “o”, “t”]
  • dot: [“d”, “o”, “t”]
  • dogs: [“d”, “o”, “g”, “s”]

步骤1:统计频率

字符对:

  • (“d”, “o”):15次(dog x8, dot x3, dogs x4)
  • (“o”, “g”):18次(dog x8, log x6, dogs x4)
  • (“l”, “o”):16次(log x6, lot x10)
  • (“o”, “t”):13次(lot x10, dot x3)
  • (“d”, “t”):3次(dot x3)
  • (“g”, “s”):4次(dogs x4)

最高频率:(“o”, “g”)(18次)
合并为"og",新词汇表:[“d”, “o”, “g”, “l”, “t”, “s”, “og”]

步骤2:重新分词

  • dog: [“d”, “og”]
  • log: [“l”, “og”]
  • lot: [“l”, “o”, “t”]
  • dot: [“d”, “o”, “t”]
  • dogs: [“d”, “og”, “s”]

继续统计新字符对频率:

  • (“l”, “o”):10次(lot x10)
  • (“o”, “t”):13次(lot x10, dot x3)
  • (“d”, “o”):3次(dot x3)
  • (“d”, “og”):12次(dog x8, dogs x4)
  • (“l”, “og”):6次(log x6)
  • (“og”, “s”):4次(dogs x4)

最高频率:(“o”, “t”)(13次)
合并为"ot",新词汇表:[“d”, “o”, “g”, “l”, “t”, “s”, “og”, “ot”]

步骤3:再次重新分词

  • dog: [“d”, “og”]
  • log: [“l”, “og”]
  • lot: [“l”, “ot”]
  • dot: [“d”, “ot”]
  • dogs: [“d”, “og”, “s”]

继续统计新字符对频率:

  • (“d”, “og”):12次(dog x8, dogs x4)
  • (“l”, “og”):6次(log x6)
  • (“l”, “ot”):10次(lot x10)
  • (“d”, “ot”):3次(dot x3)
  • (“og”, “s”):4次(dogs x4)

最高频率:(“d”, “og”)(12次)
合并为"dog",新词汇表:[“d”, “o”, “g”, “l”, “t”, “s”, “og”, “ot”, “dog”]

示例:BPE分词
假设最终词汇表为:[“d”, “o”, “g”, “l”, “t”, “s”, “og”, “ot”, “dog”, “log”]

分词"dogs":

  • 从左到右匹配最长子词:
    • "d"匹配
    • "do"不匹配
    • "dog"匹配
    • "s"匹配
  • 结果:[“dog”, “s”]

分词"logs":

  • "l"匹配
  • "lo"不匹配
  • "log"匹配
  • "s"匹配
  • 结果:[“log”, “s”]

分词"dot":

  • "d"匹配
  • "do"不匹配
  • "dot"不匹配(假设"dot"未被合并)
  • "do"不匹配
  • "ot"匹配
  • 结果:[“d”, “ot”]

BPE通过这种方式将单词分解为有意义的子词单元,既能处理未登录词,又能保持词汇表大小在合理范围内。 这种方法在现代NLP系统中被广泛应用,因为它能有效地平衡词汇表大小和表示能力。

代码示例


import re
from collections import defaultdict, Counterclass BPETokenizer:def __init__(self, special_tokens=None):# 修改初始化部分self.vocab = set()self.merge_rules = {}# 特殊标记字典,键为标记,值为IDself.special_tokens = special_tokens or {"[PAD]": 0,"[UNK]": 1,"[CLS]": 2,"[SEP]": 3,"[MASK]": 4}# 构建ID到token的反向映射self.id_to_token = {v: k for k, v in self.special_tokens.items()}self.token_to_id = self.special_tokens.copy()def train(self, corpus, vocab_size=10000):"""训练BPE模型"""# 对于中文,我们按字符分割words = list(corpus.replace(' ', '_'))  # 用下划线表示空格word_freq = Counter()# 统计字符频率for char in words:word_freq[char] += 1# 初始化词汇表self.vocab = set(self.special_tokens.keys())self.vocab.update(word_freq.keys())# 初始化符号序列,每个字符作为一个符号symbols_list = [[char] for char in words]# BPE训练过程current_vocab_size = len(self.vocab)while current_vocab_size < vocab_size:# 统计相邻符号对的频率pair_freq = defaultdict(int)for symbols in symbols_list:for i in range(len(symbols) - 1):pair = (symbols[i], symbols[i + 1])pair_freq[pair] += 1if not pair_freq:break# 找到最频繁的符号对best_pair = max(pair_freq, key=pair_freq.get)# 检查是否已经存在这个合并规则if best_pair in self.merge_rules:breaknew_token = ''.join(best_pair)# 如果新token已存在,跳过if new_token in self.vocab:del pair_freq[best_pair]if not pair_freq:breakcontinue# 添加新token到词汇表self.vocab.add(new_token)self.merge_rules[best_pair] = new_token# 更新所有符号序列new_symbols_list = []for symbols in symbols_list:new_symbols = self._apply_merge_rule_to_symbols(symbols, best_pair, new_token)new_symbols_list.append(new_symbols)symbols_list = new_symbols_listcurrent_vocab_size += 1# 构建完整的词汇表映射self._build_vocab_mappings()def _apply_merge_rule_to_symbols(self, symbols, pair, new_token):"""将合并规则应用到符号序列"""new_symbols = []i = 0while i < len(symbols):if i < len(symbols) - 1 and symbols[i] == pair[0] and symbols[i + 1] == pair[1]:new_symbols.append(new_token)i += 2else:new_symbols.append(symbols[i])i += 1return new_symbolsdef _build_vocab_mappings(self):"""构建词汇表映射"""# 清空现有的映射self.id_to_token.clear()self.token_to_id.clear()# 先添加特殊标记for token, id in self.special_tokens.items():self.id_to_token[id] = tokenself.token_to_id[token] = id# 添加普通词汇表项sorted_vocab = sorted(self.vocab)for i, token in enumerate(sorted_vocab):token_id = i + len(self.special_tokens)self.id_to_token[token_id] = tokenself.token_to_id[token] = token_iddef tokenize(self, text):"""使用训练好的BPE模型进行分词"""# 将文本转换为初始符号序列symbols = list(text.replace(' ', '_'))  # 空格用特殊符号表示# 重复应用合并规则直到无法继续合并changed = Truewhile changed:changed = Falsenew_symbols = []i = 0while i < len(symbols):if i < len(symbols) - 1:pair = (symbols[i], symbols[i + 1])if pair in self.merge_rules:new_symbols.append(self.merge_rules[pair])i += 2changed = Truecontinuenew_symbols.append(symbols[i])i += 1symbols = new_symbols# 将下划线转换回空格标记tokens = []for symbol in symbols:if symbol == '_':tokens.append('[SPACE]')  # 或者其他表示空格的方式else:tokens.append(symbol)return tokensdef encode(self, text):"""将文本转换为ID序列"""tokens = self.tokenize(text)ids = []for token in tokens:if token in self.token_to_id:ids.append(self.token_to_id[token])else:ids.append(self.token_to_id.get("[UNK]", 1))return idsdef decode(self, ids):"""将ID序列转换回文本"""tokens = []for id in ids:if id in self.id_to_token:token = self.id_to_token[id]if token == '[SPACE]':tokens.append(' ')elif token not in self.special_tokens:tokens.append(token)else:# 特殊标记不添加到解码文本中passelse:tokens.append('[UNK]')return ''.join(tokens)def vocab_size(self):"""返回词汇表大小"""return len(self.vocab) + len(self.special_tokens)# 使用示例
if __name__ == "__main__":# 初始化分词器tokenizer = BPETokenizer()# 训练语料with open("corpus.txt", "r", encoding="utf-8") as f:corpus = f.read()# 如果你希望保留一些基本的文本清理操作,可以添加如下代码:# with open("corpus.txt", "r", encoding="utf-8") as f:#     corpus = f.read().strip()  # 读取并去除首尾空白字符# 训练BPE模型tokenizer.train(corpus, vocab_size=100)print("词汇表大小:", tokenizer.vocab_size())print("部分词汇表:", list(tokenizer.vocab)[:20])print("合并规则数量:", len(tokenizer.merge_rules))# 测试分词text = "自然语言处理很有趣!"tokens = tokenizer.tokenize(text)print("Tokens:", tokens)# 测试编码ids = tokenizer.encode(text)print("IDs:", ids)# 测试解码decoded = tokenizer.decode(ids)print("Decoded:", decoded)print("原始文本:", text)

在这里插入图片描述

2.2 特殊标记处理

关键特殊标记

SPECIAL_TOKENS = {"[PAD]": 0,   # 填充标记"[UNK]": 1,   # 未知词标记"[CLS]": 2,   # 分类标记"[SEP]": 3,   # 分隔标记"[MASK]": 4,  # 掩码标记"[BOS]": 5,   # 开始标记"[EOS]": 6,   # 结束标记
}

处理策略

  • [UNK]最小化:通过足够大的词汇表减少未知词
  • 位置编码兼容:确保特殊标记不影响位置信息
  • 任务适配:不同任务需要不同的特殊标记组合
  • 长度控制:通过截断和填充保证序列长度一致

2.3 和 huggingface的Tokenizer库做对比

安装库,执行命令:

 pip install tokenizers
from tokenizers import Tokenizer, models, pre_tokenizers, trainers, processors
from tokenizers import normalizers
from tokenizers.normalizers import NFD, StripAccents# 初始化BPE分词器
tokenizer = Tokenizer(models.BPE(unk_token="[UNK]"))# 设置预分词器(按空格和标点分割)
tokenizer.pre_tokenizer = pre_tokenizers.Sequence([pre_tokenizers.Whitespace(),pre_tokenizers.Punctuation(),pre_tokenizers.Digits(individual_digits=True)
])# 设置规范化器(可选)
tokenizer.normalizer = normalizers.Sequence([NFD(), StripAccents()])# 定义特殊标记
special_tokens = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]# 定义训练器
trainer = trainers.BpeTrainer(vocab_size=30000,min_frequency=2,special_tokens=special_tokens,
)# 训练分词器
files = ["./corpus.txt"]  # 替换为你的语料文件路径
tokenizer.train(files, trainer)# 添加后处理(如添加[CLS]和[SEP])
tokenizer.post_processor = processors.TemplateProcessing(single="[CLS] $A [SEP]",pair="[CLS] $A [SEP] $B:1 [SEP]:1",special_tokens=[("[CLS]", tokenizer.token_to_id("[CLS]")),("[SEP]", tokenizer.token_to_id("[SEP]")),],
)# 启用填充和截断
from tokenizers import decoders
tokenizer.enable_padding(pad_id=0, pad_token="[PAD]")
tokenizer.enable_truncation(max_length=512)
tokenizer.decoder = decoders.BPEDecoder()# 保存和加载分词器
tokenizer.save("tokenizer.json")
tokenizer = Tokenizer.from_file("tokenizer.json")# 在现有代码的基础上增加以下内容# 使用分词器处理文本并显示详细结果
text = "自然语言处理很有趣!"
output = tokenizer.encode(text)print("Tokens:", output.tokens)
print("IDs:", output.ids)
print("Attention mask:", output.attention_mask)
print("Offsets:", output.offsets)
print("Special tokens mask:", output.special_tokens_mask)
print("Type IDs:", output.type_ids)# 增加解码功能,将ID转换回文本
decoded_text = tokenizer.decode(output.ids)
print("Decoded text:", decoded_text)# 批量处理并显示详细结果
batch = ["自然语言处理是人工智能的重要领域。", "机器学习是实现人工智能的一种方法。", "深度学习是机器学习的一个分支。"]
batch_output = tokenizer.encode_batch(batch)print("\n批量处理结果:")
for i, (text, encoding) in enumerate(zip(batch, batch_output)):print(f"\n文本 {i+1}: {text}")print(f"  Tokens: {encoding.tokens}")print(f"  IDs: {encoding.ids}")print(f"  Attention mask: {encoding.attention_mask}")print(f"  Sequence length: {len(encoding.ids)}")# 增加词汇表信息
print(f"\n词汇表大小: {tokenizer.get_vocab_size()}")
print(f"特殊标记ID:")
for token in ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]:token_id = tokenizer.token_to_id(token)if token_id is not None:print(f"  {token}: {token_id}")# 增加对未知词汇的处理示例
unknown_text = "这是一个包含罕见词汇的句子:🦙"
unknown_output = tokenizer.encode(unknown_text)
print(f"\n包含未知词汇的处理结果:")
print(f"原文本: {unknown_text}")
print(f"Tokens: {unknown_output.tokens}")
print(f"IDs: {unknown_output.ids}")# 显示词汇表中的部分词汇
vocab = tokenizer.get_vocab()
print(f"\n词汇表示例 (前20个):")
for i, (token, id) in enumerate(list(vocab.items())[:20]):print(f"  {token}: {id}")

在这里插入图片描述

2.4 WordPiece分词器

WordPiece是由Google开发的子词分词算法,作为BERT系列模型的核心分词技术,它通过最大化语言模型似然概率来决定子词合并顺序。与BPE不同,WordPiece采用基于概率的合并策略:优先合并能最大程度提升训练数据概率的字符对。其显著特征是使用##前缀标记非首子词(如"playing"→[“play”, “##ing”]),既能处理未登录词,又能保留词形信息,在跨语言任务中表现优异。

2.4.1 WordPiece的工作原理

WordPiece的训练流程基于期望最大化(EM)算法,核心步骤如下:

  1. 初始化词汇表

    • 从训练数据提取所有唯一字符(含标点、数字等)。
    • 添加特殊标记:[UNK](未知词)、[CLS](分类符)、[SEP](分隔符)。
  2. 概率驱动的合并

    • 计算合并得分:对每个字符对(x,y),计算得分 = count(xy)count(x)×count(y)\frac{\text{count}(xy)}{\text{count}(x) \times \text{count}(y)}count(x)×count(y)count(xy)
      (即合并后子词的互信息增益,值越大表示合并越能提升语言模型概率)
    • 选择最优合并:选取得分最高的字符对,合并为新子词(如"e"+"r"→"er")。
  3. 迭代优化

    • 用新子词更新语料库分词结果
    • 重新计算所有字符对得分
    • 重复直到词汇表达到预设大小(BERT-base通常为30,522)

示例:WordPiece训练
语料库["low" (10次), "lower" (8次), "slow" (6次), "flow" (5次), "lowest" (4次)]

步骤0:初始化

  • 词汇表["l", "o", "w", "e", "r", "s", "f", "t"]
  • 语料库分词
    low:    ["l", "o", "w"]
    lower:  ["l", "o", "w", "e", "r"]
    slow:   ["s", "l", "o", "w"]
    flow:   ["f", "l", "o", "w"]
    lowest: ["l", "o", "w", "e", "s", "t"]
    

步骤1:计算合并得分

字符对count(xy)count(x)count(y)得分 = count(xy)/(count(x)×count(y))
(l,o)33333333/(33×33) = 0.0303
(o,w)3333330.0303
(w,e)12331212/(33×12) = 0.0303
(e,r)81288/(12×8) = 0.0833 (最高)
(e,s)412104/(12×10) = 0.0333
(s,l)610336/(10×33) = 0.0182
  • 合并结果"e" + "r" → "er"
  • 新词汇表["l", "o", "w", "e", "r", "s", "f", "t", "er"]

步骤2:重新分词并迭代

  • 更新分词

    lower: ["l", "o", "w", "er"]  # "e"+"r"合并为"er"
    lowest: ["l", "o", "w", "e", "s", "t"]  # 未受影响
    
  • 新字符对统计

    • (w, er):8次(来自"lower")
    • (e, s):4次(来自"lowest")
  • 重新计算得分

    字符对count(xy)count(x)count(y)得分
    (e,s)44104/(4×10)=0.1000 (最高)
    (w,er)83388/(33×8)=0.0303
    (l,o)3333330.0303
  • 合并结果"e" + "s" → "es"

  • 新词汇表["l", "o", "w", "e", "r", "s", "f", "t", "er", "es"]

步骤3:最终分词效果

  • "lowest"分词
    ["l", "o", "w", "es", "t"] → 后续可能合并"l"+"o"→"lo""lo"+"w"→"low"
  • 典型词汇表示例
    ["l", "o", "w", "er", "es", "low", "##er", "##es"]

示例:WordPiece分词
假设最终词汇表["low", "##er", "##es", "s", "f", "l", "o", "w", "t"]

分词"lowest"

  1. 从左到右匹配
    • "l" → 匹配,但"lo"不在词汇表
    • "low"匹配(最长前缀)
    • 剩余"est""e"不在词汇表,尝试"es"匹配
    • 剩余"t"匹配
  2. 添加##规则
    • 首子词"low"无前缀
    • 后续子词"es""t"需加##(但"t"为独立字符)
  3. 结果["low", "##es", "t"]

分词"slower"(未登录词):

  1. 匹配过程
    • "s" → 匹配
    • "sl"不匹配 → 回退到"s"
    • "l" → 匹配
    • "lo"不匹配 → "l"
    • "ow"不匹配 → "o""w"
    • "er"匹配
  2. 结果["s", "l", "o", "w", "##er"]

分词"flow"

  • "f" → 匹配
  • "fl"不匹配 → "f"
  • "l" → 匹配
  • "lo"不匹配 → "l"
  • "ow"不匹配 → "o""w"
  • 结果["f", "l", "o", "w"]

关键特性

  1. 概率驱动合并
    优先合并高互信息的子词(如"er"在英语中作为后缀的极高概率),比BPE的频次统计更符合语言规律。

  2. ##标记机制

    • 首子词无前缀("play"
    • 后续子词强制加##"##ing"),使模型明确识别词边界
  3. 未登录词处理
    对罕见词(如"slower")自动拆解为已知子词,避免[UNK]泛滥,显著提升OOV(Out-of-Vocabulary)处理能力。

WordPiece通过概率建模实现了词形敏感的分词策略,成为BERT等模型实现跨任务迁移学习的关键基础。其设计平衡了词汇表效率与语言表达能力,尤其适合处理英语等屈折语。

代码如下:

from tokenizers import Tokenizer, models, pre_tokenizers, trainers, processors
from tokenizers import normalizers
from tokenizers.normalizers import NFD, StripAccents# 初始化BPE分词器
tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))# 设置预分词器(按空格和标点分割)
tokenizer.pre_tokenizer = pre_tokenizers.Sequence([pre_tokenizers.Whitespace(),pre_tokenizers.Punctuation(),pre_tokenizers.Digits(individual_digits=True)
])# 设置规范化器(可选)
tokenizer.normalizer = normalizers.Sequence([NFD(), StripAccents()])# 定义特殊标记
special_tokens = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]# 定义训练器
trainer = trainers.WordPieceTrainer(vocab_size=30000,min_frequency=2,special_tokens=special_tokens,continuing_subword_prefix="##"  # 对于WordPiece风格
)# 训练分词器
files = ["./corpus.txt"]  # 替换为你的语料文件路径
tokenizer.train(files, trainer)# 添加后处理(如添加[CLS]和[SEP])
tokenizer.post_processor = processors.TemplateProcessing(single="[CLS] $A [SEP]",pair="[CLS] $A [SEP] $B:1 [SEP]:1",special_tokens=[("[CLS]", tokenizer.token_to_id("[CLS]")),("[SEP]", tokenizer.token_to_id("[SEP]")),],
)# 启用填充和截断
from tokenizers import decoderstokenizer.enable_padding(pad_id=0, pad_token="[PAD]")
tokenizer.enable_truncation(max_length=512)
tokenizer.decoder = decoders.WordPiece(prefix="##")# 保存和加载分词器
tokenizer.save("tokenizer.json")
tokenizer = Tokenizer.from_file("tokenizer.json")# 使用分词器处理文本并显示详细结果
text = "自然语言处理很有趣!"
output = tokenizer.encode(text)print("Tokens:", output.tokens)
print("IDs:", output.ids)
print("Attention mask:", output.attention_mask)
print("Offsets:", output.offsets)
print("Special tokens mask:", output.special_tokens_mask)
print("Type IDs:", output.type_ids)# 增加解码功能,将ID转换回文本
decoded_text = tokenizer.decode(output.ids)
print("Decoded text:", decoded_text)# 批量处理并显示详细结果
batch = ["自然语言处理是人工智能的重要领域。", "机器学习是实现人工智能的一种方法。", "深度学习是机器学习的一个分支。"]
batch_output = tokenizer.encode_batch(batch)print("\n批量处理结果:")
for i, (text, encoding) in enumerate(zip(batch, batch_output)):print(f"\n文本 {i + 1}: {text}")print(f"  Tokens: {encoding.tokens}")print(f"  IDs: {encoding.ids}")print(f"  Attention mask: {encoding.attention_mask}")print(f"  Sequence length: {len(encoding.ids)}")# 增加词汇表信息
print(f"\n词汇表大小: {tokenizer.get_vocab_size()}")
print(f"特殊标记ID:")
for token in ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"]:token_id = tokenizer.token_to_id(token)if token_id is not None:print(f"  {token}: {token_id}")# 增加对未知词汇的处理示例
unknown_text = "这是一个包含罕见词汇的句子:🦙"
unknown_output = tokenizer.encode(unknown_text)
print(f"\n包含未知词汇的处理结果:")
print(f"原文本: {unknown_text}")
print(f"Tokens: {unknown_output.tokens}")
print(f"IDs: {unknown_output.ids}")# 显示词汇表中的部分词汇
vocab = tokenizer.get_vocab()
print(f"\n词汇表示例 (前20个):")
for i, (token, id) in enumerate(list(vocab.items())[:20]):print(f"  {token}: {id}")

运行结果:

Tokens: ['[CLS]', '自', '##然', '##语言', '##处理', '##很', '##有', '##趣', '!', '[SEP]']
IDs: [2, 526, 866, 1232, 1282, 1065, 943, 1066, 5, 3]
Attention mask: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Offsets: [(0, 0), (0, 1), (1, 2), (2, 4), (4, 6), (6, 7), (7, 8), (8, 9), (9, 10), (0, 0)]
Special tokens mask: [1, 0, 0, 0, 0, 0, 0, 0, 0, 1]
Type IDs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Decoded text: 自然语言处理很有趣!批量处理结果:文本 1: 自然语言处理是人工智能的重要领域。Tokens: ['[CLS]', '自', '##然', '##语言', '##处理', '##是', '##人', '##工智能', '##的', '##重要', '##领域', '。', '[SEP]']IDs: [2, 526, 866, 1232, 1282, 695, 867, 2139, 701, 1738, 1419, 108, 3]Attention mask: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]Sequence length: 13文本 2: 机器学习是实现人工智能的一种方法。Tokens: ['[CLS]', '[UNK]', '。', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']IDs: [2, 1, 108, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]Attention mask: [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]Sequence length: 13文本 3: 深度学习是机器学习的一个分支。Tokens: ['[CLS]', '[UNK]', '。', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']IDs: [2, 1, 108, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]Attention mask: [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]Sequence length: 13词汇表大小: 2169
特殊标记ID:[PAD]: 0[UNK]: 1[CLS]: 2[SEP]: 3[MASK]: 4包含未知词汇的处理结果:
原文本: 这是一个包含罕见词汇的句子:🦙
Tokens: ['[CLS]', '[UNK]', ':', '[UNK]', '[SEP]']
IDs: [2, 1, 642, 1, 3]词汇表示例 (20):分割: 1591c: 65[PAD]: 0##度: 782best: 1400: 592{: 89build: 1818##ard: 1762: 180##态: 774##fix: 1422: 494: 294: 169##洞: 1141沿: 424##语言特性: 1635: 604贪心合并高频对: 2129

2.5 SentencePiece

2.5.1 SentencePiece的概述

SentencePiece是由Google开发的一种无需预分词的子词分词工具,它将文本直接视为Unicode字符序列进行处理。与传统分词方法不同,SentencePiece不依赖空格或语言特定规则,能够无缝处理多语言文本、标点符号和特殊字符。它支持两种核心算法:BPE(Byte-Pair Encoding)Unigram语言模型,广泛应用于T5、ALBERT等现代语言模型中。

安装库文件,执行命令:

pip install sentencepiece
2.5.2 SentencePiece的工作原理

SentencePiece的核心创新在于直接处理原始文本,无需预分词步骤。其工作流程如下:

  1. 原始文本预处理

    • 将空格完全替换为特殊符号(U+2581,代表词界),例如"cat sat" → "▁cat▁sat"(注意:不保留原始空格)。
    • 保留所有标点和Unicode字符,避免语言依赖性。
  2. 算法选择

    • BPE模式:与标准BPE类似,但以为词边界标志进行字符对合并。
    • Unigram模式(更常用):
      • 从一个过完备的大词汇表开始(通常通过BPE或高频子串生成)
      • 通过迭代剪枝优化词汇表:计算每个子词的损失影响,移除对似然值提升最小的子词
      • 使用EM算法估计子词概率,最大化训练数据的似然概率
      • 推理时使用Viterbi算法选择全局最优分词路径
  3. 词汇表优化

    • 通过正则化控制子词长度,避免过短或过长的子词
    • 支持用户自定义词汇表大小和字符覆盖率

示例:SentencePiece训练(Unigram模式)
语料库["▁cat" (7次), "▁bat" (5次), "▁rat" (9次), "▁car" (4次), "▁cats" (3次)]

步骤0:初始化

  • 生成初始大词汇表(通过BPE或高频子串收集):
    ["▁", "c", "a", "t", "b", "r", "s", "▁c", "ca", "at", "▁b", "▁r", "ar", "ra", "ts", "cats"]
  • 使用EM算法估计初始子词概率

步骤1:迭代剪枝

  • 计算每个子词的损失影响:移除该子词后整体似然值的下降程度
  • 移除低贡献子词:如["r"]["ts"]等对似然值影响最小的子词
  • 重新估计概率:使用EM算法重新计算保留子词的概率

步骤2:最终词汇表

  • 优化后的词汇表示例
    ["▁", "c", "a", "t", "b", "s", "at", "ca", "▁c", "▁b", "cats"]
  • 子词概率(示例值):
    • P(“at”) = 0.21
    • P(“▁c”) = 0.14
    • P(“cats”) = 0.08
    • P(“a”) = 0.12

示例:SentencePiece分词
假设最终词汇表["▁", "c", "a", "t", "b", "s", "at", "ca", "▁c", "▁b", "cats"]
对应概率[0.2, 0.1, 0.12, 0.11, 0.09, 0.08, 0.21, 0.13, 0.14, 0.1, 0.08]

分词"▁cats"

  1. Viterbi路径搜索:考虑所有可能分割
    • 选项1:["▁", "c", "a", "t", "s"] → 概率 = 0.2×0.1×0.12×0.11×0.08 = 2.11e-6
    • 选项2:["▁", "c", "a", "ts"] → "ts"不在词汇表
    • 选项3:["▁", "c", "at", "s"] → 概率 = 0.2×0.1×0.21×0.08 = 3.36e-4
    • 选项4:["▁", "ca", "t", "s"] → 概率 = 0.2×0.13×0.11×0.08 = 2.29e-4
    • 选项5:["▁", "cats"] → 概率 = 0.2×0.08 = 0.016
  2. 选择最优路径:选项5概率最高
  3. 结果["▁", "cats"]

分词"▁car"

  1. Viterbi路径搜索
    • 选项1:["▁", "c", "a", "r"] → "r"不在词汇表
    • 选项2:["▁", "c", "ar"] → "ar"不在词汇表
    • 选项3:["▁c", "a", "r"] → "r"不在词汇表
    • 选项4:["▁c", "ar"] → "ar"不在词汇表
    • 选项5:["▁", "ca", "r"] → "r"不在词汇表
  2. 回退到字符级:只能选择["▁", "c", "a"] + 未知字符"r"
  3. 结果["▁", "c", "a"] + [UNK](实际处理中会对未知字符做特殊处理)

分词"▁bat"

  1. Viterbi路径搜索
    • 选项1:["▁", "b", "a", "t"] → 概率 = 0.2×0.09×0.12×0.11 = 2.38e-4
    • 选项2:["▁", "b", "at"] → 概率 = 0.2×0.09×0.21 = 3.78e-3
    • 选项3:["▁b", "a", "t"] → 概率 = 0.1×0.12×0.11 = 1.32e-3
    • 选项4:["▁b", "at"] → 概率 = 0.1×0.21 = 0.021
  2. 选择最优路径:选项4概率最高
  3. 结果["▁b", "at"]

关键优势

  1. 语言无关性:无需空格分词,适用于中文、日文等无空格语言。
  2. 端到端训练:直接从原始字节流学习,避免预处理偏差。
  3. 灵活控制:通过character_coverage参数平衡罕见字符处理能力(如中文需99.9%,英文99.95%)。

SentencePiece通过统一框架解决了多语言分词的复杂性,成为现代NLP系统中跨语言迁移学习的关键工具。 其Unigram模式尤其擅长处理形态丰富的语言(如土耳其语),而BPE模式则在资源有限场景下保持高效。

代码示例:

import sentencepiece as spm
import json# 训练SentencePiece模型
def train_sentencepiece():# SentencePiece训练spm.SentencePieceTrainer.train(input='./corpus.txt',  # 训练数据文件model_prefix='spm_model',  # 模型前缀vocab_size=8046,  # 词汇表大小character_coverage=0.9995,  # 中文建议接近1.0model_type='bpe',  # 使用BPE模型user_defined_symbols=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],  # 特殊标记max_sentence_length=4096,  # 最大句子长度shuffle_input_sentence=True,  # 随机打乱输入句子minloglevel=1  # 减少日志输出)# 加载训练好的模型
try:train_sentencepiece()sp = spm.SentencePieceProcessor()sp.load('spm_model.model')
except Exception as e:print(f"训练模型时出错: {e}")print("请确保 corpus.txt 文件存在且包含足够的文本数据")# 创建一个简单的测试模型test_text = "自然语言处理是人工智能的重要领域。机器学习很有趣。深度学习是机器学习的一个分支。"with open('test_corpus.txt', 'w', encoding='utf-8') as f:f.write(test_text)spm.SentencePieceTrainer.train(input='test_corpus.txt',model_prefix='spm_model',vocab_size=118,character_coverage=1.0,model_type='bpe',user_defined_symbols=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"])sp = spm.SentencePieceProcessor()sp.load('spm_model.model')# 定义特殊标记的ID
special_tokens = {"[PAD]": sp.piece_to_id("[PAD]"),"[UNK]": sp.piece_to_id("[UNK]"),"[CLS]": sp.piece_to_id("[CLS]"),"[SEP]": sp.piece_to_id("[SEP]"),"[MASK]": sp.piece_to_id("[MASK]")
}print("SentencePiece模型信息:")
print(f"词汇表大小: {sp.get_piece_size()}")
print(f"特殊标记ID: {special_tokens}")# 使用分词器处理文本并显示详细结果
text = "自然语言处理很有趣!"
tokens = sp.encode(text, out_type=str)
ids = sp.encode(text, out_type=int)print(f"\n处理文本: {text}")
print("Tokens:", tokens)
print("IDs:", ids)# 增加解码功能,将ID转换回文本
decoded_text = sp.decode(ids)
print("Decoded text:", decoded_text)# 批量处理并显示详细结果
batch = ["自然语言处理是人工智能的重要领域。", "机器学习是实现人工智能的一种方法。", "深度学习是机器学习的一个分支。"]print("\n批量处理结果:")
for i, text in enumerate(batch):tokens = sp.encode(text, out_type=str)ids = sp.encode(text, out_type=int)print(f"\n文本 {i + 1}: {text}")print(f"  Tokens: {tokens}")print(f"  IDs: {ids}")print(f"  Sequence length: {len(ids)}")# 增加对未知词汇的处理示例
unknown_text = "这是一个包含罕见词汇的句子:🦙"
unknown_ids = sp.encode(unknown_text, out_type=int)
unknown_tokens = sp.encode(unknown_text, out_type=str)
print(f"\n包含未知词汇的处理结果:")
print(f"原文本: {unknown_text}")
print(f"Tokens: {unknown_tokens}")
print(f"IDs: {unknown_ids}")# 显示词汇表中的部分词汇
print(f"\n词汇表示例 (前20个):")
for i in range(min(20, sp.get_piece_size())):piece = sp.id_to_piece(i)print(f"  {piece}: {i}")# 保存模型信息到JSON文件(可选)
model_info = {"vocab_size": sp.get_piece_size(),"special_tokens": special_tokens,"model_prefix": "spm_model"
}with open("spm_model_info.json", "w", encoding="utf-8") as f:json.dump(model_info, f, ensure_ascii=False, indent=2)print(f"\n模型信息已保存到 spm_model_info.json")
print(f"模型文件: spm_model.model")
print(f"词汇表文件: spm_model.vocab")

运行结果:

SentencePiece模型信息:
词汇表大小: 8046
特殊标记ID: {'[PAD]': 3, '[UNK]': 4, '[CLS]': 5, '[SEP]': 6, '[MASK]': 7}处理文本: 自然语言处理很有趣!
Tokens: ['▁自然语言', '处理', '很有趣', '!']
IDs: [2474, 129, 2034, 7887]
Decoded text: 自然语言处理很有趣!批量处理结果:文本 1: 自然语言处理是人工智能的重要领域。Tokens: ['▁自然语言处理是人工智能', '的重要领域', '。']IDs: [3090, 2570, 7683]Sequence length: 3文本 2: 机器学习是实现人工智能的一种方法。Tokens: ['▁', '机器', '学习', '是', '实现', '人工智能的', '一', '种', '方', '法', '。']IDs: [7415, 1571, 476, 7528, 378, 6694, 7523, 0, 7699, 7581, 7683]Sequence length: 11文本 3: 深度学习是机器学习的一个分支。Tokens: ['▁', '深度', '学习', '是', '机器', '学习', '的', '一个', '分', '支', '。']IDs: [7415, 997, 476, 7528, 1571, 476, 7462, 701, 7447, 0, 7683]Sequence length: 11包含未知词汇的处理结果:
原文本: 这是一个包含罕见词汇的句子:🦙
Tokens: ['▁这', '是', '一个', '包含', '罕', '见词汇', '的', '句', '子', ':', '🦙']
IDs: [1449, 7528, 701, 4228, 0, 1936, 7462, 7828, 7727, 7436, 0]词汇表示例 (20):<unk>: 0<s>: 1</s>: 2[PAD]: 3[UNK]: 4[CLS]: 5[SEP]: 6[MASK]: 7**: 8--: 9en: 10|: 11se: 12to: 13te: 14re: 15#: 16=: 17**: 18"": 19模型信息已保存到 spm_model_info.json
模型文件: spm_model.model
词汇表文件: spm_model.vocab

3. 基础应用

3.1 选择指南

任务/语言推荐分词器理由词汇表示示例
英文NLUWordPiece与BERT兼容,适合掩码任务[“play”, “##ing”]
多语言SentencePiece语言无关,统一处理[“大”, “模型”]
代码生成BPE处理符号和标识符好[“def”, “_”, “func”]
低资源语言SentencePiece无需分词器[“த”, “மி”, “ழ்”]
长文本BPE (大词汇表)减少序列长度[“transform”, “##ers”]

常见误区

误区事实解决方案
“词汇表越大越好”过大词汇表增加计算负担根据任务平衡词汇量
“分词器不影响性能”分词直接影响模型表现专门优化分词器
“中文需要特殊分词”SentencePiece统一处理使用无空格分词
“分词只需训练一次”领域变化需重新优化定期评估和调整
“特殊标记不重要”影响任务性能根据任务定制
http://www.xdnf.cn/news/19813.html

相关文章:

  • 信息融智学=信息哲学+信息科学+信息技术+信息系统工程+信息处理之智
  • 组长跟我说,她招人看重的是数据分析能力
  • 计算机视觉(七):膨胀操作
  • 机器学习 - Kaggle项目实践(8)Spooky Author Identification 作者识别
  • awk命令
  • GitHub 上那些值得收藏的英文书籍推荐(计算机 非计算机类)
  • 逻辑回归:从原理到实战的完整指南
  • 刻意练习理论
  • 群晖为家纺企业 500 名员工打造企业网盘,赋能家纺制造效率飞跃
  • Python数据分析与处理(二):将数据写回.mat文件的不同方法【超详细】
  • 第二章 Windows 核心概念通俗解析
  • Linux 的 swap 是什么
  • Vue3 警告:Runtime directive used on component with non-element root node 解决方案
  • 16k+ star! 只需要DDL就能一键生成数据库关系图!
  • 正运动控制卡学习-网络连接
  • Java 黑马程序员(进阶篇1)
  • 从BERT到RoBERTa、ALBERT:揭秘Encoder-only预训练模型的进化之路
  • 【常用SQL语句和语法总结】
  • 数据科学家如何更好地展示自己的能力
  • 项目中 Spring Boot 配置文件未生效该如何解决
  • JavaScript箭头函数与普通函数:两种工作方式的深度解析
  • 诊断服务器(Diagnostic Server)
  • Linux 字符设备驱动框架学习记录(三)
  • Spring Boot 全局异常处理问题分析与解决方案
  • PastePal for Mac 剪贴板历史记录管理器
  • prometheus+grafana搭建
  • 7.4Element Plus 分页与表格组件
  • js设计模式-装饰器模式
  • CI(持续集成)、CD(持续交付/部署)、CT(持续测试)、CICD、CICT
  • Few-Shot Prompting 实战:用5个例子让GPT-4学会复杂任务