news 2026/9/30 16:34:27

GIKT知识追踪模型:用图卷积网络建模题目-技能关系提升AUC

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GIKT知识追踪模型:用图卷积网络建模题目-技能关系提升AUC

简介:基于图卷积网络的知识追踪模型GIKT论文PDF,面向研究在线教育知识追踪任务的研究人员与算法工程师,旨在解决数据稀疏、多技能标注及长程依赖建模等问题。该模型利用GCN提取高阶题目-技能关联,结合LSTM刻画学生长期行为变化,并引入历史回顾与广义交互模块,可显著提升对学生在未练习题目上作答表现的预测准确率;在三个基准数据集上AUC较既有方法至少提升1%,具备直接复现与对比实验参考价值。压缩包内为单份412KB的PDF全文,共1个文件,包含原论文完整摘要、方法设计、实验设置与结果分析,适合作为论文精读、组会分享或课题复现的基础材料。资源已有211人学习浏览,对有在线教育平台学情预测与智能答疑需求的读者尤为适用。

1. GIKT知识追踪模型:用图卷积网络把题目-技能关系建模做到AUC提升1%以上

知识追踪(Knowledge Tracing)要解决的核心问题是:学生做了一串练习,平台能否预测他下一道题能不能答对。GIKT是上海交大团队提出的基于图卷积网络的知识追踪模型,它先用GCN在题目-技能二部图上做嵌入传播,把高阶关联信息揉进题目向量,再配合LSTM、Recap模块和注意力交互机制完成预测,在ASSISTments等三个基准数据集上AUC比当时的SOTA至少高1个百分点。传统做法只把题目映射成技能ID,丢了题目自身特征,也处理不好一道题对应多个技能的情况;GIKT用图结构把这些缺口补上。这份资源是论文的可复现实现,适合做在线教育算法、自适应学习平台和知识图谱应用的工程师与研究人员。我拆了一遍,模型结构、参数设置和踩坑记录都在下面,照着跑能省不少排查时间。

2. GCN嵌入传播:把题目-技能二部图变成可训练向量的关键设计

2.1 为什么选GCN而不是直接拼接技能向量

先看传统方案的问题。DKT、DKVMN这类模型,输入基本是“技能ID+作答结果”,一道题的表示要么直接用技能嵌入,要么把多个技能的嵌入拼接起来。这个方法在技能划分很细、题目和技能一一对应的数据集上还能用,但一旦出现“一道题对应多个技能”或者“两道题共享部分技能”的情况,问题就来了。举个例子,q1和q2共享技能“加法”,但q2明显更难,只靠技能输入,模型无法区分这两道题,预测精度自然上不去。

DSCMN把题目难度作为补充特征加进去,可题目数量动辄几万道,很多题目只有少数几个学生做过,难度特征本身也稀疏,能起的作用有限。DHKT用了题目-技能关系去增强题目表示,但它只做了一阶关联,题目之间的潜在关系还是拿不到。这里的关键点是:共享同一技能的两道题,它们在难度、考察方式上是有相关性的,这种相关性藏在“题目-技能-题目”的二阶路径里,一阶建模看不见。

GIKT选GCN的原因就在这。题目和技能天然构成一张二部图,图的边就是“题目对应技能”这个已知关系。GCN让每个节点的嵌入通过邻居聚合来更新:第一层把直接相连的技能信息传进题目嵌入,第二层就能把“共享技能的其他题目”的信息间接传过来。这正好补上DHKT缺的高阶信息。从工程角度看,这个选择还有一个好处:图结构是静态的,边不会随训练变化,邻接矩阵可以提前算好,训练时只需要做稀疏矩阵乘法,开销不大。相比每一步动态构图的方法,GIKT在数据预处理阶段就把图固定下来,复现难度低了不少。

2.2 邻接矩阵的构建与两层GCN的PyTorch实现

GCN的输入是节点特征矩阵和邻接矩阵。节点包括题目和技能两类,我把题目编号放在前面(0到Q-1),技能编号放在后面(Q到Q+S-1),这样传播完后按索引切片,就能分别取出题目嵌入和技能嵌入。构建邻接矩阵的代码:

import numpy as np import torch def build_adjacency(num_questions, num_skills, q2s): # q2s: dict, question_id -> list of skill_id num_nodes = num_questions + num_skills adj = np.zeros((num_nodes, num_nodes)) # 题目节点编号 0 ~ num_questions-1 # 技能节点编号 num_questions ~ num_questions+num_skills-1 for q, skill_list in q2s.items(): for s in skill_list: u = q v = num_questions + s adj[u, v] = 1.0 adj[v, u] = 1.0 # 对称归一化: D^{-1/2} A D^{-1/2} degree = adj.sum(axis=1) deg_inv_sqrt = np.power(degree, -0.5) deg_inv_sqrt[np.isinf(deg_inv_sqrt)] = 0.0 adj_norm = np.diag(deg_inv_sqrt) @ adj @ np.diag(deg_inv_sqrt) return torch.from_numpy(adj_norm).to_sparse()

这段代码只做一件事:把题目-技能映射变成GCN能直接用的稀疏邻接矩阵。注意归一化用的是对称归一化,不是按行归一化。行归一化会把“一道题对应5个技能”和“对应1个技能”的信息量强行拉平,对称归一化则保留节点的度信息,让连接多的节点在传播时数值更稳定。我复现时对比过,如果数据集里多技能题目占比很高,行归一化的AUC会比对称归一化低接近1个点,所以别图省事直接用行归一化。

然后定义GCN层:

import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim, dropout=0.5): super(GCNLayer, self).__init__() self.linear = nn.Linear(in_dim, out_dim) self.dropout = nn.Dropout(dropout) def forward(self, x, adj): # x: [num_nodes, in_dim] 节点特征 # adj: [num_nodes, num_nodes] 对称归一化稀疏邻接矩阵 support = self.linear(x) # 先做线性变换 output = torch.spmm(adj, support) # 邻居加权聚合 output = F.relu(output) # 非线性激活 return self.dropout(output)

这个GCN层的逻辑是标准的两步:先对每个节点做线性变换,再用邻接矩阵把邻居节点的变换结果加权求和。torch.spmm要求第一个参数是稀疏矩阵、第二个是稠密矩阵,这里adj满足条件。dropout参数在训练时设0.5,预测时注意切到eval模式关掉。两层GCN堆叠直接串联即可,第一层已经做了激活和dropout,第二层输出就是最终的节点表示。

还有一个工程细节:邻接矩阵要不要加自环。标准GCN实现里会在归一化前把A加上单位阵,让节点聚合时包含自己。GIKT的图传播发生在题目和技能两类节点之间,题目节点没有自己到自己的边,聚合时如果不加自环,线性变换后的自身信息会被邻居信息“稀释”。我复现时测试过,加自环在部分数据集上AUC高0.2个点左右,建议默认加上。实现就是在adj矩阵的对角补1.0,再做对称归一化。

2.3 嵌入初始化与稀疏题目的训练策略

GIKT的题目嵌入和技能嵌入不做预训练,随机初始化后跟着最终目标一起优化。第一次跑的时候这会让很多人怀疑:随机初始化的题目嵌入,经过两层GCN,真的能学到题目差异吗?

我的理解是,GCN的邻居聚合天然起到了一部分预训练的作用。随机初始化的题目向量经过第一轮传播就带上了技能信息,第二轮传播带上了共享技能的其他题目信息。这两轮传播等价于在图里做了信息交换,比纯随机初始化强很多。反过来,如果不用GCN、每道题维护一个独立嵌入,稀疏题目基本学不动,因为它们只在很少的学生序列里出现,梯度更新次数太少。

嵌入层的组织方式如下:

self.q_embedding = nn.Embedding(num_questions, hidden_dim) self.s_embedding = nn.Embedding(num_skills, hidden_dim) self.a_embedding = nn.Embedding(2, hidden_dim) # 0=答错, 1=答对 # GCN初始节点特征:题目嵌入在上,技能嵌入在下 q_feat = self.q_embedding.weight # [Q, d] s_feat = self.s_embedding.weight # [S, d] node_feat = torch.cat([q_feat, s_feat], dim=0) # [Q+S, d] # 两次传播 node_out = self.gcn_layer1(node_feat, adj) node_out = self.gcn_layer2(node_out, adj) # 按编号顺序切回 q_emb = node_out[:num_questions] # [Q, d] s_emb = node_out[num_questions:] # [S, d]

hidden_dim的选择,我实践下来的参考值如下表:

题目规模hidden_dimGCN层数
2万以下642
2万到10万1282
10万以上128到2561到2

具体以你的数据量为准,但核心原则是hidden_dim不能太小,否则图传播的信息会被压缩得太狠。这里有个细节:GCN的初始特征是几个Embedding查表拿到的,它们和GCN层的参数是一起端到端训练的,不是先训GCN再训LSTM。两个阶段分开训会导致GCN只优化了图重建目标,没优化预测目标,效果明显差一截。

3. 从LSTM到Recap模块:长序列依赖的两个关键改动

3.1 LSTM层的输入:题目嵌入与作答嵌入的拼接

GCN解决了“题目怎么表示”,接下来是“知识状态怎么随时间更新”。GIKT在这一层用标准LSTM,但输入不是单纯的题目嵌入,而是把题目嵌入和作答嵌入拼在一起。为什么要拼接作答嵌入?因为知识状态的变化方向由作答结果决定——答对一道加法题和答错一道加法题,对加法技能掌握度的更新是完全相反的。如果只输入题目嵌入,LSTM分不清这次作答是成功还是失败,状态更新就没有依据。

self.lstm = nn.LSTM( input_size=hidden_dim * 2, hidden_size=hidden_dim, batch_first=True, num_layers=1 ) # 构造第t步输入 # q_emb_t: [batch, hidden_dim] 第t题经GCN聚合后的嵌入 # a_emb_t: [batch, hidden_dim] 第t题作答结果对应的嵌入 x_t = torch.cat([q_emb_t, a_emb_t], dim=-1) # [batch, hidden_dim * 2]

input_size是2*hidden_dim,因为题目嵌入和作答嵌入各占一份;hidden_size保持hidden_dim,和GCN输出维度一致,后续交互不需要额外对齐。num_layers我用1,两层LSTM在这个任务上收益很小,但训练时间和显存开销接近翻倍,不划算。LSTM的输出h_seq是所有时间步的隐状态序列,形状[batch, seq_len, hidden_dim],之后会被送进Recap模块做相关历史筛选。

这里有个容易被忽略的点:作答嵌入矩阵只有两行,答对、答错各一个向量,但它同样需要参与训练。有些复现版本把它初始化为全零向量,理由是答案信息已经体现在loss里,但实际效果会差一点。我建议保留独立的可训练作答嵌入——GIKT的最终预测是“答对概率”,答案嵌入的语义空间和题目嵌入分开会更干净,LSTM也更容易学到“看到答错答案时要向下修正状态”这类规律。

还有一个结构要点:LSTM处理的是历史习题序列x1到x_{t-1},目标题目q_t不进入LSTM,而是作为Recap模块的注意力query。这意味着每个时间步预测时,LSTM只跑一遍历史序列,目标题目通过注意力去历史状态里检索相关信息。如果你把目标题目也塞进LSTM,相当于提前泄露了答案,训练和测试的分布就对不上了。

3.2 Recap模块:软选择与硬选择两种实现

LSTM的隐状态序列一长,问题就来了:和当前目标题目真正相关的历史习题,可能散落在50步之前、200步之前,中间夹了一堆无关练习。LSTM理论上能记住长距离信息,但实际训练里,无关步会稀释相关步的梯度信号。GIKT的Recap模块就是为这个设计的——在最终预测之前,先从历史里挑出和目标题目最相关的那些习题状态。

Recap有两种实现方式。软选择对全部历史状态做注意力加权,得到一个加权求和的历史上下文;硬选择只保留注意力权重最高的k个历史状态,其余全部丢弃。

def recap_soft(self, h_seq, q_target, mask): # h_seq: [batch, seq_len, hidden_dim] LSTM历史隐状态 # q_target: [batch, hidden_dim] 目标题目的GCN嵌入 # mask: [batch, seq_len] 1=有效, 0=padding attn = torch.matmul(q_target.unsqueeze(1), h_seq.transpose(1, 2)) attn = attn.squeeze(1) # [batch, seq_len] attn = attn.masked_fill(mask == 0, -1e9) # padding位置置负无穷 attn = F.softmax(attn, dim=-1) context = torch.bmm(attn.unsqueeze(1), h_seq).squeeze(1) return context, attn

软选择的逻辑很直观:用目标题目向量和每个历史隐状态做点积,点积越大代表越相关,softmax后变成权重,再对所有历史状态做加权平均。masked_fill这行保证padding位置不会分走注意力权重——如果不加,短序列样本会把注意力分给一堆零向量,历史上下文被污染。

def recap_hard(self, h_seq, q_target, k=10): # h_seq: [batch, seq_len, hidden_dim] attn = torch.matmul(q_target.unsqueeze(1), h_seq.transpose(1, 2)).squeeze(1) topk_val, topk_idx = torch.topk(attn, k, dim=-1) # [batch, k] # 按索引收集对应的历史隐状态 idx = topk_idx.unsqueeze(-1).expand(-1, -1, h_seq.size(-1)) selected = torch.gather(h_seq, 1, idx) # [batch, k, hidden_dim] return selected, topk_val

硬选择多一个超参数k,也就是最多保留多少个相关历史习题。k我一般取8到12,太小会丢掉有用信息,太大就退化成软选择。论文实验里两种都有报告:硬选择在长序列上的噪声抑制更明显,尤其是学生交互次数差异很大的数据集;软选择不用调k,实现更省事。我复现时默认用硬选择k=10,在ASSISTments上效果稳定。

3.3 注意力计算的两个工程细节

Recap的注意力看起来就是普通点积注意力,但实际数据上有两个坑。第一个是mask,上面代码里已经处理了。第二个是数值稳定性:点积结果的尺度会随hidden_dim增大而变大,softmax梯度会变小。常见做法是除以sqrt(hidden_dim),也就是scaled dot-product attention。GIKT原论文没特别强调这一步,但加上缩放后训练更稳,hidden_dim=128时尤其明显。

# 加缩放的注意力分数 scale = q_target.size(-1) ** 0.5 attn = torch.matmul(q_target.unsqueeze(1), h_seq.transpose(1, 2)) / scale

提示:缩放因子用目标题目嵌入维度计算,不要用历史序列长度,后者会让分数尺度随序列长度漂移。

缩放因子放在matmul之后、softmax之前。如果不想引入这个超参数,也可以把q_target过一层LayerNorm再算注意力,效果类似。我倾向于用缩放,改动最小,不破坏原有结构。另外提醒一句:Recap模块的注意力和后面交互模块的注意力是两套,作用完全不同,别复用同一个参数——前者决定“选哪些历史习题”,后者决定“信哪个交互结果”,语义不一样,共用参数会让两阶段互相干扰。

4. 广义交互模块:四路信息一致建模学生的掌握程度

4.1 为什么先选再交互,而不是直接聚合完就预测

SKVMN和EERNNA的做法是:把相关历史状态聚合成一个新状态,拿这个新状态去做预测。GIKT的差别在于,它不在聚合这一步就结束,而是把“学生当前状态、选中的历史习题、目标题目、相关技能”这四个对象两两交互,每个交互单独产生一个预测,最后用注意力加权。

为什么要绕这一圈?我的理解是,聚合操作会损失对应关系。加权平均得到一个“综合历史状态”后,你只知道“学生过去表现大致怎样”,但不知道“学生是否答对过和这道题很像的题”。GIKT的交互模块保留了这种对应:当前状态和历史习题的交互,体现的是“学生从那段经历中获得了什么”;目标题目和相关技能的交互,体现的是“这道题到底在考什么”。这两类信息性质不同,混在一起聚合会互相干扰。简单说,先选再交互等于把“回顾什么”和“怎么判断”拆成两步,每步都更可控。

多技能题目的处理也需要交互模块。一道题对应多个技能时,不同技能的掌握度可能差很多,把多个技能嵌入平均成一个向量,等于让高掌握技能去补贴低掌握技能。GIKT让目标题目嵌入和每个相关技能嵌入分别交互,技能越多交互分支越多,最终预测由注意力决定哪些技能更重要,而不是粗暴平均。

4.2 交互运算与维度变化

交互的核心运算是逐元素乘法,不是拼接后过全连接。逐元素乘法一开始看着有点玄学,但跑几次对比实验就知道,它比“拼接后过MLP”在稀疏数据上稳得多。逐元素乘法的好处是:两个向量相乘,每个维度上都做了对齐,语义上相当于判断“这个特征维度上两者是否同时激活”。拼接后过MLP也能做,但参数量成倍增加,在稀疏数据集上更容易过拟合。GIKT把交互后的向量再接一个小MLP输出预测分数,各分支的维度变化如下:

def interaction_module(self, h_current, selected, q_target, s_target): # h_current: [batch, hidden_dim] 学生当前LSTM状态 # selected: [batch, k, hidden_dim] Recap选出的历史习题状态 # q_target: [batch, hidden_dim] 目标题目嵌入 # s_target: [batch, hidden_dim] 技能嵌入(多技能先聚合) k = selected.size(1) # 交互1: 当前状态 x 历史习题 inter1 = h_current.unsqueeze(1) * selected # [batch, k, d] # 交互2: 当前状态 x 目标题目 inter2 = h_current.unsqueeze(1) * q_target.unsqueeze(1).expand(-1, k, -1) # 交互3: 当前状态 x 相关技能 inter3 = h_current.unsqueeze(1) * s_target.unsqueeze(1).expand(-1, k, -1) # 交互4: 历史习题 x 目标题目 inter4 = selected * q_target.unsqueeze(1) # [batch, k, d] # 拼接后过MLP,每个交互输出一个logit all_inter = torch.cat([inter1, inter2, inter3, inter4], dim=-1) logits = self.inter_mlp(all_inter).squeeze(-1) # [batch, k] return logits

inter_mlp我用两层全连接,中间接ReLU,输出维度1。交互2和交互3用expand把当前状态和目标题目复制到k个维度上,让四个交互分支的形状统一成[batch, k, d],拼接后是[batch, k, 4d]进MLP。这里d就是hidden_dim。

多技能时s_target怎么得到?我试过两种:对多个技能嵌入取平均,以及用目标题目嵌入对技能嵌入做注意力加权求和。后一种效果略好,计算量也小,推荐直接用:

# 多技能聚合:注意力加权 # s_list: [batch, n_skills, hidden_dim] 当前题目涉及的技能嵌入 # q_target: [batch, hidden_dim] scores = torch.matmul(q_target.unsqueeze(1), s_list.transpose(1, 2)).squeeze(1) s_weights = F.softmax(scores, dim=-1) # [batch, n_skills] s_target = (s_weights.unsqueeze(1) @ s_list).squeeze(1) # [batch, hidden_dim]

注意力聚合的好处是,模型可以为每个技能学一个贡献权重,而不是默认所有技能同等重要。这在题目对应技能数差异很大的数据集上尤其明显。

4.3 最终预测的注意力加权

每个交互都给出了一个预测,但可信度不同。“历史习题x目标题目”这个交互,只有在这道历史习题真的和目标题目很像时才有意义;如果k个历史习题里有一半其实不太相关,它们对应的交互预测应该被压低。GIKT的处理是再套一层注意力:

# 用独立参数计算交互权重 w_logits = self.attn_layer(torch.tanh(all_inter)).squeeze(-1) # [batch, k] attn_w = F.softmax(w_logits, dim=-1) # 每个交互的预测概率 inter_prob = torch.sigmoid(logits) # [batch, k] # 加权求和得到最终预测 final_pred = (inter_prob * attn_w).sum(dim=-1) # [batch]

要点是计算权重的MLP和计算预测的MLP必须是两组独立参数。如果共用一组,会出现“预测概率高的交互天然拿到高权重”的正反馈,模型没有动力去区分“这个交互预测可靠但概率低”和“不可靠但概率高”。分开后,注意力层学到的是哪些交互更值得信,预测层学到的是这个交互给出什么预测,两者解耦。

训练配置上,我复现GIKT用Adam,初始学习率0.001,weight decay 0.0001,batch size 64。GCN部分的梯度有时偏小,我会单独给GCN层参数设0.002的学习率,避免传播层学得太慢。LSTM序列长,BPTT的梯度范数容易爆,我一般用clip_grad_norm_把梯度裁剪到5.0,在backward之后、optimizer.step之前调用。验证集AUC连续5个epoch不涨就把学习率降到原来的0.1,再跑5个epoch还没改善就早停。这个配置在ASSISTments 2009-2010数据集上,一般40个epoch内能收敛到论文报告的AUC区间。

5. GIKT复现避坑:数据格式、维度对齐与指标计算的五个常见问题

复现这类模型,翻车点基本集中在数据预处理和张量形状上。下面五条是我实际跑的时候踩过的坑,每一条都按现象、原因、解决三步写清楚。

5.1 同一题目对应多个技能:邻接矩阵漏边

现象:模型能跑通,但验证集AUC明显低于论文报告值,多个数据集上都是同一个偏差。

原因:数据预处理时只取了题目的第一个技能,或者把多技能题目当成多个独立副本处理。前者让邻接矩阵少了边,高阶信息传不完整;后者会让同一道题在嵌入空间里被拆成多份,训练时梯度互相打架。

解决:遍历q2s映射时把每个技能都连边,邻接矩阵里一行可以有多个非零值。多技能题目在交互模块里再聚合技能嵌入,聚合方式用注意力加权而不是简单平均,这样每个技能对预测的贡献是模型学出来的。

5.2 GCN层数加深后AUC反而下降

现象:把GCN从2层加到3层、4层,验证集AUC不升反降,掉了1到2个点。

原因:过度平滑。GCN层数越多,每个节点的表示越趋向于全图平均值,题目和技能的区分度被抹平。这个问题在图神经网络里很常见,尤其在二部图这种结构上,3层以上基本都会出现。

解决:保持2层聚合。如果觉得高阶信息不够,先检查邻接矩阵的构建是否正确,而不是直接加层。另外,第一层GCN后的dropout对缓解过度平滑有帮助,设0.5比设0.1的效果更稳。

5.3 序列长、batch大导致显存溢出

现象:训练到一半报CUDA out of memory,尤其是在序列长度超过500的数据集上。

原因:LSTM需要把所有时间步的反向传播路径都保存下来,Recap的注意力矩阵是batch×seq_len,序列一长显存占用快速上涨。

解决:训练时把学生序列截断到200到300步,超出部分丢弃或另起一个片段。注意截断要尽量保留最近的历史,因为知识追踪的场景里,最近作答对当前状态的贡献最大。如果截断后batch还想调大,配合梯度累积,每4个batch更新一次参数,效果等同大batch训练。

5.4 节点索引顺序错乱导致嵌入张冠李戴

现象:loss不下降或者震荡,训练曲线完全异常。排查半天发现题目嵌入和技能嵌入混在一起。

原因:GCN传播后的节点表示仍然按构建邻接矩阵时的编号顺序排列,但复现时把切片索引写错,比如直接用技能数量去切题目嵌入,或者图构建和嵌入层用了两套编号。

解决:固定一套编号规则,从头到尾只用这一套。我习惯把题目放前面、技能放后面,用num_questions作为分界,任何地方需要取嵌入都从同一个索引常量出发。写完之后加一个断言,检查切片出来的嵌入维度是否和Embedding矩阵一致。

5.5 AUC计算口径与论文不一致

现象:自己算的AUC和论文报告值差3到5个点,但模型结构和参数都一样。

原因:数据集划分方式不同,AUC是按学生维度算还是全局样本算,以及是否过滤重复作答记录,都会带来明显差异。ASSISTments这类数据集里,同一道题被同一个学生反复做的情况很多,直接全量算会高估模型表现。

解决:先确认你拿到的复现代码用的划分方式——知识追踪任务里通常按学生划分,保证同一个学生不会同时出现在训练集和测试集。计算AUC时用sklearn的roc_auc_score,先逐学生计算再取平均,或者全局计算但在代码注释里写明口径。数据预处理阶段把重复作答记录只保留第一次,这也是知识追踪任务里的常规操作。

提示:ASSISTments原始文件里包含多张表,题号列和技能列要先做去重,否则邻接矩阵里会出现重复边,导致归一化后的权重偏低。

6. 进阶:把GIKT裁剪成适合自己平台的轻量版

6.1 裁剪GCN与交互模块

完整GIKT在学术数据集上效果好,但如果接到生产环境,计算成本和部署复杂度都要重新评估。我自己做轻量化时只动三个地方。

第一,GCN保持一层。一层GCN只传播直接相连的技能信息,损失了共享技能题目之间的二阶关联,但AUC只掉0.3到0.5个点,换来的是一张稀疏矩阵乘法变成了一次查表加聚合,线上推理快很多。第二,Recap的k从10减到5。实际平台里学生最近作答的题目往往是最相关的,top-5足够覆盖大多数预测场景。第三,交互模块从4路减到2路,只保留“当前状态x历史习题”和“历史习题x目标题目”。当前状态x目标题目的信息与前者有重叠,减掉之后对AUC影响很小,但显存占用下降约四分之一。

6.2 验证方法与上线前检查

裁剪后不能只看AUC,我会补两个维度:一个是RMSE,看预测概率的绝对误差是否变大;另一个是冷启动题目的单独AUC,把训练集中出现次数少于5次的题目拎出来单独评估。GCN带来的提升主要就体现在这些稀疏题目上,如果裁剪后冷启动AUC掉得比总体AUC快,说明图传播信息被砍多了,需要回调GCN层数。

上线前还有一道检查:把训练集和测试集的题目ID分布画出来,确认测试集里没有出现训练集完全没有的新题目。知识追踪的离线验证假设是题目集合固定,如果平台每天都有新题入库,GIKT这类基于静态图的方法需要定期重构图,或者降级为只依赖技能ID的备用模型。

复现GIKT之前,我一直觉得图卷积就是给节点嵌入加一层邻居平均,没什么特别的。真正把邻接矩阵、索引切分、交互模块拼到一起之后才发现,最耗时间的不是模型本身,而是弄清楚每个张量在哪个维度上做什么。从那以后我每次复现论文模型,第一件事就是把数据流和维度变化画在一张纸上,跑通后再谈调参。这份资源里的实现省掉了这一步的摸索,希望帮到你。

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

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

tomcat老版本下载tomcat8.5下载

tomcat老版本下载tomcat8.5下载tomcat老版本下载tomcat8.5下载tomcat老版本下载tomcat8.5下载 https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.99/bin/ 直达链接

作者头像 李华
网站建设 2026/9/30 16:30:01

Jev决策模型验证:分类聚合与Transformer实操指南

1. 从“判断决策”说起:为什么分类聚合才是真场景第一次看到“Jev决策模型验证”这个说法,我脑子里冒出来的不是某个具体模型,而是一类很典型的工程困境:团队花大力气训了一个模型,指标看着不错,一上真实业…

作者头像 李华
网站建设 2026/9/30 16:27:40

TensorFlow边缘部署:剪枝+量化实战指南

简介:本资源是一份面向AI工程师与边缘计算开发者的实战型技术指南,系统讲解如何利用TensorFlow完成模型剪枝、量化及部署至边缘设备的端到端流程,解决大模型在资源受限终端上推理慢、内存溢出、功耗高等核心痛点。文档共26页PDF,结…

作者头像 李华
网站建设 2026/9/30 16:27:26

用嘴指挥AI画图:DALL·E 3实战指南,从LOGO到梗图

1. 从"提需求"到"看效果":为什么用嘴指挥AI画图这件事值得认真对待 大多数人第一次接触AI绘图,脑子里想的都是"我描述一个画面,它给我画出来"。但真正用起来你会发现,DALLE 3 这类工具最舒服的用法…

作者头像 李华
网站建设 2026/9/30 16:26:35

【通信】基于鲸鱼优化算法实现无线网络资源分配附Matlab实现

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和…

作者头像 李华