DL notes 01:RNN/LSTM/GRU
@[toc]
一、RNN基本结构、梯度消失和梯度爆炸的原因
线性计算单元组成的RNN结构是最简单的一种,我们以此为例来说明造成梯度消失和梯度爆炸的原因: 上图为线性计算单元组成的RNN.依据上图,我们现假设存在一个RNN模型仅包含一个隐藏层,整个RNN模型关注的时间步数为3,$H{0}$是隐藏层的初始状态,则可以用如下算式表示前向传播过程: $$H{1} = W{X}X{1} + W{H}H{0} + b{H}, O{1} = W{O}H{1} + b{O} $$$$H{2} = W{X}X{2} + W{H}H{1} + b{H}, O{2} = W{O}H{2} + b{O} $$$$H{3} = W{X}X{3} + W{H}H{2} + b{H}, O{3} = W{O}H{3} + b{O} $$
其中$X =[X{0},X{1},X{2},..X{t}]$,$Y =[Y{0},Y{1},Y{2},..Y{t}]$代表$t$时间步长的特征数据和真值标签。$H =[H{0},H{1},H{2},..H{t}]$ 代表RNN中的隐节点状态,$W{X},W{H},W{O}$是各层的权重,$b{H},b{O}$是偏置。 因为是最简单的线性计算单元,我们假设最后使用MSE作为损失函数:$$L{t} = \frac{1}{2}\left(Y{t}-O{t}\right)^{2} $$ 对于整个RNN的训练,我们通常需要统计每个时刻的损失之和或平均值,这里我们以每个时间步数累积损失作为整个模型的训练损失: $$L = \sum{t=0}^{T} L{t}$$ 假设目前只考虑初始的三个时间步长,我们选取最长一条反向传播通路为例,即以$L{3}$对RNN中的参数求偏导: $$ \frac{\partial L{3}}{\partial W{O}} = \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial W{O}} $$
$$ \frac{\partial L{3}}{\partial W{X}} = \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial H{3}} \frac{\partial H{3}}{\partial W{X}} + \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial H{3}} \frac{\partial H{3}}{\partial H{2}} \frac{\partial H{2}}{\partial W{X}} + \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial H{3}} \frac{\partial H{3}}{\partial H{2}} \frac{\partial H{2}}{\partial H{1}} \frac{\partial H{1}}{\partial W{X}}$$
$$ \frac{\partial L{3}}{\partial W{H}} = \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial H{3}} \frac{\partial H{3}}{\partial W{H}} + \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial H{3}} \frac{\partial H{3}}{\partial H{2}} \frac{\partial H{2}}{\partial W{H}} + \frac{\partial L{3}}{\partial O{3}} \frac{\partial O{3}}{\partial H{3}} \frac{\partial H{3}}{\partial H{2}} \frac{\partial H{2}}{\partial H{1}} \frac{\partial H{1}}{\partial W{H}}$$
从中可以发现根据链式法则求导,$W{X},W{H}$ 会重复出现,这是由于$W{X},W{H}$在每个时间步中都参与隐节点$H$的状态估计,随着时间步数的增加,反向传播的路径将会相应的延长。相应的,我们可以总结出任意时刻$t$损失函数对$W{X}$的偏导: $$ \frac{\partial L{t}}{\partial W{X}} = \sum{k=0}^{t} \frac{\partial L{t}}{\partial O{t}} \frac{\partial O{t}}{\partial H{t}} \left( \prod{j=k+1}^{t} \frac{\partial H{j}}{\partial H{j-1}} \right) \frac{\partial H{k}}{\partial W{X}} $$ 任意时刻$t$损失函数对$W{H}$求偏导同上,只需替换$W{X}$。 如果隐藏层的激活函数为$tanh$,则有$$H{j} = tanh(W{X}X{j}+W{H}H{j-1}+b{H})$$$$\prod{j=k+1}^{t} \frac{\partial H{j}}{\partial H{j-1}} = \prod{j=k+1}^{t} tanh^{‘}·W{H}$$ 激活函数$tanh$的导数有如下特性: $$tanh^{‘} x= 1-tanh^{2}x$$ 因此可知$0< tanh^{‘} \le 1$。 在训练过程中,$H{j}$的状态极少情况下为0,$tanh^{‘}$通常是小于1的,如果$W{H}$值在$(0,1)$范围内,则$\prod{j=k+1}^{t} tanh^{‘}·W{H}$ 趋近于0,导致梯度消失。 如果$W{H}$值很大,则会导致$\prod{j=k+1}^{t} tanh^{‘}·W{H}$ 连乘后结果趋近于无穷,导致梯度爆炸。 如何避免这种现象?在RNN的课程学习中,提到裁剪梯度的方法,假设我们把所有模型参数的梯度拼接成一个向量 $\boldsymbol{g}$ ,并设裁剪的阈值是 $\theta$ 。裁剪后的梯度即: $$\min\left(\frac{\theta}{|\boldsymbol{g}|}, 1\right)\boldsymbol{g}$$ 梯度的的 $L{2}$ 范数不超过$\theta$。函数如下:
def grad_clipping(params, theta, device):
norm = torch.tensor([0.0], device=device)
for param in params:
norm += (param.grad.data ** 2).sum()
norm = norm.sqrt().item()
if norm > theta:
for param in params:
param.grad.data *= (theta / norm)
裁剪梯度是一个简单好用的防止梯度爆炸的方法,实际上造成梯度衰减或梯度爆炸的根本原因是$\prod{j=k+1}^{t} \frac{\partial H{j}}{\partial H{j-1}}$这一连乘项,理想的消除方法就是使$\frac{\partial H{j}}{\partial H_{j-1}} \in [0,1]$其实这就是LSTM做的事情,至于细节如何则会在后续的篇幅中加以介绍。
二、LSTM
长短期记忆(Long short-term memory, LSTM)是一种特殊的RNN结构单元,主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。简单来说,就是相比普通的RNN,LSTM能够在更长的序列中有更好的表现。 $$I_t = σ(XtW{xi} + H{t−1}W{hi} + b_i) $$$$F_t = σ(XtW{xf} + H{t−1}W{hf} + b_f) $$$$O_t = σ(XtW{xo} + H{t−1}W{ho} + b_o) $$$$\widetilde{C}_t = tanh(XtW{xc} + H{t−1}W{hc} + b_c) $$$$C_t = Ft ⊙C{t−1} + I_t ⊙\widetilde{C}_t $$$$H_t = O_t⊙tanh(Ct)$$$$Y{t} = \phi(H{t}W{hy}+b{y})$$ 相比RNN只有一个传递状态$H{t}$,LSTM有两个传输状态,一个 $C{t}$ (cell state),和一个 $H{t}$ (hidden state)。其中对于传递下去的 $C{t}$ 改变得很慢,通常输出的 $C{t}$ 是上一个状态传过来的$C{t-1}$ 加上一些数值。而 $H{t}$ 则在不同节点下往往会有很大的区别。$\odot$ 是Hadamard Product,也就是操作矩阵中对应的元素相乘,因此要求两个相乘矩阵是同型的。 $\oplus$ 则代表进行矩阵加法。
LSTM主要包括以下几个结构: - 遗忘门:控制上一时间步的记忆细胞 - 输入门:控制当前时间步的输入 - 输出门:控制从记忆细胞到隐藏状态 - 记忆细胞:⼀种特殊的隐藏状态的信息的流动 三个门输出都经过诸如$sigmoid$的激活函数,映射到$(0,1)$的范围,形成门控状态。而记忆细胞则是通过$tanh$转换成$(-1,1)$的范围,这里作为记忆细胞短期依赖输出而非门控信号。
LSTM 内部主要有三个阶段:
忘记阶段。这个阶段主要是对上一个节点传进来的输入进行选择性忘记。简单来说就是会 “忘记不重要的,记住重要的”。具体来说是通过计算得到的 $F{t}$ (f表示forget)来作为忘记门控,来控制上一个状态的$C{t-1}$ 哪些需要留哪些需要忘。
选择记忆阶段。这个阶段将这个阶段的输入有选择性地进行“记忆”。主要是会对输入 $X_{t}$进行选择记忆。哪些重要则着重记录下来,哪些不重要,则少记一些。当前的输入内容由前面计算得到的$\widetilde{C}t$表示。而选择的门控信号则是由 $I{t}$ (i代表information)来进行控制。 >将上面两步得到的结果相加,即可得到传输给下一个状态的$C_{t}$ 。也就是上图中的第一个公式。这里也使用$tanh$起到对输入的信息进行压缩的作用。
输出阶段。这个阶段将决定哪些将会被当成当前状态的输出。主要是通过 $O{t}$来进行控制的。并且还对上一阶段得到的 $C{t}$进行了放缩(通过一个tanh激活函数进行变化)。
与普通RNN类似,输出 $Y{t}$往往最终也是通过$H{t}$变化得到。
三、GRU
GRU(Gate Recurrent Unit)是循环神经网络(Recurrent Neural Network, RNN)的一种。和LSTM(Long-Short Term Memory)一样,也是为了解决长期记忆和反向传播中的梯度等问题而提出来的。
GRU和LSTM在很多情况下实际表现上相差无几,那么为什么我们要使用新人GRU(2014年提出)而不是相对经受了更多考验的LSTM(1997提出)呢?其实通过代码我们就可以发现,GRU的权重参数相比LSTM减少了1/4。相比LSTM,使用GRU能够达到相当的效果,并且相比之下更容易进行训练,能够很大程度上提高训练效率,因此很多时候会更倾向于使用GRU。 $$R_{t} = σ(XtW{xr} + H{t−1}W{hr} + br) $$ $$Z{t} = σ(XtW{xz} + H{t−1}W{hz} + bz) $$ $$\widetilde{H}{t-1} = Rt ⊙H{t−1} $$ $$\widetilde{H}_t = tanh(XtW{xh} + \widetilde{H}{t-1}W{hh} + b_h) $$ $$H_t = Zt⊙H{t−1} + (1−Z_t)⊙\widetilde{H}_t $$
GRU很聪明的一点就在于,我们使用了同一个门控$Z_t$ 就同时可以进行遗忘和选择记忆(LSTM则要使用多个门控)。$R_t$在GRU中被称作重置⻔有助于捕捉时间序列⾥短期的依赖关系;$Z_t$被称作更新⻔有助于捕捉时间序列⾥⻓期的依赖关系。 $Zt⊙H{t−1}$:表示对原本隐藏状态的选择性“遗忘”。这里的 $Zt$ 可以想象成遗忘门(forget gate),忘记$H{t−1}$ 维度中一些不重要的信息。 $(1−Z_t)⊙\widetilde{H}_t$: 表示对包含当前节点信息的$\widetilde{H}_t$ 进行选择性”记忆“。与上面类似,这里的$(1−Z_t)$同理会忘记 $\widetilde{H}_t$维度中的一些不重要的信息。或者,这里我们更应当看做是对$\widetilde{H}_t$ 维度中的某些信息进行选择。 $H_t = Zt⊙H{t−1} + (1−Z_t)⊙\widetilde{H}t$ :结合上述,这一步的操作就是忘记传递下来的 $H{t−1}$ 中的某些维度信息,并加入当前节点输入的某些维度信息。 >可以看到,这里的遗忘 $Z_t$和选择$(1−Z_t)$ 是联动的。也就是说,对于传递进来的维度信息,我们会进行选择性遗忘,则遗忘了多少权重 ($Z_t$),我们就会使用包含当前输入的 $\widetilde{H}_t$ 中所对应的权重进行弥补 $(1−Z_t)$。以保持一种”恒定“状态。
四、深度循环神经网络
$$\boldsymbol{H}_t^{(1)} = \phi(\boldsymbol{X}t \boldsymbol{W}{xh}^{(1)} + \boldsymbol{H}{t-1}^{(1)} \boldsymbol{W}{hh}^{(1)} + \boldsymbol{b}_h^{(1)})$$$$
\boldsymbol{H}_t^{(\ell)} = \phi(\boldsymbol{H}t^{(\ell-1)} \boldsymbol{W}{xh}^{(\ell)} + \boldsymbol{H}{t-1}^{(\ell)} \boldsymbol{W}{hh}^{(\ell)} + \boldsymbol{b}_h^{(\ell)})$$$$\boldsymbol{O}_t = \boldsymbol{H}t^{(L)} \boldsymbol{W}{hq} + \boldsymbol{b}_q$$
在pytorch的实现中以num_layers
参数进行层数的设置和调整。
五、双向循环神经网络
$$\overrightarrow{\boldsymbol{H}}_t = \phi(\boldsymbol{X}t \boldsymbol{W}{xh}^{(f)} + \overrightarrow{\boldsymbol{H}}{t-1} \boldsymbol{W}{hh}^{(f)} + \boldsymbol{b}_h^{(f)})$$$$ \overleftarrow{\boldsymbol{H}}_t = \phi(\boldsymbol{X}t \boldsymbol{W}{xh}^{(b)} + \overleftarrow{\boldsymbol{H}}{t+1} \boldsymbol{W}{hh}^{(b)} + \boldsymbol{b}_h^{(b)}) $$$$\boldsymbol{H}t=(\overrightarrow{\boldsymbol{H}}{t}, \overleftarrow{\boldsymbol{H}}_t)$$$$\boldsymbol{O}_t = \boldsymbol{H}t \boldsymbol{W}{hq} + \boldsymbol{b}_q$$
在pytorch的实现中以bidirectional
参数进行层数的设置和调整。