简介:本资源是一个基于知识图谱构建的课程学习智能问答系统完整实现,面向人工智能、自然语言处理方向的本科生与初学者,解决课程知识碎片化、查询效率低、缺乏语义理解能力等学习痛点。压缩包共88个文件,涵盖18个Python核心模块(含BERT模型微调、Neo4j图数据库交互、前端Vue组件与后端Flask接口)、7个CSV/JSON知识数据源、9组输入输出测试样例(in/out/label),以及配套README、配置文件和可视化界面资源,整体仅121KB,轻量易部署。已有77人学习下载,适合开展大作业实践、知识图谱入门实验或智能问答系统二次开发。读者可直接复用完整前后端架构、获得从实体识别、关系抽取到图谱查询与自然语言回复的全流程代码,尤其包含JointBERT联合标注模型、Neo4j CSV导入脚本及Vue+Flask联调结构,显著降低知识图谱项目落地门槛。
1. 这不是又一个“问答Demo”,而是一套可跑通的课程知识图谱闭环系统
你手头这份基于知识图谱的课程学习智能问答系统.zip,表面看是学生大作业压缩包,实际藏着一条从课程数据建模→Neo4j图谱构建→JointBERT语义理解→Flask后端服务→Vue前端交互的完整技术链。它不依赖外部API或云服务,所有模块本地可复现:原始课程数据存于_Data/neo4j_csv/下的 CSV 文件(含course.csv,teacher.csv,prerequisite.csv等),图谱用 Neo4j Desktop 4.4+ 即可一键导入;后端用 Flask + PyTorch 加载 JointBERT 模型做意图识别与槽位填充;前端 Vue 3(Vite 构建)通过/api/query接口发起自然语言提问,返回结构化答案并高亮图谱路径。适合正在做知识图谱课程设计、需要交出可演示系统的本科生,也适合想快速验证 JointBERT 在教育领域泛化能力的工程师——它没做模型预训练,但把实体链接、关系路径检索、答案生成三步拆解得足够清晰,参数全在config.py和trainer.py里可调。
2. 从课程CSV到Neo4j图谱:数据建模与导入实操
2.1 课程领域知识图谱的Schema设计逻辑
该系统图谱聚焦高校课程教学场景,核心实体类型包括Course(课程)、Teacher(教师)、Department(院系)、Prerequisite(先修课)、LearningObjective(学习目标)。关系类型严格对应教务逻辑:TEACHES(教师授课)、BELONGS_TO(课程归属院系)、REQUIRES(先修要求)、COVERS(覆盖知识点)。这种设计规避了通用知识图谱常见的冗余边(如sameAs、relatedTo),所有关系均可在教务系统中直接查证。例如AlgorithmDesign课程节点通过REQUIRES边指向DataStructure节点,而非模糊的has_prerequisite标签——这直接影响后续路径查询的准确率。
提示:不要直接运行
neo4j_csv/import_all.sh。该脚本默认使用neo4j://localhost:7687,若 Neo4j 未启用 Bolt 协议或密码未重置为neo4j,会报AuthenticationFailure。建议先手动执行bin/cypher-shell -u neo4j -p neo4j测试连接。
2.2 CSV数据清洗与Neo4j导入命令详解
原始 CSV 存于_Data/neo4j_csv/,但需预处理:course.csv中credit字段含空格(如" 3 "),teacher.csv的email列存在非法字符&。必须先用 Python 清洗:
# _Data/neo4j_csv/clean_csv.py import pandas as pd import re for file in ['course.csv', 'teacher.csv', 'prerequisite.csv']: df = pd.read_csv(f'{file}', encoding='utf-8') # 清理 credit 字段空格 if 'credit' in df.columns: df['credit'] = df['credit'].astype(str).str.strip().replace('', '0').astype(int) # 解码 HTML 实体 if 'email' in df.columns: df['email'] = df['email'].apply(lambda x: re.sub(r'&', '&', str(x))) df.to_csv(f'cleaned_{file}', index=False, encoding='utf-8')清洗后执行 Neo4j 导入(以course.csv为例):
# 在 Neo4j Desktop 的 Graph Apps → Cypher Shell 中执行 CREATE CONSTRAINT ON (c:Course) ASSERT c.code IS UNIQUE; USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM 'file:///cleaned_course.csv' AS row CREATE (:Course { code: row.code, name: row.name, credit: toInteger(row.credit), description: row.description });2.2.1 关键参数说明
USING PERIODIC COMMIT 500:每 500 行提交一次事务,避免内存溢出。若 CSV 行数超 10 万,需调至1000。toInteger(row.credit):强制类型转换,否则 Neo4j 将credit存为字符串,后续MATCH (c:Course) WHERE c.credit > 2查询失效。ASSERT c.code IS UNIQUE:课程代码(如CS201)作为主键,防止重复导入导致图谱污染。
导入全部实体后,用以下 Cypher 验证图谱连通性:
// 检查是否存在“数据结构”课程及其先修关系 MATCH (c:Course {name: "数据结构"})-[:REQUIRES]->(p:Course) RETURN c.name, p.name // 返回:["数据结构", "程序设计基础"] 表示成功若返回空,检查prerequisite.csv中source_code和target_code是否与course.csv的code完全一致(大小写、空格均敏感)。
3. JointBERT 模型实现:意图识别与实体链接双任务训练
3.1 为什么选 JointBERT 而非纯BERT或BiLSTM?
该系统在model/joint_bert.py中实现 JointBERT,核心是共享 BERT 编码层,上接两个并行分类头:
- Intent Classifier:预测用户问题意图(
course_info,prerequisite_query,teacher_contact共 7 类) - Slot Filler:标注实体位置(如 “《机器学习》的先修课是什么?” 中
机器学习→course_name槽位)
相比单任务 BERT,JointBERT 利用意图与槽位的强相关性(如prerequisite_query意图必然触发course_name槽位),在小样本下 F1 值提升 12.3%(见trainer.py中eval_results日志)。而 BiLSTM 因无法捕获长距离依赖,在“《操作系统》和《计算机网络》的共同先修课”这类多实体问题上准确率不足 65%。
3.2 模型训练与推理关键代码解析
训练入口在main.py,关键参数配置如下:
# main.py 第 42 行 args = { 'model_name_or_path': 'bert-base-chinese', # 中文BERT基础版,非微调版本 'max_seq_length': 128, # 课程名通常≤20字,128足够覆盖问题+上下文 'per_gpu_train_batch_size': 16, # GTX 1080 Ti 可跑满,显存占用≈3.2GB 'num_train_epochs': 5, # 课程领域数据量小(仅 1200 条标注样本),5轮足够收敛 'learning_rate': 5e-5, # BERT 微调标准学习率,过高易过拟合 'do_train': True, 'do_eval': True, 'output_dir': './output/joint_bert', }3.2.1 数据预处理逻辑(data_loader.py)
data_loader.py将原始data/train.json转为 JointBERT 输入格式:
// data/train.json 片段 { "text": "《数据库原理》的授课老师是谁?", "intent": "teacher_query", "slots": ["database_principle", "teacher_name"] }预处理后生成input_ids,attention_mask,token_type_ids,intent_label_id,slot_labels五元组。其中slot_labels是长度为 128 的列表,值为O(非实体)、B-COURSE_NAME(实体开始)、I-COURSE_NAME(实体延续)——这是 BIO 标注法,trainer.py中compute_metrics函数据此计算 slot F1。
3.3 模型推理时的实体链接优化
query.py中link_entities_to_kg()函数解决 NER 结果与图谱节点的映射问题。例如用户问 “编译原理的老师”,NER 输出compiler_principle,但图谱中课程节点code为CS302。该函数执行:
# query.py 第 87 行 def link_entities_to_kg(entity_text, entity_type): if entity_type == 'course_name': # 模糊匹配 course.name 或 course.code query = """ MATCH (c:Course) WHERE toLower(c.name) CONTAINS toLower($text) OR c.code = $text RETURN c.code AS code, c.name AS name LIMIT 1 """ result = graph.run(query, text=entity_text).data() return result[0]['code'] if result else None注意:
CONTAINS是 Neo4j 的子串匹配,比STARTS WITH更鲁棒(支持“编译”匹配“编译原理”),但需确保c.name已建立全文索引:CALL db.index.fulltext.createNodeIndex("courseNameIndex", ["Course"], ["name"])。
4. Flask + Vue 前后端联调:接口设计与跨域调试
4.1 Flask 后端 API 设计原则
Backend_Flask/app.py定义/api/query为唯一入口,采用 POST 方法接收 JSON:
{ "question": "《人工智能导论》的先修课有哪些?" }返回结构化结果:
{ "answer": "《人工智能导论》需要先修《程序设计基础》和《离散数学》。", "graph_path": [ { "node": "CS401", "type": "Course", "label": "人工智能导论" }, { "relation": "REQUIRES", "direction": "out" }, { "node": "CS101", "type": "Course", "label": "程序设计基础" } ], "intent": "prerequisite_query" }app.py中关键中间件配置:
# Backend_Flask/app.py 第 22 行 from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": ["http://localhost:5173"]}}) @app.route('/api/query', methods=['POST']) def handle_query(): try: question = request.json.get('question', '').strip() if not question: return jsonify({'error': 'Question cannot be empty'}), 400 # 调用 model.predict() 获取 answer/graph_path result = model.predict(question) return jsonify(result) except Exception as e: app.logger.error(f"Query failed: {str(e)}") return jsonify({'error': 'Internal server error'}), 5004.1.1 跨域配置细节
origins显式指定http://localhost:5173(Vite 默认端口),禁止使用*(否则 Vue 生产环境部署时会因credentials冲突失败)。resources={r"/api/*"}确保仅/api/路径开放跨域,其他路径(如/static/)保持安全。
4.2 Vue 前端请求与图谱可视化
Frontend_Vue/src/views/QueryView.vue使用axios发起请求:
// Frontend_Vue/src/views/QueryView.vue 第 63 行 const sendQuery = async () => { if (!question.trim()) return; loading.value = true; try { const res = await axios.post('http://localhost:5000/api/query', { question: question.value }, { headers: { 'Content-Type': 'application/json' } }); // res.data.graph_path 直接传给 <GraphVisualizer /> graphData.value = res.data.graph_path; answer.value = res.data.answer; } catch (err) { errorMsg.value = err.response?.data?.error || 'Network error'; } finally { loading.value = false; } };图谱可视化组件GraphVisualizer.vue使用vue3-force-graph(非 D3 原生,降低学习成本):
<!-- Frontend_Vue/src/components/GraphVisualizer.vue --> <template> <ForceGraph2D :nodes="nodes" :links="links" :node-label="node => node.label" :link-label="link => link.relation" /> </template> <script setup> const props = defineProps(['graphPath']); const nodes = computed(() => { return props.graphPath.filter(item => item.node) .map((item, i) => ({ id: i, label: item.label, type: item.type })); }); const links = computed(() => { const edges = []; for (let i = 0; i < props.graphPath.length - 2; i += 2) { if (props.graphPath[i].node && props.graphPath[i + 2]?.node) { edges.push({ source: i, target: i + 2, relation: props.graphPath[i + 1].relation }); } } return edges; }); </script>提示:若图谱节点重叠,调整
ForceGraph2D的d3AlphaDecay参数(默认 0.0228)至0.015可增强布局稳定性。
5. 知识图谱问答的三大典型问题与修复方案
5.1 问题:Neo4j 查询超时,MATCH (c:Course)-[:REQUIRES*1..3]->(p:Course)返回空
根因:*1..3表示 1 到 3 跳路径,但课程先修关系通常为 1 跳(直接先修),2 跳以上属间接依赖(如 A→B→C,则 C 的先修课是 B,非 A)。原查询试图找“所有间接先修课”,但图谱中未构建INDIRECTLY_REQUIRES关系。
修复方案:修改query.py中路径查询逻辑,限定为 1 跳:
# query.py 第 135 行,替换原 Cypher # 错误写法(超时且语义错误) # "MATCH (c:Course)-[:REQUIRES*1..3]->(p:Course) WHERE c.code = $code RETURN p" # 正确写法 query = """ MATCH (c:Course)-[:REQUIRES]->(p:Course) WHERE c.code = $code RETURN p.code AS code, p.name AS name """5.2 问题:JointBERT 对同义词泛化差,如“授课老师”识别为teacher_contact,但“任课教师”识别失败
根因:训练数据中teacher_contact意图样本仅含“授课老师”“谁教”等 3 种表达,未覆盖教务术语变体。
修复方案:在data/train.json中扩充同义词样本(至少 15 条),并重新训练:
// 新增样本 { "text": "《软件工程》的任课教师信息", "intent": "teacher_contact", "slots": ["software_engineering", "teacher_name"] }, { "text": "这门课由哪位老师负责讲授?", "intent": "teacher_contact", "slots": ["current_course", "teacher_name"] }注意:
current_course是动态槽位,需在data_loader.py的get_slot_labels()中添加映射规则,将“这门课”绑定到当前上下文课程。
5.3 问题:Vue 页面首次加载时图谱渲染空白,控制台报Cannot read properties of undefined (reading 'length')
根因:GraphVisualizer.vue的graphPathprops 初始为null,但computed函数未做空值校验,props.graphPath.filter()抛出异常。
修复方案:增加防御性编程:
<!-- GraphVisualizer.vue --> <script setup> const props = defineProps({ graphPath: { type: Array, default: () => [] } // 默认空数组,非 null }); const nodes = computed(() => { if (!props.graphPath || props.graphPath.length === 0) return []; return props.graphPath.filter(item => item.node) .map((item, i) => ({ id: i, label: item.label, type: item.type })); }); </script>最终效果:输入“《数据库系统概论》的授课老师”,前端显示课程节点、教师节点及TEACHES关系边,并高亮教师邮箱字段——整个流程从数据导入到界面响应,全程无需修改一行外部依赖,所有参数、路径、端口均在项目内明确声明,可直接用于课程答辩演示。
本文还有配套的精品资源,点击获取