[2025.06.05] update tokenizer train code, use the style of Qwen

This commit is contained in:
coffee
2025-06-05 18:09:18 +08:00
parent db4c2658b1
commit 6a30309848
3 changed files with 149 additions and 52 deletions
+1 -2
View File
@@ -5,5 +5,4 @@ dolly_env_local
llm_dolly.egg-info llm_dolly.egg-info
src_llm/__pycache__ src_llm/__pycache__
weights_MoE weights_MoE
weights_llm weights_llm
weights_tokenizer/*
@@ -20,81 +20,82 @@ def get_training_corpus():
if "text" in data: if "text" in data:
yield data["text"] yield data["text"]
# 使用Path对象处理路径
corpus_dir = Path("./corpus") corpus_dir = Path("./corpus/skypile_2020_head")
jsonl_files = list(corpus_dir.glob("**/*.jsonl")) jsonl_files = list(corpus_dir.glob("**/*.jsonl"))
if not jsonl_files: if not jsonl_files:
raise ValueError("No training files found") raise ValueError("No training files found")
# 使用Qwen2官方特殊标记
special_tokens = [ special_tokens = [
"<|endoftext|>", "<|endoftext|>",
"<|im_start|>", "<|im_start|>",
"<|im_end|>", "<|im_end|>",
] ]
# 创建BPE tokenizer
tokenizer = Tokenizer(models.BPE()) tokenizer = Tokenizer(models.BPE())
# 使用Qwen2的normalizer配置
tokenizer.normalizer = normalizers.Sequence([ tokenizer.normalizer = normalizers.Sequence([
normalizers.NFKC(), normalizers.NFKC(),
normalizers.StripAccents(), normalizers.StripAccents(),
]) ])
# 简化预分词器:使用ByteLevel代替多个Split规则
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(
add_prefix_space=False, # 对齐Qwen2配置
use_regex=True
)
# 移除后处理器(Qwen2不需要特殊后处理) tokenizer.pre_tokenizer = pre_tokenizers.Sequence(
tokenizer.post_processor = None [
pre_tokenizers.Split(r"[\r\n]", "isolated"),
pre_tokenizers.Split(r"\s?[A-Za-zµÀ-ÖØ-öø-ƺƼ-ƿDŽ-ʓʕ-ʯͰ-ͳͶͷͻ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-ՖႠ-ჅᎠ-Ᏽᏸ-ᏽᲐ-ᲺᲽ-Ჿᴀ-ᴫᵫ-ᵷᵹ-ᶚḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℴℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-ⱻⱾ-ⳤⳫ-ⳮⳲⳳꙀ-ꙭꚀ-ꚛꜢ-ꝯꝱ-ꞇꞋ-ꞎꭰ-ꮿff-stﬓ-ﬗA-Za-z𐐀-𐑏𐒰-𐓓𐓘-𐓻𐲀-𐲲𐳀-𐳲𑢠-𑣟𞤀-𞥃]+","isolated"),
pre_tokenizers.Split(r"\s?[!-/:-~!-/:-~‘-‟ -。]+", "isolated"),
pre_tokenizers.Split(r"\s+$", "isolated"),
pre_tokenizers.Split("[一-龥ࠀ-一가-퟿]+","isolated"),
pre_tokenizers.Digits(individual_digits=True),
pre_tokenizers.ByteLevel(add_prefix_space=False, use_regex=False),
])
# tokenizer.post_processor = None
tokenizer.post_processor = processors.ByteLevel(use_regex=True, trim_offsets=False, add_prefix_space=True)
# 配置解码器
tokenizer.decoder = decoders.ByteLevel( tokenizer.decoder = decoders.ByteLevel(
use_regex=True, use_regex=True,
trim_offsets=True, trim_offsets=True,
add_prefix_space=False # 对齐Qwen2的add_prefix_space配置 add_prefix_space=False
) )
# 配置训练器
trainer = trainers.BpeTrainer( trainer = trainers.BpeTrainer(
vocab_size=12800, vocab_size=12800,
special_tokens=special_tokens, special_tokens=special_tokens,
min_frequency=2, # 添加最小频率阈值 show_progress=True
show_progress=True # 显示进度条
) )
print("Training tokenizer...") print("Training tokenizer...")
tokenizer.train_from_iterator( tokenizer.train_from_iterator(
get_training_corpus(), get_training_corpus(),
trainer=trainer, trainer=trainer,
length=sum(1 for _ in get_training_corpus()) # 添加总长度用于进度显示 length=sum(1 for _ in get_training_corpus())
) )
# 创建PreTrainedTokenizerFast时添加Qwen2关键配置
wrapped_tokenizer = PreTrainedTokenizerFast( wrapped_tokenizer = PreTrainedTokenizerFast(
tokenizer_object=tokenizer, tokenizer_object=tokenizer,
bos_token=None, # 对齐Qwen2配置 bos_token=None,
eos_token="<|endoftext|>", eos_token="<|endoftext|>",
pad_token="<|endoftext|>", pad_token="<|endoftext|>",
unk_token=None, # 对齐Qwen2配置 unk_token=None,
additional_special_tokens=["<|im_start|>", "<|im_end|>"], additional_special_tokens=["<|im_start|>", "<|im_end|>"],
model_max_length=32768, # 添加model_max_length model_max_length=32768,
clean_up_tokenization_spaces=False, # 对齐Qwen2配置 clean_up_tokenization_spaces=False,
errors="replace", # 对齐Qwen2配置 errors="replace",
split_special_tokens=False, # 对齐Qwen2配置 split_special_tokens=False,
) )
output_dir = Path("./llm_tokenizer") output_dir = Path("./llm_tokenizer")
output_dir.mkdir(exist_ok=True, parents=True) output_dir.mkdir(exist_ok=True, parents=True)
# 保存tokenizer
wrapped_tokenizer.save_pretrained(output_dir) wrapped_tokenizer.save_pretrained(output_dir)
print(f"Tokenizer saved to {output_dir}") print(f"Tokenizer saved to {output_dir}")
# 使用Qwen2官方chat_template
QWEN2_CHAT_TEMPLATE = ( QWEN2_CHAT_TEMPLATE = (
"{% for message in messages %}" "{% for message in messages %}"
"{% if loop.first and messages[0]['role'] != 'system' %}" "{% if loop.first and messages[0]['role'] != 'system' %}"
@@ -107,13 +108,10 @@ QWEN2_CHAT_TEMPLATE = (
"{% endif %}" "{% endif %}"
) )
# 直接设置chat_template避免重新加载
wrapped_tokenizer.chat_template = QWEN2_CHAT_TEMPLATE wrapped_tokenizer.chat_template = QWEN2_CHAT_TEMPLATE
# 再次保存包含chat_template的配置
wrapped_tokenizer.save_pretrained(output_dir) wrapped_tokenizer.save_pretrained(output_dir)
# 测试对话模板
messages = [ messages = [
{"role": "user", "content": "你好!"}, {"role": "user", "content": "你好!"},
{"role": "assistant", "content": "你好,有什么可以帮您?"}, {"role": "assistant", "content": "你好,有什么可以帮您?"},
@@ -128,7 +126,11 @@ formatted = wrapped_tokenizer.apply_chat_template(
print("\n格式化后的对话:") print("\n格式化后的对话:")
print(formatted) print(formatted)
# 测试tokenization
test_text = "深度学习是人工智能的一个重要分支。Deep learning is a subset of machine learning." test_text = "深度学习是人工智能的一个重要分支。Deep learning is a subset of machine learning."
print("\nTest tokenization:") print("\nTest tokenization:")
print(wrapped_tokenizer.tokenize(test_text)) encoded = wrapped_tokenizer.tokenize(test_text)
print(encoded)
print(encoded)
decoded_text = wrapped_tokenizer.decode(encoded)
print(decoded_text)
print(decoded_text)
@@ -1,22 +1,118 @@
import os
import json
from pathlib import Path
import sentencepiece as spm import sentencepiece as spm
import logging
import tempfile
import unicodedata
from transformers import PreTrainedTokenizerFast
from tqdm import tqdm
logging.basicConfig(level=logging.INFO, format='%(message)s')
###推荐使用 utils/spm_train_tokenizer.sh,使用多核加速 def normalize_text(text):
text = text.replace('\x00', '')
text = unicodedata.normalize('NFKC', text)
text = ''.join(c for c in unicodedata.normalize('NFD', text)
if unicodedata.category(c) != 'Mn')
return text
#英文分词器 corpus_dir = Path("./corpus/skypile_2020_head")
# corpus_path = 'a.txt,b.txt' jsonl_files = list(corpus_dir.glob("**/*.jsonl"))
corpus_path_en= 'corpus/pg16457.txt' if not jsonl_files:
vocab_size_en = 1000 raise ValueError("No training files found")
model_en= 'tokenizer_file/dolly_tokenizer_spm/tokenizer_en'
# model_type 使用 bpe special_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"]
model_type = 'bpe' logging.info("Staring processing corpus")
# byte_fallback=True,避免 out of vocabulary 问题 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as temp_corpus:
spm.SentencePieceTrainer.train(input=corpus_path_en, model_prefix=model_en, temp_corpus_path = temp_corpus.name
vocab_size=vocab_size_en, model_type=model_type, byte_fallback=True,character_coverage=1) file_progress = tqdm(jsonl_files, desc="处理文件中", unit="file")
for file_path in file_progress:
file_progress.set_description(f"处理: {file_path.name[:40]}...")
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
try:
data = json.loads(line)
if "text" in data:
normalized_text = normalize_text(data["text"])
temp_corpus.write(normalized_text + "\n")
except json.JSONDecodeError as e:
logging.warning(f"JSON 解析错误在 {file_path}: {e}")
except UnicodeDecodeError as e:
logging.warning(f"编码错误在 {file_path}: {e}")
logging.info("corpus processing completed")
logging.info("Starting SentencePiece training")
#中文分词器 vocab_size = 32000
corpus_path_zh= 'corpus/wikipedia.txt'
vocab_size_zh = 32000 # character_coverage = 0.9995
model_zh= 'tokenizer_file/dolly_tokenizer_spm/tokenizer_zh'
spm.SentencePieceTrainer.train(input=corpus_path_zh, model_prefix=model_zh, spm.SentencePieceTrainer.train(
vocab_size=vocab_size_zh, model_type=model_type, byte_fallback=True,character_coverage=1) input=temp_corpus_path,
model_prefix='tokenizer_model',
vocab_size=vocab_size,
model_type='bpe',
character_coverage=1.0,
pad_id=-1,
bos_id=-1,
eos_id=-1,
unk_id=0,
user_defined_symbols=special_tokens,
split_digits=True,
byte_fallback=True,
remove_extra_whitespaces=False,
add_dummy_prefix=False,
normalization_rule_name='identity',
num_threads=os.cpu_count(),
max_sentencepiece_length=32,
shuffle_input_sentence=True,
)
os.unlink(temp_corpus_path)
logging.info("SentencePiece training completed")
tokenizer = PreTrainedTokenizerFast(
tokenizer_file='tokenizer_model.model',
bos_token=None,
eos_token="<|endoftext|>",
pad_token="<|endoftext|>",
unk_token=None,
additional_special_tokens=["<|im_start|>", "<|im_end|>"],
model_max_length=32768,
clean_up_tokenization_spaces=False,
split_special_tokens=False,
errors="replace"
)
output_dir = Path("./llm_tokenizer")
output_dir.mkdir(exist_ok=True, parents=True)
tokenizer.save_pretrained(output_dir)
logging.info(f"Tokenizer saved to {output_dir}")
QWEN2_CHAT_TEMPLATE = (
"{% for message in messages %}"
"{% if loop.first and messages[0]['role'] != 'system' %}"
"{{ '<|im_start|>system\nYou are a helpful assistant<|im_end|>\n' }}"
"{% endif %}"
"{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{ '<|im_start|>assistant\n' }}"
"{% endif %}"
)
tokenizer.chat_template = QWEN2_CHAT_TEMPLATE
tokenizer.save_pretrained(output_dir)
messages = [
{"role": "user", "content": "你好!"},
{"role": "assistant", "content": "你好,有什么可以帮您?"},
{"role": "user", "content": "Python怎么用?"}
]
formatted = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
print("\n格式化后的对话:")
print(formatted)