news 2026/10/9 11:08:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python文本关系抽取实战:HanLP实体识别与三元组提取

简介:这是一套面向自然语言处理初学者与关系抽取实践者的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_labels=4) # 假设关系类别有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_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): logits = model(**inputs).logits pred = torch.argmax(logits, dim=1).item() return relation_labels[pred]

这段代码的关键点在于:用[E1]和[E2]标记两个实体的位置,让模型知道哪两个词是它要判断关系的对象。num_labels=4是关系类别的数量,你需要根据实际业务定义。max_length=128是截断长度,中文句子一般不会太长,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 条句子的测试集,人工核对三元组的准确率和召回率。我踩过太多次「样例看着都对、全量跑完全是错」的坑,这个后悔药不好吃。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 11:07:51

Helm 3.10实战:从手工YAML到模板化部署与回滚

1. 为什么最终选择Helm管理应用:手工YAML到模板化的不归路先说一个很常见的场景:团队里最开始部署 Kubernetes 应用,基本靠 git 仓库里堆一大堆 YAML。大家心照不宣地把deployment.yaml、service.yaml、configmap.yaml一个个kubectl apply -f…

作者头像 李华
网站建设 2026/10/9 11:07:45

Coding Agent 生产级调优:Harness 工程实战与效果提升

1. 从 Vibe Coding 到生产可用:一个 Coding Agent 调优项目的真实起点Vibe Coding 这个词这两年被聊得很多,大意就是“凭感觉写代码”——你给 AI 一个模糊的意图,它帮你把代码补全、把功能搭起来,你只需要在关键节点上做判断。听…

作者头像 李华
网站建设 2026/10/9 11:05:33

OpenWorkMate:开源企业级AI工作伙伴框架,让AI真正能干活

1. 从"只会聊天"到"能干活":企业AI工作伙伴到底缺了什么公司里那套AI工具,我用了快两年,最大的感受就一个字:虚。你问它"帮我写个周报",它能给你整出八百字排比句;你问它&qu…

作者头像 李华
网站建设 2026/10/9 11:05:28

2026软件测试面试MySQL核心考点与避坑指南

MySQL 在软件测试面试里,权重一直不低。不管你是面功能测试还是测开,SQL 基础、索引原理、事务隔离级别、甚至死锁排查,都可能是面试官手里的“常规牌”。尤其这两年行业里卷得厉害,光会 select * from table 已经糊弄不过去了&am…

作者头像 李华
网站建设 2026/10/9 11:05:27

MySQL时间函数匹配实战:从格式化到索引优化避坑指南

做后端开发这些年,凡是涉及统计报表、定时任务、数据对账的话,十有八九都要跟 SQL 时间函数匹配打交道。MySQL 里的日期时间函数不算少,但真正用得上的、也最容易出幺蛾子的,基本上就是那套“格式化、转换、加减、求差、比较”的组…

作者头像 李华