lstm学习笔记(附mxnet源码及使用代码)

602 阅读12分钟
原文链接: zhuanlan.zhihu.com

写在前面:经过很长时间的磕磕碰碰,总算对lstm的细节搞得比较清楚,想写点笔记记录一下,以便以后自己复习和造福初学者,欢迎指正


本篇blog的纲要:

  • part-1 简单RNN
  • part-2 LSTM
    • part-2.1 LSTM的基本计算单元
    • part-2.2 LSTM的整过程
    • part-2.3 LSTM的门是什么
  • part-3 为什么需要LSTM
  • part-4 RNN的mxnet源码和使用代码
    • part-4.1 rnn的mxnet源码
    • part-4.2 rnn的mxnet使用代码
  • part-5 GRU
    • part-5.1 什么是GRU
    • part-5.2 为什么使用GRU
    • part-5.3 GRU的mxnet源码
  • part-6 参考资料

part-1 简单RNN:

首先简单介绍一下navie的RNN。

以最简单的对句子的分类的问题为例:

假设输入是一个句子:N={s1, s2, s3....sn},任务是对句子分类。不妨假设

n=5,即N={s1,s2,s3....s5}。也不妨假设N={I like chinese very much}

句子里的每一个单词,都会转为一个word embedding,即词向量表示,假设是128维。

每一个单词对应的位置,我们叫time_stamp,即时间戳。

RNN的基本单元如下:

对于其中的每一个组件的介绍如下:

  1. 其中x表示每个time_stamp的输入,假设目前是第三个词即Chinese:则x表示chinese的128维的词向量。
  2. h表示上一个time_stamp的隐状态。大家可以先不管它是怎么得到的,先知道他是一个向量,往下看自然就会慢慢明白了。不妨假设向量长度也是128。
  3. f表示一个函数(或者说一个黑盒子),表示可以由x和h得到y,即y=f(h,x)

对于 h^{'} 和y是怎么计算的可以参考下图:

其中计算 h^{'} 的输入有两个:x和h, W^{h}W^{i} 是两个矩阵:

  1. 一般 W^{i} 表示为 W^{i2h} ,i即input,h即hidden,即输入连接到隐藏层的参数矩阵;另外在计算完 W^{i2h} 和x的乘积后,一般会加上一个bias项,上图没表示出来
  2. 一般 W^{h} 表示为W^{h2h} ,即隐藏层连接到隐藏层的参数矩阵。这里初学者可能有疑问,可以先不用纠结,先记住h是一个向量, W^{h2h} 是一个矩阵即可。同理,在计算完 W^{h2h} 和h的乘积后,一般会加上一个bias项,上图没表示出来。

\sigma 表示激活函数,例如sigmoid或者tanh、relu等。

计算完 h^{'} 后,可以得到y,即当前时刻的输出:

  1. W^{o} 一般表示为 W^{h2o} ,即隐藏层连接到输出层的参数矩阵。

例如句子分类里的任务,有10类,y表示10个label节点的值。


至此,RNN的基本计算单元介绍完毕。RNN之所以叫Recurrent Neural Networks(循环神经网络),是因为它有循环结构。上面介绍了一个time_stamp的计算过程,那对于每一time_stamp,都会接收上一个time_stamp的隐藏层节点的输出,如下图:

公式化如下:

  1. 假设当前时刻为t。
  2. h^{t}=\sigma(W^{i2h}*x + bias^{i2h} + W^{h2h} *h^{t-1} + bias^{h2h})
  3. y^{t}=\sigma(W^{h2o}*h^{t} + bias^{h2o})

注意,不同time_stamp的 W^{i2h},W^{h2h},W^{h2o} 是共享的,整个模型只有一组 W^{i2h},W^{h2h},W^{h2o}

回到句子分类的问题,1到N-1时刻的y可以不计算不使用,只有最后一个的y需要计算,最后一个时刻的y就是句子的类别。

至此,RNN的基本原理已经介绍完毕。


part-2 LSTM

LSTM的全称是Long Short-Term Memory(长短时记忆),下面我门来看看他的基本计算单元。

part-2.1 LSTM的基本计算单元

lstm是一种特殊的rnn,他们的基本单元的差别看下图:

  • naive rnn的基本计算单元:
  • lstm的基本计算单元:

也就是和普通的RNN相比,每个time_stamp的计算单元,多了个叫 c^{t-1} 的输入和一个叫 c^{t} 的输出。另外, h^{t} 的计算过程也发现了一些变化,下面详细道来。

假设 当前时刻的输入 x^{t} 和上一时刻的隐状态输出 h^{t-1} concat到一起,然后通过四个参数矩阵,相乘后得到4个新的向量:

计算过程如下:

其中 W和W^{f}、W^{i}、W^{o} 是四个参数矩阵,我们把它们叫门(gate),可以先不管这个概念,继续往下看,只需要记住有这么四个参数矩阵即可。

然后经历一系列的复杂运算,得到 c^{t}h^{t}y^{t} :

  • c^{t}=z^{f}*c^{t-1}+z^{i}*z
  • h^{t}=z^{o}*tanh(c^{t})
  • y^{t}=\sigma(W^{'}*h^{t})

part-2.2 LSTM的整过程

好,lstm的基本计算单元讲完了,那么就可以如下图一样,把每一个time_stamp的计算单元串联起来:

简单来看就是下图的过程:

part-2.3 LSTM的门是什么

回顾一下上述所说的 C^{t}h^{t}y^{t} 的计算过程:

  • c^{t}=z^{f}*c^{t-1}+z^{i}*z :
    • c^{t} 我们叫细胞状态(states)
    • 其中 z^{f}遗忘门,决定我们会从细胞状态中丢弃什么信息。该门会读取 h^{t-1}x^{t} ,输出一个在 0 到 1 之间的数值给每个在细胞状态 c^{t-1} 用。1 表示“完全保留”,0 表示“完全舍弃”。 z^{f} 的计算过程如下:
    • z^{i}输入门z 我们叫新的候选值向量(即输入的一个变换,或者叫新的记忆内容)。 z^{i}和z 确定什么样的新信息被存放在细胞状态中。 z^{i}和z 的计算过程如下:
  • h^{t}=z^{o}*tanh(c^{t}) :
    • z^{o}输出门,确定细胞状态的哪个部分将输出出去。把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 输出门的输出相乘,输出我们确定输出的那部分。 z^{o} 的计算过程如下:
  • y^{t}=\sigma(W^{'}*h^{t})

part-3 为什么需要LSTM

  • 长期依赖
    • 例如要预测句子“我在中国出生,一直生活到14岁,14岁后搬去了加拿大,我的第一个母语是”的下一个词是什么(“中文”,不妨叫词A)。但是这样的话,需要连接到比较远的前面的“中国”(不妨叫词B)这个词。由于普通的RNN的是全部信息都往前传播,没有过滤和筛选,这样子容易导致前面的信息比较难传播到较远的地方(容易被离A更近的词“加拿大”占据上风)。
    • 但LSTM通过门操作,控制忘记过往不重要的信息(遗忘门)和让重要的输入信息更新细胞状态(输入门),和让重要的记忆信息传递到输出(输出门),来达到有用信息的筛选和不重要信息信息的过滤,所以LSTM能更好的解决长距离依赖的问题。
  • 缓解梯度消失(这部分有点复杂,可能存在理解不到位的地方):

可以参考知乎用户:为什么相比于RNN,LSTM在梯度消失上表现更好?,集思广益一下。

    • 从直观上理解, c^{t} 有一个直连边到下一时刻的 c^{t+1} ,思想类似resnet;另外, c^{t} 有两路输入(加法门,而不是乘法门),其中一路挂了,还有另一路补充
    • 数学上:
      • 普通RNN, h_{t+1}=\sigma(W_{i2h} * x_{t} + W_{h2h}*h^{t-1}+b) :
        • \frac{\partial(L)}{\partial(h_{t})}=\frac{\partial(L)}{\partial(h_{t+1})} * \frac{\partial(h_{t+1})}{\partial(h_{t})}=\frac{\partial(L)}{\partial(h_{t+1})}*\sigma^{'}*W_{h2h}
        • 其中 \sigma^{'} 是sigmoid的导数,最大值是0.25,所以k步后,有k个小于等于0.25的数连乘,所以容易梯度消失
      • LSTM, c_{t+1}=z^{f}*c_{t} + z_{i}*z :
        • \frac{\partial(L)}{\partial(c_{t})}=\frac{\partial(L)}{\partial(c_{t+1})}*(z^{f}+...) ,省略号里的东西可以先忽略;而 z^{f} 是一个sigmoid后的值,大部分会落在0.5左右,相对来说不那么容易梯度消失。 z^{f}=1 时,梯度可以比较好的导到上一时刻; z^{f}=0时,则上一时刻的信号影响不到当前时刻,梯度也不会回传回去。所以forget gate不仅影响到信号流通,也影响到梯度回传。
        • 但上述只是说明 c^{t} 这图路径不容易梯度消失,其他路径例如 h^{t} 还是有可能梯度消失的。

所以整体上,lstm只是相对来说不那么容易梯度消失,并不能完全避免。

part-4 RNN的mxnet源码和使用代码

part-4.1 rnn的mxnet源码:

比较简单,如果上述的原理已经看懂的话并且有一定的python代码基础的话,应该很容易看懂。要注意的是,mxnet的RNNCell里没有输出y,因此没有名字叫'h2o'的参数

class RNNCell(BaseRNNCell):
    """Simple recurrent neural network cell.

    Parameters
    ----------
    num_hidden : int
        Number of units in output symbol.
    activation : str or Symbol, default 'tanh'
        Type of activation function. Options are 'relu' and 'tanh'.
    prefix : str, default 'rnn_'
        Prefix for name of layers (and name of weight if params is None).
    params : RNNParams, default None
        Container for weight sharing between cells. Created if None.
    """
    def __init__(self, num_hidden, activation='tanh', prefix='rnn_', params=None):
        super(RNNCell, self).__init__(prefix=prefix, params=params)
        self._num_hidden = num_hidden
        self._activation = activation
        self._iW = self.params.get('i2h_weight')
        self._iB = self.params.get('i2h_bias')
        self._hW = self.params.get('h2h_weight')
        self._hB = self.params.get('h2h_bias')

    @property
    def state_info(self):
        return [{'shape': (0, self._num_hidden), '__layout__': 'NC'}]

    @property
    def _gate_names(self):
        return ('',)

    #传入的参数:inputs是一个time_stamp的输入,states是上一时刻的隐藏层状态
    def __call__(self, inputs, states):
        self._counter += 1
        name = '%st%d_'%(self._prefix, self._counter)
        i2h = symbol.FullyConnected(data=inputs, weight=self._iW, bias=self._iB,
                                    num_hidden=self._num_hidden,
                                    name='%si2h'%name)
        h2h = symbol.FullyConnected(data=states[0], weight=self._hW, bias=self._hB,
                                    num_hidden=self._num_hidden,
                                    name='%sh2h'%name)
        output = self._get_activation(i2h + h2h, self._activation,
                                      name='%sout'%name)
        #返回值:output是当前时刻的隐藏层的输出,states=[output] 
        return output, [output]


part-4.2 rnn的mxnet使用代码:

#encoding=utf-8
import mxnet as mx

#这里表示有3个batch,每个batch有一个time_stamp的输入;例如第一个time_stamp的的输入是词表中的第1个词,第二个time_stamp的输入是词表中的词2个词表
one_step_data_of_batch = mx.nd.array([0, 1, 0])

#表示词表的大小
input_dim = 2

#表示词向量的维度大小
embed_dim = 2

#将词向量初始化为值全是1的矩阵
emb_w = mx.nd.ones((2,2))

#隐藏层节点数是4,输入维度是2,所以i2h的维度是4*2; 初始化的值全都是1
rnn_i2h_weight = mx.nd.ones((4, 2))
rnn_i2h_bias = mx.nd.ones(4)

#隐藏层节点数是4,所以h2h的维度是4*4;初始化的值全都是1
rnn_h2h_weight = mx.nd.ones((4,4))
rnn_h2h_bias = mx.nd.ones(4)

step_input = mx.symbol.Variable('step_data')
#x = step_input.bind(mx.cpu(), {'step_data': one_step_data_of_batch})
#y = x.forward()
#print y

# First we embed our raw input data to be used as rnn's input.
embedded_step = mx.symbol.Embedding(data=step_input, input_dim=input_dim, output_dim=embed_dim)

# Then we create an rnn cell.
rnn_cell = mx.rnn.RNNCell(num_hidden=4, activation='relu')

# Initialize its hidden and memory states.
# 'begin_state' method takes an initialization function, and uses 'zeros' by default.
begin_state = rnn_cell.begin_state()


# Call the cell to get the output of one time step for a batch.
output, states = rnn_cell(embedded_step, begin_state)

print "============= output ==============:"
#hidden[0] = [0, 0, 0, 0]
#output[1][0] = Relu(hidden[0] * W_h2h + bias_h2h + input[1] * W_i2h + bias_i2h)
#          = Relu(0 + 1 + 1 * 1 * 2 + 1)
#           = 4
print output.bind(mx.cpu(), {'step_data': one_step_data_of_batch, 'embedding0_weight': emb_w,
    'rnn_i2h_weight':rnn_i2h_weight, 'rnn_i2h_bias': rnn_i2h_bias, 'rnn_h2h_weight': rnn_h2h_weight,
    'rnn_h2h_bias': rnn_h2h_bias}).forward()

print "states:"
print "len:", len(states)
#print states
print states[-1].bind(mx.cpu(), {'step_data': one_step_data_of_batch, 'embedding0_weight': emb_w,
    'rnn_i2h_weight':rnn_i2h_weight, 'rnn_i2h_bias': rnn_i2h_bias, 'rnn_h2h_weight': rnn_h2h_weight,
    'rnn_h2h_bias': rnn_h2h_bias}).forward()


print ""
#再传播一个time_stamp
output, states = rnn_cell(embedded_step, states)

print "============= output ==============2:"
#输入的值还是一样的
#hidden[0] = [4, 4, 4, 4]
#output[2][0] = Relu(hidden[1] * W_h2h + bias_h2h + input[2] * W_i2h + bias_i2h)
#          = Relu(4*1*4 + 1 + 1 * 1 * 2 + 1)
#           = 20
print output.bind(mx.cpu(), {'step_data': one_step_data_of_batch,'embedding0_weight': emb_w, 
    'rnn_i2h_weight':rnn_i2h_weight, 'rnn_i2h_bias': rnn_i2h_bias, 'rnn_h2h_weight': rnn_h2h_weight,
    'rnn_h2h_bias': rnn_h2h_bias}).forward()

part-4.3 lstm的mxnet源码

class LSTMCell(BaseRNNCell):
    """Long-Short Term Memory (LSTM) network cell.

    Parameters
    ----------
    num_hidden : int
        number of units in output symbol
    prefix : str, default 'lstm_'
        prefix for name of layers
        (and name of weight if params is None)
    params : RNNParams or None
        container for weight sharing between cells.
        created if None.
    forget_bias : bias added to forget gate, default 1.0.
        Jozefowicz et al. 2015 recommends setting this to 1.0
    """
    def __init__(self, num_hidden, prefix='lstm_', params=None, forget_bias=1.0):
        super(LSTMCell, self).__init__(prefix=prefix, params=params)

        self._num_hidden = num_hidden
        self._iW = self.params.get('i2h_weight')
        self._hW = self.params.get('h2h_weight')
        # we add the forget_bias to i2h_bias, this adds the bias to the forget gate activation
        self._iB = self.params.get('i2h_bias', init=init.LSTMBias(forget_bias=forget_bias))
        self._hB = self.params.get('h2h_bias')

    @property
    def state_info(self):
        return [{'shape': (0, self._num_hidden), '__layout__': 'NC'},
                {'shape': (0, self._num_hidden), '__layout__': 'NC'}]

    @property
    def _gate_names(self):
        return ['_i', '_f', '_c', '_o']

    def __call__(self, inputs, states):
        self._counter += 1
        name = '%st%d_'%(self._prefix, self._counter)
        #注意num_hidden=self._num_hidden*4,因为有四个门 
        i2h = symbol.FullyConnected(data=inputs, weight=self._iW, bias=self._iB,
                                    num_hidden=self._num_hidden*4,
                                    name='%si2h'%name)
        h2h = symbol.FullyConnected(data=states[0], weight=self._hW, bias=self._hB,
                                    num_hidden=self._num_hidden*4,
                                    name='%sh2h'%name)
        #相当于把input和上一time_stamp的h拼接到一起,然后通过四个矩阵转化为四个新的向量 
        #gates = i2h + h2h 
        gates = i2h + h2h
        #四个门,所以num_outputs=4
        slice_gates = symbol.SliceChannel(gates, num_outputs=4,
                                          name="%sslice"%name)
        in_gate = symbol.Activation(slice_gates[0], act_type="sigmoid",
                                    name='%si'%name)
        forget_gate = symbol.Activation(slice_gates[1], act_type="sigmoid",
                                        name='%sf'%name)
        in_transform = symbol.Activation(slice_gates[2], act_type="tanh",
                                         name='%sc'%name)
        out_gate = symbol.Activation(slice_gates[3], act_type="sigmoid",
                                     name='%so'%name)
        #根据前面的计算公式计算cell和h的值
        next_c = symbol._internal._plus(forget_gate * states[1], in_gate * in_transform,
                                        name='%sstate'%name)
        next_h = symbol._internal._mul(out_gate, symbol.Activation(next_c, act_type="tanh"),
                                       name='%sout'%name)

        return next_h, [next_h, next_c]

part-4.4 lstm的使用代码

#encoding=utf-8
import mxnet as mx

#one_step_data_of_batch = mx.nd.ones((3,2))
#one_step_data_of_batch = mx.nd.array([[0, 1], [0, 1], [1, 0]])
#one_step_data_of_batch = mx.nd.array([[[0], [1]], [[1], [0]]])
one_step_data_of_batch = mx.nd.array([0, 1, 0])

#表示词表的大小
input_dim = 2
#表示词向量的维度大小
embed_dim = 2
# Shape of 'step_data' is (batch_size,).
step_input = mx.symbol.Variable('step_data')
#将词向量初始化为值全是1的矩阵
emb_w = mx.nd.ones((2,2))

#隐藏层节点数是4,forget、input、output、tranform四个门,所以每个rnn_cell总共有16个节点,输入维度是2,所以i2h的维度是16*2; 初始化的值全都是1
lstm_i2h_weight = mx.nd.ones((16, 2))
lstm_i2h_bias = mx.nd.ones(16)

#hidden=4,每个rnn_cell有16个节点,所以h2h的size是16*4
lstm_h2h_weight = mx.nd.random.normal(-100, 100, shape=(16, 4))
lstm_h2h_bias = mx.nd.ones(16)

#x = step_input.bind(mx.cpu(), {'step_data': one_step_data_of_batch})
#y = x.forward()
#print y

# First we embed our raw input data to be used as LSTM's input.
embedded_step = mx.symbol.Embedding(data=step_input, input_dim=input_dim, output_dim=embed_dim)
#print "embedded_step:"
#print embedded_step.bind(mx.cpu(), {'step_data': one_step_data_of_batch, 'embedding0_weight':emb_w}).forward()
#print embedded_step.bind(mx.cpu(), {'step_data': one_step_data_of_batch}).forward()

lstm_cell = mx.rnn.LSTMCell(num_hidden=4)
# Initialize its hidden and memory states.
# 'begin_state' method takes an initialization function, and uses 'zeros' by default.
begin_state = lstm_cell.begin_state()


# Call the cell to get the output of one time step for a batch.
#传播一次,得到output和states
output, states = lstm_cell(embedded_step, begin_state)
#states = lstm_cell(embedded_step, begin_state)

print "============= output ==============:"
print output.bind(mx.cpu(), {'step_data': one_step_data_of_batch,'embedding0_weight': emb_w, 
    'lstm_i2h_weight':lstm_i2h_weight, 'lstm_i2h_bias': lstm_i2h_bias, 'lstm_h2h_weight': lstm_h2h_weight,
    'lstm_h2h_bias': lstm_h2h_bias}).forward()
#print output.bind(mx.cpu(), {'step_data': one_step_data_of_batch,'embedding0_weight': emb_w}).forward()

print "states:"
print "len:", len(states)
#print "len states[0]:", len(states[0])
#print states
print states[-1].bind(mx.cpu(), {'step_data': one_step_data_of_batch,'embedding0_weight': emb_w, 
    'lstm_i2h_weight':lstm_i2h_weight, 'lstm_i2h_bias': lstm_i2h_bias, 'lstm_h2h_weight': lstm_h2h_weight,
    'lstm_h2h_bias': lstm_h2h_bias}).forward()

part-5 GRU

part-5.1 什么是GRU

gru是LSTM的变种,它的外部看起来像普通的RNN,内部看起来像lstm。

他的基本计算结构图如下:

之所以说他的外部看起来像普通的RNN,是因为它和普通的RNN一样,没有c这个输入和输出(lstm有细胞状态c)。

GRU的内部计算单元如下:

隐层状态的计算公式如下:

h^{t}=(1-z^{t})*h^{'}+z^{t}*h^{t-1}

详细的计算过程如下:

1. r^{t}=\sigma(U_{r}x^{t}+W_{r}h^{t-1}+b_{r}) ,也就是 x^{t}和h^{t-1} 拼接到一起,经过一个矩阵乘法。

2. z^{t}=\sigma(U_{z}x^{t}+W_{z}h^{t-1}+b_{z})

3. h^{'}=tanh(U_{h}*x_{t}+W_{h}*(r^{t}*h^{t-1})+b_{h})

4. h^{t}=(1-z^{t})*h^{'}+z^{t}*h^{t-1}

5. y^{t}=W^{o}*h^{t}

part-5.2 为什么使用GRU

和LSTM类似,GRU使用门,使得容易建模长距离关系。和LSTM相比,GRU只用了三个矩阵,计算量少。另外,GRU的h也是使用加法门,回传梯度的时候,如果其中一路挂了(梯度为0),还有另一路补充,不容易梯度弥散。

GRU和LSTM的差异主要是没有使用输出门,和输入门的使用位置不一样(GRU中叫reset门)。

part-5.3 GRU的mxnet源码

#encoding=utf-8
class GRUCell(BaseRNNCell):
    """Gated Rectified Unit (GRU) network cell.
    Note: this is an implementation of the cuDNN version of GRUs
    (slight modification compared to Cho et al. 2014).

    Parameters
    ----------
    num_hidden : int
        number of units in output symbol
    prefix : str, default 'gru_'
        prefix for name of layers
        (and name of weight if params is None)
    params : RNNParams or None
        container for weight sharing between cells.
        created if None.
    """
    def __init__(self, num_hidden, prefix='gru_', params=None):
        super(GRUCell, self).__init__(prefix=prefix, params=params)
        self._num_hidden = num_hidden
        self._iW = self.params.get("i2h_weight")
        self._iB = self.params.get("i2h_bias")
        self._hW = self.params.get("h2h_weight")
        self._hB = self.params.get("h2h_bias")

    @property
    def state_info(self):
        return [{'shape': (0, self._num_hidden),
                 '__layout__': 'NC'}]

    @property
    def _gate_names(self):
        return ['_r', '_z', '_o']

    def __call__(self, inputs, states):
        # pylint: disable=too-many-locals
        self._counter += 1

        seq_idx = self._counter
        name = '%st%d_' % (self._prefix, seq_idx)
        prev_state_h = states[0]

        i2h = symbol.FullyConnected(data=inputs,
                                    weight=self._iW,
                                    bias=self._iB,
                                    num_hidden=self._num_hidden * 3,
                                    name="%s_i2h" % name)
        h2h = symbol.FullyConnected(data=prev_state_h,
                                    weight=self._hW,
                                    bias=self._hB,
                                    num_hidden=self._num_hidden * 3,
                                    name="%s_h2h" % name)
        #三个矩阵,输入分别是x和上一时刻的h
        i2h_r, i2h_z, i2h = symbol.SliceChannel(i2h, num_outputs=3, name="%s_i2h_slice" % name)
        h2h_r, h2h_z, h2h = symbol.SliceChannel(h2h, num_outputs=3, name="%s_h2h_slice" % name)

        reset_gate = symbol.Activation(i2h_r + h2h_r, act_type="sigmoid",
                                       name="%s_r_act" % name)
        update_gate = symbol.Activation(i2h_z + h2h_z, act_type="sigmoid",
                                        name="%s_z_act" % name)

        next_h_tmp = symbol.Activation(i2h + reset_gate * h2h, act_type="tanh",
                                       name="%s_h_act" % name)

        next_h = symbol._internal._plus((1. - update_gate) * next_h_tmp, update_gate * prev_state_h,
                                        name='%sout' % name)

        return next_h, [next_h]

part-6 参考资料:

  1. 台大李宏毅的课程(有 slide和video):Hung-yi Lee
    1. Basic Structures for Deep Learning Models: pdf, pptx, video (part 1), video (part 2) (2017/03/03)
    2. 强烈推荐这一节,本文绝大部分图都是来自该节课程的slide
  2. 理解lstm:
    1. [译] 理解 LSTM 网络
  3. YJango的循环神经网络——实现LSTM
  4. 《Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling》 https://arxiv.org/pdf/1412.3555.pdf
  5. 【干货】人人都能看懂的GRU