ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

词汇信息融合的中文NER模型:SoftLexicon原理与PyTorch实战

词汇信息融合的中文NER模型:SoftLexicon原理与PyTorch实战 简介基于词汇信息融合的中文NER实现资源聚焦LEBERT模型在中文命名实体识别任务上的表现验证面向NLP方向进行课程设计、毕业设计或模型对比实验的开发者。资源共44个文件压缩包大小约12.97MB涵盖Python源码、设计报告docx、训练脚本、图表结果及数据集压缩包源码按模型、处理器、指标、损失函数等模块清晰组织便于定位和复用。目前已有493人学习下载。内容提供Bert-Softmax、Bert-Crf、LEBert-Softmax、LEBert-Crf四种模型在Resume、Ontonote、Msra、Weibo四个中文数据集上的完整实现与输出结果可直接复现性能对比配套设计报告对词汇信息融合原理、实验设置和结论做了详细说明可帮助快速理解方法差异也可作为后续改进NER模型的参考基线。无论是复现论文实验还是学习NER建模与评估完整流程这套代码都能节省从零搭建的时间。1. 词汇信息融合的中文NER模型为什么字符模型需要词视角外挂以字符作为最小建模单位的中文NER已成主流但真正做业务项目的人会发现字符模型有一个硬伤——它看不见词边界。标题里的Python实现基于词汇信息融合的中文NER模型正是冲着这个痛点来的在字符向量之外把词典命中结果作为一组词汇特征并进网络让模型既看到长也看到长江大桥。这种 zip 包打开后通常是一个标准 Python 工程包含数据预处理、模型定义、训练和评估脚本用 PyTorch 复现 SoftLexicon 或 Lattice 一类路线。适合已跑通 BiLSTMCRF 或 BERT 基线、却被分词和未登录词召回困住的从业者新手也可以把它当第一个能完整训练的 NER 项目来啃。2. 词汇信息融合的三条路线Lattice、FLAT 与 SoftLexicon动手前先选型2.1 为什么硬分词喂给模型这条路走不通中文和英文一个很大的差别是字符没有天然空格。英文字词本身自带边界中文的实体边界需要模型自己推断。先跑一个朴素字符级 BiLSTM-CRF很容易遇到这种场景训练集里上海反复出现过模型已经认得海后面跟市的时候大概率是地名但换到一篇医疗文本里上海医疗器械厂这种词模型只能一个字一个字地猜。词典命中的医疗器械厂如果作为词特征进来不需要训练数据里出现同样句式模型也能把这几个字捏成一个整体。反过来如果把分词结果硬当作 token 输入分词器在南京市长江大桥上翻一次车NER 就跟着翻一次车而且错误无法逆转。分词把南京市 / 长江 / 大桥切出来模型默认长江和大桥是独立成分实体边界直接被污染。这就是词汇信息融合存在的理由词信息要进来但不能让分词结果成为唯一输入更不能让分词的错误一路传到输出。2.2 Lattice LSTM经典但不好工程化Lattice LSTM 的做法可以拆开看输入字符序列 c1...cn同时用词典匹配得到词序列 w1...wm每个词 wi 连接它的区间 [s, e]模型构建一张有向无环图节点表示字符或词边表示字符到字符、字符到词、词到字符的连接。LSTM 在每个时刻按图结构的拓扑序更新隐藏状态词节点把词典向量带进来。思路很顺但工程上有两个硬伤。第一不同句子的词节点数量和连接方式不同PyTorch 的 batch 只能按最长句子补齐图结构没法简单 padding只能把 DAG 导出成定长的邻接表再在反向传播时还原。很多代码包为此做了大量 mask 处理读起来非常痛苦。第二LSTM 是序列递归结构图节点顺序一变整个隐藏状态要跟着重算GPU 并行度很差。我见过不少人把 Lattice 复现跑起来了训练速度比同规模 BiLSTM 模型慢两到三倍调参周期极长。如果项目是做在线推理且对延迟敏感的服务我不会选它。2.3 FLAT用 Transformer 拉平格子的思路FLATFlat-Lattice Transformer的逻辑是把图硬掰成序列。做法简单说就是把每个字符节点和每个词节点都当作一个 token字符 token 的位置编码用它在句子里的位置词 token 的位置编码用它的开始位置和结束位置分别做两种 embedding再加到 token embedding 上。这样 Transformer 可以并行处理不需要动态 DAG。工程上的坑在于一个词可能出现多个同名的候选去重和位置编码拼接都要小心另一个问题是词数量和字符数量叠加后序列长度明显膨胀max_len 要预留。如果语料是短句比如 50 字以内FLAT 的收益不一定比 SoftLexicon 大还要背一整套位置编码代价。我一般只在句子偏长、且已确定主线是 Transformer 结构时才考虑这个方向。2.4 SoftLexicon性价比最高的融合方式SoftLexicon 的做法比前两者都朴实对句子中每个字符分别收集四类词——以它开头的词 B、它夹在中间的词 M、以它结尾的词 E、以及它单独成词的 S。每一类里的词向量做加权求和最后和字符向量拼接再进编码器。简单公式可以写成对第 i 个字符建立四个词集 S_B(i)、S_M(i)、S_E(i)、S_S(i)对每个集合里的词向量做 softmax 加权得到 f_X再拼接成新的特征。加权的好处是模型可以自己学大词优先还是小词优先。比如在南京市长江大桥里对长这个字集合 B 里有长江和长江大桥模型如果给长江大桥更高权重最终边界判断就会更稳。这个公式用一个线性层就能实现所以它不挑主干LSTM、CNN、Transformer 都能直接接入训练速度几乎不掉。缺点是对词集做软加权时一个字符在句中可命中的词太多需要截断 padding 成固定形状。2.5 三个方案的选型对比下表是我实际选型时最常参考的维度方案对主干的改动训练速度实现复杂度适合场景Lattice LSTM需要图结构编码器慢串行遍历高学术复现非服务化场景FLAT需要改位置编码中高长文本、堆 Transformer 的场景SoftLexicon只加一个融合模块几乎不掉点低业务基线、快速上线、增量调优落到标题这个 zip 项目上大多数实现走的就是 SoftLexicon 路线因为它最容易跑通也最容易在现有基线上增量改。如果项目周期只有两周直接上 SoftLexicon如果想把 F1 再顶高 0.5 到 1 个点在 SoftLexicon 基础上接预训练语言模型而不是回头换 Lattice。3. 用 PyTorch 实现 SoftLexicon 的最小训练流水线3.1 先处理语料从词级 BIO 标注转到字级 BIOES多数 NER 数据集给的是词级 BIO 标注比如南京市 B-GPE长江大桥 B-LOC但字符级模型需要的是每个字一个标签。BIO 标签没法表达词的收尾边界所以融合模型常用 BIOES其中 S 表示单字实体、E 表示实体结尾。这里有个不能含糊的点如果语料里某个词被标成 B 开头转成 BIOES 时必须同时生成末字的 E 标签漏掉这一步会导致标签长度对不齐。# format_corpus.py def bio_to_bioes(words, labels): words: list[str]词序列例如 [南京, 市, 长江大桥] labels: list[str]词级标签例如 [B-GPE, I-GPE, B-LOC] 返回 (chars, bioes)保证长度一致。 chars, bioes [], [] for w, lab in zip(words, labels): if lab O: chars.extend(list(w)) bioes.extend([O] * len(w)) elif lab.startswith(B-): t lab[2:] if len(w) 1: chars.extend(list(w)) bioes.append(S- t) else: chars.extend(list(w)) bioes.append(B- t) bioes.extend([I- t] * (len(w) - 2)) bioes.append(E- t) elif lab.startswith(I-): # 原始数据本身已按字标注时会走到这里 t lab[2:] chars.extend(list(w)) bioes.extend([I- t] * len(w)) assert len(chars) len(bioes), 字符和标签长度不一致 return chars, bioes这个函数处理的是从词级标注到字级标注的转换重点在 B 开头的词要补末尾 EO 则直接逐字展开。后面assert是保命的一行数据源偶尔会有空 token提前拦下来比跑到模型里再炸好看得多。3.2 词典匹配为每个字符收集 B/M/E/S 四组词词汇信息融合的前提是有一份词典。zip 里常见的做法是同时支持外部词典文件和从训练集抽取词表两种方式外部词典覆盖面广训练集实体词则保证对自己数据集的召回。匹配逻辑本身不复杂就是拿句子里的每个子串去查词典命中后按字符在词中的位置归类。# lexicon_utils.py from collections import defaultdict def build_lexicon(sentences): sentences: list of list[str]字切分后的句子。 常见做法是用 jieba 对训练语料分词后把词并入词典再叠加外部领域词。 lexicon set() for sent in sentences: for word in jieba.lcut(.join(sent)): if 1 len(word) 8: # 只收长度 2~8 的词控制匹配噪音 lexicon.add(word) return lexicon def match_lexicon(sent, lexicon, max_word_len8): 为每个字符收集 B/M/E/S 四组词。 B: 以该字符开头的词M: 该字符在词中间 E: 该字符在词结尾S: 单字词。 matched [defaultdict(list) for _ in range(len(sent))] n len(sent) for start in range(n): for end in range(start 1, min(start max_word_len, n) 1): w .join(sent[start:end]) if w in lexicon: if start end - 1: matched[start][S].append(w) else: matched[start][B].append(w) for i in range(start 1, end - 1): matched[i][M].append(w) matched[end - 1][E].append(w) return matchedmatching 的复杂度是 O(n × max_word_len × 字典查询)。对中文来说max_word_len 取 8 已经能覆盖绝大多数实体哪怕中国人民解放军这种 7 字词也能命中设太大会让匹配结果里混入大量长尾噪音也拖慢训练。词典命中率可以用一个小脚本统计低于 60% 时优先补词典而不是调模型。3.3 融合模块注意力加权词集并入字符向量得到每个字符的 B/M/E/S 四组词之后下一步是把这些词向量加权合并成固定维度的特征。常见实现是用字符表示作为 query对每个词集里的词向量做注意力加权再接一个门控控制注入强度。# soft_lexicon.py import torch import torch.nn as nn import torch.nn.functional as F class SoftLexiconFusion(nn.Module): def __init__(self, char_dim, word_dim, hidden_dim50): super().__init__() self.word_proj nn.Linear(word_dim, hidden_dim) self.char_proj nn.Linear(char_dim, hidden_dim) self.gate nn.Linear(char_dim hidden_dim, char_dim) def forward(self, char_reps, word_reps, word_masks): char_reps: [B, T, char_dim] 字符向量 word_reps: [B, T, 4, max_words, word_dim] 四类词集词向量, 不足补0 word_masks: [B, T, 4, max_words] boolTrue 表示有效词 B, T, _, K, _ word_reps.shape char_key self.char_proj(char_reps) # [B, T, hidden] logits torch.einsum(btkwd,btd-btkw, word_reps, char_key) logits logits.masked_fill(~word_masks, -1e9) # 盖住 padding 词 weights F.softmax(logits, dim-1) # [B, T, 4, K] fused torch.einsum(btkw,btkwd-btd, weights, word_reps) fused self.word_proj(fused) # [B, T, hidden] g torch.sigmoid(self.gate(torch.cat([char_reps, fused], dim-1))) return char_reps g * fused几个参数值得专门说。max_words 是每类词集的最大容量一般取 816超出截断不足 mask。char_dim 和 word_dim 建议保持一致非预训练场景下都设 128如果用 BERT 做主干char_dim 是 768word_dim 也同步提到 768。-1e9这个 magic number 是 attention 里常用的 mask 填充值作用是让 padding 位置的 softmax 权重趋近于 0。最后那个门控是我个人习惯加的作用是把当前字要不要信词信息交给模型自己学而不是硬拼。3.4 训练脚本关键超参与参数表zip 里一般会带一个 train.py把模型、融合模块和 CRF 解码串起来。训练入口的命令通常长这样python train.py \ --train_path data/train.txt \ --dev_path data/dev.txt \ --lexicon_path data/lexicon.txt \ --max_len 128 \ --batch_size 32 \ --lr 5e-4 \ --epochs 20关键的超参数我整理成一张表方便对照参数推荐值说明char_dim / word_dim128非预训练场景用 BERT 则同步改 768max_words8每类词集上限显存紧张先降到 4batch_size32受 max_len 影响OOM 时先降这个lr5e-4BiLSTM/ 2e-5BERT预训练主干务必换小 lrmax_len128超过训练集 90% 句子长度即可dropout0.5非预训练模型用 0.5 保险训练时用 CRF 做解码loss 取负对数似然优化器选 AdamWweight_decay 给 0.01再加 max_grad_norm 5.0 的梯度裁剪。前几次迭代先观察 loss 是否稳定下降如果 loss 在 200 步之内纹丝不动问题大概率在数据或匹配层而不是模型结构。4. 解压 zip 后的环境配置与常见问题排查命令跑不通、匹配全空、显存爆炸逐个排掉4.1 解压后运行即报模块缺失环境变量与依赖版本现象从 zip 解压完执行python train.py报ModuleNotFoundError: No module named transformers或者torch.cuda.is_available()返回 False。原因这类项目依赖较多requirements.txt 里锁的版本可能已经和当前环境不兼容。直接用系统 Python 跑容易踩到依赖冲突GPU 版 PyTorch 还需要预装对应 CUDA 版本。解决先创建独立环境再装依赖python -m venv .venv source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate pip install -r requirements.txt如果 requirements 里锁的是旧版 torch先单独装 GPU 版再装其他依赖。只想在 CPU 上先跑通的话把 batch_size 调成 8、max_len 调成 64不要上来就复现论文配置那个是在 V100 上测的。4.2 中文数据读取报错路径编码与 Windows 环境的坑现象在 Windows 上解压后读数据报UnicodeDecodeError或者文件路径明明存在却FileNotFoundError。原因zip 内的训练数据有的是 UTF-8有的是 GBKWindows 默认 locale 和 Linux 不一致Python 的open()不指定 encoding 时会用系统默认编码去读GBK 数据用 UTF-8 解码当然报错。解决所有open()显式指定encodingutf-8如果数据确实是 GBK 存的一份先转码再做预处理。另外 zip 里文件路径如果含中文解压到纯英文路径避免 VSCode 调试时工作目录解析出问题。这是 Windows 上最常见的暗坑Linux 上写惯了的人最容易忽略。4.3 B/M/E/S 匹配结果全空模型在退化不是玄学现象训练 loss 能下降但 dev 的实体召回率一直很低跑完和完全不带词典的字符模型几乎一样。原因词典太小或者执行 match_lexicon 前句子已经被错误切分导致词跨不过空格匹配不上。还有一种情况是 max_word_len 设成 4把中国人民解放军这类长词整体漏掉模型只能退化成逐字猜测。解决先写个脚本统计一行样例里非空 B/M/E/S 的比例覆盖率低于 60% 就先补词典。做法是把训练集里的实体词、外部领域词都并进 lexicon再把 max_word_len 从 4 调到 8。记住一个判断标准融合模块有没有生效看匹配覆盖率不看不收敛的 loss。4.4 显存爆炸与 loss 为 NaNmax_words 和学习率的锅现象batch_size32 一跑就 OOM或者第二个 epoch loss 变成 NaN再往后直接 inf。原因词集张量是 [batch, max_len, 4, max_words, word_dim] 五维的max_words 和 word_dim 稍大显存就指数往上走。另一类问题是学习率给到 5e-3 以上BiLSTMCRF 极其容易梯度爆炸。解决OOM 时先降 batch_size 到 8max_words 从 8 降到 4观察显存释放NaN 时把 max_grad_norm 从 5.0 降到 1.0lr 降到 1e-4 重跑。不要一上来就加 dropout先把参数降下来确认数据没有问题再做结构层面的调整。5. 验证与进阶领域词典增量与多轮次评估5.1 领域词典增量一张 txt 换一个真实场景zip 里的词典通常来自通用分词和训练集实体落到具体业务语料时召回会明显下降。我一般会准备一份领域词典每行一个实体词UTF-8 编码加载时把训练集词典和领域词典合并重新生成一遍匹配缓存再训练。合并后词的边界特征变丰富但词典只负责提供特征不负责决定标签某个领域词在训练语料里可能根本不是实体CRF 学到对应输出后自然会压成 O这是正常行为。领域词典的增量效果在标注样本少的时候尤其明显。医疗文本里盐酸二甲双胍这种长词训练集可能只出现两三次词典一旦收录模型对这个词的边界判断就从一个字一个字猜变成拿现成边界参考对比 1000 句规模的小语料F1 提升 2 到 4 个点是常见结果。5.2 多轮 seed 评估防止单次 F1 假象验证阶段最容易被忽略的细节是单次 dev F1 的随机波动。NER 模型受随机种子影响不小同一个配置跑三次F1 差 0.3 到 0.5 很正常。如果只跑一次就判断某个改动有效极可能把噪声当成收益白调一下午。我现在做这几类实验都统一跑三个随机种子取均值对比涉及融合模块的超参还会单独记录方差。判断标准是均值有提升、方差不扩大才认为改动有效。这套习惯是从做 SoftLexicon 实验开始养成的后来每次跑 NER 基线都用它至少少踩了一半调参的坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表