Seq2Seq的建模解释和Keras中Simple RNN Cell的计算及其代码示例

标签:#Keras##RNN##Seq2Seq##SimpleRNNCell##深度学习# 时间:2020/07/12 21:25:13 作者:小木

RNN的应用有很多,尤其是两个RNN组成的Seq2Seq结构,在时序预测、自然语言处理等方面有很大的用处,而每个RNN中一个节点是一个Cell,它是RNN中的基本结构。本文从如何使用RNN建模数据开始,重点解释RNN中Cell的结构,以及Keras中Cell相关的输入输出及其维度。我已经尽量解释了每个变量,但可能也有忽略,因此可能对RNN之前有一定了解的人会更友好,本文最主要的目的是描述Keras中RNNcell的参数以及输入输出的两个注意点。如有问题也欢迎指出,我会进行修改。

本文相关的Github Project地址:Github:deep_learning_with_python

代码均已上传。

关于RNN模型的更加理论的解释,可以参考之前的文章:深度学习之RNN模型

[TOC]

一、基于Seq2Seq来预测时序数据

我们知道多个RNN Cell连接就可以称为一个RNN模型,两个RNN相连接,可以组成一个Seq2Seq模型,即第一个RNN一般称为Encoder,第二个RNN称为Decoder,第一个RNN的最后一个输出作为第二个RNN的输入,这种Seq2Seq的结构在实际中有很多应用,包括时序数据预测、自然语言处理等。以时序数据为例,Encoder建模对象是过去N天的已知数据,如过去30天某个APP的流量,而Decoder建模对象就是未来M天的未知数据,如我们需要预测的未来7天某个APP的流量。

原理也很简单,就是说我们要用过去的数据预测未来,但是直接将过去的数据作为输入的话,形成回归或者分类模型的话,那么过去N天之间的关联关系我们就没有用到。例如,假如我们从历史数据中抽取了如下序列,来训练模型,预测未来:

序列ID 历史第1天 历史第30天 未来第1天 未来第7天
SEQ10000 13 14 13 14
SEQ30000 121 156 167 178

这种数据传统回归模型并不好用,一方面历史数据之间的关联关系没有利用,另一方面基于这种可以说是多分类的模型去预测也准确性很低。

利用Seq2Seq模型则比较适合,我们把过去30天当作一个RNN模型,即Seq2Seq的Encoder,Encoder中每一个Cell都对历史的一天建模,且都使用前一天的输出来构建后一个Cell的输入,那么最后一个Cell的输出也就包含了历史的信息(由于本文仅关注原理,因此这里不讨论过长的序列导致模型训练困难等其它缺点)。再使用这个Encoder输出作为Decoder阶段的输入来预测未来,那么就避免了上述问题。

二、RNN中的Cell如何对数据建模

那么一个Cell如何对数据建模呢?以上述时序数据预测为例,那么一个Encoder中的Cell就对应历史某一天。这时候Cell的输入数据就是两个,一个是前一天Cell的状态输出,另一个是当前Cell的特征输入,特征就是今天时序特征,通常,在时序数据预测中,某一个时间点的特征就是如一年的第几天,第几个季度,7天前流量值等等,这些都可以是特征。

三、Simple RNN Cell的建模理论数学说明

下图是一个Simple RNN Cell的图示:


如前所述,一个Cell的输入包括两个:

一个是前一阶段的状态,即上图中a^{(0)},初始的第一个Cell的前一阶段的状态一般来说可以随机生成。

另一个是当前Cell的特征输入,即上图的x^{(1)}。也就是前面说的当前时间点的一些特征,如这是一年的第几天、第几个季度、是否是节假日、七天前的流量是多少、全年今天的流量是多少等等。

输出也是两个:一个是当前Cell的输出y^{(1)},另一个是下一阶段的状态,即a^{(1)}

维度相关

以一个时间点的数据为例,如果当前数据特征维度是K,前一个状态a^{(0)}的维度是L,那么输出状态a^{(1)}的状态依然是L,输出y^{1}的维度是1,当然Encoder阶段我们可能不太在意这个y输出,而Decoder阶段的y输出就是我们预测的结果了。

四、Keras中SimpleRNNCell的输入输出和参数

Keras中SimpleRNNCell的参数很多,包括权重参数如何初始化,是否添加bias,激活函数是啥等等。这些都是常见的参数,就不解释了,其中一个参数是units,这个定义容易混淆,原先我也以为这个unit是叠加多层RNN的输出,最后发现其实这个参数是隐状态的维度,也就是前面的a^{(0)}a^{(1)}的维度。

SimpleRNNCell在创建的时候指定units就可以了,而使用的时候,它有两个输入参数,一个是当前阶段的特征输入,即input_x,另一个是前一个状态previous_states,注意,这里也有个坑,由于RNNCell的种类很多,包括后面的GRUCell或者是LSTMCell,这些更加复杂的Cell状态都不止一个,因此在代码中,Keras的状态并不单纯是一个state,它其实是一个列表。

我们看如下代码

# 我们先定义一个cell,units=1,也就是隐状态的维度是1
rnn_cell = tf.keras.layers.SimpleRNNCell(1, activation=None, kernel_initializer=tf.keras.initializers.Zeros())

# 我们随机生成一个当前阶段的特征和前一阶段的状态
input_x = tf.convert_to_tensor(np.asarray([[1, 1, 1], [2, 2, 2]]).astype(np.float32))

# 注意,这里state的维度是[1, 2, 1],因为Keras取前一阶段的状态会根据不同的Cell取不同的数量,如果是SimpleRNNCell,那么取第一个,如果是GRU,那么接着取,所以这里的第一个维度实际是第几种state,由于我们这里测试的是SimpleRNNCell,只有一个状态,那么用这个维度即可
previous_states = tf.convert_to_tensor(np.asarray([[[0.1], [0.1]]]).astype(np.float32))

# 输出测试
rnn_output, next_states = rnn_cell(input_x, previous_states)

init = tf.compat.v1.global_variables_initializer()
with tf.compat.v1.Session() as sess:
    sess.run(init)
    rnn_output, next_states = sess.run([rnn_output, next_states])
    print(rnn_output)
    print(next_states)

注意,这里state的维度是[1, 2, 1],因为Keras取前一阶段的状态会根据不同的Cell取不同的数量,如果是SimpleRNNCell,那么取第一个,如果是GRU,那么接着取,所以这里的第一个维度实际是第几种state,由于我们这里测试的是SimpleRNNCell,只有一个状态,那么用这个维度即可。之前测试过[2,1],结果报错,也就是这个原因。输出也是类似,输出的第一个是当前阶段的结果和状态,对应前面的y^{1}a^{(1)}。注意a^{(1)}是一个列表。这里列表中只有一个状态,该状态的维度是[2,1],和我们自己定义的previous_states变量都是一个意思。

当然,如果我们把units设置成其它数值如5,那么我们的previous_states构造就必须是一个列表,列表中有一个变量,其维度是[5,1],当然也可以构造成我们的[1,5,1]形式的变量。

欢迎大家关注DataLearner官方微信,接受最新的AI技术推送