第五章-自己搭建大模型_第2部分-自己训练 Tokenizer

总目录

  1. 第一章 NLP基础概念完整指南

    1. 第1部分-概念和发展历史
    2. 第2部分-各种任务(实体识别、关系抽取、文本摘要、机器翻译、自动问答)
    3. 第3部分-文本表示(词向量、语言模型、ELMo)
  2. 第二章 Transformer 架构原理

    1. 第1部分-注意力机制
    2. 第2部分Encoder-Decoder架构
    3. 第3部分-完整Transformer模型)
  3. 第三章 预训练语言模型

    1. 第1部分-Encoder-only(BERT、RoBERTa、ALBERT)
    2. 第2部分-Encoder-Decoder-T5
    3. 第3部分-Decoder-Only(GPT、LLama、GLM)
  4. 第四章 大语言模型

    1. 第1部分-发展历程、上下文、指令遵循、多模态
    2. 第2部分-LLM预训练、监督微调、强化学习
  5. 第五章 动手搭建大模型

    1. 第1部分-动手实现一个LLaMA2大模型
    2. 第2部分-自己训练 Tokenizer
    3. 第3部分-预训练一个小型LLM
  6. 第六章 大模型训练实践

    1. 第1部分-模型预训练
    2. 第2部分-模型有监督微调
    3. 第3部分-高效微调
  7. 第七章 大模型实战

    1. 第1部分-评测+RAG检索增强生成
    2. 第2部分-智能体Agent系统

目录

  1. 训练 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对。

算法流程

  1. 初始化:每个字符是一个token
  2. 统计所有相邻token对的频率
  3. 合并频率最高的对,形成新token
  4. 重复步骤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

基于概率模型的子词分词。

算法

  1. 从大词表开始
  2. 为每个子词计算概率
  3. 移除对损失影响最小的子词
  4. 重复直到达到目标大小

示例

输入: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, ...]
解码: 人工智能技术正在改变世界
Logo

更多推荐