RNN建模时间序列

RNN建模时间序列

RNN简介

下图是最简单的RNN。 x 为输入序列,我们需要将他展开为 x_0, x_1, ... x_t,因为实际应用中序列是有限长的。展开的过程即下图的左边到右边。

RNN
图自WILDML

图中 s_t 为隐含层,可以认为是记忆单元,即 s_t 概括了 x_0x_{t-1} 的信息,利用 x_t, s_t 可以预测输出 o_t. 训练时需要使用o_t 与真实的输出做比较,即产生一个误差,最小化这个误差就是对这个RNN的训练过程。具体可以参考网友的介绍
可以用数学公式来描述这个过程。

    \[s_t=f(Ux_t + Ws_{t-1})\]

    \[o_t = \mathrm{softmax}(Vs_t)\]

    \[cost = \sum_t f_{err}(o_t, \hat o_t)\]

其中f 为激活函数,可以是sigmoid或者tanh等其他类型的函数。Wiki上有详细的介绍。f_{err} 可以是Cross entropy(分类常用的误差函数),也可以是简单的RMSE,根据不同的情况选择不同的误差函数,只要可导(可次导)就行。

开导的目标函数就可以使用一些库的自动求导功能进行求解。如Theano可以直接进行符号求导,可以节省很多代码以及bug。

LSTM原理

RNN有个致命的缺点就是对长序列的建模效果不好,可以认为RNN记忆单元不行。因此 Hochreiter & Schmidhuber (1997) 提出了新的模型LSTM(Long short-term memory), 如下图.

 

LSTM
图自colah.github.io

网上有很多资源对其进行了详细的描述:

  1. Understanding LSTM Networks
  2. Implementing a GRU/LSTM RNN with Python and Theano

我简单说下, LSTM的单个单元的操作如下:

    \begin{align*} i &=\sigma(x_tU^i + s_{t-1} W^i)\\ f &=\sigma(x_t U^f +s_{t-1} W^f) \\ o &=\sigma(x_t U^o + s_{t-1} W^o) \\ g &=\tanh(x_t U^g + s_{t-1}W^g) \\ c_t &= c_{t-1} \circ f + g \circ i \\ s_t &=\tanh(c_t) \circ o \end{align*}

同样这复杂的操作通过程序的自动求导就很简单,可以看这个例子

基本与RNN类似,增加了记忆单元 c 和遗忘门f。 原理就是在RNN的基础上,利用记忆单元来更好的对长序列进行概括(记忆)。

除LSTM还有很多变种,如GRU, LSTM with peephole connections等,这篇文章做了一些比较。

RNN应用

RNN主要用于序列建模,有几种模型,如下图。

 

char-model
图自karpathy.github.io

 


时间序列建模

这是别的实验室一个项目的测试,数据集不好放出来,而且由于数据实在太少,效果比较一般,他目的是根据已有的数据,预测下一周的cpu busy和cpu context switches。由于是预测未来,我们需要对过去进行总结。RNN就是做这样一件事情:对过去出现过的模式进行概括记忆,看看未来会不会再次出现并预测。问题就是怎么通过RNN预测什么。这里没有代码,写得不清楚,可以看另一篇博文,神经网络近似函数,有模型有代码。

模型

在我实现的模型中,我利用0分钟到n分的数据 [x_0, x_1, ... x_n] 预测下一个时间段的数据如 [x_s, x_{s+1},...x_{s+n}], 是一个many to many的模型。在我测试的模型中,我选取n=288 即一整天的数据,s=1 即下一个5分钟。可以表示为:
目标: [x_1, x_2, x_3, ..., x_{1+n}]
输入: [x_0, x_1, x_2, ... , x_n]
但这样的模型在实际应用中是不行的,每次输入只有一个数据点,在测试时会导致很大的偏差。我采取的办法是将m 个时间点叠加,将输入变为

    \begin{align*} x_0, \quad x_{36}, \quad x_{72}, .... \\ x_1, \quad x_{37}, \quad x_{73}, .... \\ x_2, \quad x_{38}, \quad x_{74}, ... \\ ..., \quad ..., \quad ....\\ x_{m}, \quad x_{m+36},x_{m+72} \end{align*}

注意纵向为特征,即 x_0, x_1, ..x_{m} 为一个特征。36的间隔也可以选择合适的,我随便选了一个。
对应输出就变为m 长的向量(m 为 feature_length),也可以输出一个单一的预测值 x_{m+1}。我认为在测试时,不知道未来数据的情况下,使用向量更合理。如下:

    \begin{align*} x_1, \quad x_{37}, \quad x_{73}, .... \\ x_2, \quad x_{38}, \quad x_{74}, ....\\ x_3, \quad x_{39}, \quad x_{75}, ...\\ ..., \quad ..., \quad ....\\ x_{m+1}, \quad x_{m+37},x_{m+73} \end{align*}

简单的说,就是单步我要从[x_0, x_1, x_2, ... , x_m] 预测 [x_1, x_2, x_3, ..., x_{1+m}],注意第一个模型的不同之处。我训练时我一次输入几组就是seq_length。
feature_length 越长就越可以防止抖动,但是对局部细节刻画就比较难。
seq_length越长记忆的越多,可能导致过拟合,也会导致训练的困难以及数据量太小了。太短抓不住特征。

实验平台

  • 操作系统:Linux
  • 深度学习库:Theano + Blocks, 可以使用以下命令安装

pip install git+git://github.com/mila-udem/blocks.git \
-r https://raw.githubusercontent.com/mila-udem/blocks/master/requirements.txt

实验效果

在cpu context switches表现较好。我选取CPU1, CPU7, CPU8进行测试。其实都差不多, 需要调参,没有足够时间。第7周数据在训练的时候是不用的,只用前5周数据量还是太小。我用第6周的数据预测第7周。下图中蓝线代表真实数据,绿色部分是用第六周的数据一个个预测但使用真实值代替下次输入的预测值。红色部分完全使用预测值。

CPU Context Switches

CPU7: rmse: 0.06

Screenshot_20160526_232717

CPU Busy

CPU7: rmse: 0.148

Screenshot_20160526_232739