【13】Transformers快速入门:Transformers 分词器 (Tokenizer) 实战?
课程模块:Transformers 分词器 (Tokenizer) 实战
目标: 理解为什么需要分词器,掌握常见分词策略的区别,学会加载、保存分词器,并能熟练使用分词器对文本进行编码和解码。
核心概念: 分词 (Tokenization), 编码 (Encoding), 解码 (Decoding), 词表 (Vocabulary), 子词切分 (Subword Tokenization), AutoTokenizer
, from_pretrained
, save_pretrained
, tokenize
, convert_tokens_to_ids
, encode
, decode
, 特殊 Token ([CLS]
, [SEP]
, [UNK]
)
1. 为什么需要分词器?—— 让文本“说”模型能懂的语言
想象一下,你是一位精通多种语言的翻译官(模型),但你的大脑只能处理数字(神经网络)。现在,有人递给你一段英文文本(比如 “I love transformers!”),你需要把它翻译成法语。但问题来了:你的“数字大脑”看不懂英文单词!
这就是分词器 (Tokenizer) 的作用!它就像一位专业的文本预处理助手,负责:
- “切分”文本: 把一串连续的文本(句子、段落)拆分成更小的、有意义的片段,称为 Tokens。这就像把一整块面包切成方便入口的小片。
- “翻译”成数字: 把这些 Tokens 转换成模型能理解的数字——Token IDs。这就像给每个面包片贴上一个唯一的编号标签。
这个过程合起来就叫编码 (Encoding):文本 -> Tokens -> Token IDs
。
核心任务: 分词器是连接人类语言和模型数字世界的桥梁! 没有它,再强大的模型也无法理解我们输入的文字。
2. 怎么“切”?—— 揭秘分词策略
就像切面包可以用刀、锯齿刀或面包刀一样,分词也有不同的策略,各有优劣:
-
策略一:按词切分 (Word-based)
- 做法: 最简单粗暴!直接按空格、标点把文本切成一个个单词。
text = "Jim Henson was a puppeteer" tokens = text.split() # ['Jim', 'Henson', 'was', 'a', 'puppeteer']
- 优点: 直观,容易理解。
- 致命缺点:
- 词表爆炸: 每个单词(包括不同形式如 “dog”, “dogs”, “running”)都算作不同的 token,词表会变得巨大无比(几十万甚至上百万),模型难以处理。
- 无法处理未知词: 遇到词表里没有的词(比如拼写错误、罕见词、新词),只能用
[UNK]
(unknown) 表示,丢失信息。想象一下,翻译官遇到不认识的词只能说“我不知道”,这翻译就没法做了! - 忽略词间关系: “dog” 和 “dogs” 明明有关联,却被当作完全不同的东西。
- 做法: 最简单粗暴!直接按空格、标点把文本切成一个个单词。
-
策略二:按字符切分 (Character-based)
- 做法: 精细到极致!把文本切成一个个字符(字母、标点、甚至空格)。
text = "Dog" tokens = list(text) # ['D', 'o', 'g'] (英文) 或 ['狗'] (中文单字)
- 优点:
- 词表超小: 英文就几十个字符,中文几千个常用字。
- 几乎无
[UNK]
: 字符组合总能拼出新词。
- 致命缺点:
- 失去语义单元: 单个字符(尤其是英文字母)通常没有明确含义。
'D'
、'o'
、'g'
单独看谁知道是“狗”?模型需要从更零散的碎片中学习语义,难度剧增。 - 序列超长: 一个单词变成多个 token,一个句子就变成超长序列,模型处理效率低。想象翻译官要处理“D-o-g”三个信息点才能理解“狗”,太累了!
- 失去语义单元: 单个字符(尤其是英文字母)通常没有明确含义。
- 做法: 精细到极致!把文本切成一个个字符(字母、标点、甚至空格)。
-
策略三(明星策略):按子词切分 (Subword Tokenization)
- 核心思想: 高频词保留,低频词拆分! 找一个平衡点。
- 做法:
- 高频词(如 “the”, “is”, “dog”)直接作为独立的 token。
- 低频词或复杂词(如 “annoyingly”, “tokenization”)被拆分成更小的、有意义的子词(如 “annoying” + “ly”, “token” + “ization”)。
- 优点(完美解决前两种的问题):
- 词表大小适中: 通常在几万到十几万,既不会爆炸,也能覆盖绝大多数文本。
- 极少
[UNK]
: 即使遇到新词,也能拆分成已知的子词组合理解其意(比如 “Bumblebee” 可能拆成 “Bumble” + “bee”)。 - 保留语义和效率: 子词通常有意义(如 “ly” 表示副词,“ization” 表示名词化),模型更容易学习。同时,一个长词只用几个 token 表示,序列长度合理。
- 跨语言友好: 尤其擅长处理像土耳其语、芬兰语这种靠拼接词根形成复杂长词的“黏着语”。
- Transformer 模型的最爱: BERT、GPT、T5 等主流模型都使用子词切分策略(如 Byte-Pair Encoding - BPE, WordPiece, SentencePiece)。
- 例子: “Let’s do tokenization!” 可能被切分为
['Let', "'", 's', 'do', 'token', 'ization', '!']
。你看,“tokenization” 被优雅地拆成了常见的 “token” 和 “ization”。
结论: 子词切分是现代 Transformer 模型的标准分词策略! 它聪明地平衡了词表大小、语义保留和处理效率。
3. 请出助手:加载与保存分词器
和加载模型一样,transformers
库提供了两种加载分词器的方式:
- 方式一:指名道姓 (
BertTokenizer
,GPT2Tokenizer
等)from transformers import BertTokenizer bert_tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
- 方式二:智能匹配 (
AutoTokenizer
- 强烈推荐!)
为什么推荐from transformers import AutoTokenizer # 加载 BERT 分词器 tokenizer = AutoTokenizer.from_pretrained("bert-base-cased") # 想换 GPT-2?只需改名字! gpt2_tokenizer = AutoTokenizer.from_pretrained("gpt2")
AutoTokenizer
? 和AutoModel
一样,它让代码更灵活!切换模型时,只需改 checkpoint 名字,分词器代码无需改动。
加载来源:
- 云端 Hugging Face Hub (需网络):
from_pretrained("checkpoint_name")
- 本地目录 (推荐):
from_pretrained("./path/to/tokenizer/")
保存分词器:
保存和模型一样简单,使用 .save_pretrained()
:
tokenizer.save_pretrained("./my_saved_tokenizers/bert-base-cased/")
保存了什么?
保存目录下会生成几个关键文件:
tokenizer_config.json
: 分词器的“说明书”。记录了它是哪种类型的分词器(如 WordPiece),以及它的配置参数(比如特殊 token 是什么)。vocab.txt
(或vocab.json
等): 词表文件! 这是核心!里面列出了所有 token,一行一个 token。行号就是该 token 的 ID(从 0 开始)。模型就是靠这个文件把 token 变成数字 ID 的。special_tokens_map.json
: 特殊 Token 的“花名册”。记录了[UNK]
(未知词),[CLS]
(分类),[SEP]
(分隔),[PAD]
(填充),[MASK]
(掩码) 等特殊 token 对应的实际字符串是什么(比如[UNK]
可能对应"<unk>"
)。
重要提示: 分词器文件和模型文件是分开的!通常你需要同时保存(或下载)模型和对应的分词器。
4. 实战操作:编码与解码文本
现在,让我们请出 AutoTokenizer
助手,看看它如何把文本变成数字,以及如何把数字变回文本。
-
步骤一:加载分词器
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-cased") # 例子用 BERT
-
步骤二:分词 (Tokenize) - 切成 Tokens
sequence = "Using a Transformer network is simple" tokens = tokenizer.tokenize(sequence) print(tokens) # 输出:['Using', 'a', 'Transform', '##er', 'network', 'is', 'simple']
观察:
- “Transformer” 被切成了
'Transform'
和'##er'
。这是 BERT 使用的 WordPiece 算法的标志:'##'
表示这个 token 是前面 token 的一部分(后缀),需要和前面的 token 拼接起来才能组成完整单词。
- 分词器自动处理了大小写(“Using” 保持大写,因为用的是
bert-base-
cased
)。
- “Transformer” 被切成了
-
步骤三:映射为 ID (Convert Tokens to IDs)
ids = tokenizer.convert_tokens_to_ids(tokens) print(ids) # 输出:[7993, 170, 13809, 23763, 2443, 1110, 3014]
原理: 分词器拿着每个 token (如
'Using'
),去查它的词表文件vocab.txt
,找到'Using'
在第几行(比如第 7993 行),那么它的 ID 就是 7993。 -
步骤四:一步到位编码 (Encode) - 更常用!
encoded_output = tokenizer(sequence) # 或者 tokenizer.encode(sequence, ...) print(encoded_output) # 输出: # { # 'input_ids': [101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102], # 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0], # 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1] # }
tokenizer()
的强大之处:- 它自动完成了分词 + 映射 ID。
- 它自动添加了模型需要的特殊 token:
101
([CLS]
):通常放在序列开头,用于分类任务。102
([SEP]
):通常放在序列结尾,或用于分隔两个句子(如问答)。
- 它返回了模型需要的所有输入:
input_ids
:Token IDs 列表,是模型最主要的输入。token_type_ids
(或segment_ids
):用于区分句子。例如,第一个句子全为 0,第二个句子全为 1。单句任务通常全为 0。attention_mask
:告诉模型哪些位置是真实的 token (1
),哪些是填充的位置 (0
)。这里没有填充,所以全是 1。
- 这是实际编码文本时最常用的方法!
-
步骤五:解码 (Decode) - 数字变回文本
# 解码我们之前得到的 ID 列表 (注意:这个列表没有包含 [CLS] 和 [SEP]) decoded_text = tokenizer.decode([7993, 170, 13809, 23763, 2443, 1110, 3014]) print(decoded_text) # 输出:Using a Transformer network is simple# 解码包含 [CLS] 和 [SEP] 的完整 ID 列表 decoded_text_with_special = tokenizer.decode([101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102]) print(decoded_text_with_special) # 输出:[CLS] Using a Transformer network is simple [SEP]
解码的魔法:
- 它不仅仅是把 ID 变回 token 字符串。
- 它智能地合并了那些被切分的子词!比如把
'Transform'
和'##er'
无缝拼接成 “Transformer”。 - 它保留了特殊 token(如
[CLS]
,[SEP]
)。在实际任务中(如文本生成),你可能需要在解码后手动去除这些特殊 token。 - 解码是生成类任务(如翻译、摘要、对话)的关键步骤!
课程总结:
- 分词器是桥梁: 将文本 (
str
) 编码 (encode
) 成模型能吃的数字 (input_ids
),将模型生成的数字解码 (decode
) 回人类可读的文本。 - 分词策略: 子词切分 (Subword) 是主流(如 BPE, WordPiece),平衡词表大小、语义保留和处理效率。
- 加载分词器: 首选
AutoTokenizer.from_pretrained()
,来源可以是 Hub checkpoint 名或本地路径。 - 保存分词器: 使用
tokenizer.save_pretrained()
,生成tokenizer_config.json
,vocab.txt
,special_tokens_map.json
等关键文件。 - 编码文本:
- 了解过程:
text -> tokens (tokenize) -> input_ids (convert_tokens_to_ids)
。 - 实战首选:
tokenizer(text)
或tokenizer.encode()
,一步到位得到包含input_ids
,token_type_ids
,attention_mask
的字典。 - 注意:模型会自动添加特殊 token (如
[CLS]
,[SEP]
)。
- 了解过程:
- 解码文本: 使用
tokenizer.decode(input_ids)
,它能合并子词并处理特殊 token。 - 词表 (vocab.txt): 核心文件!定义了 token 到 ID 的映射关系(行号即 ID)。
提示:
- 动手实验: 用
AutoTokenizer
加载不同的模型(如'bert-base-cased'
,'gpt2'
,'t5-small'
),对同一句话调用tokenize()
和tokenizer()
,观察它们的分词策略和输出差异。 - 探索词表: 下载或保存一个分词器,打开它的
vocab.txt
文件看看里面有什么。尝试查找一些常见词和特殊 token 的 ID。 - 理解解码: 尝试用
decode()
函数解码一些你自己构造的 ID 列表(可以从vocab.txt
里挑几个 ID),观察输出结果。特别注意含有##
前缀的 token 是如何被合并的。 - 思考: 为什么
tokenizer()
返回的字典里,token_type_ids
和attention_mask
也是必要的?它们分别解决了什么问题?(提示:多句子任务、处理不同长度的序列)