ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

大模型词表扩展实战:解决生僻字与专业术语识别难题

2026/8/6 3:26:32 拓冰建站 浏览量
大模型词表扩展实战:解决生僻字与专业术语识别难题 1. 项目概述当大模型遇上中文“拦路虎”最近在折腾几个垂直领域的智能应用从古籍文献的数字化处理到某个细分工业领域的知识问答我遇到了一个相当普遍却又棘手的问题模型不认识字。不是“你好”、“世界”这种基础字词而是那些古籍里的生僻字或者某个行业内部流通、外部鲜有人知的专业术语和缩写。比如一个面向中医古籍的智能检索系统遇到“䶮”yǎn、“㵘”màn这类字直接“懵圈”一个金融风控模型对“MLF”中期借贷便利、“LPR”贷款市场报价利率的上下文理解总是差强人意。这本质上是大模型的“词表”问题——它的世界被训练时见过的那些token词元所限定对于词表外的存在要么强行拆解成奇怪的子词要么直接忽略导致语义丢失和性能下降。“大模型的词表扩展”就是为了解决这个问题。它不是一个简单的“往字典里加新词”的动作而是一套系统工程核心目标是将新的词汇无论是生僻字还是专业术语安全、高效、语义合理地融入到大模型已有的庞大知识体系中让模型能像理解“苹果”一样理解“霰弹”或“CRISPR”。这个过程的关键在于“词嵌入适配”即不仅要让模型认识这个词的“形”将其加入分词器的词表更要赋予它正确的“义”在模型的嵌入空间中为其找到一个合适的位置。本次探讨的方案“.42”并非一个具体的版本号更像是一个内部代号或迭代思路它指向的是一种兼顾效率与效果的实用化路径。这项工作适合谁如果你正在从事以下领域这篇文章或许能帮你避开不少坑垂直领域大模型应用开发者正在将通用大模型如LLaMA、ChatGLM、Qwen适配到法律、医疗、金融、科研等专业场景。古文/方言数字化研究者需要处理大量超出常用字集如GB2312的汉字。大模型算法工程师关注模型底层架构与持续学习Continual Learning机制。任何被大模型“看不懂专业话”所困扰的实践者。接下来我将拆解这套方案的完整逻辑、实操步骤以及我趟过的一些水坑。2. 核心思路与方案选型为什么不是简单的“添加”词表扩展听起来简单但粗暴操作后果严重。最天真的想法是直接修改分词器Tokenizer的词汇表文件如vocab.json在里面新增几行然后重新训练嵌入层Embedding Layer。这行不通原因有三破坏原有语义空间大模型的嵌入层是一个高维空间每个已有词汇的位置是其与海量数据共现关系的凝练。随机初始化一个新词的向量并插入就像在一个精密的天体运行图中硬塞进一颗位置随机的小行星会严重扰乱其周围词汇尤其是语义相近词的几何关系导致模型原有能力“失忆”。模型架构不匹配Transformer模型的参数是紧密耦合的。嵌入层的维度hidden_size直接关联后续所有前馈网络FFN的输入维度。仅扩展嵌入层而不调整后续层会在矩阵乘法时出现维度不匹配模型根本无法前向传播。训练成本与灾难性遗忘即使你同步扩展了所有相关层从头开始训练一个新的大模型也成本高昂。而如果只是在原有模型上微调Fine-tuning新加入的、数据量极少的词如某个生僻字会与海量旧数据中的词竞争梯度极易导致对旧知识的“灾难性遗忘”。因此一个成熟的方案必须解决两个核心矛盾如何让新词获得合理的初始嵌入值以及如何以最小的代价让模型学会正确使用新词。方案“.42”的核心理念是“先定位后微调再对齐”。先定位不随机初始化新词向量而是基于新词的形态、读音或定义从现有词表中寻找一个或多个“锚点词”用它们的嵌入向量的加权组合或某种变换作为新词的高质量初始嵌入。这相当于在已有的语义星图中根据已知星座的位置合理推测出新天体的可能轨道。后微调在获得高质量初始值后使用包含新词的、规模相对较小的领域数据进行有监督的、目标明确的微调。这里的关键是设计损失函数既要让新词学会又要保护旧知识。再对齐微调后新词的嵌入可能会发生微小漂移。需要检查其与相似旧词在语义空间中的相对位置是否合理必要时进行轻微调整确保整个语义空间的拓扑结构保持良好。3. 实操全流程解析从生僻字到专业术语下面我将以扩展中文大模型例如使用BERT或RoBERTa分词器的模型词表为例详细拆解每一步。3.1 阶段一准备与预处理——确定要扩展什么在动手之前必须明确目标。盲目添加词汇只会增加模型复杂度。1. 新词收集与清洗生僻字从目标语料如古籍扫描文本中通过字符编码范围如超出UTF-8基本多文种平面BMP的字或现有分词器的unk未知词输出进行提取。注意有些字可能是OCR错误需要人工或利用字形相似度进行清洗。专业术语从领域词典、标准文档、论文高频词中提取。特别注意缩略语如“NLP”和全称如“自然语言处理”的配对收集这对后续的锚点选择至关重要。工具可以使用正则表达式、jieba分词结合自定义词典进行初步提取再辅以人工审核。输出得到一个干净的新词列表new_words.txt每行一个词。2. 词频与重要性评估不是所有新词都值得加入。词表越大模型推理时计算注意力权重的开销就越大。我们需要一个优先级。计算词频在目标领域语料中统计每个新词的出现频率。评估语义重要性有些词频率不高但却是关键概念如某个化学反应的关键催化剂名称。这需要领域专家标注或利用TF-IDF等简单指标辅助判断。操作将new_words.txt中的词按“词频×重要性系数”排序设定一个阈值优先处理排名靠前的词。对于首次尝试建议先控制规模如50-100个词。注意分词器对中文的处理通常基于子词如WordPiece/BPE。对于多字专业术语要确认它是否已被拆散。例如分词器可能将“循环神经网络”拆成“循环”、“神经”、“网络”。如果它作为一个整体概念至关重要且拆开后语义有损才考虑将其作为整体加入词表。3.2 阶段二嵌入初始化——给新词一个“合理的起点”这是最关键的一步决定了新词学习的起点质量。这里介绍三种由简到繁的方法。方法A基于形态或拼音的近似匹配适用于生僻字对于生僻字我们往往知道它的字形结构或读音。思路找到字形相似或读音相同的常见字用这些常见字的嵌入向量的平均作为生僻字的初始嵌入。实操字形相似利用汉字结构数据库或库如cjkvi-ids将生僻字拆解为部首/组件。例如“䶮”字由“龙”和“天”组成。则可以计算embed_init(䶮) 0.5 * embed(龙) 0.5 * embed(天)。拼音相同如果生僻字有明确拼音如“㵘”读màn则找到所有读“man”的常见字如“慢”、“漫”、“曼”取它们嵌入的平均值。代码示意Pythonimport torch # 假设 model 是原始模型tokenizer 是原始分词器 def init_embed_by_components(char, component_chars, model): # component_chars: 相似字或部首列表如 [龙, 天] with torch.no_grad(): embeds [] for comp in component_chars: comp_id tokenizer.convert_tokens_to_ids(comp) if comp_id ! tokenizer.unk_token_id: embeds.append(model.get_input_embeddings().weight[comp_id]) if embeds: # 简单平均 new_embed torch.stack(embeds).mean(dim0) return new_embed else: # 回退到随机初始化但使用原有嵌入层的均值和标准差 return torch.normal(meanmodel.get_input_embeddings().weight.mean(dim0), stdmodel.get_input_embeddings().weight.std(dim0))方法B基于定义的语义近似适用于专业术语对于专业术语我们通常有它的文本定义或描述。思路将术语的定义句子输入原始模型获取句子嵌入如对最后一个隐藏层所有token的嵌入取平均用这个句子嵌入作为术语本身的初始嵌入。实操为术语“CRISPR”准备定义“一种基因组编辑技术”。用原始模型和分词器对这个句子进行编码得到各token的嵌入。对这些token嵌入取平均或使用[CLS] token的嵌入得到向量E_definition。embed_init(CRISPR) E_definition。优势这种方法赋予新词更丰富的语义信息起点更准。方法C基于上下文的共现词平均通用方法这是最稳健、最常用的方法。思路在大量领域文本中找出所有包含新词的句子。对于每个句子用原始模型获取句子中其他所有已知词的嵌入然后取平均作为该句子中新词的“上下文代表”。最后对所有句子的“上下文代表”再取平均作为新词的初始嵌入。实操收集包含新词w_new的句子集合S。对于每个句子sinS用分词器分词得到token列表其中w_new会被拆成子词或标记为[UNK]我们忽略它。获取句子中所有已知token的ID并从模型嵌入层取出对应的向量。计算这些向量的平均值得到context_embed_s。对所有句子的context_embed_s求平均得到embed_init(w_new)。解释这个方法的假设是“一个词的意义由其上下文决定”。通过平均其周围词的语义我们得到了对新词意义的最佳估计。实操心得对于生僻字推荐方法A和方法C结合。先用方法A基于字形/拼音给一个基础向量再用方法C如果能有少量包含该字的上下文进行微调。对于专业术语方法B和方法C是黄金组合。先用术语定义方法B注入核心语义再用真实上下文方法C进行平滑和修正。永远避免完全随机初始化。3.3 阶段三模型扩展与微调——让模型真正“学会”获得初始嵌入后我们需要实际修改模型并训练。1. 扩展模型嵌入层及输出层这是必须的步骤因为要增加词表大小。步骤获取原始模型的嵌入权重矩阵W_old形状为[old_vocab_size, hidden_size]。为每个新词根据上一步计算好的embed_init构建一个新权重矩阵W_new形状为[new_vocab_size, hidden_size]其中new_vocab_size old_vocab_size num_new_tokens。将W_old放在W_new的前面部分将新词的初始嵌入按顺序放在后面部分。创建一个新的nn.Embedding层其权重设置为W_new。关键对于仅解码器如GPT类或编码器-解码器模型通常需要同步扩展输出投影层最后的Linear层其输入维度是hidden_size输出维度需要从old_vocab_size扩展到new_vocab_size。新增加部分的权重可以用原始投影层权重的均值或零值初始化但偏置bias建议初始化为一个较大的负值如-10这样在训练初期模型不会轻易预测出新词避免干扰。代码示意from transformers import AutoModelForCausalLM import torch.nn as nn model AutoModelForCausalLM.from_pretrained(your/base/model) old_embed model.get_input_embeddings() old_vocab_size, hidden_size old_embed.weight.shape num_new_tokens len(new_words_list) # 创建新的嵌入权重 new_embed_weight torch.randn(old_vocab_size num_new_tokens, hidden_size) * 0.02 # 临时随机后续替换 new_embed_weight[:old_vocab_size] old_embed.weight.data.clone() # 保留旧权重 # 将之前计算好的新词初始嵌入赋值给对应位置 for i, (word, init_vec) in enumerate(zip(new_words_list, precomputed_inits)): new_embed_weight[old_vocab_size i] init_vec # 创建新的嵌入层并替换 new_embed nn.Embedding(old_vocab_size num_new_tokens, hidden_size) new_embed.weight.data new_embed_weight model.set_input_embeddings(new_embed) # 扩展输出层 (对于因果语言模型) old_lm_head model.get_output_embeddings() if old_lm_head is not None: new_lm_head nn.Linear(hidden_size, old_vocab_size num_new_tokens, biasTrue) # 复制旧权重 new_lm_head.weight.data[:old_vocab_size] old_lm_head.weight.data.clone() # 新部分权重初始化例如使用旧权重的平均 new_lm_head.weight.data[old_vocab_size:] old_lm_head.weight.data.mean(dim0, keepdimTrue).repeat(num_new_tokens, 1) # 新部分偏置初始化为负值 if new_lm_head.bias is not None: new_lm_head.bias.data[old_vocab_size:] -10.0 model.set_output_embeddings(new_lm_head)2. 分词器词表扩展模型能处理了还要让分词器能分出这个词。步骤对于BPE/WordPiece分词器直接将新词作为整体添加到词汇表末尾。使用Hugging Face Transformers库可以方便完成。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your/base/model) # 添加新词 num_added tokenizer.add_tokens(new_words_list) print(fAdded {num_added} tokens.) # 必须同步调整模型嵌入层大小上面已做并告知模型新token的数量 model.resize_token_embeddings(len(tokenizer)) # 注意上面的resize_token_embeddings会创建一个新的嵌入层可能会覆盖我们精心设置的初始值。 # 更安全的做法是先resize再手动将我们计算好的新嵌入赋值回去。3. 设计微调任务与数据微调的目标是让模型学会在正确的上下文中使用新词同时不忘旧知识。数据构建准备一个领域文本数据集其中自然地包含新词。对于语言模型任务就是标准的因果语言建模CLM或掩码语言建模MLM。确保新词在训练样本中有足够的曝光。一个技巧可以适当过采样包含新词的句子以平衡新词和旧词的数据量但不宜过度以免导致灾难性遗忘。损失函数考量标准的交叉熵损失即可。更精细的做法可以采用弹性权重合并EWC或知识蒸馏等持续学习技术来正则化保护旧权重。但对于初期实验可以先使用较低的学习率和较少的训练步数来控制。4. 执行微调关键参数learning_rate: 建议使用一个很小的值如1e-5到5e-5。num_train_epochs: 1-3个epoch通常足够具体看数据量。冻结大部分参数为了最大程度防止遗忘可以只训练新扩展的嵌入层和输出层或者再加上最后几层Transformer块。将模型其他部分的参数设置为requires_gradFalse。# 示例只训练新加入的部分和最后两层 for name, param in model.named_parameters(): if embed in name or lm_head in name: param.requires_grad True elif any([f.{i}. in name for i in range(model.config.num_hidden_layers-2, model.config.num_hidden_layers)]): param.requires_grad True # 最后两层可训练 else: param.requires_grad False # 冻结其他层3.4 阶段四评估与对齐——验证效果并微调训练完成后不能只看损失下降必须进行针对性评估。1. 基础功能测试新词识别输入包含新词的句子检查分词器是否能正确将其识别为一个token而不是拆开。嵌入相似度计算新词嵌入与已知的、语义相近或相关的旧词嵌入之间的余弦相似度。例如“CRISPR”应该与“基因”、“编辑”、“技术”等词的相似度较高。如果相似度不合理说明初始化或训练可能有问题。完形填空设计一些句子其中新词被掩码看模型能否正确预测。例如“科学家利用____技术对DNA进行了精准修改。”2. 下游任务评估如果你扩展词表是为了某个具体任务如文本分类、问答那么在该任务的验证集上测试性能是金标准。比较扩展词表前后的模型表现。特别注意模型在不含新词的通用任务上的性能是否有下降这是检验是否发生灾难性遗忘的关键。3. 语义空间对齐检查使用降维技术如t-SNE或PCA将高维嵌入投影到2D/3D空间可视化新词和一批关键旧词的位置。观察新词是否落在了语义相似的词群中。例如生僻字“䶮”应该靠近“龙”、“皇帝”等词专业术语“量化宽松”应该靠近“货币政策”、“央行”、“印钞”等词。如果发现明显偏离可能需要回到微调阶段使用更针对性的数据或调整损失函数进行微调。4. 常见问题、避坑指南与实战心得在实际操作中我遇到了不少问题这里总结一下。4.1 新词初始化效果差问题新词加入后在相似度测试或完形填空中表现混乱。排查检查初始嵌入计算用于生成初始嵌入的“锚点词”或上下文是否准确对于专业术语定义句子是否足够精确可以尝试多种初始化方法比较结果。检查分词确认新词在训练和评估时都被正确分词。有时分词器的前后空格处理会导致意外。解决采用“混合初始化”。例如对专业术语将“基于定义的嵌入”和“基于上下文的嵌入”以一定比例如7:3加权平均往往能得到更稳健的起点。4.2 微调导致模型“变傻”灾难性遗忘问题模型学会了新词但回答普通问题的能力下降甚至胡言乱语。原因学习率太高、训练步数太多、或可训练参数范围太大导致旧知识的权重被大幅修改。解决严格冻结如3.3节所述只训练新增的参数和最后极少数层。极低学习率与小数据量从1e-5开始尝试使用少量高质量数据进行1个epoch的微调。使用正则化在损失函数中加入对旧模型输出分布的KL散度约束知识蒸馏或对重要旧参数的弹性约束如EWC。回滚与检查点每训练一定步数就在保留数据集holdout dataset不含新词上评估通用能力一旦发现显著下降立即回滚到上一个检查点。4.3 扩展后模型推理速度变慢问题词表增大后生成文本或计算注意力时变慢。分析这是必然的但影响程度取决于新增词的数量。Softmax计算和嵌入查找的复杂度与词表大小线性相关。缓解精挑细选新词严格按3.1节的评估筛选只加真正高频且重要的词。使用动态词汇表在推理时根据上下文动态地从全词表中选出一个子集进行计算。但这需要修改模型推理代码实现较复杂。模型量化将模型权重从FP16/FP32量化到INT8可以显著减少内存占用和加速计算部分抵消词表增大的开销。4.4 处理多字词与子词冲突问题添加了多字术语“循环神经网络”但模型在遇到“循环神经”时可能仍然会将其分开处理。解释这是BPE等子词算法的固有特性。新增的多字词只是为分词器提供了一个优先选项。当该词作为一个整体出现时会被优先匹配。但当它被拆开出现在不同上下文时模型依然会使用子词。建议这通常不是问题反而是子词算法的优势所在处理未登录的复杂组合。除非该术语的完整性对任务至关重要否则不必强求。如果必须保证完整性可以考虑在预处理阶段进行词表外OOV词替换或者使用全词掩码Whole Word Masking策略进行训练。4.5 一个实用的检查清单在部署扩展后的模型前运行这个检查清单[ ]分词测试输入包含新旧词的混合句子分词结果是否符合预期[ ]嵌入加载确认模型加载后新词的嵌入值是我们预设的初始值而非随机值。[ ]前向传播输入一个简单的batch模型是否能正常跑通不报维度错误[ ]损失下降在微调时训练损失是否在稳步下降[ ]新词预测在掩码测试中新词的预测概率是否显著高于其他无关词[ ]旧词保留用一组标准测试题如常识问答测试模型性能下降是否在可接受范围如2%[ ]相似度合理新词与相关旧词的余弦相似度是否在合理区间如0.35. 方案演进与高级技巧在基础方案之上还有一些更高级的思路可以进一步提升效果和效率。5.1 增量式扩展与版本管理对于需要持续增加新词的场景如新闻热点词、不断发展的科技术语可以采用增量式扩展。做法不是每次都在原始模型上扩展而是在上一次已扩展的模型基础上继续扩展。需要维护一个扩展词表映射文件记录每次新增词的ID范围及其初始嵌入的来源信息。优势避免每次都要从原始大模型开始节省初始化计算时间。挑战需要仔细管理模型版本和词表文件防止冲突。5.2 利用适配器Adapter进行轻量级扩展这是目前非常热门的研究方向旨在实现更轻量的参数更新。思路不直接修改模型的嵌入层而是在嵌入层之后插入一个轻量级的适配器模块。这个适配器学习将新词的嵌入可以仍存放在一个外部的小表中映射到模型原有的语义空间中。训练时只更新适配器和外部小表的参数冻结原模型所有参数。优势极大减少了可训练参数量几乎完全避免了灾难性遗忘扩展和部署非常灵活。工具可以参考adapter-transformers等库来实现。5.3 针对超大规模词表的优化当需要一次性添加成千上万个词时如添加整个医学词典上述方法在计算和内存上可能成为瓶颈。聚类初始化将新词按其定义或上下文嵌入进行聚类。对每一类新词使用该类中心词的嵌入作为该类所有新词的共享初始嵌入。微调时同类词共享一个嵌入向量或者从一个共享向量加上一个小的、词特定的偏移量开始学习。参数化生成训练一个小的神经网络如一个两层MLP以新词的某种特征如它的n-gram特征、或定义句子的句子嵌入为输入直接生成该词在模型嵌入空间中的向量。这样模型只需要存储这个生成器的参数而不是所有新词的嵌入矩阵可以极大节省空间。词表扩展是大模型落地到专业领域必须跨过的一道坎。它考验的不仅是技术实现更是对问题本质的理解——如何在不破坏原有智慧的前提下为模型注入新的、专门的知识。方案“.42”所代表的这种谨慎的、基于语义初始化的渐进式方法在实践中被证明是可靠且有效的。它提醒我们在AI的世界里“学习”和“记忆”同样重要而一个好的开始往往是成功的一半。