1. 模型定义

循环神经网络(recurrent neural network, RNN)是一类专门设计处理不定长序列数据的神经网络。

与使用一种新计算1作为核心的卷积神经网络不同,循环神经网络仍使用特征的线性组合作为计算核心,并使用共享参数策略使模型能泛化不同长度的序列数据。

2. 循环神经网络的由来:从全连接神经网络(DNN)到循环神经网络(RNN)

2.1 全连接神经网络的不足

最初,研究者直接将时间序列看作特征向量,将序列每一时刻的观测作为一种特征输入全连接神经网络,来预测序列的标签。其Vanilla模型2

循环神经网络分类问题 循环神经网络的模型_深度学习


图1 前馈神经网络


循环神经网络分类问题 循环神经网络的模型_神经网络_02


可见,全连接网络模型可泛化的样本要求其特征向量的维度固定(即序列长度固定),不能很好处理具有序列长度不固定特性的时间序列数据,由此发展出了循环神经网络(RNN)模型。

2.2 循环神经网络的诞生

循环神经网络先为序列每一时刻的观测,单独构建一个基于全连接网络(全连接前馈神经网络,Fully Connected Feedforward Neural Network)的子模型,来预测此时刻序列的隐藏状态(或称:隐藏变量),然后根据该隐藏状态预测出序列此时刻的标签(这是模型的输出层)。Vanilla循环神经网络(RNN)的结构如下图所示:

循环神经网络分类问题 循环神经网络的模型_神经网络_03


图2 循环神经网络组成单元中的隐藏状态计算

循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_04
由上图可见,子模型的输入为本时刻序列观测和上一时刻的隐藏状态,且在不同时间步上共享同一组线性组合的权重参数(简称:共享参数),使模型能泛化不同长度的序列数据。

如果在每个时间步上使用独立的参数,不但不能泛化到训练时没有见过的序列长度,而且也不能在时间维度上共享不同序列长度和不同位置的统计强度。当信息的特定部分会在序列内多个位置出现时,这样的共享尤为重要。

3. 循环神经网络的传播

3.1 循环神经网络的前向传播

循环神经网络的前向传播过程,先沿时间轴方向,以循环神经网络分类问题 循环神经网络的模型_神经网络_05中隐藏状态计算单元为单位,从 循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_06 时刻传播到 循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_07 时刻,形成一层单向循环神经网络;然后再沿网络层级方向,以一层循环神经网络为单位,逐层叠加形成深度循环神经网络。此外,也可以在一层网络结构中添加两组(时序)传播方向相反的单向循环神经网络,组成一层双向循环神经网络

循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_08 时刻,单向循环神经网络的前向传播公式如下所示(且深度循环神经网络和双向循环神经网络的前向传播公式与之类似,可类比得到):
循环神经网络分类问题 循环神经网络的模型_神经网络_09

式中 循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_10 代表批量大小(batch size),即每个小批量中的样本数量;循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_11 代表词向量的维度;循环神经网络分类问题 循环神经网络的模型_神经网络_12循环神经网络分类问题 循环神经网络的模型_rnn_13

3.2 循环神经网络的反向传播

神经网络模型采用梯度下降法得到模型参数的极大似然估计,反向传播是求损失函数对模型参数的偏导数(即参数梯度)并用其更新该模型参数的过程。

一般来讲,模型的损失函数会是一个极其复杂的复合函数,直接按导数公式求其对某模型参数偏导数的解析式十分繁琐。实践中,常根据导数的链式法则,借助计算图快速得到损失函数关于模型某参数的偏导数。在求解偏导数时,计算图将损失函数中涉及参数 循环神经网络分类问题 循环神经网络的模型_深度学习_14 的每一个子项转化成一条由损失函数节点通向模型参数节点的路径,然后将偏导数求解过程简化为求解每条路径中上一节点对本节点的导数之和的过程。且求上一节点对本节点导数时,在连接两节点的前向传播公式中,除本节点所代表参数外,所有变量均可视作常数

为更好地说明RNN模型的反向传播过程,本文根据循环神经网络分类问题 循环神经网络的模型_rnn_15所示前向传播过程,绘制出一幅具有三个时间步的循环神经网络模型依赖关系的计算图3。图中未着色的方框表示变量,着色的方框表示参数,圆表示运算符。且为简单起见,本文将考虑一个无偏差的单向循环神经网络,并认为激活函数为恒等映射(即:循环神经网络分类问题 循环神经网络的模型_循环神经网络_16。该计算图如下所示:

循环神经网络分类问题 循环神经网络的模型_循环神经网络_17


图3 循环神经网络计算图

循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_18

我们先沿计算图,求解反向传播中损失函数对模型各参数的偏导数(梯度)公式:

循环神经网络分类问题 循环神经网络的模型_rnn_19循环神经网络分类问题 循环神经网络的模型_深度学习_20

由计算图可见,损失函数对于隐藏状态 循环神经网络分类问题 循环神经网络的模型_循环神经网络_21 的偏导数有两种可能的情况:当 循环神经网络分类问题 循环神经网络的模型_神经网络_22 时,循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_23 只与当前时刻模型的输出 循环神经网络分类问题 循环神经网络的模型_深度学习_24 有关;而当 循环神经网络分类问题 循环神经网络的模型_rnn_25 时,循环神经网络分类问题 循环神经网络的模型_循环神经网络_21 即与当前时刻模型的输出 循环神经网络分类问题 循环神经网络的模型_深度学习_24 有关,也与后一时刻的隐藏状态 循环神经网络分类问题 循环神经网络的模型_深度学习_28

循环神经网络分类问题 循环神经网络的模型_神经网络_29
将上述递归公式展开后,根据数学归纳法整理可得对任意时间步(循环神经网络分类问题 循环神经网络的模型_神经网络_30)的损失函数关于隐藏状态的偏导数的通项公式:

循环神经网络分类问题 循环神经网络的模型_神经网络_31

循环神经网络分类问题 循环神经网络的模型_rnn_32 时,得到 循环神经网络分类问题 循环神经网络的模型_rnn_33(这是导致RNN模型出现梯度消失,存在长期依赖问题的根本原因),使得损失函数关于隐藏状态梯度的通项公式等于:
循环神经网络分类问题 循环神经网络的模型_循环神经网络_34进而得到损失函数对模型参数 循环神经网络分类问题 循环神经网络的模型_神经网络_35循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_36

循环神经网络分类问题 循环神经网络的模型_深度学习_37循环神经网络分类问题 循环神经网络的模型_深度学习_38

综上所述,可得到循环神经网络模型的反向传播公式:

循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_39

由上述反向传播公式4可见,当 循环神经网络分类问题 循环神经网络的模型_循环神经网络_40 较大时,循环神经网络分类问题 循环神经网络的模型_神经网络_35 中小于1的特征值将会消失(趋近于0),大于1的特征值将会发散(趋近于无穷大)。这导致训练过程中,模型参数 循环神经网络分类问题 循环神经网络的模型_神经网络_35循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_36 容易出现梯度消失梯度爆炸现象,使距离句子结尾较远处单词的信息不再影响模型参数地更新(即:模型不能学习到距离句子结尾处较远的单词所蕴含的特征信息),这被称作RNN模型的长期依赖问题

4. 模型的代码实现

循环神经网络的实现代码如下,分别给出了基于目前两大主流框架的实现。值得注意的是循环神经网络中,隐藏层的参数初始化方法,其中上一时刻隐藏状态的权重参数 循环神经网络分类问题 循环神经网络的模型_神经网络_35 应采用正交初始化(Orthogonal Initialization)方法,以避免在沿时间步传播过程中出现梯度消失或梯度爆炸现象。而本时刻输入的权重参数 循环神经网络分类问题 循环神经网络的模型_循环神经网络分类问题_36 不涉及深度网络传播,可采用正常的 Xavier uniform 初始化方法。在LSTM、GRU等其他RNN类网络中,参数的初始化方法与此一致

4.1 TensorFlow 框架实现

4.2 Pytorch 框架实现

"""
v2.0 修复RNN参数初始化不当,引起的时间步传播梯度消失问题。   2022.04.28
"""
import torch
from torch import nn
from torch.nn import functional as F
from torch.nn.parameter import Parameter


#
class Simple_RNN_Cell(nn.Module):
    def __init__(self, token_dim, hidden_dim
                 , activation=nn.Tanh()
                 , use_bias=True
                 , kernel_initializer=nn.init.xavier_uniform_
                 , recurrent_initializer=nn.init.orthogonal_
                 , bias_initializer=nn.init.zeros_
                 , device="cpu"):
        super().__init__()
        # 超参定义
        self.hidden_dim = hidden_dim
        self.device = device
        self.kernel_initializer = kernel_initializer
        self.recurrent_initializer = recurrent_initializer
        self.bias_initializer = bias_initializer
        #
        self.Hidden = nn.Linear(token_dim, hidden_dim, bias=use_bias).to(self.device)
        self.RecurHidden = nn.Linear(hidden_dim, hidden_dim, bias=use_bias).to(self.device)
        self.Activation = activation.to(self.device)
        # 参数初始化
        self.kernel_initializer(self.Hidden.weight)
        self.recurrent_initializer(self.RecurHidden.weight)
        self.bias_initializer(self.Hidden.bias), self.bias_initializer(self.RecurHidden.bias)

    def forward(self, inputs, last_state: list):
        hidden = self.Hidden(inputs)
        recur_hidden = self.RecurHidden(last_state[-1])
        hidden = self.Activation(
           hidden + recur_hidden
        )
        return [hidden]

    def zero_initialization(self, batch_size):
        return [torch.zeros([batch_size, self.hidden_dim]).to(self.device)]


#
class RNN_Layer(nn.Module):
    """
    bidirectional:  If ``True``, becomes a bidirectional RNN network. Default: ``False``.
    padding:        String, 'pre' or 'post' (optional, defaults to 'pre'): pad either before or after each sequence.
    """
    def __init__(self, rnn_cell, bidirectional=False, pad_position='post'):
        super().__init__()
        self.RNNCell = rnn_cell
        self.bidirectional = bidirectional
        self.padding = pad_position

    def forward(self, inputs, mask=None, initial_state=None):
        """
        inputs:   it's shape is [batch_size, time_steps, token_dim]
        mask:     it's shape is [batch_size, time_steps]
        :return
        sequence:    it is hidden state sequence, and its' shape is [batch_size, time_steps, hidden_dim]
        last_state: it is the hidden state of input sequences at last time step,
                    but, attentively, the last token wouble be a padding token,
                    so this last state is not the real last state of input sequences;
                    if you want to get the real last state of input sequences, please use utils.get_rnn_last_state(hidden state sequence).
        """
        batch_size, time_steps, token_dim = inputs.shape
        #
        if initial_state is None:
            initial_state = self.RNNCell.zero_initialization(batch_size)
        if mask is None:
            if batch_size == 1:
                mask = torch.ones([1, time_steps]).to(inputs.device.type)
            elif self.padding == 'pre':
                raise ValueError('请给定掩码矩阵(mask)')
            elif self.padding == 'post' and self.bidirectional is True:
                raise ValueError('请给定掩码矩阵(mask)')

        # 正向时间步循环
        hidden_list = []
        hidden_state = initial_state
        last_state = None
        for i in range(time_steps):
            hidden_state = self.RNNCell(inputs[:, i], hidden_state)
            hidden_list.append(hidden_state[-1])
            if i == time_steps - 1:
                """获取最后一时间步的输出隐藏状态"""
                last_state = hidden_state
            if self.padding == 'pre':
                """如果padding值填充在序列尾端,则正向时间步传播应加 mask 操作"""
                hidden_state = [
                    hidden_state[j] * mask[:, i:i + 1] + initial_state[j] * (1 - mask[:, i:i + 1])  # 重新初始化(加数项作用)
                    for j in range(len(hidden_state))
                ]
        sequence = torch.reshape(
            torch.unsqueeze(
                torch.concat(hidden_list, dim=1)
                , dim=1)
            , [batch_size, time_steps, -1]
        )

        # 反向时间步循环
        if self.bidirectional is True:
            hidden_list = []
            hidden_state = initial_state
            for i in range(time_steps, 0, -1):
                hidden_state = self.RNNCell(inputs[:, i - 1], hidden_state)
                hidden_list.insert(0, hidden_state[-1])
                if i == time_steps:
                    """获取最后一时间步的cell_state"""
                    last_state = [
                        torch.concat([last_state[j], hidden_state[j]], dim=1)
                        for j in range(len(hidden_state))
                    ]
                if self.padding == 'post':
                    """如果padding值填充在序列首端,则正反时间步传播应加 mask 操作"""
                    hidden_state = [
                        hidden_state[j] * mask[:, i - 1:i] + initial_state[j] * (1 - mask[:, i - 1:i])  # 重新初始化(加数项作用)
                        for j in range(len(hidden_state))
                    ]
            sequence = torch.concat([
                sequence,
                torch.reshape(
                    torch.unsqueeze(
                        torch.concat(hidden_list, dim=1)
                        , dim=1)
                    , [batch_size, time_steps, -1]
                )
            ], dim=-1)

        return sequence, last_state

  1. 卷积神经网络(convolutional neural netword, CNN)以卷积运算(convolution)或更直观的互相关运算(cross-crorelation)作为计算核心。 ↩︎
  2. Vanilla模型指:最初的、最简单版本的模型,即该种模型的原型。 ↩︎
  3. 图片摘自《动手学深度学习》的RNN讲解章节 ↩︎
  4. 循环神经网络分类问题 循环神经网络的模型_循环神经网络_46 ↩︎