大模型原理与实践:第五章-自己搭建大模型_第2部分-自己训练 Tokenizer
本文介绍了训练Tokenizer的方法,重点讲解了BPE、WordPiece和Unigram等子词分词算法。BPE通过合并高频字符对构建词表,WordPiece基于语言模型似然选择合并,Unigram则采用概率模型优化词表。文章详细演示了使用Hugging Face库训练BPE Tokenizer的完整流程,包括数据准备、配置设置和模型训练。不同Tokenizer各有优缺点:基于词的方法简单但词表
   ·  
 第五章-自己搭建大模型_第2部分-自己训练 Tokenizer
总目录
目录
2. 训练 Tokenizer
Tokenizer是NLP的基础设施,它负责将文本转换为模型能理解的数字序列。不同的tokenizer方法适用于不同场景,选择合适的tokenizer对模型效果有重要影响。
2.1 Tokenizer 类型介绍
2.1.1 基于词的Tokenizer
最直观的方法,按空格和标点分词。
优点:
- 实现简单
- 符合人类认知
缺点:
- 词表巨大(英文需要50k+,中文更多)
- 无法处理未登录词(OOV)
- 形态变化丰富的语言(如德语)效果差
示例:
输入:I don't know what you're talking about.
输出:["I", "do", "n't", "know", "what", "you", "'re", "talking", "about", "."]
2.1.2 基于字符的Tokenizer
最细粒度的分词。
优点:
- 词表极小(英文仅需128个字符)
- 完全没有OOV问题
- 适合拼写纠错等任务
缺点:
- 序列变得很长(10倍于词级)
- 丢失词级语义信息
- 计算开销大
示例:
输入:Hello
输出:['H', 'e', 'l', 'l', 'o']
2.1.3 子词Tokenizer
当前主流方法,平衡了词级和字符级的优缺点。
(1) BPE(Byte Pair Encoding)
核心思想:从字符开始,迭代合并最频繁的相邻token对。
算法流程:
- 初始化:每个字符是一个token
- 统计所有相邻token对的频率
- 合并频率最高的对,形成新token
- 重复步骤2-3,直到达到目标词表大小
示例:
初始:['l', 'o', 'w', 'e', 'r']  ['n', 'e', 'w', 'e', 's', 't']
最终:lower → ['low', 'er']  newest → ['new', 'est']
(2) WordPiece
与BPE类似,但选择合并时考虑语言模型似然。Google的BERT使用此方法。
特点:
- 使用##前缀标记子词
- 优化目标是最大化训练数据的似然
示例:
输入:unhappiness
输出:['un', '##happiness']
(3) Unigram
基于概率模型的子词分词。
算法:
- 从大词表开始
- 为每个子词计算概率
- 移除对损失影响最小的子词
- 重复直到达到目标大小
示例:
输入:unhappiness
可能输出:['un', 'happiness'] 或 ['unhap', 'piness'](取决于概率)
2.2 训练一个 BPE Tokenizer
我们选择BPE算法训练tokenizer,因为它:
- 实现简单高效
- 在多种语言上表现良好
- 被GPT、LLaMA等主流模型采用
步骤1:安装依赖
pip install tokenizers datasets transformers
步骤2:准备数据加载器
import json
from typing import Generator
def read_texts_from_jsonl(file_path: str) -> Generator[str, None, None]:
    """
    从JSONL文件逐行读取文本
    
    Args:
        file_path: JSONL文件路径,每行格式为 {"text": "..."}
        
    Yields:
        文本字符串
    """
    with open(file_path, 'r', encoding='utf-8') as f:
        for line_num, line in enumerate(f, 1):
            try:
                data = json.loads(line)
                if 'text' not in data:
                    raise KeyError(f"第{line_num}行缺少'text'字段")
                yield data['text']
            except json.JSONDecodeError:
                print(f"警告:第{line_num}行JSON解码失败,跳过")
                continue
            except KeyError as e:
                print(f"警告:{e}")
                continue
步骤3:创建配置文件
import os
def create_tokenizer_config(save_dir: str) -> None:
    """
    创建tokenizer配置文件
    包括tokenizer_config.json和special_tokens_map.json
    """
    # 主配置
    config = {
        "add_bos_token": False,
        "add_eos_token": False,
        "bos_token": "<|im_start|>",
        "eos_token": "<|im_end|>",
        "pad_token": "<|im_end|>",
        "unk_token": "<unk>",
        "model_max_length": 1000000000000000019884624838656,
        "clean_up_tokenization_spaces": False,
        "tokenizer_class": "PreTrainedTokenizerFast",
        # 聊天模板:兼容Qwen2.5格式
        "chat_template": (
            "{% for message in messages %}"
            "{% if message['role'] == 'system' %}"
            "<|im_start|>system\n{{ message['content'] }}<|im_end|>\n"
            "{% elif message['role'] == 'user' %}"
            "<|im_start|>user\n{{ message['content'] }}<|im_end|>\n"
            "{% elif message['role'] == 'assistant' %}"
            "<|im_start|>assistant\n{{ message['content'] }}<|im_end|>\n"
            "{% endif %}"
            "{% endfor %}"
            "{% if add_generation_prompt %}"
            "{{ '<|im_start|>assistant\n' }}"
            "{% endif %}"
        )
    }
    
    os.makedirs(save_dir, exist_ok=True)
    with open(os.path.join(save_dir, "tokenizer_config.json"), "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)
    # 特殊token映射
    special_tokens_map = {
        "bos_token": "<|im_start|>",
        "eos_token": "<|im_end|>",
        "unk_token": "<unk>",
        "pad_token": "<|im_end|>",
        "additional_special_tokens": ["<s>", "</s>"]
    }
    with open(os.path.join(save_dir, "special_tokens_map.json"), "w", encoding="utf-8") as f:
        json.dump(special_tokens_map, f, ensure_ascii=False, indent=2)
聊天模板说明:
聊天模板定义了如何将对话格式化为模型输入。我们采用的格式:
<|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
你好<|im_end|>
<|im_start|>assistant
你好!有什么可以帮你的吗?<|im_end|>
步骤4:训练Tokenizer
from tokenizers import Tokenizer, models, pre_tokenizers, decoders, trainers
from tokenizers.normalizers import NFKC
def train_tokenizer(data_path: str, save_dir: str, vocab_size: int = 6144):
    """
    训练BPE tokenizer
    
    Args:
        data_path: 训练数据路径(JSONL格式)
        save_dir: 保存目录
        vocab_size: 词表大小
    """
    # 初始化BPE模型
    tokenizer = Tokenizer(models.BPE(unk_token="<unk>"))
    
    # 文本规范化:NFKC统一Unicode表示
    tokenizer.normalizer = NFKC()
    
    # 预分词器:ByteLevel处理所有字节
    tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
    
    # 解码器
    tokenizer.decoder = decoders.ByteLevel()
    # 特殊token(顺序很重要!)
    special_tokens = [
        "<unk>",        # ID=0 未知token
        "<s>",          # ID=1 序列开始
        "</s>",         # ID=2 序列结束
        "<|im_start|>", # ID=3 对话开始
        "<|im_end|>"    # ID=4 对话结束
    ]
    # 训练器配置
    trainer = trainers.BpeTrainer(
        vocab_size=vocab_size,
        special_tokens=special_tokens,
        min_frequency=2,  # 最小词频
        show_progress=True,
        initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
    )
    # 开始训练
    print(f"开始训练tokenizer...")
    print(f"数据路径: {data_path}")
    print(f"词表大小: {vocab_size}")
    
    texts = read_texts_from_jsonl(data_path)
    tokenizer.train_from_iterator(
        texts,
        trainer=trainer,
        length=os.path.getsize(data_path)
    )
    # 验证特殊token ID
    assert tokenizer.token_to_id("<unk>") == 0
    assert tokenizer.token_to_id("<s>") == 1
    assert tokenizer.token_to_id("</s>") == 2
    assert tokenizer.token_to_id("<|im_start|>") == 3
    assert tokenizer.token_to_id("<|im_end|>") == 4
    print("✓ 特殊token ID验证通过")
    # 保存
    os.makedirs(save_dir, exist_ok=True)
    tokenizer.save(os.path.join(save_dir, "tokenizer.json"))
    create_tokenizer_config(save_dir)
    
    print(f"✓ Tokenizer已保存到 {save_dir}")
步骤5:测试Tokenizer
from transformers import AutoTokenizer
def test_tokenizer(tokenizer_path: str):
    """测试训练好的tokenizer"""
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
    print("\n" + "="*50)
    print("Tokenizer基本信息")
    print("="*50)
    print(f"词表大小: {len(tokenizer)}")
    print(f"特殊tokens: {tokenizer.all_special_tokens}")
    print(f"特殊token IDs: {tokenizer.all_special_ids}")
    # 测试聊天模板
    messages = [
        {"role": "system", "content": "你是一个AI助手"},
        {"role": "user", "content": "你好"},
        {"role": "assistant", "content": "你好!有什么可以帮你的?"}
    ]
    
    print("\n" + "="*50)
    print("聊天模板测试")
    print("="*50)
    prompt = tokenizer.apply_chat_template(messages, tokenize=False)
    print(prompt)
    # 测试编码
    print("\n" + "="*50)
    print("编码测试")
    print("="*50)
    text = "人工智能技术正在改变世界"
    encoded = tokenizer(text)
    print(f"原文: {text}")
    print(f"Token IDs: {encoded['input_ids']}")
    print(f"解码: {tokenizer.decode(encoded['input_ids'])}")
# 运行测试
test_tokenizer('./tokenizer_k/')
预期输出:
==================================================
Tokenizer基本信息
==================================================
词表大小: 6144
特殊tokens: ['<|im_start|>', '<|im_end|>', '<unk>', '<s>', '</s>']
特殊token IDs: [3, 4, 0, 1, 2]
==================================================
聊天模板测试
==================================================
<|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
你好<|im_end|>
<|im_start|>assistant
你好!有什么可以帮你的?<|im_end|>
==================================================
编码测试
==================================================
原文: 人工智能技术正在改变世界
Token IDs: [1234, 5678, ...]
解码: 人工智能技术正在改变世界
更多推荐
 
 



所有评论(0)