讲透Transformer(一):Tokenizer详解——从文本到数字的桥梁
Tokenizer 详解:从文本到数字的桥梁
在理解 Transformer 架构时,我们经常听到 Tokenizer 和 Embedding 这两个概念。它们是什么关系?Tokenizer 到底在做什么?让我为你详细解析。
什么是 Tokenizer?
1.1 Tokenizer 的基本概念
Tokenizer(分词器) 是将原始文本转换为模型可以处理的数字表示的工具。它是 NLP 流程中的第一步,负责将人类语言转化为机器能理解的"语言"。
Tokenizer : 原始文本 → Token IDs \text{Tokenizer}: \text{原始文本} \rightarrow \text{Token IDs} Tokenizer:原始文本→Token IDs
直观理解:
┌─────────────────────────────────────────────────────────────┐
│ Tokenizer 的工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原始文本: "I love AI" │
│ ↓ │
│ 分词 (Tokenization): ["I", "love", "AI"] │
│ ↓ │
│ 映射到 ID: [1045, 2293, 9932] │
│ ↓ │
│ 输入模型 → Embedding 层 → Transformer ... │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 为什么需要 Tokenizer
计算机无法直接理解文本,需要将文本转换为数字:
| 阶段 | 数据形式 | 说明 |
|---|---|---|
| 原始文本 | “Hello world” | 人类可读,机器不可读 |
| Tokenization | [“Hello”, “world”] | 分割成最小单元 |
| Token IDs | [7592, 2088] | 机器可读的数字 |
| Embeddings | [ 0.12 , − 0.34 , . . . ] [0.12, -0.34, ...] [0.12,−0.34,...] | 密集向量表示 |
Tokenizer 的三种主要类型
2.1 Word-based Tokenizer(基于词的分词)
原理:按空格和标点分割单词。
示例:
"Let's learn tokenizer!" → ["Let", "'", 's', "learn", "tokenizer", "!"]
优缺点:
| 优点 | 缺点 |
|---|---|
| 直观易懂 | 词汇表巨大(>50万) |
| 实现简单 | 无法处理未登录词 |
| 保留完整词义 | 时态变体占用多个位置(“run”, “running”, “ran”) |
词汇表大小估算:
- 英语约有 17 万单词
- 加上专有名词、缩写等,轻松超过 50 万
- 导致模型参数量爆炸: ∣ V ∣ × d m o d e l |V| \times d_{model} ∣V∣×dmodel
2.2 Character-based Tokenizer(基于字符的分词)
原理:按单个字符分割。
示例:
"AI" → ["A", "I"]
"hello" → ["h", "e", "l", "l", "o"]
优缺点:
| 优点 | 缺点 |
|---|---|
| 词汇表很小(~100) | 序列长度剧增 |
| 无未登录词问题 | 丢失词级别语义 |
| 任何语言都可处理 | 学习难度大 |
序列长度对比:
- 词级别:“hello” → 1 个 token
- 字符级别:“hello” → 5 个 tokens
- Transformer 计算复杂度 O ( n 2 ) O(n^2) O(n2),长度增加 5 倍意味着计算量增加 25 倍!
2.3 Subword Tokenizer(子词分词)—— 当前主流
原理:在词和字符之间取得平衡,将词拆分为更小的语义单元。
示例(BPE):
"unhappiness" → ["un", "happiness"] # 保留语义
"tokenization" → ["token", "ization"] # 词根+后缀
"playing" → ["play", "ing"] # 词干+时态
核心优势:
┌─────────────────────────────────────────────────────────────┐
│ Subword 的优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 词汇表大小适中:30k-100k │
│ │
│ 处理未登录词:"deeplearning" → ["deep", "learning"] │
│ │
│ 共享语义单元:"run", "running", "ran" 共享 "run" │
│ │
│ 平衡序列长度:比字符短,比词略长 │
│ │
└─────────────────────────────────────────────────────────────┘
主流 Subword 分词算法
3.1 BPE (Byte-Pair Encoding)
原理:从字符开始,迭代合并最频繁的字符对。
算法步骤:
┌─────────────────────────────────────────────────────────────┐
│ BPE 算法流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 初始词汇表:所有字符 + 特殊token │
│ {"h", "e", "l", "o", "w", "r", "d"} │
│ │
│ 训练语料:["hello", "world"] │
│ │
│ Step 1: 统计字符对频率 │
│ ("h", "e"): 1, ("e", "l"): 1, ("l", "l"): 1, ... │
│ │
│ Step 2: 合并最频繁的对 ("l", "o") → "lo" │
│ {"h", "e", "l", "o", "w", "r", "d", "lo"} │
│ │
│ Step 3: 重复直到达到目标词汇表大小 │
│ ... │
│ │
│ 最终词汇表包含:{"h", "e", "l", "o", "w", "r", "d", │
│ "lo", "hel", "wor", "hell", "world"} │
│ │
└─────────────────────────────────────────────────────────────┘
BPE 编码示例:
训练后词汇表:["a", "b", "c", "ab", "bc", "abc"]
文本:"abc" 的分词可能:
- 最优:["abc"] (1 token)
- 次优:["ab", "c"] (2 tokens)
- 最差:["a", "b", "c"] (3 tokens)
算法会选择最长匹配的子词!
3.2 WordPiece (BERT 使用)
原理:类似 BPE,但使用似然概率而非频率决定合并。
核心公式:
合并两个子词 x x x 和 y y y 的得分:
score ( x , y ) = count ( x y ) count ( x ) × count ( y ) \text{score}(x, y) = \frac{\text{count}(xy)}{\text{count}(x) \times \text{count}(y)} score(x,y)=count(x)×count(y)count(xy)
得分越高,表示这两个子词越应该合并。
示例:
统计信息:
count("un") = 1000
count("happy") = 500
count("unhappy") = 200
score("un", "happy") = 200 / (1000 × 500) = 0.0004
如果得分高于阈值,就合并为 "unhappy"
3.3 Unigram (SentencePiece 使用)
原理:假设每个子词独立出现,通过 EM 算法训练。
数学模型:
假设句子 X X X 的分词概率为:
P ( X ) = ∑ x ∈ seg ( X ) ∏ i = 1 ∣ x ∣ p ( x i ) P(X) = \sum_{x \in \text{seg}(X)} \prod_{i=1}^{|x|} p(x_i) P(X)=x∈seg(X)∑i=1∏∣x∣p(xi)
其中 seg ( X ) \text{seg}(X) seg(X) 是 X X X 的所有可能分词方式, p ( x i ) p(x_i) p(xi) 是子词 x i x_i xi 的概率。
Viterbi 算法用于解码:
输入:"unhappiness"
词汇表概率:
p("un") = 0.1
p("happiness") = 0.05
p("ha") = 0.01
...
动态规划找到最大概率路径:
["un", "happiness"] 概率 = 0.1 × 0.05 = 0.005
["un", "ha", "ppiness"] 概率 = 0.1 × 0.01 × ... = 更小
选择概率最大的分词方式!
3.4 三种算法对比
| 算法 | 代表模型 | 特点 | 词汇表大小 |
|---|---|---|---|
| BPE | GPT, RoBERTa | 简单高效,频率驱动 | 50k |
| WordPiece | BERT, DistilBERT | 概率驱动,更精细 | 30k |
| Unigram | T5, ALBERT | 概率模型,可采样 | 32k-250k |
Tokenizer 的工作流程详解
4.1 完整流程
┌─────────────────────────────────────────────────────────────┐
│ Tokenizer 完整流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原始文本: "I love AI!" │
│ ↓ │
│ Step 1: 文本规范化 (Normalization) │
│ 处理:统一大小写、Unicode 标准化等 │
│ "i love ai!" │
│ ↓ │
│ Step 2: 预分词 (Pre-tokenization) │
│ 按空格和标点初步分割 │
│ ["i", "love", "ai", "!"] │
│ ↓ │
│ Step 3: 子词分割 (Subword Segmentation) │
│ 应用 BPE/WordPiece 算法 │
│ ["i", "love", "ai", "[UNK]"] (如果 "!" 不在词汇表) │
│ ↓ │
│ Step 4: 编码到 ID (Encode to IDs) │
│ 查找词汇表映射 │
│ [1045, 2293, 9932, 1000] │
│ ↓ │
│ Step 5: 添加特殊标记 (Add Special Tokens) │
│ 添加 [CLS], [SEP], [PAD] 等 │
│ [101, 1045, 2293, 9932, 1000, 102] │
│ ↓ │
│ Step 6: 创建注意力掩码 (Attention Mask) │
│ 标记哪些是真实 token,哪些是 padding │
│ [1, 1, 1, 1, 1, 1] │
│ ↓ │
│ 最终输入: │
│ input_ids: [101, 1045, 2293, 9932, 1000, 102] │
│ attention_mask: [1, 1, 1, 1, 1, 1] │
│ token_type_ids: [0, 0, 0, 0, 0, 0] (单句任务) │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 代码实现 (HuggingFace Tokenizers)
from transformers import AutoTokenizer
# 加载 BERT 的 tokenizer
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
# 原始文本
text = "I love AI!"
# Step 1-4: 直接编码
encoded = tokenizer(text)
print(encoded)
# {
# 'input_ids': [101, 1045, 2293, 9932, 999, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0],
# 'attention_mask': [1, 1, 1, 1, 1, 1]
# }
# 分步查看
# 1. 预分词
pretokens = tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(text)
print("Pre-tokens:", pretokens)
# [('i', (0, 1)), ('love', (2, 6)), ('ai', (7, 9)), ('!', (9, 10))]
# 2. 子词分词
tokens = tokenizer.tokenize(text)
print("Tokens:", tokens)
# ['i', 'love', 'ai', '!']
# 3. 转 ID
ids = tokenizer.convert_tokens_to_ids(tokens)
print("IDs:", ids)
# [1045, 2293, 9932, 999]
# 4. 添加特殊标记
input_ids = tokenizer.build_inputs_with_special_tokens(ids)
print("With special tokens:", input_ids)
# [101, 1045, 2293, 9932, 999, 102]
# 解码回文本
decoded = tokenizer.decode(input_ids)
print("Decoded:", decoded)
# "[CLS] i love ai! [SEP]"
Tokenizer 与 Embedding 的关系
5.1 完整数据处理流水线
┌─────────────────────────────────────────────────────────────┐
│ 从文本到向量:完整流水线 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原始文本 │
│ "I love AI" │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Tokenizer │ │
│ │ • 文本规范化 │ │
│ │ • 预分词 │ │
│ │ • 子词分割 │ │
│ │ • 映射到 ID │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ Token IDs: [1045, 2293, 9932] │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Embedding Layer │ │
│ │ • 查找表:E ∈ R^{|V| × d} │ │
│ │ • 每个 ID 映射到向量 │ │
│ │ • 输出形状:[seq_len, d] │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ 词嵌入矩阵: │
│ [[0.12, -0.34, 0.56, ...], ← "I" 的向量 │
│ [0.23, 0.45, -0.67, ...], ← "love" 的向量 │
│ [-0.11, 0.78, -0.22, ...]] ← "AI" 的向量 │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ + Positional Encoding │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ 带位置信息的嵌入: │
│ [[0.12+PE₀, -0.34+PE₀, ...], │
│ [0.23+PE₁, 0.45+PE₁, ...], │
│ [-0.11+PE₂, 0.78+PE₂, ...]] │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Transformer Layers │ │
│ │ • Self-Attention 融合上下文 │ │
│ │ • FFN 非线性变换 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ 上下文嵌入: │
│ [[0.45, -0.12, 0.89, ...], ← "I" 在上下文中的表示 │
│ [0.67, 0.23, -0.45, ...], ← "love" 在上下文中的表示 │
│ [0.11, -0.56, 0.78, ...]] ← "AI" 在上下文中的表示 │
│ │
└─────────────────────────────────────────────────────────────┘
5.2 Tokenizer 和 Embedding 的分工
| 组件 | 输入 | 输出 | 作用 | 是否可学习 |
|---|---|---|---|---|
| Tokenizer | 原始文本 | Token IDs | 文本 → 数字 | ❌ 不可学习 |
| Embedding | Token IDs | 向量 | 数字 → 语义空间 | ✅ 可学习 |
关键区别:
- Tokenizer 是规则/统计驱动的预处理步骤
- Embedding 是神经网络参数,随训练更新
5.3 数学关系
假设:
- 词汇表 V \mathcal{V} V,大小 ∣ V ∣ |V| ∣V∣
- 嵌入维度 d d d
Tokenizer 定义映射:
T : 文本 → N ∣ V ∣ T: \text{文本} \rightarrow \mathbb{N}^{|V|} T:文本→N∣V∣
Embedding 定义映射:
E : N ∣ V ∣ → R d E: \mathbb{N}^{|V|} \rightarrow \mathbb{R}^d E:N∣V∣→Rd
完整流程:
文本 → T Token ID → E 向量 → Transformer 上下文向量 \text{文本} \xrightarrow{T} \text{Token ID} \xrightarrow{E} \text{向量} \xrightarrow{\text{Transformer}} \text{上下文向量} 文本TToken IDE向量Transformer上下文向量
主流模型的 Tokenizer 配置
6.1 各模型 Tokenizer 对比
| 模型 | Tokenizer 类型 | 词汇表大小 | 特殊标记 | 特点 |
|---|---|---|---|---|
| BERT | WordPiece | 30,522 | [CLS], [SEP], [MASK] | 大小写敏感/不敏感版本 |
| GPT-2/3 | BPE | 50,257 | `< | endoftext |
| RoBERTa | BPE | 50,265 | <s>, </s>, <mask> |
动态掩码 |
| T5 | SentencePiece | 32,000 | <extra_id_0>… |
无监督预训练 |
| LLaMA | BPE | 32,000 | <s>, </s> |
字节级 BPE |
| Qwen | BPE | 152,064 | `< | im_start |
| DeepSeek | BPE | 128,256 | <|begin▁of▁sentence|> |
字节级 BPE |
6.2 代码示例:不同模型的 Tokenizer
from transformers import AutoTokenizer
# BERT (WordPiece)
bert_tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
print(bert_tokenizer.tokenize("unhappiness"))
# ['un', '##happiness'] # ## 表示是子词的一部分
# GPT-2 (BPE)
gpt2_tokenizer = AutoTokenizer.from_pretrained('gpt2')
print(gpt2_tokenizer.tokenize("unhappiness"))
# ['un', 'h', 'appiness'] # 字节级 BPE
# LLaMA (BPE)
llama_tokenizer = AutoTokenizer.from_pretrained('meta-llama/Llama-2-7b-hf')
print(llama_tokenizer.tokenize("unhappiness"))
# ['un', 'h', 'appiness'] # 类似 GPT-2
# T5 (SentencePiece - Unigram)
t5_tokenizer = AutoTokenizer.from_pretrained('t5-small')
print(t5_tokenizer.tokenize("unhappiness"))
# ['▁un', 'happiness'] # ▁ 表示词开头
Tokenizer 的挑战与优化
7.1 未登录词处理
问题:遇到词汇表中没有的词怎么办?
解决方案:
┌─────────────────────────────────────────────────────────────┐
│ 未登录词处理策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 退化为子词 (Subword Fallback) │
│ "deeplearning" → ["deep", "learning"] ✓ 最常用 │
│ │
│ 2️⃣ 退化为字符 (Character Fallback) │
│ "deeplearning" → ["d", "e", "e", "p", ...] ✗ 序列过长 │
│ │
│ 3️⃣ 使用 [UNK] 标记 (Unknown Token) │
│ "deeplearning" → ["[UNK]"] ✗ 信息丢失严重 │
│ │
│ 4️⃣ 字节级表示 (Byte-level BPE) │
│ 任何 UTF-8 字符都可表示,无 [UNK] ✓ GPT-2/LLaMA 使用 │
│ │
└─────────────────────────────────────────────────────────────┘
字节级 BPE 示例:
# 字节级 BPE 可以处理任何 Unicode
text = "Hello 你好 🌟"
tokens = tokenizer.tokenize(text)
# 所有字符都能被表示,没有 [UNK]
7.2 多语言支持
| 模型 | 语言覆盖 | Tokenizer 策略 |
|---|---|---|
| mBERT | 104 种 | 共享 WordPiece |
| XLM-R | 100 种 | 共享 BPE |
| Qwen | 中英为主 | 针对中文字符优化 |
| GPT-4 | 多语言 | 大规模 BPE |
7.3 效率优化
问题:长文本的 Tokenization 可能成为瓶颈。
优化策略:
- 缓存机制:重复文本避免重算
- 批处理:同时处理多个文本
- 预分词加速:使用 Rust 实现(HuggingFace Tokenizers)
- 流式处理:逐块处理超长文本
实际应用示例
8.1 完整的 BERT 输入构建
from transformers import BertTokenizer
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 单句任务
sentence = "I love AI"
encoded = tokenizer(
sentence,
max_length=10,
padding='max_length',
truncation=True,
return_tensors='pt'
)
print("Input IDs:", encoded['input_ids'])
print("Attention Mask:", encoded['attention_mask'])
print("Token Type IDs:", encoded['token_type_ids'])
# Input IDs: tensor([[ 101, 1045, 2293, 9932, 102, 0, 0, 0, 0, 0]])
# Attention Mask: tensor([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0]])
# Token Type IDs: tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
# 句子对任务 (NLI/QA)
sentence1 = "I love AI"
sentence2 = "AI is fascinating"
encoded_pair = tokenizer(
sentence1,
sentence2,
max_length=20,
padding='max_length',
truncation=True,
return_tensors='pt'
)
print("Input IDs shape:", encoded_pair['input_ids'].shape)
# torch.Size([1, 20])
# 解码查看特殊标记
print(tokenizer.decode(encoded_pair['input_ids'][0]))
# "[CLS] i love ai [SEP] ai is fascinating [SEP] [PAD] [PAD] ..."
8.2 批量处理
texts = [
"I love AI",
"Machine learning is amazing",
"Deep learning",
"NLP"
]
# 批量处理自动 padding
encoded_batch = tokenizer(
texts,
padding=True,
truncation=True,
max_length=10,
return_tensors='pt'
)
print("Batch input_ids shape:", encoded_batch['input_ids'].shape)
# torch.Size([4, 10]) # 4 个句子,统一长度 10
# 查看 padding 情况
for i, ids in enumerate(encoded_batch['input_ids']):
print(f"Sentence {i}: {tokenizer.decode(ids)}")
Tokenizer 与 Embedding 的交互
9.1 前向传播中的角色
import torch.nn as nn
class TransformerModel(nn.Module):
def __init__(self, vocab_size, d_model):
super().__init__()
# Embedding 层依赖 Tokenizer 定义的词汇表大小
self.embedding = nn.Embedding(vocab_size, d_model)
self.transformer = nn.TransformerEncoder(...)
def forward(self, text):
# Step 1: Tokenizer (外部处理)
# input_ids = tokenizer(text)['input_ids']
# Step 2: Embedding 查找
x = self.embedding(input_ids) # [seq_len, d_model]
# Step 3: Transformer 处理
output = self.transformer(x)
return output
9.2 梯度传播
重要:Tokenizer 本身不参与梯度传播!
原始文本 ──→ Token IDs ──→ Embedding ──→ Transformer ──→ Loss
↑ ↑ ↑
不可学习 可学习 可学习
(规则/统计) (梯度更新) (梯度更新)
总结
10.1 Tokenizer 核心要点
┌─────────────────────────────────────────────────────────────┐
│ Tokenizer 核心要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 什么是 Tokenizer │
│ • 将文本转换为数字 ID 的工具 │
│ • NLP 流程的第一步,连接人类语言和机器 │
│ │
│ 2️⃣ 三种主要类型 │
│ • 词级别:直观但词汇表大 │
│ • 字符级别:词汇表小但序列长 │
│ • 子词级别:平衡两者,当前主流 │
│ │
│ 3️⃣ 主流算法 │
│ • BPE (GPT):频率驱动,迭代合并 │
│ • WordPiece (BERT):概率驱动,更精细 │
│ • Unigram (T5):概率模型,可采样 │
│ │
│ 4️⃣ 与 Embedding 的关系 │
│ • Tokenizer:文本 → 数字 (不可学习) │
│ • Embedding:数字 → 向量 (可学习) │
│ • 共同构成从文本到语义空间的桥梁 │
│ │
└─────────────────────────────────────────────────────────────┘
10.2 简单记忆
| 概念 | 类比 |
|---|---|
| Tokenizer | 字典的索引系统(找到词的编号) |
| Embedding | 字典的解释(词的向量表示) |
| Token IDs | 词的编号(如 “001” 代表 “猫”) |
| Embeddings | 词的含义(“猫” 的完整描述) |
10.3 一句话总结
Tokenizer 负责把文本变成数字 ID,Embedding 负责把这些数字 ID 变成有语义的向量,两者配合让模型能够理解和处理自然语言! 🚀
更多推荐



所有评论(0)