大模型原理与实践:第五章-自己搭建大模型_第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, ...]
解码: 人工智能技术正在改变世界
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)