大语言模型词表裁剪的实现思路与代码
在多语言 LLM 里,词表通常比实际业务需要大得多。模型为了覆盖更多语言,会带上大量目标场景根本用不上的 token。若你的任务只落在中文和英文上,继续背着整套词表跑推理和存储,浪费很明显。词表裁剪就是把这部分多余成本拿掉,做法并不复杂,关键是别把特殊 token、映射关系和权重对齐搞乱。
词表裁剪到底在改什么
词表大小直接影响两块参数:Embedding 层和 LM Head。原词表越大,这两层越重。裁剪时,不是重新训练一套模型,而是保留新词表对应的 token,把这些 token 在原模型里的权重一行行拷出来,再把配置里的 vocab_size 改掉。
流程其实很直白:先确认新词表是原词表的子集,再建立 new_token_id -> old_token_id 的映射,接着复制 Embedding 和 LM Head 的权重,最后保存新模型和 tokenizer。真正容易出问题的地方,通常不是复制权重,而是 token 对不上,或者裁完以后 tokenizer 和模型配置不同步。
准备工作
这里需要两样东西:
- 原模型,比如
bigscience/bloom-560m - 新 tokenizer,比如只保留目标语言 token 的
tokenizer.json或vocab.txt
新词表必须是原词表的子集。这条很硬,不能绕。如果新 tokenizer 里出现原模型没有的 token,后面查表时就会直接报错。
代码实现
下面这段代码基于 Hugging Face Transformers。思路是先写一个通用裁剪器,再针对 Bloom 做一次具体适配。
基础裁剪类
import os
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from tqdm import tqdm
class VocabularyPruner(object):
def check(self, old_model_name_or_path, new_model_name_or_path, text):
"""
检查模型裁剪后,生成结果是否一致
"""
max_length = 20
# 使用老模型对文本编码
old_model = AutoModelForCausalLM.from_pretrained(old_model_name_or_path)
old_tokenizer = AutoTokenizer.from_pretrained(old_model_name_or_path)
old_input_ids = old_tokenizer(text, return_tensors='pt').input_ids
old_output = old_model.generate(old_input_ids, max_length=max_length)
old_output_text = old_tokenizer.batch_decode(old_output)
print('old_output:{}'.format(old_output_text))
# 使用新模型对文本编码
new_model = AutoModelForCausalLM.from_pretrained(new_model_name_or_path)
new_tokenizer = AutoTokenizer.from_pretrained(new_model_name_or_path)
new_input_ids = new_tokenizer(text, return_tensors=).input_ids
new_output = new_model.generate(new_input_ids, max_length=max_length)
new_output_text = new_tokenizer.batch_decode(new_output)
(.(new_output_text))
old_output_text == new_output_text:
()
:
()
():
NotImplementedError
():
os.path.exists(save_path):
os.makedirs(save_path)
new_tokenizer = AutoTokenizer.from_pretrained(new_tokenizer_name_or_path)
old_tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
old_vocab = old_tokenizer.vocab
new_vocab = new_tokenizer.vocab
token tqdm(new_vocab.keys()):
token old_vocab:
Exception(.(token))
()
new2old_token_id = {}
token, token_id tqdm(new_vocab.items()):
old_token_id = old_vocab[token]
new2old_token_id[token_id] = old_token_id
model = AutoModelForCausalLM.from_pretrained(model_name_or_path, torch_dtype=)
old_params = (p.numel() p model.parameters())
( % (old_params / ))
vocab_size = (new_tokenizer)
hidden_size = model.config.hidden_size
new_embeds = torch.nn.Embedding(vocab_size, hidden_size, dtype=model.dtype)
new_lm_head = torch.nn.Linear(in_features=hidden_size, out_features=vocab_size, bias=, dtype=model.dtype)
.update_embeddings(model, new2old_token_id, new_embeds, new_lm_head)
model.config.__dict__[] = vocab_size
new_name_or_path :
model.config.__dict__[] = new_name_or_path
new_params = (p.numel() p model.parameters())
( % (new_params / ))
(.(((new_tokenizer) / (old_tokenizer), )*))
(.((new_params / old_params, )*))
model.save_pretrained(save_path)
new_tokenizer.save_pretrained(save_path)


