循环神经网络(RNN)

循环神经网络(Recurrent Neural Network)

循环神经网络(RNN)_第1张图片

循环神经网络有别于普通的全连接神经网络,其特殊的神经元输入设置使得循环神经网络对于自带序列性质的数据有着更好的效果。在最简单的RNN神经元中,每个神经元会同时接受时刻t的输入和上一时刻t-1的输出从而达到捕捉序列中信息的效果。用数学公式可表达为:s^{t}=\sigma (w_{x}x^{t}+w_{s}s^{t-1})

对于上图的循环神经网络,梯度反向传播的过程可表达为:

\frac{\partial E^{t}}{\partial w_{y}}=\frac{\partial E^{t}}{\partial y^{t}}\cdot\frac{\partial y^{t}}{\partial W_{y}}

\frac{\partial E^{t}}{\partial y^{t}}=2(d^{t}-y^{t})

\frac{\partial y^{t}}{\partial w_{y}}=s^{t}

\frac{\partial E^{t}}{\partial w_{s}}=\frac{\partial E^{t}}{\partial y^{t}}\cdot\frac{\partial y^{t}}{\partial s^{t}}\cdot \frac{\partial s^{t}}{\partial w_{s}}

\frac{\partial E^{t}}{\partial w_{x}}=\frac{\partial E^{t}}{\partial y^{t}}\cdot\frac{\partial y^{t}}{\partial s^{t}}\cdot \frac{\partial s^{t}}{\partial w_{x}}

s分别对W_{x}W_{s}求导的过程中,与全连接神经网络不同的是:s^{t}是关于(s^{t-1},s^{t-2},...,s^{0})的函数。因此,\frac{\partial s^{t}}{\partial w_{x}}=gradient from (s^{t})+gradient from (s^{t-1})+...+gradient from (s^{0})\frac{\partial s^{t}}{\partial W_{s}}也同理。

更进一步地,\frac{\partial s^{t}}{\partial w_{x}}=1+\frac{\partial s^{t}}{\partial s^{t-1}}\cdot \frac{\partial s^{t-1}}{\partial w_{x}}+...+\frac{\partial s^{t}}{\partial s^{t-1}}\cdot \frac{\partial s^{t-1}}{\partial s_{t-2}}\cdot ...\cdot \frac{\partial s^{0}}{\partial w_{x}}

其中,\frac{\partial s^{k}}{\partial s^{k-1}}=\sigma^{'}\cdot w_{s}k\epsilon [1,t]

上述梯度有一个非常明显的问题在于每一项中s对自己上一时刻求导的连乘。当激活函数为sigmoid或tanh且w_s较小时,梯度连乘会造成梯度消失的问题,反之则会造成梯度爆炸。对于距离此刻较近的信息,RNN可以相对较好的处理,但随着序列增长,序列开端的信息将不会对RNN造成有用的影响。由于这个原因,RNN不适合用来处理需要捕捉较长的序列信息的问题。

对于梯度消失问题,批量归一化(Batch Normalization)是一个常用的解决方法,但对于循环神经网络该方法却不能很好地解决这个问题。原因在于批量归一化只作用于RNN输出的数据而对于RNN内部每个时刻的输出却不能处理。因此对于循环神经网络常用的方法是层归一化(Layer Normalization)。与批量归一化不同的是层归一化计算的不是每个批量数据的统计量而是每个样本特征的统计量。在RNN中,如果把神经元每个时刻输出的隐藏状态看作一个特征,则层归一化计算的就是每个神经元所有隐藏状态的统计量。

使用tf.keras我们可以很方便的自定义一个带有层归一化的RNN单元:

class RNNWithLayerNormalization(keras.layers.Layer):
    def __init__(self,units,activation='tanh',**kwargs):
        super().__init__(**kwards)
        # 输出和隐藏状态与神经元数量一致
        self.state_size=units
        self.output_size=units
        # 不使用激活函数因为之后要进行层归一化
        self.simple_rnn=keras.layers.SimpleRNNCell(units,activation=None)
        # 定义层归一化
        self.layer_norm=keras.layers.LayerNormalization()
        # 定义激活函数
        self.activation=keras.activation.get(activation)
    def call(self,inputs,states):
        # 计算新的输出和隐藏状态
        outputs,states=self.simple_rnn(inputs,states)
        # 层归一化输出
        normalized_outputs=self.layer_norm(outputs)
        # 激活函数计算新的输出
        activated_outputs=self.activation(normalized_outputs)
        # 返回输出和隐藏状态(相同)
        reuturn activated_outputs,[activated_outputs]

Long Short Term Memory (LSTM)

循环神经网络(RNN)_第2张图片

LSTM是循环神经网络一个非常重要的变体,其内部的构造相比于其他神经元非常复杂,网络上也有很多对LSTM的介绍,本文不再对其数学表达进行推导,感兴趣的读者可以参考这篇博客,讲解的非常清晰。简单地来说,LSTM中存在两种状态和三个门控制器。长期状态由遗忘门和输入门负责控制,长期状态经过输出门后会输出短期状态(当前时刻输出)。这其中长期状态只负责记录关键信息而不参与任何其他的计算,而短期状态和RNN中的隐藏状态一样会和下一时刻的输入x^{t}一起作为输入参与计算。上图的四个全连接层中,g_{(t)}的作用与CNN中的神经元一样,只是现在它不直接输出结果而是需要经过输出门的控制再添加到长期状态中,因此通常情况下g_{(t)}使用与RNN相同的默认激活函数tanh。其他三个全连接层分别起到遗忘门,输入门和输出门的作用。从图中可以看到每个门控制器的输出都是以element-wise的方式与输入进行计算,也就是说每个门可以控制输入中对应值是否参与下一次计算。每个门使用的激活函数为sigmoid,输出分布在0到1之间,可以达到门控制的效果。

LSTM是一个相当复杂的结构,但其对于长序列的处理效果非常好。LSTM的成功之处可以理解为使用了一个稳定的长期状态来保存序列里的关键信息而再像普通RNN一样递归地寻找之前序列中的有效信息。LSTM还有一个简化版的变体,门控循环单元(Gated Recurrent Unit),其基本原理与LSTM相似,但将两个状态合并为一个状态并且使用一个门控制器来控制输出门和输入门。

写在最后的话

如有错误请指正

转载请注明出处

你可能感兴趣的:(学习心得,神经网络,rnn,深度学习)