让英文大模型懂中文!手把手教你构建中文 Tokenization(基于 LLaMA)
很多开发者都遇到过这样的问题:基于 LLaMA 家族的英文大模型,处理中文时要么分词混乱,要么生成效果差。核心原因很简单 —— 原始 LLaMA 模型的训练语料以英文为主,中文词表覆盖率极低,无法正确理解和处理中文文本。
今天就带大家从零开始,用《斗破苍穹》作为中文语料,一步步构建中文 Tokenization,合并中英文词表,让 LLaMA 类模型完美支持中文。全程附可直接复制的代码和详细解释,新手也能轻松上手!
一、为什么要单独构建中文 Tokenization?
LLaMA 家族模型的原生词表只有 32000 个 token,且大多是英文相关的子词和符号。直接用它处理中文,会出现两个严重问题:
- 中文被拆分成零散的 UTF-8 编码(比如 “依” 拆成
<0xE4><0xBE><0x9D>),模型无法理解语义; - 分词效率极低,一句话需要比英文多得多的 token,既浪费上下文窗口,又降低推理速度。
而构建中文 Tokenization 的核心目标,就是让模型拥有专门的中文词表,能将中文词汇、短语作为完整 token 处理,从根源上解决中文支持问题。
二、准备工作:环境搭建与语料选择
1. 环境依赖安装
需要用到sentencepiece(训练词表)和transformers(加载模型和 tokenizer),直接用 pip 安装:
bash
运行
pip install sentencepiece transformers torch 2. 语料选择与准备
中文语料需要满足 “量大、规范、覆盖广” 的特点,本文选用经典网络小说《斗破苍穹》(全文约 290 万字,包含大量日常用语和固定搭配)。你也可以替换为其他中文语料(如维基百科中文、新闻语料等)。
语料文件命名为《斗破苍穹》.txt,放在data/目录下(新建data文件夹即可)。
三、Step1:语料预处理 —— 清洗无效内容
原始语料中会包含换行、特殊标记(如 “=== 上架感言 ===”)等无效信息,需要先清洗,只保留纯中文句子。
预处理代码(保存为data_preprocess.py)
python
运行
# 1. 加载原始语料 with open("data/《斗破苍穹》.txt", "r", encoding="utf-8") as fp: data = fp.read().strip().split("\n") # 按换行分割 # 2. 过滤无效内容,提取有效句子 sentences = [] for line in data: line = line.strip() # 去除前后空格 # 过滤条件:排除空行、特殊标记行、来源标注行 if "===" in line or len(line) == 0 or line == "《斗破苍穹》来自:": continue sentences.append(line) # 3. 保存预处理后的语料 with open("data/corpus.txt", "w", encoding="utf-8") as fp: fp.write("\n".join(sentences)) print(f"预处理完成!共提取 {len(sentences)} 个有效句子") print("示例句子:", sentences[:3]) 运行结果
会在data/目录下生成corpus.txt,里面是清洗后的纯中文句子,示例如下:
plaintext
又一次上架了,这次比上次还激动,甚至激动到了上传了章节却不知道发出来的地步。 尴尬,关于新书,上架前成绩好得有些出乎土豆的意料,对于这份厚硕的成绩,土豆心里还真有几分惶恐与忐忑,虽说曾经有人说土豆是刷出来的数据,对于这些留言,我也并未太过在意,别的我不知道,我唯一能知道的,就是人在做,天在看! 究竟刷没刷,自己心中有杆秤就能衡量,问心无愧,何惧留言? 四、Step2:训练中文词表 —— 用 SentencePiece 实现 BPE 分词
SentencePiece 是谷歌开源的词表训练工具,支持 BPE(字节对编码)等多种分词算法,是大模型 Tokenization 的首选工具。我们用它基于预处理后的语料,训练一个 50000 词的中文词表。
中文词表训练代码(保存为train_chinese_vocab.py)
python
运行
import sentencepiece as spm # 训练配置 spm.SentencePieceTrainer.train( input='data/corpus.txt', # 预处理后的语料路径 model_prefix='tokenizer', # 输出模型前缀(会生成tokenizer.model和tokenizer.vocab) vocab_size=50000, # 词表大小(可根据需求调整,如32000、64000) user_defined_symbols=['foo', 'bar'], # 自定义特殊符号(可选,测试用) character_coverage=1.0, # 覆盖全部字符(确保没有未登录字符) model_type="bpe", # 分词算法(BPE最常用,也可选择unigram、char) bos_id=1, # 句子开头符号(<s>)的ID eos_id=2, # 句子结束符号(</s>)的ID unk_id=0, # 未登录词符号(<unk>)的ID pad_id=-1 # 不使用padding符号(可根据模型需求调整) ) print("中文词表训练完成!生成文件:tokenizer.model(模型文件)、tokenizer.vocab(词表文件)") 关键参数说明
vocab_size:词表越大,能覆盖的中文词汇越多,但模型参数量也会增加,建议 32000-64000;model_type:BPE 适合中文分词,能平衡词汇覆盖率和 token 长度;character_coverage:设置为 1.0 确保覆盖所有语料中的字符,避免出现未登录字符。
运行结果
当前目录下会生成两个文件:
tokenizer.model:SentencePiece 模型文件(用于分词和编码);tokenizer.vocab:词表文件(包含所有 token 及其得分,可打开查看)。
补分词算法
1. BPE(字节对编码):最常用,平衡效率与语义
- 核心逻辑:从最小单位(单个字符)开始,反复合并出现频率最高的字符对,直到达到预设词表大小。
- 通俗理解:就像拼乐高,先有单个积木(字符),把经常凑在一起的积木粘成大块(子词 / 词语),比如 “白”+“日”→“白日”、“千”+“里”→“千里”。
- 优点:词表大小可控,兼顾语义完整性(比如 “黄河” 不会拆成 “黄”+“河”)和计算效率,支持中英文混合。
- 适用场景:LLaMA、GPT-2 等绝大多数大模型(本文中文分词也用的 BPE)。
2. Unigram(单字语言模型):灵活性强,适合多语言
- 核心逻辑:先生成一个超大候选词表,再用语言模型筛选出 “语义最优” 的 token 组合,逐步剪枝到目标词表大小。
- 通俗理解:先列出所有可能的 “词语候选”(比如 “白”“日”“白日”“白日落”),再根据 “哪个组合最符合语言习惯” 来选择拆分方式。
- 优点:分词更灵活,能处理生僻词和多语言混合场景,拆分结果更贴近自然语言。
- 适用场景:T5、mT5 等多语言模型。
3. Char(字符级分词):最简单,无词表依赖
- 核心逻辑:不做任何合并,直接将文本拆分成单个字符(中文)或字母(英文)。
- 通俗理解:把 “白日依山尽” 拆成 “白”“日”“依”“山”“尽”,每个字都是独立 token。
- 优点:实现简单,无未登录词问题(再生僻的字都能拆分)。
- 缺点:token 数量多,上下文窗口利用率低,模型难以捕捉词语级语义(比如 “黄河” 的含义不等于 “黄”+“河”)。
- 适用场景:早期小模型、低资源语言(无足够语料训练词表)。
4. Word(词语级分词):最直观,依赖词典
- 核心逻辑:基于预设词典,将文本拆分成完整词语,不拆分词语内部结构。
- 通俗理解:用一本词典去匹配文本,比如 “黄河入海流” 拆成 “黄河”“入海”“流”。
- 优点:语义最完整,token 数量少。
- 缺点:依赖高质量词典,处理未登录词(如网络新词 “YYDS”)能力差,难以适配多语言。
- 适用场景:传统 NLP 任务(如文本分类)、中文专用模型(早期)。
五、Step3:加载中文词表 —— 适配 Transformers 框架
训练好的 SentencePiece 模型需要封装成 Transformers 支持的 Tokenizer,才能和 LLaMA 模型兼容。我们参考 LLaMA 原生 Tokenizer 的实现,自定义一个中文 Tokenizer。
1. 自定义中文 Tokenizer(保存为tokenization.py)
python
运行
# coding=utf-8 import os from shutil import copyfile from typing import Any, Dict, List, Optional, Tuple import sentencepiece as spm from transformers.tokenization_utils import AddedToken, PreTrainedTokenizer from transformers.utils import logging logger = logging.get_logger(__name__) VOCAB_FILES_NAMES = {"vocab_file": "tokenizer.model"} class ChineseTokenizer(PreTrainedTokenizer): """适配Transformers的中文Tokenizer(基于SentencePiece)""" vocab_files_names = VOCAB_FILES_NAMES model_input_names = ["input_ids", "attention_mask"] def __init__( self, vocab_file, unk_token="<unk>", bos_token="<s>", eos_token="</s>", pad_token=None, sp_model_kwargs: Optional[Dict[str, Any]] = None, add_bos_token=True, add_eos_token=False, clean_up_tokenization_spaces=False, **kwargs, ): self.sp_model_kwargs = {} if sp_model_kwargs is None else sp_model_kwargs # 初始化特殊符号 bos_token = AddedToken(bos_token, lstrip=False, rstrip=False) if isinstance(bos_token, str) else bos_token eos_token = AddedToken(eos_token, lstrip=False, rstrip=False) if isinstance(eos_token, str) else eos_token unk_token = AddedToken(unk_token, lstrip=False, rstrip=False) if isinstance(unk_token, str) else unk_token pad_token = AddedToken(pad_token, lstrip=False, rstrip=False) if isinstance(pad_token, str) else pad_token super().__init__( bos_token=bos_token, eos_token=eos_token, unk_token=unk_token, pad_token=pad_token, add_bos_token=add_bos_token, add_eos_token=add_eos_token, clean_up_tokenization_spaces=clean_up_tokenization_spaces,** kwargs, ) self.vocab_file = vocab_file self.add_bos_token = add_bos_token self.add_eos_token = add_eos_token # 加载SentencePiece模型 self.sp_model = spm.SentencePieceProcessor(**self.sp_model_kwargs) self.sp_model.Load(vocab_file) @property def vocab_size(self): """返回词表大小""" return self.sp_model.get_piece_size() def get_vocab(self): """返回词表(token→id映射)""" vocab = {self.convert_ids_to_tokens(i): i for i in range(self.vocab_size)} vocab.update(self.added_tokens_encoder) return vocab def _tokenize(self, text): """分词核心方法:将文本转为token列表""" return self.sp_model.encode(text, out_type=str) def _convert_token_to_id(self, token): """将token转为ID""" return self.sp_model.piece_to_id(token) def _convert_id_to_token(self, index): """将ID转为token""" return self.sp_model.IdToPiece(index) def convert_tokens_to_string(self, tokens): """将token列表转为文本""" current_sub_tokens = [] prev_is_special = False for token in tokens: if token in self.all_special_tokens: if not prev_is_special and len(out_string) > 0: out_string += " " out_string += token prev_is_special = True current_sub_tokens = [] else: current_sub_tokens.append(token) prev_is_special = False out_string += self.sp_model.decode(current_sub_tokens) return out_string def save_vocabulary(self, save_directory, filename_prefix: Optional[str] = None) -> Tuple[str]: """保存Tokenizer到指定目录(适配Transformers的from_pretrained)""" if not os.path.isdir(save_directory): logger.error(f"保存目录 {save_directory} 不存在") return out_vocab_file = os.path.join( save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["vocab_file"] ) if os.path.abspath(self.vocab_file) != os.path.abspath(out_vocab_file) and os.path.isfile(self.vocab_file): copyfile(self.vocab_file, out_vocab_file) elif not os.path.isfile(self.vocab_file): with open(out_vocab_file, "wb") as f: f.write(self.sp_model.serialized_model_proto()) return (out_vocab_file,) def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None): """添加特殊符号(<s>和</s>)""" bos_tokens = [self.bos_token_id] if self.add_bos_token else [] eos_tokens = [self.eos_token_id] if self.add_eos_token else [] output = bos_tokens + token_ids_0 + eos_tokens if token_ids_1 is not None: output += bos_tokens + token_ids_1 + eos_tokens return output 2. 加载并保存中文 Tokenizer(保存为load_chinese_tokenizer.py)
python
运行
import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" # 解决protobuf兼容性问题 from transformers import PreTrainedTokenizer from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model import sentencepiece as spm from tokenization import ChineseTokenizer # 导入自定义Tokenizer # 加载训练好的中文SentencePiece模型 chinese_sp_model_file = "tokenizer.model" chinese_sp_model = spm.SentencePieceProcessor() chinese_sp_model.Load(chinese_sp_model_file) # 转换为Transformers兼容格式并保存 output_dir = './transformers_tokenizer/chinese/' os.makedirs(output_dir, exist_ok=True) # 保存model文件 chinese_spm = sp_pb2_model.ModelProto() chinese_spm.ParseFromString(chinese_sp_model.serialized_model_proto()) with open(output_dir + 'tokenizer.model', 'wb') as f: f.write(chinese_spm.SerializeToString()) # 初始化并保存Tokenizer tokenizer = ChineseTokenizer(vocab_file=output_dir + 'tokenizer.model') tokenizer.save_pretrained(output_dir) print(f"中文Tokenizer已保存到 {output_dir}") # 测试Tokenizer test_tokenizer = ChineseTokenizer.from_pretrained(output_dir)'白日依山尽,黄河入海流。欲穷千里目,更上一层楼。 The primary use of LLaMA is research on large language models, including''' print("\n测试文本:") print(text) print("\n中文Tokenizer分词结果:") print(test_tokenizer.tokenize(text)) 运行结果
会在transformers_tokenizer/chinese/目录下生成兼容 Transformers 的 Tokenizer 文件,测试分词结果如下(中文被正确分词,英文保留原始字符):
plaintext
['▁', '白日', '依', '山', '尽', ',', '黄', '河', '入', '海', '流', '。', '欲', '穷', '千里', '目', ',', '更', '上一层', '楼', '。', '▁', 'T', 'h', 'e', ...] 六、Step4:合并中英文词表 —— 让模型同时支持双语
单独的中文 Tokenizer 无法处理英文,我们需要将训练好的中文词表与 LLaMA 原生英文词表合并,构建双语词表。
合并词表代码(保存为merge_en_zh_vocab.py)
python
运行
import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" from transformers import LlamaTokenizer from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model import sentencepiece as spm # 1. 加载LLaMA原生英文Tokenizer(需提前下载LLaMA的tokenizer.model) llama_tokenizer_dir = "transformers_tokenizer/llama/" # 存放LLaMA原生tokenizer的目录 chinese_sp_model_file = "tokenizer.model" # 之前训练的中文词表模型 # 加载LLaMA原生词表 llama_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir) llama_spm = sp_pb2_model.ModelProto() llama_spm.ParseFromString(llama_tokenizer.sp_model.serialized_model_proto()) # 加载中文词表 chinese_sp_model = spm.SentencePieceProcessor() chinese_sp_model.Load(chinese_sp_model_file) chinese_spm = sp_pb2_model.ModelProto() chinese_spm.ParseFromString(chinese_sp_model.serialized_model_proto()) # 2. 查看合并前词表大小 print(f"LLaMA原生词表大小:{len(llama_tokenizer)}") print(f"中文词表大小:{len(chinese_sp_model.pieces)}") # 3. 合并词表(只添加LLaMA中没有的中文token) llama_token_set = set(p.piece for p in llama_spm.pieces) print(f"合并前LLaMA词表唯一token数:{len(llama_token_set)}") new_token_count = 0 for p in chinese_spm.pieces: token = p.piece if token not in llama_token_set: # 新增token,得分设为0(不影响原有分词) new_p = sp_pb2_model.ModelProto().SentencePiece() new_p.piece = token new_p.score = 0.0 llama_spm.pieces.append(new_p) new_token_count += 1 print(f"新增中文token数:{new_token_count}") print(f"合并后总词表大小:{len(llama_spm.pieces)}") # 4. 保存合并后的双语Tokenizer output_dir = './transformers_tokenizer/llama_chinese/' os.makedirs(output_dir, exist_ok=True) # 保存SentencePiece模型 with open(output_dir + 'tokenizer.model', 'wb') as f: f.write(llama_spm.SerializeToString()) # 保存为Transformers兼容的Tokenizer bilingual_tokenizer = LlamaTokenizer(vocab_file=output_dir + 'tokenizer.model') bilingual_tokenizer.save_pretrained(output_dir) print(f"双语Tokenizer已保存到 {output_dir}") # 5. 测试合并效果 original_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir) bilingual_tokenizer = LlamaTokenizer.from_pretrained(output_dir)'白日依山尽,黄河入海流。欲穷千里目,更上一层楼。 The primary use of LLaMA is research on large language models, including''' print("\n=== 分词效果对比 ===") print("LLaMA原生Tokenizer(中文乱码):") print(original_tokenizer.tokenize(test_text)) print("\n双语Tokenizer(中文正常分词):") print(bilingual_tokenizer.tokenize(test_text)) 关键说明
- 合并逻辑:只添加 LLaMA 原生词表中没有的中文 token,避免重复;
- 得分设置:新增中文 token 的得分设为 0.0,不影响英文文本的原有分词逻辑;
- 依赖:需要提前获取 LLaMA 原生的
tokenizer.model(可从 HuggingFace 下载开源复刻版本)。
运行结果
合并后词表大小为 “LLaMA 原生 32000 + 新增中文 token 约 49000”= 81000 左右,测试对比明显:
- 原生 LLaMA Tokenizer:中文被拆成 UTF-8 编码(如
<0xE4><0xBE><0x9D>); - 双语 Tokenizer:中文被正确分词(如
白日、千里、上一层),英文正常处理。
七、Step5:模型中使用新词表 —— 保留原有权重 + 初始化新词
合并好词表后,需要让 LLaMA 模型适配新词表才能使用。核心是 “调整 embedding 层大小” 和 “初始化新词的 embedding 权重”。
模型适配代码(保存为adapt_model_to_new_vocab.py)
python
运行
from transformers import AutoConfig, LlamaForCausalLM, LlamaTokenizer import torch.nn as nn # 加载合并后的双语Tokenizer和原始LLaMA模型 tokenizer_path = "./transformers_tokenizer/llama_chinese/" model_path = "path/to/original/llama-model/" # 原始LLaMA模型路径 # 加载Tokenizer和模型配置 tokenizer = LlamaTokenizer.from_pretrained(tokenizer_path) config = AutoConfig.from_pretrained(model_path) model = LlamaForCausalLM.from_pretrained(model_path, config=config) # 1. 查看原始模型embedding大小和新词表大小 original_vocab_size = model.get_output_embeddings().weight.size(0) new_vocab_size = len(tokenizer) print(f"原始模型词表大小:{original_vocab_size}") print(f"新词表大小:{new_vocab_size}") # 2. 调整embedding层大小(关键步骤) model.resize_token_embeddings(new_vocab_size) # 3. 初始化新增token的embedding权重 # 逻辑:如果新增token是中文词汇,用随机正态分布初始化;如果是特殊符号,设为0 def init_new_embeddings(model, original_vocab_size, new_vocab_size, initializer_range=0.02): embedding_layer = model.get_input_embeddings() output_embedding_layer = model.get_output_embeddings() # 对新增的token初始化 for i in range(original_vocab_size, new_vocab_size): # 随机正态分布初始化(和LLaMA原生初始化一致) embedding_layer.weight.data[i].normal_(mean=0.0, std=initializer_range) output_embedding_layer.weight.data[i].normal_(mean=0.0, std=initializer_range) # 如果有padding token,将其embedding设为0 if tokenizer.pad_token_id is not None and tokenizer.pad_token_id >= original_vocab_size: embedding_layer.weight.data[tokenizer.pad_token_id].zero_() output_embedding_layer.weight.data[tokenizer.pad_token_id].zero_() # 执行初始化 init_new_embeddings(model, original_vocab_size, new_vocab_size) # 4. 保存适配后的模型 adapted_model_path = "./adapted_llama_chinese/" model.save_pretrained(adapted_model_path) tokenizer.save_pretrained(adapted_model_path) print(f"适配新词表的模型已保存到 {adapted_model_path}") # 测试模型生成 inputs = tokenizer("白日依山尽,黄河入海流。", return_tensors="pt") outputs = model.generate(**inputs, max_new_tokens=50, temperature=0.7) print("\n模型生成结果:") print(tokenizer.decode(outputs[0], skip_special_tokens=True)) 关键步骤说明
resize_token_embeddings:调整模型输入 / 输出 embedding 层的大小,以适配新词表;- 权重初始化:新增 token 的 embedding 用正态分布初始化(和 LLaMA 原生逻辑一致),确保模型训练稳定;
- 保存模型:适配后的模型可直接用于微调或推理,同时支持中英文。
八、总结:构建中文 Tokenization 的完整流程
到这里,我们已经完成了从语料预处理到模型适配的全流程,核心步骤可以总结为:
- 语料清洗:过滤无效内容,提取纯中文句子;
- 词表训练:用 SentencePiece 训练中文 BPE 词表;
- Tokenizer 封装:适配 Transformers 框架,支持分词、编码 / 解码;
- 词表合并:融合 LLaMA 英文词表和中文词表,构建双语支持;
- 模型适配:调整 embedding 层大小,初始化新词权重。
通过这套流程,LLaMA 类英文大模型就能完美支持中文,分词更合理、生成效果更自然。如果想要进一步提升中文性能,还可以用中文语料对适配后的模型进行微调(如 LoRA 微调)。