中文命名实体识别实战:HMM、CRF、BiLSTM+CRF与ALBERT模型源码解析
简介面向中文命名实体识别NER任务的一份Python实现资料包适合自然语言处理初学者、课程设计或毕设项目参考。内容覆盖HMM、CRF、BiLSTM、BiLSTMCRF四种模型的完整代码与训练流程并提供人名、地名、机构名等实体标注数据集以及基于ALBERT预训练模型的扩展方案。数据采用B词首、M词中、E词尾标记方式共十种标签可直接用于序列标注训练。压缩包共28个文件包含Python源码、Jupyter Notebook训练脚本、前端展示页面、配置与依赖说明、模型性能对比图等总大小约625KB目录结构清晰便于按模型模块对照学习。notebooks内梳理了各模型的实验步骤app.py可快速启动交互式演示README及注释有助于理解特征设计与序列标注细节。已有90人下载学习适合需要系统掌握中文命名实体识别模型原理与代码实现的中高级学习者。1. 中文命名实体识别这份源码包不是玩具四个模型带数据直接能跑拿到ner-master.zip的时候我本来以为又是一个只有 README 的空壳毕设。解压之后发现HMM、CRF、BiLSTM、BiLSTMCRF 四类模型都有对应的 Python 实现还带着数据集、训练好的权重和三张性能截图。这不是一个演示用的玩具而是一套能直接改、直接复现的代码工程。中文命名实体识别要处理的核心问题就是把人名、地名、机构名从文本里抠出来而这份资源把统计模型和深度模型都焊在了一起。你只需要会一点 Python、能装依赖就能从 HMM 的维特比跑到 ALBERT 的微调。适合做毕设、课设、AI 岗位新手进阶也适合想快速拿一个可运行的基线模型去改业务场景的人。2. 数据与预处理B/M/E 标签体系、config.py 和喂给模型的输入格式2.1 实体标签为什么用 B/M/E人名、地名、机构名变成十种标签数据集的实体类型是四个人名nr、地名ns、机构名nt和无关词o。每个实体里的字符按位置继续分成三类B代表词首M代表词中E代表词尾。所以总共的标签是B-nr、M-nr、E-nr、B-ns、M-ns、E-ns、B-nt、M-nt、E-nt、O这十种。比如“张三在北京的阿里巴巴工作”这句话理想标注结果是张 B-nr 三 E-nr 在 O 北 B-ns 京 E-ns 的 O 阿 B-nt 里 M-nt 巴 M-nt 巴 E-nt 工 O 作 O你可能在其它项目里见过BIO体系也就是词首、词中、词外。这里用B/M/E而不是B/I/O是因为中文实体的平均长度比英文短但边界信息仍然很重要。M能明确告诉我们一个实体从哪开始、到哪结束对后续 CRF 的转移矩阵约束和 BiLSTM 的序列建模都有帮助。这个项目的数据格式也做得很规整每行一个字符加一个空格加一个标签空行分隔句子。如果你是自己准备数据最好也按这个格式来后面接config.py时最省事。2.2 config.py 参数说明batch_size、max_len、epochs 都在这项目里的config.py是整个实验的开关。所有模型共享一套配置路径和超参数都集中在这里。我打开之后看到的是典型的训练配置结构核心参数至少包含这几项参数名典型取值作用train_filedata/train.txt训练数据路径格式为“字符 标签”test_filedata/test.txt测试数据路径max_len128句子最大长度超过截断不足补PADbatch_size32每个 batch 的句子数显存不够就降到 16epochs50训练轮数配合早停使用embedding_dim100字向量的维度hidden_dim128LSTM 隐藏层维度num_tags10标签总数也就是上面说的十种learning_rate0.001优化器学习率model_typehmm/lstm_crf/albert选择跑哪个模型max_len是最容易忽略的参数。中文数据集里句子长度分布很分散如果设成 512显存会成倍增加设成 32长句子里的实体又被截断。我建议先从 128 开始看一下训练集里句子的 95 分位长度再调。2.3 数据处理从“字符 标签”文件到模型输入的 index 序列模型不能直接吃字符要先建立一个字符表把每个字映射成一个数字索引。项目里应该有现成的load_data类似函数常见做法是这样的def load_data(filepath): sentences, labels [], [] sent_chars, sent_tags [], [] with open(filepath, encodingutf-8) as f: for line in f: line line.strip() if line : if sent_chars: sentences.append(sent_chars) labels.append(sent_tags) sent_chars, sent_tags [], [] else: char, tag line.split( ) sent_chars.append(char) sent_tags.append(tag) return sentences, labels def build_vocab(sentences): char2idx {PAD: 0, UNK: 1} for sent in sentences: for ch in sent: if ch not in char2idx: char2idx[ch] len(char2idx) return char2idx逻辑说明load_data按空行切分句子每行用空格拆成字符和标签build_vocab收集所有出现过的字符并预留PAD和UNK。参数说明PAD的 id 固定为 0UNK固定为 1这样后续 padding 和未知字符都统一。3. 统计模型实现HMM 与 CRF 的具体代码和跑通 HMM.ipynb3.1 HMM 在 hmm.py 中的实现频数估计和维特比解码HMM 是这堆模型里最朴素的。它把命名实体识别看作一个序列标注问题观测值是字符隐藏状态是标签。需要估计三类概率初始概率pi、状态转移概率A、发射概率B。项目里hmm.py的实现思路很直接训练阶段用频数统计加平滑预测阶段用维特比算法找最优标签序列。class HMM(object): def __init__(self, num_tags, num_chars, smooth1e-4): self.A np.full((num_tags, num_tags), smooth) self.B np.full((num_tags, num_chars), smooth) self.pi np.full((num_tags,), smooth) def train(self, X, Y): # X: 字符索引序列列表, Y: 标签索引序列列表 for sent_chars, sent_tags in zip(X, Y): self.pi[sent_tags[0]] 1 for t in range(len(sent_tags) - 1): self.A[sent_tags[t], sent_tags[t 1]] 1 for ch, tag in zip(sent_chars, sent_tags): self.B[tag, ch] 1 self.A / self.A.sum(axis1, keepdimsTrue) self.B / self.B.sum(axis1, keepdimsTrue) self.pi / self.pi.sum() def viterbi(self, sent_chars): # 维特比解码 T len(sent_chars) dp np.zeros((T, self.A.shape[0])) ptr np.zeros((T, self.A.shape[0]), dtypeint) dp[0] self.pi * self.B[:, sent_chars[0]] for t in range(1, T): scores dp[t - 1][:, None] np.log(self.A) ptr[t] np.argmax(scores, axis0) dp[t] np.max(scores, axis0) np.log(self.B[:, sent_chars[t]]) best np.argmax(dp[-1]) path [best] for t in range(T - 1, 0, -1): best ptr[t][best] path.append(best) return path[::-1]逻辑说明train里对pi统计第一个标签对A统计相邻标签共现对B统计每个标签下字符出现次数最后归一化成概率。viterbi用动态规划维护每个时刻每个标签的最优路径。参数说明smooth是拉普拉斯平滑系数防止某个标签或字符从未出现导致概率为 0。3.2 项目里的 CRF从与 BiLSTM 配合的层到独立使用要注意一点这个项目里 CRF 不是传统意义上手写特征的统计 CRF而是作为 BiLSTM 的输出层在lstm_crf.py中实现的。它学习一个num_tags x num_tags的转移矩阵约束标签之间的顺序关系比如B-nr后面不能直接跟O。如果你需要独立 CRF 做对比实验可以从lstm_crf.py里把转移矩阵的部分抽出来。核心损失函数是负对数似然解码还是维特比。def crf_score(self, emissions, tags): # emissions: (seq_len, num_tags) # tags: (seq_len,) seq_len emissions.size(0) score torch.zeros(1) score self.start_trans[tags[0]] emissions[0, tags[0]] for t in range(1, seq_len): score self.trans[tags[t - 1], tags[t]] emissions[t, tags[t]] score self.end_trans[tags[-1]] return score逻辑说明crf_score计算一条完整路径的得分包括起始约束、标签间转移约束、发射得分和结束约束。参数说明start_trans和end_trans是两个可学习向量trans是num_tags x num_tags的矩阵这些都会在训练中自动更新。3.3 跑通 HMM.ipynb训练、预测和看 hmm-performance.JPG打开notebooks/HMM.ipynb从上到下运行就行。先读数据、建词表、转 id然后实例化HMM调用train最后在测试集上做预测并输出 F1。通常跑完会得到一张类似hmm-performance.JPG的训练过程图。X_train, Y_train ..., ... X_test, Y_test ..., ... model HMM(num_tags10, num_charslen(char2idx)) model.train(X_train, Y_train) preds [model.viterbi(sent) for sent in X_test]逻辑说明这四步是标准流程。num_tags10对应十种标签num_chars是字符表大小。预测时每句话走一次维特比得到一条等长的标签序列。参数说明HMM 没有 batch 概念数据量大时train会逐句累加概率内存占用很低。4. 深度学习模型与 ALBERT 微调BiLSTM、BiLSTMCRF 和预训练中文模型4.1 BiLSTM 模块Embedding、双向 LSTM 与全连接深度学习部分在lstm_crf.py里。BiLSTM 把每个字符先映射成一个稠密向量然后经过双向 LSTM 抽取上下文特征最后接一个全连接层输出每个字符在每个标签上的得分。class BiLSTM(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_dim, num_tags): super().__init__() self.embed nn.Embedding(vocab_size, embedding_dim) self.lstm nn.LSTM(embedding_dim, hidden_dim // 2, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(hidden_dim, num_tags) def forward(self, x): x self.embed(x) # (batch, seq, embed) h, _ self.lstm(x) # (batch, seq, hidden) out self.fc(h) # (batch, seq, num_tags) return out逻辑说明hidden_dim设成 128 时实际 LSTM 的每个方向是 64 维拼接后才是 128。fc层把 128 维映射成 10 维这 10 维就对应每个标签的发射得分。参数说明batch_firstTrue让输入形状是(batch, seq, embed)方便和pad_sequence对齐。4.2 BiLSTMCRF 中的 CRF 层标签约束与解码单纯 BiLSTM 的每个位置分类是独立的可能出现B-nr后面接E-ns这种不合理输出。加上 CRF 层之后模型会对整个标签序列打分如果某个标签转移在训练数据里很少出现它就会被抑制。class BiLSTM_CRF(nn.Module): def __init__(self, ...): super().__init__() self.bilstm BiLSTM(...) self.trans nn.Parameter(torch.randn(num_tags, num_tags)) self.start_trans nn.Parameter(torch.randn(num_tags)) self.end_trans nn.Parameter(torch.randn(num_tags))逻辑说明trans矩阵是 CRF 的核心参数代表从标签 i 转移到标签 j 的得分。训练时对比所有可能路径的总得分让正确路径得分最高。参数说明start_trans和end_trans分别表示句首和句尾的标签约束不能忽略否则长句子的边界会有问题。4.3 用 albert.py 接入中文预训练模型models/albert.py负责加载 ALBERT 预训练模型。这里需要先做好一件事把transformers装好并把预训练权重下载到本地。很多情况下跑不起来不是代码问题而是权重没下载全。from transformers import AutoTokenizer, AutoModel class AlbertNER(nn.Module): def __init__(self, albert_dir, num_tags): super().__init__() self.tokenizer AutoTokenizer.from_pretrained(albert_dir) self.albert AutoModel.from_pretrained(albert_dir) self.fc nn.Linear(768, num_tags) def forward(self, input_ids, mask): out self.albert(input_idsinput_ids, attention_maskmask)[0] logits self.fc(out) # (batch, seq, num_tags) return logits逻辑说明albert_dir是本地权重文件夹路径例如./models/albert_chinese/。ALBERT 输出的每个 token 是 768 维接一个线性层就变成标签得分。参数说明attention_mask不能少否则把PAD位置的字符也算进损失了。4.4 三张性能图告诉我们什么项目里带了hmm-performance.JPG、lstm-performance.JPG和albert-performance.JPG分别对应统计模型、深度模型和预训练模型的效果。从我的经验看HMM 的 F1 通常最低BiLSTM 能上一个台阶BiLSTMCRF 会再高一点ALBERT 在这四个模型里通常是最高的尤其是在中文数据集上因为预训练模型已经学到了大量语言知识。模型训练速度对上文长距离依赖对标签约束适合场景HMM秒级弱有但弱冷启动、快速基线BiLSTM分钟级中无学习序列特征BiLSTMCRF分钟级中强通用 NER 任务ALBERT小时级强取决于上层追求高准确率这四句话不是玄学而是序列标注任务里的普遍规律。你在自己的数据上跑完这三张图基本就能看到类似的排序。5. 避坑与排查四个让新手翻车的细节5.1 loss 变成 nan标签映射不一致现象训练 BiLSTMCRF 时第几个 epoch 之后 loss 突然变成nan然后一直不变。原因训练集中的标签和num_tags的索引对不上。比如数据里出现了O标签但tag2idx里只建了九种标签某一行的index越界算损失时取到非法值。解决在load_data之后加一个断言检查标签种类和配置文件是否一致。我一般会直接输出set(all_labels)和config.num_tags对比确保len(set) num_tags。5.2 预训练权重加载失败transformers 版本和缓存路径现象运行ALBERT.ipynb时报错大意是找不到模型名或权重文件有时还会莫名其妙地重新下载。原因一是transformers版本太老或太新AutoModel.from_pretrained内部的接口变了二是默认缓存路径没有写入权限下载失败后自动回退到假模型。解决先在终端执行pip install transformers4.18.0这类固定版本我建议用 4.x 的中间版本最稳。然后把albert_dir指向已经下载好的本地目录就算没网也能跑。5.3 训练极慢或 OOMmax_len、batch_size 和设备现象BiLSTM_CRF.ipynb运行时显卡显存不够或者 CPU 模式下跑一个 epoch 要半小时。原因默认max_len128对很多短句子来说太宽一个 batch 里大量位置是PAD浪费计算。batch_size 设成 64 可能在单卡上就爆了。解决先统计训练集中句子的最大长度和 95 分位长度把max_len下调到 64 或 96。batch_size从 16 开始显存占用稳定后再往上加。如果只有 CPU把num_layers减少到 1hidden_dim减到 64。5.4 app.py 打开页面空白templates 路径和模型权重缺失现象运行app.py后浏览器打开http://127.0.0.1:5000/显示空白或者报模板找不到。原因app.py依赖templates/index.html和static目录文件路径必须和 Flask 默认结构一致。另外如果模型没有保存成可加载的.pkl或.pt文件Flask 启动时加载失败页面自然就白屏。解决确认templates和static和app.py在同一级目录。检查models/下是否已经有训练好的权重如果没有先跑一遍 HMM 或 BiLSTM 的 notebook把模型save出来。不要直接从压缩包里拉到桌面就运行路径别带中文。6. 进阶把自定义实体数据接进项目只需改三个地方6.1 三步接入自己的标注数据当你不想用自带的人名、地名、机构名想跑自己的实体类型时不需要重构代码改三个地方就够了。第一步改config.py。把train_file、test_file指向你的数据文件把num_tags改成你的标签数量。比如你只标产品名和品牌名实体类型只有两个加上O那就是2 * 3 1 7种标签。第二步把数据转换成“字符 标签”的格式。如果你手上的标注是BIO体系可以用这个函数转成BMESdef bio_to_bmes(sent_chars, tags): out [] i 0 while i len(tags): tag tags[i] if tag O: out.append((sent_chars[i], O)) i 1 else: entity tag[2:] # 去掉 B-/I- 前缀 j i while j len(tags) and tags[j][2:] entity and j - i 3: j 1 if j - i 1: out.append((sent_chars[i], fB-{entity})) out.append((sent_chars[i 1], fE-{entity})) if i 1 len(sent_chars) else None else: out.append((sent_chars[i], fB-{entity})) for k in range(i 1, j - 1): out.append((sent_chars[k], fM-{entity})) out.append((sent_chars[j - 1], fE-{entity})) i j return out这个转换的逻辑是单字实体直接标B和E多字实体中间部分用M。注意M不能被跳过否则 CRF 转移矩阵会学到错误约束。第三步同步标签映射表。在config.py或模型代码里维护tag2idx确保顺序一致。我在这里栽过跟头大量E-产品被丢弃因为映射表里漏了M标签。从那以后我每次接入新数据都强制走一遍set(all_tags)和num_tags的断言再跑第一个训练 batch。这三个地方对齐后HMM、BiLSTMCRF 和 ALBERT 都能用你的数据重新训练。app.py里的前端界面也不用动它只负责把预测结果渲染成高亮实体。希望帮到你。本文还有配套的精品资源点击获取