DeepKnowledgeTracing
DeepKnowledgeTracing(DKT)是一种基于深度学习的模型,用于对学生的知识掌握情况进行建模和预测,可以通过分析学生的历史答题记录来推断他们的知识水平和学习进度。
DKT的核心思想是利用递归神经网络(RNN)来捕捉学生的学习轨迹和知识状态的动态变化。模型接受学生的答题序列作为输入,并通过时间步的迭代来预测学生在下一个时间步的答题情况。通过不断地对学生的答题进行预测和比较,DKT可以逐渐调整模型的参数,提高对学生知识状态的准确预测能力。
DKT的关键创新是引入了一个称为”学生知识状态向量”的概念,它用于表示学生在不同知识点上的掌握程度。该向量通过RNN中的记忆单元来维护和更新,从而捕捉学生的知识状态随时间的变化。通过将学生的答题序列与学生知识状态向量进行联合建模,DKT可以更准确地推断学生在不同知识点上的知识水平。
使用DKT进行知识状态预测可以有助于教育领域的一些应用,例如个性化学习和智能辅导系统。通过对学生的知识状态进行建模和预测,教育者可以更好地了解学生的学习需求和困难,从而为他们提供个性化的学习支持和指导。
新型的RNN输入编码,表现遥遥领先,无需专家注释,生成定制题库
此模型的自我学习能力很强,可以根据做过的题目类型以及准确率来预测涉及某些知识点的答题情况,并不必需人工分类注释,很多功能类似的大模型在做数据分析时往往需要人工确定参数阈值,兼容性不够好。
RNN的LSTM所提供的长期记忆功能是其重要优势
在DeepKnowledgeTracing(DKT)模型中,one-hot编码用于表示学生的答题情况或知识点的状态。
在one-hot编码中,对于一个具有N个可能取值的离散变量,将其编码为一个长度为N的二进制向量。向量中只有一个元素为1,表示当前取值,而其他元素都为0。这样的编码方式能够将每个离散取值映射到一个唯一的向量表示。在DKT中,one-hot编码用于表示学生的答题情况。假设有M个不同的题目,每个题目都有可能的答案选项(如A、B、C、D等)。那么,每个题目的答题情况可以使用一个长度为M的one-hot向量来表示。向量中只有对应学生选择的答案选项为1,其他选项均为0。
类似地,DKT中的知识点状态也可以使用one-hot编码表示。如果有K个不同的知识点,那么每个知识点的状态可以使用一个长度为K的one-hot向量来表示。向量中只有对应知识点为当前学生所掌握的知识点时为1,其他知识点均为0。
通过使用one-hot编码,DKT模型能够将离散的答题情况或知识点状态转换为可供神经网络处理的向量表示形式。这样的表示方式使得模型能够更好地处理离散数据,并进行相应的预测和推断。
DKT需要大量答题数据,对于线上的大规模教学更有帮助。
DKT核心代码是三个文件:”deep_knowledge_tracing_model.py“,”data.py”,”main.py”.
”deep_knowledge_tracing_model.py“
import torch.nn as nn
# 导入PyTorch中的神经网络模块
class DeepKnowledgeTracing(nn.Module):
"""Deep Knowledge tracing model"""
#定义了一个名为 DeepKnowledgeTracing 的类,它是 nn.Module 的子类,因此表示这是一个 PyTorch 模型。
# 构造方法 tie_weights是参数共享的意思
def __init__(self, rnn_type, input_size, hidden_size, num_skills, nlayers, dropout=0.6, tie_weights=False):
#以上是参数名,实际上的self的基本成员变量是rnn_type,nhid,nlayers,其他的是自定义初始化的参数
# 重构8个字典属性
#这是类的构造方法。它接受一些参数,包括循环神经网络的类型(rnn_type)、输入的大小(input_size)、隐藏层的大小(hidden_size)、技能数量(num_skills)、层数(nlayers)等。还有一些可选参数,如丢弃率(dropout)和是否共享参数权重(tie_weights)。
super(DeepKnowledgeTracing, self).__init__()
#调用父类 nn.Module 的构造方法,确保正确地初始化模型。
if rnn_type in ['LSTM', 'GRU']:
# 让rnn模型为LSTM或者GRU,后使用getattr函数取出对应模型的实例
self.rnn = getattr(nn, rnn_type)(input_size, hidden_size, nlayers, batch_first=True, dropout=dropout)
#创建 RNN 模型,并根据给定的参数初始化。
else:
try:
nonlinearity = {'RNN_TANH': 'tanh', 'RNN_RELU': 'relu'}[rnn_type]
except KeyError:
raise ValueError("""An invalid option for `--model` was supplied,
options are ['LSTM', 'GRU', 'RNN_TANH' or 'RNN_RELU']""")
#类型不匹配报错
self.rnn = nn.RNN(input_size, hidden_size, nlayers, nonlinearity=nonlinearity, dropout=dropout)
self.decoder = nn.Linear(hidden_size, num_skills)
#Decoder 是一个重要的组件,用于生成目标序列或标签。最简单的 Decoder 可能是一个基于循环神经网络(Recurrent Neural Network,RNN)的 Decoder。在这种情况下,Decoder 将上一个时间步的输出作为当前时间步的输入,并依次生成序列。常见的 RNN Decoder 包括基本的 RNN、长短期记忆网络(Long Short-Term Memory,LSTM)和门控循环单元(Gated Recurrent Unit,GRU)等。
#创建一个线性层作为解码器,将 RNN 的隐藏状态映射到技能数量上。
# input维度是(seq_size, batch_size, input_size)
# h0(初始隐藏状态)维度是(numLayers, batch_size, hidden_size)numLayers 表示 RNN 模型中的层数,即神经网络中堆叠的 RNN 层的数量。batch_size 表示每个时间步输入的样本数。hidden_size 表示每个隐藏层中的隐藏单元数。
# 成员变量的初始化
self.init_weights()
self.rnn_type = rnn_type
self.nhid = hidden_size
self.nlayers = nlayers
def forward(self, inputs, hidden): # 这里提示函数名与内置函数名一样
# 定义了模型的前向传播方法。接受输入 inputs 和隐藏状态 hidden,然后通过 RNN 模型和线性解码器进行计算,返回预测的技能掌握程度 decoded 和更新后的隐藏状态 hidden。
# input = self.drop(input)
output, hidden = self.rnn(inputs, hidden)#RNN模型计算
# output表示 RNN 模型的输出,它是一个张量(是向量的推广,它可以有多个维度,并且每个维度可以有多个分量),维度(seq_size, batch_size, hidden_size)
#seq_size 表示序列的长度,即 RNN 模型处理的时间步数。batch_size 表示每个时间步输入的样本数。hidden_size 表示每个隐藏层中的隐藏单元数。
# hidden维度(numLayers, batch_size, hidden_size)
# 表示 RNN 模型在最后一个时间步的隐藏状态,它也是一个元组,其中包含两个张量:第一个张量的维度为 (numLayers, batch_size, hidden_size),表示最后一个时间步的隐藏层状态。第二个张量的维度与第一个张量相同,如果使用的是 LSTM,则表示最后一个时间步的细胞状态;如果使用的是其他类型的 RNN(如 GRU),则为 None。
# output = self.drop(output)
decoded = self.decoder(output.contiguous().view(output.size(0) * output.size(1), output.size(2)))
# 这一步是通过线性层 self.decoder 对二维张量进行解码操作,得到模型的预测结果 decoded。线性层是一种全连接层,它将输入张量进行线性变换,并将其映射到指定维度的输出空间。在这里,self.decoder 的输入是形状变换后的二维张量,它将其映射到维度为 num_skills 的输出空间,即模型需要预测的技能数量。
# 全连接层(Fully Connected Layer),也称为密集连接层或仿射层,是神经网络中常用的一种基本层类型。在全连接层中,每个输入神经元都与输出神经元相连接,即每个输入神经元都与每个输出神经元相连,形成完全连接的网络结构。这意味着全连接层中的每个输出值都是由输入值的线性组合和激活函数的非线性变换得到的。
# 全连接层的作用是学习输入数据的特征表示,并将这些特征表示映射到输出空间,通常用于实现从低级特征到高级特征的变换。在深度学习模型中,全连接层通常用于模型的最后几层,用于将抽取的特征表示映射到模型输出(例如分类标签或回归值)。
# 与全连接层相对应的是局部连接层或者卷积层(Convolutional Layer),它们是另一种常见的神经网络层类型。在卷积层中,神经元只与输入数据的局部区域连接,而不是与整个输入数据连接。卷积层在处理二维图像数据时,可以有效地提取局部区域之间的特征关系。卷积层使用一组称为卷积核(或滤波器)的小窗口,通过在输入图像上滑动这些卷积核,并在每个位置上进行卷积操作,从而计算输出特征图。在卷积操作中,卷积核与输入图像的局部区域进行点乘,然后将结果相加,最终生成输出特征图中的一个单个像素值。这种局部连接的特性使得卷积层能够有效地捕获输入数据中的空间结构信息,因此在处理图像等二维数据时非常有效。在许多视觉任务中,卷积层通常被用于提取图像的特征表示。
#在典型的循环神经网络(RNN)架构中,并没有卷积层,而是由循环层组成,如循环神经网络单元(如LSTM、GRU等)或者简单的循环神经网络。这是因为 RNN 主要用于处理序列数据,而不是像图像处理等空间结构数据,因此通常不需要卷积层来提取空间特征。循环神经网络通过在时间步上传递隐藏状态来处理序列数据。在处理每个时间步的输入时,RNN会根据当前时间步的输入和前一个时间步的隐藏状态生成当前时间步的隐藏状态。这样,RNN 可以对序列数据的时间依赖关系进行建模。
# contiguous()一般都会和view()搭配在一起使用,view的使用要求空间连续,相当于tensorflow(类似PyTorch的深度学习框架)中的reshape,这一步是为了确保张量 output 是连续的内存布局。在 PyTorch 中,有些操作会导致输出张量的内存布局不连续,而连续的内存布局对于某些操作(例如 view 操作)是必要的。因此,通过 contiguous() 操作可以确保 output 张量的内存布局是连续的。
#.view(output.size(0) * output.size(1), output.size(2)):这一步是将 output 张量进行形状变换,将其转换为二维张量。具体来说,它将 output 张量的前两个维度(seq_size 和 batch_size)合并成一个维度,并保留第三个维度(hidden_size)。这样做的目的是将 RNN 模型的输出转换为线性层的输入形状,以便进行解码操作。
# 假设output是2*3*3的,则view的2个参数值分别为2*3,3
# 将output的最后一个维度hidden_size变为num_skills
return decoded, hidden
# 这里只初始化了每个cell中的h和c
# 在 LSTM 中,细胞状态(cell state)是一个独立于隐藏状态(hidden state)的状态,用于在不同时间步之间传递和保存信息。细胞状态会根据门控单元(遗忘门、输入门、输出门)的控制来更新,门控单元的作用是调节细胞状态的流动,从而有效地捕捉长期依赖关系。而在门控循环单元(GRU)中,虽然没有显式的细胞状态(cell state),但是在GRU中仍然存在一个称为“隐状态”的状态,用于保存模型在当前时间步的信息。
def init_hidden(self, bsz):
# 初始化隐藏状态的方法,根据指定的批大小 bsz 返回对应形状的零张量,用于 RNN 模型的初始隐藏状态。
# 每一隐层参数
weight = next(self.parameters())
# 如果是LSTM的话还需要c
# 在门控循环单元(GRU)中,相对于长短期记忆网络(LSTM),没有显式的细胞状态(cell state,通常记为c)。GRU 的设计旨在减少参数数量,简化网络结构,同时保持对长期依赖关系的建模能力。
# 在 GRU 中,隐藏状态(hidden state,通常记为h)不仅充当了记忆单元的角色,还负责传递和保存信息。相比于 LSTM,GRU 只有两个门控单元:重置门(reset gate)和更新门(update gate)。这两个门控单元控制着隐藏状态的更新和重置操作,从而实现对序列数据的建模。
# 重置门用于控制是否应该忽略先前的隐藏状态,而更新门则用于控制新的隐藏状态应该多大程度上保留先前的隐藏状态。这种设计使得 GRU 具有较少的参数量和计算复杂度,同时仍然能够有效地捕捉长期依赖关系。
# 因此,相对于 LSTM,GRU 在结构上更加简洁,不需要显式的细胞状态(c),而是仅仅依赖于隐藏状态(h)来传递和保存信息,从而在一定程度上减少了网络的复杂性。
if self.rnn_type == 'LSTM':
return (weight.new_zeros(self.nlayers, bsz, self.nhid),
weight.new_zeros(self.nlayers, bsz, self.nhid))
else:
return weight.new_zeros(self.nlayers, bsz, self.nhid)
# 这里只初始化了线性层decoder的参数
# 初始化模型参数的方法。这里对解码器的权重进行了均匀分布初始化,并将偏置初始化为零。
def init_weights(self):
initrange = 0.05
self.decoder.bias.data.zero_()
self.decoder.weight.data.uniform_(-initrange, initrange)
“data.py”
import csv
# 导入 CSV 模块,用于读取 CSV 文件。
import random
# 导入 random 模块,用于打乱数据。
import sys
# 导入sys模块,用于输出信息到标准错误流
def load_data(fileName): # 提示参数都要小写
rows = []
# row用于储存读取的csv文件的每一行数据
max_skill_num = 0
max_num_problems = 0
# 将数据集存储到列表中
with open(fileName, "r") as csvfile:
reader = csv.reader(csvfile, delimiter=',')
for row in reader:
rows.append(row)
index = 0
print("the number of rows is " + str(len(rows)), file=sys.stderr)
tuple_rows = []
# 用于储存转换后的数据,每三行数据组成一个三元组
# turn list to tuple 每3个row作为1个tuple
while index < len(rows)-1:
problems_num = int(rows[index][0])
# 得到问题数量,及每个tuple的第一行的数字
# 找出每个tuple第2行的最大数值作为最大技能编号
tmp_max_skill = max(map(int, rows[index+1]))
if tmp_max_skill > max_skill_num:
max_skill_num = tmp_max_skill
# 去除问题回答数量小于2的学生数据
if problems_num <= 2:
index += 3
else:
# 找出所有学生回答问题数量的最大值
# 通过找到所有学生回答问题数量的最大值,可以将所有学生的序列数据(如问题答题情况)进行统一长度的填充或截断,以便输入到模型中进行训练或测试。这样做的好处是保持数据的一致性,并且能够更有效地利用计算资源和模型训练速度。
if problems_num > max_num_problems:
max_num_problems = problems_num
# tup即每个学生的三元组
tup = (rows[index], rows[index+1], rows[index+2])
# tuple_rows即所有学生的三元组
tuple_rows.append(tup)
index += 3
# shuffle the tuple
random.shuffle(tuple_rows) # 打乱原元祖的顺序
# 也就是输出过滤后的学生数量
print("The number of students is ", len(tuple_rows), file=sys.stderr)
print("Finish reading data", file=sys.stderr)
# 技能编号从0开始 故返回技能个数需要+1
return tuple_rows, max_num_problems, max_skill_num+1
”main.py“
from typing import List
import sys
import torch
import torch.nn as nn
import argparse
# 解析命令行参数
import numpy as np
import time
# 用于计时
from data import load_data
from deep_knowledge_tracing_model import DeepKnowledgeTracing
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
from math import sqrt
from sklearn import metrics
# 导入了 sklearn 库中的评估指标,如均方误差(Mean Squared Error, MSE)、R平方值(R-squared)以及其他评估指标。
from sklearn.metrics import roc_auc_score, accuracy_score
# 导入了 sklearn 库中的 ROC AUC 和准确率评估指标。
# 我们很多时候,需要用到解析命令行参数的程序,例如在终端窗口输入(深度学习)训练的参数和选项。
parser = argparse.ArgumentParser(description='Deep Knowledge tracing model')
parser.add_argument('-epsilon', type=float, default=0.1, help='Epsilon value for Adam Optimizer')
parser.add_argument('-l2_lambda', type=float, default=0.3, help='Lambda for l2 loss')
parser.add_argument('-learning_rate', type=float, default=0.1, help='Learning rate')
parser.add_argument('-max_grad_norm', type=float, default=20, help='Clip gradients to this norm')
parser.add_argument('-keep_prob', type=float, default=0.6, help='Keep probability for dropout')
parser.add_argument('-hidden_layer_num', type=int, default=1, help='The number of hidden layers')
parser.add_argument('-hidden_size', type=int, default=200, help='The number of hidden nodes')
parser.add_argument('-evaluation_interval', type=int, default=5, help='Evalutaion and print result every x epochs')
parser.add_argument('-batch_size', type=int, default=32, help='Batch size for training')
parser.add_argument('-epochs', type=int, default=200, help='Number of epochs to train')
parser.add_argument('-allow_soft_placement', type=bool, default=True, help='Allow device soft device placement')
parser.add_argument('-log_device_placement', type=bool, default=False, help='Log placement ofops on devices')
parser.add_argument('-train_data_path', type=str, default='data/length/n_700/result_train.csv', help='Path to the training dataset')
parser.add_argument('-test_data_path', type=str, default='data/length/n_700/result_val.csv', help='Path to the testing dataset')
# 定义了一系列命令行参数,包括 epsilon 值、L2 正则化系数、学习率、梯度裁剪阈值、丢弃率、隐藏层数量、隐藏节点数量、评估间隔、批大小、迭代次数、数据集路径等。
args = parser.parse_args()
# 解析命令行参数
print(args, file=sys.stderr)
# 将信息输出到标准错误流的好处在于,它可以与标准输出流分开,这意味着即使程序的标准输出被重定向到文件或管道中,错误信息仍然会显示在控制台上。这有助于用户更容易地注意到可能导致程序出错或异常的问题。在这个特定的情况下,当程序执行时,它会将 args 对象的值输出到标准错误流,这样在程序出现错误时,用户就可以看到程序的参数配置,有助于调试和定位问题。
def add_gradient_noise(t, stddev=1e-3):#没用到
# 添加梯度噪声的函数,用于防止梯度爆炸。
"""
Adds gradient noise as described in http://arxiv.org/abs/1511.06807 [2].
The input Tensor `t` should be a gradient.
The output will be `t` + gaussian noise.
0.001 was said to be a good and fixed value for memory networks [2].
"""
# t是梯度张量
m = torch.zeros(t.size())
stddev = torch.full(t.size(), stddev)
# 均值为m,方差为stddev
gn = torch.normal(mean=m, std=stddev)
return torch.add(t, gn)
def compute_acc(all_target, all_pred):
# all_pred = np.concatenate(pred_list, axis=0)
#将 pred_list 中的所有元素按照指定的轴(axis)进行连接,生成一个新的 NumPy 数组 all_pred。 np.concatenate() 函数用于将数组沿着指定的轴连接起来。在这个例子中,axis=0 表示沿着第一个轴(通常是行) 进行连接,因此将 pred_list 中的所有数组按行连接起来,生成一个新的数组。
# all_target = np.concatenate(target_list, axis=0)
all_pred = np.array(all_pred)
# 概率分析,预测值>0.5就预测会做对,<0.5就预测会做错
all_pred[all_pred > 0.5] = 1.0
all_pred[all_pred <= 0.5] = 0.0
all_target = np.array(all_target)
return accuracy_score(all_target, all_pred)
def repackage_hidden(h):
"""Wraps hidden states in new Tensors, to detach them from their history."""
# 如果A网络的输出被喂给B网络作为输入,如果我们希望在梯度反传的时候只更新B中参数的值,
# 而不更新A中的参数值,这时候就可以使用detach()
if isinstance(h, torch.Tensor):
return h.detach()
else:
return tuple(repackage_hidden(v) for v in h) # 这个应该是h和c同在的时候, 就需要分开来进行摘下
def run_epoch(m, optimizer, students, batch_size, num_steps, num_skills, training=True, epoch=1):
"""Runs the model on the given data."""
# lr = args.learning_rate # learning rate
total_loss = 0
input_size = num_skills * 2
# start_time = time.time()
index = 0
actual_labels = [] # 真实值
pred_labels = [] # 预测值
hidden = m.init_hidden(batch_size)
count = 0
# 计算需要进行多少次batch
batch_num = len(students) // batch_size
# 最后一组的学生数量可能不到batch_size的大小,用小于号防止越界
all_loss, all_auc, all_acc, all_rmse, all_r2 = [], [], [], [], []
while index+batch_size < len(students):
x = np.zeros((batch_size, num_steps)) # 比如32*128
target_id: List[int] = []
target_correctness = []
# 对每一组batch进行操作
for i in range(batch_size):
# 可以看作是锁定每个3元组
student = students[index+i]
# 每个3元组的第二行-回答的技能编号
problem_ids = student[1]
# 每个3元组的第三行-回答的正误
correctness = student[2]
# a = torch.range(1, 6) a的tensor中是存在6的,也就是右区间是闭的
# 每一个学生
for j in range(len(problem_ids)-1):
problem_id = int(problem_ids[j])
# label_index = 0
if int(correctness[j]) == 0:
label_index = problem_id
else:
label_index = problem_id + num_skills
x[i, j] = label_index
# 问题编号是通过某种方式编码的标识符。在这段代码中,根据问题或技能的正确性,将其编号存储到 x 数组中。如果学生回答的问题或技能是正确的,则直接使用问题或技能的编号;如果是错误的,则使用问题或技能的编号加上技能数量(即 problem_id + num_skills)。这样的编码方式可能用于区分学生正确回答和错误回答的情况。
target_id.append(i*num_steps*num_skills+j*num_skills+int(problem_ids[j+1]))
#这行代码用于构建目标ID列表 target_id 的一部分。在一个特定的序列预测问题中,通常会有一个目标(或标签)与每个输入相对应,以便模型进行预测和训练。具体来说,这行代码的作用是将当前问题的目标 ID 添加到 target_id 列表中。假设 i 表示当前样本(学生)的索引,j 表示当前问题在学生答题序列中的位置。在这个问题中,i * num_steps * num_skills 用于定位到当前学生在目标 ID 列表中的起始位置,而 j * num_skills 用于定位到当前问题对应的起始位置。然后,int(problem_ids[j + 1]) 表示当前问题的下一个问题的编号(或者可以理解为当前问题的答案),通过加上这个编号就可以得到当前问题的目标 ID。这个过程是为了构建训练过程中的目标数据,以便在训练模型时使用。目标 ID 是模型在训练时需要预测的真实值,模型会根据输入数据预测这些目标值,然后通过计算预测值与真实值之间的损失来进行参数更新。
target_correctness.append(int(correctness[j+1]))
actual_labels.append(int(correctness[j+1]))
# 在这段代码中,x 是一个用于存储问题或技能编号的二维 NumPy 数组,用于构建模型的输入数据。具体来说,x 的形状是 (batch_size, num_steps),其中 batch_size 是批处理大小,num_steps 是问题或技能序列的长度。
# 每一行代表一个学生的问题或技能序列,每一列代表序列中的一个时间步(即学生回答问题的顺序)。数组中的每个元素表示学生在特定时间步回答的问题或技能的编号。
# 在这段代码中,通过循环遍历每个学生,以及每个学生回答的问题或技能,并将问题或技能的编号填充到 x 数组中的相应位置。对于每个学生的每个问题或技能,将其编号填充到数组的对应位置,以构建表示问题或技能序列的二维数组。
# x 数组用于存储学生的问题或技能序列的编号,是模型的输入数据之一,用于训练和测试深度知识追踪模型。
# index指向下一组batch
index += batch_size
count += 1
target_id = torch.tensor(target_id, dtype=torch.int64)
target_correctness = torch.tensor(target_correctness, dtype=torch.float)
# One Hot encoding input data [batch_size, num_steps, input_size]
# x代表每个学生回答每个问题的正误情况
x = torch.tensor(x, dtype=torch.int64)
# unsqueeze:扩充数据维度,在0起的指定位置N加上维数为一的维度,这里的x本来是两维,现在变为3维
x = torch.unsqueeze(x, 2)
input_data = torch.FloatTensor(batch_size, num_steps, input_size)
input_data.zero_()
# 在input_data的第三维找到x中所记录的位置,并将之修改为1,one-hot编码转换
input_data.scatter_(2, x, 1)
if training:
#在训练模式下,模型的目标是通过学习从输入到输出的映射关系,最小化损失函数,使得模型的预测 尽可能接近真实值。
#为了实现这一目标,我们通过计算损失函数来衡量模型预测与真实值之间的差异,并使用反向传播算 法来计算损失函数对模型参数的梯度.然后,利用这些梯度来更新模型的参数,以便不断优化模型,使 其在训练数据上的表现逐渐提升。
hidden = repackage_hidden(hidden)
# 将梯度初始化为零(因为一个batch的loss关于weight的导数是所有sample的loss关于weight的导数的累加和)
optimizer.zero_grad()
output, hidden = m(input_data, hidden)
# Get prediction results from output [batch_size, num_steps, num_skills]
# 将output转成1维
output = output.contiguous().view(-1)
# 从output的特定位置取值,具体位置由target_id给出
logits = torch.gather(output, 0, target_id)
# preds 使用sigmod函数
preds = torch.sigmoid(logits)
# 在浮点数结果上使用 .item() 函数可以提高显示精度
for p in preds:
pred_labels.append(p.item())
# criterion = nn.CrossEntropyLoss()
# sigmoid+BCEloss
# softmax+CrossEntropyLoss
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, target_correctness)
loss.backward()
# `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
# max_norm越小,裁剪的梯度越大,得到的梯度就越小,防止梯度爆炸的效果越明显
torch.nn.utils.clip_grad_norm_(m.parameters(), args.max_grad_norm)
optimizer.step()
# total_loss += loss.item()
all_loss.append(loss.item())
else:
# 后边的内容不进行计算图的构建,测试评估模型性能
with torch.no_grad():
m.eval()
output, hidden = m(input_data, hidden)
output = output.contiguous().view(-1)
logits = torch.gather(output, 0, target_id)
# preds
preds = torch.sigmoid(logits)
# pred_labels用于后面计算rmse
for p in preds:
pred_labels.append(p.item())
# criterion = nn.CrossEntropyLoss()
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, target_correctness)
# total_loss += loss.item()
all_loss.append(loss.item())
hidden = repackage_hidden(hidden)
# print pred_labels
rmse = sqrt(mean_squared_error(actual_labels, pred_labels))
all_rmse.append(rmse)
# 计算ROC
fpr, tpr, thresholds = metrics.roc_curve(actual_labels, pred_labels, pos_label=1)
# 计算AUC
auc = metrics.auc(fpr, tpr)
all_auc.append(auc)
# count指当前在第几个batch的计算,batch_num指的是一共有几个batch
# print("Epoch: {}, Batch {}/{} AUC: {}".format(epoch, count, batch_num, auc))
# calculate r^2
r2 = r2_score(actual_labels, pred_labels)
all_r2.append(r2)
acc = compute_acc(actual_labels, pred_labels)
all_acc.append(acc)
loss = sum(all_loss) / len(all_loss)
auc = sum(all_auc) / len(all_auc)
acc = sum(all_acc) / len(all_acc)
rmse = sum(all_rmse) / len(all_rmse)
r2 = sum(all_r2) / len(all_r2)
# return rmse, auc, r2, acc
return loss, auc, acc, rmse, r2
def main():
train_data_path = args.train_data_path
test_data_path = args.test_data_path
batch_size = args.batch_size
train_students, train_max_num_problems, train_max_skill_num = load_data(train_data_path)
num_steps = train_max_num_problems
num_skills = train_max_skill_num
num_layers = 1
test_students, test_max_num_problems, test_max_skill_num =load_data(test_data_path)
# num_skills*2是为了包含答对答错的信息
model = DeepKnowledgeTracing('LSTM', num_skills*2, args.hidden_size, num_skills, num_layers)
optimizer = torch.optim.Adam(model.parameters(), lr=args.learning_rate, eps=args.epsilon)
for i in range(args.epochs):
loss, auc, acc, rmse, r2 = run_epoch(model, optimizer, train_students, batch_size, num_steps, num_skills, epoch=i)
print(f"Training epoch {i}: loss {loss:.5f}, auc {auc:.5f}, acc {acc:.5f}, rmse {rmse:.5f}, r2 {r2:.5f}.", file=sys.stderr)
# Testing
if (i + 1) % args.evaluation_interval == 0:
ii = (i + 1) // args.evaluation_interval
loss, auc, acc, rmse, r2 = run_epoch(
model, optimizer, test_students, batch_size, num_steps, num_skills, training=False)
print(f"Testing epoch {ii}: loss {loss:.5f}, auc {auc:.5f}, acc {acc:.5f}, rmse {rmse:.5f}, r2 {r2:.5f}.", file=sys.stderr)
if __name__ == '__main__':
main()
数据形式
(题目信息)
{"RECORDS": [
{
"AnswerId": "690384",
"QuestionId": "440",
"UserId": "0",
"CorrectAnswer": "2",
"AnswerValue": "2",
"IsCorrect": "1",
"Context": "What is the name of the mathematical instrument used to construct circles?",
"Option_A": "Callipers",
"Option_B": "Compass",
"Option_C": "Protractor",
"Option_D": "Slide rule",
"GroupId": "216",
"QuizId": "85",
"SubjectId": "[3, 71, 83, 190]",
"DateAnswered": "2020-04-17 13:38:00.000"
},
{
"AnswerId": "1427450",
"QuestionId": "814",
"UserId": "0",
"CorrectAnswer": "4",
"AnswerValue": "4",
"IsCorrect": "1",
"Context": "Which of the following diagrams shows a diameter?",
"Option_A": "pic814_1_0",
"Option_B": "pic814_1_1",
"Option_C": "pic814_1_2",
"Option_D": "pic814_1_3",
"GroupId": "216",
"QuizId": "85",
"SubjectId": "[3, 71, 77, 184]",
"DateAnswered": "2020-04-17 13:39:00.000"
},
...
...
...
{
"AnswerId": "860053",
"QuestionId": "933",
"UserId": "0",
"CorrectAnswer": "2",
"AnswerValue": "2",
"IsCorrect": "1",
"Context": "Jo and Paul are arguing about the red line drawn on the circle to the right. Jo says it is a chord. Paul says it is an arc. Who is correct? pic933_0",
"Option_A": "Only Jo",
"Option_B": "Only Paul",
"Option_C": "Both Jo and Paul",
"Option_D": "Neither is correct",
"GroupId": "216",
"QuizId": "85",
"SubjectId": "[3, 71, 77, 184]",
"DateAnswered": "2020-04-17 13:40:00.000"
},
]
}
(学生答题情况)
27
13,16,13,14,16,14,16,19,19,19,19,10,13,120,120,120,10,13,120,120,120,10,13,10,13,10,13
0,0,0,0,1,0,1,1,1,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,1,1
26
56,78,6,6,6,6,6,2,2,11,11,11,11,11,11,30,35,30,30,30,30,35,11,11,70,70
1,1,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,0,1,1,1,1,1,0,0
46
45,45,45,45,45,45,45,45,89,89,89,89,89,89,89,45,45,45,45,45,45,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89,89
0,0,0,0,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,1,1,0,0
20
74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74
0,0,0,1,0,1,0,0,0,1,1,1,0,1,0,1,1,0,0,1
20
19,19,19,19,19,120,120,120,19,120,120,120,19,19,19,19,19,19,19,19
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
85
4,11,56,43,2,2,81,81,81,81,71,71,71,71,71,73,56,78,2,46,45,119,119,119,119,119,46,45,46,45,46,45,46,46,45,71,71,71,71,19,19,19,17,119,119,119,18,17,119,119,119,17,119,119,18,17,19,119,19,10,19,119,119,19,19,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119
1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,1,1,1,1,0,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1,0,0,1,1,1,1,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,1
16
0,14,0,14,0,14,0,14,0,14,0,14,0,16,0,14
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
5
33,38,38,38,38
1,1,1,0,1
...
...
...