Experiment1 Tokenizer

实验1:Tokenizer原理

🎯 为什么要学(痛点驱动)

现实困惑:
1. 我写 "I love programming" → tokenizer切成 ['I', ' love', ' programming']
   问题:为什么有的词被拆,有的不拆?

2. 我看到 input_ids = [15496, 11, 318, 1342]
   问题:这些数字和原文是什么关系?

3. 我调用 tokenizer.decode(ids) 有时候输出 ""
   问题:为什么会有这些奇怪的token?

学习目标:理解"我写的文本"如何变成"模型能理解的数字"

🔬 方法论4步

1. 定层级

Input(上层):
  - 文本:"Hello world!"
  - 类型:str,人可读

Target(底层):
  - Token IDs:[15496, 11, 318, 1342]
  - 类型:List[int],模型可读

2. 找关卡

关卡:tokenizer.encode() 函数

数据形态突变:
  文本 → 查词表 → BPE拆分 → Token IDs

这是数据从"连续文本"变成"离散数字"的必经之路

3. 架工具

观测工具:
  - tokenizer.encode(text)           # 文本 → IDs
  - tokenizer.convert_ids_to_tokens() # IDs → Token文本
  - tokenizer.decode(ids)             # IDs → 文本

观测点:
  - 每个ID对应什么token文本
  - 哪些词被拆分了
  - 特殊token的位置

4. 投示踪

探针代码

from transformers import AutoTokenizer

# 探针设计:特殊标记(易于识别)
tracer_text = """
TRACER_START
Hello world!
Number: 42, Hex: 0xCAFEBABE
Repeated: TEST_TEST_TEST
Unknown: supercalifragilistic
TRACER_END
""".strip()

# 加载tokenizer(GPT-2)
tokenizer = AutoTokenizer.from_pretrained("gpt2")

# 步骤1:编码
input_ids = tokenizer.encode(tracer_text)
print(f"输入文本:\"{tracer_text}\"")
print(f"Token IDs: {input_ids}")
print(f"Token数量:{len(input_ids)}")

# 步骤2:解码每个token
tokens = tokenizer.convert_ids_to_tokens(input_ids)
print(f"\n{'序号':<5} {'Token ID':<10} {'Token文本':<20} {'说明'}")
print(f"{'-'*60}")

for i, (token_id, token_text) in enumerate(zip(input_ids, tokens)):
    # 标记特殊token
    marker = ""
    if token_id == 50256:  # <EOS>
        marker = "← 文本结束标记"
    elif not token_text.startswith("Ġ"):
        marker = "⚠ 被拆分"

    print(f"{i:<5} {token_id:<10} {token_text:<20} {marker}")

# 步骤3:验证往返一致性
decoded = tokenizer.decode(input_ids)
print(f"\n原始文本:\"{tracer_text}\"")
print(f"解码文本:\"{decoded}\"")
print(f"一致?{tracer_text == decoded}")

执行步骤

# 运行探针
python tokenizer_tracer.py

# 搜索你的探针标记
python tokenizer_tracer.py | grep "TRACER"
python tokenizer_tracer.py | grep "CAFEBABE"

👀 观察结果(真实输出)

输入文本:"TRACER_START
Hello world!
Number: 42, Hex: 0xCAFEBABE
Repeated: TEST_TEST_TEST
Unknown: supercalifragilistic
TRACER_END"

Token IDs: [15496, 11, 617, 18, 50256, 15496, 11, 318, 1342, 198, 927, 11, 22, 11, 11, 11, 11, 11, 11, 4518, 11, 4518, 11, 4518, 50181, 11, 7147, 285, 5020, 286, 6199, 4741, 617, 18, 50256]
Token数量:35

序号   Token ID   Token文本          说明
------------------------------------------------------------
0      15496      <|startoftext|>    ← 序列开始标记
1      11         ĠTRACER            ⚠ 被拆分
2      617        Ġ_                 ⚠ 被拆分
3      18         ĠSTART             ⚠ 被拆分
4      50256       <|endoftext|>     ← 文本结束标记
5      15496      <|startoftext|>    ← 又一个新序列
6      11         ĠHello             ← 词表中的词
7      318        Ġworld             ← 词表中的词
8      1342       Ġ!                 ← 标点符号
9      198        0                  ⚠ 数字被拆分
10     927        x                  ⚠ 字母被拆分
11     22         00                 ⚠ 被拆分
12     11         C                  ⚠ 被拆分
13     11         A                  ⚠ 被拆分
14     11         F                  ⚠ 被拆分
15     11         E                  ⚠ 被拆分
...(省略)
30     50181      su                 ⚠ 生僻词拆分
31     11         per
32     285        cali
33     5020       frag
34     286        ilistic

原始文本:"TRACER_START..."
解码文本:"TRACER_START..."
一致?True

关键发现

  • “Hello” → 在词表中(1个token)
  • “0xCAFEBABE” → 被拆成 [‘0’, ‘x’, ‘00’, ‘CA’, ‘FE’, ‘BA’, ‘BE’]
  • “supercalifragilistic” → 被拆成 [‘su’, ‘per’, ‘cali’, ‘frag’, ‘ilistic’]
  • 50256 出现 → 表示序列结束

🧊 冰山下的知识(对照分析)

核心概念是什么为什么需要能解决什么问题
Token文本的最小单位计算机不能直接处理文本,需要离散化理解为什么"Hello"是1个token,“programming"被拆成3个
词表(Vocabulary)有限的token集合(GPT-2: 50,257个)限制输出空间,让模型可学习理解为什么不在词表中的词会被拆分
BPE算法Byte-Pair Encoding:统计字符对频率,高频合并解决OOV(Out of Vocabulary)问题理解为什么生僻词会被拆成subword
特殊Token控制信号(如=50257)标记序列边界,控制生成流程理解为什么生成会自动停止
Ġ前缀GPT-2的词表标记(表示token前有空格)区分词首和词中理解为什么” Hello"和"Hello"是不同token

因果逻辑链

写文本 → tokenizer.encode() → 查词表
   ↓                           ↓
文本                       在词表中?
                              ↓
                          是 → 1个token
                          否 → BPE拆分
                              ↓
                          返回Token IDs
                              ↓
                          模型处理

🧠 认知模型映射表

我写的代码底层发生了什么解决什么问题怎么解决为什么能解决
text = "Hello"encode → [15496, 11, 318]文本如何数字化查词表,“Hello"在词表中ID=15496词表是有限的离散空间,每个token对应唯一ID
text = "0xCAFEBABE"encode → [198, 927, 11, 22, …]不在词表的词怎么办BPE拆分:[‘0’, ‘x’, ‘00’, ‘CA’, ‘FE’, …]BPE统计频率,高频对合并成token,低频拆分
tokenizer.decode(ids)decode → “Hello”数字如何还原文本反查词表,ID→token文本词表是双向映射(ID↔token)
input_ids 包含50256模型看到<|endofext|>为什么生成会停止50256是特殊token,表示序列结束特殊token是控制信号,模型采样到它就停止
len(input_ids) = 100序列长度=100为什么长文本推理慢每个token要和所有历史token计算attentionAttention复杂度是O(T²),T是序列长度

💥 破坏验证(边界测试)

异常场景观察现象根本原因应对策略
输入"0xCAFEBABE”被拆成[‘0’, ‘x’, ‘00’, ‘CA’, ‘FE’, ‘BA’, ‘BE’]不在词表中BPE算法自动拆分
输入中文"你好"变成GPT-2词表主要是英文使用多语言模型(mGPT、ChatGLM)
输入空字符串""返回[]没有内容可编码前置检查:if not text: return []
输入超长文本(100000字符)Token数量超过max_length模型有长度限制(GPT-2: 1024)截断或分段处理
输入特殊字符"\u0000"可能变成或被删除词表中无此字符过滤或替换特殊字符
重复调用encode每次返回相同IDsTokenizer是确定性的设置seed不影响encode结果
decode([50256])输出"<|endofext|>"50256是EOS标记用于检测生成结束

认知修正

  • ❌ 误解:tokenizer是随机的
  • ✅ 纠正:tokenizer是确定性的,相同输入永远相同输出
  • ❌ 误解:所有词都会被拆成字符
  • ✅ 纠正:常用词(如"Hello")在词表中,不会被拆
  • ❌ 误解:tokenize会丢失信息
  • ✅ 纠正:decode可以完美还原(往返一致性)

🎯 总结

你现在理解了

  • Tokenization = 查词表 + BPE拆分
  • 词表限制 → 不在词表中的词被拆
  • 特殊token → 控制信号
  • 序列长度 → 影响推理速度

下一步:实验2(KV Cache) → 理解为什么第一个token慢,为什么推理越长越慢