首页 / 资讯中心 / 文章详情

Python文本关系抽取实战:HanLP实体识别与三元组提取

Python文本关系抽取实战:HanLP实体识别与三元组提取 ★ FEATURED ARTICLE
简介这是一套面向自然语言处理初学者与关系抽取实践者的Python工具源码基于HanLP完成实体识别、语义角色标注与依存句法分析最终输出三元组结果覆盖event施事者—谓语—受事者、svo主谓宾、keyword关键词、freq高频词、ner实体词、coexist实体共现及ner_keyword实体与关键词关联等多种抽取模式适合用于信息抽取课程实验、知识图谱构建前期数据准备与文本分析项目。资源包共66个文件以35个py源码为核心辅以16个md说明文档、4个txt配置、3张png与1张jpeg示意图另有json、yaml、css、js、license、pdf等配套文件压缩包约1.76MB目录按utils、uie、examples、tests、docs等模块划分结构清晰。已有432人学习下载。读者可从中获得可直接运行的抽取脚本、UIE模型微调与预测示例、TextRank关键词提取实现、单元测试用例及论文参考便于快速复现三元组抽取流程并在此基础上二次开发。1. 从一句中文里抽出三元组这套 Python 工具到底在解决什么你手里有一堆中文句子比如「张三于 2021 年加入北京字节跳动科技有限公司担任算法工程师」老板要你把里面的人和公司、任职关系、时间都结构化出来存进图数据库或者喂给下游的风控、知识图谱、推荐系统。人工标一天标不了几百条。正则中文表达千变万化写到最后你自己都不信。这时候文本关系抽取就派上用场了——它的目标很朴素把非结构化文本变成(头实体, 关系, 尾实体)这样的三元组比如(张三, 任职于, 北京字节跳动科技有限公司)。这套 Python 实现的文本关系抽取工具核心思路是先用 HanLP 做实体识别拿到句子里的机构名、人名、地名这些实体再在实体对之间判断关系最终输出三元组。它适合谁适合手上有中文语料、想快速搭一个能跑通的关系抽取流水线、又不想一上来就训大模型的工程师。你不需要 GPU 集群一台装了 Python 的笔记本就能把最小闭环跑起来。下面我按「先立住原理、再动手复现、最后讲坑」的顺序把这条链路拆开讲清楚。2. HanLP 实体识别先让机器认出句子里有谁2.1 为什么实体识别是关系抽取的前置条件关系抽取不是凭空判断两个词有没有关系它必须先知道「谁是实体」。如果连「北京字节跳动科技有限公司」是一个机构名都认不出来后面判断它和张三的关系就无从谈起。所以整条流水线的第一步永远是命名实体识别NER。HanLP 在这件事上的优势是中文开箱即用。它内置了预训练的中文 NER 模型能识别人名PER、地名LOC、机构名ORG等常见类型而且 API 设计得很直白不需要你从头写 BERT 微调脚本。常见做法是先用 HanLP 把句子切分并标注实体拿到每个实体的文本、类型和在句中的起止位置再把这些实体两两组合交给关系分类模块。这里有个选型理由值得说清楚为什么不用 jieba 自定义词典因为 jieba 只做分词不做实体类型标注你得自己维护一堆规则去判断「这个词是不是机构名」维护成本随语料增长迅速失控。HanLP 把分词和 NER 打包在一起省掉大量脏活。当然如果你的领域特别垂直比如医疗、法律HanLP 的通用模型可能认不准专业术语那就需要后面用自定义词典或微调来补。2.2 用 HanLP 跑通实体识别的最小代码先装依赖。HanLP 的 1.x 和 2.x API 差别很大这里用 2.x 的写法因为它对多任务流水线支持更好pip install hanlp然后写一个最小脚本输入一句话输出识别到的实体import hanlp # 加载中文 NER 模型首次运行会自动下载模型文件 # 如果网络受限可以提前把模型放到本地缓存目录 ner hanlp.load(hanlp.pretrained.ner.MSRA_NER_ELECTRA_SMALL_ZH) text 张三于2021年加入北京字节跳动科技有限公司担任算法工程师 # 调用模型返回的是 [(实体文本, 实体类型, 起始位置, 结束位置), ...] result ner(text) for entity in result: print(f实体: {entity[0]}, 类型: {entity[1]}, 位置: {entity[2]}-{entity[3]})这段代码的逻辑很直接hanlp.load加载预训练模型ner(text)返回实体列表。每个实体是一个四元组第一个元素是实体文本第二个是类型标签后两个是字符级的位置索引。位置信息很关键因为后面做关系判断时你需要知道两个实体在句子里的相对顺序和距离。参数说明MSRA_NER_ELECTRA_SMALL_ZH是在 MSRA 数据集上训练的轻量级中文 NER 模型体积小、推理快适合本地跑。如果你对精度要求更高可以换成更大的模型但推理速度会下降。首次加载会下载模型文件如果公司内网下载不了就手动把模型文件放到~/.hanlp目录下。跑完你会看到类似输出实体: 张三, 类型: PERSON, 位置: 0-2 实体: 2021年, 类型: DATE, 位置: 3-8 实体: 北京字节跳动科技有限公司, 类型: ORGANIZATION, 位置: 10-22拿到这些实体第一步就算完成了。但注意HanLP 返回的实体类型标签在不同模型里可能不一样有的用PERSON有的用PER写代码时不要硬编码类型字符串最好先打印出来确认。2.3 实体识别的三个必调参数第一个是模型选择。HanLP 提供了多个 NER 模型从轻量到重量都有。我的经验是先用小模型跑通流程确认整条链路没问题再根据实际语料的准确率决定要不要换大模型。不要一上来就上最大的模型调试阶段慢得让你怀疑人生。第二个是batch_size。如果你要处理几万条句子逐条调用ner(text)会非常慢。HanLP 支持批量输入把句子列表传进去它会自动批处理。批量大小根据你的内存调一般 32 或 64 起步。第三个是自定义词典。HanLP 允许你加载自定义词典来补充领域实体。比如你的语料里全是药品名通用模型认不出来就可以把药品名列表喂进去。这个功能在 2.x 里通过hanlp.load的dict_force参数或者后处理来实现具体写法看你的 HanLP 版本。3. 从实体对到三元组关系判断的两种落地路径3.1 规则匹配快但脆适合冷启动拿到实体列表后最直接的关系判断方式就是规则匹配。核心思路是如果两个实体之间的文本片段里出现了某些关系触发词就判定它们之间存在对应关系。比如「张三」和「北京字节跳动科技有限公司」之间出现了「加入」就判定关系是「任职于」。# 定义关系触发词表 relation_patterns { 任职于: [加入, 入职, 担任, 就职于], 出生于: [出生于, 生于, 籍贯], 毕业于: [毕业于, 就读于, 求学于], } def extract_by_rules(text, entities): triples [] # 实体两两组合 for i in range(len(entities)): for j in range(len(entities)): if i j: continue head entities[i] tail entities[j] # 取两个实体之间的文本片段 start min(head[3], tail[3]) end max(head[2], tail[2]) between text[start:end] # 检查触发词 for relation, keywords in relation_patterns.items(): if any(kw in between for kw in keywords): triples.append((head[0], relation, tail[0])) return triples这段代码的逻辑是遍历所有实体对取它们在句子中的中间文本如果中间文本包含某个关系的触发词就生成一条三元组。参数方面relation_patterns是你可以根据业务不断扩充的词典between的截取范围决定了你检查的上下文窗口大小。规则匹配的优点是快、可解释、不需要训练数据。缺点是脆——换个说法就失效了。比如「张三加入了北京字节跳动科技有限公司」能匹配到「张三成为北京字节跳动科技有限公司的一员」就匹配不到因为触发词表里没有「成为……一员」。所以规则匹配适合冷启动阶段先跑通流程别指望它覆盖所有情况。3.2 模型分类用预训练模型做关系判断当规则覆盖不住的时候就得上模型。关系分类本质上是一个句子级分类任务给定一个句子和两个实体判断它们之间是什么关系。常见做法是用预训练语言模型比如 BERT做微调把两个实体的位置信息编码进去。HanLP 本身也提供了一些关系抽取的预训练模型但更通用的做法是自己搭一个分类器。下面是一个用 HuggingFace 的 transformers 库做关系分类的简化示例from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch # 加载中文预训练模型和分词器 model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels4) # 假设关系类别有4种任职于、出生于、毕业于、无关系 relation_labels [任职于, 出生于, 毕业于, 无关系] def classify_relation(text, head, tail): # 用特殊标记把两个实体标出来 marked_text text.replace(head, f[E1]{head}[/E1]).replace(tail, f[E2]{tail}[/E2]) inputs tokenizer(marked_text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): logits model(**inputs).logits pred torch.argmax(logits, dim1).item() return relation_labels[pred]这段代码的关键点在于用[E1]和[E2]标记两个实体的位置让模型知道哪两个词是它要判断关系的对象。num_labels4是关系类别的数量你需要根据实际业务定义。max_length128是截断长度中文句子一般不会太长128 够用。参数说明model_name可以换成任何 HuggingFace 上的中文预训练模型比如hfl/chinese-roberta-wwm-ext。num_labels必须和你的关系类别数一致否则训练时会报错。推理时torch.no_grad()关掉梯度计算省内存。模型分类的优点是泛化能力强能处理规则覆盖不到的表达。缺点是需要标注数据来微调而且推理速度比规则慢。我的建议是规则和模型结合使用规则先过滤掉明显的关系模型处理剩下的疑难杂症。3.3 三元组去重与后处理不管用哪种方式你都会遇到重复三元组的问题。比如一句话里「张三」出现了两次或者多个触发词同时匹配到同一个关系。去重逻辑很简单def deduplicate(triples): seen set() unique [] for t in triples: key (t[0], t[1], t[2]) if key not in seen: seen.add(key) unique.append(t) return unique除了去重还要做实体对齐。比如「北京字节跳动科技有限公司」和「字节跳动」可能指向同一个实体如果不做归一化图谱里会出现两个节点。常见做法是维护一个别名表或者用编辑距离做模糊匹配。这一步没有银弹需要根据你的数据特点来调。4. 避坑指南这套流水线最容易翻车的五个地方4.1 实体边界切错导致关系判断全错现象HanLP 把「北京字节跳动科技有限公司」识别成了「北京字节跳动」和「科技有限公司」两个实体导致关系判断时头实体不完整三元组变成(张三, 任职于, 北京字节跳动)。原因通用 NER 模型对长机构名的边界识别不稳定尤其是包含地名前缀的机构名。解决加载自定义词典把已知的机构全称加进去强制 HanLP 优先匹配长实体。或者在 NER 输出后加一层后处理如果两个相邻实体类型相同且位置连续就合并成一个。4.2 关系触发词被实体本身包含现象句子是「张三毕业于北京大学」触发词表里有「毕业于」但「北京大学」这个实体本身包含了「毕业」两个字导致规则匹配时误判。原因规则匹配检查的是两个实体之间的文本但如果触发词出现在实体内部逻辑就乱了。解决在截取between文本时严格排除实体本身占用的字符区间。另外触发词匹配时加一个优先级先匹配长触发词再匹配短触发词避免「毕业」覆盖「毕业于」。4.3 批量推理时内存溢出现象用 HanLP 处理一万条句子跑了几百条之后程序被系统杀掉。原因逐条调用模型时每次都会创建新的计算图Python 的垃圾回收跟不上内存持续增长。解决改用批量输入把句子分成每批 32 或 64 条一次性传给模型。同时在每批处理完后手动调用gc.collect()。如果还是不够就换更小的模型或者把处理逻辑拆成多个进程。4.4 关系类别不平衡导致模型只预测「无关系」现象训练关系分类模型时准确率看起来很高但实际预测出来全是「无关系」。原因标注数据里「无关系」的样本占了绝大多数模型学会了偷懒全部预测成多数类就能拿到高准确率。解决训练时对少数类做过采样或者用 focal loss 替代交叉熵。评估时不要只看准确率要看每个类别的 F1 值。如果某个关系的 F1 低于 0.5说明模型根本没学会需要补充该类别的标注数据。4.5 三元组方向搞反现象输出(北京字节跳动科技有限公司, 任职于, 张三)方向完全反了。原因实体对遍历时没有区分头尾或者关系定义本身没有明确方向。解决在关系定义阶段就明确方向比如「任职于」的方向是「人 → 机构」。代码里根据实体类型来决定谁做头谁做尾而不是盲目遍历所有组合。如果关系是对称的比如「同事」那就不需要区分方向但要在输出时统一格式。5. 进阶技巧用依存句法把关系抽取的准确率再提一档规则和模型都跑通之后如果你还想再压榨准确率可以引入依存句法分析。HanLP 本身也提供依存句法分析功能能告诉你句子中词与词之间的语法关系比如主谓宾、定中关系等。把句法信息和实体信息结合起来关系判断会准很多。具体做法是先用 HanLP 做依存句法分析拿到每个词的依存弧。然后找到两个实体在依存树中的最低公共祖先节点如果这个祖先节点是动词而且两个实体分别是该动词的主语和宾语那它们之间大概率存在某种关系。比如「张三加入公司」中「加入」是动词「张三」是主语「公司」是宾语关系方向就是「张三 → 加入 → 公司」。import hanlp # 加载依存句法分析模型 dep hanlp.load(hanlp.pretrained.dep.MSRADEP_ELECTRA_SMALL_ZH) text 张三于2021年加入北京字节跳动科技有限公司 result dep(text) # result 包含词、词性、依存关系等信息 for word, pos, dep_rel, head_idx in result: print(f词: {word}, 词性: {pos}, 依存关系: {dep_rel}, 中心词索引: {head_idx})拿到依存树后你可以写一个函数输入两个实体的位置输出它们在依存树中的路径。如果路径符合「主语 → 动词 → 宾语」的模式就把动词作为关系触发词方向由主语指向宾语。这个方法比纯规则匹配准得多因为它考虑了句子的语法结构而不是简单地看两个实体之间有没有出现某个词。参数说明MSRADEP_ELECTRA_SMALL_ZH是在 MSRA 数据集上训练的依存句法模型和 NER 模型配套使用效果最好。head_idx是中心词在句子中的索引-1 表示根节点。解析结果里每个词是一个四元组顺序是词、词性、依存关系、中心词索引。这个技巧的代价是推理速度会慢一些因为多跑了一个模型。但如果你对准确率的要求高于吞吐量这点开销是值得的。我一般会在离线批处理场景用这套组合在线实时场景还是以规则为主、模型为辅。最后说个我自己的习惯每次改完关系抽取逻辑不要只看几条样例就下结论一定要跑一个包含至少 200 条句子的测试集人工核对三元组的准确率和召回率。我踩过太多次「样例看着都对、全量跑完全是错」的坑这个后悔药不好吃。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站