02-Tokenizer原理与实现

阶段 2:Tokenizer 原理与实现

教学目标

  • 理解 Tokenizer 在 LLM 中的核心作用
  • 掌握 BPE(Byte Pair Encoding)算法的原理
  • 理解 Byte-Level BPE 的优势与实现
  • 了解 MiniMind Tokenizer 的词表设计和特殊 Token
  • 能够独立使用 Tokenizer 进行文本编码/解码实验

课时安排

  • 理论讲解:1.5 课时(分词原理 + BPE 算法)
  • 上机实践:0.5 课时(编码/解码实验 + 分析)

2.1 为什么需要 Tokenizer?

2.1.1 从文本到数字

大语言模型的输入输出都是数字(整数索引) ,但人类使用的是自然语言文本。Tokenizer 就是连接两者的"翻译器":

plain 复制代码
原始文本                        模型输入
"Transformer 是一种            →  [2341, 589, 1023, 87,
 深度学习模型"                     4521, 312, 7689]
                                         │
                                    词表索引(vocab_size=6400)

2.1.2 Tokenizer 的三大核心任务

任务 说明 示例
编码(Encode) 文本 → Token 序列 → 整数序列 "你好世界" → "你", "好", "世", "界"234, 567, 89, 12
解码(Decode) 整数序列 → Token 序列 → 文本 234, 567, 89, 12 → "你好世界"
词表管理 维护 token → index 的映射 {"你": 234, "好": 567, ...}

2.1.3 Tokenizer 设计的核心矛盾

方案 优点 缺点
字符级(Character) 词表极小(几千),无 OOV 问题 序列太长,模型难以捕获长距离依赖
词级(Word) 序列较短,语义完整 词表巨大(数十万),大量 OOV(未登录词)
子词级(Subword / BPE) 平衡词表大小和序列长度 需要训练,实现稍复杂

教学提示:MiniMind 选择的是**子词级(BPE)**方案,这也是当前主流 LLM(GPT、LLaMA、Qwen 等)的共同选择。


2.2 BPE(Byte Pair Encoding)算法原理

2.2.1 BPE 核心思想

BPE 是一种数据驱动的子词切分算法,通过不断合并语料中出现频率最高的相邻 token 对,逐步构建词表。

核心直觉:经常一起出现的字符组合应该被合并为一个 token。

2.2.2 BPE 算法步骤(训练阶段)

以一个简单语料为例:

plain 复制代码
语料:"low lower newest widest"
初始词序列(字符级):
  l o w </w>  l o w e r </w>  n e w e s t </w>  w i d e s t </w>

Step 1:统计所有相邻字符对的频率

字符对 频率
e, s 3
t, 2
s, t 2
l, o 2
o, w 2
w, 1
... ...

Step 2 :合并频率最高的字符对 → es 成为一个新 token

plain 复制代码
l o w </w>  l o w e r </w>  n e w es t </w>  w i d es t </w>

词表新增:es

Step 3 :重新统计,合并下一个最高频对 → est

plain 复制代码
l o w </w>  l o w e r </w>  n e w est </w>  w i d est </w>

词表新增:est

Step 4 :继续合并 → lolowlow</w>newnewest</w> ...

plain 复制代码
low</w>  low e r </w>  newest</w>  w i d est</w>

Step 5:重复直到达到目标词表大小

最终词表包含:所有单个字符 + 所有合并产生的新 token

2.2.3 BPE 编码过程(推理阶段)

对新文本进行切分时,按照训练得到的合并规则从低频到高频逐步应用:

plain 复制代码
输入:"lowest"
1. 初始字符分解:l o w e s t </w>

2. 查表,es 存在?是 → l o w es t </w>

3. 查表,est 存在?是 → l o w est </w>

4. 查表,lo 存在?是 → lo w est </w>

5. 查表,low 存在?是 → low est </w>

6. 查表,lowest 存在?否 → 停止

编码结果:[low, est, </w>] → [4521, 312, 7689]

2.2.4 BPE 算法伪代码

python 复制代码
def train_bpe(corpus, vocab_size):
    # 初始化:将所有词拆分为字符序列
    word_freqs = count_words(corpus)  # {"l o w </w>": 1, ...}
    vocab = set(all_chars(corpus))     # 初始词表 = 所有单个字符

    while len(vocab) < vocab_size:
        # 统计所有相邻对的频率
        pairs = count_pair_freqs(word_freqs)

        # 找出最高频的相邻对
        best_pair = max(pairs, key=pairs.get)

        # 将该对合并为新 token
        new_token = best_pair[0] + best_pair[1]
        vocab.add(new_token)

        # 在所有词中执行合并
        word_freqs = merge_pair(best_pair, word_freqs)

    return vocab, merge_rules

2.3 Byte-Level BPE

2.3.1 为什么选择 Byte-Level?

传统 BPE 直接在字符级别工作,存在一个问题:不同语言的字符集差异巨大。

语言 字符集大小 问题
英文 128(ASCII) 字符集小,容易处理
中文 ~20,000 常用字 字符集巨大,词表膨胀
日文/韩文/emoji 数千种特殊字符 混合文本处理困难

Byte-Level BPE 的解决方案:不再以 Unicode 字符为单位,而是以**字节(Byte,0-255)**为单位。

2.3.2 Byte-Level BPE 核心优势

优势 说明
词表基础固定 基础词表只有 256 个字节 token,无需为每种语言维护单独字符集
无 OOV 问题 任何文本(包括 emoji、代码、特殊符号)都可以被编码
多语言统一 中英文混合文本使用同一套词表,无需额外处理
与模型输入兼容 模型看到的始终是固定范围的整数(0-255 或合并后的更大范围)

2.3.3 Byte-Level BPE 工作流程

plain 复制代码
原始文本:"你好,Hello!"
     │
     ▼ UTF-8 编码
字节序列:[228, 189, 160, 229, 165, 189, 239, 188, 140, 72, 101, 108, 108, 111, 33]
     │
     ▼ BPE 合并(训练阶段学习到的合并规则)
Token 序列:[228, 189, 160, 229, 165, 189, 239, 188, 140, 72, "ell", "o", 33]
     │
     ▼ 查词表映射为整数索引
整数序列:[142, 78, 203, 156, 312, 445, 789, 45, 2341, 567, 12]

2.4 MiniMind Tokenizer 详解

2.4.1 词表设计

MiniMind 使用自定义的 minimind_tokenizer,关键参数:

参数 说明
词表大小(vocab_size) 6400 极简设计,远小于主流模型(通常 32K-128K)
编码方式 Byte-Level BPE 基于字节的子词切分
特殊 Token __(PAD)、`< endoftext
思考 Token minimumsorrytool_calls 用于自适应思考和工具调用

教学提示:6400 的词表在主流 LLM 中非常小(GPT-4 约 100K,LLaMA 约 32K)。这是 MiniMind "大道至简" 理念的体现,但也意味着每个 token 携带的信息更少,序列更长。

2.4.2 中文/英文压缩比

语言 平均字符/token 比 说明
中文 1.5~1.7 字符/token 一个中文字通常需要 2-3 个 UTF-8 字节,经 BPE 合并后平均 1.5-1.7 字符对应 1 个 token
英文 4~5 字符/token 英文单词常见组合被 BPE 合并为完整 token

示例:

python 复制代码
text_zh = "Transformer通过自注意力机制建模上下文关系"
tokens_zh = tokenizer.encode(text_zh)
# token 数量:约 20 个
# 字符数:19
# 压缩比:19/20 ≈ 0.95 字符/token

text_en = "Transformer models context relationships through self-attention"
tokens_en = tokenizer.encode(text_en)
# token 数量:约 8 个
# 字符数:64(含空格)
# 压缩比:64/8 = 8 字符/token

2.4.3 Tokenizer 代码结构

MiniMind 的 Tokenizer 相关代码通常位于项目中的 tokenizer 模块:

plain 复制代码
tokenizer/
├── minimind_tokenizer.py    # Tokenizer 核心实现
├── vocab.json               # 词表文件(token → id 映射)
├── merges.txt               # BPE 合并规则文件
└── special_tokens.json      # 特殊 Token 定义

2.5 Tokenizer 实践实验

2.5.1 基础编码/解码

python 复制代码
from tokenizer.minimind_tokenizer import MiniMindTokenizer

# 初始化 Tokenizer
tokenizer = MiniMindTokenizer()

# 编码:文本 → 整数序列
text = "大语言模型是人工智能的重要发展方向"
token_ids = tokenizer.encode(text)
print(f"原始文本: {text}")
print(f"Token IDs: {token_ids}")
print(f"Token 数量: {len(token_ids)}")

# 解码:整数序列 → 文本
decoded_text = tokenizer.decode(token_ids)
print(f"解码文本: {decoded_text}")
print(f"编解码一致性: {text == decoded_text}")

2.5.2 查看分词结果

python 复制代码
# 查看文本被切分为哪些 token
text = "深度学习DeepLearning"
tokens = tokenizer.tokenize(text)  # 获取 token 字符串
print(f"分词结果: {tokens}")
# 预期输出类似: ['深', '度', '学', '习', 'Deep', 'Learning']

# 查看 token 对应的 ID
for token in tokens:
    token_id = tokenizer.token_to_id(token)
    print(f"  '{token}' -> {token_id}")

2.5.3 特殊 Token 实验

python 复制代码
# 查看所有特殊 Token
special_tokens = tokenizer.special_tokens
print(f"特殊 Token: {special_tokens}")

# 构造对话格式的输入
user_text = "你好,请介绍一下自己"
assistant_text = "我是MiniMind,一个小型语言模型"

# 模拟对话编码
conversation_ids = tokenizer.encode_conversation([
    {"role": "user", "content": user_text},
    {"role": "assistant", "content": assistant_text}
])
print(f"对话编码 IDs: {conversation_ids}")
print(f"Token 数量: {len(conversation_ids)}")

2.5.4 压缩比统计实验

python 复制代码
import json

test_texts = {
    "中文短句": "你好世界",
    "中文长句": "Transformer通过自注意力机制建模上下文关系,是自然语言处理的核心架构",
    "英文短句": "Hello World",
    "英文长句": "Transformer models context relationships through self-attention mechanism",
    "代码片段": "def quick_sort(arr):\n    if len(arr) <= 1: return arr",
    "中英混合": "使用PyTorch框架实现Transformer模型的训练",
    "数学公式": "交叉熵损失函数 H(p,q) = -Σ p(x) log q(x)",
}

print(f"{'文本类型':<10} {'字符数':<8} {'Token数':<8} {'压缩比':<10}")
print("-" * 40)

for name, text in test_texts.items():
    char_count = len(text)
    token_count = len(tokenizer.encode(text))
    ratio = char_count / token_count
    print(f"{name:<10} {char_count:<8} {token_count:<8} {ratio:<10.2f}")

2.5.5 词表分析实验

python 复制代码
# 加载词表
vocab = tokenizer.get_vocab()  # 返回 {token: id} 字典

# 基本统计
total_tokens = len(vocab)
print(f"词表总大小: {total_tokens}")

# 分析 token 长度分布
from collections import Counter
length_dist = Counter(len(token) for token in vocab.keys())
print(f"\nToken 长度分布:")
for length in sorted(length_dist.keys()):
    count = length_dist[length]
    pct = count / total_tokens * 100
    print(f"  长度 {length}: {count} ({pct:.1f}%)")

# 查找最长的 token
longest_tokens = sorted(vocab.keys(), key=len, reverse=True)[:10]
print(f"\n最长的 10 个 token:")
for token in longest_tokens:
    print(f"  '{token}' (id={vocab[token]})")

# 查看特殊 Token
print(f"\n特殊 Token:")
for token, token_id in sorted(vocab.items()):
    if token.startswith('<') or token in ['__']:
        print(f"  '{token}' -> {token_id}")

2.6 Tokenizer 与训练的关系

2.6.1 Tokenizer 对模型的影响

影响维度 大词表(32K+) 小词表(6400)
序列长度 较短 较长(同样文本需要更多 token)
Embedding 层参数 多(32K × dim) 少(6400 × dim)
训练数据效率 高(每个 token 信息密度高)
覆盖语言能力 广(多语言) 窄(主要中文)
推理速度 快(序列短) 慢(序列长)

2.6.2 MiniMind 词表对 Embedding 参数的影响

plain 复制代码
Embedding 参数量 = vocab_size × dim
                 = 6400 × 768
                 = 4,915,200
                 ≈ 4.9M

占总参数量的比例:
  4.9M / 64M ≈ 7.7%

教学思考:如果词表从 6400 扩大到 32000,Embedding 层参数将从 4.9M 增加到 24.6M,增加约 19.7M 参数。这说明词表大小对模型总参数量有显著影响。


实践任务

任务 2.1:BPE 算法手写实现(必做)

根据 2.2.4 的伪代码,用 Python 从零实现一个简化版 BPE 训练器:

python 复制代码
# 要求:
# 1. 输入:一个文本列表(如 ["low", "lower", "newest", "widest"])
# 2. 输出:训练后的词表和合并规则
# 3. 支持对新文本进行编码
# 4. 打印每一步合并的结果

# 提示步骤:
# - 将每个词拆分为字符 + 结尾标记
# - 统计字符对频率
# - 循环合并最高频对
# - 输出最终词表

任务 2.2:MiniMind Tokenizer 实验(必做)

完成 2.5 节中的所有实验,记录并分析结果:

  1. 中英文不同文本的编码/解码结果
  2. 不同类型文本的压缩比统计
  3. 词表长度分布分析
  4. 特殊 Token 的使用方式

思考题

  • 为什么中文的压缩比(字符/token)远低于英文?
  • MiniMind 6400 的词表大小对中文处理有什么影响?会带来什么问题?

任务 2.3:词表覆盖率分析(选做)

python 复制代码
# 使用 MiniMind 的预训练数据集,统计:
# 1. 数据集中出现的所有 token 的覆盖率
# 2. 是否有大量 OOV(未登录词)
# 3. 词表中哪些 token 的使用频率最高
# 4. 是否存在"死 token"(从未被使用的 token)

# 提示:读取 pretrain_t2t_mini.jsonl,逐行编码,统计频率

挑战任务(选做)

  • 对比 MiniMind Tokenizer 与 LLaMA Tokenizer 对同一段中文文本的编码结果差异
  • 尝试用 SentencePiece 训练一个 32000 词表的 Tokenizer,对比效果

知识拓展

BPE vs WordPiece vs SentencePiece

算法 使用者 核心区别
BPE GPT 系列、MiniMind 基于频率贪心合并相邻对
WordPiece BERT 系列 基于语言模型似然度合并(而非频率)
SentencePiece LLaMA、T5 将文本视为 Unicode 字节序列,直接训练(无需预分词)

主流 LLM 的 Tokenizer 对比

模型 词表大小 编码方式 中文效率
GPT-4 ~100K Byte-Level BPE (tiktoken) 1.5 字符/token
LLaMA 2 32K SentencePiece (BPE) 1.5 字符/token
Qwen2 151K tiktoken (BPE) 1.5 字符/token
MiniMind 6400 Byte-Level BPE 1.5-1.7 字符/token

下一阶段预告

在阶段 3 中,我们将进入 MiniMind 的核心------Transformer 架构。从 RMSNorm 到 RoPE,从 GQA 到 SwiGLU,逐组件拆解 Transformer Decoder-Only 模型的每个细节,并尝试从零手写一个简化版的 Transformer Block。

相关推荐
明志数科3 小时前
具身智能全栈模型开源:从模型架构演进看数据需求的范式转移
人工智能·机器人
中微极客3 小时前
RAG评估工具2026横向测评:从手工到自动化
人工智能·自动化
三品吉他手会点灯3 小时前
嵌入式机器学习 - 学习笔记1.2.3 - 机器学习软件框架
人工智能·笔记·嵌入式硬件·学习·机器学习
LaughingZhu4 小时前
Product Hunt 每日热榜 | 2026-07-15
前端·人工智能·神经网络·react.js·搜索引擎·前端框架
爱吃提升4 小时前
Python自动驾驶图像识别完整实战教程(OpenCV+YOLOv8,附可直接运行源码)
python·opencv·自动驾驶
TMT星球4 小时前
浦东新区与阅文集团携手,人工智能文创产业基地揭牌启用
人工智能·百度
repokey-Yao4 小时前
AI 如何看懂 FPGA 大工程?RepoKey Starter 使用记录
人工智能·chatgpt·fpga
意图共鸣4 小时前
《智能体三角模型白皮书》发布:意图共鸣科技如何定义私有知识域?
人工智能
潜龙95274 小时前
AI Agent评测体系实施规划
人工智能·ai agent评测实施
2601_956743684 小时前
上海AI Agent智能体开发选型:从工程架构看落地路径解析
大数据·人工智能·ai·架构·开发经验·上海