LLaMA-2 7B 中文词表扩充实战:新增 17953 个 Token 后 PPL 降低 15%
1. 为什么需要中文词表扩充
当我们将英文基座大模型(如LLaMA-2)适配到中文场景时,原始的词表(Vocabulary)设计往往成为第一个需要突破的瓶颈。英文为主的模型通常采用BPE(Byte Pair Encoding)分词算法,这种算法在处理中文时存在几个显著问题:
- 汉字被拆分为字节级单元:一个中文字符可能被拆分为3个独立的token,导致序列长度膨胀
- 语义单元割裂:像"人工智能"这样的专业术语会被拆分为单字,丢失整体语义
- 计算效率低下:更长的序列意味着更高的计算成本和更差的推理速度
我们通过对比实验发现,在相同的中文语料上:
- 原始LLaMA-2词表平均每个中文字符消耗1.8个token
- 扩充后的词表降至1.1个token/字
- 序列长度减少约40%
2. 词表扩充技术方案
2.1 整体流程设计
词表扩充的核心流程可分为四个关键步骤:
graph TD A[中文语料准备] --> B[新词表训练] B --> C[词表合并] C --> D[嵌入层初始化]2.2 中文语料选择
优质的中文语料是训练有效词表的基础。我们推荐以下数据源组合:
| 语料类型 | 推荐比例 | 示例来源 | 特点 |
|---|---|---|---|
| 通用网络文本 | 40% | Common Crawl中文部分 | 覆盖日常用语和网络表达 |
| 专业领域文本 | 30% | 学术论文、技术文档 | 包含专业术语和领域词汇 |
| 高质量书籍 | 20% | 开源电子书 | 语言规范,句式丰富 |
| 代码注释 | 10% | GitHub中文项目 | 包含技术术语和代码相关表达 |
2.3 使用SentencePiece训练新词表
我们采用SentencePiece工具训练中文子词词表,关键参数配置如下:
import sentencepiece as spm spm.SentencePieceTrainer.train( input='corpus.txt', model_prefix='chinese_sp', vocab_size=20000, # 目标词表大小 character_coverage=0.9995, # 字符覆盖度 model_type='bpe', # 使用BPE算法 max_sentence_length=16384, # 最大句子长度 pad_id=0, unk_id=1, bos_id=2, eos_id=3, user_defined_symbols=['<|im_start|>', '<|im_end|>'] # 添加特殊token )训练完成后,我们需要分析生成的词表:
- 检查高频词是否被合理合并
- 确保专业术语保持完整
- 验证生僻字是否被正确处理
3. 词表合并实战
3.1 合并算法实现
原始LLaMA-2词表与中文词表的合并需要特殊处理,以下是关键代码片段:
def merge_vocab(original_vocab, new_vocab, reserved_tokens=100): """合并两个词表,保留原始词表的前N个token""" # 保留原始词表的基础token merged = original_vocab[:reserved_tokens] # 添加新词表中不重复的token existing_tokens = set(original_vocab) for token in new_vocab: if token not in existing_tokens: merged.append(token) existing_tokens.add(token) return merged注意:实际操作中需要处理更复杂的边界情况,如特殊token的冲突处理、字节级token的兼容性等
3.2 嵌入层初始化策略
新增token的嵌入向量初始化直接影响模型性能,我们对比了三种方法:
| 初始化方法 | PPL降低幅度 | 训练稳定性 | 适用场景 |
|---|---|---|---|
| 随机初始化 | 8-10% | 较差 | 小规模扩充(<5k token) |
| 相近语义字平均 | 12-14% | 良好 | 中等规模扩充 |
| 聚类中心初始化 | 14-16% | 优秀 | 大规模扩充(>10k token) |
推荐使用聚类中心初始化方法:
def init_embeddings(original_emb, new_tokens, tokenizer): # 使用k-means在原始嵌入空间寻找聚类中心 from sklearn.cluster import KMeans kmeans = KMeans(n_clusters=len(new_tokens)) kmeans.fit(original_emb) # 将新token分配到最近的聚类中心 new_emb = kmeans.cluster_centers_ return new_emb4. 完整实现与性能对比
4.1 端到端实现脚本
我们提供完整的Python实现脚本,包含以下功能:
- 中文语料预处理
- SentencePiece词表训练
- 词表合并与冲突解决
- 嵌入层初始化
- 模型保存与测试
# 示例:主流程控制 def main(): # 1. 训练中文词表 train_chinese_spm('zh_corpus/*.txt') # 2. 加载原始LLaMA tokenizer orig_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b") # 3. 合并词表 merged_vocab = merge_vocabs(orig_tokenizer, "chinese_sp.model") # 4. 初始化新token的嵌入 model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b") init_new_embeddings(model, merged_vocab.new_tokens) # 5. 保存扩充后的模型 save_expanded_model(model, merged_vocab, "llama2-7b-zh-expanded")4.2 性能对比数据
我们在相同的中文测试集(100万字)上对比了扩充前后的性能差异:
| 指标 | 原始词表 | 扩充词表 | 提升幅度 |
|---|---|---|---|
| 困惑度(PPL) | 45.2 | 38.4 | 15.0% |
| 推理速度(tokens/s) | 128 | 185 | +44.5% |
| 显存占用(GB) | 12.8 | 13.1 | +2.3% |
| 序列平均长度 | 1850 | 1120 | -39.5% |
5. 实际应用建议
5.1 词表大小权衡
根据我们的实践经验,推荐以下词表扩充策略:
- 通用场景:新增15k-20k中文token
- 专业领域:新增5k专业token + 10k通用token
- 多语言支持:每种语言新增10k-15k token
提示:词表并非越大越好,过大的词表会导致嵌入层参数膨胀,增加计算开销
5.2 后续训练策略
词表扩充后建议采用两阶段训练:
嵌入层微调阶段:
- 冻结所有参数,只训练新token的嵌入
- 使用较高学习率(1e-4)
- 约1-2个epoch
全参数微调阶段:
- 解冻所有参数
- 使用较低学习率(5e-6)
- 3-5个epoch
5.3 常见问题解决
问题1:扩充后模型输出乱码
- 检查词表合并时是否保留了原始字节级token
- 验证新token的嵌入初始化是否合理
问题2:训练loss波动大
- 降低初始学习率
- 增加warmup步数
- 检查语料质量
问题3:推理速度提升不明显
- 确认实际输入序列是否缩短
- 检查是否启用了正确的tokenizer版本