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 就是连接两者的"翻译器":

原始文本                        模型输入
"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 算法步骤(训练阶段)

以一个简单语料为例:

语料:"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, s3
t,2
s, t2
l, o2
o, w2
w,1

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

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

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>

low</w>  low e r </w>  newest</w>  w i d est</w>

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

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

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

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

输入:"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 算法伪代码

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 工作流程

原始文本:"你好,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
思考 Tokenminimumsorrytool_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

示例:

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 模块:

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

2.5 Tokenizer 实践实验

2.5.1 基础编码/解码

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 查看分词结果

# 查看文本被切分为哪些 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 实验

# 查看所有特殊 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 压缩比统计实验

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 词表分析实验

# 加载词表
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 参数的影响

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 训练器:

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

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

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

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

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

思考题

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

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

# 使用 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

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

主流 LLM 的 Tokenizer 对比

模型词表大小编码方式中文效率
GPT-4~100KByte-Level BPE (tiktoken)1.5 字符/token
LLaMA 232KSentencePiece (BPE)1.5 字符/token
Qwen2151Ktiktoken (BPE)1.5 字符/token
MiniMind6400Byte-Level BPE1.5-1.7 字符/token

下一阶段预告

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值