RNN

Baileys2022年5月25日
大约 10 分钟...

一、RNN的几种基本的结构

1.1 结构一

结构一

上述所示RNN的结构中,输入序列为X=(x0,x1,...,xl1)X=(x^{0},x^{1},...,x^{l-1})。对于任意时刻tthth^{t}ht1h^{t-1}xtx^{t}共同决定。且目标时刻tt对应于目标输出yty^{t}。这样的结构很适合时间序列类预测的任务,模型可以通过之前的信息ht1h^{t-1}和当前的输入xtx_{t}来预测当前时刻的输出y^t\hat{y}^{t}。该结构最大的问题是时刻tt的损失LtL^{t}与所有之前的状态ht1h^{t-1},ht2h^{t-2},...,h0h^{0}都有关系。因此求hth^{t}时,要先求ht1h^{t-1}。在反向传播时,需要对每一个输入进行前向传播过程来计算网络的activation,而RNN的顺序计算,会导致难以并行化,导致训练较慢。

1.2 结构二

结构二

与结构一相比,这种结构取消了相邻时刻间状态hh之间的连接,取而代之的是前一时刻的输出ot1o^{t-1}与当前时刻状态hth^{t}的连接,此时仍然是顺序计算,但oo的维度通常小于hh,因此训练参数可能减少。模型的缺点主要是yty^{t}中的信息远少于hth^{t},因此模型对历史信息的学习能力不如结构一。

这种网络可以采用teacher forcing的方法来训练,hth^{t}的输入部分来自ot1o^{t-1},而ot1o^{t-1}本身在训练过程中受yt1y^{t-1}的约束(ot1o^{t-1}要尽可能逼近yt1y^{t-1}),因此可以直接使用yt1y^{t-1}来代替ot1o^{t-1}作为hth^{t}的输入。

1.3 结构三

结构三

该结构的RNN仅在最后时刻ll才会有输出olo^{l}

二、前向传播

2.1 理论推导

结构三
定义完全连接的循环神经网络,输入为xtx_{t},输出为yty_{t}

ht=f(Uht1+Wxt+b)h_{t}=f(Uh_{t-1}+Wx_{t}+b)

yt=Vhty_{t}=Vh_{t}

提示

其中hh为隐状态,f()f(\cdot)为非线性激活函数,通常使用Logisitic函数或Tanh函数,UUWWbbVV为网络参数。

xtRMx_{t}{\in}{\mathcal{R}^{M}}表示时刻t网络的输入,htRDh_{t}{\in}{\mathcal{R}^{D}}表示隐藏层状态,URD×DU{\in}{\mathcal{R}}^{D{\times}D}表示状态-状态权重矩阵,WRD×MW{\in}{\mathcal{R}}^{D{\times}M}表示状态-输入权重矩阵,bRDb{\in}{\mathcal{R}}^{D}表示偏置向量。

若将每个时刻的状态都看作前馈神经网络的一层,循环神经网络可以看作在时间维度上权值共享的神经网络。

2.2 代码实现

使用矩阵的推导。

def get_params(embedding_size, output_size, num_hiddens, device):
    """
    获取RNN的参数

    Args:
        embedding_size: 词嵌入的维度
        output_size: 输出的维度
        num_hiddens: 隐藏层大小
        device: 训练设备
    
    Returns:
        参数的元组
    """
    # 输入的维度假设为词嵌入的维度
    num_inputs = embedding_size
    # 输出的维度假设为output_size
    num_outputs = output_size
    # 使用正态分布初始化权重
    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.1
    # 隐藏层的参数
    W = normal((num_inputs, num_hiddens))
    U = normal((num_hiddens, num_hiddens))
    b_h = torch.zeros(num_hiddens, device=device)
    # 输出层参数
    V = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # 附加梯度
    params = [W, U, b_h, V, b_q]
    for param in params:
        param.requires_grad_(True)
    return params

三、应用模式

3.1 序列到类别模式

序列到类别模式

提示

主要用于序列数据的分类:输入数据为序列,输出为类别。

输入的样本x1:T=(x1,...,xT)x_{1:T}=(x_{1},...,x_{T})为一个长度为T的序列,输出类别y{1,...,C}y{\in}{\{}1,...,C{\}},将样本xx,按不同时刻输入到循环神经网络中,得到不同时刻的隐藏状态h1,...,hTh_{1},...,h_{T},可以将hTh_{T}看作整个序列的最终表示,并输入给分类器g()g(\cdot)进行分类。

如上图所示,除了使用最后时刻的状态作为整个序列的表示,也可以使用整个状态的所有状态进行平均,并使用这个平均状态作为整个序列的表示,即y^=g(1Tt=1Tht)\hat{y}=g(\frac{1}{T}\sum\limits_{t=1}^{T}h_{t})

3.2 同步的序列到序列模式

同步的序列到序列模式

提示

主要用于序列标注任务,即每一时刻都有输入和输出,输入序列和输出序列的长度相同。

输入的样本x1:T=(x1,...,xT)x_{1:T}=(x_{1},...,x_{T})为一个长度为T的序列,输出为序列y1:T=(y1,...,yT)y_{1:T}=(y_{1},...,y_{T})。将样本xx,按不同时刻输入到循环神经网络中,得到不同时刻的隐藏状态h1,...,hTh_{1},...,h_{T},每个时刻的隐状态hth_{t}代表了当前时刻的和历史的信息,并输入给分类器g()g(\cdot)得到当前时刻的标签y^\hat{y},即y^t=g(ht),t[1,T]\hat{y}_{t}=g(h_{t}), \forall{t}{\in}[1,T].

3.3 异步的序列到序列模式

异步的序列到序列模式

f1()f_{1}(\cdot)f2()f_{2}(\cdot)分别为编码器和解码器的循环神经网络,则编码器-解码器模型可以写为:

ht=f1(ht1,xt),t[1,T]h_{t}=f_{1}(h_{t-1},x_{t}),{\forall}t{\in}[1,T]

hT+t=f2(hT+t1,y^t1),t[1,M]h_{T+t}=f_{2}(h_{T+t-1},\hat{y}_{t-1}),{\forall}t{\in}[1,M]

y^t=g(hT+t),t[1,M]\hat{y}_{t}=g(h_{T+t}),{\forall}t{\in}[1,M]

其中g()g(\cdot)为分类器,y^t\hat{y}_{t}为预测输出y^t\hat{y}_{t}的向量表示。 在解码器通常采用自回归模型,每个时刻的输入为上一时刻的预测结果y^t1\hat{y}_{t-1}。如上图所示,其中<EOS><EOS>表示输入序列的结束,虚线表示将上一时刻的输出作为下一时刻的输入。

提示

异步的序列到序列模式也称为编码器-解码器模型。即输入序列和输出序列不需要严格的对应关系,也不需要保持相同的长度,例如机器翻译任务。

输入的样本x1:T=(x1,...,xT)x_{1:T}=(x_{1},...,x_{T})为一个长度为T的序列,输出为长度为M的序列y1:M=(y1,...,yM)y_{1:M}=(y_{1},...,y_{M})

异步的序列到序列模式一般通过先编码后解码的方式来实现,先将样本xx按不同时刻输入到一个循环神经网络(编码器)中,得到其编码hTh_{T},然后再使用另一个循环神经网络(解码器),得到输出序列y^1:M\hat{y}_{1:M}。为了建立输出序列之间的依赖关系,再解码器中通常使用非线性的自回归模型。

四、参数学习

循环神经网络的参数可以通过梯度下降的方法来进行学习。

以随机梯度下降为例,给定一个训练样本(x,y)(x,y),其中x1:T=(x1,...,xT)x_{1:T}=(x_{1},...,x_{T})为长度是TT的输入序列,y1:T=(y1,...,yT)y_{1:T}=(y_{1},...,y_{T})是长度为TT的标签序列,每个时刻tt都有一个监督信息yty_{t},我们定义时刻tt的损失函数为

Lt=L(yt,g(ht))\mathcal{L}_{t}={\mathcal{L}}(y_{t},g(h_{t}))

其中g(ht)g(h_{t})为第tt时刻的输出,L\mathcal{L}为可微分的损失函数,如交叉熵等,整个序列的损失函数为

L=t=1TLt\mathcal{L}=\sum_{t=1}^{T}{\mathcal{L}}_{t}

则整个序列的损失函数L\mathcal{L}关于参数UU的梯度为

LU=t=1TLtU\frac{\partial{\mathcal{L}}}{\partial{U}}={\sum_{t=1}^{T}{\frac{\partial{\mathcal{L}_{t}}}{\partial{U}}}}

即每个时刻损失Lt\mathcal{L}_{t}对参数UU的偏导数之和。

BPTT和RTRL比较:在循环神经网络中,一般网络输出维度要远低于输入维度,因此BPTT算法的计算量会小,但是BPTT算法需要保存中间所有时刻的中间梯度,空间复杂度高。RTRL算法不需要梯度回传,因此非常适用于需要在线学习或无限序列的任务中。

4.1 随时间反向传播算法 BPTT

算法主要思想:通过类似前馈神经网络的误差反向传播算法来计算梯度。

BPTT算法将循环神经网络看作一个展开的多层前馈网络,其中“每一层”对应循环网络中的“每个时刻”。这样,循环神经网络可以按照前馈神经网络中的反向传播算法计算参数梯度。在“展开”的前馈神经网络中,所有层的参数是共享的,因此参数的真实梯度是所有“展开层”的参数梯度之和。

计算偏导数LtU\frac{\partial{\mathcal{L}_{t}}}{U}:首先计算第tt时刻损失对参数UU的偏导数。
因为参数UU和隐藏层在每个时刻k(1kt)k(1{\leq}k{\leq}t)的净输入zk=Uhk1+Wxk+bz_{k}=Uh_{k-1}+Wx_{k}+b有关(根据前向传播计算公式),因此tt时刻的损失函数Lt\mathcal{L}_{t}关于参数uiju_{ij}的梯度为(链式法则):

Ltuij=k=1t+zkuijLtzk{\frac{\partial{L_{t}}}{u_{ij}}}=\sum_{k=1}^{t}{\frac{\partial{^{+}z_{k}}}{\partial{u_{ij}}}\frac{\partial{L_{t}}}{\partial{z_{k}}}}

其中+zkuij\frac{\partial{^{+}z_{k}}}{\partial{u_{ij}}}为直接偏导数,即zk=Uhk1+Wxk+bz_{k}=Uh_{k-1}+Wx_{k}+bhk1h_{k-1}保持不变,对uiju_{ij}求偏导数,得到

+zkuij=[0,...,[hk1]j,...,0]\frac{\partial{^{+}z_{k}}}{\partial{u_{ij}}}=[0,...,[h_{k-1}]_{j},...,0]

其中[hk1]j[h_{k-1}]_{j}为第k1k-1时刻隐状态的第jj维。
定义误差项δt,k=Ltzk{\delta_{t,k}}=\frac{\partial{L_{t}}}{\partial{z_{k}}}为第tt时刻的损失对第kk时刻隐藏层神经层的净输入zkz_{k}的导数,则当1k<t1{\leq}k<t

δt,k=Ltzk=hkzkzk+1hkLtzk+1{\delta_{t,k}}=\frac{\partial{\mathcal{L}_{t}}}{\partial{z_{k}}}=\frac{\partial{h_{k}}}{\partial{z_{k}}}\frac{\partial{z_{k+1}}}{\partial{h_{k}}}\frac{\partial{L_{t}}}{\partial{z_{k+1}}}

因此有

δt,k=diag(f(zk))UTδt,k+1{\delta_{t,k}}=diag(f^{'}(z_{k}))U^{T}{\delta_{t,k+1}}

δt,k{\delta_{t,k}}+zkuij\frac{\partial{^{+}z_{k}}}{\partial{u_{ij}}}代入到Ltuij{\frac{\partial{L_{t}}}{u_{ij}}}中得到

Ltuij=k=1t[δt,k]i[hk1]j{\frac{\partial{L_{t}}}{u_{ij}}}=\sum_{k=1}^{t}[\delta_{t,k}]_{i}[h_{k-1}]_{j}

写成矩阵形式有

LtU=k=1tδt,khk1T\frac{\partial{\mathcal{L_{t}}}}{U}=\sum_{k=1}^{t}{\delta_{t,k}}h_{k-1}^{T}

代入到LU\frac{\partial{\mathcal{L}}}{\partial{U}},得到整个序列的损失函数L\mathcal{L}关于参数UU的梯度

LU=t=1Tk=1tδt,khk1T\frac{\partial{\mathcal{L}}}{\partial{U}}=\sum_{t=1}^{T}\sum_{k=1}^{t}\delta_{t,k}h_{k-1}^{T}

同理可得L\mathcal{L}关于权重WW和偏置bb的梯度为

LW=t=1Tk=1tδt,kxkT\frac{\partial{\mathcal{L}}}{\partial{W}}=\sum_{t=1}^{T}\sum_{k=1}^{t}\delta_{t,k}x_{k}^{T}

Lb=t=1Tk=1tδt,k\frac{\partial{\mathcal{L}}}{\partial{b}}=\sum_{t=1}^{T}\sum_{k=1}^{t}\delta_{t,k}

误差随时间进行反向传播算法的示例:
BPTT
由上述推导过程可知,在BPTT算法中,参数的梯度需要在一个完整的“前向”计算和“反向”计算后才能得到并进行参数更新。

4.2 实时循环学习算法 RTRL

实时循环学习通过前向传播的方式来计算梯度。
假设循环神经网络中第tt时刻的状态为ht+1h_{t+1}

ht+1=f(zt+1)=f(Uht+Wxt+1+b)h_{t+1}=f(z_{t+1})=f(Uh_{t}+Wx_{t+1}+b)

其中关于参数uiju_{ij}的偏导数为

ht+1uij=(+zt+1uij+htuijUT)ht+1zt+1\frac{\partial{h_{t+1}}}{\partial{u_{ij}}}=(\frac{\partial{^{+}z_{t+1}}}{\partial{u_{ij}}}+\frac{\partial{h_{t}}}{\partial{u_{ij}}}U^{T})\frac{\partial{h_{t+1}}}{\partial{z_{t+1}}}

ht+1uij=(+zt+1uij+htuijUT)diag(f(zt+1))\frac{\partial{h_{t+1}}}{\partial{u_{ij}}}=(\frac{\partial{^{+}z_{t+1}}}{\partial{u_{ij}}}+\frac{\partial{h_{t}}}{\partial{u_{ij}}}U^{T})diag(f'(z_{t+1}))

RTRL算法从第1个时刻开始,除了计算循环神经网络的隐状态之外,还依次前向计算偏导数h1uij\frac{\partial{h_{1}}}{\partial{u_{ij}}},h2uij\frac{\partial{h_{2}}}{\partial{u_{ij}}},h3uij\frac{\partial{h_{3}}}{\partial{u_{ij}}},...
假设在第tt时刻存在一个监督信息,其损失函数为Lt\mathcal{L}_{t},可以同时计算损失函数对uiju_{ij}的偏导数

Ltuij=htuijLtht\frac{\partial{\mathcal{L}_{t}}}{\partial{u_{ij}}}=\frac{\partial{h_{t}}}{\partial{u_{ij}}}\frac{\partial{\mathcal{L}_{t}}}{\partial{h_{t}}}

这样可以在tt时刻,实时计算损失Lt\mathcal{L}_{t}关于参数UU的梯度,并更新参数。参数WWbb同样按上述方法实时计算。

参考文献

[1] 邱锡鹏,神经网络与深度学习,机械工业出版社,https://nndl.github.io/, 2020.
[2] 深度学习(三):详解循环神经网络RNN,含公式推导open in new window
[3] 动手学深度学习open in new window
[4] 零基础入门深度学习(5) - 循环神经网络open in new window

评论
Powered by Waline v2.6.1