☰
PDF解析到知识图谱:从文本抽取到Neo4j检索的完整链路
2026/10/3 0:53:12 网站建设 项目流程

简介:该项目使用Python实现了从PDF解析、信息抽取到知识图谱构建及检索的完整流程,适合计算机、数学、电子信息类专业的学生作为课程设计、期末大作业或毕业设计参考。核心功能包括PDF文档内容识别、实体与关系抽取、知识图谱存储与基于图谱的语义检索,并且提供了前端交互界面,方便直观查看检索结果。整个压缩包共有一百一十九个文件,以四十七个Python源码文件为核心,辅以二十八个字节码文件、十五个JavaScript文件、九个JSON配置以及若干Vue组件、Markdown说明和SQLite数据库,整体大小约四点七九兆,目录组织清晰,便于快速定位和学习。目前已有九百六十二人学习,说明该项目具有一定的代表性和实用价值。下载后即可直接运行,对于希望快速上手自然语言处理、知识图谱应用的开发者来说,是一份不错的参考资料。

1. 把一份PDF变成可问答的知识图谱:这个毕设链路最难的不是图谱,是读PDF

一份二十页的产品手册,用解析脚本跑完,抽出来的实体和关系却乱成一锅粥——做过「PDF识别与分析 + 信息抽取 + 知识图谱 + 信息检索」这类项目的人,多半都栽在第一步。这个标题串起的链路很长,但真正的难点往往不在知识图谱,也不在检索,而在第一公里:PDF文档能不能被稳定地读成干净文本。本文按我实际做过的方案把这条链路拆开讲,覆盖文本型PDF与扫描版PDF的解析选型、正则与模型两种抽取路线、Neo4j的批量导入与去重、基于图结构的检索模板和验证指标。适合正在做毕设、或者被文档智能化需求卡住的从业者照着复现。

2. PDF识别与分析:先把“读得进”解决,再谈信息抽取

2.1 先分清两类PDF:文本型与扫描版,解析路线完全不同

PDF在格式上是个容器,不是“一篇文章”。文本型PDF的内容流里有文本层,页面上的每个字符都对应一个编码位置,可以直接提取;扫描版PDF本质是图片序列,每一页是一张位图,必须走OCR才能得到文字。用错路线的最直接后果是:文本型PDF上OCR又慢又容易错,扫描版PDF上直接提取文本返回一片空白。判断文档类型是开始写解析代码前的第一件事。

我一般用一个非常简单的探针脚本判断,先加载PDF,取首页尝试提取文本,看返回内容长度:

import fitz # PyMuPDF doc = fitz.open("sample.pdf") print("页数:", doc.page_count) sample_page = doc[0] text = sample_page.get_text("text") if len(text.strip()) < 20: print("首页几乎没有可提取文本,疑似扫描版,需要OCR") else: print("文本型PDF,可直接进入抽取流程") doc.close()

这个脚本的逻辑是用PyMuPDF打开文档,调用get_text("text")读取首页文本层。阈值20不是固定值,取决于文档正文密度:如果是契约扫描件,首页通常只有图章和手写签名,提取结果接近空;如果是论文PDF,首页至少能提出标题和摘要。对混合型PDF——比如前几页是扫描封面、后面是印刷正文——这种按页探针的方式会更可靠,后面OCR章节会专门处理。

判断完之后,如果走文本提取路线,要意识到PDF里的文本和你在阅读器里看到的关系:阅读器帮你做了排序和排版,但get_text拿到的原始内容可能横跨多个文本块,页眉页脚、图表标注都会混进来。这一步的正确心态是:先拿到“能用的粗文本”,归一化处理留给抽取前奏。

2.2 用PyMuPDF提取文本:最小脚本与三个常用参数

在文本型PDF上,我默认用PyMuPDF而不是PDFPlumber。原因很朴素:速度差一个量级,几百页的PDF用PDFPlumber逐页解析会明显卡顿,而PyMuPDF的内存占用和耗时都更可控;它对部分损坏PDF的容错也更好。PDFPlumber的强项是精确定位字符坐标、提取表格,如果你的下游要做表格还原,可以单独用它处理特定页,全量文本提取不必用它。

下面是一个可以直接抄走的批量提取脚本:

import fitz def extract_text_from_pdf(path, start_page=1, end_page=None, sort=True): doc = fitz.open(path) if end_page is None: end_page = doc.page_count if end_page > doc.page_count: end_page = doc.page_count pages_text = [] for pg in range(start_page - 1, end_page): # PyMuPDF 页码从0开始 page = doc[pg] content = page.get_text("text", sort=sort) pages_text.append({ "page": pg + 1, "text": content }) doc.close() return pages_text

这里有三个参数需要认真对待。start_page和end_page控制页码范围,注意循环里写的是range(start_page - 1, end_page),因为PyMuPDF内部页码从0开始,写字的人习惯从1开始,这个偏移很容易漏。sort=True的作用是让提取的文本块按阅读顺序输出,对双栏排版的PDF尤其重要;如果设成False,文本块顺序可能按底层内容流排列,抽取正则时你会发现一段被截断的话散落在各页里。还有一个不常提但实用的点:get_text("text")返回的是普通字符串,如果遇到字符映射异常,可以改用get_text("rawdict")看字符级信息,定位是哪个字符出了问题。

拿到文本后不要急着做正则。先把PDF特有噪声清掉:行内的换行符、全角空格、制表符、页眉页脚。我一般会做一次统一的文本归一化,把单换行合并成空格,把全角转半角,再进抽取模块。这个习惯帮我避免了很多“正则明明写了却匹配不到”的排查时间。

2.3 扫描版PDF的OCR兜底:PaddleOCR与Tesseract选型

扫描版PDF跑文本提取,返回的是空字符串或者零星几个字。这时唯一靠谱的做法是把页面渲染成位图,再用OCR识别。常见方案是PaddleOCR和Tesseract二选一,我在这两类上的选型判断比较明确:识别中文为主的文档,选PaddleOCR;英文扫描件或只想快速验证,选Tesseract。

PaddleOCR自带方向分类器,对歪斜的扫描页有修正能力,中文识别效果在开源方案里是稳的。代价是依赖体积偏大,首次运行还要下载模型权重。Tesseract胜在轻量、历史久,但中文识别需要额外安装chi_sim语言包,对低分辨率扫描件经常出现错字和漏字。下面是用PyMuPDF渲染页面、再交给PaddleOCR识别的常用流程:

import fitz from paddleocr import PaddleOCR # lang="ch" 使用中文模型,use_angle_cls 开启方向分类 ocr = PaddleOCR(use_angle_cls=True, lang="ch") def ocr_pdf_page(path, page_no, zoom=2.0): doc = fitz.open(path) page = doc[page_no - 1] # zoom 控制渲染分辨率,2.0 相当于 144 DPI mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat) img_path = f"page_{page_no}.png" pix.save(img_path) result = ocr.ocr(img_path, cls=True) # result[0] 是文本行列表,每项为 [坐标, (文本, 置信度)] lines = [line[1][0] for line in result[0]] text = "\n".join(lines) doc.close() return text

参数上最值得调的是zoom。扫描原件清晰度不够,或者页面上有浅色字时,把zoom从2.0提到3.0,识别率会明显上升。但不要无脑调大,图片尺寸变大后OCR耗时成倍增加,且原本噪点会被放大。对300 DPI以上的扫描原件,zoom=1.5通常够用。cls=True让OCR对每个文本区域做方向分类,适合页面里有横向表格或旋转过的图章。还要提醒一点:PaddleOCR的实例初始化很耗资源,不要在页面循环里重复PaddleOCR(),全局初始化一次就好。

这两类方案可以共存。对混合型PDF,先逐页尝试文本提取,某页get_text结果里的有效字符占比过低,就单独走OCR。这样整份文档的解析成本最低,且不会漏页。

3. 信息抽取:把PDF正文变成可入图的实体与关系

3.1 先定义本体:实体类型与关系类型决定图谱长什么样

信息抽取的目标不是“抽出所有名词”,而是抽成符合图谱结构的三元组。三元组需要先有骨架——实体有哪些类型,关系有哪些类型,关系指向哪个方向。很多毕设项目在这一步翻车,原因是上来就套命名实体识别模型,抽出了大量人名地名,但关系完全没有定义,最后图谱里全是孤立的点,信息检索阶段什么都查不出来。

最常见的做法是先在项目里定义一个最简本体,不用上升到完整OWL标准,但至少覆盖你关心的领域。以论文PDF为例,我会定义这样的结构:

ONTOLOGY = { "entity_types": { "ORG": {"label": "机构", "description": "大学、研究院、公司"}, "PERSON": {"label": "人员", "description": "作者、负责人"}, "METHOD": {"label": "方法", "description": "算法、框架、模型名称"}, "DATASET": {"label": "数据集", "description": "评测数据集"}, "METRIC": {"label": "指标", "description": "准确率、F1、耗时"} }, "relation_types": { "AFFILIATED_WITH": {"from": "PERSON", "to": "ORG", "label": "任职于"}, "PROPOSED_BY": {"from": "METHOD", "to": "PERSON", "label": "提出"}, "EVALUATED_ON": {"from": "METHOD", "to": "DATASET", "label": "在…上评测"}, "ACHIEVES": {"from": "METHOD", "to": "METRIC", "label": "取得指标"} } }

这个字典的价值在于:它强制你在写抽取代码之前,先想清楚图谱里允许出现哪些节点和边。比如PROPOSED_BY的方向是METHOD -> PERSON,因为方法是被人物提出的,不是人物被方法提出。这个方向会直接影响后面写Cypher查询时的MATCH方向,如果一开始不定清楚,后面导入数据时关系方向会很混乱。更实际的价值是,本体定义可以直接转成Neo4j的节点标签和关系类型,抽取代码和入库代码共用一份结构,不会出现两边字段对不上的问题。

3.2 规则与正则抽取:90%场景最快见效的做法

对格式相对固定的PDF——合同、招标文件、政策通知、论文摘要——正则表达式是投入产出比最高的抽取手段。它不依赖模型下载和GPU,运行结果完全可解释,调试也直观。一个典型的场景是从合同文本里抽机构、金额、人名,我用命名分组写规则:

import re text = "由北京华夏科技有限公司与华北理工大学共同承担,项目经费500万元,李明担任项目负责人。" patterns = { "ORG": r"([\u4e00-\u9fa5A-Za-z0-9]{2,20}?(?:有限公司|大学|研究院|集团|银行))", "PERSON": r"([\u4e00-\u9fa5]{2,4})(?:担任|负责|主持)", "MONEY": r"(\d+(?:\.\d+)?)\s*(万元|亿元|元)" } for entity_type, pattern in patterns.items(): matches = re.findall(pattern, text) print(entity_type, matches)

运行这段代码,ORG会匹配到“北京华夏科技有限公司”“华北理工大学”,PERSON匹配到“李明”,MONEY匹配到("500", "万元")。这里两个细节值得注意。第一,机构名的正则结尾要枚举后缀词,这是中文机构名抽取的常见做法,用后缀词表比用纯词性规则稳定得多;如果你处理的文档来自特定领域,比如法院判决书,就把“人民法院”“人民检察院”加进后缀词表。第二,PERSON规则里({2,4})限制了中文姓名长度,这能误伤少数三个字以上的名字,但对批量文档来说,优先保证精确率是正确的选择——宁可漏掉,也不要为图谱灌进大量噪声。

正则的边界也要讲清楚。它对“结构化的非结构化文本”有效,但对纯自然语言句子就力不从心。比如“该模型由李明团队研发,性能优于基线”这句话,PERSON规则能抓住“李明”,但“研发”这个动作和“该模型”之间的关系,正则写不出来。毕设项目里最稳妥的策略是:先用正则处理高置信度字段,跑不动的句子再留给模型。

3.3 模型抽取兜底:用LTP或BERT做命名实体识别

当文档里的句子无法用正则锁定,比如人物出现位置不固定、机构名称不在后缀词表里,就需要命名实体识别模型兜底。常见做法是引入哈工大LTP或Hugging Face上的中文BERT系模型。LTP是老牌中文NLP工具,安装简单,对中文实体类型支持完整;transformers生态的模型则胜在统一接口,换模型只改一个参数。

以transformers的NER pipeline为例,代码非常短:

from transformers import pipeline # hfl/rbt3 是中文RoBERTa小模型,CPU可以跑 ner_pipeline = pipeline("ner", model="hfl/rbt3", aggregation_strategy="simple") text = "该模型由李明团队在华北理工大学完成研发,在 NewsQA 数据集上取得了 F1 92.3 的成绩" entities = ner_pipeline(text) for ent in entities: print(ent["word"], ent["entity_group"], round(ent["score"], 3))

aggregation_strategy="simple"很关键:默认的NER输出会把“李”“明”拆成两个token,simple策略会把它们合并成完整实体。模型抽取结果和正则有一个重要差别:它有置信度score,进入图谱前应该按阈值过滤。我在毕设项目里通常设定score >= 0.85才写入图谱,这能挡掉很大一批边界噪声。还要注意,模型输出的实体类型是“LOC、ORG、PER”,你不一定全要,应该在入库前按你的本体做映射,比如模型里的PER对应本体的PERSON,LOC如果不在本体里就直接丢弃。

模型方案的实际成本不在调用那几行代码,而在环境准备。transformers首次加载要下载模型权重,几百MB到1GB不等,需要一个稳定的网络环境。如果你在离线内网做毕设,更现实的做法是用LTP离线部署,或者干脆把抽取策略收敛到纯规则。这一点在项目早期就要决定,不要等到连模型都装好了才发现部署环境不满足。

4. 知识图谱构建:把三元组写进Neo4j

4.1 Neo4j为什么是构建知识图谱的默认选择

实体和关系抽取完成后,下一步是把三元组持久化成图结构。这个环节的选型决定了查询和检索的体验。我几乎不会用MySQL来存三元组,因为关系的多跳查询在关系型数据库里要写多层JOIN,SQL又长又慢;也不会直接选RDF三元组库,因为Jena这类工具的部署和查询语法对毕设项目来说太重了。Neo4j是属性图模型最直接的代表,Cypher查询语言对路径和关系遍历的支持天然友好,而且自带可视化界面,演示时直接能看到图谱结构。

下面是三种存储方案的直观对比:

对比项Neo4jMySQLRDF三元组库
关系查询原生支持,Cypher遍历需要多层JOIN支持但SPARQL学习成本高
可视化自带浏览器面板无需另配工具
部署难度有桌面版和Docker镜像低较高
适合场景图结构、路径检索、问答事务型业务数据语义网标准场景

Neo4j启动后默认端口是7687用于Bolt协议连接,7474是HTTP管理界面。在vscode里配置好python环境后,直接pip install py2neo就能连。对毕设来说,Neo4j Desktop比手动配置服务端省事,下载安装后创建一个本地库,记下用户名密码就能开始写代码。

4.2 用py2neo把三元组写进图库:批量导入与MERGE去重

写入三元组的第一个坑是逐条插入。用py2neo的graph.create()一条接一条建节点和关系,数据量到几千条就开始慢,几万条会卡到怀疑人生。常见做法是走Cypher的UNWIND批量插入,一次提交几百条三元组。同时要用MERGE而不是CREATE:CREATE每次都会新建节点,同一份PDF解析两遍,图谱里就会出现两套完全相同的实体,后面的检索就会被重复结果污染。

from py2neo import Graph graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) def import_triples(triples, batch_size=500): """triples 是列表,每个元素为 dict,包含 head/head_type/rel/tail/tail_type""" total = len(triples) for start in range(0, total, batch_size): batch = triples[start:start + batch_size] graph.run(""" UNWIND $batch AS row MERGE (h:Entity {name: row.head, type: row.head_type}) MERGE (t:Entity {name: row.tail, type: row.tail_type}) MERGE (h)-[r:REL {type: row.rel}]->(t) """, batch=batch) print(f"已处理 {start + len(batch)} / {total} 条")

这段代码里有三个关键点。第一,$batch是Cypher的参数占位符,graph.run()的第二个参数会把Python列表注入进去,不要用f-string拼Cypher语句,否则转义和注入问题会让你排查很久。第二,MERGE是“有则匹配、无则创建”,它依赖节点的匹配键——这里匹配键是name和type的组合,所以同名的“人”和“机构”不会被认为是同一个实体,这个设计要提前想好。第三,batch_size=500是一个稳妥的范围,我试过调到2000也能跑,但一旦某条数据格式异常导致事务失败,重试一个2000条的事务比一个500条的更痛苦。

4.3 建立约束与索引:让MERGE和查询都快起来

MERGE的性能高度依赖底层索引。没有索引时,每条三元组入库都要全图扫描找同名节点;有了唯一约束,Neo4j会走索引直接定位,导入速度能差出一个量级。在导入大批量数据之前,先建一个节点名称的唯一约束:

graph.run( "CREATE CONSTRAINT entity_name IF NOT EXISTS " "FOR (n:Entity) REQUIRE n.name IS UNIQUE" )

这个约束的作用是:Neo4j会自动为n.name创建索引,同时保证不会出现重名节点。注意这里如果把type也并入唯一性,就要改成REQUIRE (n.name, n.type) IS UNIQUE,语法略有不同,取决于你的业务到底允许不允许“同名但不同类型”的实体同时存在。

约束建好后,查询也要习惯用参数化写法。比如查“某个机构的所有关联实体”,不要每查一次就把实体的名字拼进字符串,而是用$name参数:

records = graph.run(""" MATCH (n:Entity {name: $name})-[r]-(m:Entity) RETURN m.name, type(r), m.type LIMIT 20 """, name="华北理工大学").data()

这一段返回的是目标实体一跳到任意实体的关系,type(r)是Neo4j内置函数,返回关系类型字符串。拿到这种查询结果,后续做信息检索的答案组装就有了数据基础。

5. PDF图构建常见问题与排查:四类翻车现象的原因和修法

5.1 现象:文本型PDF提取结果为空或乱码

有人拿到一份PDF,读出来是一堆空白或者方块字符,第一反应是“PDF加密了”。实际上更常见的原因是这份PDF根本没有文本层——它表面是文字,实际是扫描图片,直接用get_text当然什么也拿不到。另一种情况是字体子集化问题,文档用了自定义内嵌字体,字符映射表不完整,提取出来的字符变成空白或替代符。

原因:PDF类型误判,或者内嵌字体缺少Unicode映射。 解决:先跑一遍章节2.1的探针脚本,判断首页有没有文本层;确认是扫描版就走渲染+OCR。对字体子集化导致的空白,用get_text("rawdict")检查字符级别的信息:

doc = fitz.open("sample.pdf") raw = doc[0].get_text("rawdict") # 检查第一个文本块是否有 chars 字段 if raw and "chars" in raw[0]: print("首字符码点:", raw[0]["chars"][0]["c"])

如果码点正常显示但最终文本是空,多半是字体映射问题,这种PDF需要渲染成图片后OCR,不值得在字体层面深挖。

5.2 现象:正则抽取集体失败,明明文本里有目标词

文本提取成功,页面里也清清楚楚写着“北京华夏科技有限公司”,但正则跑完一个匹配都没有。最典型的原因是PDF提取文本时在换行处把中文词截断了,比如“北京华夏科技”和“有限公司”被拆成两行,中间隔着一个换行符;还有全角逗号、全角空格混入,导致\s和普通字符匹配不上。

原因:PDF文本流按视觉行存储,提取结果里的换行与印刷行的位置严格对应,中文姓名、机构名经常被从中间截断。 解决:在做正则之前先做一次激进的文本归一化。把单个换行符替换为空字符,合并连续空白,全角字符转半角:

import re def normalize_text(text): # 去掉行内换行,PDF的行尾换行不是语义边界 text = re.sub(r"\n+", "", text) # 统一全角空格和常规空白 text = text.replace("\u3000", " ") text = re.sub(r"[ \t]+", " ", text) # 全角标点转半角,视文档情况决定是否启用 table = {ord(f): ord(t) for f, t in zip(",。;:()", ",.;:()")} return text.translate(table)

这个函数有三个动作,顺序很重要:先去掉换行,否则全角转半角处理完换行还在;再把全角空格转成普通空格;最后处理标点。注意如果你后续还要做段落切分,不要在这里把空行也删掉,空行是区分段落的信号。正则匹配失败时,把文本先print出来看原始字符,比在正则表达式上反复改要快得多。

5.3 现象:实体抽出一堆,图谱里全是孤立节点

Neo4j可视化界面里,图谱呈现为大量分散的点,实体之间有边的很少,关系类型大多是“未知”。这类项目做到信息检索阶段会非常被动,因为检索依赖的是“路径”,没有边就没有路径。

原因:本体定义时关系类型没想好,或者抽取流程只跑了实体识别,没有做关系分类;还有可能是关系抽取模块对候选三元组没有做置信度过滤,把大量无关的共现关系当成了真实关系。 解决:回到本体,重新检查关系类型里from和to的定义;在入库前增加一条硬规则:缺少头实体、尾实体、关系类型中任意一项的三元组直接丢弃;为每条关系记录置信度,低于阈值的候选不写入图谱。实际项目里我会在导入前加一个过滤函数:

def filter_triples(candidates, min_score=0.7): filtered = [] for c in candidates: if not (c.get("head") and c.get("tail") and c.get("rel")): continue if c.get("score", 1.0) < min_score: continue filtered.append(c) return filtered

这个过滤规则看起来简单,但能解决“图谱里全是噪点”这类最让人头疼的问题。宁可用更少的边换更高的可信度,信息检索阶段查询结果才有意义。

5.4 现象:Neo4j导入慢、事务失败,重启后只导入一半

几千条三元组导入耗时几十秒还算正常,但如果几万条数据导入时直接卡死,或者中途报错退出,就要考虑导入策略的问题。最常见的原因是逐条CREATE和缺失索引,每条数据单独开启事务,事务提交开销远大于数据本身。

原因:循环逐条写入,事务数量过多;MERGE匹配时没有索引,每次都要全图扫描;批量导入的事务过大,内存被撑爆后触发事务失败。 解决:统一改用章节4.2里的UNWIND批量导入,batch_size固定在200到500之间;导入前先建好唯一约束;如果数据里有重复三元组,要提前在Python侧去重,减少MERGE的开销。一个额外的经验是:导入过程不要和查询过程并发跑,Neo4j的写事务会和长查询竞争内存,严重点会让导入慢到像卡死一样。毕设演示时,先导完数据,再开查询页面,体验会顺畅很多。

6. 让检索真正可用:从模板匹配到图路径问答

6.1 用模板匹配把自然语言问句翻译成Cypher:最稳的“伪问答”

许多毕设项目的“信息检索”做成了关键词匹配,用户输入“华北理工大学”,返回的是一堆包含该词的原文片段,这没有发挥知识图谱的长处。可行的做法是把自然语言问句通过模板映射成Cypher查询,再返回结构化答案。模板数量不需要多,覆盖“A和B的关系是什么”“A有哪些关联实体”“谁提出了A”这三类高频问句,演示效果已经足够。

import re def ask(question): text = question.strip() m = re.match(r"(.+?)和(.+?)的关系是什么", text) if m: head, tail = m.group(1), m.group(2) cypher = """ MATCH (a:Entity {name: $h})-[r]-(b:Entity {name: $t}) RETURN type(r) AS rel, a.name AS a, b.name AS b, r.type AS reason """ records = graph.run(cypher, h=head, t=tail).data() if records: return records return "图谱中没有直接关系,可以尝试问:…和…的关系是什么" m2 = re.match(r"谁.*?提出(.+)", text) if m2: method_name = m2.group(1).strip() cypher = """ MATCH (m:Entity {name: $name})-[r:PROPOSED_BY]->(p:Entity) RETURN p.name AS proposer """ return graph.run(cypher, name=method_name).data() return "我能回答的关系问题,请参考:A和B的关系是什么 / 谁提出了A"

这套模板匹配的优点是可控和稳定。答辩时不会因为模型推理偶然出错,答案是“对就是对、没有就是没有”;由于查询落在图结构上,返回结果天然带关系上下文,看一眼就能判断对不对。相比直接调用大模型做问答,这种方案不依赖外网API、不用花钱、可解释性也更好——被问“为什么不用大模型”时,回答“为了保证可解释性和离线可用”是一个站得住的立场。

6.2 用图路径让回答带证据,并验证整个链路

模板查询能回答单跳关系问题,更有知识图谱味儿的用法是拿多跳路径当“证据链”。比如想问“A机构的负责人曾经在哪家机构任职”,需要走AFFILIATED_WITH和PROPOSED_BY两条边。Neo4j里这条路能直接表达为带限制的路径查询:

graph.run(""" MATCH path = shortestPath( (a:Entity {name: $start})-[*..4]-(b:Entity {name: $end}) ) RETURN [n IN nodes(path) | n.name] AS node_names, [r IN relationships(path) | type(r)] AS rel_types """, start="华北理工大学", end="北京华夏科技有限公司").data()

这段Cypher返回的是一串节点名和关系类型拼接成的路径,比如“华北理工大学 <- 任职于 - 李明 -> 任职于 -> 北京华夏科技有限公司”。把这条路径作为答案返回给用户,比单纯输出“有关系”更有说服力,因为它展示了图谱的推断能力。验证链路是否可用,我用四个指标:实体抽取精确率和召回率、三元组入库准确率、模板问句回答准确率、单查询响应耗时。前两个在抽取阶段抽样人工标注来算,后两个在检索阶段统计。毕设答辩时能把这四个数字讲清楚,比堆功能更让人信服。

做这类项目我最大的教训是:别一上来就套深度学习模型,先把正则抽取、文本归一化和Neo4j这批“土办法”跑通,再拿模型在漏掉的部分上做增量。用这个顺序做完,你会对每个模块之间的边界很清楚,排查问题时少走弯路。希望帮到你。

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

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

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

立即咨询