简介:这份资源是面向计算机相关专业学生与项目实战学习者的毕业设计级Python项目,核心为基于GIKT深度知识追踪模型的习题推荐系统,可用于毕业设计、课程设计或期末大作业,帮助解决推荐算法与知识追踪结合的实际落地问题。压缩包共59个文件,约9.46MB,以20个Python后端源码、13个Vue前端组件、7个JavaScript脚本为主,另含JSON配置、PNG/JPEG图像、CSV数据、NPY模型文件及README说明,前后端分离结构清晰,便于按模块阅读与二次开发。项目经导师指导并认可,已严格调试确保可运行,配套使用教程可辅助快速理解GIKT模型推理与习题推荐流程。目前已有236人学习下载,适合需要完整源码参考、算法实践与排错思路的中高级学习者。
1. 从一份 GIKT 习题推荐源码说起:它到底能跑出什么
如果你正在做毕业设计或者期末大作业,选题方向是「知识追踪 + 习题推荐」,大概率已经在 GitHub 和各类资源站里翻过一圈了。这份基于 GIKT 深度知识追踪模型的习题推荐系统 Python 实现源码,解决的就是一个很具体的问题:给定学生在历史答题序列上的对错表现,预测他在下一个知识点上的掌握程度,并据此推荐最合适的下一道题。它不是那种只跑一个模型、输出一个准确率就结束的 demo,而是把数据处理、模型训练、推荐生成串成了一条能走通的链路。
适合谁用?一是需要快速搭出一个可演示系统的同学,二是想理解 GIKT 相比 DKT、DKVMN 这些经典模型到底改了什么的人。源码里通常包含数据集加载脚本、模型定义、训练入口和推荐接口,你拿到手之后需要自己配 Python 环境、装依赖、把数据路径改对。下面我按「先搞清楚模型在算什么 → 再动手把环境跑通 → 最后处理那些一定会遇到的坑」的顺序拆一遍,中间会给可直接抄的命令和参数说明。
2. GIKT 模型拆解:图结构怎么把知识点和习题连起来
2.1 为什么不是直接用 DKT 做推荐
DKT 把答题序列当成一个纯时序问题,用 RNN 去建模,隐藏状态里隐含了学生的知识状态。问题在于,它没有显式地利用「知识点之间的先修关系」和「习题与知识点的归属关系」。GIKT 的核心改进就是引入图结构:把知识点、习题、学生答题记录组织成异构图,让信息沿着边传播。常见做法是构建两类边——知识点与知识点之间的关联边(比如同一章节、先修后继),以及习题与知识点之间的归属边。这样模型在做预测时,不只是看学生答过什么,还能看到这些题背后牵连的知识网络。
从推荐的角度看,这一点很关键。纯时序模型容易推荐出「学生刚做过的同类题」,而图结构能让推荐结果覆盖到相邻但尚未掌握的知识点,推荐多样性会好一些。源码里一般会有一个build_graph或类似的函数,负责从原始交互记录里抽边、建邻接矩阵。这里就会用到python构建邻接矩阵的常见写法,通常是numpy或scipy.sparse。
2.2 数据到图的转换:邻接矩阵怎么建
先看一段典型的构图代码,逻辑是从「学生-习题-知识点」三元组里抽出知识点共现关系:
import numpy as np from scipy.sparse import coo_matrix def build_knowledge_graph(records, num_skills): """ records: list of (student_id, exercise_id, skill_id, correct) num_skills: 知识点总数 返回归一化后的知识点邻接矩阵 """ rows, cols, data = [], [], [] # 统计同一学生序列中相邻知识点的共现 for stu_records in group_by_student(records): skills = [r[2] for r in stu_records] for i in range(len(skills) - 1): s1, s2 = skills[i], skills[i + 1] if s1 != s2: rows.append(s1) cols.append(s2) data.append(1.0) # 用 coo 格式快速构建稀疏矩阵 adj = coo_matrix((data, (rows, cols)), shape=(num_skills, num_skills)).tocsr() # 对称化并做行归一化,避免度数差异过大 adj = adj + adj.T deg = np.array(adj.sum(axis=1)).flatten() deg[deg == 0] = 1.0 norm_adj = adj.multiply(1.0 / deg[:, None]).tocsr() return norm_adj这段代码做了三件事:遍历每个学生的答题序列,把相邻出现的不同知识点记成一条边;用coo_matrix构建稀疏邻接矩阵,避免知识点数量大时内存爆掉;对称化后按行归一化,让每个知识点在聚合邻居信息时权重稳定。参数上,num_skills必须和数据集里知识点编号的最大值对齐,否则矩阵形状会错。如果你发现训练时 loss 一直是 nan,先检查这里有没有除零——deg[deg == 0] = 1.0就是防这个的。
2.3 模型前向:图卷积和序列建模怎么拼
GIKT 的前向一般分两路:一路用图卷积在知识点图上传播,得到每个知识点的增强表示;另一路用 RNN 或注意力机制处理学生的答题序列。两路结果在预测层融合,输出学生在每个知识点上的掌握概率。源码里模型定义通常在model.py,你会看到类似GCNLayer、SequenceEncoder、PredictLayer的类。关键参数包括图卷积层数(一般 2 层就够,多了会过平滑)、隐藏维度(常见 64 或 128)、dropout 比例(0.2 到 0.5 之间调)。
这里有个容易忽略的点:图卷积层数不是越多越好。知识点图本身不大,两层之后每个节点已经能聚合到二跳邻居,再深反而让所有节点表示趋同,推荐结果会变得没有区分度。我一般会先把层数设成 2,跑一版看验证集 AUC,再决定要不要加。
3. 把源码跑起来:环境、数据、训练三步走
3.1 Python 环境与依赖安装
拿到源码包后,第一件事是确认 Python 版本。这类模型代码大多在 Python 3.8 到 3.10 之间验证过,太新的版本可能遇到 PyTorch 和 numpy 的兼容问题。如果你机器上还没装 Python,去官网下载安装包时记得勾选「Add Python to PATH」,否则后面在命令行里敲python会提示找不到命令。装完先验证:
python --version pip --version然后建一个虚拟环境,避免和系统里其他项目的包打架:
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活后装依赖。源码包里一般有requirements.txt,直接:
pip install -r requirements.txt如果没有这个文件,常见依赖是这些:
pip install torch numpy scipy pandas scikit-learn tqdm注意numpy和torch的版本要匹配,比如 torch 1.13 配 numpy 1.24 一般没问题,但 torch 2.x 配太老的 numpy 可能报module compiled against API version的错。遇到这种,先pip install numpy --upgrade再试。
3.2 数据集准备与路径配置
源码通常自带一个小规模示例数据,或者要求你下载 ASSISTments、EdNet 这类公开数据集。不管哪种,你都需要确认三件事:数据文件放在哪个目录、代码里读数据的路径是什么、字段格式对不对。常见的数据格式是每行一条交互记录,包含学生 ID、习题 ID、知识点 ID、答题结果(0/1)和时间戳。
打开配置文件或config.py,找到类似这样的字段:
DATA_PATH = "./data/assistments/" BATCH_SIZE = 32 MAX_SEQ_LEN = 200 NUM_SKILLS = 123 HIDDEN_DIM = 64 EPOCHS = 50 LR = 0.001NUM_SKILLS必须和你的数据集里知识点实际数量一致。如果设小了,构图时会出现索引越界;设大了,邻接矩阵里多出一堆孤立节点,浪费显存还拉低效果。MAX_SEQ_LEN控制每条序列截断长度,太长显存吃紧,太短会丢历史信息,一般 100 到 200 之间。BATCH_SIZE在显存允许的前提下尽量大一点,训练更稳。
3.3 训练与推荐结果生成
配置改好后,训练入口通常是train.py或main.py:
python train.py --data_path ./data/assistments/ --epochs 50 --lr 0.001训练过程中关注两个指标:验证集上的 AUC 和 loss 曲线。AUC 在 0.75 以上说明模型学到了东西,如果一直在 0.5 附近晃,多半是数据标签或图构建出了问题。loss 不降反升,先查学习率是不是太大,或者图归一化有没有做对。
训练完会保存模型权重,推荐脚本加载权重后,对每个学生输出知识点掌握概率,再按「掌握度低 + 知识点重要度高」排序,选出推荐习题。推荐接口一般长这样:
def recommend(model, student_seq, top_k=10): model.eval() with torch.no_grad(): probs = model.predict(student_seq) # shape: [num_skills] # 掌握度越低越优先推荐 sorted_idx = torch.argsort(probs) return sorted_idx[:top_k].tolist()top_k控制推荐数量,实际演示时设 5 到 10 比较合适。probs是每个知识点的掌握概率,排序取最低的几个,再映射回该知识点下的习题。这里要注意,如果某个知识点下没有习题,映射会返回空,代码里最好加一层过滤。
4. 避坑与排查:那些让训练跑不起来的常见问题
4.1 现象:ImportError 找不到模块
原因:依赖没装全,或者虚拟环境没激活。有时候你在系统 Python 里装了包,但跑代码用的是虚拟环境,两边不互通。
解决:先确认which python或where python指向的是虚拟环境里的解释器,再pip list看关键包在不在。缺什么补什么,别在系统环境里硬装。
4.2 现象:训练 loss 为 nan
原因:常见有三种——学习率太大、图归一化时出现除零、输入里有非法索引。GIKT 涉及图卷积,邻接矩阵如果有全零行,归一化就会产生 nan。
解决:把学习率降到 1e-4 试一版;检查构图代码里有没有deg[deg == 0] = 1.0这类保护;打印一下NUM_SKILLS和实际最大知识点 ID,确认没越界。
4.3 现象:AUC 一直在 0.5 附近
原因:数据标签反了,或者序列顺序被打乱。知识追踪极度依赖答题的先后顺序,如果 DataLoader 里做了 shuffle 且没保留序列结构,模型看到的就是乱序。
解决:检查标签 0/1 的含义是否和模型预期一致;确认序列是按时间戳排序后再截断的,不要先截断再排序。
4.4 现象:显存不够,CUDA out of memory
原因:BATCH_SIZE或MAX_SEQ_LEN设太大,或者图卷积在稠密矩阵上算。
解决:先把BATCH_SIZE减半,再把MAX_SEQ_LEN降到 100。构图时坚持用scipy.sparse,不要用numpy稠密数组,知识点上千时差距非常明显。
4.5 现象:推荐结果全是同一类题
原因:图卷积层数过多导致过平滑,所有知识点表示趋同;或者推荐时只按掌握度排序,没考虑知识点多样性。
解决:把图卷积层数降到 2;推荐排序时加一个多样性惩罚,比如对已经推荐过的知识点下的题做降权。
5. 进阶技巧:让推荐结果更可解释、更好演示
5.1 给推荐结果加上知识点解释
答辩或演示时,光说「推荐了第 3、7、12 题」说服力不够。可以在推荐接口里把知识点名称和掌握概率一起输出:
def recommend_with_explain(model, student_seq, skill_names, top_k=5): model.eval() with torch.no_grad(): probs = model.predict(student_seq).cpu().numpy() sorted_idx = probs.argsort() results = [] for idx in sorted_idx[:top_k]: results.append({ "skill_id": int(idx), "skill_name": skill_names.get(idx, "未知知识点"), "mastery": round(float(probs[idx]), 4) }) return resultsskill_names是一个从知识点 ID 到名称的字典,可以从数据集附带的元信息里读。这样输出的是「你在‘一元二次方程求根’上掌握度只有 0.23,建议先练这道题」,比单纯给题号直观得多。
5.2 用验证集做一次离线评估
在真正推荐之前,建议用验证集模拟一遍:对每个学生的前 N 条记录做输入,预测第 N+1 条的知识点,看推荐列表里有没有命中。命中率(Hit Rate)和 NDCG 是两个常用指标。代码不复杂,核心就是遍历验证集、调推荐接口、统计命中:
def evaluate_hit_rate(model, val_loader, top_k=10): hits, total = 0, 0 for batch in val_loader: seq, next_skill = batch recs = recommend(model, seq, top_k) if next_skill.item() in recs: hits += 1 total += 1 return hits / total这个指标比 AUC 更贴近推荐场景,答辩时拿出来也更有说服力。top_k一般设 10,Hit Rate 能到 0.4 以上就算不错。
5.3 一个我踩过的坑
最早跑这份源码时,我图省事直接把NUM_SKILLS设成了数据集里知识点 ID 的最大值加一,结果训练集里有些知识点在验证集里从没出现过,图卷积聚合时这些节点没有邻居,表示一直是初始化的随机值,推荐出来全是噪声。从那以后我每次构建图之前都强制走一遍「统计实际出现的知识点 ID 集合 → 重新映射成连续编号 → 再设 NUM_SKILLS」的流程,宁可多写十行映射代码,也不让孤立节点混进去。希望这份拆解帮到你,少走点弯路。
本文还有配套的精品资源,点击获取