一、RNN基础(参考了https://www.atyun.com/30234.html)
1. RNN模块
先前隐藏状态和当前输入组合成一个向量,经过tanh激活(将数据范围压缩到-1到+1之间,以防多次连乘数据爆炸),输出隐藏状态,并将隐藏状态传递给序列的下一步。隐藏状态充当神经网络的记忆,它保存着网络以前见过的数据信息。
(图片来自https://www.atyun.com/30234.html)
2. RNN存在什么问题?
(1)问题:只有短期记忆,不能处理长序列里的长距离依赖。如果序列很长,RNN很难将信息从较早的时间步传送到后面的时间步,比如“She, a girl that ..., is Shirley." 和 “You, ..., are my friend."这种隔了一个很长的从句的谓语动词的确定问题。
(2)影响:如果处理一段文本,RNN可能会遗漏开头的重要信息。
(3)原因:BPTT的梯度消失导致的。在反向传播时,RNN是随时间反向传播(back propagation through time,BPTT)的模式,如果时间步比较多,梯度(通常小于1)会随时间步不断相乘逐渐收缩,到了较早的时间步时梯度值变得非常小,则不太能通过梯度下降来更新参数,相应的较早的时间步的信息也就很难由学出来的参数传递到后面去。
(4)解决方案:LSTM、GRU
3. LSTM vs GRU
LSTM和GRU是作为短期记忆的解决方案而创建的。它们具有称为门(gate)的内部机制,它可以调节信息流。这些门可以了解序列中哪些数据重要以进行保留或丢弃。
(图片来自https://www.atyun.com/30234.html)
(1)LSTM
LSTM block之间不只有RNN里用于保存之前信息的隐藏状态,还有更重要的单元状态(cell state),它是用门对信息做了取舍之后的信息流。LSTM用三个不同的门(用sigmoid调节到到0或1,对输入做点乘)来决定哪些信息可以允许进入单元状态:遗忘门决定了哪些内容与前面的时间步相关(所以是作用于前一单元状态);输入门决定了从当前时间步添加哪些信息(所以是作用于前一隐藏状态和当前输入组成的向量);输出门决定下一个隐藏状态应该是什么(作用于当前单元状态经过tanh的输出)。
总的来看单元状态的通路就是:忘了什么信息、新加入了什么信息、输出、同时也影响到隐藏状态的输出;总的来看隐藏状态的通路就是:不是原来RNN里那种直接将信息一股脑输出,而是对已经对信息取舍后的单元状态进行进一步的取舍。
伪代码(图片来自https://www.atyun.com/30234.html,ps:用伪代码来表示这个主意真是太好了!!!)
其他:有non-peephole和peephole两个版本,TensorFlow里默认的是non-peephole(https://pdfs.semanticscholar.org/1154/0131eae85b2e11d53df7f1360eeb6476e7f4.pdf Felix Gers, Jurgen Schmidhuber, and Fred Cummins. "Learning to forget: Continual prediction with LSTM." IET, 850-855, 1999.),也可以用peephole(https://research.google.com/pubs/archive/43905.pdf Hasim Sak, Andrew Senior, and Francoise Beaufays. "Long short-term memory recurrent neural network architectures for large scale acoustic modeling." INTERSPEECH, 2014.)
(2)GRU
GRU不使用单元状态,直接使用隐藏状态来传输信息。它只有两个门:重置门用来决定要忘记多少过去的信息;更新门决定要丢弃哪些信息和要添加哪些新信息。
(3)二者区别
4. Memory Network
记忆网络发展知乎专栏:https://zhuanlan.zhihu.com/c_129532277
Recurrent memory network网络:https://zhuanlan.zhihu.com/p/45330963
二、代码实践(参考了https://zhuanlan.zhihu.com/p/37070414)
TensorFlow中与RNN相关的类都在tf.nn.rnn_cell.下,部分与tf.contrib.rnn.下的类同名,在r1.12里共有10个类:
单层LSTM:
lstm = tf.nn.rnn_cell.BasicLSTMCell(hidden_size) # hidden_size表示LSTM cell中单元数量
state = lstm.zero_state(batch_size, tf.float32)
for i in range(num_steps):
if i > 0: tf.get_variable_scope().reuse_variables()
lstm_output, state = lstm(current_input, state)
final_output = fully_connected(lstm_output)
多层LSTM:
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell
stacked_lstm = tf.nn.rnn_cell.MultiRNNCell([lstm_cell(lstm_size) for _ in range(number_of_layers)])
state = stacked_lstm.zeros_state(batch_size, tf.float32)
for i in range(len(num_steps)):
if i > 0:
tf.get_variable_scope().reuse_variables()
stacked_lstm_output, state = stacked_lstm(current_input, state)
final_output = fully_connected(stacked_lstm_output)
DropoutWrapper的用法如下,这样的返回值也是一个cell
def dropout(): # 为每一个rnn核后面加一个dropout层
if (self.config.rnn == 'lstm'):
cell = lstm_cell()
else:
cell = gru_cell()
return tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=self.keep_prob)
但是,真的在用的时候一般不需要自己按照各时间步展开,也即不用自己些for num_steps这个循环,而是采用以下三种函数来unrolling成单向或双向的RNN,它们接收以上十种cell子类作为输入,输出outputs和hidden state,具体用法例子在下面链接的API中有。
tf.nn.dynamic_rnn
tf.nn.bidirectional_dynamic_rnn
tf.nn.raw_rnn