1. 项目缘起当大模型遇上“生僻字”与“黑话”最近在做一个医疗领域的智能问答项目用的是一个主流的开源大模型。测试时我输入了一个药品的化学名“盐酸帕罗西汀”模型回复得头头是道。但当我换成一个更专业的、描述某种罕见病理特征的医学术语时模型的回答就开始“胡言乱语”要么是答非所问要么干脆把这个术语拆成几个常见字来理解完全丢失了其作为一个整体概念的含义。这让我意识到一个普遍存在但容易被忽视的问题我们手里这些动辄千亿参数、看似无所不能的大模型其“词汇量”可能远没有我们想象的那么丰富。这背后的核心就是“词表”Vocabulary。你可以把它理解成大模型的一本“内置词典”。模型在训练前会从海量语料中统计出高频出现的字、词或子词Subword形成一个固定大小的词表。模型认识的所有“单词”都出自这里。对于英文主流模型如GPT系列通常采用基于字节对编码BPE的子词切分能较好地处理未登录词。但对于中文情况更复杂。大多数开源大模型如LLaMA系列的中文衍生版的词表是在原始英文词表基础上通过添加大量中文常用汉字和词语扩展而来。这就导致了两个典型瓶颈第一中文生僻字缺失。像“㙓”、“䶮”这类字在通用语料中出现的频率极低几乎不可能被收入词表。当模型遇到它们时只能退而求其次使用一个特殊的UNK未知标记来代替或者错误地将其拆解。这意味着模型从根本上就无法“看见”这个字的完整信息后续的理解和生成自然无从谈起。第二领域专业术语“失准”。像“区块链中的零知识证明”、“生物信息学里的BLAST比对”、“机械设计里的有限元分析”这些在垂直领域内司空见惯的复合词或缩写在通用词表中可能不存在。更糟糕的是模型可能会用BPE算法将其切分成令人啼笑皆非的片段。例如“零知识证明”可能被切分成“零”、“知”、“识证”、“明”完全破坏了术语的语义完整性。此时即使每个子词都有嵌入向量组合起来的语义也与原术语相去甚远。因此“词表扩展”不是一个锦上添花的功能而是让大模型真正深入专业领域、理解特定场景语言的“敲门砖”。本项目要探讨的就是如何为现有的大模型“扩容词典”特别是针对中文生僻字和专业术语设计一套可行的词嵌入适配方案。这不是简单地往列表里加几个词而是涉及词表管理、嵌入向量初始化、模型微调适配等一系列环环相扣的技术决策。2. 核心挑战为什么不能直接“硬塞”新词直觉上给模型增加新词似乎就是修改一下词表文件然后告诉模型“这几个新词你记一下。”但实际操作起来远非如此简单。这里面的核心挑战直接关系到扩展的成败。2.1 模型架构的刚性约束现代大模型Transformer架构的输入层和输出层其维度与词表大小是严格绑定的。假设原模型词表大小为V词嵌入维度为d那么输入嵌入矩阵Input Embedding的形状就是[V, d]输出层的投影矩阵Output Projection通常与之共享权重或形状相同。当我们把词表扩大到VV V时我们必须同步调整这两个矩阵的大小为新词分配对应的d维向量。问题来了新词的向量从哪里来如果我们随机初始化那么这个新词对于模型来说就是一个彻头彻尾的“陌生人”它与已有词汇的语义关系是混乱的模型需要重新学习这需要大量的后续训练数据。如果我们用某个已有词的向量来初始化又可能引入意想不到的语义偏差。2.2 子词切分与语义完整性冲突对于专业术语另一个棘手问题是子词切分算法如BPE、WordPiece的介入。以“有限元分析”为例在一个未针对工程领域优化的通用词表中它很可能被切分为“有”、“限元”、“分析”。其中“限元”这个片段本身没有意义这就导致了“词汇碎片化”。即使我们成功将“有限元分析”作为一个整体加入词表模型在预处理阶段是否会对齐使用这个新词还是继续沿用旧的切分方式这需要修改模型的tokenizer分词器逻辑。2.3 嵌入向量的“冷启动”问题这是最关键的挑战。词嵌入向量不是孤立的它是在数十亿甚至数万亿的文本数据中通过上下文共现关系学习到的。一个新词加入后它的向量初始状态与整个语义空间是脱节的。例如我们为生僻字“㙓”随机初始化一个向量。在模型看来这个向量与表示“土”、“山”、“地”等相关字的向量之间没有任何预设的语义关联。模型必须通过后续的训练在大量的上下文例句中逐渐将“㙓”的向量“推”到语义空间中合适的位置靠近其他表示地形、地貌的字词。这个过程需要高质量、足量的包含该新词的训练语料否则扩展就是无效的。注意直接使用预训练好的、基于大规模语料训练的中文字向量如腾讯词向量、中文BERT字向量来初始化生僻字是一个不错的起点。因为这至少赋予了新字一个符合统计规律的“先验”语义位置远比随机初始化强。2.4 扩展的规模与效率权衡词表不是越大越好。每增加一个词模型参数就会增加d个通常d4096或更大。无节制地扩展会显著增加模型体积影响推理速度。更重要的是过大的词表可能会稀释注意力机制的效果因为模型需要在更多的候选词中进行选择。因此扩展必须有针对性聚焦于目标领域的高频、关键术语。3. 实战方案四步走实现词表与嵌入的平滑扩展基于上述挑战一个稳健的词表扩展方案需要系统性的步骤。下面我结合一个为法律领域大模型扩展专业术语的例子拆解整个流程。3.1 第一步领域语料挖掘与新词候选集生成目标从你的专业领域数据中自动找出那些“词表里没有但很重要”的词。操作流程收集原始语料汇集尽可能多的领域文本如法律条文、判决书、学术论文、行业报告等。清洗后假设我们得到10GB的法律文本。利用现有分词器进行对比分析# 伪代码示例 original_tokenizer AutoTokenizer.from_pretrained(“your/base/model”) text “本案涉及不当得利请求权与物权返还请求权的竞合问题。” tokens original_tokenizer.tokenize(text) # 输出可能为[‘本’, ‘案’, ‘涉’, ‘及’, ‘不’, ‘当’, ‘得’, ‘利’, ‘请’, ‘求’, ‘权’, ‘与’, ‘物’, ‘权’, ‘返’, ‘还’, ‘请’, ‘求’, ‘权’, ‘的’, ‘竞’, ‘合’, ‘问’, ‘题’, ‘。’]你会发现“不当得利”、“请求权”、“物权”、“竞合”这些法律核心概念都被拆碎了。采用统计方法提取候选词高频n-gram统计语料中连续出现的2-4字组合的频率。像“不当得利”这样的四字组合在法律文本中频率会远高于其在通用文本中的随机组合概率。点互信息PMI计算字与字之间的结合紧密度。PMI值高的组合更可能是一个有意义的复合词。例如“得”和“利”单独出现很常见但“得利”与“不当”一起出现的概率远高于它们随机共现的概率这就能识别出“不当得利”。结合领域词典如果有现成的法律术语词典这是最直接、最准确的来源。实操心得不要完全依赖自动化统计。自动挖掘出的候选词需要人工审核剔除“的的”、“之一”这类无意义高频组合并确认“第三人撤销之诉”这样的长术语是否应该作为一个整体加入。这一步的质量直接决定了扩展的精准度。3.2 第二步分词器Tokenizer的适配与更新目标让模型的分词器认识我们新增的词确保输入时术语能被作为一个整体处理。操作流程获取基础分词器加载你基座模型如ChatGLM、Qwen、LLaMA的中文版的分词器。它通常基于transformers库的PreTrainedTokenizer类。添加新词大多数分词器都提供了add_tokens方法。new_tokens [“不当得利”, “请求权竞合”, “善意取得”, “表见代理”] # 你的候选词列表 num_added tokenizer.add_tokens(new_tokens) print(f”Added {num_added} new tokens”)对于基于BPE的分词器如LLaMA的这个方法会在词表末尾新增这些词并为其分配新的token id。对于基于字的分词器可能需要确保新增词中的每个字本身已在词表中。验证分词效果text “在善意取得制度中原权利人可向无权处分人主张损害赔偿。” new_tokens tokenizer.tokenize(text) print(new_tokens) # 期望输出应包含 ‘善意取得’ 作为一个整体token而不是 ‘善’, ‘意’, ‘取’, ‘得’关键点对于生僻字如“㙓”如果分词器是基于字的你需要确保这个字被加入如果是基于子词的你可能需要将这个字作为一个独立的“词”加入。同时要更新分词器的特殊标记、词汇表文件等。3.3 第三步模型嵌入层扩容与向量初始化目标同步扩大模型的嵌入矩阵并为新词赋予一个合理的初始向量值。操作流程获取模型并调整嵌入层model AutoModelForCausalLM.from_pretrained(“your/base/model”) # 获取模型原始的嵌入层 embedding_layer model.get_input_embeddings() # 获取当前词表大小和嵌入维度 old_vocab_size, embedding_dim embedding_layer.weight.shape # 计算新词表大小 new_vocab_size old_vocab_size num_added # 创建一个新的嵌入矩阵 new_embeddings torch.nn.Embedding(new_vocab_size, embedding_dim) # 将旧矩阵的参数复制过来 new_embeddings.weight.data[:old_vocab_size] embedding_layer.weight.data.clone()初始化新词向量核心环节方案A均值初始化适用于复合术语。将术语中各个子词或单字的嵌入向量取平均作为新词的初始向量。for new_token in new_tokens: # 获取新词中每个字的token id假设分词器已能正确切分单字 char_ids tokenizer.convert_tokens_to_ids(list(new_token)) # 如“不当得利” - [‘不’ ‘当’ ‘得’ ‘利’]的id if char_ids: char_vectors embedding_layer.weight.data[char_ids] # 取出这些字的向量 mean_vector char_vectors.mean(dim0) # 计算均值 new_token_id tokenizer.convert_tokens_to_ids(new_token) new_embeddings.weight.data[new_token_id] mean_vector为什么这么做“不当得利”的语义可以粗略地看作是“不”、“当”、“得”、“利”四个字语义的合成。均值初始化提供了一个符合直觉的、相对平滑的起点比随机初始化更稳定。方案B外部嵌入初始化适用于生僻字或已有高质量向量的词。从训练好的中文词向量模型如Tencent AI Lab Embedding中查找新词的预训练向量。如果找到直接使用如果生僻字没有可以用字形、字义相近的字的向量来近似。方案C零初始化或小随机数初始化。这是最朴素的方法但要求后续有足够强的微调数据让模型能从零开始学习这些新词的语义。更新模型的嵌入层model.set_input_embeddings(new_embeddings) # 通常语言模型头lm_head与输入嵌入层共享权重所以也需要同步调整 if model.get_output_embeddings() is not None: model.set_output_embeddings(new_embeddings) # 告诉模型词表大小已变更 model.config.vocab_size new_vocab_size踩坑记录务必确保分词器新增词的顺序与模型嵌入层中新向量行的顺序完全一致。即第old_vocab_size i个新词其token id必须对应嵌入矩阵的第old_vocab_size i行。否则会导致词向量错乱模型输出乱码。3.4 第四步针对性微调与评估目标让模型真正“学会”使用这些新词理解它们在具体上下文中的含义和用法。操作流程准备微调数据这是最关键的一步。你需要构建一个包含大量新词出现的高质量文本数据集。数据格式可以是完形填空/掩码预测构造句子将新词作为待预测的部分。“在民法中因他人没有法律根据取得不当利益受损失的人有权请求其返还不当利益这被称为 [MASK] 制度。”标签“不当得利”因果续写给出包含新词的上下文让模型续写。“原告主张被告的行为构成不当得利因此请求法院判令被告……”领域问答对直接使用你业务场景中的问答数据确保问题和答案中自然包含专业术语。冻结大部分参数聚焦嵌入层由于我们只改变了词表的一小部分一种高效的策略是在微调初期只训练新添加的嵌入向量即嵌入矩阵的最后num_added行而冻结模型的其他所有参数。这可以防止模型在少量数据上对原有知识产生灾难性遗忘。# 冻结所有参数 for param in model.parameters(): param.requires_grad False # 只解冻新词的嵌入参数 new_embedding_params model.get_input_embeddings().weight[-num_added:] new_embedding_params.requires_grad True # 如果输出层也共享权重同样处理进行轻量级微调使用较小的学习率如1e-5到5e-5在准备好的数据上训练1-3个epoch。监控训练损失确保其平稳下降。评估扩展效果基础测试让模型生成或补全包含新词的句子检查新词是否能被正确、流畅地使用。类比测试设计领域内的类比问题。例如“不当得利之于民法好比 [MASK] 之于刑法。”期望“侵占罪”或类似概念。这可以测试新词是否被整合到了正确的语义关系中。下游任务评估在你的实际任务如法律问答、病历生成上测试对比扩展前后的性能指标如准确率、F1分数、人工评估的满意度。经验之谈微调数据的质量远重于数量。1000条精心构造、包含丰富上下文和正确用法的新词例句比10万条只是简单包含新词但语境贫乏的文本更有效。同时在微调后期可以逐步解冻模型靠近顶部的几层网络如最后的2-4个Transformer层让模型学习如何基于新的词汇进行更复杂的推理。4. 进阶策略与疑难杂症处理完成了上述四步一个基本的词表扩展流程就走通了。但在实际生产中还会遇到一些更复杂的情况和优化选择。4.1 处理大规模术语集增量扩展与动态路由当需要添加的术语成千上万时一次性扩展会大幅增加模型尺寸。此时可以考虑更精巧的设计外部术语向量库不修改主模型词表而是维护一个独立的、小型的“领域术语嵌入表”。当输入文本经过分词器后用一个额外的模块识别出领域术语并用外部向量库中的向量替换或增强原有的子词向量表示。这类似于一个可插拔的“专业词典插件”。适配器Adapter在模型的嵌入层后或中间层插入轻量级的适配器模块。适配器负责将通用词汇的表示根据上下文转换为更贴近领域知识的表示。这种方式不改变词表而是改变了语义的映射方式。4.2 解决多义词与术语冲突一个词形可能在不同领域有不同含义。例如“并行”在计算机领域指“同时执行”在法律文书中可能指“一并审理”。简单扩展可能会造成混淆。上下文感知的扩展在初始化术语向量时不仅考虑其组成字词还可以用领域语料训练一个轻量的上下文编码器为该术语生成一个更贴合领域语境的初始向量。保留歧义有时让模型意识到一个词有多个含义是更好的选择。这可以通过在微调数据中均衡地提供该词在不同领域或不同含义的例句来实现让模型学习到其多义性。4.3 与现有微调方法的结合词表扩展通常与全参数微调Full Fine-tuning、参数高效微调PEFT如LoRA、QLoRA结合使用。LoRA词表扩展这是一种非常实用的组合。我们用LoRA的方式微调模型的主体部分以低成本适配下游任务。同时对于LoRA无法覆盖的新词嵌入向量我们仍然需要对其进行全参数训练因为LoRA通常作用于注意力权重而非嵌入层。流程是先按上述方法扩展词表和嵌入层然后在此基础上应用LoRA进行微调。此时LoRA的适配器会学习如何基于新的、已初步初始化的术语向量进行更好的推理。4.4 常见失败案例与排查模型输出乱码或重复检查点首先确认分词器新增词的token id与模型嵌入层新行的索引是否一一对应。这是最常见的错误来源。检查点验证微调后保存的模型其配置文件config.json中的vocab_size是否已正确更新。新词在生成中从未出现检查点微调数据是否足够新词出现的上下文是否多样检查点生成时的采样策略如top-p, top-k是否过于保守抑制了低概率新词的出现可以尝试提高温度temperature或调整采样参数。检查点新词的初始向量是否太“偏”如果初始化值离语义空间的主体区域太远模型可能难以在有限训练中将其“拉”回来。尝试使用“均值初始化”或更好的预训练向量。扩展后模型通用能力下降检查点是否在微调时过度训练或学习率设置过高尝试更少的epoch和更小的学习率。检查点是否可以考虑使用更参数高效的方法如只训练新词向量LoRA减少对原始模型参数的干扰。词表扩展是一项细致入微的工作它要求我们对模型的基础架构、分词原理和训练动力学有清晰的认识。它不是一个一劳永逸的魔法而是一个需要数据、技巧和耐心相结合的系统工程。当你看到模型终于能准确理解并流畅运用那些曾经让它“语塞”的专业术语时这种让工具更贴合业务需求的成就感正是技术落地中最实在的回报。