如何让英文大语言模型支持中文:构建自定义 Tokenization
Part1 前言
目前,大语言模型(LLM)呈爆发式增长,其中基于 LLaMA 家族的模型占据了半壁江山。然而,原始的 LLaMA 模型主要基于英文语料训练,对中文的支持并不友好,直接处理中文文本时往往会出现分词破碎、语义理解偏差等问题。为了解决这一问题,我们需要扩充原始词表(Vocabulary),使其包含中文字符或词汇,从而实现对中文的有效 Tokenization。
本文将详细讲解如何扩充词表以支持中文 Token 化,涵盖数据预处理、SentencePiece 训练、Transformers 库加载、中英文词表合并以及模型 Embedding 层适配等完整流程。
Part2 数据预处理
高质量的语料是训练高质量词表的基础。我们通常选择具有代表性的中文文本进行预处理。这里我们以《斗破苍穹》小说语料为例,每一行代表一句或多句话。
在预处理阶段,主要目标是清洗噪声数据,确保输入给训练器的文本干净且格式规范。具体步骤包括去除特殊标记、空行以及无关的元数据信息。
import os
def preprocess_corpus(input_path, output_path):
"""
预处理中文语料文件
:param input_path: 输入文件路径
:param output_path: 输出文件路径
"""
if not os.path.exists(input_path):
raise FileNotFoundError(f"Input file {input_path} not found")
with open(input_path, "r", encoding="utf-8") as fp:
data = fp.read().strip().split("\n")
sentences = []
for d in data:
d = d.strip()
if "===" in d or len(d) == 0 or d.startswith("《斗破苍穹》来自:"):
continue
sentences.append(d)
with open(output_path, "w", encoding="utf-8") as fp:
fp.write("\n".join(sentences))
preprocess_corpus("data/《斗破苍穹》.txt", "data/corpus.txt")
最终得到 corpus.txt,该文件将作为 SentencePiece 的训练输入。在实际生产中,建议收集更多样化的中文语料(如新闻、百科、技术文档等),以提高词表的泛化能力。
Part3 SentencePiece 词表训练
SentencePiece 是目前主流的子词分词算法实现之一,它不依赖空格分词,能够很好地处理多语言混合场景。安装指令如下:
pip install sentencepiece
准备好语料后,使用 spm.SentencePieceTrainer.train 进行训练。以下是核心代码示例及参数详解:
import sentencepiece as spm
spm.SentencePieceTrainer.train(
input='data/corpus.txt',
model_prefix='tokenizer',
vocab_size=50000,
user_defined_symbols=['foo', 'bar'],
character_coverage=1.0,
model_type="bpe",
unk_id=0,
bos_id=1,
eos_id=2,
pad_id=-1
)
参数详解:
- input: 指定输入文本文件的路径或目录。可以指定多个文件或目录,每行可包含一句话或多句话。
- model_prefix: 保存模型的名称前缀。运行后会生成
tokenizer.model 和 tokenizer.vocab 两个文件。
- vocab_size: 设置的词表大小。对于中文任务,通常建议在 30000 到 60000 之间,过大会增加显存占用,过小则会导致 OOV(未登录词)过多。
- user_defined_symbols: 用于指定用户自定义的符号。这些符号将被视为单独的 Token,不会被拆分成子词。例如,我们可以加入一些特定的控制符或特殊词汇。
- character_coverage: 指定覆盖字符的数量,限制字符集的大小。默认值为 1.0,即覆盖全部字符。对于生僻字较多的场景,可适当调低此值以优先保留高频字。
- model_type: 指定模型的类型,可选
unigram, bpe, char, word。本例使用 bpe (Byte-Pair Encoding),适合处理长文本和未知词。
- unk_id / bos_id / eos_id / pad_id: 分别指定未登录词、句子开头、句子结束和填充符号的 ID。注意
pad_id 默认为 -1 表示不使用填充符号。
运行后会得到 tokenizer.model 和 tokenizer.vocab 文件。查看 tokenizer.vocab 内容,可以看到特殊符号(如 <unk>, <s>, </s>)、自定义符号(如 foo, bar)以及 BPE 训练得到的常用词(如 萧炎, 也是)。
Part4 使用 Transformers 库加载 SentencePiece 模型
为了在 Hugging Face 生态中使用训练好的 SentencePiece 模型,我们需要将其封装为 PreTrainedTokenizer 类。这允许我们直接使用 transformers 库的标准接口。
首先,需要加载模型并转换为 Hugging Face 兼容的格式:
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
chinese_sp_model_file = "sentencepiece_tokenizer/tokenizer.model"
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())
output_dir = './transformers_tokenizer/chinese/'
os.makedirs(output_dir, exist_ok=True)
with open(output_dir + 'chinese.model', 'wb') as f:
f.write(chinese_spm.SerializeToString())
from tokenization import ChineseTokenizer
tokenizer = ChineseTokenizer(vocab_file=output_dir + 'chinese.model')
tokenizer.save_pretrained(output_dir)
print(f"Chinese tokenizer has been saved to {output_dir}")
chinese_tokenizer = ChineseTokenizer.from_pretrained(output_dir)
print("Special Tokens:", chinese_tokenizer.all_special_tokens)
print("Special IDs:", chinese_tokenizer.all_special_ids)
text = '''白日依山尽,黄河入海流。欲穷千里目,更上一层楼。
The primary use of LLaMA is research on large language models, including'''
print("Test text:\n", text)
print(f"Tokenized by Chinese-LLaMA tokenizer:{chinese_tokenizer.tokenize(text)}")
ChineseTokenizer 类实现逻辑:
该类继承自 PreTrainedTokenizer,核心在于重写 _tokenize, _convert_token_to_id, _convert_id_to_token 等方法,使其调用底层的 sentencepiece 处理器。这样既保留了 SentencePiece 的分词能力,又兼容了 Transformers 的 API 标准。
关键方法说明:
_tokenize: 调用 self.sp_model.encode(text, out_type=str) 进行编码。
_convert_token_to_id: 调用 self.sp_model.piece_to_id(token) 获取 ID。
convert_tokens_to_string: 负责将 Token 序列解码回字符串,需处理特殊 Token 与子词的拼接逻辑。
Part5 合并英文词表和中文词表
为了让英文基座模型支持中文,最直接的方法是将中文词表中的新 Token 追加到英文词表中。这需要修改 SentencePiece 的模型结构。
import os
from transformers import LlamaTokenizer
from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model
import sentencepiece as spm
llama_tokenizer_dir = "transformers_tokenizer/llama/tokenizer.model"
chinese_sp_model_file = "sentencepiece_tokenizer/tokenizer.model"
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())
print(f"Original Vocab Size: {len(llama_tokenizer)}")
print(f"Chinese Vocab Size: {len(chinese_sp_model)}")
llama_spm_tokens_set = set(p.piece for p in llama_spm.pieces)
print(f"Before merging: {len(llama_spm_tokens_set)}")
for p in chinese_spm.pieces:
piece = p.piece
if piece not in llama_spm_tokens_set:
new_p = sp_pb2_model.ModelProto().SentencePiece()
new_p.piece = piece
new_p.score = 0
llama_spm.pieces.append(new_p)
print(f"New model pieces: {(llama_spm.pieces)}")
output_sp_dir =
os.makedirs(output_sp_dir, exist_ok=)
(output_sp_dir + , ) f:
f.write(llama_spm.SerializeToString())
output_hf_dir =
tokenizer = LlamaTokenizer(vocab_file=output_sp_dir + )
tokenizer.save_pretrained(output_hf_dir)
()
chinese_llama_tokenizer = LlamaTokenizer.from_pretrained(output_hf_dir)
text =
(, llama_tokenizer.tokenize(text)[:])
(, chinese_llama_tokenizer.tokenize(text)[:])
合并策略分析:
- 集合去重:通过
set 快速判断中文 Token 是否已存在于英文词表中,避免重复添加。
- Score 重置:新加入的 Token 没有经过统计训练,其 Score 设置为 0,不影响原有 Token 的概率分布。
- ID 映射:新增的 Token 会自动分配新的 ID,从原词表大小开始递增。
通过这种方式,原始 LLaMA 模型原有的 32000 个 Token 得以保留,同时增加了约 50000 个中文相关 Token,总词表规模扩大至 8 万左右。测试结果显示,中文诗句现在可以被更完整地切分为有意义的词汇(如 白日, 千里),而非被拆分为单个汉字或字节。
Part6 如何在模型中使用修改后的词表
词表合并完成后,我们需要调整模型结构以适配新的词表大小。主要有两种情况:从头训练和使用预训练权重。
6.1 重新初始化 Embedding
如果不需要保留原始模型的参数,可以直接重新初始化:
from transformers import AutoConfig, LlamaForCausalLM, LlamaTokenizer
config = AutoConfig.from_pretrained("path/to/original_llama")
tokenizer = LlamaTokenizer.from_pretrained("path/to/merged_tokenizer")
model = LlamaForCausalLM.from_pretrained("path/to/original_llama", config=config)
model_vocab_size = model.get_output_embeddings().weight.size(0)
model.resize_token_embeddings(len(tokenizer))
6.2 保留原始 Embedding 参数
如果需要保留原始英文参数的知识,仅对新加入的中文 Token 进行随机初始化,则需要手动处理权重映射:
- 建立映射关系:找到新词表和旧词表 ID 之间的对应关系。旧词表的 ID 保持不变,新词表的 ID 从原词表大小开始。
- 复制权重:将模型内部新词表包含的旧词表部分用原始模型的 Embedding 替换。
- 初始化新权重:对于新加入的中文 Token,使用高斯分布进行随机初始化。
参考 Transformers 库中的权重初始化逻辑:
import torch.nn as nn
def _init_weights(self, module):
std = self.config.initializer_range
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=std)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.Embedding):
module.weight.data.normal_(mean=0.0, std=std)
if module.padding_idx is not None:
module.weight.data[module.padding_idx].zero_()
在实际操作中,可以使用 model.resize_token_embeddings 后,遍历新增的 Embedding 向量并应用上述初始化函数。对于更复杂的迁移学习场景,可以参考 LLMPruner 项目中的权重扩展策略。
Part7 总结
本文系统讲解了如何让英文大语言模型支持中文的技术路径,主要涵盖了以下关键点:
- 数据准备:使用 SentencePiece 训练中文词表,需注意语料质量和清洗。
- 模型加载:通过自定义
PreTrainedTokenizer 子类,将 SentencePiece 模型集成到 Transformers 框架中。
- 词表合并:利用 SentencePiece 协议缓冲区(Protobuf)结构,将中文 Token 追加至英文词表,保持原有结构不变。
- 模型适配:通过
resize_token_embeddings 调整模型维度,并对新增权重进行合理初始化。
通过上述步骤,我们可以低成本地赋予英文基座模型中文处理能力,为后续的中英双语微调奠定基础。
Part8 最佳实践与注意事项
在实际应用中,除了完成上述流程外,还需注意以下几点以确保模型效果:
- 词表平衡:尽量保证中英文 Token 数量的比例协调。如果中文 Token 占比过高,可能会稀释英文单词的表示能力;反之则可能导致中文分词过细。建议通过统计语料频率来动态调整
vocab_size。
- 特殊 Token 管理:合并词表时,务必检查特殊 Token(如
<bos>, <eos>, <pad>)的 ID 是否冲突。通常建议保留原始词表的特殊 Token ID,仅在末尾追加新 Token。
- 评估指标:在正式微调前,应计算困惑度(Perplexity)或 BLEU 分数,评估新词表对中文文本的压缩率和语义保留程度。
- 显存优化:词表扩大意味着 Embedding 层参数量增加。若显存受限,可考虑使用量化技术(如 INT8/FP4)或 LoRA 微调策略。
参考资料