Back

动机#

RNN 是一类用来对 序列数据 进行建模的模型。如果使用 MLP 或 CNN 来建模的话,会有如下问题:

  • 固定的输入大小,不适用于变长序列;
  • 无法捕捉长距离 / 时间依赖性;
  • 模型复杂度过高导致的参数爆炸。

语言模型#

2 元语言模型

语言模型基于 马尔可夫假设,即根据词序列中若干个连续的上下文来预测下一个词出现的概率。若上下文长度固定为 2,则称为 2 元语言模型,即用前两个词来预测当前词。

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

该模型使用独热编码来表示词表中的每个词,称为 词向量。若词表为 ['apple', 'banana'],则每个词的词向量表示为:

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

这种表示虽然简单,但缺点也是显而易见的:

  • 随着 词表(Vocabulary) 增加,单个编码是 高维稀疏 的,不利于计算;
  • 词向量彼此之间 正交 ,无法通过余弦相似度进行距离度量。

在后续的技术中,词向量通过词嵌入模型转换为低维的稠密向量,代表性工作就是 word2vec。

但是语言模型随着上下文长度的增加,也会导致参数爆炸的问题,且时间依赖性建模是固定的上下文长度。

那么如何优化该模型呢?我们可以引入两个归纳偏置:

局部依赖性假设 - 除去当前时刻,将历史时刻的信息都编码到一个 隐状态(Hidden State) 中。

p(x1,,xT)=i=1Tp(xtx1,,xt1)=i=1Tg(st2,xt1)\begin{aligned} p(x_1,\cdots,x_T)&=\prod_{i=1}^{T}p(x_t|x_1,\cdots,x_{t-1}) \\ &=\prod_{i=1}^{T}g(s_{t-2},x_{t-1}) \end{aligned}

其中 st2s_{t-2} 就是 t1t-1 前所有信息的隐状态。

时间平稳性假设 - 特征在任意时刻都是有效的,也就是参数共享。

p(xt1+τ,,xtn+τ)=p(xt1,,xtn)p(x_{t_1+\tau},\cdots,x_{t_n+\tau})=p(x_{t_1},\cdots,x_{t_n})

可以发现 RNN 跟 CNN 十分相似:

  • CNN 是空间上的局部性(感受野) / RNN 是时间上的局部性(马尔可夫假设);
  • CNN 是空间上的参数共享 / RNN 是时间上的参数共享。

将这两个先验知识强行加给 MLP 就得到了 RNN,即语言模型的参数化建模。

循环神经网络#

循环单元#

循环层

  • 参数共享体现在任意时刻 WWUUVV 都是一样的,且对应 MLP 不同的层的权重;
  • 局部依赖体现在任意时刻的隐状态只依赖前一个时刻的隐状态与当前的输入。

双向循环神经网络#

双向循环神经网络

引入两组隐状态 h1h^{1}h2h^{2} ,分别表示顺时间和逆时间的状态。Bert 是该类模型 / 双向建模的代表,其好处是信息更完备了。

深度循环神经网络#

如果建模隐状态对应的层数超过两层,就称为深度循环神经网络。其中第 ll 层的第 tt 个时刻的隐状态形式化表示为:

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

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

RNN 用于语言模型#

使用 RNN 来建模语言模型

隐状态的计算公式为:

ht=tanh(Wht1+Uxt)h_t=\text{tanh}(Wh_{t-1}+Ux_{t})

使用 RNN 来建模语言模型的好处和坏处如下:

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

循环神经网络架构#

循环神经网络架构

自回归模型#

自回归(Auto-regressive) 模型指的是根据之前的令牌来预测下一个令牌。形式化表示为:

p(y1,,yT)=t=1Tp(yty1,,yt2,yt1)=t=1Tg(st2,yt1)\begin{aligned} p(y_1,\cdots,y_{T})&=\prod_{t=1}^{T}p(y_t|y_1,\cdots,y_{t-2},y_{t-1}) \\ &=\prod_{t=1}^{T}g(s_{t-2},y_{t-1}) \end{aligned}

注意该模型也将历史信息编码为隐状态。在训练时,模型通常使用真实的目标序列作为输入;而在推理时,它使用自己生成的输出作为下一步的输入。这种训练和推理的失配可能导致模型在推理时表现不佳,因为它未曾见过只依赖于自身生成的输入的情境。

Scheduled Sampling 通过在训练中逐步引入模型自身生成的输出,来减轻这种失配。具体来说,它是一种混合策略,在训练过程中根据一个预设的概率选择输入:

  • 教师强制 - 以高概率(例如 0.9)使用真实的目标序列;
  • 模型生成 - 以低概率(例如 0.1)使用模型生成的输出作为输入。

随着训练的进行,教师强制的概率逐渐降低,而模型生成的概率逐渐增高。

序列到序列模型#

序列到序列建模

序列到序列(Sequence to Sequence) 模型可以看作 给定输入序列,去生成输出序列的联合概率分布。它是 编码器-解码器(Encoder-Decoder) 架构的,编码器负责将输入文本压缩成固定长度的上下文向量,并且期望这个上下文向量能很好的概括输入信息。解码器负责通过这个上下文向量得到输出文本。其中编码器、解码器可以是任意的神经网络架构。

我将该过程看作瓶颈层的计算过程, 首先将特征进行压缩方便特征提取,然后再将特征 “还原”。注意力机制通过关注输入的特定部分来提取需要的特征。

形式化表达为:

p(y1,,yTx1,,xT)=t=1Tp(ytc,y1,,yt1)=t=1Tg(ytc,st2,yt1)\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-2},y_{t-1}) \end{aligned}

其中 cc 是单一的上下文向量、yt1y_{t-1}t1t-1 时刻的输出作为当前时刻 tt 的输入、st2s_{t-2} 是历史时刻的隐状态。

该模型的缺点也十分明显:

  • 将长序列压缩到一个上下文向量必然会导致信息损失;
  • 长序列在梯度回传的时候会导致梯度消失;
  • 在自然语言任务中,输入与输出序列可能是偏序关系,无法使用 RNN 建模;
  • 传统序列到序列模型仅依赖最后一个编码器状态,会造成信息瓶颈。

束搜索#

自回归模型和序列到序列模型都会有一个问题,就是当我们得到了词表中的概率分布之后如何进行采样?若是沿用分类任务中的贪心思想,取条件概率最大的作为当前的输出,并不能保证得到联合概率最大的最优输出序列。

一个自然的做法就是在第一个时刻,选取条件概率最大的 kk 个令牌作为候选输出序列的第一个令牌,然后在接下来的每个时刻,继续选择组合中条件概率最大的 kk 个,直到组合结果达到序列长度。

束搜索

展开来说就是将 <bos> 作为 decoder 第一个时刻的输入,然后选取词表中概率最大的两个令牌 A、C。然后将 A、C 作为 decoder 第二个时刻的输入,分别得到概率最大的 B、E 令牌。依此类推得到最终序列。

时间反向传播#

时间方向传播

最下方的公式计算任意时刻对 UU 的偏导数,例如第四个时刻 L4L_4 对第一个时刻 UU 的偏导数。我们可以发现只有 hth_thsh_s 不是直接依赖关系,需要通过链式法则得到 Jocobian 矩阵的乘积:

hths=htht1ht1ht2hs+1hs=k=s+1tWTdiag[f(Whk1)]\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γ\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\sigma_{\text{max}} 表示权重矩阵 WTW^{\text{T}} 中最大的特征值(特征值分解);
  • γ\gamma 表示 diag[f(Wht1)]\Vert \text{diag}\left[f^{\prime}(Wh_{t-1})\right]\Vert 的上界,依赖于激活函数 ff 偏导数的上界。例如 tanh(x)1|\text{tanh}^{\prime}(x)| \leq 1

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

hthsk=s+1tWTdiag[f(Whk1)](σmaxγ)ts\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)(t-s) 越来越大,整个结果会因为 σmaxγ\sigma_{\text{max}}\gamma 大于(小于) 1 从而引发梯度爆炸(消失)问题。原因就是在时间上的参数共享机制(共享参数 WW)会导致连乘的发生。

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

梯度消失#

长短期记忆单元(LSTM)#

lstm

形式化表示为:

Ct=FtCt1+ItC~tHt=Ottanh(Ct)\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\text{F}_t 控制了前一时刻的记忆单元状态 Ct1\text{C}_{t-1} 在当前状态 Ct\text{C}_t 中的保留程度,若 Ft\text{F}_t 接近 1,则保留大部分信息,梯度也能顺利传播。输入门 It\text{I}_t 和候选单元 C~t\tilde{\text{C}}_t 共同决定了新信息的流入。

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

Ct=FtCt1+ItC~t=FtFt1Ct2+FtIt1C~t1+ItC~t=τ=0t(FtFτ+1遗忘门连续点乘)IτC~τ\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\text{H}_t=\text{Z}_t\odot \text{H}_{t-1}+(1-\text{Z}_t)\odot\tilde{\text{H}}_t

重置门用来捕捉序列中的短期依赖关系,更新门用来捕捉序列中的长期依赖关系。

门控循环单元是长短期记忆单元的简化版本,它将遗忘门和输入门整合成了更新门,将输出门替换为重置门。在实际效果中,它能保持和长短期记忆单元相似的精度,但是训练和推理速度更快。

梯度爆炸#

梯度裁剪#

梯度裁剪(Gradient Clipping) 指的是当梯度超过某一个阈值的时候,就对它进行归一化操作。

g^thresholdg^g^\hat{\bold{g}} \leftarrow \frac{\text{threshold}}{\Vert\bold{\hat{g}} \Vert}\hat{\bold{g}}

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

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

丢弃法#

如果将传统 Dropout 应用到 RNN 中会导致如下问题:

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

正确的做法是在序列处理前采样一次 Mask,然后所有时间戳上使用相同的 Mask,即舍弃相同的神经元。

层归一化#

层归一化(Layer Normalization) 是沿着隐状态通道维度进行归一化操作,形式化地表示为:

z^t=ztμtσt2+ϵγ+β\hat{z}_{t}=\frac{z_t-\mu_t}{\sqrt{\sigma_t^2+\epsilon}}\odot \gamma + \beta

其中 μt\mu_tσt\sigma_t 是时刻 tt 单个特征通道的均值和方差,缩放参数 γ\gamma 和偏移参数 β\beta 在所有时刻都是共享的。

注意力机制的 RNN#

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

在循环神经网络中,注意力机制使得 decoder 的每一个状态都能够看见 encoder 的全局输入信息,从而引导模型算出输入中每一个令牌对当前状态的相关性,也就将注意力分配到了输入中的不同部分。这里需要注意的是每一个时刻状态分配的注意力是不一样的(动态分配)。

那么如何计算呢?一个自然的想法就是通过一个简单的神经网络算出来,形式化地表示为:

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

其中 si1s_{i-1} 表示当前时刻之前的历史时刻的隐状态、xjx_j 表示输入序列中第 jj 个令牌,eije_{ij} 对于第 ii 个状态而言,输入序列的第 jj 个令牌有没有贡献。

注意力机制

我们至下而上地理解上图中的公式,首先使用双向 RNN 作为 encoder 来对 xjx_j 进行增强得到 hjh_j;然后计算相关性 eije_{ij},通过 softmax 函数进行相关性分配得到 αij\alpha_{ij};将分配比率与增强信息进行汇总得到上下文向量 cic_i,然后传入到 decoder 中的 RNN 进行下一个状态 sis_i 的计算。

上下文向量可以看作输入序列中的哪些部分对我当前状态来说是有用的。与序列到序列模型中的单一向量不同,注意力机制使得每个状态对应的上下文向量都不一样。

注意力机制克服了梯度消失的问题,并且特别适合长序列任务。我们可以通过 对齐矩阵(Alignment Matrix) 查看输入序列与输出序列元素之间的相关性。

Google’s NMT System#

nmt

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

记忆力增强的 RNN#

记忆增强的 RNN 通过引入 外部可读写记忆模块,扩展传统 RNN 的记忆容量,解决它在处理长序列时的梯度消失和有限状态容量的问题。核心目标是通过动态存储和检索关键信息,增强模型对长期依赖的捕捉能力。

神经图灵机(NTM)#

Neural Turing Machine(NTM) 是一种结合神经网络与图灵机概念的架构,通过引入可微分的注意力机制实现对外部记忆的读写操作。它的核心目标是赋予神经网络显式的记忆存储能力,使其能够像图灵机一样通过读写头(Read/Write Heads)与外部记忆交互,从而解决复杂序列任务。

神经图灵机

它主要包含以下组件:

  • 控制器(Controller) - 通常为 LSTM 或 MLP,负责生成读写操作的参数;
  • 外部记忆矩阵(Memory) - MtRN×DM_t \in \mathbb{R}^{N\times D},其中 NN 是记忆槽数量,DD 是每个记忆向量的维度;
  • 读写头(Read/Write Head) - 通过注意力权重 wtRNw_t\in \mathbb{R}^{N} 访问记忆,支持内容寻址和位置寻址。

读操作 - 控制器生成查询向量 ktRDk_t \in \mathbb{R}^D 和强度因子 βtR+\beta_t \in \mathbb{R}^{+}。然后将查询向量与外部记忆矩阵计算内容相似度(余弦相似度),并生成内容寻址权重:

wt=exp(βtK[kt,Mt(i)])jexp(βtK[kt,Mt(j)])w_t=\frac{\exp (\beta_t K[k_t, M_t(i)])}{\sum_{j}\exp(\beta_tK[k_t, M_t(j)])}

将内容寻址权重与外部记忆融合生成读取出来的向量:

rtiRwt(i)Mt(i)r_t \leftarrow \sum_{i}^R w_t(i)M_t(i)

写操作 - 控制器生成擦除向量 et[0,1]De_t\in [0,1]^{D} 和添加向量 atRDa_t\in \mathbb{R}^D。然后对外部记忆进行擦除和添加:

M~t(i)Mt1(i)[1wt(i)et]Mt(i)M~t1(i)wt(i)at\tilde{M}_t(i) \leftarrow M_{t-1}(i)\odot[1-w_t(i)e_t] \quad M_t(i) \leftarrow \tilde{M}_{t-1}(i)w_t(i)a_t

可微分神经计算机(DNC)#

可微分神经计算机

状态空间的 RNN#

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

状态空间模型(SSM)#

连续状态空间模型

为适配 RNN 的离散时间步,需要将连续方程离散化。使用 零阶保持(ZOH) 的方法经过一系列计算之后:

Aˉ=exp(ΔtAt)Bˉ=(ΔtAt)1(exp(ΔtAt)I)ΔtBt\begin{aligned} \bar{A}&=\exp(\Delta_tA_t) \\ \bar{B}&=(\Delta_tA_t)^{-1}(\exp(\Delta_tA_t)-I)\cdot \Delta_tB_t \end{aligned}

在线形时不变(LTI)系统的前提下,离散模型在采样时刻与连续系统具有完全相同的输入-输出行为。

Mamba#

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

Mamba

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

At=LinearA(xt)Bt=LinearB(xt)Δt=SoftPlus(LinearΔ(xt))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))

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

Aˉ=exp(Δtdiag(At))Bˉ=ΔtBtexp(ΔtAt)1ΔtAt\begin{aligned} \bar{A}&=\exp(\Delta_t\cdot \text{diag}(A_t)) \\ \bar{B}&=\Delta_tB_t\odot \frac{\exp{(\Delta_tA_t)}-1}{\Delta_tA_t} \end{aligned}

动态参数导致模型失去卷积并行性,Mamba 将循环计算 ht=Aˉht1+Bˉxth_t=\bar{A}h_{t-1}+\bar{B}x_t 转换为并行的类前缀和操作。将离散化、扫描、投影等步骤融合成单个 CUDA 内核,减少内存读写操作。将隐状态分块存储于SRAM/寄存器,避免全局内存访问瓶颈。

Credit#

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