ARTICLE DETAIL

资讯详情

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

实体关系抽取实战:从依赖树到图卷积神经网络的完整实现

实体关系抽取实战:从依赖树到图卷积神经网络的完整实现 简介基于图卷积神经网络的实体关系抽取项目面向深度学习、自然语言处理方向的在校学生、研究人员及企业开发者完整覆盖实体关系抽取中数据预处理、GCN模型构建、训练测试、结果评估与可视化展示的流程。整个资源包共41个文件以Python源码为主体包含17个py脚本和10个pyc文件另提供PyTorch模型权重、JSON配置、文本字典、数据文件、说明文档及部署指南包体大小约93.32MB。项目核心模块聚焦GCN关系抽取涉及词表构建、语法树处理与模型评估并配有可运行的训练与测试脚本、GUI交互界面以及官方评分工具可支持在Windows/macOS环境下复现实验结果。同时包内包含训练好的模型权重与全量数据方便直接进行验证、扩展或继续微调适合课设、毕设等场景。该项目为高分毕业设计源码已有151人学习使用非常适合作为实体关系抽取方向的进阶参考和二次开发基础。1. 实体关系抽取用普通 CNN 还是图卷积句法结构是分水岭实体关系抽取的任务是从一句话里识别出两个实体之间是什么关系并且分清方向。许多人在跑 SemEval 2010 数据集时会发现普通 CNN 和 BiLSTM 对Cause-Effect(e1,e2)和Cause-Effect(e2,e1)这种反向关系经常混淆因为它们依赖的是局部 n-gram 窗口很难表达主语、宾语在句法树上的长距离联系。图卷积神经网络把依赖树构造成邻接矩阵让消息沿树边传播从而把句法路径编码进表示里。这个开箱项目正好是这条技术的完整实现附带源码、部署教程、数据集、训练好的权重和 GUI 演示适合用于毕业设计、课程设计也适合想从数据处理到模型推理完整走一遍的人。2. SemEval 数据预处理从原始语料到依赖树邻接矩阵2.1 项目里数据流是怎么排布的以GCN_relation_extra-master的目录结构为例dataset下一般会分raw_sem、sem、vocab三个目录。raw_sem放原始下载语料sem存放预处理之后的干净句子vocab里是训练时用到的词表。Python 文件里pre_processing.py跑在最前把原始数据转成 loader 能读的格式loader.py负责构造 batchtree.py负责生成依赖树再往后是gcn.py和trainer.py。我第一次拿到这个资源时并没有急着改网络而是先把pre_processing.py的输出打印出来看因为后面模型不管怎么调输入不对一切白搭。2.2 SemEval 2010 Task 8 的 19 类标签这个数据集常见格式是id 实体1 实体2 句子句子里用e1.../e1和e2.../e2标出实体。标签总数不是 10 类而是 19 类因为除了 Other剩下 9 种关系都分为正向、反向。比如Cause-Effect(e1,e2)表示 e1 是原因、e2 是结果Cause-Effect(e2,e1)则是反过来。项目里如果直接做 19 分类就必须在标签映射时保留方向信息不能把两者合并。关系语义说明反向关系示例Cause-Effecte1/e2 的因果关系爆炸导致停电反向是停电源于爆炸Instrument-Agency工具与施动者用锤子砸墙工具和施动者互换方向Product-Producer产品与生产者出版社出版的书Content-Container内容与容器瓶子里装着水Entity-Origin实体与来源来自北京的团队Entity-Destination实体与目的地货物运往上海Component-Whole部件与整体汽车的一个轮子Member-Collection成员与集合球队里的队员Communication-Topic通信对象与主题关于 AI 的会议Other无上述关系默认类别不区分方向这个表格对调试很关键因为最后的 F1 分数是各类别单独统计后再算 macro某个方向混淆会直接拖低成绩。2.3 pre_processing.py 到底在做什么我一般会观察预处理输出的三件事分词粒度、是否保留大小写以及依赖树解析器的选用。这个项目里通常会对原始句子做 token 化再调用tree.py得到每个词的父节点 index。以下代码可以用来验证树是否构建正确def build_tree_mask(tokens, heads): seq_len len(tokens) adj torch.zeros(seq_len, seq_len) for i, head in enumerate(heads): if head 0: adj[i][head] 1 # 从子节点指向父节点 adj[head][i] 1 # 从父节点指向子节点 adj[i][i] 1 # 自环保留自身信息 row_sum adj.sum(dim1, keepdimTrue) adj_norm adj / (row_sum 1e-9) return adj_norm这段代码把依赖树转成无向带自环的邻接矩阵然后按行归一化。很多 GCN 实现都采用无向边原因是有向边对解析错误更敏感如果父节点识别错反向消息还是能兜底一部分。1e-9是为了避免空行除零批量填充出来的PADtoken 那一行只有自环归一化后不会出 NaN。实际项目中pre_processing.py会在写文件前把每个 token、实体标记、邻接矩阵都验证一遍避免到了 loader 阶段才发现解析失败。2.4 词表与自定义 DataLoadervocab.py建立word2idx同时放入PAD、UNK和实体标记。如果预训练词向量是直接加载的文件里还会保存向量矩阵。loader.py则要对句子按 batch 内最大长度做 pad并返回四个张量token ids、mask、邻接矩阵、标签。一个自定义 collate 的关键是这样处理变长def collate_fn(batch): max_len max(len(item[tokens]) for item in batch) ids torch.zeros(len(batch), max_len, dtypetorch.long) adj torch.zeros(len(batch), max_len, max_len) label torch.zeros(len(batch), dtypetorch.long) for i, item in enumerate(batch): cur_len len(item[tokens]) ids[i, :cur_len] torch.tensor(item[tokens]) adj[i, :cur_len, :cur_len] item[adj] label[i] item[label] return ids, adj, labeladj在 pad 区域保持全零这样 GCN 层做批量矩阵乘时不会把PAD的表示引入真实 tokenmask 则用于 BiLSTM 的 packed sequence 或之后的池化。这里需要特别说明全零邻接矩阵会让该 token 的卷积结果变成零向量所以模型输出阶段要保留真实长度不让 pad 区参与分类。loader.py的核心就是解决这件事。3. BiLSTM 图卷积模型如何在依赖树上抽取关系特征3.1 为什么把 BiLSTM 放在 GCN 前面直接用 GCN 吃词向量也可以但效果通常会差很多。词向量给当前词一个孤立表示实体关系却需要依赖语境才能判断方向。BiLSTM 沿时间顺序编码上下文让每个 token 携带前后信息再把这组表示放到 GCN 上做句法聚合。这样 GCN 接收的输入已经不纯粹是词本身而是这个词在当前句子里所处的语义位置。gcn.py里的 embed_size、hidden_size 都集中在constant.py把超参抽出来之后做对照实验会方便很多。3.2 GCN 层的核心计算图卷积层的核心是一个线性变换加一个按邻接矩阵的聚合。常见写法是class GraphConvLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.W nn.Linear(in_dim, out_dim, biasFalse) self.bias nn.Parameter(torch.zeros(out_dim)) def forward(self, x, adj): support self.W(x) # [batch, seq_len, out_dim] output torch.bmm(adj, support) self.bias return F.relu(output)W把所有节点表示映射到新的维度bmm(adj, support)等价于对每个节点把邻居节点的变换结果做加权和。因为前面已经做了行归一化所以可理解为求邻居平均。这里要注意bias是每个输出通道共享的而不是每个节点独立如果给每个节点加独立 bias参数数量会失控而且对变长序列不友好。工程上更实用的做法是每层 GCN 之间插入Dropout(0.2~0.5)比单纯增加隐藏维度更有效。3.3 实体位置信息怎么注入方向敏感关系要求模型知道谁是 e1、谁是 e2。这个项目的常见做法是在 token 序列中插入类似e1、/e1的特殊标记并让这些标记也参与 embedding 训练。这样 BiLSTM 和 GCN 都能感知实体边界。模型最后从 GCN 输出里把 e1 范围内 token 的向量和 e2 范围内 token 的向量分别做最大池化再拼接后送给全连接分类器。也可以直接取第一个实体 token 的向量但那样会丢失实体内部多词搭配的信息。从实际结果看带实体标记的池化方式对Member-Collection这类需要完整实体语义的关系更友好。3.4 多层图卷积与路径裁剪单独一层 GCN 只能拿到直接父节点和子节点的信息。比如The fire caused the damage如果fire和damage在树上的距离是 2一层 GCN 无法把二者信息聚合到一起。解决办法有两种堆多层图卷积或者把两个实体之间的依赖路径单独建子图。项目配置通常把gcn_layers设置为 2 或 3消息在树边上往返传播两次后相当于两个相邻 token 和共同邻居都参与了聚合。继续叠层收益会递减反而会把无关修饰语和附属从句的噪声带进表示。第二种做法是后续改造时最常见的优化不把整棵句法树都作为图而是先取实体节点在依赖树上的最短路径只保留路径上的节点或者额外向外扩一跳。路径受限的好处是减少与关系无关的修饰词干扰训练收敛更快在长句上尤其明显。拿到这个项目后可以先在tree.py之后加一个路径裁剪函数再对比裁剪前后的宏 F1通常能稳定提升 1 到 2 个点。4. 训练器和超参数从 tem_checkpoint 到 best_model4.1 训练入口和 constant.py 里的默认值train.py负责组装模型、加载 loader、调用trainer.py。constant.py通常会把学习率、batch_size、hidden_size、dropout、epoch 数全部抽出来方便反复修改。我在微调时习惯用命令行参数覆盖默认值典型训练命令如下python train.py \ --data_dir dataset/sem \ --vocab_dir dataset/vocab \ --embed_dim 300 \ --hidden_dim 200 \ --gcn_layers 2 \ --dropout 0.3 \ --batch_size 32 \ --epochs 30 \ --lr 0.001如果train.py没有提供 argparse 入口直接改constant.py里的同名常量效果一样。hidden_dim 200配合batch_size 32在普通显卡上就能跑得动。对 SemEval 这种只有 8000 条训练句的数据集来说模型容量不需要很大网络更深反而更容易过拟合。4.2 关键超参数表参数常见取值调整倾向embed_dim100-300使用 300 维预训练向量时效果最好但显存占用更高hidden_dim100-200建议不超过 256否则小数据集上很容易过拟合gcn_layers2-3层数太少覆盖不了长距离依赖太多会把噪声传开dropout0.2-0.5GCN 层之间加 dropout比只在 embedding 加更有效learning_rate1e-3 或 5e-4Adam 通常从 1e-3 起步发现 loss 震荡就降到 5e-4batch_size16-32显存不够时先降 batch_size再考虑降 hidden_dim损失函数直接用nn.CrossEntropyLoss()就可以不需要为方向关系额外加权因为 19 类中 9 组方向关系样本数相对均衡。Other 类占比大如果模型倾向把所有不确定样本都丢给 Other可以在损失函数里把 Other 类权重压到 0.5 左右测试时再看 macro F1 变化。4.3 为什么会有两个 checkpoint 文件tem_checkpoint.pt和best_model.pt的区别在保存时机。前一个通常每个 epoch 或固定 step 覆盖一次只用于断点续训后一个在验证集 F1 提升时才覆盖是最终要保留的模型。加载时要特别注意键名checkpoint torch.load(best_model.pt, map_locationcpu) model.load_state_dict(checkpoint[model_state]) optimizer.load_state_dict(checkpoint[optimizer_state]) start_epoch checkpoint[epoch] 1map_locationcpu是关键尤其在一台机器上训练、另一台机器上测试的情况。如果不加这个参数模型里残留的 CUDA 设备信息会让 CPU 环境直接报错。optimizer_state只在断点续训时需要纯推理可以不加载但训练恢复时如果不恢复 optimizer 的动量状态学习率衰减和动量信息都会错位。另外checkpoint[epoch]要接住否则接着训练时可能破坏已经算好的学习率调度。4.4 验证集与早停SemEval 2010 官方训练集约 8000 句测试集 2717 句。项目里通常会在训练集里单独切 10% 做验证而不是直接用官方测试集选择模型否则选择的顶点会带上测试集噪声。trainer.py的循环会统计准确率和 macro F1当验证集 F1 连续 3 到 5 个 epoch 不再上升时停止训练并回滚到最佳状态。这种早停策略对 GCN 尤其重要因为句法树特征本身结构性强后期 epoch 很容易过度拟合训练集中的标点习惯和表达方式。5. 官方评测脚本与测试输出score.txt 里每个数字的来路5.1 test.py 和 run_test.py 的分工这个资源里同时有test.py和run_test.py。按项目常见的维护习惯test.py适合跑单条数据或小批量验证run_test.py负责对整个测试集生成预测文件。两个脚本都会先加载词表和 checkpoint再做模型推理。区别是后者最后会调用官方 Perl 评分脚本把结果写进score.txt。如果你的目标是复现论文分数直接看run_test.py的输出路径就可以不需要自己造测试轮子。5.2 调用评分脚本的命令在解压后的目录里可以按下面顺序执行python run_test.py --checkpoint best_model.pt --output resoult.txt perl semeval2010_task8_scorer-v1.2.pl resoult.txt test_key.txt score.txtresoult.txt必须保持与测试集相同的行序每行一个预测标签。官方评分器对格式要求非常死板多一个空格、多一个空行、标签大小写不一致都会导致该行按错误处理。常见错误是把Other写成other或者把反向关系写成不带括号的形式。这类细节在跑分前要统一做一遍字符清洗建议用sed -i s/[[:space:]]*$// resoult.txt去掉行尾空格。5.3 精准率、召回率、macro F1 怎么统计评分器统计的是 macro F1不是准确率。它会对除了 Other 之外的 18 个方向性标签分别计算 precision 和 recall然后求平均。score.txt里通常前 18 行是每个关系方向的结果最后一行是 overall。如果你只盯准确率会被 Other 类别误导测试集中 Other 占比多模型把难样本切成 Other 反而能掩盖错误。所以调参要始终看score.txt最后的 F1 值而不是看 loss 或准确率。5.4 读 resoult.txt 做错误分析借助混淆矩阵可以快速定位问题类别。一个简洁的分析脚本长这样from collections import defaultdict def build_confusion(pred_file, gold_file): preds [line.strip() for line in open(pred_file)] golds [line.strip() for line in open(gold_file)] pairs defaultdict(int) for p, g in zip(preds, golds): pairs[(g, p)] 1 return pairs pairs build_confusion(resoult.txt, test_key.txt) for (g, p), cnt in sorted(pairs.items()): if g ! p and cnt 0: print(f{g} - {p}: {cnt})最后打印出来的通常是一大批真实关系被预测成 Other。SemEval 测试集里 Other 占比高模型只要不确定就会往 Other 靠所以先看这些样本的依赖树是不是解析错了如果Entity-Origin和Entity-Destination经常互相混则说明池化阶段没有抓住实体相对位置信息可以考虑把实体间的距离特征拼接到分类层输入。这个分析手段比单独看 F1 更能定位问题。6. 部署 GUI 与推理检查把 best_model.pt 真正用起来6.1 启动 GUI.py 的前置条件项目里保留的GUI.py可以直接用python GUI.py启动。启动前先确认三件事词表必须和训练时一致best_model.pt路径能访问依赖树解析器已经初始化好。很多部署报错不是出在模型结构而是出在词表不匹配GUI 加载到不认识的 token 时会直接映射到UNK关系结果自然不准。6.2 模型推理的核心路径model.eval() with torch.no_grad(): ids, adj preprocess_sentence( The fire caused the damage, e1(2, 3), e2(4, 5) ) logits model(ids.unsqueeze(0), adj.unsqueeze(0)) pred torch.argmax(logits, dim-1).item() print(idx2rel[pred])preprocess_sentence会复用训练时的vocab.py和tree.py所以 GUI 与训练时的分词规则必须一致。同一个句子训练时拆成 5 个 tokenGUI 里拆成 4 个 token后面张量长度对不上模型即使能跑结果也完全不可复现。逻辑层里显式调用model.eval()和torch.no_grad()是为了关闭 dropout 和梯度缓存这会让预测结果稳定下来否则多次点击 GUI 可能出现概率差异。6.3 部署时容易被忽略的 checkpoint 路径问题在 Mac 或无 NVIDIA GPU 机器上torch.load默认保留训练时的设备指针所以加载时务必加上map_locationcpu。如果 GUI 内部用了 MPS 设备还需要把ids和adj同时.to(device)只移 ids 不移动邻接矩阵是常见的 CPU/GPU 张量不匹配来源。把这些路径和参数对齐后GUI 输出的关系类型就能和resoult.txt里的结果对应上。本文还有配套的精品资源点击获取
返回列表