循环神经网络知识总结
一、概念部分
RNN中的Dropout
Dropout可以理解为bagging。
- 在训练阶段,在每次迭代中,按一定比例随机丢弃神经元之间的连接来改变网络结构,以实现训练不同网络的目的。
- 在测试时,使用全部的神经元,相当于全部进行投票。这边要注意比例的处理,训练÷p或者测试×。
- 但与bagging不同点在于不是独立的,相当于是参数共享的。
在RNN中,有两种形式:
- 对于同1个t,前馈阶段dropput
- 对于同一个序列,不同step,循环阶段用相同的丢弃方法。
- 第二种效果好,因为大量参数是在循环连接的阶段,是主要矛盾。
梯度消失问题
RNN中的梯度消失与前馈网络梯度消失类似,但有所区别。
- 前馈网络梯度消失:是因为误差反向传播,每次都要乘上激活函数的导数。当激活函数是sigmoid或者tanh时,其饱和区的导数接近为0。因此,误差在每一层后逐渐衰减。当网络很深的时候甚至会消失(→0),网络就很难训练。这个现象也称为梯度弥散。
- 解决办法:最为直接的改变激活函数,比如relu之类的。也可以用残差网络、batch_normalization。详解深度学习中的梯度消失、爆炸原因及其解决方法
-
RNN中的梯度消失:并不是说整体的梯度小时了,而是对于与t时刻间隔比较远的位置的梯度消失了。也就是说,参数的更新主要依赖于与当前时刻相邻的状态,长距离的状态对参数没啥影响。
-
搞清楚反向传播中那部分导致了。
𝒛𝑘 = 𝑼𝒉_𝑘−1 +𝑾𝒙𝑘 + 𝒃
可见,一般选用的激活函数是sigmoid,所以里面导数的范围是0-0.25。因此,取决于 U T U^T UT的里面元素的大小。只要距离变长,就造成系统的不稳定。这就是长时间间隔的依赖关系难以建模的原因。
所以,真正的问题是出在
其实说白了就是 h k h_k hk与 h k − 1 h_{k-1} hk−1的导数需要一直算下去。比如你计算 δ t , 1 \delta_{t,1} δt,1,理解为t时刻对于1时刻的误差项,那就要从 h 1 , h 2 , . . . . h t h_1,h_2,....h_t h1,h2,....ht算下来,所以连乘的代价就是消失或者爆炸。 -
解决办法:LSTM、GRU。但注意是缓解,并不是真正解决。LSTM中的缓解主要体现在cell单元的更新上。
c t c_t ct对于 c t − 1 c_{t-1} ct−1的导数写起来很复杂,但第一部分就是遗忘门 f t f_t ft。其可以决定有多少信息要被遗忘(有多少信息保留)。这个能力是网络自己学习的。所以最终连乘的这部分梯度,在LSTM中不会始终大于1或者小于1,一定程度缓解。但也要注意, f t f_t ft的值也和参数初始化有关,一般而言,会把偏置项设为1、2这样。
一般在深度网络参数学习时,参数初始化的值一般都比较小.但是在训练LSTM 网络时,过小的值会使得遗忘门的值比较小.这意味着前一时刻的信息大部分都丢失了,这样网络很难捕捉到长距离的依赖信息.并且相邻时间间隔的梯度会非常小,这会导致梯度弥散问题.因此遗忘的参数初始值一般都设得比较大,其偏置向量𝒃𝑓 设为1 或2.
- 《Why LSTMs Stop Your Gradients From Vanishing: A View from the Backwards Pass》翻译:
- How LSTM sovle this
- 面试回答:
- 漫谈LSTM系列的梯度问题
二、模型部分
LSTM
理论部分
理解梯度消失,就很容易记住LSTM的结构了。
用top-down的写法。
-
隐藏单元: h t = o t t a n h ( c t ) h_t = o_ttanh(c_t) ht=ottanh(ct)
-
记忆单元 c t = f t c t − 1 + i t c ~ t c_t = f_tc_{t-1}+i_t\tilde c_t ct=ftct−1+itc~t
-
候选状态: c ~ t = t a n h ( W c x t + U c h t − 1 + b c ) \tilde c_t = tanh(W_cx_t+U_ch_{t-1}+b_c) c~t=tanh(Wcxt+Ucht−1+bc)
-
遗忘门: f t = σ ( W f x t + U f h t − 1 + b f ) f_t = \sigma(W_fx_t+U_fh_{t-1}+b_f) ft=σ(Wfxt+Ufht−1+bf)决定上一时刻记忆单元要遗忘多少
-
输入门: i t = σ ( W i x t + U i h t − 1 + b i ) i_t = \sigma(W_ix_t+U_ih_{t-1}+b_i) it=σ(Wixt+Uiht−1+bi)决定候选状态要保留多少
-
输出门: o t = σ ( W o x t + U o h t − 1 + b o ) o_t = \sigma(W_ox_t+U_oh_{t-1}+b_o) ot=σ(Woxt+Uoht−1+bo)决定当前记忆单元要输出多少
-
说明
- 门控机制的作用是解决记忆容量问题。如果没有信息传入传出的限制,单纯不断累加,就会发生饱和。但其实,隐状态容量是有限的。
- 记忆单元的形式是为了缓解梯度消失问题。
另一个细节,两类激活函数:
- sigmoid:值域是[0,1]就是控制信息的传输。门的激活函数如果用relu的话会有个问题,就是relu是没有饱和区域的,那么就没法起到门的作用。
- tanh:候选记忆用tanh是因为tanh的输出在-1~1,是0中心的,并且在0附近的梯度大,模型收敛快
LSTM中遗忘门和输出门的激活函数比较重要。删除任何一个激活函数都会对性能有较大的影响。说明这两个激活函数对于信息的取舍作用很重要。
代码部分
来看看tensorflow中的LSTM【当然我用的是keras里面的】
tf.keras.layers.LSTM(units,return_sequences=True,return_state=True)
参数有很多,不一一列举,看文档就很清晰,最关键是把输入、输出、维度搞清楚。
units:Positive integer, dimensionality of the output space.
【其实就是隐藏层呀,输出的维度】比如embeeding的维度是128,units是64,最后就是64。
return_sequences:返回的是h1,…ht还是最后一个。
看下源代码:
if self.return_sequences:
output = outputs
else:
output = last_output
return_state:Whether to return the last state in addition to the output.
这里的state包含了h与c。
看下源代码:
states = [new_h, new_c]
if self.return_state:
return [output] + list(states)
elif self.return_runtime:
return output, runtime
else:
return output
看个例子:
注意下,如果return_sequences=False,output与hidden_state还是不一样的。
双向LSTM
注意,前向后向的输入都是Input Layer,然后进行拼接。
双向的输出应该是[forward,backward],所以如果输出维度每个是128,最后应该是256。
看下如何实现:
bi_output = Bidirectional(LSTM(128,return_sequences=True),backward_layer = LSTM(128,return_sequences=True,go_backwards= True) )(x)
注意,backward_layer的那一层要有参数go_backwards 。就是方向一定要相反,不然会报错。【我试了下不同层,比如LSTM GRU】好像不太行,不过可以手工拼接(如果想尝试的话)。
其实backward_layer可以忽略,这个层是会默认算的。
bi_output = Bidirectional(LSTM(128,return_sequences=True))(x)
这样即可。
最后输出shape=(None, 64, 256)。
GRU
- 与LSTM相比具有更少的参数,更易于计算和实现。
- 用两个门控单元重置门与更新门实现一个控制短期记忆,一个控制长期记忆。
重置门:
r
t
r_t
rt =
σ
(
W
r
x
t
+
U
r
h
t
−
1
)
\sigma(W_rx_t + U_rh_{t-1})
σ(Wrxt+Urht−1)
更新门:
z
t
z_t
zt =
σ
(
W
z
x
t
+
U
z
h
t
−
1
)
\sigma(W_zx_t + U_zh_{t-1})
σ(Wzxt+Uzht−1)
状态的更新公式:
h
^
t
=
t
a
n
h
(
W
h
x
t
+
U
h
(
r
t
⋅
h
t
−
1
)
)
\hat h_t=tanh(W_hx_t+U_h(r_t\cdot h_{t-1}))
h^t=tanh(Whxt+Uh(rt⋅ht−1))
h
t
=
(
1
−
z
t
)
h
t
−
1
+
z
t
h
^
t
h_t = (1-z_t)h_{t-1}+z_t\hat h_t
ht=(1−zt)ht−1+zth^t
因此用一个更新门 z t z_t zt实现了遗忘和更新的功能。