ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GIKT深度知识追踪与习题推荐系统实践

GIKT深度知识追踪与习题推荐系统实践 简介这是一份面向计算机相关专业毕业设计或课程设计场景的Python源码项目核心是基于深度知识追踪GIKT模型的习题推荐系统。资源包含完整的后端与前端工程后端以Python Flask实现模型训练与推荐接口前端基于Vue 2构建交互界面并配有JSON配置、数据库文件、说明文档与少量图片素材方便学习者从数据到模型再到推荐的完整链路进行调试与复现。压缩包共59个文件累计约9.46MB主要涉及20个py核心代码、13个vue页面组件、7个js脚本以及相关静态资源结构清晰便于按模块阅读。当前已有126人学习下载适合具备一定Python和深度学习基础、希望快速搭建智能教育类应用的学生参考。代码经严格调试可运行通过阅读源码可掌握GIKT在知识点建模与习题推荐中的实际落地方式同时为毕业论文或课程答辩提供可演示的系统原型。1. 基于深度知识追踪GIKT模型的习题推荐系统先锁定概念关系毕业设计选“基于深度知识追踪GIKT模型的习题推荐系统”通常踩中两个刚需一是预测学生下一题正确率二是推荐真正有查漏补缺意义的习题。传统DKT把知识点当成独立编号只依赖答题序列丢失概念关联GIKT引入图交互先建知识点关系图再用图卷积更新表示让模型感知“学过一元二次方程后再做根的判别式是否合理”。对教育推荐开发者来说这套方案打通“先预测掌握度再按缺口推送习题”的完整链路。真实工作量集中在三处知识点关系图构建、图卷积与序列编码耦合、预测概率改造成推荐列表。2. GIKT的图交互建模深度知识追踪如何把概念关联算进前向传播2.1 从序列模型到图模型GIKT在建模上的关键改动知识追踪的目标是根据学生过去答题表现预测当前答题表现。DKT用RNN编码每一步交互但所有知识点都用独立one-hot模型学不到“相似知识点”。GIKT在深度知识追踪基础上加入图信息先定义知识点关系图把习题映射到对应知识点再通过图卷积得到知识嵌入。这样模型在读取每一步交互时不仅知道“学生做了一道什么题”也知道这道题牵连了哪些相邻知识点。实际训练中这个改动让预测AUC比同配置DKT高2到4个百分点在ASSISTments这类数据集上尤其明显。2.2 知识点关系图的构造与邻接矩阵常见做法有两种直接用数据集自带的技能关系或者从答题记录统计共现频率。毕业设计优先用后者因为数据可得性好可解释性强。把两个知识点在同一道习题中出现的次数归一化后保留大于阈值的边得到邻接矩阵A。A[i][j]越大说明做完知识点i的学生越有可能碰知识点j。需要注意阈值不能太大否则会截断大多数关系也不能太小否则邻接矩阵全连接图卷积退化成多层全连接。2.3 用PyTorch实现图卷积知识嵌入以下是GIKT前向传播中必要的图卷积部分大多数Python实现里这一步的核心逻辑如下import torch import torch.nn as nn class GraphConv(nn.Module): def __init__(self, in_dim, out_dim, adj): super().__init__() # register_buffer保证邻接矩阵随模型迁移到GPU self.register_buffer(adj, adj) self.w nn.Linear(in_dim, out_dim, biasFalse) self.act nn.Tanh() def forward(self, x): # x: [num_concepts, in_dim] 或 [batch, num_concepts, in_dim] if x.dim() 3: # 批量节点特征adj.t()让每个节点聚合邻居特征 out torch.matmul(x, self.adj.transpose(0, 1)) else: out self.adj x return self.act(self.w(out))GraphConv使用矩阵乘法聚合邻居表示再用线性层映射最后用Tanh激活。register_buffer可以让adj在模型调用.to(cuda)时自动迁移避免GPU训练时数据类型不一致。adj在传入前必须做对称归一化A_norm D^{-1/2} A D^{-1/2}否则多层卷积会放大高阶邻居的数值训练很快出现NaN。注意torch.matmul(x, adj.transpose(0,1))中的转置方向。邻接矩阵如果是对称的转不转都一样但写成转置形式更通用便于扩展到有向图。2.4 图嵌入怎么接入GRU交互序列完成图卷积后得到每个知识点的稳定嵌入c_g。把习题嵌入和知识点图嵌入拼起来再拼接上一题正确标签的嵌入一起输入GRU。隐藏状态h_t作为当前掌握度表示与全部知识点嵌入点积并经过Sigmoid即可得到下一题正确率预测。常见参数嵌入维度64到128图卷积层数1到2层超过2层在中小规模数据上容易过平滑所有节点表示趋同AUC反而下降。下表对比了常见深度知识追踪模型的特点模型知识点关联建模输入特征适合习题推荐DKT无题目one-hot 正确标签可预测难解释DKVMN隐式记忆矩阵概念键、值向量有记忆机制但依赖数据GIKT显式图卷积图嵌入 交互状态能感知知识缺口推荐可控提示如果数据集没有提供现成知识图谱不要手动编概念树。用“同一道习题涵盖多个知识点”的关系生成共现矩阵通常比人工规则更平滑也更贴近真实做题路径。推荐系统在召回阶段同样能利用这张图从GIKT预测概率较高的题目集合出发沿邻接矩阵扩散出邻居知识点上的备选题目可以避免推荐列表全部集中在同一个高频知识点。这正是后文推荐模块要用的图扩散思路。3. 用Python实现GIKT习题推荐系统从数据预处理到训练循环3.1 数据集格式与基于学生聚合的预处理演示用最小数据集CSV文件包含student_id, question_id, concept_id, correct, timestamp。训练前必须按学生和时间排序。预处理分三步把知识点映射成连续索引把每个学生的答题序列切成长度为L的窗口从全部记录构造共现邻接矩阵。下面代码是构造序列和邻接矩阵的核心部分import pandas as pd import numpy as np from collections import defaultdict def build_sequence(df, max_seq_len100): df df.sort_values([student_id, timestamp]) seq_map defaultdict(list) correct_map defaultdict(list) for row in df.itertuples(): seq_map[row.student_id].append(row.concept_id) correct_map[row.student_id].append(row.correct) # 统一截断到最近max_seq_len题保留最新答题反应 for sid in list(seq_map.keys()): seq_map[sid] seq_map[sid][-max_seq_len:] correct_map[sid] correct_map[sid][-max_seq_len:] return seq_map, correct_map def extract_pairs(df): q_concepts df.groupby(question_id)[concept_id].apply(list) pairs set() for concepts in q_concepts: for i in range(len(concepts)): for j in range(i 1, len(concepts)): pairs.add((concepts[i], concepts[j])) return list(pairs) def build_adj(concept_pairs, num_concepts): adj np.zeros((num_concepts, num_concepts), dtypenp.float32) for i, j in concept_pairs: adj[i][j] 1.0 adj[j][i] 1.0 degree adj.sum(axis1, keepdimsTrue) inv_sqrt_deg np.power(degree, -0.5) inv_sqrt_deg[np.isinf(inv_sqrt_deg)] 0.0 return inv_sqrt_deg * adj * inv_sqrt_deg.Tbuild_sequence按学生和时间聚合原始答题记录得到每个学生的知识点ID序列和正确标签序列。extract_pairs把同一道题中的知识点两两组合成共现边写入集合去重。build_adj先统计边计数再做对称归一化。degree为0时np.power(degree, -0.5)会产生inf需要替换成0。如果产生的边太多只保留权重最高的30%作为最终图既能降低图卷积参数规模也能去掉低频噪声。3.2 GIKT模型定义图卷积与GRU的完整耦合确认数据没问题后模型部分最难的是把图卷积后的概念嵌入和GRU序列编码结合。下面是一个可直接训练的基础版本也可以当作正式项目源码里的gikt_model.pyimport torch import torch.nn as nn import torch.nn.functional as F class GIKTModel(nn.Module): def __init__(self, num_concepts, num_questions, emb_dim, adj, dropout0.1): super().__init__() self.q_emb nn.Embedding(num_questions, emb_dim) self.c_emb nn.Embedding(num_concepts, emb_dim) self.corr_emb nn.Embedding(2, emb_dim) self.gcn1 GraphConv(emb_dim, emb_dim, adj) self.gcn2 GraphConv(emb_dim, emb_dim, adj) self.gru nn.GRU(emb_dim * 3, emb_dim, batch_firstTrue) self.out nn.Linear(emb_dim, 1) self.drop nn.Dropout(dropout) def forward(self, q_ids, c_ids, correct): q_emb self.q_emb(q_ids) # [batch, seq, emb] c_graph self.gcn2(self.gcn1(self.c_emb.weight)) # [num_concepts, emb] c_emb F.embedding(c_ids, c_graph) # [batch, seq, emb] corr_emb self.corr_emb(correct.long()) # [batch, seq, emb] x torch.cat([q_emb, c_emb, corr_emb], dim-1) h, _ self.gru(x) logits self.out(self.drop(h)).squeeze(-1) # [batch, seq] return logits模型先对概念嵌入做两层图卷积得到融合了邻居信息的概念表示c_graph再用F.embedding按当前交互的知识点ID抽取对应向量。每个时间步的输入是习题嵌入、概念图嵌入、上一题对错嵌入的三者拼接维度是emb_dim * 3。如果去掉corr_embGRU就只能学到题目顺序学不到“上次答错后应变难”的重要信号。correct传入后只在输入中使用训练损失时需要进行时间步错位否则模型会直接抄当前答案。3.3 训练循环时间步错位与梯度裁剪GIKT属于典型序列预测输入t时刻的特征预测t1时刻的正确率。训练时要把模型的输出和标签错开一位同时用掩码过滤补零位置。训练循环和超参设置如下optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.5) criterion nn.BCEWithLogitsLoss(pos_weighttorch.tensor([1.5])) for epoch in range(20): for q_ids, c_ids, correct, mask in dataloader: logits model(q_ids, c_ids, correct) pred logits[:, :-1] # 预测第1到L-1步 target correct[:, 1:].float() # 对应真实标签第2到L步 valid mask[:, 1:] loss criterion(pred[valid], target[valid]) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step()logits[:, :-1]剔除了最后一步的输出correct[:, 1:]剔除了第一步的输入标签这样时间步严格对齐。mask用来过滤padding位置避免序列长度不一时把无效位置计入损失。pos_weight1.5是因为答对样本通常多于答错样本适度提高答错权重能让预测更敏感。梯度裁剪对GRU尤其重要GIKT训练到第3到第5轮时loss突然变成NaN绝大多数原因是图卷积梯度爆炸max_norm1.0能直接避免。学习率用StepLR每5轮衰减一半比固定学习率更容易收敛。3.4 常用超参数与过拟合判断下表是这类系统里比较靠谱的起点参数建议在其他数据集上围绕它们做小范围搜索参数推荐值说明emb_dim64嵌入维度低于32会出现欠拟合gcn_layers1~2图卷积层数3层以上易过平滑seq_len100学生最长输入序列过长占用显存dropout0.1~0.2全连接与输出层使用lr1e-3Adam配合StepLR每5轮减半pos_weight1.5根据正负样本比例调整判断是否过拟合不能只看损失训练集AUC超过0.95而验证集不到0.8时先加dropout到0.3同时减少一层图卷积。如果验证集AUC连续10轮不升检查数据有没有按时间切分。随机打乱学生序列会让验证集混入同一学生的数据GIKT会直接记住最近表现得到一个虚高的精度。4. 把GIKT输出变成习题推荐候选召回、难度校准与Top-N列表4.1 从正确率预测到推荐分数的变换GIKT输出的logits经过Sigmoid后是正确率概率但直接用这个概率排序推荐会让学生一直拿到简单题。推荐系统要把概率改造成“推荐分”score p - alpha * difficulty beta * coverage。p是GIKT预测正确率difficulty是题目归一化难度coverage表示待推荐题目的知识点是否属于该学生最近答错的知识点。alpha通常取0.2到0.4beta取0.1到0.3。这样推荐的题目既不会太难也不会永远停留在简单区。为什么直接排序不可行预测正确率p会偏向高频简单题因为这些题在训练集中正确率高且样本量大模型不需要真正掌握概念也能输出高分。减去难度校准后排名会向中等难度题移动这种题目才是学生最近发展区。coverage项则负责把待测知识点纳入排序即使该题预测正确率不高只要它覆盖的知识点恰好是最近两次答错的技能推荐分也会上升。这三项之间的权重需要根据数据集做5次左右的小规模网格搜索alpha和beta各取0.1的步长即可不需要搜索得太细。4.2 基于学生历史会话的推荐候选生成下面代码演示了为单个学生生成Top-K推荐的过程。候选题目是学生没做过的题排除已答题后再做打分def recommend_for_student(model, c_graph, history, candidate_questions, qid2concept, difficulty_map, alpha0.3, top_k10): model.eval() with torch.no_grad(): logits model(history[q_ids], history[c_ids], history[correct]) prob torch.sigmoid(logits) # [batch, seq_len] mastery prob[:, -1] # 取最后一步隐藏概率作为掌握度近似 candidates [] for qid in candidate_questions: if qid in history[seen_q]: continue concept qid2concept[qid] # 用该题的concept嵌入和mastery做点积得个性化正确率 p_item (c_graph[concept] * mastery).sigmoid() diff difficulty_map[qid] score p_item - alpha * diff candidates.append((qid, score)) candidates.sort(keylambda x: -x[1]) return [qid for qid, _ in candidates[:top_k]]mastery严格来说应该是GRU最后一步的隐藏状态而不是输出概率这里为了缩短代码用最后一步logits近似实际工程中建议在模型里暴露get_mastery()返回h[-1]。c_graph是前面图卷积得到的概念嵌入。score中的p_item已经有学生个性化成分alpha * diff用于抵消“简单题得分虚高”的问题。如果候选集合很大可以先按知识点召回一部分再做上面排序避免全量打分。4.3 推荐效果评估不只是AUC只报AUC容易被追问“推荐到底好在哪”。毕业设计至少要补Recall10和NDCG10。Recall10看推荐列表是否覆盖学生下一阶段实际答错或需要的题目NDCG看推荐对的题是否排在前面。手写Recall10很直观def recall_at_k(recommended, actual, k10): if not actual: return 0.0 hit len(set(recommended) set(actual)) return hit / min(len(actual), k)分母取min(len(actual), k)而不是len(actual)因为很多学生一个阶段只做几道题如果题目数少于k打分应按有效作答题目数计算。评估时用GIKT在验证集上的预测结果生成推荐列表取学生“下一时间片”的答题记录作为actual。如果一个推荐列表里全是高频概念题Recall可能很高但学习价值低所以还需要报告推荐列表的知识点覆盖率覆盖率高意味着推荐没有堆在一个知识点上。4.4 三种推荐策略的对照下表给出了不同策略在常见公开数据集上的大致趋势具体数值随实现不同而变化重点是看相对差异策略是否感知难度是否利用概念关系推荐多样性直接按预测正确率排序否否低正确率 难度校准是否中GIKT 图扩散召回是是高图扩散召回的具体做法是先用GIKT预测所有候选题目的正确率取概率最高的N道题作为种子沿邻接矩阵连出这些题所涉及知识点的邻居再把邻居知识点上的题目加入候选池最后统一用4.1的公式打分。扩散系数控制在0.3左右过大会引入大量无关知识点上的题目推荐列表看起来丰富但学生根本没见过前置概念。如果想让推荐结果具备可解释性可以在推荐卡片上标注“本题涉及一元二次方程关联你上次做错的抛物线顶点式”这需要把GIKT的图路径显式提取出来。5. GIKT训练中的四个典型陷阱与验证技巧5.1 数据划分陷阱随机打乱会让AUC虚高知识追踪样本来自学生答题序列随机划分会把同一学生的时间片段同时放进训练和测试模型看见“未来”的答题记录。正确做法是按学生最后答题时间切分前80%时间作为训练后20%作为测试。如果数据来自多个学期按学期边界切分比按学生切分更能反映真实场景。5.2 邻接矩阵稀疏性陷阱知识点数量超过200时共现矩阵往往非常稀疏图卷积会把大部分节点表示退化。建议只保留每个知识点权值最高的3到5个邻居并打印孤立节点数量degree adj.sum(dim1) print(孤立节点数:, (degree 0).sum().item())如果孤立节点很多要么降低构建阈值要么用SVD初始化概念嵌入但不要手工伪造边。孤立概念在GIKT中相当于没有图信息只能依赖GRU序列信号这会影响后续推荐召回。5.3 预测概率的置信度校准BCE损失训练出的概率不等于真实正确率。用温度缩放校准让低分题目更保守、高分题目更激进。校准前先找出验证集上预测概率接近0.5的题目如果这类题对应难度中等的题目说明模型没区分开“会做”和“蒙对”把温度T设为0.9后再做推荐排序Top-N列表往往更稳定。5.4 用验证AUC模型做最终评估GIKT训练第1轮AUC可能只有0.6第2轮图卷积生效后升到0.8但继续跑到第10轮不一定更好。每轮保存验证AUC最高的权重文件而不是最后一个epoch。最终推荐效果以这个权重文件重新生成测试集上的推荐列表评估结果才可复现。同时在训练日志里同时记录loss和AUCloss下降不代表AUC上升因为二分类loss没有针对排序优化。本文还有配套的精品资源点击获取
返回列表