外观
英文大语言模型中文支持方案:构建中文 Tokenization
内容整理自学习笔记,仅供面试备考参考;不构成录用、培训或考试承诺。
1. 为什么需要构建中文 Tokenization?
当前大语言模型以基于 LLaMA 家族的模型为主,但原始 LLaMA 的训练语料以英文为主,中文语料占比较少,直接导致对中文支持不友好。
核心问题: LLaMA 的中文 token 被切分为多个字节级别的碎片(如 <0xE4>, <0xBE>),编码效率极低。
本文以《斗破苍穹》小说为语料,介绍如何扩充 vocab 以改善中文 token 化。
2. 原始数据预处理
2.1 原始数据问题
原始文本包含大量换行和无效内容(如章节标记、空白行等):
===上架感言===
又一次上架了,这次比上次还激动...
尴尬,关于新书,上架前成绩好得有些出乎土豆的意料...2.2 预处理代码
python
# Step 1: 加载原始数据
with open("data/《斗破苍穹》.txt", "r", encoding="utf-8") as fp:
data = fp.read().strip().split("\n")
# Step 2: 清洗——过滤无效行,保留正文
sentences = []
for d in data:
d = d.strip()
if "===" in d or len(d) == 0 or d == "《斗破苍穹》来自:":
continue
sentences.append(d)
# Step 3: 写入预处理后的语料文件
with open("data/corpus.txt", "w", encoding="utf-8") as fp:
fp.write("\n".join(sentences))预处理后的 corpus.txt 只保留干净的中文正文内容。
3. 构建中文词库
3.1 SentencePiece 安装
bash
pip install sentencepiece3.2 训练中文词库
python
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",
)3.3 参数说明
| 参数 | 说明 |
|---|---|
input | 输入文本文件路径,每行可为一句话或多句话 |
model_prefix | 保存模型的名称前缀 |
vocab_size | 目标词表大小 |
user_defined_symbols | 用户自定义符号,作为完整 Token 不被拆分 |
model_type | 模型类型:unigram、bpe、char、word |
character_coverage | 覆盖字符集的比例,1.0 = 全部覆盖 |
unk_id | 未登录词 ID,默认 0 |
bos_id | 句子起始符 ID,默认 1 |
eos_id | 句子结束符 ID,默认 2 |
pad_id | 填充符 ID,默认 -1(不使用) |
训练后生成 tokenizer.model 和 tokenizer.vocab 两个文件。
词表示例:
<unk> 0
<s> 0
</s> 0
foo 0
bar 0
萧炎 -0
也是 -2
便是 -3
了一 -4
...4. 使用 Transformers 加载 SentencePiece 模型
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
chinese_sp_model_file = "sentencepisece_tokenizer/tokenizer.model"
# 加载 SentencePiece 模型
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())
# 保存为 Transformers 兼容格式
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())
tokenizer = ChineseTokenizer(vocab_file=output_dir + 'chinese.model')
tokenizer.save_pretrained(output_dir)说明:
ChineseTokenizer继承自PreTrainedTokenizer,内部使用 SentencePiece 的 API 实现 tokenization。
运行结果
Test text:
白日依山尽,黄河入海流。欲穷千里目,更上一层楼。
The primary use of LLaMA is research on large language models, including
Tokenized by Chinese tokenizer:
['▁', '白日', '依', '山', '尽', ',', '黄', '河', '入', '海', '流', '。',
'欲', '穷', '千里', '目', ',', '更', '上一层', '楼', '。',
'▁', 'T', 'h', 'e', '▁', 'p', 'r', 'i', 'm', 'a', 'r', 'y', ...]5. 合并英文词表和中文词表
5.1 核心逻辑
遍历中文词表中的所有 token,如果在英文词表中不存在,则将其添加到合并后的词表中:
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
# 加载原始 LLaMA 词表和中文词表
llama_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir)
chinese_sp_model = spm.SentencePieceProcessor()
chinese_sp_model.Load(chinese_sp_model_file)
llama_spm = sp_pb2_model.ModelProto()
llama_spm.ParseFromString(llama_tokenizer.sp_model.serialized_model_proto())
chinese_spm = sp_pb2_model.ModelProto()
chinese_spm.ParseFromString(chinese_sp_model.serialized_model_proto())
print(f"原始词表: {len(llama_tokenizer)} tokens")
print(f"中文词表: {len(chinese_sp_model)} tokens")
# 输出: 原始词表: 32000 tokens, 中文词表: 50000 tokens
# 核心:将中文新词加入 LLaMA 词表
llama_spm_tokens_set = set(p.piece for p in llama_spm.pieces)
print(f"Before: {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"After: {len(llama_spm.pieces)}")
# 输出: Before: 32000, After: 81163
# 保存合并后的词表
output_dir = 'transformers_tokenizer/llama_chinese'
os.makedirs(output_dir, exist_ok=True)
with open(output_dir + '/chinese_llama.model', 'wb') as f:
f.write(llama_spm.SerializeToString())
tokenizer = LlamaTokenizer(vocab_file=output_dir + '/chinese_llama.model')
tokenizer.save_pretrained(output_dir)5.2 合并效果对比
| Tokenizer | 中文分词效果 |
|---|---|
| LLaMA 原始 | ['▁', '白', '日', '<0xE4>', '<0xBE>', '<0x9D>', '山', ...] — 汉字被拆为字节碎片 |
| LLaMA + 中文词表 | ['▁白', '日', '依', '山', '尽', ',', '黄', '河', ...] — 中文按词切分 |
关键结论: 合并后词表从 32,000 增至 81,163,中文分词质量显著提升。
6. 如何在模型中使用新词表?
6.1 从头训练(最简单)
python
config = AutoConfig.from_pretrained(...)
tokenizer = LlamaTokenizer.from_pretrained(...)
model = LlamaForCausalLM.from_pretrained(..., config=config)
model.resize_token_embeddings(len(tokenizer))6.2 保留原始 Embedding 参数
如果需要保留原始模型的 embedding 参数(不从头训练),需要三步:
- 找到新旧词表的 ID 映射关系
- 将模型中新词表包含的旧词用原始 embedding 替换
- **新词(旧词表不存在的)**按模型初始化规则进行赋值
python
# LLaMA 的 embedding 初始化方式
def _init_weights(self, module):
std = self.config.initializer_range
if 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_()7. 总结
构建中文 Tokenization 的完整流程:
| 步骤 | 内容 |
|---|---|
| 1 | 使用 SentencePiece 训练中文词表 |
| 2 | 使用 Transformers 加载 SentencePiece 模型 |
| 3 | 合并中英文词表,使用 Transformers 加载合并后的词表 |
| 4 | 在模型中应用新词表(从头训练或保留原始 embedding) |