简介:这份资源面向具备一定Python基础、希望入门自然语言处理与知识图谱构建的开发者与学习者,围绕「文本转知识图谱」这一典型AI应用场景,提供可运行的完整项目代码。包内共63个文件,以41个JavaScript前端脚本、5个XML配置、4个Python源码及3个编译缓存文件为主,另含CSS、HTML与说明文本,压缩包约605.62MB,其中包含LTP模型数据等依赖,便于直接复现。项目覆盖文本预处理与分词、命名实体识别、实体关系抽取、图结构构建、可视化展示及模型训练优化等环节,并配有测试样例用于验证各阶段输出。目前已有597人学习下载,适合想系统理解从原始文本到图谱落地全流程、并借助现成代码快速搭建实验环境的读者参考实践。
1. 文本转知识图谱:从一段合同文本到可查询图谱,中间到底缺了什么
手里有一批合同、工单、故障报告或者论文摘要,想把它变成能查询、能推理的知识图谱,这是很多团队做知识管理的起点。但真正动手会发现,从纯文本到图谱之间隔着一条鸿沟:文本是非结构化的,图谱要求实体、关系、属性都是结构化的。基于 Python 实现文本转化知识图谱,本质上就是搭一座桥,把自然语言里的「谁对谁做了什么」抽出来,映射成本体定义好的节点和边,再写进图数据库。这套方案适合有 Python 基础、需要处理中文文本、又想快速看到图谱效果的开发者。它不要求你从头训练模型,但要求你理解本体建模、实体关系抽取、图数据库写入这三个环节怎么串起来。下面按我实际做过的路径,把每一步拆开讲清楚。
2. 先定本体再写代码:知识图谱的骨架怎么搭
2.1 为什么本体建模决定了后面所有代码的写法
很多人一上来就写正则抽实体,抽完发现实体类型五花八门,关系也理不清,最后图谱变成一团乱麻。问题出在跳过了本体建模。本体是图谱的 schema,它规定了有哪些实体类型、哪些关系类型、实体有哪些属性。比如做设备故障知识图谱,本体里应该有「设备」「故障现象」「故障原因」「维修措施」四类实体,关系有「表现为」「由…导致」「采取…维修」。定好这些,后面抽取代码才知道该往哪个标签里塞。
本体建模不需要多复杂,用 Python 字典或者 YAML 文件描述就行。常见做法是用 Protégé 画一版,导出 OWL,但小项目直接手写配置更快。我一般会先列一个实体关系表,确认业务方要查什么,再反推需要哪些类型。这一步偷懒,后面返工成本极高。
2.2 用 Python 定义一套可复用的本体配置
下面这段代码定义了一个简单的本体配置,包含实体类型、关系类型和每种关系的头尾实体约束。把它存成ontology.py,后面抽取和写入都从这里读。
# ontology.py # 本体定义:实体类型、关系类型、关系约束 ONTOLOGY = { "entities": { "Equipment": {"desc": "设备", "attrs": ["name", "model"]}, "Fault": {"desc": "故障现象", "attrs": ["name", "severity"]}, "Cause": {"desc": "故障原因", "attrs": ["name"]}, "Action": {"desc": "维修措施", "attrs": ["name"]}, }, "relations": { "shows": {"head": "Equipment", "tail": "Fault", "desc": "表现为"}, "caused_by": {"head": "Fault", "tail": "Cause", "desc": "由…导致"}, "handled_by": {"head": "Fault", "tail": "Action", "desc": "采取…维修"}, } } def validate_relation(head_type, rel_type, tail_type): """校验一条关系是否符合本体约束""" rel = ONTOLOGY["relations"].get(rel_type) if not rel: return False return rel["head"] == head_type and rel["tail"] == tail_type这段代码的关键在于validate_relation函数。抽取阶段每产出一条三元组,都先过一遍校验,不符合本体约束的直接丢弃。参数方面,head和tail写实体类型名,必须和entities里的键一致。如果业务扩展,比如增加「备件」实体和「更换」关系,只改ONTOLOGY字典即可,抽取和写入代码不用动。这就是本体先行的好处:schema 变,逻辑不变。
2.3 本体落地的三个检查点
定完本体别急着写抽取,先做三个检查。第一,实体类型是否互斥,比如「设备」和「备件」不能是同一个东西的两种叫法。第二,关系方向是否唯一,避免「A 导致 B」和「B 由 A 导致」同时存在。第三,属性是否够用,如果业务要按型号查设备,Equipment里就必须有model属性。这三个检查点过了,再进入抽取环节,返工概率会低很多。
3. 实体关系抽取:用 Python 把中文句子拆成三元组
3.1 规则、模型、大模型三条路怎么选
实体关系抽取有三条常见路径。纯规则用正则和词典,快但泛化差;深度学习模型用 BERT 加 CRF,需要标注数据;大模型 API 抽取,零样本效果好但成本和延迟高。我一般会混合用:高频实体走词典,关系走规则模板,兜底用大模型。这样在保证召回的同时控制成本。
选型时看两个指标:你的文本领域是否固定,以及你有没有标注数据。领域固定且有几百条标注,微调 BERT 最稳;领域开放又没标注,大模型 API 加规则后处理是现实选择。别一上来就追求端到端模型,很多场景规则能覆盖八成。
3.2 基于词典和正则的抽取代码实现
下面这段代码演示用词典匹配实体、用正则模板抽关系。词典从本体配置生成,关系模板针对「设备 X 出现故障 Y」这类句式。
import re from ontology import ONTOLOGY, validate_relation # 从本体生成实体词典,实际项目可替换为业务词表 ENTITY_DICT = { "Equipment": ["压缩机", "水泵", "电机"], "Fault": ["过热", "异响", "漏油"], "Cause": ["轴承磨损", "润滑不足"], "Action": ["更换轴承", "补充润滑油"], } def extract_entities(text): """基于词典匹配实体,返回 (实体, 类型, 起始位置)""" results = [] for etype, words in ENTITY_DICT.items(): for w in words: for m in re.finditer(re.escape(w), text): results.append((w, etype, m.start())) # 按位置排序,便于后续关系判断 return sorted(results, key=lambda x: x[2]) def extract_relations(text, entities): """基于距离和模板抽关系,这里用简单邻近策略""" triples = [] for i in range(len(entities) - 1): head, htype, _ = entities[i] tail, ttype, _ = entities[i + 1] # 根据类型组合推断关系 for rel_type, rel in ONTOLOGY["relations"].items(): if rel["head"] == htype and rel["tail"] == ttype: if validate_relation(htype, rel_type, ttype): triples.append((head, rel_type, tail)) return triples if __name__ == "__main__": text = "压缩机出现过热,原因是轴承磨损,已更换轴承。" ents = extract_entities(text) print("实体:", ents) print("三元组:", extract_relations(text, ents))extract_entities用re.finditer拿到每个词的位置,排序后保证关系抽取时顺序正确。extract_relations这里用了最简单的邻近策略:相邻两个实体如果类型组合符合本体关系,就产出一条三元组。参数上,ENTITY_DICT可以换成从数据库或文件加载,validate_relation保证不会产出本体外的关系。实际项目中,邻近策略会误抽,需要加距离阈值和否定词判断,比如两个实体间隔超过 20 个字就不抽。
3.3 抽取结果的质量怎么快速评估
抽完别直接入库,先人工看 50 条。重点看三类错误:实体边界错(「压缩机过热」被拆成两个实体)、关系方向反(「原因导致故障」抽成「故障导致原因」)、漏抽(同义表述没进词典)。我一般会写个脚本把三元组按关系类型分组,每组随机抽 10 条打印出来,半小时就能定位主要问题。召回率低就扩词典,准确率低就收紧模板。
4. 写入 Neo4j:把三元组变成可查询的图谱
4.1 为什么选 Neo4j 以及连接前的准备
图数据库选 Neo4j 是因为它的 Cypher 查询语言对关系遍历非常友好,社区版免费,Python 驱动成熟。写入前需要确认三件事:Neo4j 服务已启动,默认端口 7687 可访问;Python 环境装了neo4j驱动,用pip install neo4j即可;数据库用户名密码已知,默认是neo4j加首次启动设置的密码。
连接代码里不要硬编码密码,用环境变量或配置文件。下面代码用环境变量读取,避免密码进版本库。
4.2 批量写入的 Python 代码与参数调优
import os from neo4j import GraphDatabase from ontology import ONTOLOGY URI = os.getenv("NEO4J_URI", "bolt://localhost:7687") USER = os.getenv("NEO4J_USER", "neo4j") PWD = os.getenv("NEO4J_PWD", "password") driver = GraphDatabase.driver(URI, auth=(USER, PWD)) def write_triples(triples): """批量写入三元组,先合并实体再建关系""" with driver.session() as session: for head, rel, tail in triples: # 用 MERGE 避免重复节点 session.run( """ MERGE (a:Entity {name: $head}) MERGE (b:Entity {name: $tail}) MERGE (a)-[r:REL {type: $rel}]->(b) """, head=head, tail=tail, rel=rel ) def write_with_labels(triples, entity_types): """带标签写入,entity_types 是 name 到类型的映射""" with driver.session() as session: for head, rel, tail in triples: htype = entity_types.get(head, "Entity") ttype = entity_types.get(tail, "Entity") session.run( f""" MERGE (a:`{htype}` {{name: $head}}) MERGE (b:`{ttype}` {{name: $tail}}) MERGE (a)-[r:`{rel}`]->(b) """, head=head, tail=tail ) if __name__ == "__main__": triples = [("压缩机", "shows", "过热"), ("过热", "caused_by", "轴承磨损")] types = {"压缩机": "Equipment", "过热": "Fault", "轴承磨损": "Cause"} write_with_labels(triples, types) driver.close()write_with_labels用动态标签把实体类型写进节点,这样查询时可以直接MATCH (e:Equipment)过滤。参数上,MERGE保证幂等,重复写入不会产生重复节点。批量写入时建议每 500 条提交一次事务,太大容易内存溢出,太小则慢。如果三元组上万,用UNWIND批量传参比循环session.run快一个数量级。
4.3 写入后怎么验证图谱结构正确
写完跑三条 Cypher 验证。第一条查节点总数和标签分布,确认没有Entity兜底标签残留。第二条查关系类型分布,确认没有本体外的关系。第三条随机抽一个设备,查它的两跳邻居,看路径是否符合业务预期。如果发现大量节点只有Entity标签,说明entity_types映射没覆盖全,需要补全。
5. 避坑与排查:文本转图谱最容易翻车的五个地方
5.1 实体重叠导致关系抽错
现象:一句话里「压缩机过热」被同时识别为「压缩机」和「过热」两个实体,但位置有重叠,关系抽取时把「压缩机」和「过热」当成两个独立实体处理,结果正确;但如果词典里有「压缩机过热」这个长词,就会和短词冲突,产出重复实体。原因:词典匹配没有做最长优先。解决:匹配时按词长降序,匹配到长词后跳过其覆盖区间。
5.2 关系方向反了但校验没拦住
现象:本体定义caused_by是 Fault 指向 Cause,但抽取时把 Cause 写在前面,校验却通过了。原因:validate_relation只校验类型组合,没校验实际抽取顺序。解决:在抽取函数里显式按本体方向组装三元组,校验函数增加方向断言,或者写入前统一做一次方向修正。
5.3 Neo4j 写入中文标签报错
现象:用中文做节点标签或关系类型,Cypher 报语法错误。原因:Cypher 标签需要用反引号包裹,且部分版本对中文支持有限。解决:标签用英文,中文放属性里;如果必须用中文,确保用反引号,并测试目标版本是否支持。我一般标签用英文,展示层再做映射。
5.4 大模型抽取结果格式不稳定
现象:用大模型 API 抽三元组,返回的 JSON 有时多一层嵌套,有时字段名变了,解析直接崩。原因:大模型输出不受严格 schema 约束。解决:在 prompt 里给 few-shot 示例,要求固定 JSON schema;解析时用json.loads加 try-except,失败则重试或降级到规则抽取。别把大模型输出直接喂给写入函数。
5.5 图谱越写越慢的隐性原因
现象:写入几千条后速度明显下降。原因:每次MERGE都在全图扫描,没有对name建索引。解决:写入前执行CREATE INDEX FOR (n:Entity) ON (n.name),带标签的也对每个标签建索引。索引建完,写入速度通常能提升一个数量级。
6. 让图谱真正可用:从查询到增量更新的一个实用技巧
图谱写完不是终点,能查、能更新才是。我习惯在项目里加一个「查询模板」层,把业务问题翻译成 Cypher 模板,再用 Python 填参。比如「某设备出现过哪些故障」对应MATCH (e:Equipment {name:$name})-[:shows]->(f:Fault) RETURN f.name。这样业务方不用学 Cypher,调函数就行。
增量更新是另一个关键。新文本进来,先走抽取,再和已有图谱做实体对齐。对齐用名称精确匹配加别名表,别名表可以人工维护,也可以用编辑距离兜底。对齐后只写入新关系,已有节点用MERGE复用。下面这段代码演示增量更新的核心逻辑。
def incremental_update(text, entity_types, alias_map=None): """增量更新:抽取后对齐再写入""" ents = extract_entities(text) triples = extract_relations(text, ents) # 别名归一 if alias_map: triples = [(alias_map.get(h, h), r, alias_map.get(t, t)) for h, r, t in triples] # 只写入新关系,节点 MERGE 复用 write_with_labels(triples, entity_types) return len(triples)alias_map是别名字典,比如「压缩机」和「空压机」指向同一个节点。参数上,alias_map可以从数据库加载,也可以先用difflib算相似度自动生成候选再人工确认。增量更新时注意事务粒度,一批文本一个事务,避免中途失败导致半写入。
我踩过最大的坑是早期没做实体对齐,同一台设备因为叫法不同在图谱里变成三个节点,查询结果永远不全。后来强制所有写入前过一遍别名表,问题才消失。如果你刚开始做,建议第一版就把别名表加上,哪怕先手工维护几十条,也比后期清洗省事。希望帮到你。
本文还有配套的精品资源,点击获取