Skip to content

英文大语言模型中文支持方案:构建中文 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 sentencepiece

3.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模型类型:unigrambpecharword
character_coverage覆盖字符集的比例,1.0 = 全部覆盖
unk_id未登录词 ID,默认 0
bos_id句子起始符 ID,默认 1
eos_id句子结束符 ID,默认 2
pad_id填充符 ID,默认 -1(不使用)

训练后生成 tokenizer.modeltokenizer.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 参数(不从头训练),需要三步:

  1. 找到新旧词表的 ID 映射关系
  2. 将模型中新词表包含的旧词用原始 embedding 替换
  3. **新词(旧词表不存在的)**按模型初始化规则进行赋值
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_()

参考实现: https://github.com/yangjianxin1/LLMPruner

7. 总结

构建中文 Tokenization 的完整流程:

步骤内容
1使用 SentencePiece 训练中文词表
2使用 Transformers 加载 SentencePiece 模型
3合并中英文词表,使用 Transformers 加载合并后的词表
4在模型中应用新词表(从头训练或保留原始 embedding)