写在前面:经过很长时间的磕磕碰碰,总算对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的基本单元如下:
对于其中的每一个组件的介绍如下:
- 其中x表示每个time_stamp的输入,假设目前是第三个词即Chinese:则x表示chinese的128维的词向量。
- h表示上一个time_stamp的隐状态。大家可以先不管它是怎么得到的,先知道他是一个向量,往下看自然就会慢慢明白了。不妨假设向量长度也是128。
- f表示一个函数(或者说一个黑盒子),表示可以由x和h得到y,即y=f(h,x)
对于 和y是怎么计算的可以参考下图:
其中计算 的输入有两个:x和h,
和
是两个矩阵:
- 一般
表示为
,i即input,h即hidden,即输入连接到隐藏层的参数矩阵;另外在计算完
和x的乘积后,一般会加上一个bias项,上图没表示出来
- 一般
表示为
,即隐藏层连接到隐藏层的参数矩阵。这里初学者可能有疑问,可以先不用纠结,先记住h是一个向量,
是一个矩阵即可。同理,在计算完
和h的乘积后,一般会加上一个bias项,上图没表示出来。
表示激活函数,例如sigmoid或者tanh、relu等。
计算完 后,可以得到y,即当前时刻的输出:
一般表示为
,即隐藏层连接到输出层的参数矩阵。
例如句子分类里的任务,有10类,y表示10个label节点的值。
至此,RNN的基本计算单元介绍完毕。RNN之所以叫Recurrent Neural Networks(循环神经网络),是因为它有循环结构。上面介绍了一个time_stamp的计算过程,那对于每一time_stamp,都会接收上一个time_stamp的隐藏层节点的输出,如下图:
公式化如下:
- 假设当前时刻为t。
注意,不同time_stamp的 是共享的,整个模型只有一组
。
回到句子分类的问题,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的计算单元,多了个叫 的输入和一个叫
的输出。另外,
的计算过程也发现了一些变化,下面详细道来。
假设 当前时刻的输入 和上一时刻的隐状态输出
concat到一起,然后通过四个参数矩阵,相乘后得到4个新的向量:
计算过程如下:
其中 是四个参数矩阵,我们把它们叫门(gate),可以先不管门这个概念,继续往下看,只需要记住有这么四个参数矩阵即可。
然后经历一系列的复杂运算,得到 和
、
:
part-2.2 LSTM的整过程
好,lstm的基本计算单元讲完了,那么就可以如下图一样,把每一个time_stamp的计算单元串联起来:
简单来看就是下图的过程:
part-2.3 LSTM的门是什么
回顾一下上述所说的 和
、
的计算过程:
:
我们叫细胞状态(states)
- 其中
叫遗忘门,决定我们会从细胞状态中丢弃什么信息。该门会读取
和
,输出一个在 0 到 1 之间的数值给每个在细胞状态
用。1 表示“完全保留”,0 表示“完全舍弃”。
的计算过程如下:
叫输入门,
我们叫新的候选值向量(即输入的一个变换,或者叫新的记忆内容)。
确定什么样的新信息被存放在细胞状态中。
的计算过程如下:
:
叫输出门,确定细胞状态的哪个部分将输出出去。把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 输出门的输出相乘,输出我们确定输出的那部分。
的计算过程如下:
part-3 为什么需要LSTM
- 长期依赖:
- 例如要预测句子“我在中国出生,一直生活到14岁,14岁后搬去了加拿大,我的第一个母语是”的下一个词是什么(“中文”,不妨叫词A)。但是这样的话,需要连接到比较远的前面的“中国”(不妨叫词B)这个词。由于普通的RNN的是全部信息都往前传播,没有过滤和筛选,这样子容易导致前面的信息比较难传播到较远的地方(容易被离A更近的词“加拿大”占据上风)。
- 但LSTM通过门操作,控制忘记过往不重要的信息(遗忘门)和让重要的输入信息更新细胞状态(输入门),和让重要的记忆信息传递到输出(输出门),来达到有用信息的筛选和不重要信息信息的过滤,所以LSTM能更好的解决长距离依赖的问题。
- 缓解梯度消失(这部分有点复杂,可能存在理解不到位的地方):
可以参考知乎用户:为什么相比于RNN,LSTM在梯度消失上表现更好?,集思广益一下。
- 从直观上理解,
有一个直连边到下一时刻的
,思想类似resnet;另外,
有两路输入(加法门,而不是乘法门),其中一路挂了,还有另一路补充
- 数学上:
- 普通RNN,
:
- 其中
是sigmoid的导数,最大值是0.25,所以k步后,有k个小于等于0.25的数连乘,所以容易梯度消失
- LSTM,
:
,省略号里的东西可以先忽略;而
是一个sigmoid后的值,大部分会落在0.5左右,相对来说不那么容易梯度消失。
时,梯度可以比较好的导到上一时刻;
时,则上一时刻的信号影响不到当前时刻,梯度也不会回传回去。所以forget gate不仅影响到信号流通,也影响到梯度回传。
- 但上述只是说明
这图路径不容易梯度消失,其他路径例如
还是有可能梯度消失的。
所以整体上,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的内部计算单元如下:
隐层状态的计算公式如下:
详细的计算过程如下:
1. ,也就是
拼接到一起,经过一个矩阵乘法。
2.
3.
4.
5.
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 参考资料:
- 台大李宏毅的课程(有 slide和video):Hung-yi Lee
- Basic Structures for Deep Learning Models: pdf, pptx, video (part 1), video (part 2) (2017/03/03)
- 强烈推荐这一节,本文绝大部分图都是来自该节课程的slide
- 理解lstm:
- YJango的循环神经网络——实现LSTM
- 《Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling》 https://arxiv.org/pdf/1412.3555.pdf
- 【干货】人人都能看懂的GRU