机器学习|从0开发大模型之Tokenizer训练
继续写《从0开发大模型》系列文章,本文主要介绍从头快速训练一个Tokenizer。
1、Tokenizer
什么是Tokenizer,作用是什么?对于从0开发大模型的开发者,这里解释一下。Tokenizer(标记器)是 NLP 管道的核心组件之一,它们有一个目的:将文本转换为模型可以处理的数据。
模型只能处理数字,因此Tokenizer需要将我们的文本输入转换为数字数据,比如在NLP的任务中,有如下原始文本:
我是中国人,我爱我的祖国
以上文本我们没法直接丢给模型处理,因此需要将原始文本分词,然后将对应的分词分配对应的ID,从 0 开始一直到词汇表的大小,那么该模型使用这些 ID 来识别每个词,比如上述文本变成:我 | 是 | 中 | 国 | 人 | , | 我 | 爱 | 我 | 的 | 祖 | 国
当然像这种词标记的技术有很多,如:
Byte-level BPE, 用于 GPT-2;
WordPiece, 用于 BERT;
...
2、准备数据
Tokenizer训练之前需要准备数据,其中huggingface上有很多相关的数据(huggingface.co/datasets?so…
def read_texts_from_jsonl(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
yield data['text']
data_path = '../datasets/tokenizer_train.jsonl'
3、训练
准备好数据以后,使用 tokenizers
库来训练,如下代码:
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
首先创建了一个 BPE
模型的分词器实例,并设置其预处理器为 ByteLevel
,这意味着输入文本将被视为字节流进行处理,add_prefix_space=False
表示在每个字节前不添加空格。
# 定义特殊token
special_tokens = ["<unk>", "<s>", "</s>"]
# 设置训练器并添加特殊token
trainer = trainers.BpeTrainer(
vocab_size=2048,
special_tokens=special_tokens, # 确保这三个token被包含
show_progress=True,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
)
然后定义 special_tokens
,指定词汇表大小为2048,其中三个特殊的token:<unk>
(未知词),<s>
(开始标记),</s>
(结束标记)。
texts = read_texts_from_jsonl(data_path)
tokenizer.train_from_iterator(texts, trainer=trainer)
tokenizer.decoder = decoders.ByteLevel()
tokenizer_dir = "./my_tokenizer"
os.makedirs(tokenizer_dir, exist_ok=True)
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
tokenizer.model.save("./my_tokenizer")
读取预处理的数据,通过 tokenizer.train_from_iterator
训练分词器,设置 tokenizer.decoder
解码,用于将token还原回原始文本,最后将分词模型保存到 my_tokenizer
文件夹中。
这里 HuggingFace
的 Tokenizer
还需要用到 tokenizer_config.json,用于分词模型的配置信息,用于指定分词模型的超参和其他的相关信息,例如分词器的类型、词汇表大小、最大序列长度、特殊标记等,这个一般固定就可以。
4、测试
tokenizer = AutoTokenizer.from_pretrained("./my_tokenizer")
new_prompt = '我是中国人,我爱我的祖国 @微博 当前数据为测试文件! '
print(new_prompt)
model_inputs = tokenizer(new_prompt)
print(model_inputs)
print('长度:', len(model_inputs['input_ids']))
// 输出
我是中国人,我爱我的祖国 @微博 当前数据为测试文件!
{'input_ids': [397, 345, 413, 1317, 415, 270, 397, 1699, 397, 667, 101, 247, 1317, 223, 34, 454, 109, 469, 251, 223, 938, 1143, 1327, 428, 1577, 1410, 677, 1223, 264, 226, 223], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
长度: 31
上述代码主要是加载预训练模型,然后打印对应的词的ID,然后通过反序列:
input_ids_ = model_inputs['input_ids']
response = tokenizer.decode(input_ids_)
print(response, end='')
// 输出
我是中国人,我爱我的祖国 @微博 当前数据为测试文件!
5、总结
(1)以上就是训练一个 Tokenizer
的流程,当然也可以使用其他的,比如使用gpt2:
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("gpt2")
tokenizer = AutoTokenizer.from_pretrained("gpt2")
prompt = "GPT2 is a model developed by OpenAI."
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
gen_tokens = model.generate(
input_ids,
do_sample=True,
temperature=0.9,
max_length=100,
)
gen_text = tokenizer.batch_decode(gen_tokens)[0]
print("gen_text: ", gen_text)
(2)完整的 0-train_tokenizer
代码如下:
import random
import json
from tokenizers import (
decoders,
models,
pre_tokenizers,
trainers,
Tokenizer,
)
import os
random.seed(42)
def train_tokenizer():
# 读取JSONL文件并提取文本数据
def read_texts_from_jsonl(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
yield data['text']
data_path = '../datasets/tokenizer_train.jsonl'
# 初始化tokenizer
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
# 定义特殊token
special_tokens = ["<unk>", "<s>", "</s>"]
# 设置训练器并添加特殊token
trainer = trainers.BpeTrainer(
vocab_size=2048,
special_tokens=special_tokens, # 确保这三个token被包含
show_progress=True,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
)
# 读取文本数据
texts = read_texts_from_jsonl(data_path)
# 训练tokenizer
tokenizer.train_from_iterator(texts, trainer=trainer)
# 设置解码器
tokenizer.decoder = decoders.ByteLevel()
# 检查特殊token的索引
assert tokenizer.token_to_id("<unk>") == 0
assert tokenizer.token_to_id("<s>") == 1
assert tokenizer.token_to_id("</s>") == 2
# 保存tokenizer
tokenizer_dir = "./my_tokenizer"
os.makedirs(tokenizer_dir, exist_ok=True)
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
tokenizer.model.save("./my_tokenizer")
# 手动创建配置文件
config = {
"add_bos_token": False,
"add_eos_token": False,
"add_prefix_space": True,
"added_tokens_decoder": {
"0": {
"content": "<unk>",
"lstrip": False,
"normalized": False,
"rstrip": False,
"single_word": False,
"special": True
},
"1": {
"content": "<s>",
"lstrip": False,
"normalized": False,
"rstrip": False,
"single_word": False,
"special": True
},
"2": {
"content": "</s>",
"lstrip": False,
"normalized": False,
"rstrip": False,
"single_word": False,
"special": True
}
},
"additional_special_tokens": [],
"bos_token": "<s>",
"clean_up_tokenization_spaces": False,
"eos_token": "</s>",
"legacy": True,
"model_max_length": 1000000000000000019884624838656,
"pad_token": None,
"sp_model_kwargs": {},
"spaces_between_special_tokens": False,
"tokenizer_class": "PreTrainedTokenizerFast",
"unk_token": "<unk>",
"use_default_system_prompt": False,
"chat_template": "{% if messages[0]['role'] == 'system' %}{% set system_message = messages[0]['content'] %}{% endif %}{% if system_message is defined %}{{ system_message }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<s>user\n' + content + '</s>\n<s>assistant\n' }}{% elif message['role'] == 'assistant' %}{{ content + '</s>' + '\n' }}{% endif %}{% endfor %}"
}
# 保存配置文件
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w", encoding="utf-8") as config_file:
json.dump(config, config_file, ensure_ascii=False, indent=4)
print("Tokenizer training completed and saved.")
def eval_tokenizer():
from transformers import AutoTokenizer
# 加载预训练的tokenizer
tokenizer = AutoTokenizer.from_pretrained("./my_tokenizer")
messages = [
{"role": "system", "content": "你好,你是中国人,请用中文回答我的问题"},
{"role": "user", "content": '123'},
]
new_prompt = tokenizer.apply_chat_template(
messages,
tokenize=False
)
print(new_prompt)
# 获取词汇表大小(不包括特殊符号)
print('tokenizer词表大小:', tokenizer.vocab_size)
# 获取实际词汇表长度(包括特殊符号)
actual_vocab_size = len(tokenizer)
print('实际词表长度:', actual_vocab_size)
new_prompt = '我是中国人,我爱我的祖国 @微博 当前数据为测试文件! '
print(new_prompt)
model_inputs = tokenizer(new_prompt)
print(model_inputs)
print('长度:', len(model_inputs['input_ids']))
input_ids_ = model_inputs['input_ids']
response = tokenizer.decode(input_ids_)
print(response, end='')
def main():
train_tokenizer()
eval_tokenizer()
if __name__ == '__main__':
main()