news 2026/9/14 9:15:36

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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, bias=False) 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_len=100): 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), dtype=np.float32) for i, j in concept_pairs: adj[i][j] += 1.0 adj[j][i] += 1.0 degree = adj.sum(axis=1, keepdims=True) 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.T

build_sequence按学生和时间聚合原始答题记录,得到每个学生的知识点ID序列和正确标签序列。extract_pairs把同一道题中的知识点两两组合成共现边,写入集合去重。build_adj先统计边计数,再做对称归一化。degree为0时,np.power(degree, -0.5)会产生inf,需要替换成0。如果产生的边太多,只保留权重最高的30%作为最终图,既能降低图卷积参数规模,也能去掉低频噪声。

3.2 GIKT模型定义:图卷积与GRU的完整耦合

确认数据没问题后,模型部分最难的是把图卷积后的概念嵌入和GRU序列编码结合。下面是一个可直接训练的基础版本,也可以当作正式项目源码里的gikt_model.py

import 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, dropout=0.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_first=True) 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_emb,GRU就只能学到题目顺序,学不到“上次答错后应变难”的重要信号。correct传入后只在输入中使用,训练损失时需要进行时间步错位,否则模型会直接抄当前答案。

3.3 训练循环:时间步错位与梯度裁剪

GIKT属于典型序列预测,输入t时刻的特征,预测t+1时刻的正确率。训练时要把模型的输出和标签错开一位,同时用掩码过滤补零位置。训练循环和超参设置如下:

optimizer = torch.optim.Adam(model.parameters(), lr=0.001) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) criterion = nn.BCEWithLogitsLoss(pos_weight=torch.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_norm=1.0) optimizer.step() scheduler.step()

logits[:, :-1]剔除了最后一步的输出,correct[:, 1:]剔除了第一步的输入标签,这样时间步严格对齐。mask用来过滤padding位置,避免序列长度不一时把无效位置计入损失。pos_weight=1.5是因为答对样本通常多于答错样本,适度提高答错权重能让预测更敏感。梯度裁剪对GRU尤其重要,GIKT训练到第3到第5轮时loss突然变成NaN,绝大多数原因是图卷积梯度爆炸,max_norm=1.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 * coveragep是GIKT预测正确率,difficulty是题目归一化难度,coverage表示待推荐题目的知识点是否属于该学生最近答错的知识点。alpha通常取0.2到0.4,beta取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, alpha=0.3, top_k=10): 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(key=lambda 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容易被追问“推荐到底好在哪”。毕业设计至少要补Recall@10和NDCG@10。Recall@10看推荐列表是否覆盖学生下一阶段实际答错或需要的题目,NDCG看推荐对的题是否排在前面。手写Recall@10很直观:

def recall_at_k(recommended, actual, k=10): 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(dim=1) 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和AUC,loss下降不代表AUC上升,因为二分类loss没有针对排序优化。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 9:14:15

大模型微调实战:用llmfit实现LoRA/QLoRA高效训练与业务落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 9:12:45

yq 安全策略全解析:漏洞报告流程、安全边界与依赖治理

yq 安全策略全解析:漏洞报告流程、安全边界与依赖治理 【免费下载链接】yq yq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor 项目地址: https://gitcode.com/GitHub_Trending/yq/yq 导读 本文以 yq 项目官方安全策…

作者头像 李华
网站建设 2026/9/14 9:12:45

Python实战:TCP端口扫描与DoS攻击检测及iptables自动封禁

简介:面向高校计算机网络、信息安全专业的毕业设计、课程设计与Python网络编程实践,这份资源提供了一套基于Python的TCP入侵检测系统完整源码。系统针对端口扫描与分布式拒绝服务两类典型威胁,从TCP连接请求频率、协议头部标志位组合、非监听…

作者头像 李华
网站建设 2026/9/14 9:12:38

阿里开源Agent全家桶实测:多Agent协作原理与落地实践

刷热搜刷到“阿里开源了一个神级Agent项目”,第一反应是翻收藏夹,把阿里系那几个Agent仓库挨个拉出来重新看了一遍。说实话,“神级”这种词放在标题里多少有点标题党,但当我真的把一个多Agent协作Demo跑起来之后,我发现…

作者头像 李华
网站建设 2026/9/14 9:11:06

MyBatis-Plus分页机制与性能优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华