搜 索

Tokenization:大模型的碎碎念

  • 3阅读
  • 2025年03月01日
  • 0评论
首页 / AI/大数据 / 正文

前言:大模型的"第一步"

你有没有想过,当你输入 "今天天气真好" 给 ChatGPT 时,它看到的是什么?

不是汉字,不是字母,而是一串数字:

"今天天气真好" → [36661, 34208, 37955, 30008]

这个把文本变成数字的过程,就是 Tokenization(分词/标记化)

看起来很简单对吧?其实这里面的学问大了去了:

  • 为什么 "今天" 是一个 token,而 "天气" 也是一个 token?
  • 为什么 GPT-4 算 token 数和 Claude 不一样?
  • 为什么大模型数学不好?(剧透:和 Tokenization 有关)
  • 为什么同样一句话,中文要用更多 token?

这篇文章,我们来彻底搞懂 Tokenization。

flowchart LR A[原始文本] --> B[Tokenizer] B --> C[Token 序列] C --> D[Token ID 序列] D --> E[Embedding] E --> F[输入模型] subgraph 示例 A1["今天天气真好"] --> B1["[今天, 天气, 真, 好]"] B1 --> C1["[36661, 34208, 37955, 30008]"] end

一、为什么需要 Tokenization?

1.1 模型只认识数字

神经网络的输入必须是数字(向量)。文本是离散的符号,必须转换成数字才能处理。

文本: "Hello"
     ↓ Tokenization
Token IDs: [15496]
     ↓ Embedding
向量: [0.023, -0.156, 0.892, ...]  # 768 维
     ↓
输入到 Transformer

1.2 词表大小的权衡

最直观的方案:每个词一个 ID

vocab = {"今天": 0, "天气": 1, "真好": 2, "我": 3, "爱": 4, "学习": 5, ...}

问题来了:词表会有多大?

  • 英语单词:约 17 万(Webster 词典)
  • 中文词语:约 50 万+(现代汉语词典 + 网络新词)
  • 加上各种专业术语、人名、地名...

词表太大会导致

  1. Embedding 层巨大:vocab_size × embed_dim,50 万词 × 768 维 = 3 亿参数(光 Embedding 层就这么大)
  2. 稀疏性问题:很多词出现频率极低,学不好
  3. OOV 问题:新词、拼写错误、专业术语 → 无法处理

1.3 OOV:未登录词问题

# 假设词表里没有 "ChatGPT"
text = "ChatGPT 很强大"
tokens = tokenize(text)  # → ["[UNK]", "很", "强大"]

所有不在词表里的词都变成 [UNK](Unknown),信息直接丢失了。

这在以前的 NLP 模型中是个大问题。

1.4 另一个极端:字符级别

那每个字符一个 ID 呢?

text = "Hello"
tokens = ["H", "e", "l", "l", "o"]  # 5 个 token

优点

  • 词表很小(英语只需要 ~100 个字符)
  • 没有 OOV 问题

缺点

  • 序列太长:一个词要拆成好多字符
  • 语义稀释:单个字符几乎没有语义
# 字符级别的问题
"Tokenization" → ["T", "o", "k", "e", "n", "i", "z", "a", "t", "i", "o", "n"]
# 12 个 token,模型需要从这些字符中"拼凑"出词的含义

1.5 Subword:最佳平衡

现代的解决方案:Subword(子词)分词

核心思想:

  • 高频词:保持完整(如 "the", "今天")
  • 低频词:拆成更小的单元(如 "unhappiness" → "un" + "happiness")
# Subword 分词示例
"Tokenization" → ["Token", "ization"]     # 2 个 token
"unhappiness" → ["un", "happi", "ness"]   # 3 个 token
"今天天气" → ["今天", "天气"]              # 2 个 token
"深度求索" → ["深度", "求", "索"]          # 3 个 token(如果"求索"不在词表)

优点

  • 词表大小可控(通常 32K - 128K)
  • 没有 OOV(任何词都可以拆成子词)
  • 保留了一定的语义信息

这就是为什么叫 Token 而不是 Word——一个 Token 可能是一个词、一个子词、甚至一个字符。


二、BPE:字节对编码

BPE(Byte Pair Encoding) 是最流行的 Subword 分词算法,GPT 系列就用它。

2.1 算法原理

BPE 的思想很简单:不断合并出现频率最高的相邻字符/子词对

初始词表: 所有单字符
重复以下步骤:
    1. 统计所有相邻 token 对的出现频率
    2. 找出频率最高的 token 对
    3. 合并这个 token 对,加入词表
    4. 直到达到目标词表大小

2.2 BPE 训练过程示例

假设我们的训练语料是:

low low low low low
lower lower
newest newest newest newest newest newest
widest widest widest

Step 0: 初始化

把每个词拆成字符,加上词尾标记 </w>

l o w </w>      : 5 次
l o w e r </w>  : 2 次
n e w e s t </w>: 6 次
w i d e s t </w>: 3 次

初始词表:{l, o, w, e, r, n, s, t, i, d, </w>}

Step 1: 第一次合并

统计相邻对频率:

(e, s): 6+3 = 9 次  ← 最高!
(s, t): 6+3 = 9 次
(l, o): 5+2 = 7 次
(o, w): 5+2 = 7 次
...

合并 (e, s)es

l o w </w>       : 5 次
l o w e r </w>   : 2 次
n e w es t </w>  : 6 次  ← e+s 变成 es
w i d es t </w>  : 3 次

词表:{l, o, w, e, r, n, s, t, i, d, </w>, es}

Step 2: 第二次合并

统计:

(es, t): 6+3 = 9 次  ← 最高!
(l, o): 7 次
...

合并 (es, t)est

l o w </w>        : 5 次
l o w e r </w>    : 2 次
n e w est </w>    : 6 次
w i d est </w>    : 3 次

词表:{..., es, est}

Step 3, 4, ...:继续合并

合并 (est, </w>) → est</w>
合并 (l, o) → lo
合并 (lo, w) → low
合并 (low, </w>) → low</w>
合并 (n, e) → ne
合并 (ne, w) → new
合并 (new, est</w>) → newest</w>
...

最终,高频词如 "low"、"newest" 会变成单个 token,低频的部分保持拆分。

2.3 BPE 编码过程

训练好词表后,如何对新文本编码?

def bpe_encode(text, merges):
    """
    BPE 编码
    merges: 合并规则列表,按训练时的顺序
    """
    # 初始化:拆成字符
    tokens = list(text) + ['</w>']
    
    # 按顺序应用合并规则
    for (a, b), merged in merges:
        i = 0
        while i < len(tokens) - 1:
            if tokens[i] == a and tokens[i+1] == b:
                tokens = tokens[:i] + [merged] + tokens[i+2:]
            else:
                i += 1
    
    return tokens

# 示例
merges = [
    (('e', 's'), 'es'),
    (('es', 't'), 'est'),
    (('l', 'o'), 'lo'),
    (('lo', 'w'), 'low'),
    # ...
]

print(bpe_encode("lowest", merges))
# → ['low', 'est', '</w>'] 或类似

2.4 手撕 BPE 训练

from collections import defaultdict
import re

def get_stats(vocab):
    """统计相邻 token 对的频率"""
    pairs = defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols) - 1):
            pairs[symbols[i], symbols[i+1]] += freq
    return pairs


def merge_vocab(pair, vocab):
    """合并词表中的 token 对"""
    new_vocab = {}
    bigram = ' '.join(pair)
    replacement = ''.join(pair)
    
    for word, freq in vocab.items():
        new_word = word.replace(bigram, replacement)
        new_vocab[new_word] = freq
    
    return new_vocab


def train_bpe(corpus, num_merges):
    """训练 BPE"""
    # 初始化词表:每个词拆成字符
    vocab = defaultdict(int)
    for word in corpus:
        # 加空格分隔字符,加词尾标记
        word_with_end = ' '.join(list(word)) + ' </w>'
        vocab[word_with_end] += 1
    
    merges = []
    
    for i in range(num_merges):
        pairs = get_stats(vocab)
        if not pairs:
            break
        
        # 找最高频的 pair
        best_pair = max(pairs, key=pairs.get)
        
        # 合并
        vocab = merge_vocab(best_pair, vocab)
        merges.append(best_pair)
        
        print(f"Merge {i+1}: {best_pair} (freq: {pairs[best_pair]})")
    
    return vocab, merges


# 训练示例
corpus = ['low'] * 5 + ['lower'] * 2 + ['newest'] * 6 + ['widest'] * 3

vocab, merges = train_bpe(corpus, num_merges=10)
print("\n最终词表:")
for word, freq in sorted(vocab.items(), key=lambda x: -x[1]):
    print(f"  {word}: {freq}")

输出:

Merge 1: ('e', 's') (freq: 9)
Merge 2: ('es', 't') (freq: 9)
Merge 3: ('est', '</w>') (freq: 9)
Merge 4: ('l', 'o') (freq: 7)
Merge 5: ('lo', 'w') (freq: 7)
Merge 6: ('w', 'e') (freq: 6)
Merge 7: ('n', 'e') (freq: 6)
Merge 8: ('ne', 'we') (freq: 6)
Merge 9: ('newe', 'st</w>') (freq: 6)
Merge 10: ('low', '</w>') (freq: 5)

最终词表:
  newest</w>: 6
  low</w>: 5
  w i d est</w>: 3
  low e r </w>: 2

2.5 Byte-level BPE

GPT-2 开始使用 Byte-level BPE:不是在字符级别,而是在字节级别做 BPE。

# 字符级 BPE
text = "Hello 你好"
chars = ['H', 'e', 'l', 'l', 'o', ' ', '你', '好']  # 需要巨大的字符表支持中文

# 字节级 BPE
text = "Hello 你好"
bytes = [72, 101, 108, 108, 111, 32, 228, 189, 160, 229, 165, 189]  # UTF-8 字节
# 基础词表只需要 256 个(0-255)

优点

  • 基础词表固定 256 个(所有可能的字节)
  • 天然支持任何语言、任何字符(emoji 也行 🚀)
  • 不会有 OOV

缺点

  • 非 ASCII 字符会被拆成多个字节 token
  • 中文每个字可能变成 2-3 个 token

三、WordPiece:BERT 的选择

3.1 与 BPE 的区别

WordPiece(由 Google 提出)和 BPE 很像,但合并策略不同:

  • BPE:合并频率最高的 pair
  • WordPiece:合并使语言模型困惑度下降最多的 pair

数学上,WordPiece 选择最大化以下分数:

$$ \text{score}(a, b) = \frac{\text{freq}(ab)}{\text{freq}(a) \times \text{freq}(b)} $$

这其实就是在看:$ab$ 一起出现的频率,是否高于它们独立出现的期望?

3.2 WordPiece 的特殊标记

WordPiece 用 ## 前缀表示"这不是词的开头":

# WordPiece 分词
"Tokenization" → ["Token", "##ization"]
"unhappiness" → ["un", "##hap", "##pi", "##ness"]

这样在解码时,只需要去掉 ## 并拼接:

def decode_wordpiece(tokens):
    text = ""
    for token in tokens:
        if token.startswith("##"):
            text += token[2:]  # 去掉 ##
        else:
            text += " " + token
    return text.strip()

tokens = ["Token", "##ization", "is", "fun"]
print(decode_wordpiece(tokens))  # "Tokenization is fun"

3.3 BERT Tokenizer 使用

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

text = "Tokenization is the first step of NLP."
tokens = tokenizer.tokenize(text)
print(f"Tokens: {tokens}")
# ['token', '##ization', 'is', 'the', 'first', 'step', 'of', 'nl', '##p', '.']

# 转换为 ID
input_ids = tokenizer.encode(text)
print(f"IDs: {input_ids}")
# [101, 19204, 3989, 2003, 1996, 2034, 3357, 1997, 17953, 2361, 1012, 102]
# 101 = [CLS], 102 = [SEP]

# 解码回文本
decoded = tokenizer.decode(input_ids)
print(f"Decoded: {decoded}")
# [CLS] tokenization is the first step of nlp. [SEP]

四、SentencePiece:语言无关的分词

4.1 为什么需要 SentencePiece?

BPE 和 WordPiece 都有一个假设:文本已经被预分词(按空格切开)

这对英语没问题,但对中文、日语等没有空格的语言就麻烦了:

英语: "I love NLP" → ["I", "love", "NLP"] → BPE
中文: "我爱自然语言处理" → ??? → BPE

SentencePiece 的解决方案:直接在原始文本上训练,不需要预分词

4.2 SentencePiece 的特点

  1. 把空格当作普通字符:用特殊符号 (U+2581)表示空格
# SentencePiece
"I love NLP" → ["▁I", "▁love", "▁NL", "P"]
# ▁ 表示"这个 token 前面有空格"
  1. 语言无关:不依赖任何语言特定的预处理
  2. 两种算法

    • BPE 模式(和 Byte-level BPE 类似)
    • Unigram 模式(概率模型,下面介绍)

4.3 Unigram Language Model

SentencePiece 还支持 Unigram LM 算法,这是一种概率方法。

核心思想:

  1. 从一个很大的初始词表开始
  2. 计算每个 subword 的概率(出现频率)
  3. 去掉那些"贡献小"的 subword
  4. 重复直到词表达到目标大小

"贡献小"的定义:去掉这个 subword 后,语料的总概率下降最小。

# Unigram 的分词:找概率最大的切分方式
text = "unaffable"

# 所有可能的切分
option1: ["un", "aff", "able"]  → P = P(un) × P(aff) × P(able)
option2: ["una", "ff", "able"]  → P = P(una) × P(ff) × P(able)
option3: ["unaffable"]          → P = P(unaffable)
...

# 选择概率最大的切分

4.4 SentencePiece 实战

import sentencepiece as spm

# 训练
spm.SentencePieceTrainer.train(
    input='corpus.txt',           # 训练语料
    model_prefix='my_tokenizer',  # 输出文件前缀
    vocab_size=32000,             # 词表大小
    model_type='bpe',             # 'bpe' 或 'unigram'
    character_coverage=0.9995,    # 字符覆盖率
    pad_id=0,                     # padding token id
    unk_id=1,                     # unknown token id
    bos_id=2,                     # begin of sentence id
    eos_id=3,                     # end of sentence id
)

# 加载
sp = spm.SentencePieceProcessor()
sp.load('my_tokenizer.model')

# 编码
text = "今天天气真好"
tokens = sp.encode_as_pieces(text)
ids = sp.encode_as_ids(text)
print(f"Tokens: {tokens}")  # ['▁今天', '天气', '真', '好']
print(f"IDs: {ids}")        # [1234, 5678, 9012, 3456]

# 解码
decoded = sp.decode_pieces(tokens)
print(f"Decoded: {decoded}")  # 今天天气真好

4.5 主流大模型的 Tokenizer 选择

模型Tokenizer词表大小特点
GPT-2Byte-level BPE50,257基于字节
GPT-3/4tiktoken (BPE)100,277cl100k_base
BERTWordPiece30,522##前缀
LLaMASentencePiece (BPE)32,000语言无关
LLaMA 2SentencePiece (BPE)32,000同上
Qwen自研 (BPE)151,936大词表,中文友好
ClaudeBPE 变体~100K具体未公开
DeepSeekSentencePiece100,000中英平衡

五、中文分词的特殊挑战

5.1 没有天然分隔符

英语有空格,中文没有:

英语: "I love machine learning"
     → 自然切分: ["I", "love", "machine", "learning"]

中文: "我爱机器学习"
     → 怎么切?["我", "爱", "机器", "学习"]?
                ["我", "爱", "机器学习"]?
                ["我爱", "机器", "学习"]?

5.2 字 vs 词 vs 子词

中文有三种粒度:

# 字粒度
"自然语言处理" → ["自", "然", "语", "言", "处", "理"]  # 6 个 token

# 词粒度
"自然语言处理" → ["自然", "语言", "处理"]  # 3 个 token

# 子词粒度(BPE/SentencePiece)
"自然语言处理" → ["自然", "语言", "处理"]  # 3 个 token(如果这些词频足够高)
              → ["自然", "语", "言", "处理"]  # 4 个 token(如果"语言"拆开了)

5.3 Token 效率问题

由于大多数大模型的词表是英语主导的,中文的 token 效率通常较低:

from transformers import AutoTokenizer

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

# 比较 token 数量
en_text = "Machine learning is amazing."
zh_text = "机器学习非常神奇。"

print(f"英文: {len(gpt2_tok.encode(en_text))} tokens")  # 5 tokens
print(f"中文: {len(gpt2_tok.encode(zh_text))} tokens")  # 12 tokens

# 同样的语义,中文需要更多 token!

这导致:

  • 成本更高:API 按 token 计费,中文更贵
  • 上下文更短:同样的 token 限制,中文能输入的内容更少

5.4 各家模型的中文优化

# 对比不同模型的中文 token 效率
from transformers import AutoTokenizer

text = "大型语言模型正在改变人工智能的未来发展方向。"

tokenizers = {
    "GPT-2": "gpt2",
    "LLaMA": "meta-llama/Llama-2-7b-hf",
    "Qwen": "Qwen/Qwen-7B",
    "ChatGLM": "THUDM/chatglm3-6b",
}

for name, model_name in tokenizers.items():
    try:
        tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
        tokens = tok.encode(text)
        print(f"{name:10s}: {len(tokens):3d} tokens | {tok.convert_ids_to_tokens(tokens)[:5]}...")
    except:
        print(f"{name:10s}: 无法加载")

# 大概结果:
# GPT-2     : 25+ tokens(对中文很不友好)
# LLaMA     : 15+ tokens
# Qwen      : 12  tokens(专门优化了中文)
# ChatGLM   : 12  tokens(专门优化了中文)

5.5 为什么 Qwen 中文效果好?

Qwen(通义千问)的一个重要优化就是 词表设计

  1. 大词表:151,936 个 token(GPT-2 只有 50,257)
  2. 中文优先:词表中包含大量中文词语
  3. 更多完整中文词:减少拆分
# Qwen 的分词效果
text = "大型语言模型"

# GPT-2: ["大", "型", "语", "言", "模", "型"]  # 拆成单字
# Qwen:  ["大型", "语言", "模型"]             # 保持完整词

六、Tokenization 的坑

6.1 数字处理问题

这是个著名的问题:大模型数学不好,Tokenization 要背一半锅

from transformers import GPT2Tokenizer

tok = GPT2Tokenizer.from_pretrained("gpt2")

# 数字的分词
print(tok.tokenize("123456789"))
# → ['123', '456', '789']  ← 被随机切开了!

print(tok.tokenize("1000"))
# → ['1000']  ← 这个是完整的

print(tok.tokenize("1001"))
# → ['100', '1']  ← 又切开了

print(tok.tokenize("42"))
# → ['42']  ← 完整

print(tok.tokenize("43"))
# → ['43']  ← 完整

print(tok.tokenize("8723"))
# → ['87', '23']  ← 切开了

问题:

  • 同一个数字,切法不一致
  • 模型很难学习到数字的"值"
  • 加减乘除变得困难(不同的数字有不同的 token 表示)

解决方案

  • 一些模型会把每个数字单独作为 token
  • 或者用特殊的数字编码方式

6.2 空格和特殊字符

# 空格的处理
tok.tokenize("Hello World")   # → ['Hello', 'ĠWorld']
tok.tokenize("Hello  World")  # → ['Hello', 'ĠĠWorld']  # 两个空格

# Ġ 是 GPT-2 用来表示"前面有空格"的特殊字符

这会导致:

  • "Hello World" 和 "HelloWorld" 的 token 不同
  • 多个空格会产生奇怪的 token
  • 代码缩进可能被奇怪地处理

6.3 大小写问题

# BERT 的 uncased 模型
bert_tok = BertTokenizer.from_pretrained("bert-base-uncased")
print(bert_tok.tokenize("Hello WORLD"))
# → ['hello', 'world']  # 全变小写了!

# BERT 的 cased 模型
bert_tok = BertTokenizer.from_pretrained("bert-base-cased")
print(bert_tok.tokenize("Hello WORLD"))
# → ['Hello', 'WORLD']  # 保留大小写

对于需要区分大小写的任务(如命名实体识别),要用 cased 模型。

6.4 编程语言的处理

代码有自己的特殊性:

# Python 代码的分词
code = "def hello_world():\n    print('Hello')"

tok.tokenize(code)
# 可能的结果:
# ['def', 'Ġhello', '_', 'world', '():', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġprint', "('", 'Hello', "')"]
# Ċ 表示换行符
# 缩进被切成多个空格 token

代码专用模型(如 CodeLlama, StarCoder)通常会:

  • 在代码上训练 tokenizer
  • 特殊处理缩进(把 4 个空格作为一个 token)
  • 更好地处理标识符命名

6.5 多语言混合

# 中英混合
text = "我正在学习Machine Learning"

gpt2_tok.tokenize(text)
# 中文部分会被切成很多小块
# 英文部分相对正常

七、tiktoken:OpenAI 的高效实现

OpenAI 开源了他们的 tokenizer 库:tiktoken

7.1 基本使用

import tiktoken

# GPT-4 使用的 tokenizer
enc = tiktoken.encoding_for_model("gpt-4")

# 或者直接指定 encoding
enc = tiktoken.get_encoding("cl100k_base")

# 编码
text = "Hello, world! 你好,世界!"
tokens = enc.encode(text)
print(f"Tokens: {tokens}")
print(f"Token count: {len(tokens)}")

# 解码
decoded = enc.decode(tokens)
print(f"Decoded: {decoded}")

# 查看每个 token
for token_id in tokens:
    token_bytes = enc.decode_single_token_bytes(token_id)
    print(f"{token_id:6d} → {token_bytes}")

7.2 不同模型的 Encoding

import tiktoken

# 不同模型使用的 encoding
encodings = {
    "gpt-3.5-turbo": "cl100k_base",
    "gpt-4": "cl100k_base",
    "gpt-4o": "o200k_base",  # 更新的 encoding
    "text-embedding-ada-002": "cl100k_base",
    "gpt-2": "gpt2",
    "davinci": "p50k_base",
}

text = "Machine learning is transforming the world. 机器学习正在改变世界。"

for model, encoding_name in encodings.items():
    enc = tiktoken.get_encoding(encoding_name)
    tokens = enc.encode(text)
    print(f"{model:30s} ({encoding_name:15s}): {len(tokens):3d} tokens")

7.3 计算 Token 数量(估算 API 成本)

import tiktoken

def count_tokens(text, model="gpt-4"):
    """计算文本的 token 数量"""
    enc = tiktoken.encoding_for_model(model)
    return len(enc.encode(text))

def estimate_cost(text, model="gpt-4", is_input=True):
    """估算 API 成本"""
    tokens = count_tokens(text, model)
    
    # GPT-4 定价(2024年,可能已变化)
    prices = {
        "gpt-4": {"input": 0.03, "output": 0.06},      # per 1K tokens
        "gpt-4-turbo": {"input": 0.01, "output": 0.03},
        "gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
    }
    
    price_type = "input" if is_input else "output"
    price_per_1k = prices.get(model, {}).get(price_type, 0)
    cost = (tokens / 1000) * price_per_1k
    
    return tokens, cost

# 使用
text = open("my_document.txt").read()
tokens, cost = estimate_cost(text)
print(f"Token count: {tokens}")
print(f"Estimated cost: ${cost:.4f}")

八、实战:构建自己的 Tokenizer

8.1 使用 HuggingFace tokenizers 库

from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders

def train_custom_bpe_tokenizer(corpus_files, vocab_size=32000, output_path="my_tokenizer"):
    """训练自定义 BPE tokenizer"""
    
    # 初始化 BPE 模型
    tokenizer = Tokenizer(models.BPE(unk_token="[UNK]"))
    
    # 预处理器:按空格和标点切分
    tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
    
    # 训练器配置
    trainer = trainers.BpeTrainer(
        vocab_size=vocab_size,
        special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],
        min_frequency=2,
        show_progress=True,
    )
    
    # 训练
    tokenizer.train(files=corpus_files, trainer=trainer)
    
    # 解码器
    tokenizer.decoder = decoders.ByteLevel()
    
    # 保存
    tokenizer.save(f"{output_path}.json")
    
    return tokenizer


def test_tokenizer(tokenizer, texts):
    """测试 tokenizer"""
    for text in texts:
        output = tokenizer.encode(text)
        print(f"\n文本: {text}")
        print(f"Tokens: {output.tokens}")
        print(f"IDs: {output.ids}")
        print(f"解码: {tokenizer.decode(output.ids)}")


# 使用
if __name__ == "__main__":
    # 假设有训练语料
    corpus_files = ["corpus_zh.txt", "corpus_en.txt"]
    
    # 训练
    tokenizer = train_custom_bpe_tokenizer(corpus_files, vocab_size=50000)
    
    # 测试
    test_texts = [
        "今天天气真好",
        "Machine learning is fun",
        "我正在学习 Transformer 架构",
    ]
    test_tokenizer(tokenizer, test_texts)

8.2 词表分析工具

import json
from collections import Counter

def analyze_tokenizer(tokenizer_path):
    """分析 tokenizer 的词表"""
    
    with open(tokenizer_path, 'r', encoding='utf-8') as f:
        tokenizer_data = json.load(f)
    
    vocab = tokenizer_data.get('model', {}).get('vocab', {})
    
    print(f"词表大小: {len(vocab)}")
    
    # 分析 token 长度分布
    lengths = [len(token) for token in vocab.keys()]
    length_dist = Counter(lengths)
    
    print("\nToken 长度分布:")
    for length in sorted(length_dist.keys())[:10]:
        print(f"  长度 {length}: {length_dist[length]} 个")
    
    # 分析字符类型
    chinese_tokens = [t for t in vocab.keys() if any('\u4e00' <= c <= '\u9fff' for c in t)]
    english_tokens = [t for t in vocab.keys() if t.isascii() and t.isalpha()]
    digit_tokens = [t for t in vocab.keys() if any(c.isdigit() for c in t)]
    
    print(f"\n中文相关 token: {len(chinese_tokens)}")
    print(f"纯英文 token: {len(english_tokens)}")
    print(f"包含数字的 token: {len(digit_tokens)}")
    
    # 展示一些示例
    print("\n中文 token 示例:", chinese_tokens[:20])
    print("英文 token 示例:", english_tokens[:20])


# 使用
# analyze_tokenizer("my_tokenizer.json")

8.3 Token 效率对比工具

def compare_tokenizers(text, tokenizers_dict):
    """对比不同 tokenizer 的效率"""
    
    print(f"测试文本: {text[:100]}..." if len(text) > 100 else f"测试文本: {text}")
    print(f"文本长度: {len(text)} 字符\n")
    
    results = []
    
    for name, tokenizer in tokenizers_dict.items():
        if hasattr(tokenizer, 'encode'):
            # HuggingFace tokenizer
            tokens = tokenizer.encode(text)
            if hasattr(tokens, 'ids'):
                token_count = len(tokens.ids)
            else:
                token_count = len(tokens)
        else:
            # tiktoken
            token_count = len(tokenizer.encode(text))
        
        efficiency = len(text) / token_count  # 字符/token 比
        results.append((name, token_count, efficiency))
    
    # 排序(按 token 数量升序,数量少的效率高)
    results.sort(key=lambda x: x[1])
    
    print(f"{'Tokenizer':<20} {'Token数':<10} {'字符/Token比':<15}")
    print("-" * 45)
    for name, count, eff in results:
        print(f"{name:<20} {count:<10} {eff:.2f}")
    
    return results


# 使用示例
import tiktoken
from transformers import AutoTokenizer

tokenizers = {
    "GPT-4 (tiktoken)": tiktoken.encoding_for_model("gpt-4"),
    "GPT-4o (tiktoken)": tiktoken.get_encoding("o200k_base"),
}

# 添加 HuggingFace tokenizers
try:
    tokenizers["LLaMA-2"] = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
except:
    pass

try:
    tokenizers["Qwen"] = AutoTokenizer.from_pretrained("Qwen/Qwen-7B", trust_remote_code=True)
except:
    pass

# 测试
test_text = """
人工智能(Artificial Intelligence,简称AI)是计算机科学的一个分支,
它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。
Machine learning is a subset of AI that enables systems to learn and improve from experience.
"""

compare_tokenizers(test_text, tokenizers)

九、总结

核心概念回顾

mindmap root((Tokenization)) 分词算法 BPE 频率驱动 GPT 系列 WordPiece 似然驱动 BERT Unigram 概率模型 SentencePiece 关键挑战 OOV问题 词表大小 多语言 数字处理 实践考量 Token效率 API成本 中文优化

关键 Takeaway

  1. Tokenization 是大模型的第一步:把文本变成模型能理解的数字
  2. Subword 是现代主流:平衡词表大小和语义完整性
  3. BPE 最常用:GPT 系列、LLaMA 都用它
  4. 中文需要特别关注:词表设计决定了 token 效率
  5. 数字处理是个坑:这是大模型数学能力弱的原因之一
  6. 不同模型 tokenizer 不同:同样的文本,token 数量可能差很多

Tokenizer 选择建议

场景推荐原因
英语为主GPT 系列 tokenizer成熟稳定
中文为主Qwen / ChatGLM tokenizer中文优化
多语言SentencePiece语言无关
代码CodeLlama tokenizer代码优化
自定义HuggingFace tokenizers 库灵活可控

下一步学习

  • [ ] 预训练:如何"喂"出一个大模型
  • [ ] Embedding:从 token 到向量
  • [ ] 位置编码:让模型理解顺序

参考资料

  1. BPE 原论文 - Neural Machine Translation of Rare Words with Subword Units
  2. SentencePiece - Google 开源实现
  3. tiktoken - OpenAI 的高效 tokenizer
  4. HuggingFace tokenizers - 快速 tokenizer 库
  5. The Tokenizer Playground - OpenAI 可视化工具

评论区
暂无评论
avatar