GIKT知识追踪实战:用图卷积网络聚合知识点关系,提升答题预测准确率

发布时间:2026/9/30 10:18:05
GIKT知识追踪实战:用图卷积网络聚合知识点关系,提升答题预测准确率 简介基于图卷积网络的知识追踪模型GIKT是一份面向在线教育知识追踪研究人员的学术论文PDF。该模型借助GCN提取高阶题目-技能关联关系结合注意力机制与LSTM层捕获学习者长期行为变化并设计历史回顾模块和广义交互模块完成对新题目的作答预测有效缓解数据稀疏与多技能问题实验显示其在三个基准数据集上均达到最优AUC至少提升1%。压缩包内共1个PDF文件大小仅412KB包含完整方法设计、实验对比与模型结构细节出自上海交通大学研究团队对理解图神经网络在知识追踪中的应用很有帮助。目前已有211人浏览/学习适合科研入门、模型复现以及在线教育学情预测系统设计参考尤其适合作为相关课题的起点和基线对比模型。1. 当知识追踪撞上图卷积GIKT 到底在解决什么做过在线教育或自适应学习系统的工程师大概率都遇到过这样的需求根据学生过去几十道题的答题记录预测他下一道题能不能做对。这个任务在学术界叫知识追踪Knowledge Tracing最广为流传的解法是 DKT——拿一个 LSTM 把答题序列过一遍输出一个掌握状态向量。这套思路能跑但有个非常别扭的地方知识点之间的关联性LSTM 完全没用到。比如一次函数和二次函数强相关学生错了一次函数那二次函数出错的概率应该跟着涨但 DKT 只能靠隐向量里那点微弱信号去悟样本少了根本悟不出来。GIKTGraph-based Interaction Knowledge Tracing就是冲着这个痛点来的把知识点建模成一张图用图卷积网络GCN把相邻知识点的信息聚合进每个知识点的表示里再去预测答题结果。本文会从原理到代码把这套模型的实现路径、参数设定、踩坑点完整讲一遍。适合谁看正在做智能教育平台、想从 DKT 换到图方案但不知道从哪下手的工程师以及想在公开数据集上复现一个能跑的知识追踪模型的研究者。目标很明确看完你能动手写出一个 GIKT 的训练流程而不是只停留在概念上。2. 从 DKT 到 GIKT为什么非要引入图卷积这一层2.1 DKT 的短板序列模型学了顺序丢了结构DKT 的思路是把学生的答题记录按时间排列每个时刻输入一道题的相关特征题目编号、知识点编号、答对与否经过 LSTM 的循环更新把隐藏状态当作当前的知识掌握度。这个状态随后和一个题目嵌入做点积经过 sigmoid 输出预测概率。问题在于LSTM 的隐藏状态是稠密的、无结构的向量。它确实能捕捉到这个学生最近表现变好了这种时序趋势但无法显式表达这道题涉及的知识点和上一道题涉及的知识点是什么关系。两个知识点如果经常在同一张试卷出现、在教材目录里相邻、或者有前置依赖关系这种结构信息对预测极有价值但 LSTM 接收不到显式的图结构全部要靠数据里隐性的共现模式去拟合。数据量大还行数据量小——比如一个新知识点的题只有几十条作答记录——LSTM 基本就是瞎猜。还有一个实操问题DKT 的输入特征是题目粒度不是知识点粒度。一道题可能挂多个知识点DKT 一般用多热编码multi-hot表示这等于把知识点信息揉碎在一个固定维度的向量里知识点之间的交互完全被压扁了。GIKT 的思路就是把这个被压扁的结构重新立起来。2.2 GIKT 的核心思路先聚合邻居再建模交互GIKT 的出发点很直接既然知识点之间有先验关系比如从教材目录、考纲、专家标注得到那就把它当成一张图。图的节点是知识点边表示这两个知识点相关。然后跑两层 GCN让每个知识点的嵌入向量带上邻居的信息——学过一次函数的学生他的一次函数知识点表示会包含函数概念坐标系等邻居的知识。这个做法的收益有两层第一层缓解稀疏性。某个知识点做题记录少但它的邻居做题记录多那经过 GCN 聚合后它的表示也获得了足够的语义支撑。这在冷启动场景下效果明显。第二层预测更符合直觉。学生答错一道二次函数的题如果模型里二次函数节点的表示已经融合了一元二次方程的信息那下一次遇到一元二次方程的题时预测概率会自然下调。这个调整不是靠序列模型猜出来的而是图结构直接告诉模型的。具体到实现GIKT 通常分两阶段第一阶段用 GCN 对知识点嵌入做编码eg 得到知识点 k 的最终表示 e_k。这个表示既包含自身语义也包含图邻居的聚合信息。第二阶段针对学生历史上的每一次答题交互q_t, a_t计算一个新的交互表示。常见做法是把当前题目的知识点嵌入经过了 GCN和学生上一个时刻的掌握状态做某种融合再丢进循环网络GRU/LSTM更新状态。最后预测时用当前状态和下一道题的知识点嵌入做内积或 MLP输出正确概率。2.3 为什么用 GCN 而不是别的图模型这里有必要解释一下选型。做图嵌入的模型不少GCN、GAT、GraphSAGE、GIN 都能用。GIKT 的核心诉求是稳定聚合邻居信息不是找最重要的邻居——知识点之间的关联是相对确定的不需要复杂的注意力机制去动态筛选。GAT 那种带 attention 的结构在超大图上效果好但在这里反而容易过拟合训练样本学生答题记录通常只有几万到几十万而知识点图往往只有几十到几百个节点注意力权重学不出稳定的分布。GraphSAGE 的采样策略适合大规模图但知识追踪的知识点图规模小全图卷积计算代价可以忽略没必要引入采样。GCN 在这里的关键设定是邻接矩阵的归一化。常见做法是A_norm D^-0.5 * (A I) * D^-0.5加自环I是为了让每个节点在信息传递时保留自己的原始特征对称归一化D^-0.5是为了避免大度节点主导梯度。这个归一化公式基本算是 GCN 的标配直接用就好。3. 搭建 GIKT 的数据管道知识点图构建与序列化3.1 数据格式从原始答题日志到可训练序列先定义输入。知识追踪的原始数据一般是 CSV 或数据库表每行包含三个关键字段学生 ID、题目 ID或知识点 ID、答题结果0/1。如果题目绑定了多个知识点还需要一张题目-知识点的映射表。笔者习惯把原始数据先整理成两个结构化对象# data_loader.py import numpy as np import torch from torch.utils.data import Dataset class KTDataset(Dataset): def __init__(self, seq_len, num_q, num_k): self.seq_len seq_len # 每个序列的最大长度 self.num_q num_q # 题目总数 self.num_k num_k # 知识点总数 self.samples [] # 每个样本是 (q_seq, k_seq, r_seq) def load_from_log(self, student_logs): # student_logs: dict, keystudent_id, valuelist of (question_id, knowledge_id, correct) for sid, records in student_logs.items(): if len(records) 2: continue for i in range(0, len(records) - 1): q_seq [r[0] for r in records[max(0, i - self.seq_len 1): i 1]] k_seq [r[1] for r in records[max(0, i - self.seq_len 1): i 1]] # 结果序列与题目序列错一位r_seq[t] 对应 q_seq[t] 的答题结果 r_seq [r[2] for r in records[max(0, i - self.seq_len 1): i 1]] target_q records[i 1][0] target_k records[i 1][1] target_r records[i 1][2] self.samples.append((q_seq, k_seq, r_seq, target_q, target_k, target_r))这段代码做了三件事把每个人的答题记录滑动窗口切成固定长度的子序列保持题目、知识点、结果三者对齐单独留出下一道题作为预测目标。参数说明seq_len一般取 50200。太短学不到长时间依赖太长 LSTM 容易梯度消失且训练变慢。公开数据集上常用 50。切分时用的是滑窗 单步预测模式即每个位置都生成一条样本。这样数据量最大但样本之间高度重叠训练时要小心过拟合后面会讲。3.2 知识点关系图的构建三种来源与邻接矩阵知识点图是 GIKT 最重要的先验输入。构建方式按可靠性递减排列专家标注最可靠。教材目录、考纲中的章节关系人工抽取出前置依赖同类概念两类边。比如一元二次方程的判别式依赖一元二次方程的定义。题目共现统计。两道题如果经常出现在同一张试卷或同一次测验里认为它们涉及的知识点相关。计算知识点两两的共现频次超过阈值就建边。响应共现数据驱动。同一批学生在两道题上的作答情况高度相关比如皮尔逊相关系数超过 0.3则对应的知识点建边。实践中建议混合前两种先用专家标注保证图的质量再用共现统计补全缺失边。以下是邻接矩阵的构建代码# build_graph.py import torch import numpy as np def build_adjacency(num_k, edge_pairs, self_loopTrue): edge_pairs: list of (knowledge_i, knowledge_j) 返回对称归一化后的邻接矩阵可直接用于 GCN adj np.zeros((num_k, num_k), dtypenp.float32) for i, j in edge_pairs: if i ! j: adj[i, j] 1.0 adj[j, i] 1.0 if self_loop: adj np.eye(num_k, dtypenp.float32) # 对称归一化: D^(-1/2) * A * D^(-1/2) degree adj.sum(axis1) d_inv_sqrt np.power(degree, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] 0.0 d_mat np.diag(d_inv_sqrt) adj_norm d_mat adj d_mat return torch.from_numpy(adj_norm).float() # 示例num_k5边为 (1,2), (2,4), (0,3) adj build_adjacency(5, [(1, 2), (2, 4), (0, 3)]) print(adj)两点说明。self_loopTrue是必须的否则节点在第一层 GCN 卷积后会丢失自己的原始特征。归一化必须用对称版本行归一化D^-1 * A会让大度节点信息被稀释得面目全非。注意边界情况当知识点图里存在孤立节点没有任何边时上述代码经过归一化后该节点的特征会全部变为 0。解决方式是给孤立节点至少加一条自环——但代码里self_loop已经加了所以孤立节点仍然能保留自身特征只是得不到邻居信息这不算错误属于可以接受的降级表现。3.3 题目与知识点的映射一道题挂了多个知识点怎么办数据集中一道题可能同时考查多个知识点。GIKT 的处理方式题目嵌入用多热向量的方式聚合多个知识点嵌入。## 4. 核心模型实现GCN 编码器与交互聚合 ### 4.1 GCN 编码器把知识点嵌入变成图感知表示 这一层是整个 GIKT 的基石。实现上就是标准的两层图卷积输入是所有知识点的初始嵌入矩阵 E0形状是 [num_k, embed_dim]经过两层卷积后得到 E_final。 python import torch import torch.nn as nn import torch.nn.functional as F class GCNEncoder(nn.Module): def __init__(self, num_k, embed_dim, hidden_dim, adj): super().__init__() self.num_k num_k self.embed_dim embed_dim self.adj adj # 归一化后的邻接矩阵 [num_k, num_k] # 初始化知识点嵌入这里是可学习的参数 self.k_embed nn.Embedding(num_k, embed_dim) # 两层 GCN 的线性变换 self.w1 nn.Linear(embed_dim, hidden_dim, biasFalse) self.w2 nn.Linear(hidden_dim, embed_dim, biasFalse) def forward(self): # h0: [num_k, embed_dim] h0 self.k_embed.weight # 第一层卷积邻接矩阵乘特征再做线性变换 h1 torch.mm(self.adj, h0) # [num_k, embed_dim] 聚合邻居 h1 self.w1(h1) # 线性变换到 hidden_dim h1 F.relu(h1) # 激活 # 第二层卷积 h2 torch.mm(self.adj, h1) # 再聚合一次 h2 self.w2(h2) # 变换回 embed_dim return h2 # [num_k, embed_dim]这段代码的关键参数embed_dim一般取 64128。太小表示能力不足太大学得慢且容易过拟合。hidden_dim通常取 embed_dim 的 2 倍即 128256。两层的理由是一层只能聚合直接邻居两层能聚合到二阶邻居。理论上三层效果更好但知识点的图一般很稀疏两层足够三层反而引入噪声。w1和w2都设了biasFalse这是因为加了 bias 会破坏 GCN 的等变性——虽然对最终效果影响不大但保持标准实现可以避免奇怪的调试问题。4.2 交互编码把学生 题目 结果压成一个向量拿到知识点的图感知表示后下一步是把每次答题交互编码成一个向量。这是知识追踪模型的核心区别所在。GIKT 的典型做法是用题目涉及的知识点嵌入乘以答题结果得到交互嵌入。## 5. 训练流程与参数设定 ### 5.1 训练循环最小可跑版 GIKT python import torch import torch.nn as nn import torch.optim as optim from model import GIKT from data_loader import KTDataset def train(model, dataloader, epochs30, lr0.001): optimizer optim.Adam(model.parameters(), lrlr) loss_fn nn.BCEWithLogitsLoss() for epoch in range(epochs): total_loss 0.0 for batch in dataloader: q_seq, k_seq, r_seq, target_q, target_k, target_r batch logits model(q_seq, k_seq, r_seq, target_q, target_k) loss loss_fn(logits, target_r.float()) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() avg_loss total_loss / len(dataloader) print(fEpoch {epoch1}, Loss: {avg_loss:.4f}) # 参数说明 # - max_norm5.0 是 LSTM 训练的标准配置防止梯度爆炸。 # - BCEWithLogitsLoss 内部自带 sigmoid不要在模型末尾手动加 sigmoid。超参数这一块给出常用配置学习率 0.001 配 Adam 是绝大多数知识追踪模型的默认组合batch_size64128 都行太大容易导致收敛慢seq_len取 50 或 100需要根据平均答题序列长度调整。5.2 评估指标AUC 为主ACC 为辅知识追踪的标准评估指标是 AUCROC 曲线下面积。原因很简单正负样本答对/答错往往不平衡ACC 在正确率 70% 以上的数据集上区分度太低。AUC 对排序质量敏感更适合衡量模型是否把该答对的排在前面。from sklearn.metrics import roc_auc_score, accuracy_score def evaluate(model, dataloader): model.eval() preds [] labels [] with torch.no_grad(): for batch in dataloader: q_seq, k_seq, r_seq, target_q, target_k, target_r batch logits model(q_seq, k_seq, r_seq, target_q, target_k) probs torch.sigmoid(logits) preds.extend(probs.cpu().numpy()) labels.extend(target_r.cpu().numpy()) auc roc_auc_score(labels, preds) acc accuracy_score(labels, (np.array(preds) 0.5).astype(int)) return auc, acc5.3 关键超参数的影响embed_dim、图结构、dropoutembed_dim 对效果的影响在 64 到 128 之间最敏感。公开实验里64 维到 128 维能涨 12 个点 AUC再往上收益衰减明显但参数量翻倍训练时间拉长。图结构的影响更大。如果知识点图构建质量差比如边连错了GIKT 会比 DKT 还差——因为它把错误的结构信息强行塞进了表示里。这是先验知识用对了是提分用错了是灾难的典型场景。dropout 在知识追踪里有讲究。GCN 里的 dropout 通常设在 0.20.5位置有两个GCN 卷积后的特征上以及交互嵌入输入到循环网络之前。后者更重要因为它直接影响模型对训练样本的拟合程度。项目中若是训练集 AUC 能到 0.95 以上但验证集只有 0.7大概率是交互嵌入这部分过拟合了。6. GIKT 避坑笔记5 条实战中反复踩过的坑6.1 知识点图构建失真共现统计建出虚假边现象在图里加入了题目共现边之后模型 AUC 反而下降。原因题目共现不等于知识点相关。同一张卷子出现可能只是因为考试章节安排不代表有概念上的依赖关系。用这个边做 GCN 聚合反而把不相关的知识点表示互相污染了。解决只保留强共现边——设阈值比如共现次数低于总题量 5% 的边全部丢弃。更稳妥的做法是只用专家标注的依赖关系共现边只在一阶邻居基础上补。6.2 数据泄漏切分序列时把未来信息带进了历史现象训练集 AUC 极高超过 0.98但测试集完全不行。原因滑窗切分时相邻样本高度重叠如果测试集和训练集按学生 ID 随机切分同一个学生的答题片段会同时出现在训练和测试里。模型等于见过近乎一样的序列这不是预测是记忆。解决按学生切分数据。所有样本按 student_id 分成训练/验证/测试三组保证同一个学生的所有答题记录只出现在一组里。6.3 邻居聚合过度平滑GCN 层数越多效果越差现象两层 GCN 效果好加到四层之后 AUC 明显下跌且训练损失很难下降。原因知识追踪的知识点图规模小、边稀疏深层 GCN 反复聚合导致所有节点表示趋同——这就是过平滑现象。此时节点自身的区分性彻底丢失。解决GCN 最多两层。如果确需更大的感受野可以把邻接矩阵做幂运算模拟多跳聚合而不是真的堆叠卷积层。6.4 LSTM 训练不收敛梯度爆炸与学习率选择的拉锯现象loss 在训练初期剧烈震荡甚至出现 NaN。原因LSTM 在长序列上容易梯度爆炸加上 BCE loss 对 logits 的梯度形态学习率稍大就会炸。解决梯度裁剪是保底手段max_norm5.0基本够用更稳妥的是把学习率降到 0.00050.001。不要用 SGDAdam 在此类模型上是默认选择。6.5 孤立知识点拖后腿图里没边的节点预测全错现象某几个知识点的题目预测准确率接近随机猜测。原因孤立节点没有邻居可以聚合它的 GCN 输出等同于自身原始嵌入等于退化成了没有图信息的 DKT。如果这个知识点答题样本又少那基本就是瞎猜。解决领域中如果有知识点的依赖关系但图没画全优先补边实在补不了可以考虑将孤立节点连接到虚拟全局节点让它们至少能获取所有节点的全局平均信号。提示以上五条是 GIKT 落地最常遇到的坑建议在模型开发阶段就把 6.2学生级切分和 6.5孤立节点两条纳入基线检查能省掉大量返工时间。7. 最终章消融实验与落地的验证技巧7.1 消融实验证明 GCN 真的在起作用做应用不是发论文但消融实验至少能告诉你投入是否值得。最少做三组第一组去掉 GCN直接把知识点嵌入作为可学习参数即退化成 DKT 的嵌入方式。第二组保留 GCN但把所有边权重设为相同值不看图结构只看有无边。第三组完整 GIKT。对比三组在验证集上的 AUC完整的 GIKT 应该比第一组高 13 个点——如果高不出来说明数据规模太小或图质量太差需要回头检查图构建。这里的血泪经验是不要因为模型最终结果还行就跳过这一步否则你永远不知道自己的收益是来自 GCN 还是来自其他改动。7.2 场景迁移一个最少改动的适配清单GIKT 的应用不止于预测下一题正误。常见迁移方向试题推荐用模型输出概率排序优选答对概率适中的题推荐给学生避免过难或过易。知识点诊断把学生的循环状态向量做聚类或降维观察薄弱知识点簇输出诊断报告。注意这个用途必须在训练结束后单独做分析不要期待模型直接给你标签。课程路径规划基于知识点图的 GCN 表示做路径依赖判断——模型告诉了你哪些知识点状态变化会影响其他知识点规划器可以利用这个关系安排学习顺序。7.3 最后一招热启动带来的收益如果生产环境已有 DKT 或 BKT 在跑不要推倒重来。把 DKT 训练好的循环网络参数直接初始化给 GIKT只重训 GCN 部分和输出层通常 10 个 epoch 以内就能收敛。这个热启动技巧在实际项目中会省至少三分之一的调参时间也是我把 GIKT 从论文搬到生产环境的常用路径——先证明能跑、有效再谈优化和替换。希望这篇笔记能帮你少走几条弯路开着调试器把每条问答日志都看懂再谈模型升级这个习惯我一直保留希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询