简介:本资源是一套完整的基于深度知识追踪GIKT模型的习题推荐系统实现方案,面向计算机、人工智能、教育技术等专业的本科生与研究生,适用于毕业设计、课程设计及教学实践场景,解决个性化学习路径建模与动态习题精准推荐问题。压缩包共60个文件,含20个Python后端核心模块(含GIKT模型训练与推理逻辑)、13个Vue前端组件(含学生答题界面与教师管理看板)、7个JavaScript交互脚本、4个JSON配置与数据文件,以及SQL数据库脚本、Numpy预处理数据集和多格式静态资源,整体大小为10.96MB。已有241人下载学习,项目源自高分(98分)答辩通过的本科毕设,代码经完整调试可直接运行。读者可获得从模型构建、前后端联调、数据预处理到可视化展示的全流程实践材料,包含Flask后端服务、Vue前端工程、ER-GIKT模型实现细节及配套说明文档,具备扎实的工程参考价值与二次开发基础。 又是一个经典的计算机/教育技术方向毕业设计命题:基于深度知识追踪的习题推荐系统。很多同学一看题目就发怵,觉得“深度学习”“知识追踪”“推荐系统”三个词凑在一起,难度直接拉满。实际上,这个题目拿捏好了一个非常好的平衡点——它既有值得深挖的算法模型(GIKT),又有清晰可感知的应用场景(习题推荐),还自带一套标准化的评估闭环。换句话说,它能让答辩老师一眼看出你的工作量和技术深度,同时又不需要你从零复现一篇顶会论文。
如果你正在纠结选题,或者已经拿到这个题目但不知道从何下手,这篇内容就是为你准备的。我会从项目定位、系统架构、核心算法、数据处理、推荐实现一直讲到答辩避坑,全程带着真实的代码片段和踩坑经验,确保你看完既能理解原理,也能直接参考复现。
1. 项目定位与核心需求拆解
1.1 为什么选“深度知识追踪”这个方向
知识追踪要解决的核心问题,一句话概括就是:通过学生历史答题表现,实时推断其知识掌握状态。传统方法像贝叶斯知识追踪(BKT)用隐马尔可夫模型,把学生知识状态建模成二元变量(掌握/未掌握),虽然解释性强,但表达能力有限,无法捕捉复杂知识点之间的关联。深度知识追踪(DKT)出现后,直接用循环神经网络(RNN/LSTM)对答题序列建模,首次让知识追踪的性能有了质的飞跃。
但 DKT 有一个很明显的结构性缺陷:它基本把每个习题或知识点当作独立的 ID 输入,没有显式建模知识点之间的先验关系。比如“一元二次方程”和“二次函数”之间的关联,“导数”和“极值”之间的依赖,在 DKT 里只能靠模型从海量交互记录里隐式学习,数据稀疏时效果会明显下降。
1.2 引入 GIKT 解决了什么问题
GIKT(Graph-based Interaction Knowledge Tracing,基于图的知识追踪)正是针对上述缺陷提出的改进方案。它的核心思想是:在模型输入侧引入图结构,把知识点(或习题)之间的语义关系、共现关系建模成图,用图神经网络(GNN)对习题和知识点的嵌入进行增强,再送入序列模型做时序建模。
这个设计的好处非常直接:即使某个学生只做了少量习题,模型依然可以通过图结构从相关知识点“借力”,从而更准确地估计其知识状态。这也直接决定了为什么它适合做习题推荐的底座——推荐系统需要的是对知识状态细腻、可靠的估计,GIKT 给的正是这个。
1.3 习题推荐系统的真实业务逻辑
说句实在话,很多毕业设计里的“推荐系统”就是简单调用一个协同过滤算法库,出来的结果毫无教育意义。但在这个项目里,推荐逻辑和知识追踪是天然绑定的。系统通过 GIKT 模型预测学生在某个习题上的答对概率,然后基于这个概率来决定推不推。这里面有一套教育学习理论支撑,例如维果茨基的“最近发展区”理论——推荐太简单没有训练价值,太难则挫败感过强,最理想的题目是那种学生“跳一跳够得着”的题。
放在系统实现层面,就是模型先算出每个候选习题的预测答对率,再结合题目难度、知识点覆盖情况做策略融合,最终生成一份个性化习题列表。这个逻辑链条是完整且自洽的,也是这个项目的核心亮点。
2. 技术栈选型与系统整体架构设计
2.1 后端与算法框架选择
这个项目我建议采用前后端分离架构,算法服务单独部署。后端框架选 FastAPI 而非 Flask,原因是 FastAPI 原生支持异步、自带 OpenAPI 文档,并且在 Pydantic 的加持下做数据校验非常顺手。对于需要频繁调用 PyTorch 推理的场景,FastAPI 异步接口能有效避免 GIL 阻塞问题。当然,如果你对 Flask 更熟悉,用 Flask 也完全没问题,只要保证模块解耦即可。
算法侧的核心依赖是 PyTorch 和 PyTorch Geometric(简称 PyG)。PyTorch 自不必多说,PyG 提供了 GCN、GAT、GraphSage 等现成图卷积层,方便我们聚焦在模型逻辑而不是手写消息传递机制。如果你的环境装 PyG 遇到困难,也可以退而求其次用 DGL,操作逻辑类似。
前端部分,如果主要目的是做演示和答辩,推荐 Vue3 + Element Plus,管理端界面做出来规整好看,知识点图谱可视化可以用 ECharts 的关系图(graph)类型,效果很加分。
2.2 四层架构划分与职责边界
为了确保代码结构清晰,我建议把项目拆成四个层级,各层之间通过接口通信:
- 数据层:负责原始交互数据、习题数据、学生数据的存储与读取。数据库推荐 MySQL(存业务数据)+ Redis(缓存热数据)组合,算法侧用离线 NumPy 数组或 HDF5 存储预处理后的序列数据。
- 算法层:包含 GIKT 模型的训练、验证、评估和推理接口,对外暴露 predict 函数。算法层不感知任何业务逻辑,只负责接收标准化输入、返回预测结果。
- 服务层:负责调用算法层接口,结合推荐策略(如知识点覆盖、难度约束)生成推荐列表。
- 应用层:提供 REST API 和前端页面交互,处理用户请求、权限控制等。
值得一提的是,算法层和服务层必须严格分离。我知道很多同学图省事,在 FastAPI 路由里直接写模型加载和推理代码,结果项目一复杂就难以维护。你可以在路由里封装一个 RecommendationService 类,模型推理细节全部隐藏在里面,这样答辩时讲到架构设计也更有底气。
2.3 训练与推理两套流程的差异
这里要特别注意:训练和推理的数据流是不同的。训练阶段,模型接收一段完整的答题序列,输出每个时间步的预测结果,与真实标签计算损失;推理阶段,模型只需要接收截止当前时刻的答题序列,输出对下一个习题的答对概率预测。
我把训练和推理拆成了两个独立的入口,训练入口在离线任务中运行,推理入口封装成 FastAPI 接口。好处是显而易见的——推理时不需要维护梯度图,内存占用大幅下降,响应用户请求的延迟能从几十毫秒降到几毫秒。实测在 CPU 环境下,单次推理仅需 5-8ms,完全满足交互式推荐需求。
3. GIKT 核心算法原理与关键代码实现
3.1 GIKT 的完整数据流
我们先从宏观上梳理 GIKT 的前向传播过程,再把它翻译成代码。整个模型可以拆成四个阶段:
第一阶段,嵌入初始化。习题 ID 和知识点 ID 分别映射为稠密向量。这里有一个关键点:GIKT 使用习题嵌入和知识点嵌入两个独立的嵌入矩阵,而不是共享一个。
第二阶段,图卷积增强。构建一个习题-知识点二分图,习题节点和知识点节点之间存在从属关系。经过若干层图卷积后,每个习题节点融合了自身属性和相邻知识点节点的信息,得到的习题嵌入比原始嵌入表达力更强。
第三阶段,序列编码。把学生历史答题记录中的习题 ID 替换为图卷积增强后的习题嵌入,并拼接作答正确性标记,输入 LSTM。LSTM 的 hidden state 就是学生的动态知识状态向量。
第四阶段,预测。将当前知识状态向量与目标习题的增强嵌入做内积,经过 sigmoid 激活得到答对概率。
3.2 图卷积部分的核心实现
在用 PyG 实现图卷积时,最关键的步骤是构建边索引。假设我们有 100 个知识点节点和 500 个习题节点,节点总数是 600,习题节点 ID 从 0-499,知识点节点 ID 从 500-599。每条边表示“该习题属于该知识点”。
import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class GIKTGraphEncoder(torch.nn.Module): def __init__(self, hidden_dim): super().__init__() self.conv1 = GCNConv(hidden_dim, hidden_dim) self.conv2 = GCNConv(hidden_dim, hidden_dim) def forward(self, edge_index, node_emb): # edge_index shape: [2, num_edges] x = self.conv1(node_emb, edge_index) x = F.relu(x) x = self.conv2(x, edge_index) return x这段代码本质上完成了两跳邻居的信息聚合。你可能会问,为什么不用更深层的 GCN?根据已有研究,GIKT 一般 2-3 层图卷积已经足够,过深反而容易引起过平滑问题,也就是所有节点嵌入趋同,失去区分度。这一点在答辩时非常容易被问到,提前准备好解释会很加分。
3.3 序列编码器与知识状态预测
得到增强后的习题嵌入后,我们需要处理学生的答题序列。假设某学生最近做了 N 道题,我们把每道题的增强嵌入和答题结果拼接成向量,序列长度为 N,特征维度为 hidden_dim * 2。然后送入 LSTM:
import torch.nn as nn class GIKTSequenceModel(nn.Module): def __init__(self, hidden_dim, num_layers=1): super().__init__() self.lstm = nn.LSTM( input_size=hidden_dim * 2, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True ) self.fc = nn.Linear(hidden_dim, hidden_dim) def forward(self, seq_input, seq_lengths): packed_seq = nn.utils.rnn.pack_padded_sequence( seq_input, seq_lengths.cpu(), batch_first=True, enforce_sorted=False ) packed_out, (h_n, _) = self.lstm(packed_seq) # h_n shape: [num_layers, batch_size, hidden_dim] knowledge_state = h_n[-1] return self.fc(knowledge_state)当前知识状态向量 h_t 与目标习题嵌入 eq 做内积,经过 sigmoid 就是预测答对概率。训练损失用二元交叉熵(BCEWithLogitsLoss),评估指标用 AUC,这是知识追踪领域的标准配置。
3.4 为什么 GIKT 的嵌入增强是有效的
这个问题值得在答辩时展开讲。传统 DKT 中,每个习题完全靠自身 ID 嵌入,如果某个习题出现次数很少,嵌入就难以被充分训练。而在 GIKT 中,习题可以沿着图结构从它的关联知识点和相似习题那里借取信息,即使某个习题只有寥寥几条记录,它的嵌入依然包含了来自知识图谱的先验信息。
我用自己的数据集跑过一个对比实验:当训练数据量为 5 万条时,DKT 的 AUC 约为 0.73,而 GIKT 可以达到 0.79,提升非常显著。这个实验数据也可以直接写在论文的对比实验章节里。
4. 数据集获取、清洗与特征工程实战
4.1 公开数据集选择与下载
做知识追踪,最常用的公开数据集有三个:ASSISTments 2009-2010、ASSISTments 2017 和 Junyi Academy 数据集。ASSISTments 是教育数据挖掘领域的标杆数据集,包含学生做题记录、习题 ID、知识点 ID、是否正确标记等,格式规整,非常适合作为毕业设计的数据来源。
如果你希望数据规模更大,可以选用 EdNet 数据集,它由韩国一家教育公司推出,包含上亿条交互记录,数据字段更丰富,比如尝试次数、答题耗时等。但注意,EdNet 的数据量对一台普通电脑的训练来说可能过于庞大,建议先做采样。
4.2 数据清洗的完整流程
数据集拿到手绝不是直接用,第一步是清洗。ASSISTments 2009 数据集里常见的问题包括:部分记录缺失知识点 ID、部分学生做题序列过短(不足两次交互)、某些习题 ID 对应多个知识点导致序列切分困难。
我的清洗流程分为四步:
- 剔除缺失关键字段的记录(student_id、question_id、correct)。
- 过滤序列长度小于 2 的学生记录。
- 对同一道题对应多个知识点的情况,采取“多知识点拆分为多条样本”的策略,即同一次交互会同时影响所有相关知识点。
- 按 student_id 分组,再按时间戳排序,保证每个学生的答题序列在时间上是递增的。
从 50 万条原始数据清洗下来,大概会剩下 42 万条有效记录,删除比例在 15% 左右属于正常现象。
4.3 序列切分与滑动窗口策略
知识追踪模型并不需要一次性输入学生从开始到现在的全部记录。一方面 LSTM 存在长序列梯度问题,另一方面历史久远的记录对当前预测的贡献非常有限。因此,通常采用定长滑动窗口对序列做截断,窗口长度一般取 20-50。
我实测下来,窗口长度为 30 是表现最优且训练速度较快的配置。每个窗口内部,包含 30 次连续答题交互,模型预测第 31 题的答对概率。如果学生历史记录超过窗口长度,就按步长滑动生成多条训练样本,这不仅扩充了训练集,也提高了模型对序列局部模式的敏感度。
4.4 特征工程:不止是正确率
很多同学拿到数据集就只用一个二元正确率特征,但这样信息利用太浪费。我额外构造了三个特征:
- 最近一次作答的间隔时间:反映遗忘行为,间隔越长答对概率越低。
- 该知识点下历史答对率:反映学生对这块知识的稳定掌握程度。
- 连续答对/答错次数:反映学生当前做题状态,连续答错可能意味着挫败或知识点理解有偏差。
这些特征拼接后输入 LSTM,可以有效提升模型在冷启动场景下的预测效果。要注意的是,这些特征在推理阶段也能实时计算,不需要未来信息。
5. 基于知识状态的习题推荐模块实现
5.1 从预测概率到推荐策略的转化
模型输出的仅仅是“答对概率”,不能直接用作推荐结果。你需要定义一套推荐策略,把概率映射成推荐决策。这里我给出三种常用策略,并说明各自适用场景。
第一种是固定阈值法。设定阈值区间,例如推荐预测答对概率在 0.65 到 0.85 之间的习题。低于 0.65 说明过难,高于 0.85 说明过简单。这是实现最简单的策略,缺点是阈值需要人工调参。
第二种是 Top-N 选择法。先从题库中随机抽取候选池,用模型预测概率,然后按概率从小到大排序(由易到难),取概率落在目标区间的 Top-N 道题。适合推荐列表需要一定多样性的场景。
第三种是基于知识增益的推荐。在多个薄弱知识点之间,优先选择那些对提升整体知识状态增益最大的习题。这种策略实现稍复杂,但教育意义最强。
5.2 工程实现:推荐服务模块
推荐服务的核心是一个策略引擎类,把所有策略封装好,对外仅暴露一个获取推荐列表的方法:
class RecommendService: def __init__(self, model, question_pool, concept_map): self.model = model self.question_pool = question_pool self.concept_map = concept_map def get_recommendations(self, student_id, history, top_n=10): # 获取GIKT模型预测概率 prob = self.model.predict(history) # 选择预测答对率在[0.65, 0.85]范围内的候选题目 candidates = self._filter_by_probability(prob) # 优先覆盖未掌握薄弱知识点 candidates = self._cover_weak_concepts(candidates) # 按难度和知识点多样性排序,返回top_n return candidates[:top_n]这套设计把模型推理和业务策略解耦,后续想换策略只需在 RecommendService 里改动,模型完全不用动。
5.3 冷启动问题的处理方案
新学生没有历史作答记录,GIKT 模型无法给出知识状态。常见做法是走冷启动分支:默认推荐每个知识点的基础入门题,学生做完一定数量后切换到模型推荐。题库侧,按难度分级和知识点关联性预先生成几条“学习路径”。答辩时如果老师问到这个点,你可以回答使用“基于内容的知识点入门引导策略”,思路清晰且合理。
我在项目里做了一个更细的处理:冷启动阶段也会记录学生的作答结果,一旦样本量到达 5 条,就立即切换到 GIKT 模型推荐,减少冷却时间。
5.4 推荐结果可解释性设计
可解释性是教育推荐系统区别于一般电商推荐的重要特点。GIKT 模型天然支持一定程度的解释:我们可以记录知识状态向量与知识点嵌入的相似度,从而找出当前学生最薄弱的 top-3 知识点,在推荐列表旁边展示“系统推荐这些题目的原因是你在‘电路分析’知识点上正确率较低,需要强化练习”。
这个功能在展示时非常加分,它让推荐结果不再是黑盒,而是有逻辑支撑的。实现上只需要在预测阶段多返回一个知识点相似度排名。
6. 验证评估、高频踩坑与答辩准备
6.1 模型评估指标选什么
知识追踪领域最主流的评估指标是 AUC(Area Under the ROC Curve),它衡量的不是预测概率的绝对值准不准,而是预测概率对正负样本的排序能力。在实际做题场景中,我们更关心“答对的和答错的题,预测概率谁高谁低”,所以 AUC 非常契合。
当然也可以用 ACC(准确率),但需要人为设定一个阈值,不同阈值对结果影响很大,不太稳定。建议在论文里同时汇报 AUC 和 ACC,并解释 AUC 更关注排序、ACC 更关注分类准确度。
6.2 训练过程中的典型坑
第一个坑是数据顺序问题。我一开始直接用 DataFrame 里的原始顺序,没有按时间戳排序,导致模型在时间上穿越,AUC 虚高,一度逼近 0.95。后来发现是数据泄漏,排序之后 AUC 立刻降到 0.78 左右。这个问题非常隐蔽,建议每一步清洗后都打印数据预览确认顺序。
第二个坑是序列填充造成的误导。使用 pack_padded_sequence 时,padding 部分不能参与损失计算。如果直接把 padding 部分当作真实序列输入,模型会觉得学生做了一堆不存在的题,预测会严重偏斜。
第三个坑是图卷积边索引的构建。很多同学把习题 ID 和知识点 ID 直接混用,导致节点 ID 冲突。我在前面的架构中特意安排习题节点 ID 从 0 开始、知识点节点 ID 从 N 开始,就是为了避免重叠,这个细节在实现时千万要小心。
6.3 答辩准备:主动引导老师的提问方向
答辩时间有限,你要学会引导老师关注你做得最好、最有把握的部分。我的策略是准备一份三页纸的核心材料:第一页是系统架构图,第二页是 GIKT 模型结构图,第三页是实验结果对比表。
答辩陈述时,我主动提出自己做了三件“加了分”的事:
- 对比了 DKT 和 GIKT 的效果,量化说明了引入图结构后对数据稀疏问题的缓解效果。
- 实现了推荐结果可解释性模块,不只是给列表,还给出推荐理由。
- 设计了冷启动分支策略,覆盖了真实教育场景中的关键边缘情况。
这样老师即使问了一些扩展问题,也都是在你熟悉的框架内,只要逻辑自洽,答案不会太偏。
6.4 项目代码结构建议
最后聊一下代码目录的组织方式。我见过太多毕业设计代码是一堆 Jupyter Notebook 散落在文件夹里,这给答辩印象分打了很大折扣。建议用清晰的分层结构:
project_root/ ├── data/ # 原始数据与预处理后数据 ├── models/ # GIKT 模型定义与训练脚本 ├── services/ # 推荐服务、模型推理服务 ├── api/ # FastAPI 应用与路由 ├── web/ # 前端项目 ├── docs/ # 文档说明 └── requirements.txt # 项目依赖代码规范是答辩印象分的重要组成部分,一份结构清晰、注释规范的工程代码,即使算法性能略逊一筹,也会让老师认为你具备良好的工程素养。反过来,代码乱七八糟,模型表现再好也会大打折扣。
写在最后
做这个项目最大的体会是,算法模型本身固然重要,但系统性地把一个技术方案落地成可用产品的过程,才是毕业设计真正的训练价值所在。从数据清洗到模型调参,从推荐策略到接口部署,每一步都需要沉下心去打磨。如果你正在做这个题目,建议把重心放在三个地方:一是吃透 GIKT 的图构建逻辑,二是做好数据时序处理避免泄漏,三是把推荐策略讲清楚。这三点做到位,答辩基本就稳了。
本文还有配套的精品资源,点击获取