恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

GIKT深度知识追踪:从交互日志到个性化习题推荐的Python实践

  • 首页
  • 资讯中心
  • /
  • GIKT深度知识追踪:从交互日志到个性化习题推荐的Python实践

相关资讯

射频系统入门:从链路架构到测试调试的关键技术解析 2026/10/3 9:57:04
电磁炉线圈加热仿真:涡流损耗与热耦合建模实战指南 2026/10/3 9:57:04
基于事件触发机制的孤岛微电网二次电压与频率协同控制仿真 2026/10/3 9:57:04

最新资讯

AI辅助科研全流程实战指南:从选题到投稿的效率提升方法论
从零起步的AI工程实战:数据、评测与模型迭代全指南
从零搭建AI工程全链路:数据、训练、推理到Agent编排的实践指南
游戏公司AI绘画大赛背后:从扩散模型到团队协作的工程化落地指南
Flink与Kafka集成实战:版本选型、读取方式与调优避坑
Excel VBA模板同步总控台搭建实战:告别版本混乱

今日推荐

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成
编译原理实验:递归下降分析器消除左递归与避坑指南
Python协议级爬取Shopee商品数据实战

本周热门

从像素到笔画:srt-whiteboard-animation骨架笔迹追踪实现(Zhang-Suen细化+8邻接追踪)
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
新手入门看这篇:建设网站加盟避坑指南与SEO实操

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

GIKT深度知识追踪:从交互日志到个性化习题推荐的Python实践

发布时间:2026/10/3 9:57:04
GIKT深度知识追踪:从交互日志到个性化习题推荐的Python实践 简介这是一套基于GIKT深度知识追踪的个性化习题推荐系统完整工程面向计算机专业毕业设计、课程实践与期末大作业等场景解决从算法原理到系统部署的全链路需求。系统采用深度学习方法构建学生知识状态动态画像据此生成个性化习题推送策略附带的Python源码已通过多轮测试执行稳定部署指南覆盖环境配置、前后端联调等关键环节。压缩包共143个文件、约10.85MB内容包括Python后端核心模块py/pyc、Vue前端页面vue/js、模型权重与训练中间结果pth/npy/npz、实验数据csv/xlsx以及配置文件json/yaml/xml并含Flask-Vue项目结构、推荐效果测试脚本与知识追踪相关笔记数据样例与Notebook组合可辅助快速验证推荐效果。当前已有53人学习下载适合需要高质量毕业设计参考、期末大作业模板或教育技术研究基础的学习者。1. 为什么个性化习题推荐要上 GIKT 深度知识追踪个性化习题推荐里最闹心的一环不是推荐算法本身而是模型根本不知道学生此刻掌握了什么。基于 GIKT 深度知识追踪的个性化习题推荐系统正是把知识追踪和推荐串成一条流水线的 Python 工程方案GIKT 用图结构把知识点之间的依赖关系统一建模沿着答题序列持续更新学生的掌握状态再把这种状态翻译成“下一题推什么”。我见过不少团队把召回、排序做得花里胡哨结果一上 AB 实验新增点击全被没有知识状态的对照组吊打。这套方案适合已经有一定规模的在线练习产品、题库系统或者自适应学习平台也适合想尽快跑通“知识追踪 推荐”全流程的算法工程师。2. 构建 GIKT 的输入交互日志清洗与知识点依赖图生成GIKT 和普通序列模型最大的差别是它还要一张“知识点依赖图”。所以动手写模型之前先要把原始练习日志变成两样东西按学生切好的交互序列以及一张表达知识点先决/共现关系的邻接矩阵。这一步做不好后面模型结构再对也白搭。2.1 从原始交互表到训练样本id 对齐与序列截断以最常见的题库导出格式为例一张交互表通常至少有 user_id、order_id、skill_id、correct 四个字段。order_id 是全局作答顺序用来还原每个学生的答题时间线。我的习惯是先排序、再过滤缺失知识点、最后把 id 映射成从 0 开始的连续整数这一步非常关键因为 PyTorch 的 Embedding 层不接受乱序大 id。import pandas as pd raw pd.read_csv(data/assistments-2009-2010.csv) # 1) 按用户和时间序整理 raw raw.sort_values([user_id, order_id]).reset_index(dropTrue) raw raw.dropna(subset[skill_id]) # 2) 连续化 id raw[u_idx] raw[user_id].astype(category).cat.codes raw[s_idx] raw[skill_id].astype(category).cat.codes raw[y] raw[correct].astype(int) print(raw[[u_idx, s_idx, y]].head())逻辑说明先排序是为了保证后面切序列时顺序不乱dropna 把没有知识点标注的脏样本直接剔除astype(category).cat.codes会在 pandas 内部完成去重和编码比手动建字典省事而且结果稳定。映射之后s_idx 的最大值就是知识点总数 N后面模型里的 Embedding 需要用到这个数。接下来把交互记录切成“每个学生最多一条序列”的样本。不同学生的做题长度差异很大有的十几条有的上千条GIKT 里常见的处理方式是截断到固定长度比如保留最近 50 次作答。MAX_LEN 50 def build_sequences(df, max_lenMAX_LEN): 按学生聚合保留最近 max_len 条作答 seqs [] for _, g in df.groupby(u_idx): g g.tail(max_len) # 保留离当前时刻最近的数据 seqs.append({ skill_seq: g[s_idx].tolist(), correct_seq: g[y].tolist(), }) return seqs train_seq build_sequences(raw) print(train_seq[0])参数说明max_len 我一般取 30~50太短会丢失早期学习趋势太长容易让 GRU 的梯度传到后面就衰减了训练也慢。tail(max_len) 是业务上的一个假设预测当前时刻只需要最近一段交互这个假设在真实练习场景下基本成立因为知识点掌握状态是随时间变化的。2.2 生成知识点依赖图共现统计与邻接矩阵构建GIKT 的图有两条路可以构建。如果题库本身有标注知识点先决关系比如“因式分解”是“一元二次方程”的先决知识点直接把它转成邻接矩阵最可靠大多数题库没有这个标注这时就用“共现图”做近似统计同一个学生序列里两个知识点在固定窗口内同时出现的频率。窗口共现的意义是学生刚做完知识点 A 又去做 B很可能是因为 A 没掌握或者 B 依赖 A。from collections import Counter import scipy.sparse as sp def build_cooccurrence_graph(df, window5, min_cnt3): n_skills df[s_idx].max() 1 counter Counter() for _, g in df.groupby(u_idx): skills g[s_idx].tolist() for i, s in enumerate(skills): left max(0, i - window) right min(len(skills), i window 1) for j in range(left, right): if i ! j: counter[(s, skills[j])] 1 row, col, val [], [], [] for (a, b), cnt in counter.items(): if cnt min_cnt: row.append(a) col.append(b) val.append(cnt) adj sp.coo_matrix((val, (row, col)), shape(n_skills, n_skills)).tocsr() # 对称化A 指向 B 与 B 指向 A 都保留 adj adj.maximum(adj.T) return adj adj build_cooccurrence_graph(raw) print(adj.shape, adj.nnz)参数说明window 控制共现范围5 是我试下来比较稳的默认值min_cnt 用来过滤偶发共现防止把噪声当依赖数据稀疏时降到 2数据量大时用到 5 都没问题。对称化是因为我们无法从共现频率判断依赖方向保留双向关系最保守。最后得到的是 scipy 的稀疏矩阵模型里直接用 torch.spmm 做图传播就不用转稠密矩阵了否则知识点上千之后内存会先崩。2.3 按用户切分的训练/验证集划分切分是这一章最容易翻车的地方。很多新手直接对行做随机切分同一个学生的作答会同时出现在训练和验证里造成严重的数据泄漏验证集 AUC 虚高得离谱。正确做法是按用户划分先分出 20% 的用户做验证其余做训练再保证验证集的用户在训练阶段完全不可见。import numpy as np np.random.seed(42) n_users raw[u_idx].nunique() all_users np.arange(n_users) np.random.shuffle(all_users) n_val int(n_users * 0.2) val_users set(all_users[:n_val].tolist()) train_users set(all_users[n_val:].tolist()) train_raw raw[raw[u_idx].isin(train_users)] val_raw raw[raw[u_idx].isin(val_users)] print(ftrain users: {len(train_users)}, val users: {len(val_users)})注意GIKT 的图最好在这两类样本之外单独构建一次比如基于全量数据构建图关系但序列窗口统计只使用训练集这样能避免验证集知识“渗透”进图的边。我通常把 build_cooccurrence_graph 里的 df 换成 train_raw图中的边就只能反映训练集里的共现规律。3. 编写 GIKT 模型核心代码图传播、序列编码与预测头GIKT 的主体结构是两个 Encoder一个用 GCN 把知识点依赖图编码成稳定的知识点表征另一个用 GRU 把学生的答题序列编码成动态掌握状态。最后把两者合并预测当前习题的正确概率。这一章直接给完整可跑的简化实现代码按项目内常见的 models/gikt.py 组织。3.1 模型骨架与知识点嵌入先定义模型类和基础层。GIKT 有一个细节知识点 Embedding 既作为 GCN 的输入特征又要在序列编码阶段被查询所以它必须同时服务于两个模块。import torch import torch.nn as nn import torch.nn.functional as F class GIKTModel(nn.Module): def __init__(self, n_skills, n_hidden128, n_layers2, dropout0.5, gru_layers1): super().__init__() self.n_skills n_skills self.n_hidden n_hidden # 知识点嵌入训练 GCN 和序列编码共享 self.emb_skill nn.Embedding(n_skills, n_hidden) # 负答操作嵌入答错时的交互向量 self.emb_fail nn.Embedding(n_skills, n_hidden) # 两个 Encoder self.gcn_layer self._build_gcn(n_hidden, n_layers, dropout) self.gru nn.GRU(input_sizen_hidden, hidden_sizen_hidden, num_layersgru_layers, batch_firstTrue, dropoutdropout if gru_layers 1 else 0.0) # 预测头状态 当前知识点表征 - logit self.pred_fc nn.Linear(n_hidden * 2, 1) self.dropout nn.Dropout(dropout) def _build_gcn(self, hidden, layers, dropout): 把多层 GCN 组装成 ModuleList return nn.ModuleList([ nn.Sequential( nn.Linear(hidden, hidden), nn.ReLU(inplaceTrue), nn.Dropout(dropout), ) for _ in range(layers) ])逻辑说明emb_skill 和 emb_fail 是两个独立的 Embedding这样答对与答错会被编码成不同的向量轨迹GRU 才能区分“这个知识点一遍过”和“这个知识点反复错”两种状态。GCN 层我用 LinearReLUDropout 做一个轻量实现替换掉复杂的 graph conv 写法降低新手理解成本。n_hidden 控制所有表征维度全项目保持同一个数后面拼接时才不会出现维度不一致。3.2 图正则化与 GCN 图传播GCN 直接吃邻接矩阵是不行的因为原始邻接矩阵的行和往往不等于 1会导致传播后知识点表征的尺度漂移。标准做法是先做对称归一化D^-1/2 * A * D^-1/2。用 scipy 在预处理阶段算好存成稀疏张量每次训练只做一次前向传播。import scipy.sparse as sp import numpy as np def normalize_adj(adj): D^-1/2 * A * D^-1/2 adj adj sp.eye(adj.shape[0], formatcsr) # 加自环 d np.array(adj.sum(axis1)).flatten() d_sqrt_inv np.power(d, -0.5) d_sqrt_inv[np.isinf(d_sqrt_inv)] 0.0 d_mat sp.diags(d_sqrt_inv) norm_adj d_mat adj d_mat return norm_adj.tocoo() norm_adj normalize_adj(adj) # 转成 torch 稀疏张量 idx torch.LongTensor(np.vstack([norm_adj.row, norm_adj.col])) val torch.FloatTensor(norm_adj.data) adj_t torch.sparse_coo_tensor(idx, val, torch.Size(norm_adj.shape))参数说明加自环是为了让每个知识点在传播后保留自身信息这也是 GCN 的标准做法。注意np.power(d, -0.5)在 d0 时会产生 inf必须用np.isinf归零否则后面 torch.spmm 直接报 NaN。边界节点在稀疏数据里很常见尤其是出现次数少的知识点。GCN 前向传播代码挂在模型里def forward_gcn(self, x, adj_t): for layer in self.gcn_layer: x torch.spmm(adj_t, x) # [N, d] 图传播 x layer(x) return x这里的 x 是 emb_skill.weight也就是所有知识点的 embedding 矩阵。GCN 每层都做一次“邻居表征加权求和 非线性变换”两层之后每个知识点的表征就包含了它一步和两步邻居的信息。这就是 GIKT 优于 DKT 的关键DKT 的知识点关系全靠隐式学习GIKT 直接把它显式写进图结构里尤其在长尾知识点上不容易跑偏。3.3 序列编码与掌握概率预测模型前向传播的输入是三样答题序列里的知识点 id、答题对错标签、归一化后的邻接张量。一步到位看代码。def forward(self, skill_seq, correct_seq, adj_t): skill_seq: [B, T] 知识点 id 序列 correct_seq: [B, T] 0/1 对错序列 adj_t: [N, N] 归一化稀疏邻接矩阵 B, T skill_seq.size() # 1) 所有知识点通过 GCN 得到结构增强表征 skill_rep self.forward_gcn(self.emb_skill.weight, adj_t) # [N, d] # 2) 取出本序列涉及的知识点表征 cur_skill skill_rep[skill_seq] # [B, T, d] # 3) 答对用 skill_rep答错用 emb_fail组成交互向量 correct correct_seq.float().unsqueeze(-1) # [B, T, 1] interact correct * cur_skill (1 - correct) * self.emb_fail(skill_seq) # 4) GRU 编码完整交互历史 state, _ self.gru(interact) # [B, T, d] # 5) 拼接当前知识点表征与历史状态预测正确率 logit self.pred_fc(torch.cat([state, cur_skill], dim-1)).squeeze(-1) return logit # [B, T]逻辑说明第 3 步用 correct 做了软开关答对那一步输入的是“该知识点的结构表征”答错输入的是“失败表征”GRU 就能从轨迹里区分“同样是三次作答顺序是 对-对-错 还是 错-错-对”。第 5 步把历史状态和当前知识点表征拼起来做二分类是因为预测正确率不仅取决于历史掌握度也取决于当前题对应哪个知识点两者缺一不可。整套代码跑通之后logit 再经过 sigmoid 就是模型预测的答对概率。4. 训练、评估与推荐策略从预测概率到下一道习题模型结构确定之后训练和评估协议决定这个模型能不能在真实场景里立住。这一章给训练循环、切分评估协议以及最后怎么把预测概率翻译成推荐列表。4.1 训练循环与梯度裁剪GIKT 的序列用 GRU 编码梯度容易在时间步上爆炸所以梯度裁剪是标配。损失函数用二分类交叉熵只计算有真实作答的位置padding 位置必须屏蔽。import torch.nn.functional as F def train_one_epoch(model, loader, optimizer, adj_t, devicecuda): model.train() total_loss 0.0 for batch in loader: skill_seq batch[skill_seq].to(device) # [B, T] correct_seq batch[correct_seq].to(device) # [B, T] mask batch[mask].to(device) # [B, T] logit model(skill_seq, correct_seq, adj_t) # [B, T] loss F.binary_cross_entropy_with_logits( logit, correct_seq.float(), reductionnone ) # 只统计有效位置 loss (loss * mask).sum() / mask.sum().clamp(min1.0) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() return total_loss / max(len(loader), 1)注意 mask 要在 DataLoader 的 collate_fn 里同步生成padding 位置 mask0。裁剪的 max_norm 我一般取 5.0太小会让模型学不动太大起不到保护作用。4.2 评估协议按练习计算 AUC 与 ACC评估时同样只统计有效位置。AUC 能反映排序能力ACC 能反映预测的绝对正确率两个都要看因为推荐场景更依赖 AUC而“这个学生到底会不会”更依赖 ACC。from sklearn.metrics import roc_auc_score, accuracy_score torch.no_grad() def evaluate(model, loader, adj_t, devicecuda): model.eval() y_true, y_pred [], [] for batch in loader: skill_seq batch[skill_seq].to(device) correct_seq batch[correct_seq].to(device) mask batch[mask].to(device) logit model(skill_seq, correct_seq, adj_t) prob torch.sigmoid(logit) y_true.extend(correct_seq[mask].cpu().tolist()) y_pred.extend(prob[mask].cpu().tolist()) auc roc_auc_score(y_true, y_pred) acc accuracy_score(y_true, (np.array(y_pred) 0.5).astype(int)) return auc, acc4.3 把掌握概率变成推荐列表知识追踪模型输出的不是“推荐分数”而是“每个习题的答对概率”。推荐系统要把它变成可落地的排序策略我常用的方法是给每个知识点聚合一个“掌握度”在该知识点最近的 N 次作答中取预测概率均值然后推荐“掌握度低于及格线但又不是完全不会”的知识点对应习题。def recommend_next_exercise(model, user_seq, user_corrects, adj_t, candidate_pool, devicecuda): user_seq: 最近答题知识点 id 列表 model.eval() with torch.no_grad(): skill_seq torch.LongTensor([user_seq]).to(device) correct_seq torch.LongTensor([user_corrects]).to(device) logit model(skill_seq, correct_seq, adj_t) probs torch.sigmoid(logit)[0].cpu().numpy() # [T] # 聚合到知识点最近作答的平均掌握概率 mastery {} for s, p in zip(user_seq, probs): mastery[s] mastery.get(s, []) [p] for s in mastery: mastery[s] float(np.mean(mastery[s])) # 推荐最薄弱的知识点再在该知识点下选题目 weak sorted(mastery.items(), keylambda x: x[1])[:5] candidates candidate_pool[weak[0][0]] # 知识点对应的习题池 return candidates[:10], weak关键点推荐的“最近发展区”一般落在 0.4~0.7 之间完全没掌握0.3的题推给学生很容易挫败掌握度接近 1 的题再推没有学习价值。candidate_pool 的构建方式因题库而异记住一个原则先按知识点定位薄弱项再在习题池里筛难度匹配的题而不是直接拿全量题库排序。5. GIKT 实战避坑5 个让模型翻车的常见问题这章写我在这类项目里反复踩过的坑按“现象 → 原因 → 解决”记录。每一个都值得在动手前先看一遍。5.1 长序列截断后预测全崩现象用完整序列训练时指标正常部署后发现线上预测值和离线差距很大检查发现线上用户历史很长截断到最近 50 条之后很多早期关键作答直接丢失。原因评测时验证集用户的平均序列长度可能只有 20模型没见过长序列而线上真实用户动辄几百条截断策略不一致导致分布偏移。解决训练和预测必须使用同一种截断策略。我后来把 MAX_LEN 统一为 50并且在验证集里故意保留一部分超过 100 条作答的用户做压力测试确认模型在长序列上不会漂移。提示简单粗暴地把所有序列都保留只做 padding 不截断会让 GRU 在 200 步以后梯度衰减严重训练时间也翻倍。优先选择截断 压力测试。5.2 图正则化参数调不好模型反而欠拟合现象GIKT 的图传播加了很强的 L2 正则后训练 loss 降不下去AUC 比不上不加图的普通 GRU 基线。原因知识点的共现图本身含噪尤其是用窗口共现构建的图很多边是“偶然一起出现”而不是真正的先决关系强正则会把有效信号也压掉了。解决我一般把图相关的正则权重初始化到 1e-3每次训练先看训练集 AUC 是否持续上升若 5 个 epoch 不动就降一个量级另外在构建图时提高 min_cnt 阈值用 5 以上过滤低质量边比单纯调正则更有效。5.3 测试集随机切导致 AUC 虚高现象上线前离线 AUC 0.82上线后实际推荐效果很差点击率提升几乎为零。原因数据行数级随机切分导致同一个学生的作答同时出现在训练和验证集模型“见过”这个学生的部分答案验证指标严重失真。解决严格按用户切分且保证验证集用户在训练阶段完全不存在。我后来连图构建都只用训练集统计验证集仅用来评估指标直接下降了近 0.1但真实效果和线上表现一致了。5.4 部署时 skill_id 对不上导致预测结果错位现象离线脚本一切正常部署到推荐服务后同一个学生同一批数据预测结果和离线对不上且错误集中在某些知识点上。原因离线预处理把 skill_id 重编码成 0 到 N-1 的连续 id部署接口收到的是题库原生 skill_id没有查映射表就直接喂进模型id 错位导致 embedding 张冠李戴。解决训练完把 id 映射表保存成 json部署时加载同一份映射。import json skill_mapping dict(enumerate( raw[skill_id].astype(category).cat.categories.tolist() )) # 保存映射skill_id - s_idx with open(skill_mapping.json, w) as f: json.dump({str(v): k for k, v in skill_mapping.items()}, f)部署时收到前端传的 skill_id先查这份 json 得到 s_idx再进模型。这个坑最隐蔽也最容易在测试环境漏掉因为本地数据集的 skill_id 恰好是连续数字线上不是。5.5 稀疏矩阵原地修改导致训练数据被污染现象训练到第二个 epoch 时 loss 突然变成 NaN第一个 epoch 还是正常的。原因我在数据预处理时复用了同一个邻接矩阵变量某次在模型 forward 里做了adj_t adj_t eye等原地操作稀疏张量的 data 被反复累加数值越滚越大直接溢出。解决所有对 adj 的归一化、加自环、对称化操作全部放在预处理里完成模型 forward 只读不改。torch 稀疏张量对原地操作的支持不稳宁可多复制一份也不要图省事。import copy adj_t_infer copy.deepcopy(adj_t) # 给推理单独留一份6. 部署到推荐服务模型导出、API 封装与上线前验证训练完成后模型要变成线上可调用的推荐服务。我习惯用 FastAPI 做一层薄封装因为它的异步能力和 pydantic 校验正好够用又不需要引入额外框架。部署前第一件事是把模型权重和之前保存的 skill_mapping.json 打包在一个目录里保证回滚时版本对应。torch.save(model.state_dict(), models/gikt_weights.pt) # 训练用的邻接矩阵也存下来 torch.save(adj_t, models/adj_t.pt)线上接口只接收最近一次作答序列推荐服务内部负责映射 id、查图、推理和取候选题库。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class RecommendRequest(BaseModel): user_id: int skill_seq: list[int] correct_seq: list[int] app.post(/recommend) def recommend(req: RecommendRequest): s_idx [mapping[str(s)] for s in req.skill_seq] prob, weak recommend_next_exercise( model, s_idx, req.correct_seq, adj_t_infer ) return {prob: prob, weak_skills: weak}上线前务必用最近一周的真实日志回放一遍对比新旧模型给同一用户推荐的重叠度重叠度低于 30% 时先查 id 映射和序列截断再怀疑模型本身。我自己的习惯是每次训练都保留一份 weights、一份 mapping、一份图结构、一份数据预处理脚本四个文件同版本号打包。这套流程帮我省过无数次“三个月后模型要回滚但根本不知道当时的 id 映射是哪份”的尴尬。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号