简介:面向心理学与自然语言处理交叉方向的毕业设计资料包,基于Neo4j构建《基础心理学》教材知识图谱并实现可视化,适合本科毕业设计选题参考、知识图谱项目入门及NLP实体关系抽取实践。资源约390MB,内含任务书、开题报告、参考文献、中期与最终答辩材料、NLP实现代码以及实验自建数据集,文件类型以docx、pdf、代码和PPT为主,可直接复用数据与脚本。项目采用Bert-BiLSTM-CRF模型抽取人名与心理学概念,定义“同一”“对立”“由…提出”等关系,并通过脚本自动创建节点和关系映射至图数据库,完整覆盖从数据处理、模型训练到图谱构建的闭环流程。已有191人学习下载,适合需要快速掌握知识图谱构建方法、获取完整毕设方案与可运行代码的读者。
1. 教材是线性的,记忆是网的:用 Neo4j 把《基础心理学》重组成知识图谱
做《基础心理学》期末复习时,我总在翻书:看到“感觉记忆”想回去查定义,看到“遗忘曲线”又得翻到第六章。教材章节是线性组织的,但大脑里的知识是网状关联的。基于neo4j的基础心理学教材知识图谱构建与可视化,正是把课本里的概念、理论、人物、效应拆成节点和关系,存成一张能跨章节查询的图。它能回答“艾宾浩斯遗忘曲线被哪些实验支持”这类教科书目录给不了的问题。
对毕业设计来说,这本教材有一个天然友好的点:目录、术语表、课后习题就是半成品的本体,不需要你从零做命名实体识别。你只需要搭好 Neo4j 图数据库、写一个导入脚本、再用可视化把成果展示出来,就能形成一套完整且有实物的作品。
这条路适合心理学专业想往数据分析方向走的本科生、教育技术方向要展示知识库的同学,以及刚接触 Neo4j 想拿真实数据练手的入门者。下面各章按我实际做过的路线展开:本体设计、数据导入、可视化、排坑和交付前体检。
2. 知识图谱构建的第一步:给《基础心理学》定实体、关系和属性
知识图谱构建常犯的第一个错误是跳过本体设计。拿到教材文本就开始写爬虫抽句子,抽出来的三元组互相打架,最后图里全是零散边。教材类知识图谱的优势在于:章节结构、术语索引、课后习题本身就是强约束。利用这些约束,先把实体类型和关系类型定死,后面的导入就是匹配填空。
2.1 为什么教材适合先定本体再抽取:和通用知识图谱的差别
通用知识图谱通常先做命名实体识别,再训练关系抽取模型,从自由文本中找出实体间的语义连接。教材则完全不同:一个章节里有明确的概念定义句式,课后练习里还会反复出现术语。所以教材场景下可以反着做,先定义好实体类型和关系类型,再用规则去课文里配对。这样做的准确率比纯模型高,每一步都可解释,这也是毕业答辩时最容易被追问的部分。
我一般把这一步做成一张术语抽取规则表:凡是被加粗并且括号里带“又称”的,记为定义式概念;凡是出现人名加年份加“提出”的,记为理论提出关系。规则先覆盖目录和章节引言,再递归到正文。本体建模、语义层、知识管理这些说法,本质上都落在这张规则表和关系表上,先把它们理顺,比急着跑代码重要得多。
2.2 实体类型与关系类型:两张表把边界定清楚
《基础心理学》里能当作节点的实体,常见的是六类:概念、理论、人物、效应、实验、章节。其中“章节”节点本身不是心理学知识,但它承担聚合功能,让每个概念能回溯到教材来源。下面这张表是实体部分的约定:
| 实体类型 | 例子 | 来源 |
|---|---|---|
| 概念 | 感觉记忆、知觉恒常性、条件反射 | 术语表和加粗定义 |
| 理论 | 加工水平说、衰减理论、情绪ABC理论 | 章节核心论点 |
| 人物 | 冯特、巴甫洛夫、艾宾浩斯 | 教材人名索引 |
| 效应 | 首因效应、近因效应、鸡尾酒会效应 | 术语表和正文案例 |
| 实验 | 感觉剥夺实验、记忆广度实验 | 正文“实验”小节 |
| 章节 | 第三章 感觉与知觉 | 目录 |
关系类型控制在 10 条以下。关系越少,可视化越干净,用户越容易看懂。以下是教材里高频出现的关系:
| 关系类型 | 方向语义 | 例句 |
|---|---|---|
| 定义 | 章节 -> 概念 | 第二章定义“感觉” |
| 上位于 | 概念 -> 概念 | “知觉”上位于“空间知觉” |
| 提出 | 人物 -> 理论 | 艾宾浩斯提出遗忘曲线 |
| 相关 | 概念 -> 概念 | 记忆相关注意 |
| 佐证 | 实验 -> 理论 | 记忆广度实验佐证短时记忆容量 |
| 基于 | 理论 -> 理论 | 加工水平说基于感觉记忆研究 |
“上位于”对应本体论里的 is-a 关系。建图时不建“下位于”,统一从上位节点指向下位节点,查询时靠方向判断层级,少一半边。
2.3 属性设计:节点上放三类属性就够用
节点属性不要贪多。教材类文本常用的只有三类:name 作为唯一标识,defined_at 记录来源章节,comment 放一句话定义。关系属性一般只放一个 example 字段,用来存原文例句,方便答辩时直接展示出处。
比如“感觉记忆”节点的 JSON 大致长这样:
{ "name": "感觉记忆", "defined_at": "第三章 感觉与知觉", "comment": "刺激作用于感觉器官后短暂保持的映像" }这份 JSON 也可以直接作为标注模板,让同伴按固定结构补数据,避免各写各的格式,后续导入脚本才不用反复改解析逻辑。
3. 用 py2neo 构建图谱数据库:从 CSV 到 Neo4j 的批量导入脚本
本体定完,下一步是把教材转成实体表和关系表,再批量写进 Neo4j。这个环节最容易翻车:不是不会写代码,而是对 Neo4j 的约束和事务机制不熟,导致导入慢、重复数据多。以下是我整理过的最小可运行方案。
3.1 检查 Neo4j 安装与配置:装完之后先做三件事
新装 Neo4j 社区版时,按这个顺序确认环境:
- 浏览器打开
http://localhost:7474,能进入 Neo4j Browser 说明服务已启动; - 用默认账号
neo4j登录后立即修改初始密码,否则 Python 驱动连不上; - 执行
RETURN 1 AS ok;验证数据库写入权限正常。
确认后再装 Python 依赖:
pip install py2neo pandaspy2neo 是 Neo4j 官方维护的 Python 驱动,pandas 用来读 CSV。如果 pip 下载慢,可以先把源换成清华 PyPI 镜像,再把这两行指令重新执行,不需要任何额外网络配置。
3.2 实体表和关系表:先让数据长成图想要的样子
两张 CSV 是导入脚本的输入。entities.csv的每一行代表一个节点:
| name | type | defined_at | comment |
|---|---|---|---|
| 感觉记忆 | 概念 | 第三章 感觉与知觉 | 刺激作用后极短时间内保持的映像 |
| 加工水平说 | 理论 | 第七章 记忆 | 记忆保持取决于编码加工深度 |
| 艾宾浩斯 | 人物 | 第七章 记忆 | 遗忘曲线提出者 |
relations.csv每一行代表一条边:
| src | rel_type | dst | example |
|---|---|---|---|
| 艾宾浩斯 | 提出 | 遗忘曲线 | 艾宾浩斯用无意义音节研究遗忘 |
| 遗忘曲线 | 相关 | 记忆保持 | 教材第7章第2节 |
两个硬规定:第一,type 和 rel_type 是英文或中文都可以,但不能带空格;第二,src 和 dst 的 name 必须和实体表完全一致,包括全角半角。这两点提前检查,能省掉后面大量排错时间。
写入前还要对entities.csv做一次去重,用 pandas 处理:
import pandas as pd df = pd.read_csv("entities.csv", dtype=str) df = df.drop_duplicates(subset=["name"], keep="first") df.to_csv("entities_dedup.csv", index=False, encoding="utf-8-sig")用dtype=str是为了防止概念名里的数字被 pandas 读成 int;输出用utf-8-sig是因为这个编码能让 Excel 和 Neo4j 都正确识别中文。
3.3 py2neo 导入脚本:merge 防重,批次提交防卡死
下面是核心导入代码,用Graph.begin()维持一个事务,到批大小边界再提交,比逐条 create 快一个数量级:
# import_kg.py # 依赖:pip install py2neo pandas import pandas as pd from py2neo import Graph, Node, Relationship, NodeMatcher GRAPH = Graph("bolt://localhost:7687", auth=("neo4j", "你的密码")) # 唯一约束,保证同一概念名只有一个节点 GRAPH.run( "CREATE CONSTRAINT concept_name IF NOT EXISTS " "FOR (n:Concept) REQUIRE n.name IS UNIQUE" ) entities = pd.read_csv("entities_dedup.csv", dtype=str) relations = pd.read_csv("relations.csv", dtype=str) BATCH_SIZE = 500 batch = GRAPH.begin() count = 0 # 第一遍:写节点 for row in entities.itertuples(): node = Node("Concept", row.type, name=row.name) if row.defined_at is not None: node["defined_at"] = row.defined_at if row.comment is not None: node["comment"] = row.comment batch.merge(node, "Concept", "name") count += 1 if count % BATCH_SIZE == 0: batch.commit() batch = GRAPH.begin() batch.commit() # 收尾,提交剩余事务 # 第二遍:写关系 matcher = NodeMatcher(GRAPH) batch = GRAPH.begin() count = 0 for row in relations.itertuples(): src = matcher.match("Concept", name=row.src).first() dst = matcher.match("Concept", name=row.dst).first() if src is None or dst is None: # 源或目标缺失时打印出来,而不是静默跳过 print(f"缺失节点: {row.src} 或 {row.dst}") continue rel = Relationship(src, row.rel_type, dst) if row.example is not None: rel["example"] = row.example batch.merge(rel) count += 1 if count % BATCH_SIZE == 0: batch.commit() batch = GRAPH.begin() batch.commit() print("导入完成")逻辑说明:Node("Concept", row.type, name=row.name)创建节点时同时带两个标签,“Concept”用于统一查询,row.type用于区分概念、理论、人物等类型。batch.merge以 name 为键做 upsert,重复运行脚本不会产生双份节点。第二遍关系写入用 NodeMatcher 先定位两端的节点再创建关系,找不到节点时打印日志而不是跳过,方便排查数据表缺失。
参数调整:BATCH_SIZE 在 200 到 1000 之间都合理,取决于 Neo4j 所在机器内存。教材知识图谱的数据量在几千条时设 500 体感最好;如果一次导入上万条且机器内存小,降到 200,否则事务内内存占用会过高。导入耗时过长时,先看有没有其他进程占用 Neo4j 堆内存,再决定是否调低批次。
3.4 备选方案:LOAD CSV 什么时候更合适
数据量超过十万边时,py2neo 逐条 match 会很慢,此时可以改用 Cypher 原生的 LOAD CSV。先把 CSV 放到 Neo4j 安装目录的 import 文件夹,然后执行:
LOAD CSV WITH HEADERS FROM 'file:///relations.csv' AS row MATCH (a:Concept {name: row.src}) MATCH (b:Concept {name: row.dst}) MERGE (a)-[r:相关]->(b) SET r.example = row.example这段 Cypher 的好处是全程在服务端执行,不走 Python 与数据库之间的往返,网络开销少,吞吐量高很多。缺点是 Cypher 不支持把row.rel_type动态当成关系类型,遇到多类型关系时,要么先按 rel_type 把 CSV 拆成多个文件,每个文件写一段固定关系类型的查询,要么就回到 py2neo 脚本。毕设数据量在几千到几万边时,py2neo 方案更可控,我建议优先用 3.3 的脚本。
4. 可视化与查询:先满足答辩,再谈知识图谱大屏
Neo4j 的优势不止存储,查询和可视化也直接影响毕业设计能不能被看懂。常见路径是先用 Neo4j Browser 验证数据,再用 ECharts 做定制界面,也就是常说的可视化大屏。两条路不冲突,按阶段推进即可。
4.1 从一个节点出发怎么查多条路径:Cypher 的两个常用模板
“neo4j查询从一个节点出发如何查询多条”是这种项目里问得最多的问题。第一种写法是固定一层、限定关系类型:
MATCH (n:Concept {name: '遗忘曲线'})-[r]-(m:Concept) WHERE type(r) IN ['提出', '佐证', '相关'] RETURN n.name AS src, type(r) AS rel, m.name AS dst ORDER BY rel LIMIT 100这里type(r)拿到关系类型并做过滤,结果是一张扁平的表格,适合导出成 Excel 或放进论文附录。第二种写法是不限层数,直接取子图:
MATCH p = (n:Concept {name: '遗忘曲线'})-[*1..2]-(m:Concept) WHERE n <> m RETURN p LIMIT 200[*1..2]表示一到两跳,适合看某个概念的周边网络。p 是路径对象,Browser 会把它画成图,n 和 m 是路径两端的节点。LIMIT 必须加,否则一个高连接度概念会把几千个邻居全拖出来,浏览器直接卡死。个人经验是先跑第二条看到全貌,再跑第一条取结构化明细。
4.2 在 Neo4j Browser 里调出答辩可用的视图
Browser 默认布局通常一团乱麻。答辩前我会设置三个点:左侧选择“概念”“理论”等标签分批显示,避免所有类型挤在一起;右下角 Layout 选 Hierarchical 并按 defined_at 排序,让同一章节的节点自动聚拢;节点颜色按标签分组后,只在图例里留下概念和理论两类,人物和实验收进 tooltip 说明。这些设置在浏览器界面即时生效,不需要写前端代码。中期检查如果只要求展示图结构,这一步已经足够;但最终答辩通常还要求一个独立页面,这才需要 ECharts。
4.3 用 Python 拉取查询结果,交给 ECharts 画力导向图
常见做法是用 ECharts 的 graph 系列做知识图谱前端插件。步骤分为两步:后端用 py2neo 把图数据转成 JSON,前端拿到 JSON 直接渲染。
后端导出脚本:
# export_graph.py import json from py2neo import Graph GRAPH = Graph("bolt://localhost:7687", auth=("neo4j", "你的密码")) # 查询与"记忆"两跳以内的节点和关系 query = """ MATCH p = (n:Concept {name: '记忆'})-[*1..2]-(m:Concept) WITH collect(DISTINCT n) + collect(DISTINCT m) AS nodes, collect(DISTINCT relationships(p)) AS rels RETURN nodes, rels """ data = GRAPH.query(query) nodes, rels = data[0] def get_type(labels): for label in labels: if label != "Concept": return label return "概念" node_list = [ {"id": n["name"], "name": n["name"], "category": get_type(n.labels)} for n in nodes ] link_list = [ { "source": r.start_node["name"], "target": r.end_node["name"], "rel_type": type(r).__name__, } for r in rels ] with open("graph_data.json", "w", encoding="utf-8") as f: json.dump({"nodes": node_list, "links": link_list}, f, ensure_ascii=False)这段脚本的关键点是ensure_ascii=False,保证中文写进 JSON 后可直接读;get_type负责从多标签里取出“概念”“理论”这类实际类型,因为所有节点都带着统一的 Concept 标签。
前端用 ECharts 渲染时,核心配置是一个 graph series:
// 前端拿到 graph_data.json,用 echarts 渲染力导向图 const resp = await fetch('graph_data.json'); const graphData = await resp.json(); const chart = echarts.init(document.getElementById('kg')); chart.setOption({ tooltip: { trigger: 'item' }, legend: { data: ['概念', '理论', '人物', '效应', '实验', '章节'] }, series: [{ type: 'graph', layout: 'force', roam: true, // 支持拖拽和缩放 categories: [ { name: '概念' }, { name: '理论' }, { name: '人物' }, { name: '效应' }, { name: '实验' }, { name: '章节' } ], data: graphData.nodes, links: graphData.links.map(l => ({ source: l.source, target: l.target, label: { show: true, formatter: l.rel_type } })), force: { repulsion: 300, edgeLength: 120 }, label: { show: true, position: 'right', fontSize: 12 } }] });参数说明:repulsion是节点间的斥力,数值越大节点摊得越开,300 左右适合几百个节点的教材图谱;edgeLength是边长期望值,两跳内查询建议 120 到 150,太短会叠成一团。数据量到几千节点时,可以加上layoutAnimation: false关掉入场动画,否则每次刷新都要卡一次。实际部署时如果页面加载慢,优先检查 graph_data.json 文件体积,几百 KB 对现代浏览器很轻松,超过 5 MB 就要考虑按查询局部渲染。
5. 避坑与常见问题:从安装到图查询的 5 个翻车点
这个章节把毕设阶段和读者群里最常见的几类问题,统一按“现象 → 原因 → 解决办法”列出来。这些问题不解决,后面功能再正确都展示不出来。
5.1 现象:本机能开 Neo4j 服务,局域网内别的电脑访问不了
原因:Neo4j 默认只监听 127.0.0.1,配置文件里的默认地址限制了外部访问。解决办法:修改 Neo4j 安装目录下的conf/neo4j.conf,加入或改写成下面三行:
server.default_listen_address=0.0.0.0 server.connectors.http.listen_address=0.0.0.0:7474 server.bolt.listen_address=0.0.0.0:7687需要注意不同小版本的配置键不一样,Neo4j 4.4 用dbms.connector.http.address,5.x 用server.connectors.http.listen_address。改完重启服务:
# macOS / Linux 下重启 Neo4j bin/neo4j restartWindows 上则在系统服务管理器中重启 Neo4j 服务。重启后再用本机查到的局域网地址访问,如果仍连不上,检查防火墙入站规则是否放行 7474 和 7687 端口。这条坑在答辩现场用笔记本投屏时特别常见,提前配好能避免当场尴尬。
5.2 现象:用 LOAD CSV 导入中文数据后出现乱码
原因:CSV 文件没有保存为 UTF-8 编码,很多编辑器默认另存为 ANSI。解决办法:导入前统一转码。Excel 导出的文件用“另存为 CSV UTF-8”格式;Python 脚本里写文件时用encoding="utf-8-sig",这个参数会在文件头部写入 BOM 标记,能被 Neo4j 正确识别。如果已经乱码,重新转码后再导一次即可,不需要改 Cypher 语句。
5.3 现象:py2neo 导入几千条数据要几分钟,甚至卡死
原因:没有使用事务,逐条执行graph.create(),每个节点都触发一次事务提交;另一种是把所有节点一次性放进一个事务,超过 Neo4j 默认的查询内存上限。解决办法是用Graph.begin()分批提交,每批 200 到 500 条。出现卡死时,先在 Neo4j Browser 执行CALL dbms.listTransactions()看事务状态,确认没有堆积的长事务后重启服务,再改成分批写入。
5.4 现象:同一段脚本跑两次,关系数量翻倍
原因:导入时使用了create或没建唯一约束,第二次运行把同名节点再建一遍;另一个隐蔽点是关系 merge 时如果带了属性,Neo4j 会把属性不同的关系也当成新关系,跑两遍就出现两条重复边。解决办法:节点写入一律用 merge,并在建库前创建唯一约束。关系写入时先MERGE (a)-[r:提出]->(b)创建不带属性的关系,再SET r.example = '原文例句'补充属性,这样第二次运行不会新增边。
5.5 现象:网上教程说用 LOAD CSV 导入,我一执行就报找不到文件
原因:file:///指向 Neo4j 安装目录下的 import 文件夹,不是任意路径;很多教程默认你已把文件放进了 import 目录,但没说明这一点。解决办法:把 CSV 放进 Neo4j 安装目录下的 import 文件夹,再用相对路径引用:
LOAD CSV WITH HEADERS FROM 'file:///relations.csv' AS row MATCH (a:Concept {name: row.src}) MATCH (b:Concept {name: row.dst}) MERGE (a)-[r:相关]->(b)还有一类报错是“Invalid input”,原因通常是 CSV 的列名和 Cypher 里row.列名对不上,检查 csv 表头和字段是否带空格。建议建表时列名不要用中文,避免半角全角差异带来莫名问题。
6. 交付前的一次图谱体检:孤立节点、覆盖率与错题关联检查
功能写完后,别急着打包交文档。我会先用三组 Cypher 做一遍“体检”,这几条查询在答辩现场也能当演示动作。
第一组查孤立节点。没有关系的概念,说明导入时关系表漏了边:
MATCH (n:Concept) WHERE NOT (n)--() RETURN n.name, labels(n)[0] AS type ORDER BY type LIMIT 50第二组查章节覆盖率,看每个章节到概念的“定义”关系数量:
MATCH (c:章节)<-[:定义]-(n:Concept) RETURN c.name AS chapter, count(n) AS concept_count ORDER BY concept_count DESC第三组查跨章节关联。统计每个概念连接的不同关系类型数,数量为 0 或只有 1 的节点,通常是图谱里的“孤点”:
MATCH (n:Concept)-[r]-(m:Concept) WITH n, count(DISTINCT type(r)) AS rel_kind RETURN n.name, rel_kind ORDER BY rel_kind ASC LIMIT 30这三组查询跑完之后,孤立节点要手工补“相关”边,覆盖率不足的章节回到实体表补数据。我印象最深的一次,是在交文档前一天发现“暗示”这个概念没和“催眠”建立关系,教材正文里明明有这句论述,但实体表漏了抄,体检脚本一跑就暴露了。后来每做知识图谱,我都把这三条查询存成一个 reports.cql 文件,边导边查,不等到最后阶段再做检查。
另一个值得加的做法是错题联动:把题库里选择题的题干和正确选项也建成节点,让教材图谱去支撑“为什么选 A 不选 B”的推理。这样毕设就从“查得到”变成了“能解释”,答辩时说服力会明显不一样。希望这个习惯能帮到你。
本文还有配套的精品资源,点击获取