Back

动机#

循环神经网络(RNN)是用来对 序列数据 建模的模型,若使用 MLP 或 CNN 的话会有如下问题:

  • MLP 和 CNN 只能接受固定的输入大小,不适用于变长序列,从而无法捕捉长距离/时间依赖性;
  • MLP 和 CNN 会因为输入序列过长而导致模型复杂度过高,造成参数爆炸。

语言模型#

2 元语言模型

语言模型基于 马尔可夫假设,即根据词序列中若干个连续的上下文来预测下一个词出现的概率(基于给定的上下文输出词表上的概率分布,然后采样得到输出词元)。若上下文长度为 2,则用前两个词来预测下一词。

wiwi1,wi2p^(wiwi1,wi2)\small w_i|w_{i-1},w_{i-2}\sim \hat{p}(w_i|w_{i-1},w_{i-2})

语言模型使用 独热编码(One-Hot) 来表示 词表 中的每个词,称为 词向量(Word Embedding)。但是当词表增加的时候,单个词向量是高维稀疏的,不利于计算。且词向量之间彼此正交,不利于相似度计算。

apple: [1, 0, 0]
banana: [0, 1, 0]
orange: [0, 0, 1]

随着语言模型上下文长度的增加,也会引入参数爆炸的问题,且只具备固定上下文的时间依赖性。那么如何优化该模型呢?我们引入两个归纳偏置:1)局部依赖性假设;2)时间平稳性假设。

归纳偏置

循环神经网络#

循环单元#

循环层

参数共享 体现在任意时间步 W\small WU\small UV\small V 都是一样的,且对应 MLP 不同的层的权重;而 局部依赖 体现在任意时间步的隐状态只依赖前一时间步的隐状态和当前时间步的输入。

参数维度分析 - 输入为 xtRn×d\small x_t \in \mathbb{R}^{n\times d}、隐状态为 htRn×h\small h_t \in \mathbb{R}^{n\times h}、输出为 ytRn×q\small y_t \in \mathbb{R}^{n\times q}、输入权重参数为 URd×h\small U \in \mathbb{R}^{d\times h}、隐状态权重参数为 WRh×h\small W \in \mathbb{R}^{h\times h}、输出权重参数为 VRh×q\small V \in \mathbb{R}^{h\times q},其中 n\small n 表示批量大小、d\small d 表示每个样本的词向量维度、h\small h 表示隐状态维度、q\small q 表示输出词元的词向量维度,即词表的大小。

双向 / 深层循环神经网络#

双向循环神经网络

引入前向/反向的隐状态 h1\small h^{1}h2\small h^{2},则对应的计算公式为:

ht1=g(xtWxh1+ht11Whh1+bh1)前向隐状态计算ht2=g(xtWxh2+ht+12Whh2+bh2)反向隐状态计算yt=htWhq+bq,ht=concat[ht1,ht2]当前时间戳输出计算\small \begin{aligned} h^1_t &= g(x_tW_{xh}^1 + h^1_{t-1}W_{hh}^1 + b_h^1) && \quad \leftarrow \text{\footnotesize 前向隐状态计算} \\ h^2_t &=g(x_tW_{xh}^2 + h^2_{t+1}W_{hh}^2 + b_h^2) && \quad \leftarrow \text{\footnotesize 反向隐状态计算} \\ y_t &= h_tW_{hq} + b_q, \quad h_t = \text{concat}[h_t^1, h_t^2] && \quad \leftarrow \text{\footnotesize 当前时间戳输出计算} \end{aligned}

如果隐状态的层数超过两层,就称为深层循环神经网络。其中第 l\small l 层的第 t\small t 个时刻的隐状态为:

htl=tanh(Wlht1l+Ulhtl1)\small h_t^{l}=\text{tanh}(W_lh_{t-1}^{l}+U_lh_{t}^{l-1})

即来自上一层相同时间步的隐状态以及当前层上一个时间步的隐状态的融合。

RNN 用于语言模型#

使用 RNN 来建模语言模型

  • 理论上可以建模长距离依赖,实际上隐状态信息会随着时间逐渐更新,导致历史信息被遗忘
  • 将历史状态的信息压缩到单个固定大小的隐状态中,且权重参数会因为共享机制的存在不会爆炸。

网络架构#

循环神经网络架构

序列到序列模型#

序列到序列建模

编码器-解码器 架构的序列到序列(S2S)模型可以看作是 给定输入序列,去生成输出序列的联合概率分布。编码器负责将输入序列压缩成固定长度的上下文向量,解码器负责通过该上下文向量得到输出序列。

p(y1,,yTx1,,xT)=t=1Tp(ytc,y1,,yt1)=t=1Tg(ytc,st1,yt1)\small \begin{aligned} p(y_1,\cdots,y_{T^{\prime}}|x_1,\cdots,x_T)&=\prod_{t=1}^{T^{\prime}}p(y_t|c,y_1,\cdots,y_{t-1}) \\ &= \prod_{t=1}^{T^{\prime}}g(y_t|c,s_{t-1},y_{t-1}) \end{aligned}

其中 yt\small y_t 表示当前时间步输出、yt1\small y_{t-1} 表示前一时间步的输出作为当前时间步的输入、c\small c 表示上下文向量、st1\small s_{t-1} 表示历史信息的隐状态。解码器在每个时间步 t\small t 基于上一时间步的隐状态和输出来更新当前隐状态,然后基于当前隐状态生成下一个词元的概率分布。

该架构的缺点也十分明显:1)将长序列压缩到一个上下文向量会导致信息损失,且在梯度回传的时候会导致梯度消失;2)输入与输出序列可能是偏序关系(翻译任务),无法使用 RNN 建模;3)传统 S2S 模型中的解码器仅依赖最后一个编码器的隐状态,会造成信息瓶颈。

束搜索#

当我们得到了词表中的概率分布之后如何基于词表上的概率分布进行采样?若沿用贪心采样的策略取概率最大的词元作为输出,并不能保证得到联合概率最大的最优输出序列(输出语句通顺)。

束搜索

束搜索(Beam Search) 提出在解码的每个时间步都 维护一个大小为 B\small B 的序列集合(称为 “束”),且将 “束” 中的每一个候选序列作为当前输入来预测词表上的概率分布,并取概率最大的 B\small B 个词元,因此会产生 B×B\small B \times B 个新序列,计算新序列的 累积对数分数 并选取最高的 B\small B 个,然后不断重复直至结束。

时间反向传播#

时间方向传播

最下方的公式计算任意时刻对 U\small U 的偏导数,可以发现只有 ht\small h_ths\small h_s 不是直接依赖关系,需要通过链式法则得到 Jocobian 矩阵的乘积:

hths=htht1ht1ht2hs+1hs=k=s+1tWTdiag[f(Whk1)]\small \begin{aligned} \frac{\partial h_t}{\partial h_s}&=\frac{\partial h_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial h_{t-2}}\cdots \frac{\partial h_{s+1}}{\partial h_s}\\ &=\prod_{k=s+1}^{t}W^{\text{T}}\text{diag}\left[f^{\prime}(Wh_{k-1})\right] \end{aligned}

若只考虑相邻时刻,通过柯西不等式得到:

htht1htht1ht1ht2hs+1hs长期依赖WTdiag[f(Wht1)]σmaxγ\small \begin{aligned} \Vert \frac{\partial h_t}{\partial h_{t-1}}\Vert &\leq \Vert\overbrace{ \frac{\partial h_t}{\partial h_{t-1}} \frac{\partial h_{t-1}}{\partial h_{t-2}} \cdots \frac{\partial h_{s+1}}{\partial h_s} }^{\text{长期依赖}}\Vert\\ &\leq \Vert W^{\text{T}}\Vert \Vert \text{diag}\left[f^{\prime}(Wh_{t-1})\right]\Vert \\ &\leq \sigma_{\text{max}}\gamma \end{aligned}
  • σmax\small \sigma_{\text{max}} 表示权重矩阵 WT\small W^{\text{T}} 中最大的特征值(特征值分解);
  • γ\small \gamma 表示 diag[f(Wht1)]\small \Vert \text{diag}\left[f^{\prime}(Wh_{t-1})\right]\Vert 的上界,依赖于激活函数 f\small f 的偏导数的上界,例如 tanh(x)1\small |\text{tanh}^{\prime}(x)| \leq 1

若考虑所有时刻,则可以得到:

hthsk=s+1tWTdiag[f(Whk1)](σmaxγ)ts\small \begin{aligned} \Vert\frac{\partial h_t}{\partial h_s} \Vert &\leq \Vert \prod_{k=s+1}^{t} W^{\text{T}}\text{diag}\left[f^{\prime}(Wh_{k-1})\right]\Vert \\ &\leq (\sigma_{\text{max}}\gamma)^{t-s} \end{aligned}

可以发现当 (ts)\small (t-s) 越来越大,整个结果会因为 σmaxγ\small \sigma_{\text{max}}\gamma 大于(小于) 1 从而引发梯度爆炸(消失)问题,原因是 时间上的参数共享机制(共享参数 W\small W)会导致连乘的发生

一个很自然的解决办法就是将 (ts)\small (t-s) 分成均匀的长度,然后在每个长度内进行参数更新,这会带来真实梯度的近似,也就是 截断时间反向传播(Truncated BPTT) 的思想。

梯度消失#

长短期记忆单元(LSTM)#

lstm

形式化表示为:

Ct=FtCt1+ItC~tHt=Ottanh(Ct)\small \text{C}_t= \text{F}_t \odot \text{C}_{t-1} + \text{I}_t \odot \tilde{\text{C}}_t \\ \text{H}_t= \text{O}_t \odot \text{tanh}(\text{C}_t)

可以发现遗忘门 Ft\small \text{F}_t 控制了前一时刻的记忆单元状态 Ct1\small \text{C}_{t-1} 在当前时刻的状态 Ct\small \text{C}_t 中的保留程度,若 Ft\small \text{F}_t 接近 1,则保留大部分信息,梯度也能顺利传播。输入门 It\small \text{I}_t 和候选单元 C~t\small \tilde{\text{C}}_t 共同决定了新信息的流入。

展开记忆单元的计算公式:

Ct=FtCt1+ItC~t=FtFt1Ct2+FtIt1C~t1+ItC~t=τ=0t(FtFτ+1遗忘门连续点乘)IτC~τ\small \begin{aligned} \text{C}_t&=\text{F}_t\odot \text{C}_{t-1}+ \text{I}_t \odot\tilde{\text{C}}_t \\ &= \text{F}_t\odot \text{F}_{t-1} \odot \text{C}_{t-2} + \text{F}_t \odot \text{I}_{t-1} \odot \tilde{\text{C}}_{t-1}+\text{I}_t \odot\tilde{\text{C}}_t \\ &= \sum_{\tau=0}^{t}(\underbrace{\text{F}_t\odot \cdots \odot \text{F}_{\tau+1}}_{\text{遗忘门连续点乘}} )\odot \text{I}_{\tau} \odot \tilde{\text{C}}_{\tau} \end{aligned}

可以发现记忆单元依赖于多个时刻的遗忘门,也是通过该设计来确保该模型能够在长时间序列中保留重要的信息,使得梯度可以在时间步之间流动,避免了梯度消失问题。

门控循环单元(GRU)#

门控循环单元是长短期记忆单元的简化版本,它将遗忘门和输入门整合成了更新门,将输出门替换为重置门。

gru

形式化表示为:

Ht=ZtHt1+(1Zt)H~t\small \text{H}_t=\text{Z}_t\odot \text{H}_{t-1}+(1-\text{Z}_t)\odot\tilde{\text{H}}_t

重置门用来捕捉序列中的短期依赖关系,更新门用来捕捉序列中的长期依赖关系。在实际效果中,它能保持和长短期记忆单元相似的精度,但是训练和推理速度更快。

梯度爆炸#

梯度裁剪#

梯度裁剪(Gradient Clipping) 是用来预防梯度爆炸的,指的是当梯度超过某一个阈值的时候,就对它进行归一化操作,将其拉回一个可控的范围,并保留了参数更新的正确方向,从而保证训练过程的稳定。

g^thresholdg2g^\small \hat{\bold{g}} \leftarrow \frac{\text{threshold}}{\Vert \bold{g} \Vert_2}\hat{\bold{g}}

实践中在计算梯度之后进行裁剪:

...
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

变分丢弃法#

首先给出应用变分丢弃法的循环神经网络的计算公式:

ht=tanh[W(ht1M1)使用同一个掩码+U(xtM2)]隐状态计算yt=V(htM2)输出计算\small \begin{aligned} h_t &= \text{tanh}[W\overbrace{(h_{t-1}\odot M_1)}^{\text{使用同一个掩码}} + U(x_t\odot M_2)] && \quad \leftarrow \text{\footnotesize 隐状态计算} \\ y_t &= V(h_t\odot M_2) && \quad \leftarrow \text{\footnotesize 输出计算} \end{aligned}

如果将传统 Dropout 应用到循环神经网络中会导致如下问题:

  • 参数共享机制 - 在每个时间步独立应用 Dropout,会导致隐状态/输入权重不一样,破坏了时序一致性;
  • 梯度噪声累计 - 随机的 Dropout 可能在时间维度上引入噪声累计,导致训练不稳定。

Variational Dropout

变分丢弃法(Variational Dropout) 将循环神经网络中的连接分为两种类型,分别是纵向连接(同一个时间步的连接)和横向/循环连接(跨越时间步的的连接),并分别使用不同的丢弃法策略。这样无论是纵向还是横向,丢弃的神经元都是相同的,迫使网络学会利用剩下的神经元来存储和传递信息。

层归一化#

层归一化(Layer Normalization) 是沿着单个样本隐状态的特征维度上计算统计量,不依赖批次中的其他样本。假设隐状态大小为 (n,h)\small (n, h),其中 h\small h 就是特征维度。

h^t=htμtσt2+ϵγ+β\small \hat{h}_{t}=\frac{h_t-\mu_t}{\sqrt{\sigma_t^2+\epsilon}}\odot \gamma + \beta

其中 μt\small \mu_tσt\small \sigma_t 是时间步 t \small t 的隐状态中特征通道的均值和方差,缩放参数 γ\small \gamma 和偏移参数 β\small \beta 在所有时间步都是共享的。

为什么批归一化不适用于 RNN - 1)RNN 中每个时间步的批次中的样本长度不一致,为每个时间步存储独立的统计量是复杂且低效的;2)RNN 推理阶段只处理单个样本,不存在批次概念,且统计量的计算依赖训练阶段的移动平均。

训练推理失配#

RNN 在序列生成任务中存在训练推理失配问题(Training-Inference Shift),即 模型在训练时看到的数据分布(以 “正确” 的历史为前提/教师强制)和在推理时看到的数据分布(以 “自己生成的、可能错误” 的历史为前提)之间存在明显的差异。若在推理阶段的某一步出错,那么错误会迅速累积,导致生成的序列质量下降。

解决该问题的方法是利用课程学习(Curriculum Learning)的思想,在模型训练初期依赖真实数据,随着训练的进行,“强迫” 模型更多地依赖自己生成的数据,从 真实值(简单样本)逐步过渡到预测值(复杂样本)

基于计划采样(Scheduled Sampling)在每一个时间步基于概率来决定使用真实还是预测的数据:

  • 在时间步 t\small tp\small p 的概率选择真实数据 yt1\small y_{t-1}
  • 在时间步 t\small t1p\small 1-p 的概率选择预测数据 y^t1\small \hat{y}_{t-1}

随着训练的进行,概率 p\small p 会逐渐降低,促使模型基于预测数据进行学习,使得模型逐步提升自身的鲁棒性。

注意力机制#

从脑科学的角度出发,注意力功能负责分配认知处理资源以便集中注意力于特定的信息上。深度学习只实现了选择注意力,即 将注意力根据任务的相关性动态地分配到输入中的不同部分 以获得更好的性能。

注意力机制 S2S#

在 S2S 架构中,注意力机制使得解码器(右)在每一个时间步生成词元的时候,都能够看见编码器(左)所有时间步的隐状态,而不仅仅是最后一个。这 使得解码器能够 “动态关注” 编码器中隐状态中的不同部分,找出对当前输出贡献最大的隐状态,并汇聚成上下文向量用来与解码器当前的隐状态结合来生成输出。

通过训练一个神经网络 α\small \alpha 来判断解码器的隐状态 st\small s_t 对编码器的隐状态 ht\small h_t 的关注程度,形式化表示为:

eij=α(si1,hj)=vαTtanh(Wαsi1+Uαhj)\small \begin{aligned} e_{ij}&=\alpha{(s_{i-1},h_j)} \\ &=v_{\alpha}^{\text{T}}\text{tanh}(W_{\alpha}s_{i-1}+U_{\alpha}h_j) \end{aligned}

其中 eij\small e_{ij} 表示编码器的第 j\small j 个隐状态对解码器的第 i\small i 的隐状态的贡献程度。

注意力机制

使用双向 RNN 作为编码器对输入序列 x\small x 进行增强得到隐状态 h\small h,计算相关性分数 eij\small e_{ij} 并通过 Softmax 分配得到相关性权重 a\small a,将其与隐状态加权求和得到上下文向量 c\small c,然后传入到解码器中进行隐状态 s\small s 的计算。

具体链路为 - 编码器输入 -> 编码器隐状态 -> 网络计算相关性分数 -> 相关性权重 -> 上下文向量 -> 解码器隐状态 -> 解码器输出

在 Transfomer 模型中通过 qk 内积计算相关性分数,然后经过 softmax 得到相关性权重,最后与 v 进行加权求和得到上下文向量。

Google’s NMT System#

nmt

Google 神经机器翻译系统是之前所学知识的集大成者,利用了残差连接、双向循环神经网络(序列到序列模型)、注意力机制、逐层的分布式训练。

状态空间的 RNN#

状态空间模型(State Space Model) 与循环神经网络结合旨在通过线性系统理论建模长期依赖关系,同时保留 RNN 的变长序列处理能力。核心目标是 通过状态方程描述隐状态的动态变化,捕捉序列的全局依赖关系。

状态空间模型(SSM)#

连续状态空间模型

为了适配循环神经网络的离散时间步,需要在 线形时不变(LTI) 的背景下将连续时间系统离散化,来构建深度学习数字模型与物理模型的桥梁。使用 零阶保持(ZOH) 假设来得到一个与原连续系统在所有采样时刻表现完全一致的离散模型。

Aˉ=exp(ΔtAt)离散状态转移矩阵Bˉ=(ΔtAt)1(exp(ΔtAt)I)×ΔtBt离散输入矩阵\small \begin{aligned} \bar{A}&=\exp(\Delta_tA_t) && \quad \leftarrow \text{\footnotesize 离散状态转移矩阵} \\ \bar{B}&=(\Delta_tA_t)^{-1}(\exp(\Delta_tA_t)-I)\times \Delta_tB_t && \quad \leftarrow \text{\footnotesize 离散输入矩阵} \end{aligned}

Mamba#

Mamba 是一种 动态选择性状态空间模型,通过输入相关的状态转移机制解决传统 SSM 的静态参数限制。

Mamba

传统 SSM 的参数 A\small AB\small BC\small CΔ\small \Delta 是静态的,而 Mamba 将它们变为输入的函数:

At=LinearA(xt)Bt=LinearB(xt)Δt=SoftPlus(LinearΔ(xt))\small A_t=\text{Linear}_A(x_t) \quad B_t=\text{Linear}_B(x_t) \quad \Delta_t=\text{SoftPlus}(\text{Linear}_\Delta(x_t))

A\small A 为对角矩阵的时候,连续方程的离散化简化为:

Aˉ=exp(Δtdiag(At))离散状态转移矩阵Bˉ=ΔtBtexp(ΔtAt)1ΔtAt离散输入矩阵\small \begin{aligned} \bar{A}&=\exp(\Delta_t \text{diag}(A_t)) && \quad \leftarrow \text{\footnotesize 离散状态转移矩阵} \\ \bar{B}&=\Delta_tB_t\odot \frac{\exp{(\Delta_tA_t)}-1}{\Delta_tA_t} && \quad \leftarrow \text{\footnotesize 离散输入矩阵} \end{aligned}

Credit#

循环神经网路
https://k1tyoo.ink/blog/dl/rnn
Author K1tyoo
Published at January 11, 2025