RNN基础
RNN之所以称为循环神经网路,即一个序列当前的输出与前面的输出也有关。具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,即隐藏层之间的节点不再无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。
RNN的应用领域有很多, 可以说只要考虑时间先后顺序的问题都可以使用RNN来解决.这里主要说一下几个常见的应用领域:
- 自然语言处理(NLP): 主要有视频处理, 文本生成, 语言模型, 图像处理
- 机器翻译, 机器写小说
- 语音识别
- 图像描述生成
- 文本相似度计算
- 音乐推荐、网易考拉商品推荐、Youtube视频推荐等新的应用领域.
RNN
RNN层级结构, 它主要有输入层,Hidden Layer, 输出层组成.
其中,
$$
S_{t} = f(Ux_{t} + WS_{t-1}) \\
o_{t} = g(V S_{t})
$$
其中,f和g均为激活函数. 其中f可以是tanh,relu,sigmoid等激活函数,g通常是softmax也可以是其他。时间就向前推进,此时的状态s1作为时刻1的记忆状态将参与下一个时刻的预测活动。
注意:
- 这里的W,U,V在每个时刻都是相等的(权重共享).
- 隐藏状态可以理解为: S=f(现有的输入+过去记忆总结)
RNN的反向传播
由于每一步的输出不仅仅依赖当前步的网络,并且还需要前若干步网络的状态,那么这种BP改版的算法叫做Backpropagation Through Time(BPTT) , 也就是将输出端的误差值反向传递,运用梯度下降法进行更新.
设每一次的输出值$O_t$都会产生一个误差值$E_t$, 则总的误差可以表示为:
$$
E = \sum_t e_t
$$
损失函数可以使用交叉熵损失函数也可以使用平方误差损失函数.
以$t = 3$时刻为例, 根据链式求导法则可以得到$t = 3$时刻的偏导数为:
$$
\frac{\partial E_3}{\partial W} = \frac{\partial E_3}{\partial o_3} \frac{\partial o_3}{\partial S_3} \frac{\partial S_3}{\partial W}
$$
此时, 根据公式$ S_{t} = f(Ux_{t} + WS_{t-1})$我们会发现, $ S_3$除了和$W$有关之外, 还和前一时刻$S_2$有关.
对于$S_3$直接展开得到下面的式子:
$$
\frac{\partial S_3}{\partial W} = \frac{\partial S_3}{\partial S_3} \frac{\partial S_3^+}{\partial W}+ \frac{\partial S_3}{\partial S_2} \frac{\partial S_2}{\partial W}
$$
对于$S_2$直接展开得到下面的式子:
$$
\frac{\partial S_2}{\partial W} = \frac{\partial S_2}{\partial S_2} \frac{\partial S_2^+}{\partial W}+ \frac{\partial S_2}{\partial S_1} \frac{\partial S_1}{\partial W}
$$
对于$S_1$直接展开得到下面的式子:
$$
\frac{\partial S_1}{\partial W} = \frac{\partial S_1}{\partial S_1} \frac{\partial S_1^+}{\partial W}+ \frac{\partial S_1}{\partial S_0} \frac{\partial S_0}{\partial W}
$$
将上述三个式子合并得到:
$$
\frac{\partial S_3}{\partial W} = \sum_{k=0}^3
\frac{\partial S_3}{\partial S_k} * \frac{\partial S_k^+}{\partial W}
$$
这样就得到了公式:
$$
\frac{\partial E_3}{\partial W} = \sum_{k=0}^3 \frac{\partial E_3}{\partial o_3} \frac{\partial o_3}{\partial S_3} \frac{\partial S_3}{\partial S_k} \frac{\partial^+ S_k}{\partial W}
$$
其中$\frac{\partial^+ S_k}{\partial W}$表示的是$S_k$对$W$直接求导, 不考虑$S_{k-1}$的影响.
其次是对U的更新方法. 由于参数U求解和W求解类似,这里就不在赘述了,最终得到的具体的公式如下:
$$
\frac{\partial E_3}{\partial U} = \sum_{k=0}^3 \frac{\partial E_3}{\partial o_3} \frac{\partial o_3}{\partial S_3} \frac{\partial W^{3-k} a_k}{\partial U} \frac{\partial^+ S_3}{\partial f}
$$
最后,给出$V$的更新公式($V$只和输出$O$有关):
$$
\frac{\partial E_3}{\partial V} = \frac{\partial E_3}{\partial O_3}
\frac{\partial O_3}{\partial V}
$$
累乘会导致激活函数导数的累乘,进而会导致“梯度消失“和“梯度爆炸“现象的发生。在实际操作中,
激活函数一般选取sigmoid或者tanh函数,他们的导数最大都不大于1,随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。但,tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。还有一个原因是sigmoid函数还有一个缺点,Sigmoid函数输出不是零中心对称。sigmoid的输出均大于0,这就使得输出不是0均值,称为偏移现象,这将导致后一层的神经元将上一层输出的非0均值的信号作为输入。关于原点对称的输入和中心对称的输出,网络会收敛地更好。
解决“梯度消失“的方法主要有:
1、选取更好的激活函数
如ReLU函数,ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但恒为1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。还有一点就是如果左侧横为0的导数有可能导致把神经元学死,不过设置合适的步长也可以有效避免这个问题的发生。
2、改变传播结构
如长短时记忆(LSTM)或门限递归单元(GRU)结构等。
LSTM
LSTM (Long Short Term Memory networks)解决了传统RNN的长期依赖性问题。
和RNN不同的是: RNN中$h_t=U x_t+W S_{t-1}$,就是个简单的线性求和的过程. 而LSTM可以通过“门”结构来去除或者增加“细胞状态”的信息,实现了对重要内容的保留和对不重要内容的去除。通过Sigmoid层输出一个0到1之间的概率值,描述每个部分有多少量可以通过,0表示“不允许任务变量通过”,1表示“运行所有变量通过 ”。
结构如下:
相比RNN只有一个传递状态$ h^t $ ,LSTM有两个传输状态,一个$ c^t $(cell state),和一个 $ h^t $(hidden state)。(Tips:RNN中的$ h^t $对于LSTM中的$ c^t $)。
其中对于传递下去的$ c^t $改变得很慢,通常输出的$ c^t $是上一个状态传过来的$ c^{t-1} $加上一些数值。
$ c^t $作为记忆单元(cell state),综合了当前词$x_t$和前一时刻记忆单元$c_{t-1}$的信息。这和ResNet中的残差逼近思想十分相似,通过从$c_{t-1}$到$c_t$的”短路连接”, 梯度得已有效地反向传播。 当$f_t$处于闭合状态时, $c_t$的梯度可以直接沿着最下面的短路线传递到$c_{t-1}$,不受参数$W$的影响,这是LSTM能有效地缓解梯度消失现象的关键所在。
而$ h^t $则在不同节点下往往会有很大的区别。
首先使用LSTM的当前输入$ x^t $和上一个状态传递下来的$ h^{t-1} $拼接训练得到四个状态。
其中,$ z^f $,$ z^i $,$ z^o $ 是由拼接向量乘以权重矩阵之后,再通过一个 $sigmoid$ 激活函数转换成0到1之间的数值,来作为一种门控状态。而$z$则是将结果通过一个 $tanh$ 激活函将转换成-1到1之间的值(这里使用 $tanh$ 是因为这里是将其做为输入数据,而不是门控信号)。
LSTM有三个门,用于保护和控制细胞的状态。
$ z^f $作为忘记门(forget gate),控制上一时刻记忆单元$c_{t-1}$的信息融入记忆单元$c_t$。在理解一句话时,当前词$x_t$可能继续延续上文的意思继续描述,也可能从当前词$x_t$开始描述新的内容,与上文无关忘记门门的目的就是判断的是上一时刻的记忆单元$c_{t-1}$对计算当前记忆单元$c_t$的重要性。
$ z^i $作为输入门(input gate),控制当前词$x_t$的信息融入记忆单元$c_t$。在理解一句话时,当前词$x_t$可能对整句话的意思很重要,也可能并不重要。输入门的目的就是判断当前词$x_t$对全局的重要性。
$ z^o $作为输出门(output gate): 是从记忆单元$c_t$产生隐层单元$h_t$。并不是$c_t$中的全部信息都和隐层单元$h_t$有关,$c_t$可能包含了很多对$h_t$无用的信息,因此, 输出门的作用就是判断$c_t$中哪些部分是对$h_t$有用的,哪些部分是无用的。
其中,$\bigodot$是Hadamard Product,也就是操作矩阵中对应的元素相乘,因此要求两个相乘矩阵是同型的。$\bigoplus$则代表进行矩阵加法。
LSTM内部主要有三个阶段:
忘记阶段。这个阶段主要是对上一个节点传进来的输入进行选择性忘记。简单来说就是会 “忘记不重要的,记住重要的”。
具体来说是通过计算得到的$z^f$(f表示forget)来作为忘记门控,来控制上一个状态的$c^{t-1}$ 哪些需要留哪些需要忘。选择记忆阶段。这个阶段将这个阶段的输入有选择性地进行“记忆”。主要是会对输入$x^{t}$ 进行选择记忆。哪些重要则着重记录下来,哪些不重要,则少记一些。当前的输入内容由前面计算得到的$z$表示。而选择的门控信号则是由$z^i$(i代表information)来进行控制。
将上面两步得到的结果相加,即可得到传输给下一个状态的$c^t$。也就是上图中的第一个公式。
- 输出阶段。这个阶段将决定哪些将会被当成当前状态的输出。主要是通过$z^o$来进行控制的。并且还对上一阶段得到的$z^o$进行了放缩(通过一个tanh激活函数进行变化)。
与普通RNN类似,输出$y^t$往往最终也是通过$h^t$变化得到。
LSTM的内部通过门控状态来控制传输状态,记住需要长时间记忆的,忘记不重要的信息;而不像普通的RNN那样只能够“呆萌”地仅有一种记忆叠加方式。对很多需要“长期记忆”的任务来说,尤其好用。
但也因为引入了很多内容,导致参数变多,也使得训练难度加大了很多。因此很多时候我们往往会使用效果和LSTM相当但参数更少的GRU来构建大训练量的模型。
GRU
门控循环单元(GRU)将忘记和输入门组合成一个单一的“更新门”。它还将单元格状态和隐藏状态合并,并进行了一些其他更改。 所得到的模型比标准LSTM模型更简单。相比LSTM,使用GRU能够达到相当的效果,并且相比之下更容易进行训练,能够很大程度上提高训练效率,因此很多时候会更倾向于使用GRU。
GRU的输入输出结构与普通的RNN是一样的。
有一个当前的输入$x_t$,和上一个节点传递下来的隐状态(hidden state)$h_{t-1}$,这个隐状态包含了之前节点的相关信息。
结合$x_t$和$h_{t-1}$,GRU会得到当前隐藏节点的输出$y_t$和传递给下一个节点的隐状态$h_t$。
首先,我们先通过上一个传输下来的状态$h_{t-1}$和当前节点的输入入$x_t$来获取两个门控状态。如下图所示,其中$r$控制重置的门控(reset gate),$z$为控制更新的门控(update gate)。
得到门控信号之后,首先使用重置门控来得到“重置”之后的数据$h_{t-1}^{‘}=h_{t-1}\bigodot r$,再将$h_{t-1}^{‘}$与输入$x_t$进行拼接,再通过一个tanh激活函数来将数据放缩到-1~1的范围内。即得到如下图所示的$h^{‘}$ 。
这里的$h^{‘}$主要是包含了当前输入的$x_t$数据。有针对性地对$h^{‘}$添加到当前的隐藏状态,相当于”记忆了当前时刻的状态“。类似于LSTM的选择记忆阶段。
图中的$\bigodot$是Hadamard Product,也就是操作矩阵中对应的元素相乘,因此要求两个相乘矩阵是同型的。$\bigoplus$则代表进行矩阵加法操作。
最后介绍GRU最关键的一个步骤,我们可以称之为”更新记忆“阶段。
在这个阶段,我们同时进行了遗忘了记忆两个步骤。我们使用了先前得到的更新门控$z$(update gate)。
更新表达式:
$$
h^t = z \bigodot h^{t-1} + (1-z) \bigodot h^{‘}
$$
首先再次强调一下,门控信号(这里的$z$)的范围为0~1。门控信号越接近1,代表”记忆“下来的数据越多;而越接近0则代表”遗忘“的越多。
GRU很聪明的一点就在于,我们使用了同一个门控$z$就同时可以进行遗忘和选择记忆(LSTM则要使用多个门控)。
$z \bigodot h^{t-1}$:表示对原本隐藏状态的选择性“遗忘”。这里的$z$可以想象成遗忘门(forget gate),忘记$h^{t-1}$维度中一些不重要的信息。
$(1-z) \bigodot h^{‘}$: 表示对包含当前节点信息的$h^{‘}$进行选择性”记忆“。与上面类似,这里的$-z
$同理会忘记$h^{‘}$维度中的一些不重要的信息。或者,这里我们更应当看做是对$h^{‘}$维度中的某些信息进行选择。
$h^t = z \bigodot h^{t-1} + (1-z) \bigodot h^{‘}$:结合上述,这一步的操作就是忘记传递下来的$h^{t-1}$中的某些维度信息,并加入当前节点输入的某些维度信息。
可以看到,这里的遗忘$z$和选择$1-z$是联动的。也就是说,对于传递进来的维度信息,我们会进行选择性遗忘,则遗忘了多少权重($z$),我们就会使用包含当前输入的$h^{‘}$中所对应的权重进行弥补$1-z$。以保持一种”恒定“状态。
LSTM与GRU的关系
$r$(reset gate)实际上与他的名字有点不符。我们仅仅使用它来获得了$h^{‘}$ 。
那么这里的$h^{‘}$实际上可以看成对应于LSTM中的hidden state;上一个节点传下来的$h^{t-1}$ 则对应于LSTM中的cell state。$z$对应的则是LSTM中的$z^f$ forget gate,那么$1-z$ 我们似乎就可以看成是选择门$z^i$了。
LSTM与GRU在从$t$ 到 $t-1$ 的更新时都引入了加法。这个加法的好处在于能防止梯度弥散,因此LSTM和GRU都比一般的RNN效果更好。
梯度消失或者梯度爆炸问题处理方法
LSTM 和 GRU对于梯度消失或者梯度爆炸的问题处理方法主要是:
对于梯度消失: 由于它们都有特殊的方式存储”记忆”,那么以前梯度比较大的”记忆”不会像简单的RNN一样马上被抹除,因此可以一定程度上克服梯度消失问题。
对于梯度爆炸:用来克服梯度爆炸的问题就是gradient clipping,也就是当你计算的梯度超过阈值c或者小于阈值-c的时候,便把此时的梯度设置成c或-c。