《Addressing Two Problems in Deep Knowledge Tracing via Prediction-Consistent Regularization》
1. 无法重构观察到的输入 :DKT有时候不能重构观察到的输入,因为模型的预测有时候是反常的。例如某个学生答对了一个问题 s i s_i si,该学生对该知识点 s i s_i si的掌握反而是下降的。
2. 预测状态的波动:随着时间的推移,预测的状态是波动的和不稳定的;而我们期待学生的状态是平稳渐变的。
因此,作者对DKT提出了下面的改进:
作者在文中首先重现了第一个问题:
之前的DKT模型是在ASSISTment 2009数据集上面训练的,上面的这个图表示随着训练的进行,某个学生的知识状态的变化情况。在垂直维度上的标签si对应于技能标签,并且只显示那些学生已经回答过的问题。水平维度上的标签指的是在每个时间步长输入到DKT的数据。热图的颜色表示该学生在下一个时间步骤中正确回答si的预测概率。颜色越深,答对的概率越高。
从上面这个图可以看出,当这个学生答错了 s 32 s_{32} s32之后,与前一个时间步长相比,正确回答 s 32 s_{32} s32的概率反而显著增加。这个问题的原因在于DKT模型使用了下面这个损失函数:
具体来说,这个损失函数只考虑下一个交互作用的预测性能,而不考虑当前交互作用的预测性能。因此,当输入序列 ( ( s 32 , 0 ) , ( s 33 , 0 ) ) ((s_{32}, 0), (s_{33}, 0)) ((s32,0),(s33,0))足够频繁时,DKT模型就会了解到,如果学生答错了 s 32 s_{32} s32,他很可能会答错 s 33 s_{33} s33,而不是答错 s 32 s_{32} s32。
但是也存在这样一种可能: s 32 s_{32} s32本来就是 s 33 s_{33} s33的先决条件:只有当DKT模型收到( s 33 s_{33} s33,0)时, s 32 s_{32} s32的预测性能更低,而当DKT模型收到( s 32 s_{32} s32,0)时, s 32 s_{32} s32的预测性能更高。我们假设,如果 s 32 s_{32} s32确实是 s 33 s_{33} s33的先决条件,那么当学生在当前时间步答错 s 32 s_{32} s32时,他/她在下一个时间步答错 s 33 s_{33} s33的可能性更大,反之则不然。为了验证这一假设,表1和表2列出了 s 32 s_{32} s32和 s 33 s_{33} s33按不同顺序连续出现时的频率计数。
从表1可以看出,如果学生在当前的时间步长答错了 s 32 s_{32} s32,那么在下一个时间步长答错 s 33 s_{33} s33的可能性更大。然而,从表2可以看出,如果学生答错了 s 33 s_{33} s33,那么在接下来的时间步中,他/她也更有可能答错 s 32 s_{32} s32。这意味着逆相依性也存在,与上述假设相矛盾,因此 s 32 s_{32} s32是 s 33 s_{33} s33的先决条件的说法就有问题了。这就意味着DKT模型确实存在上面谈到的问题1.
作者认为DKT模型的损失函数只考虑下一个相互作用(t+1时刻)的预测性能,而忽略当前交互(t时刻)的性能(例如在上面那个蓝色的图中:DKT模型只在乎下一时刻能够准确预测 s 33 s_{33} s33会答错,而不在乎当前时刻能否准确预测 s 32 s_{32} s32,也就是说,当前学生的知识状态实际上是不准确的)。于是作者就重新构造了下面的正则化项:
该正则化项使得模型考虑了当前时刻学生的知识状态。
第二个问题是学生预测知识状态的波浪式转变。该问题可能与RNN的隐藏状态表示有关。隐藏状态 h t h_t ht由之前的隐藏状态 h t − 1 h_{t-1} ht−1和当前输入 x t x_t xt决定。将学生对所有习题的潜在知识状态归纳为一个单一的隐含层。作者认为通过在输出层上正则化来限制隐含状态表示使其更加不变量是可行的。因此定义了两个波动度量 w 1 w_1 w1和 w 2 w_2 w2作为正则化项来平滑预测中的过渡:
最终的损失函数如下:
其中,三个 λ \lambda λ是正则化参数。
另外,作者实现了文中的DKT+模型:github,其中的一些重要的代码如下:
def _create_loss(self):
print("Creating Loss...")
last_layer_size = self.hidden_layer_structure[-1]
last_layer_outputs = self.hidden_layers_outputs[-1]
# Output Layer Construction
with tf.variable_scope("output_layer", reuse=tf.get_variable_scope().reuse):
W_yh = tf.get_variable("weights", shape=[last_layer_size, self.num_problems],
initializer=tf.random_normal_initializer(stddev=1.0 / np.sqrt(self.num_problems)))
b_yh = tf.get_variable("biases", shape=[self.num_problems, ],
initializer=tf.random_normal_initializer(stddev=1.0 / np.sqrt(self.num_problems)))
# Flatten the last layer output
num_steps = tf.shape(last_layer_outputs)[1]
self.outputs_flat = tf.reshape(last_layer_outputs, shape=[-1, last_layer_size])
self.logits_flat = tf.matmul(self.outputs_flat, W_yh) + b_yh
self.logits = tf.reshape(self.logits_flat, shape=[-1, num_steps, self.num_problems])
self.preds = tf.sigmoid(self.logits)
# self.preds_flat = tf.sigmoid(self.logits_flat)
# y_seq_flat = tf.cast(tf.reshape(self.y_seq, [-1, self.num_problems]), dtype=tf.float32)
# y_corr_flat = tf.cast(tf.reshape(self.y_corr, [-1, self.num_problems]), dtype=tf.float32)
# Filter out the target indices as follow:
# Get the indices where y_seq_flat are not equal to 0, where the indices
# implies that a student has answered the question in the time step and
# thereby exclude those time step that the student hasn't answered.
target_indices = tf.where(tf.not_equal(self.y_seq, 0))
self.target_logits = tf.gather_nd(self.logits, target_indices)
self.target_preds = tf.gather_nd(self.preds, target_indices) # needed to return AUC
self.target_labels = tf.gather_nd(self.y_corr, target_indices)
self.cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.target_logits,
labels=self.target_labels)
self.loss = tf.reduce_mean(self.cross_entropy)
# add current performance into consideration
current_seq = self.X[:,:,:self.num_problems] # slice out the answering exercise
current_corr = self.X[:,:,self.num_problems:]
self.target_indices_current = tf.where(tf.not_equal(current_seq, 0))
self.target_logits_current = tf.gather_nd(self.logits, self.target_indices_current)
self.target_preds_current = tf.gather_nd(self.preds, self.target_indices_current) # needed to return AUC
self.target_labels_current = tf.gather_nd(current_corr, self.target_indices_current)
self.cross_entropy_current = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.target_logits_current,
labels=self.target_labels_current)
# 这里就是正则化项r
self.loss += self.lambda_o * tf.reduce_mean(self.cross_entropy_current)
# Regularize the model to smoothen the network.
mask = length(self.y_seq)
self.total_num_steps = tf.reduce_sum(tf.cast(mask, tf.float32))
# l1-norm
# waviness_norm_l1 = tf.norm(self.preds[:, 1:, :] - self.preds[:, :-1, :], ord=1)
waviness_norm_l1 = tf.abs(self.preds[:, 1:, :] - self.preds[:, :-1, :])
self.waviness_l1 = tf.reduce_sum(waviness_norm_l1) / self.total_num_steps / self.num_problems
# 这里就是正则化项w1
self.loss += self.lambda_w1 * self.waviness_l1
# l2-norm
# waviness_norm_l2 = tf.norm(self.preds[:, 1:, :] - self.preds[:, :-1, :], ord=2)
waviness_norm_l2 = tf.square(self.preds[:, 1:, :] - self.preds[:, :-1, :])
self.waviness_l2 = tf.reduce_sum(waviness_norm_l2) / self.total_num_steps / self.num_problems
# 这里就是正则化项w2
self.loss += self.lambda_w2 * self.waviness_l2