0%

Deep Knowledge Tracing

学情跟踪是一个比较典型的时间序列的数据分析和预测。其目的是根据之前的学习情况,对学生的学习情况进行建模。虽然这里描述的是学情跟踪建模,但是,接下来描述的算法同样适用于其它的类似的场景,例如股票涨跌情况预测。
在这一类应用场景中,与其它应用最重要的一个区别是,该类应用需要预测每一个输出位置的概率。例如,需要预测每一只股票的涨跌。而其它场景需要预测的一般都是 one hot 类型。

这里使用的是 LSTM, 具体的 LSTM 是什么或者 RNN 是什么不是这篇的目的。
如下图左,输出节点使用 softmax 做映射,结果就是所有输出的和为 1. 如果输出节点记为 \(O_0,\cdots, O_{n-1}\), \(\sum_{i=0}^{n-1}p\left(O_i\right)=1\). 下图右即为这里要讨论的情形,在每一个输出节点上分别使用 sigmoid 做映射,即对于任意一个输出节点,\(p(O_i=0)+p(O_i=1)=1\).

既然输出变了,那么,相应的 loss function 也会随之改变,就不能用常见的 softmax 的 cross entropy loss 了。在这里,使用的 sigmoid 二分类的 loss function. 具体的方法是有一点点小 trick 的。
另外,要解释一下输入数据,我们拿到的数据学生历史做题对错情况,那么对于输入,一个比较简单的编码就是取一个向量,假设共有 N 各题目,那么,向量的长度为 2N, 如果,在一个 timestep 上,表现为正 (做对题,股票涨等), 该向量的 第 i 各位置为 1, 其余为 0, 否则,第 i+N 位置为 1, 其余为 0.
输出节点个数是 N, 每一个节点表示的是这道题目做对的概率。

损失函数是这个工作中最最重要的地方,在每一次 forward 之后,会在相应的输出打上 mask, 例如,输入是第 i 只股票,那么,只看第 i 各输出节点,计算其 loss, 其它所有的节点都不看。如下图,第一次输入的是第 3 各学生的做题情况,那么,只激活第 3 各输出节点,第二次输入的是第 2 个学生的做题情况,那么,只激活第 2 个输出节点。

\[ L = \sum_t l\left(y^T \sigma\left(q_{t+1}\right), a_{t+1}\right) \]

其中,\(y^T\) 就是上面说到的 mask.
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import tensorflow as tf
x = tf.placeholder(tf.float32, [None, n_steps, n_input])
y = tf.placeholder(tf.float32, [None, n_steps])
skill = tf.placeholder(tf.float32, [None, n_steps, n_classes])
seqlens = tf.placeholder(tf.int32, [None, ])
keep_prob = tf.placeholder(tf.float32)
weights = tf.Variable(tf.random_normal([n_hidden, n_classes]))
weights = tf.get_variable("weights", shape=[n_hidden, n_classes], initializer=tf.contrib.layers.variance_scaling_initializer(factor=2.0, mode="FAN_IN"))
biases = tf.Variable(tf.zeros([n_classes]))
def dynamicRNN(x, n_steps):
# data input shape (batch_size, n_steps, n_input)
# convert to n_steps tensors list of shape(batch_size, n_input)
x = tf.transpose(x, [1, 0, 2])
# x = tf.sparse_tensor_to_dense(x)
x = tf.reshape(x, [-1, n_input])
x = tf.split(0, n_steps, x)
lstm_cell = tf.nn.rnn_cell.GRUCell(n_hidden)
lstm_cell = tf.nn.rnn_cell.DropoutWrapper(
lstm_cell, output_keep_prob=keep_prob)
outputs, states = tf.nn.rnn(
lstm_cell, x, dtype=tf.float32, sequence_length=seqlens)
outputs = tf.pack(outputs)
# (batch_size, n_step, n_output)
outputs = tf.transpose(outputs, [1, 0, 2])
batch_size = tf.shape(outputs)[0]
# (batch_size*n_step, n_output)
outputs = tf.reshape(outputs, [-1, n_hidden])
outputs = tf.matmul(outputs, weights)+biases
outputs = tf.reshape(outputs, [batch_size, n_steps, n_classes])
# 给输出打上 mask, 获取目标输出
outputs = tf.reduce_sum(skill*outputs, axis=2)
loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(outputs, y))
# accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.round(tf.sigmoid(outputs)), y), tf.float32))
predict = (tf.sigmoid(outputs))
# learning_rate = tf.train.exponential_decay(learning_rate=0.1, global_step=tf.Variable(0, trainable=False), decay_steps=3578, decay_rate=0.96, staircase=True)
optimizer = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)
return optimizer, loss, predict, outputs