从文本到知识图谱:实体识别与关系抽取的构建实践
2026/9/9 3:06:20 网站建设 项目流程

简介:KnowledgeGraph Builder是一套面向自然语言处理与知识图谱开发者的端到端开源工具,能从非结构化文本中自动抽取实体、解析共指、识别关系,并生成RDF三元组知识图谱,有效缓解人工知识库覆盖率不足、维护成本高的问题。资源包共106个文件,大小仅3.12MB,包含38个Python脚本用于核心算法,20个CSV数据集供验证,10个Jupyter Notebook提供分步讲解,另有C源文件、Shell脚本与配置文件,整体结构清晰,方便按模块学习和改造。目前已有299人下载学习,适合有一定Python基础的NLP学习者、知识图谱方向研究生或工程师。内置《星球大战》文本作为演示案例,完整覆盖实体识别、共指消解、关系抽取、实体链接等环节,配合Notebook与数据文件,可帮助读者快速跑通项目并在自备语料上实践,深入理解无监督知识图谱构建技术细节。 说实话,我第一次把一篇文章拆成一张图的时候,感觉特别奇妙。这也正是 KnowledgeGraph_Builder 这个项目想做的事:把散落的非结构化文本,变成能查询、能计算的知识图。做这个项目之前,我在整理一批产品文档和用户评论,文档不短,但想回答“谁和谁是什么关系”要比想象中难得多。后来干脆写了这个脚手架,一路解决了清洗、实体识别、关系抽取、图谱入库,最后终于能用图回答问题。如果你也在处理大量文本,想快速抽出实体关系,或者想弄懂知识图是怎么从零搭起来的,这篇内容应该能给你不少可复用的思路。

1. 为什么我要写 KnowledgeGraph_Builder

1.1 非结构化文本为什么难处理

一篇文章看起来很顺畅,但计算机看到的只是一串字符。没有表格、没有外键、没有明确的关系字段。比如某篇手机评测里写:“A14芯片的性能比上一代提升了20%,同时续航时间长了两个小时”。要回答“谁提升了什么”“提升了多少”“和谁比”,对模型来说其实是三个独立问题。实体、属性、数值、比较关系,全部散落在语法结构里,传统关键词检索只能把含有“A14”“性能”的句子找出来,却不能直接告诉你结论,更不可能画出一张关系图。

这也是知识图价值的起点。知识图本质上是用“节点”和“边”重新表达文本:节点是实体,比如产品、公司、人物;边是关系,比如“发布”“包含”“提升了”。一旦文本被改写成这种结构,很多问题就从“自然语言理解和匹配”变成了“图上的邻接查询”,后者要简单得多。比如“哪些产品采用了A14芯片”,在知识图里就是找“A14芯片”的所有“采用”类型入边;再比如“某公司过去一年发布了什么”,也只是一次简单的图遍历。

1.2 知识图到底能解决什么问题

我最终需要的不只是一个演示,而是一个能快速落地到具体领域的“从文本到图”的工具链。KnowledgeGraph_Builder 的目标就是:输入一堆文本,输出由实体、关系、属性组成的图。不追求大而全,但希望在一个垂直领域里能稳定跑通。

做这个项目之前,我试过直接用现成的知识图谱平台,比如把文本丢进去自动抽取,但效果很不稳定。主要原因在于,通用模型不了解领域词汇,比如产品代号、内部名称、版本号,经常被识别成普通单词,甚至会拆成多个词。后来我意识到,不能指望一个黑盒模型解决所有抽取问题,必须把“词典、规则、句法分析、可选的模型”组合起来,让每一步都可解释、可调整。项目名称叫 Builder 而不是 Server,也是这个原因:它更像一套搭建知识图的脚手架,而不是某种开箱即用的控制台。

2. 整体设计:从文本到图形的四个阶段

整个 pipeline 很朴素:清洗 -> 分句 -> 实体识别 -> 关系抽取 -> 入库/可视化。看起来像是几个 NLP 任务的简单拼装,但实际做起来,每个环节都有坑。先说整体设计,后面再给代码。

2.1 文本预处理与分句

关系抽取基本是以句子为单位的,所以分句质量直接影响结果。我刚开始直接用句号分割,结果英文缩写、小数点、引号把句子切得稀碎。后来逐步加上规则:只有句号后面跟着空格和大写字母时才认为是句子边界;遇到换行、分号、问号、感叹号也切分。中文还要额外处理引号里的句号,不能一看到“。”就断句。

清洗阶段也要做扎实。我遇到最多的是从 PDF 或网页里复制出来的文本,常有页眉页脚、表格噪点、多余空行,甚至还有“Table 1: xxx”这类标题插在段落中间。我的做法是先把整段文本按块切分,过滤掉内容过长或过短的异常块,再统一空格和换行符,最后把“图1”“表2”这类孤立标题单独标记,避免它们混入正文形成错误实体。

2.2 实体识别:让字符变成节点

实体识别的目标是给每个句子标注出“谁、什么”。我尝试过三条路:纯正则和词典、预训练 NER 模型、大语言模型。最终选的是混合方案:词典优先,模型兜底。

词典适合领域专有词,比如产品名、公司名、技术名词;正则擅长处理版本号、日期、货币金额。通用人名、地名、机构名交给预训练模型,比如 spaCy 的en_core_web_sm,或者中文场景用zh_core_web_sm。这样搭配的原因是:纯规则召回低,只要词典里没有的词就全丢;纯模型对特定领域不准,我测试的时候,产品代号“KG-12”被识别成“12”和“KG”两个 token,完全没有用。

2.3 关系抽取:让节点连起来

关系抽取是整个项目最难的部分。我早期陷入过一个误区:一上来就上复杂模型。后来发现,在一个垂直领域里,规则和句法模板往往能覆盖很大比例的高价值关系。

最基础的模式是“触发词 + 句法位置”。比如句子“OpenAI released GPT-4”,触发词是“released”,它的主语是发布者,宾语是发布物,于是得到三元组(OpenAI, release, GPT-4)。同样的方式可以扩展到“发布”“推出”“包含”“导致”“提升”等动词。这一步不要求理解整段文章,只要找到动词前后的核心名词短语即可。把规则定义好之后,再用依存句法分析去定位主语和宾语的位置,比单纯用空格切词靠谱得多。

2.4 图谱存储与可视化:让结构可被消费

实体和关系都抽出来之后,需要入图。小规模实验我用 NetworkX,直接在内存里用 dict of list 存储,方便算度、连通分量、社区发现。当数据量到了几十万条三元组,我会导入 Neo4j,因为 Cypher 查询邻接关系非常方便,也能直接对接可视化前端。

为了不把业务逻辑绑死在具体图数据库上,我在项目里先定义了一个统一的三元组 JSON 格式,类似{"head": "...", "relation": "...", "tail": "..."},后面再按需转成 NetworkX 的边或 Neo4j 的 relationship。这一步看似多了一层,但在后面换存储、做图融合时省了很多事。

3. 核心实现细节与代码拆解

这一节给出两个关键实现:混合实体识别器,以及基于依存句法的关系抽取器。代码是简化版,但思路和线上版本一致。

3.1 实体识别器的实现思路

我用 spaCy 作为基础框架,用 EntityRuler 把领域词典注入到 pipeline 里。这样既能保留通用模型对“人、组织、地点”的识别能力,又能保证“KnowledgeGraph_Builder”这类领域词不会被切碎。

import spacy from spacy.pipeline import EntityRuler nlp = spacy.load("en_core_web_sm") # 自定义领域实体 patterns = [ {"label": "PROD", "pattern": [{"LOWER": "knowledge"}, {"LOWER": "graph"}, {"LOWER": "builder"}]}, {"label": "PROD", "pattern": [{"LOWER": "neo4j"}]}, {"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}, ] ruler = EntityRuler(nlp) ruler.add_patterns(patterns) nlp.add_pipe("entity_ruler", name="domain_ruler", before="ner") text = "KnowledgeGraph_Builder is built on top of spaCy and can extract entities from Neo4j docs." doc = nlp(text) for ent in doc.ents: print(ent.text, ent.label_)

这段代码里有个细节值得说:before="ner"不是随便写的。EntityRuler 如果放在 NER 之前,会先标记领域词,后面模型再做通用实体识别,避免模型把已经合并好的专名拆开。如果放在 NER 之后,那么“KnowledgeGraph_Builder”很可能已经被模型切成了两个或三个词,再用 ruler 匹配就不容易了。

在实际项目中,词典不是手工敲进代码的,而是从语料库自动统计加人工审核生成的。我会先用高频名词短语统计跑一遍,抽出 top 500 词,再过滤掉停用词和通用词,最后把保留下来的词作为词典初稿。这份词典配合正则,能解决大部分产品名和内部代号问题。

3.2 基于依存句法的关系抽取

实体识别得到节点之后,关系抽取要解决边。我给出的方案是:遍历句子的依存树,找触发词,再看触发词的主语和宾语。

def extract_triples(doc): triples = [] trigger_words = {"release", "publish", "include", "cause", "improve"} for token in doc: lemma = token.lemma_.lower() if lemma not in trigger_words: continue subject = None object_ = None for child in token.children: if child.dep_ in ("nsubj", "nsubjpass"): subject = child.text if child.dep_ in ("dobj", "attr", "acomp"): object_ = child.text if subject and object_: # 简单清洗,去掉空白和引号 triples.append((subject.strip().lower(), lemma, object_.strip().lower())) return triples # 示例 text = "OpenAI released GPT-4 in March 2023." doc = nlp(text) print(extract_triples(doc)) # 预期输出: [("openai", "release", "gpt-4")]

这里为什么不直接看token.text?因为动词可能有不同时态,比如 “released”“releases”“releasing”,统一用lemma_做归一化,图谱里就不会出现三种不同的“发布”边。对中文场景,可以用 stanza 或者 HanLP 的依存句法,原理是类似的。

不过规则抽取的覆盖度有限。如果触发词不在列表里,关系就抽不到。我的做法是给触发器加一层“泛化”:某些后缀近义的词自动归并,比如“发布”“推出”“上线”都映射到publish;“提升”“增长”“增加”映射到increase。这样图里的关系类型不会爆炸,后面的查询和统计才可控。

4. 避坑指南:我踩过的5个坑

这部分是整篇文章里我最想分享的内容。因为这些坑在官方文档里几乎不会写,但只要跑一遍真实数据就一定会遇到。

4.1 同一实体有多种写法

“Neo4j”“neo4j”“Neo4j Inc.”“Neo4j 图数据库”在原始文本里可能分别出现,如果不做实体归一化,图中就会出现多个其实指向同一现实对象的节点,查询结果就会散掉。

我后来加了一个实体归一化层:先做大小写折叠,再去掉常见后缀词,比如“公司”“科技”“Inc.”“Corp.”;然后用字符相似度做模糊匹配,相似度超过 0.85 就自动合并。这个阈值是从实验结果里选的,太低了会误合并“OpenAI”和“Open AI”,太高了又合不了“KG_Builder”和“KG Builder”。

4.2 关系方向和重复边

关系方向是新手最容易忽略的问题。依存句法里,主语自然成为关系的起点,宾语是终点。但换一个被动句,方向就反了。比如“GPT-4 was released by OpenAI”,依存关系会显示 GPT-4 是 nsubj,如果不做处理,抽出来的三元组就成了(GPT-4, release, OpenAI),完全反了。

我的解决办法是:对被动结构做专门判断,检测nsubjpassauxpass,如果出现就交换头尾实体。同时入库前去重,同一对实体之间只保留最频繁的关系方向,避免图中出现一对双向冗余边。

4.3 数字、版本号被切碎

领域文本中有大量类似“GPT-4”“iOS 17.2”“8GB”这种表示。预训练模型经常把版本号从主体上拆开,比如“GPT”和“4”,甚至“17”和“.2”会被当成两个 token。

解决思路是正则预匹配:在进入实体识别之前,先把常见版本号、芯片型号、规格参数用正则标注成临时占位符,比如把“iOS 17.2”整体替换为一个特殊标记__VER_1__,等关系抽取完成后再替换回来。这个方法简单粗暴,但能显著提升后续实体和关系抽取的准确性。

4.4 图谱太稀疏,连不起来

我第一次跑完一套文档后,图谱里出现了大量孤立节点。统计发现,很多实体只在句子中出现一次,没有和任何其他实体共现。这样的节点不仅增加存储,还会让图算法变得很慢。

我的做法是加一个“最小支持度”过滤:实体至少在语料中出现两次以上,才允许作为节点进入最终图谱;关系至少出现过一次且两端节点都保留,才作为边。虽然会丢掉一些低频信息,但换来的是图谱密度和算法稳定性。对大多数业务问答场景来说,低频知识靠检索去补更合适,不适合塞进图里。

4.5 处理速度慢

规则和句法分析都比较慢,尤其是跑大语料时,spaCy 的完整 pipeline 在 CPU 上每分钟大概只能处理几千个词。实测下来,瓶颈主要在依存句法分析,而不是实体识别。

优化手段有三层:先用正则快速过滤不含触发词的句子,减少进入依存分析的文本量;再用 spaCy 的disable参数停用不用的组件,比如disable=["ner"]在只做关系抽取时可以提速;最后把分句、实体识别和关系抽取拆成三个独立任务,用多进程并行处理。这样一套组合拳下来,处理速度大约能提升 2 到 3 倍。

5. 进阶:让知识图更好用

跑通基础 pipeline 之后,我还在项目里做了两个方向上的扩展,个人认为很值得。

5.1 用 LLM 做开放关系抽取

规则和依存句法能覆盖“发布”“包含”这类固定动词关系,但处理不了“虽然 A 比 B 便宜,但 B 续航更好”这种比较关系。这时候可以引入 LLM 做开放关系抽取。

我的做法是把句子和已经识别出的实体列表一起发给 LLM,让模型输出候选三元组,返回 JSON 格式,然后和规则抽取的结果做投票融合。只有规则和 LLM 都输出的三元组,或者 LLM 输出且经过人工抽检的三元组,才会进入最终图谱。这样做的好处是保留规则的可解释性,同时利用 LLM 的语义泛化能力。需要注意控制成本,通常我只会对规则抽不到的高价值句子调 LLM。

5.2 图谱融合:从单文档到多文档

单文档抽取只是第一步,真正落地时要处理多文档来源,同一实体在不同文档里可能有不同表述,甚至相互矛盾的信息。我在图谱融合时采用“先合并节点,再合并边”的顺序:先用实体归一化层把同义实体合并成一个 canonical 节点;然后对不同来源的同一条关系做频次统计,只有出现多次的关系才保留。

如果两个文档对同一关系给出不同属性值,比如“电池容量 4000mAh”和“电池容量 4200mAh”,我会额外增加一个 evidence 列表,把原始句子挂在边属性上,查询时能直接看证据来源。这样图谱就不是一个冷冰冰的抽象结构,而是可以追溯到原文的“知识索引”。

最后再分享一点体会:KnowledgeGraph_Builder 看起来是个小工具,但真正做下来,它逼着我把自然语言问题重新拆成了“实体、关系、属性、证据”四件事。别指望一个模型解决所有抽取问题,一定要在自己的数据上反复调词典、补规则、看失败案例。图谱的质量不是靠模型刷分刷出来的,而是靠一遍遍修正边界情况磨出来的。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询