尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
GIKT知识追踪实战:图卷积网络与LSTM融合的题目-技能二部图建模
简介面向在线教育平台知识追踪任务的研究者与工程技术人员这份资源围绕基于图卷积网络的GIKT模型展开用于缓解题目-技能数据稀疏与多技能关联带来的预测难题提升对学生新题目掌握程度的预测准确性。资源包共1个PDF文件大小约412KB内容为GIKT论文原文系统阐述嵌入层、LSTM层、历史回顾模块与广义交互模块的整体设计并给出在三个基准数据集上AUC至少提升1%的实验结果。读者可从中获取高阶题目-技能关系图的GCN嵌入传播思路、长期行为依赖的LSTM建模方式以及学生当前状态、历史相关练习、目标题目与相关技能之间的一致性交互建模方法适合作为知识追踪方向复现与改进的参考材料。目前已有211人学习下载。1. 从题目-技能二部图切入GIKT 到底解决了知识追踪里的哪个死结在线教育平台里学生做题记录看着挺多但真正落到单道题目上数据稀疏得吓人。一个学生可能只做过某道题一次甚至没做过可系统还得预测他下一题能不能答对。传统知识追踪模型大多盯着“技能”这个粒度做文章比如 BKT、DKT、DKVMN它们把题目映射到技能再根据技能掌握度推断答题表现。问题在于一道题可能对应多个技能一个技能又关联大量题目这种高阶、多对多的关系用简单的技能标签根本表达不全。更麻烦的是题目本身的特征被丢掉了两个共享同一技能的题目难度不同答对概率可能差很远。GIKT 这篇工作就是冲着这个死结来的用图卷积网络把题目-技能二部图里的高阶关联信息抽出来再配合 LSTM 和注意力机制把学生历史作答、当前状态、目标题目、相关技能放在一个框架里做交互。它适合谁做在线教育平台算法落地的人、研究知识追踪想找新 baseline 的人以及需要处理多技能题目和数据稀疏场景的工程师。你拿到这份资源核心不是跑一个 demo而是理解它怎么把图结构信息塞进序列模型以及在自己的数据集上怎么复现出那个 AUC 提升。2. 拆开 GIKT 的四个模块从嵌入传播到广义交互的工程实现2.1 嵌入层与图卷积传播题目和技能的向量怎么来的GIKT 的输入是三元组序列题目 ID、技能 ID 集合、答题结果。嵌入层维护三个矩阵题目嵌入矩阵 (E_q \in \mathbb{R}^{|Q| \times d})、技能嵌入矩阵 (E_s \in \mathbb{R}^{|S| \times d})、答案嵌入矩阵 (E_a \in \mathbb{R}^{2 \times d})。这里的 (d) 是嵌入维度常见取值 64、128、256论文里用的 128。答案嵌入只有两行分别对应答错和答对。这些嵌入不预训练跟着最终损失端到端学。关键在嵌入传播。题目-技能关系被建模成二部图 (G {(q, r_{qs}, s) | q \in Q, s \in S})(r_{qs}1) 表示题目 (q) 关联技能 (s)。GCN 在这一层做邻居聚合让题目嵌入吸收关联技能的信息技能嵌入也吸收关联题目的信息。公式上第 (l1) 层的节点表示由自身和邻居的线性变换加激活得到。实际实现时邻接矩阵按行归一化避免高度数节点主导更新。import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear nn.Linear(in_dim, out_dim) self.act nn.ReLU() def forward(self, x, adj_norm): # x: (N, in_dim) 节点特征 # adj_norm: (N, N) 归一化邻接矩阵含自环 support self.linear(x) out torch.matmul(adj_norm, support) return self.act(out) # 构建题目-技能二部图邻接矩阵的常见做法 def build_adj(question_skill_pairs, num_q, num_s): # question_skill_pairs: list of (q_idx, s_idx) n num_q num_s adj torch.zeros(n, n) for q, s in question_skill_pairs: adj[q, num_q s] 1.0 adj[num_q s, q] 1.0 adj torch.eye(n) # 自环 deg adj.sum(dim1) deg_inv_sqrt torch.pow(deg, -0.5) deg_inv_sqrt[torch.isinf(deg_inv_sqrt)] 0.0 d_mat torch.diag(deg_inv_sqrt) return torch.matmul(torch.matmul(d_mat, adj), d_mat)这段代码里build_adj把题目和技能拼成一个大矩阵题目索引在前技能索引在后。adj_norm是对称归一化后的结果GCN 层直接拿它做消息传递。参数上in_dim和out_dim通常设成一样比如 128堆两层就够了。层数多了会过平滑题目和技能嵌入趋同反而掉点。我一般会先跑一层 GCN 看 baseline再试两层超过两层基本不用看。2.2 LSTM 序列建模与 recap 模块长期依赖怎么抓学生做题序列送进 LSTM得到每个时间步的隐藏状态 (h_t)。LSTM 的输入是当前题目的嵌入加上对应答案嵌入常见拼接方式为 ([q_t; a_t])。隐藏层维度跟嵌入维度对齐比如 128。这里有个工程细节序列长度不固定padding 后的 batch 里要用pack_padded_sequence处理否则 LSTM 会把 padding 也当有效步影响隐藏状态。recap 模块是 GIKT 区别于 SKVMN、EERNNA 的地方。它不直接把所有历史隐藏状态加权求和而是先做一次选择。具体来说用当前目标题目的嵌入 (q_t) 跟历史每个时间步的题目嵌入算注意力分数选出 top-k 个最相关的历史练习。论文里叫 soft selection 和 hard selection 两种实现。soft 是带权聚合hard 是直接取 top-k 再聚合。我一般先用 soft因为可导训练稳定hard 在推理阶段可以加速但训练时得用 Gumbel softmax 之类的技巧不然梯度断掉。class RecapModule(nn.Module): def __init__(self, hidden_dim, top_k5): super().__init__() self.top_k top_k self.attn nn.Linear(hidden_dim * 2, 1) def forward(self, h_seq, q_emb, mask): # h_seq: (B, T, H) LSTM 隐藏状态 # q_emb: (B, H) 当前题目嵌入 # mask: (B, T) 有效步为 1padding 为 0 B, T, H h_seq.shape q_expand q_emb.unsqueeze(1).expand(-1, T, -1) concat torch.cat([h_seq, q_expand], dim-1) scores self.attn(concat).squeeze(-1) # (B, T) scores scores.masked_fill(mask 0, -1e9) weights F.softmax(scores, dim-1) # soft selection: 加权求和 recap_vec torch.bmm(weights.unsqueeze(1), h_seq).squeeze(1) return recap_vec, weightstop_k这个参数在 hard selection 里才真正用上soft 版本里所有有效历史都参与只是权重不同。注意力打分函数用一层线性加 tanh 也行论文里是直接线性。mask 那步不能省不然 padding 位置的分数会干扰 softmax。recap 出来的向量跟当前隐藏状态 (h_t) 拼接送进交互模块。2.3 广义交互模块与预测层多路 pairwise 交互怎么拼交互模块做的是 pairwise 交互学生当前状态 (h_t)、recap 选出的历史聚合向量、目标题目嵌入 (q_t)、相关技能嵌入 (s_t)这几路两两做交互。常见做法是元素级相乘再拼接或者用双线性池化。GIKT 论文里用的是 generalized interaction把 (h_t) 跟 (q_t)、(h_t) 跟 (s_t)、recap 向量跟 (q_t) 等组合分别算交互向量再拼起来过一层 MLP 得到最终预测概率。class InteractionModule(nn.Module): def __init__(self, hidden_dim): super().__init__() self.fc nn.Sequential( nn.Linear(hidden_dim * 4, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1) ) def forward(self, h_t, recap_vec, q_emb, s_emb): # 元素级交互 inter1 h_t * q_emb inter2 h_t * s_emb inter3 recap_vec * q_emb inter4 recap_vec * s_emb concat torch.cat([inter1, inter2, inter3, inter4], dim-1) logit self.fc(concat) return torch.sigmoid(logit)这里hidden_dim * 4是因为四路交互各占一个 hidden_dim。技能嵌入 (s_t) 如果是多技能通常取平均或者注意力加权。预测层输出一个标量用 BCE 损失训练。整个模型端到端优化器用 Adam学习率 1e-3 起步batch size 64 或 128。AUC 是主要评估指标论文里在三个数据集上至少涨了 1 个绝对点。3. 在自己的数据上跑通 GIKT数据准备、训练循环与参数调优3.1 数据格式与图构建从原始日志到二部图你拿到的原始数据通常长这样学生 ID、题目 ID、技能 ID 列表、答题结果、时间戳。第一步是转成序列样本。每个学生按时间排序切成长度固定的序列比如 200 步不足补零。题目和技能做 ID 重映射从 0 开始连续编号。题目-技能关系从全量数据里抽去重后得到二部图的边列表。import pandas as pd import numpy as np def build_sequences(df, max_len200): # df 列: student_id, question_id, skill_ids, correct, timestamp df df.sort_values([student_id, timestamp]) sequences [] for sid, group in df.groupby(student_id): q_seq group[question_id].tolist() s_seq group[skill_ids].tolist() a_seq group[correct].tolist() # 截断或补齐 if len(q_seq) max_len: q_seq, s_seq, a_seq q_seq[-max_len:], s_seq[-max_len:], a_seq[-max_len:] pad max_len - len(q_seq) q_seq [0] * pad s_seq [[0]] * pad a_seq [0] * pad sequences.append((q_seq, s_seq, a_seq)) return sequences def build_question_skill_graph(df): pairs set() for _, row in df.iterrows(): for s in row[skill_ids]: pairs.add((row[question_id], s)) return list(pairs)max_len设 200 是常见起点太长显存吃紧太短截掉早期历史。题目 ID 和技能 ID 重映射后0 通常留给 padding所以真实 ID 从 1 开始。图构建时题目-技能对去重一个题目关联多个技能就加多条边。如果技能列表是字符串先 split 再展开。3.2 训练循环与损失函数AUC 怎么算才不翻车训练循环里每个 batch 送进模型输出预测概率跟真实标签算 BCE。AUC 计算要注意只在有效步上算padding 位置不能混进去。常见做法是收集所有有效步的预测和标签拼成一个长向量再算 AUC。from sklearn.metrics import roc_auc_score def train_epoch(model, loader, optimizer, device): model.train() all_preds, all_labels [], [] for batch in loader: q, s, a, mask [x.to(device) for x in batch] preds model(q, s, a, mask) # (B, T) loss F.binary_cross_entropy(preds[mask 1], a[mask 1].float()) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() all_preds.append(preds[mask 1].detach().cpu()) all_labels.append(a[mask 1].detach().cpu()) preds torch.cat(all_preds).numpy() labels torch.cat(all_labels).numpy() return roc_auc_score(labels, preds)梯度裁剪设 5.0 是防 LSTM 梯度爆炸的常规操作。学习率用 1e-3每 10 个 epoch 乘 0.5。如果 AUC 在验证集上连续 5 个 epoch 不涨就早停。注意preds[mask 1]这步mask 是 bool 张量直接索引会把有效步拉平成一维跟标签对齐。3.3 关键参数与消融哪些开关真正影响 AUC参数常见取值影响嵌入维度 d64 / 128 / 256太小欠拟合太大过拟合128 最稳GCN 层数1 / 22 层通常比 1 层好3 层过平滑LSTM 隐藏维度128 / 256跟嵌入维度对齐即可recap top-k5 / 10hard selection 时影响大soft 时影响小dropout0.2 / 0.3加在嵌入层和 LSTM 输出上学习率1e-3配合衰减策略消融实验里去掉 GCN 掉点最明显说明高阶题目-技能关系确实是核心。去掉 recap 模块次之说明历史选择对长期依赖有帮助。交互模块换成简单拼接也会掉但幅度小一些。这些结论在你自己的数据上不一定完全一致但方向可以参考。4. 避坑与排查GIKT 复现路上最容易翻车的五个点4.1 现象AUC 一直在 0.5 附近晃模型完全不收敛原因最常见的是标签对齐错了。序列里 padding 位置的标签是 0如果损失函数没加 mask模型会学成全部预测 0AUC 自然 0.5。另一个可能是题目 ID 重映射时padding 的 0 跟真实题目 ID 冲突嵌入查表查到了错误向量。解决检查损失函数里有没有preds[mask 1]和a[mask 1]。ID 重映射时真实 ID 从 1 开始0 专门留给 padding。嵌入矩阵大小设成num_questions 1多出来的那一行给 padding。4.2 现象训练 loss 正常下降但验证集 AUC 比训练集低一大截原因过拟合。GIKT 参数量不小尤其是题目嵌入矩阵如果题目数上万嵌入层就占很大显存和参数量。学生序列短、数据量少的时候模型容易记住训练集里的题目-答案对。解决加 dropout嵌入层和 LSTM 输出都加比例 0.2 到 0.3。加 weight decay1e-5 到 1e-4。如果还不行减小嵌入维度从 128 降到 64。早停策略也要有验证集 AUC 不涨就停。4.3 现象GCN 那层输出全是 NaN原因邻接矩阵归一化时出现了度数为 0 的孤立节点。题目-技能图里如果某个题目没有任何技能关联或者某个技能没有任何题目关联它的度就是 0deg_inv_sqrt算出 inf。解决归一化前给邻接矩阵加自环保证每个节点至少度数为 1。代码里adj torch.eye(n)就是干这个的。如果加了自环还有 NaN检查deg_inv_sqrt里有没有 0 除用torch.where(deg 0, deg_inv_sqrt, torch.zeros_like(deg_inv_sqrt))兜底。4.4 现象recap 模块的注意力权重全集中在一个历史步上原因注意力打分函数的初始化不好或者学习率太大导致 softmax 饱和。另一个可能是历史序列里有效步太少模型没得选。解决注意力线性层用 Xavier 初始化学习率调小到 5e-4。如果序列普遍很短recap 的 top-k 设小一点比如 3。还可以在注意力分数上加温度系数训练初期温度高一点让权重分散后期再降。4.5 现象多技能题目的技能嵌入怎么聚合都不对原因一道题关联多个技能时直接平均会丢失技能重要性差异。有的技能是主要考点有的是辅助考点平均之后主次不分。解决用注意力加权聚合技能嵌入。以题目嵌入为 query技能嵌入为 key 和 value算注意力权重再加权求和。这样主要技能权重大辅助技能权重小。如果技能数固定且不多也可以拼接后过一层线性降维。5. 进阶技巧把 GIKT 的图结构换成动态图以及 AUC 之外看什么GIKT 用的是静态题目-技能二部图图结构在训练前就定好了。但真实场景里题目和技能的关系可能随时间变化新题目不断加入旧题目的技能标注也可能修正。一个自然的进阶方向是把静态图换成动态图每个时间窗口重建一次邻接矩阵或者用时序图神经网络。常见做法是滑动窗口窗口内重新统计题目-技能共现更新边权重。边权重可以用共现频率或者 PMI 来算比 0/1 边信息量大。我试过在 OULAD 数据集上做窗口为 30 天的动态图AUC 比静态图涨了 0.8 个点但训练时间翻倍得权衡。另一个进阶点是评估指标。AUC 只看排序不看校准。知识追踪场景里预测概率的绝对值也有意义比如用来决定推不推题。这时候要看 Brier score 或者 ECEExpected Calibration Error。GIKT 输出经过 sigmoid概率值本身有校准空间。如果发现 AUC 高但 Brier score 差可以在验证集上做 Platt scaling 或者 isotonic regression把概率校准一下再用。from sklearn.calibration import CalibratedClassifierCV from sklearn.isotonic import IsotonicRegression # 假设 val_preds 是验证集预测概率val_labels 是真实标签 iso_reg IsotonicRegression(out_of_boundsclip) iso_reg.fit(val_preds, val_labels) calibrated_preds iso_reg.predict(test_preds)这段校准代码很轻量但能明显改善概率的可靠性。注意校准集和测试集要分开别拿测试集自己校准自己。还有一个工程上的习惯每次改模型结构或者调参固定随机种子跑至少三次取平均 AUC。GIKT 这种带注意力机制的模型初始化敏感单次结果波动可能有 0.5 个点。我一般会跑 5 个种子看均值和方差方差大的配置直接弃掉。从那以后我每次复现知识追踪模型都强制走一遍「固定种子 多次平均 校准检查」的流程省得被单次高 AUC 骗了。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

大模型+智慧河长:从巡河识别到知识问答的落地实践

大模型+智慧河长:从巡河识别到知识问答的落地实践

简介:这份PPT资料面向水利信息化从业者、智慧水务方案设计人员及河长制管理平台开发者,围绕大模型与智慧河长理念的结合,系统梳理河流管理从现状痛点到落地实施的完整思路。内容涵盖大模型技术原理与应用场景、智慧河长系统分层架构设计、水质…

📅 2026/10/5 3:18:43
深信服SIP V3.0.53安全感知平台部署联动与风险处置全攻略

深信服SIP V3.0.53安全感知平台部署联动与风险处置全攻略

简介:针对深信服安全感知平台SIP 3.0.53版本的官方用户手册,面向网络设计工程师、系统运维人员等需要部署、调试和维护SIP安全感知平台的读者,帮助其快速掌握产品在实际环境中的软硬件要求、安装配置流程与日常运维方法。文档不仅涵盖SIP的产…

📅 2026/10/5 3:18:43
MySQL 8.0 Windows 安装全指南:从下载配置到排错实战

MySQL 8.0 Windows 安装全指南:从下载配置到排错实战

很多人卡在 MySQL 安装这一步,不是下载慢就是装到一半报错,要么就是装完连不上、启动不了服务。我这些年帮同事、朋友处理过各种奇奇怪怪的 MySQL 安装问题,从 5.7 到 8.0 都遇到过。这篇文章就把 MySQL 8.0 在 Windows 系统上的完整安装过程…

📅 2026/10/5 3:18:43
MORE NEWS

更多资讯

📰

Day7时间记录复盘:拆解三个时间块,让习惯真正落地

看到这份Day7记录——10:00-10:30,10:30-13:00,14:40-22:00——我第一反应是:这份记录是真的。因为假的时间表永远从6:00开始,永远顶着正点,永远密不透风;而真人的时间记录,起步晚、冒号一会儿半…

📰

基于深度学习的复杂稀疏数据预测系统:从掩码嵌入到自监督预训练

简介:这份资源是面向深度学习方向的毕业设计、课程设计与期末大作业场景的完整项目包,聚焦复杂稀疏数据下的预测建模问题。项目以卷积神经网络与循环神经网络为基础架构,并引入YOLO目标检测算法提取关键特征,覆盖数据预处理、模型…

📰

YOLOv8快递包裹缺陷检测权重包:开箱即用与训练验证

简介:本资源面向快递物流质检、仓储自动化及计算机视觉方向的开发者与研究者,提供一套可直接推理的YOLOv8快递包裹与包装盒缺陷检测权重,并配套1200余张标注数据集,用于快速验证与二次训练。压缩包共2000个文件,以982个…

📰

一键搞定Zotero参考文献超链接格式:Word宏批量变黑去下划线

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

📰

Android车机用USB Gadget配置CarPlay的完整指南

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

📰

C++模块化设计实战:切依赖而非切文件,从接口到信息隐藏

写了六年多的 C,面试过不少候选人,也重构过好几个“改一个功能要加班三小时”的老项目,我越来越觉得 C模块化设计 这四个字被严重低估了。很多人一提模块化就想到“把代码拆成多个文件”,似乎文件拆得越多就越模块化——实际不是这…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬