传统模式的RNN
反向流传
$$\begin{equation}S_t = f(UX_t+WS_{t-1})\tag{1}\end{equation}$$
$$\begin{equation}O_t=g(VS_t)\tag{2}\end{equation}$$
其中,$f$和$g$为激活函数,$U,W,V$为RNN的参数。
假如$T$时刻的loss为$L_T$,则反向流传时传递到$t$时刻的对于$W$的梯度为,
$$\begin{equation}[\frac{\partial L_T}{\partial W}]_t^T=\frac{\partial L_T}{\partial O_T}\frac{\partial O_T}{\partial S_T}(\Pi_{k=T-1}^{t-1}\frac{\partial S_{k+1}}{\partial S_k})\tag{3}\end{equation}$$
求$S_k$对于$S_{k-1}$的偏导(对于矩阵的求导,能够参考矩阵求导),
$$\begin{equation}\frac{\partial S_k}{\partial S_{k-1}}=\frac{\partial f(UX_k+WS_{k-1})}{\partial (UX_k+WS_{k-1})}\frac{\partial (UX_k+WS_{k-1})}{\partial S_{k-1}}=diag(f^{'}(UX_k+WS_{k-1}))W\tag{4}\end{equation}$$
梯度隐没和梯度爆炸的起因
RNN罕用的两种激活函数,sigmoid和tanh。如果抉择sigmoid函数作为激活函数,即$f(z)=\frac{1}{1+e^{-z}}$,其导数为$f^{'}(z)=f(z)(1-f(z))$,导数的取值范畴为0~0.25。如果抉择tanh作为激活函数,即$f(z)=\frac{e^z-e^{-z}}{e^z+e^{-z}}$,其导数为$f^{'}(z)=1-f(z)^2$,导数的取值范畴为0~1。并且RNN网络的同一层中,所有工夫步的$W$都是共享的。
因而,入选用sigmoid或者tanh作为激活函数时,大多数时候$f^{'}(z)$都是大于0小于1的,如果W的值也是大于0小于1,在计算式(3)波及到屡次$f^{'}(z)$和$W$的屡次连乘,后果会趋于0,从而造成了梯度隐没的问题。如果W的值特地大,则屡次连乘后就会呈现梯度爆炸的问题。
如果应用relu作为激活函数,即$f(z)=max(0,z)$,relu的导数在x>0时恒为1,肯定水平上能够缓解梯度隐没问题,然而如果W的值特地大,也会呈现梯度爆炸的问题。而且当$z<0$时,导数恒为0,会造成局部神经元无奈激活(可通过设置小学习率局部解决)。
不同之处
DNN中梯度隐没和RNN梯度隐没意义不一样,DNN中梯度隐没的问题是:梯度在反向流传过程中,流传到低层的网络时,梯度会变得很小,这样低层网络的参数就不会更新,但高层网络是更新的;而RNN中的梯度隐没的问题是:工夫步$T$的梯度无奈传递到工夫步$t$($t<T$且$t$与$T$时刻相差较大),因而工夫步$t$在更新参数时,只会受到工夫步$T^{'}$的影响($t\leq T^{'}$,$t$与$T^{'}$相差不大),即RNN中的参数还是能够更新的,因为RNN中同一层的参数都是一样的,并不会呈现参数不更新的状况,然而没方法满足学习到长期依赖。
如图,在工夫步1,更新参数时,依照公式3,应该思考梯度
$$\begin{equation}\sum_{T=1}^{6}[\frac{\partial L_T}{\partial W}]_1^T\tag{5}\end{equation}$$
然而通过多层反向流传后,梯度$[\frac{\partial L_6}{\partial W}]_1^6$和$[\frac{\partial L_5}{\partial W}]_1^5$可能会隐没,因而工夫步1的在更新参数$W$时,只思考了工夫步1、2、3、4这几个间隔它比拟近的工夫步,这也就导致了学习不到远距离的依赖关系。这与DNN的梯度隐没是不同的,因为RNN中的参数$W$还是能够更新的。因为,RNN同一层的参数是一样的,而MLP/CNN 中不同的层有不同的参数。最终,参数$W$更新的梯度为各个工夫步参数$W$的更新梯度之和。
LSTM
在原始RNN的根底上,LSTM和GRU被提出,通过引入门控机制,肯定水平上缓解了梯度隐没的问题。引入门控的目标在于将激活函数导数的连乘变为加法。
以LSTM为例,
$$\begin{equation}c^{(t)}=f^{(t)}\odot c^{(t-1)}+i^{(t)}\odot\tilde{c}^{(t-1)}\tag{6}\end{equation}$$
$$\begin{equation}h^{(t)}=o^{(t)}\odot \tanh(c^{(t)})\tag{7}\end{equation}$$
假如时刻T的损失$L_T$,思考$L_T$对$c^{(t)}$求导,由式(6)和(7)可知有两条求导门路,别离为$L_T->c^{(t+1)}->c^{(t)}$和$L_T->h^{(t)}->c^{(t)}$。即,
$$\begin{equation}\begin{aligned}\frac{\partial L_T}{\partial c^{(t)}}&=\frac{\partial L_T}{\partial c^{(t+1)}}\frac{\partial c^{(t+1)}}{\partial c^{(t)}}+\frac{\partial L_T}{\partial h^{(t)}}\frac{\partial h^{(t)}}{\partial c^{(t)}}\\&=\frac{\partial L_T}{\partial c^{(t+1)}}\odot f^{(t+1)}+\frac{\partial L_T}{\partial h^{(t)}}\odot o^{(t)}\odot (1-\tanh^2(c^{(t)}))\end{aligned}\tag{8}\end{equation}$$
$$\begin{equation}[\frac{\partial L_T}{\partial W_f}]_t=\frac{\partial L_T}{\partial c^{(t)}}\frac{\partial c^{(t)}}{W_f}\tag{9}\end{equation}$$
$$\begin{equation}[\frac{\partial L_T}{\partial W_i}]_t=\frac{\partial L_T}{\partial c^{(t)}}\frac{\partial c^{(t)}}{W_i}\tag{10}\end{equation}$$
留神到,当$f^{(t)}$为1时,即便第二项很小,t+1时刻的梯度依然能够很好地传导到上一时刻t。此时即便序列的长度很长,也不会产生梯度隐没的问题。当$f^{(t)}$为0时,即t时刻的cell 信息不会影响到t+1时刻的信息,此时在反向流传过程中,t+1时刻的梯度也不会传导到t时刻。因而forget gate $f^{(t)}$起到了管制梯度流传的瘦弱水平的作用。
多层LSTM个别只采纳2~3层。
LSTM 中梯度的流传有很多条门路, 例如$c^{(t+1)}->c^{(t)}$这条门路上只有逐元素相乘和相加的操作,梯度流最稳固;然而其余门路,例如 $c^{(t+1)}->i^{(t+1)}->h^{(t)}->c^{(t)}$门路上梯度流与一般 RNN 相似,照样会产生雷同的权重矩阵和激活函数的导数的重复连乘,因而仍然会爆炸或者隐没。然而,正如式(8)~(9)所示,在计算$T$时刻的损失传递到$t$时刻对于$W_f$和$W_i$的梯度时,具备多个梯度流,且模式相似于
$$\begin{equation}(a_1+a_2)(b1+b2+b3)(c_1+c_2)...\tag{10}\end{equation}$$
即在反向流传过程中,梯度流是一种和的乘积的模式,因而能够了解为总的远距离梯度 = 各条门路的远距离梯度之和,即使其余远距离门路梯度隐没了,只有保障有一条远距离门路梯度不隐没,总的远距离梯度就不会隐没(失常梯度 + 隐没梯度 = 失常梯度)。因而 LSTM肯定水平上缓解了梯度隐没的问题,然而梯度爆炸问题任然可能产生,因为失常梯度 + 爆炸梯度 = 爆炸梯度。然而因为LSTM 的梯度流门路十分起伏,且和一般RNN相比多通过了很屡次激活函数(导数都小于 1),因而 LSTM 产生梯度爆炸的频率要低得多。实际中梯度爆炸个别通过梯度裁剪来解决。
IndRNN
为了解决梯度隐没和梯度爆炸问题,IndRNN将层内的神经元独立开来,对式(1)稍加批改,
$$\begin{equation}h^{(t)}=\sigma(Wx^{(t)} + u\odot h^{(t-1)}+b)\tag{11}\end{equation}$$
其中,激活函数$f$为relu函数。IndRNN中,在利用上一时刻t-1时刻的hidden state$h^{(t-1)}$计算以后时刻t的的hidden state $h^{(t)}$时,不再是与权重矩阵$U$相乘,而是与权重向量$u$计算哈达玛积(对应元素相乘),这就使得同一层的RNN Cell的神经元互相独立了。即$h^{(t)}$的第k个维度只与$h^{(t-1)}$的第k个维度无关。
将这种神经元之间解耦的思维利用到LSTM,进一步提出了IndyLSTM。
$$\begin{equation}f^{(t)} =\sigma_g(W_fx^{(t)}+u_f\odot h^{(t-1)}+b_f)\tag{12}\end{equation}$$
$$\begin{equation}i^{(t)}=\sigma_g(W_i x^{(t)}+u_i\odot h^{(t-1)}+b_i)\tag{13}\end{equation}$$
$$\begin{equation}o^{(t)}=\sigma_g(W_o x^{(t)}+u_o\odot h^{(t-1)}+b_o)\tag{14}\end{equation}$$
$$\begin{equation}\tilde{c}^{(t)}=\sigma_c(W_c x^{(t)}+u_c\odot h^{(t-1)}+b_c)\tag{15}\end{equation}$$
$$\begin{equation}c^{(t)}=f^{(t)}\odot c^{(t-1)}+i^{(t)}\odot \tilde{c}^{(t)}\tag{16}\end{equation}$$
$$\begin{equation}h^{(t)}=o^{(t)}\odot \sigma_h(c^{(t)})\tag{17}\end{equation}$$
对神经元进行解耦,使得在反向流传过程中,多条门路的梯度流都较为安稳,能够无效地缓解梯度降落问题和梯度爆炸问题。
源码
tensorflow中,能够通过tensorflow.nn.rnn_cell.LSTMCell
调用LSTM,通过tensorflow.contrib.rnn.IndyLSTMCell
调用IndyLSTM。
LSTMCell的build办法
输出门$i$的权重参数为$W_i$和$U_i$,忘记门$f$的权重参数为$W_f$和$U_f$,输入门$o$的参数为$W_o$和$U_o$,候选cell$\tilde{c}$的参数为$W_c$和$U_c$。因而总的权重参数_kernel
的shape为[input_depth + h_depth, 4 * self._num_units]。
self._kernel = self.add_variable( _WEIGHTS_VARIABLE_NAME, shape=[input_depth + h_depth, 4 * self._num_units], initializer=self._initializer, partitioner=maybe_partitioner)
LSTMCell的call办法
在call办法中,将以后工夫步的inputs
和上一时刻的hidden state $h$拼接,与权重矩阵相乘,在切分,失去输出门、候选的cell、忘记门和输入门。
# i = input_gate, j = new_input, f = forget_gate, o = output_gate lstm_matrix = math_ops.matmul( array_ops.concat([inputs, m_prev], 1), self._kernel) lstm_matrix = nn_ops.bias_add(lstm_matrix, self._bias) i, j, f, o = array_ops.split( value=lstm_matrix, num_or_size_splits=4, axis=1)
IndyLSTMCell的build办法
由式(12)~(15)可知,神经元进行理解耦,参数$W$任然为对于输出的权重矩阵,然而对于$h$的权重矩阵$U$变成了权重向量$u$。_kernel_w
的shape为[input_depth, 4 self._num_units],而权重向量_kernel_u
的shape为[1, 4 self._num_units]。
self._kernel_w = self.add_variable( "%s_w" % rnn_cell_impl._WEIGHTS_VARIABLE_NAME, shape=[input_depth, 4 * self._num_units], initializer=self._kernel_initializer) self._kernel_u = self.add_variable( "%s_u" % rnn_cell_impl._WEIGHTS_VARIABLE_NAME, shape=[1, 4 * self._num_units],
IndyLSTMCell的call办法
gen_array_ops.tile(h, [1, 4]) * self._kernel_u
即是在计算$u_i\odot h$、$u_c\odot h$、$u_f\odot h$和$u_o\odot h$。
gate_inputs = math_ops.matmul(inputs, self._kernel_w) gate_inputs += gen_array_ops.tile(h, [1, 4]) * self._kernel_u gate_inputs = nn_ops.bias_add(gate_inputs, self._bias) # i = input_gate, j = new_input, f = forget_gate, o = output_gate i, j, f, o = array_ops.split( value=gate_inputs, num_or_size_splits=4, axis=one)
参考
- https://www.zhihu.com/questio...
- https://zhuanlan.zhihu.com/p/...
- https://www.zhihu.com/questio...