尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

BiLSTM+CRF命名实体识别源码解析:从课设到实战

BiLSTM+CRF命名实体识别源码解析:从课设到实战 简介本资源为基于Python实现双向LSTM条件随机场CRF的命名实体识别模型课程作业完整包面向计算机、人工智能、自动化等专业的学生与教师适用于期末课程设计、课程大作业或毕业设计场景。项目聚焦NLP四大基础任务之一的序列标注通过循环神经网络对句子逐字打标签并在LSTM层后引入CRF模型利用梯度下降自动学习转移参数有效避免连续B-LOC等非法标签序列从而提升标注准确率。压缩包共3个文件包含1个py源码、1个pdf作业报告和1个md说明文档整体约201KB源码经本地调试可直接运行报告则完整记录了实验思路与结果分析。目前已有138人学习下载得分96分读者可借此掌握BiLSTM与CRF结合的建模流程、损失函数设计及序列标注评估方法也可在此基础上修改调整以适配不同实体类型或数据集具备较高的学习借鉴价值。1. 从一份 96 分课设说起BiLSTMCRF 到底解决了什么如果你正在搜「双向LSTM 命名实体识别 源码」大概率是两种情况要么课程大作业卡在序列标注这一步要么想找一个能跑通、有报告、结构清晰的参考实现。这份资源就是冲着这个场景来的——一个基于 Python 的 BiLSTMCRF 命名实体识别项目附带作业报告和说明文档得分 96。它要解决的核心问题很具体给定一句「我 爱 北 京」模型要输出「O O B-LOC I-LOC」这样的标签序列把地名、人名、机构名从文本里抠出来。命名实体识别是 NLP 四大基础任务里序列标注的典型代表也是很多人第一次接触「输入输出等长」建模的入口。纯 LSTM 能跑但会冒出「B-LOC 后面跟 B-LOC」这种现实中不存在的标签组合CRF 层就是来兜这个底的。这份源码把 LSTM 的上下文建模能力和 CRF 的标签转移约束拼在一起适合想搞懂序列标注完整链路、又不想从零搭框架的人。2. 拆开 sequence_tagging.py数据流、模型结构与训练循环2.1 从原始句子到嵌入向量输入管线的三个关键决策拿到一份序列标注源码第一件事不是看模型多大而是看数据怎么进、标签怎么对齐。这份项目的输入是一条句子输出是等长的标签序列中间要经过词表映射、序列填充、批次对齐三步。常见做法是先把语料按字切分中文场景下字级比词级更稳避免分词误差传导统计频次建词表低频字统一映射到UNK然后每条样本转成索引序列。真正容易翻车的是填充和 mask。一个 batch 里句子长度不一短句要补PAD到统一长度但PAD位置的损失不能算进总损失否则模型会学着去预测填充位。我一般会在数据预处理阶段就生成一个 mask 向量训练时用它乘掉无效位置的 loss。下面这段是典型的批处理逻辑你可以对照自己的实现看有没有漏掉 maskdef batchify(data, word2id, tag2id, batch_size, pad_tokenPAD): # data: [(words, tags), ...] batches [] for i in range(0, len(data), batch_size): chunk data[i:i batch_size] max_len max(len(w) for w, _ in chunk) word_ids, tag_ids, masks [], [], [] for words, tags in chunk: w_id [word2id.get(w, word2id[UNK]) for w in words] t_id [tag2id[t] for t in tags] # 补齐到 batch 内最大长度 pad_len max_len - len(words) w_id [word2id[pad_token]] * pad_len t_id [tag2id[PAD]] * pad_len mask [1] * len(words) [0] * pad_len word_ids.append(w_id) tag_ids.append(t_id) masks.append(mask) batches.append((word_ids, tag_ids, masks)) return batches这段代码里mask是后续损失计算的关键1表示真实 token0表示填充位。参数上batch_size一般设 16 到 64太小梯度震荡太大显存吃紧pad_token和UNK要分开别混用。如果你发现训练 loss 降得很快但验证集 F1 不动先查 mask 有没有正确传到 loss 函数里。2.2 BiLSTM 编码层为什么双向比单向更适合中文 NER单向 LSTM 只能看到当前字左边的上下文但「北京」这个词光看「北」不知道后面跟的是不是「京」得双向才能同时利用前后信息。BiLSTM 就是把一个正向 LSTM 和一个反向 LSTM 的输出拼起来每个时间步得到2 * hidden_dim维的表示。这份源码里隐藏层维度通常设 128 或 256层数 1 到 2 层足够再深对小数据集反而容易过拟合。实现上要注意两点一是 pack_padded_sequence把填充位压掉再送进 LSTM否则反向 LSTM 会从PAD开始读污染上下文表示二是 dropout 的位置一般加在嵌入层之后和 LSTM 输出之后概率 0.3 到 0.5。下面是一个简化的编码层写法import torch import torch.nn as nn from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence class BiLSTMEncoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers1, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, bidirectionalTrue, batch_firstTrue) self.dropout nn.Dropout(dropout) def forward(self, word_ids, lengths): # word_ids: (B, L), lengths: 每条真实长度 emb self.dropout(self.embedding(word_ids)) packed pack_padded_sequence(emb, lengths, batch_firstTrue, enforce_sortedFalse) out, _ self.lstm(packed) out, _ pad_packed_sequence(out, batch_firstTrue) return self.dropout(out) # (B, L, 2*hidden_dim)padding_idx0让嵌入层不更新填充位的向量enforce_sortedFalse省去手动排序的麻烦。hidden_dim设 128 时输出维度是 256后面接 CRF 之前通常再加一个线性层把维度映射到标签数。如果你用的是中文数据集嵌入维度 128 到 300 都常见预训练词向量能加就加对 F1 提升明显。2.3 CRF 层标签转移矩阵是怎么学出来的LSTM 输出的是每个位置对每个标签的「发射分数」但标签之间不是独立的——「B-LOC」后面接「I-LOC」合理接「B-PER」就不合理。CRF 层引入一个转移矩阵transitions[i][j]表示从标签 i 转到标签 j 的分数训练时这个矩阵和 LSTM 参数一起用梯度下降学。解码时用 Viterbi 算法找全局最优路径而不是每个位置贪心取最大。这份源码里 CRF 的实现有两种常见写法一种是直接调torchcrf这类库一种是手写前向算法和 Viterbi。手写的好处是能看清 loss 怎么算的。核心是forward算所有路径的分数和配分函数viterbi找最优路径。下面这段是手写 CRF 的关键部分class CRF(nn.Module): def __init__(self, num_tags): super().__init__() self.num_tags num_tags # 转移矩阵transitions[i][j] 表示 i - j 的分数 self.transitions nn.Parameter(torch.randn(num_tags, num_tags)) # 非法转移如 START - 非 B在训练中会被压低 self.start_trans nn.Parameter(torch.randn(num_tags)) self.end_trans nn.Parameter(torch.randn(num_tags)) def forward(self, emissions, tags, mask): # emissions: (B, L, num_tags), tags: (B, L), mask: (B, L) # 返回负对数似然训练时最小化 ...transitions是随机初始化的训练几轮后你会看到「B-LOC - I-LOC」的分数明显高于「B-LOC - B-LOC」这就是 CRF 在自动学约束。参数上num_tags等于标签集大小比如 BIO 标注下 LOC/PER/ORG 各两个标签加 O就是 7 个。注意PAD标签不要参与转移计算mask 要传进 CRF 的 loss 里。2.4 训练循环与评估loss 怎么算、F1 怎么看训练循环本身不复杂但序列标注的评估不能只看准确率。因为「O」标签通常占大多数全预测 O 也能有很高的 accuracy但 F1 会惨不忍睹。这份报告里用的应该是实体级别的 precision/recall/F1也就是把预测的标签序列还原成实体区间再和真实区间比对。def train_step(model, crf, batch, optimizer): word_ids, tag_ids, masks batch lengths [sum(m) for m in masks] emissions model(word_ids, lengths) # CRF 的负对数似然 loss crf.neg_log_likelihood(emissions, tag_ids, masks) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() return loss.item()梯度裁剪max_norm5.0是防梯度爆炸的常规操作LSTM 加 CRF 的结构对学习率比较敏感一般用 Adam学习率 1e-3 起步训练 20 到 50 轮看验证集 F1 什么时候不再涨。如果你发现 loss 变成 nan先查学习率是不是太大再查 CRF 的转移矩阵有没有出现极端值。3. 跑通这份源码环境、参数与复现步骤3.1 环境配置Python 版本、依赖与常见安装坑这份项目是纯 Python 实现核心依赖是 PyTorch。环境上建议 Python 3.8 到 3.10太新的版本有些旧版 torch 装不上。依赖清单一般包括torch、numpy、tqdm如果报告里用了预训练词向量可能还有gensim。安装时最容易卡在 torch 的版本和 CUDA 匹配上——如果你没有 GPU直接装 CPU 版就行命令里带cpu后缀。# 创建虚拟环境避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装 CPU 版 torch版本按你 Python 版本选 pip install torch1.13.1 --index-url https://download.pytorch.org/whl/cpu pip install numpy tqdm如果你用 VSCode 或 PyCharm记得把解释器切到刚建的 venv 里否则跑起来会报「No module named torch」。Windows 下路径分隔符和编码问题也常见读语料时统一用encodingutf-8别依赖系统默认编码。3.2 数据格式与标签体系BIO 标注怎么对齐序列标注的数据格式通常是每行「字 标签」句子之间空行分隔。这份项目用的应该是 BIO 标注B- 表示实体开头I- 表示实体内部O 表示非实体。比如「我 爱 北 京」对应「O O B-LOC I-LOC」。你要做的是确认自己的数据标签集和代码里的tag2id一致不一致就改配置别硬跑。标签含义示例O非实体我、爱B-LOC地点实体开头北I-LOC地点实体内部京B-PER人名实体开头张I-PER人名实体内部三标签体系一旦定了训练集、验证集、测试集必须用同一套。常见错误是训练集有 B-ORG 但验证集没有导致评估时除零。我一般会在预处理阶段先扫一遍所有标签打印标签集和频次确认没有遗漏。3.3 参数怎么调嵌入维度、隐藏层、学习率的实操建议这份源码的默认参数能跑但想拿到更好的 F1 得调几个关键项。嵌入维度 128 起步有预训练词向量就设 300隐藏层维度 128 到 256层数 1 层够用学习率 1e-3配合 Adam 和梯度裁剪dropout 0.3 到 0.5数据量小就调大。batch_size 看显存CPU 跑就设 16GPU 可以 64。config { embed_dim: 128, hidden_dim: 256, num_layers: 1, dropout: 0.4, lr: 1e-3, batch_size: 32, epochs: 30, max_norm: 5.0 }调参顺序建议先固定其他项单独调学习率和 dropout。如果训练 loss 震荡大降学习率如果训练 loss 降但验证 F1 不涨加 dropout 或减层数。别一上来就网格搜索小数据集上随机性很大跑三组对比就够了。4. 避坑与排查跑这份源码时最容易翻车的五个地方4.1 现象loss 一直是 nan原因学习率过大或 CRF 转移矩阵溢出解决降学习率并加梯度裁剪这是最典型的翻车现场。BiLSTM 加 CRF 的结构里CRF 的前向算法涉及指数和对数运算如果发射分数或转移分数数值过大logsumexp会溢出。先把学习率从 1e-3 降到 5e-4 或 1e-4再确认clip_grad_norm_有没有加。如果还不行检查 CRF 的transitions初始化是不是用了太大的方差改成torch.randn(num_tags, num_tags) * 0.1试试。4.2 现象验证集 F1 始终为 0原因标签映射错位或 mask 没传进评估解决打印第一条样本的预测和真实标签逐位比对F1 为 0 通常不是模型没学到而是评估代码把标签对错了。先拿一条训练集样本过一遍模型把预测标签序列和真实标签序列都打印出来看是不是整体偏移了一位或者PAD标签混进了实体区间。另一个常见原因是评估时没排除填充位导致实体边界算错。我一般会在评估函数里加一句断言确认预测长度和真实长度一致。4.3 现象训练集 loss 降但验证集 loss 涨原因过拟合解决加 dropout、减层数、早停小数据集上 BiLSTM 很容易过拟合训练 loss 一路降验证 loss 从第几轮开始反弹。这时候别硬训加 dropout 到 0.5隐藏层降到 128或者直接早停——验证 F1 连续 5 轮不涨就停。如果数据量实在小可以考虑冻结嵌入层只训 LSTM 和 CRF 部分。4.4 现象CPU 跑一轮要几小时原因batch_size 太小或没压填充解决用 pack_padded_sequence 并适当加大 batchCPU 上跑 LSTM 本来就慢如果还没用pack_padded_sequence填充位也参与计算时间翻倍。确认编码层有没有压填充batch_size 从 16 加到 32 或 64能明显提速。如果还是慢考虑把隐藏层维度降到 128或者先用小规模数据调试通再上全量。4.5 现象换自己的数据后报 KeyError原因标签集或词表不匹配解决统一用配置文件管理标签映射这份源码的tag2id和word2id大概率是写死或从训练集建的。换数据后如果出现训练集没有的标签就会 KeyError。正确做法是把标签集和词表都存成 json训练和评估都从同一份配置读。别在代码里硬编码标签列表改起来容易漏。5. 从能跑到好用CRF 解码验证与一个提分技巧跑通之后怎么确认 CRF 真的在起作用最直接的办法是对比实验把 CRF 层去掉只留 BiLSTM 加 softmax看 F1 掉多少。如果掉得明显说明转移约束确实在压非法序列。另一个验证方法是拿一条测试样本打印 Viterbi 解码的最优路径分数再和贪心解码的结果比看 CRF 有没有纠正贪心解出来的非法标签组合。# 对比 CRF 解码和贪心解码 with torch.no_grad(): emissions model(word_ids, lengths) # CRF Viterbi 解码 best_path crf.viterbi_decode(emissions, mask)[0] # 贪心解码每个位置取最大发射分数 greedy_path emissions.argmax(dim-1)[0].tolist() print(Viterbi:, best_path) print(Greedy :, greedy_path)如果 Viterbi 路径里没有出现「B-LOC 接 B-LOC」这种组合而贪心路径出现了就说明 CRF 的转移矩阵学到了合理约束。这个对比我每次调完 CRF 参数都会跑一遍比只看 F1 更直观。提分技巧上最划算的是加预训练词向量。中文可以用腾讯词向量或中文维基训练的词向量加载后冻结前几轮再解冻微调。另一个技巧是调整 CRF 的初始转移矩阵把明显非法的转移比如 O 到 I-XXX初始化成很小的值能加快收敛。我一般会在训练前手动把transitions里非法组合设成 -1e4让模型少走弯路。从那以后我每次拿到序列标注项目都强制先跑一遍「去 CRF 对比」和「Viterbi vs 贪心」这两个验证确认 CRF 不是摆设再继续调参。希望帮到你。本文还有配套的精品资源点击获取
返回列表