跳转至

循环神经网络 (RNN)

普通 RNN 的前向传播公式是什么?隐藏状态是如何更新的?

image.png

基本 RNN 的前向传播

image.png

隐藏状态更新

image.png

深层 RNN:可以堆叠多个 RNN 层,第 ll 层的隐藏状态公式为:

image.png


RNN 为什么能处理变长序列?与 CNN 处理序列的方式有何不同?

RNN 处理变长序列的原理

image.png

入。不论序列多长,模型均按时间顺序逐步处理,因此天然适配变长序列。在实现时,只需在输入时按实际长度展开计算图,序列结束后停止更新隐藏状态。对于变长 batch,通过 padding 和 masking 忽略无效时间步即可。

与 CNN 处理序列的不同

  • 处理机制:
  • RNN:串行循环处理,每个时间步的隐藏状态依赖于前一步。本质是时间维度上的“动态系统”,适合序列依赖建模。
  • CNN:在序列上应用一维卷积,利用固定大小的滑动窗口并行计算每个位置的局部上下文特征。它不显式维护贯穿序列的隐藏状态,感受野由卷积核大小和层数决定。为了捕捉长距离依赖,需要堆叠多层或使用空洞卷积来扩大感受野。

  • 并行性:

  • RNN 的递归特性导致时间步之间必须串行计算,难以并行化(但 batch 内不同样本可并行)。
  • CNN 在整个序列上可以完全并行计算,训练和推理速度远快于 RNN。

  • 长距离依赖:

  • RNN 理论上可以记忆无限长的历史,但实际受梯度消失影响,只能捕获有限的上下文。
  • CNN 长距离依赖需要很深的网络或特殊设计(如膨胀卷积),否则感受野受限。

  • 参数量与效率:

  • RNN 参数量与序列长度无关,共享权重。
  • CNN 卷积核参数也与序列长度无关,计算效率高,适合大规模并行处理(GPU 友好)。

因此,CNN 更适用于离线的序列分类和某些需要并行处理的场合,而 RNN 在时间序列预测、在线学习、需要严格顺序处理的场景中仍有优势。


解释 BPTT(随时间反向传播)算法,并说明为什么 RNN 容易出现梯度消失/爆炸。

BPTT(Backpropagation Through Time)

BPTT 是用于训练 RNN 的反向传播算法。思想是将 RNN 在时间维度上展开成一个深度前馈网络(共享权重),然后应用标准的反向传播计算梯度。

步骤:

image.png

梯度消失/爆炸的原因

展开后的 RNN 相当于一个非常深的网络(深度等于序列长度)。隐藏状态的梯度在反向传播时会连乘雅可比矩阵:

image.png

当反向传播经过 T 步时,累计梯度大约为:

image.png

image.png

因此,RNN 难以学习长距离依赖,梯度要么消失(早期时间步学不到信息),要么爆炸(训练不稳定)。


为什么 Sigmoid 和 Tanh 在 RNN 中更容易导致梯度消失?

在 RNN 中,Sigmoid 和 Tanh 作为激活函数用于计算隐藏状态。它们的导数特性加剧了梯度消失:

image.png

在深层网络中,如果激活函数导数始终小于 1,则连乘后梯度呈指数衰减。RNN 的时间步通常很多,因此比普通前馈网络更容易出现梯度消失。即使 Tanh 导数最大为 1,一旦 Whh 的谱范数小于 1,梯度依然会消失。门控机制(如 LSTM)通过引入加性传递路径(细胞状态)使得导数可以不经过压缩的激活函数直接传播,从而缓解此问题。


梯度爆炸可以通过梯度裁剪解决,梯度消失有什么根本性的解决方案?

梯度爆炸的解决方案

梯度裁剪:设定一个阈值,当梯度的 L2 范数超过该阈值时,将梯度缩放至该阈值以下。这保证了参数更新的最大步长,避免爆炸。

梯度消失的根本性解决方案

  1. 门控架构(LSTM、GRU):通过门控机制(遗忘门、输入门)创建一条“恒定误差流”,使得梯度可以在时间上无衰减地回传。细胞状态的更新是线性累加,其导数接近 1,避免了连乘导致的衰减。

  2. 使用 ReLU 类激活函数:IRNN 证明,使用 ReLU 并正确初始化循环权重矩阵为单位矩阵,可使梯度长时间保持稳定。但 ReLU 在 RNN 中可能导致激活爆炸,需配合梯度裁剪。

  3. 正交初始化:将循环权重矩阵初始化为正交矩阵,其谱范数接近 1,能在早期保持梯度范数稳定,减少消失或爆炸风险。

  4. 跳跃连接/高速公路网络:在时间维度上加入残差连接(如 Residual RNN),使梯度可以跳过非线性层传播。

  5. 截断 BPTT:虽然不能根治,但通过限制反向传播的时间步长,避免极长序列导致的严重衰减。训练时常用。

  6. 批归一化/层归一化:应用在时间维度上的归一化可以稳定隐藏状态的分布,有助于梯度流动。

  7. 优化器选择:自适应优化器(如 Adam、RMSProp)通过调整每个参数的学习率,有助于减轻梯度消失的负面影响。

  8. 根本性架构替代:Transformer 使用自注意力机制直接建模全局依赖,并通过残差连接和层归一化彻底解决了长距离梯度传播问题,在 NLP 中逐渐取代 RNN。

从根本上说,改变循环状态更新方式,引入加性更新和恒定误差流是最有效的解决方案,LSTM 即是典范。


LSTM 是如何通过门控机制缓解梯度消失的?画出其内部结构图。

门控机制缓解梯度消失的原理

image.png

因此,LSTM 能够学习到远超普通 RNN 的长距离依赖,有效缓解了梯度消失。梯度爆炸仍可能发生,但可通过梯度裁剪控制。

内部结构图(文字描述)

LSTM 单元包含三个门:

image.png

结构图若用 ASCII 可大致表示:

image.png

注意:o_t 也由 h_{t-1}, x_t 决定,并与经 tanh 的 c_t 相乘产生 h_t。


LSTM 的遗忘门、输入门、输出门各自的计算公式和作用是什么?

image.png

细胞状态更新

image.png

总结:遗忘门控制记忆保留,输入门控制新信息写入,输出门控制信息输出,使 LSTM 能精细管理长期和短期记忆。


细胞状态在 LSTM 中扮演什么角色?为什么它能传递长距离信息?

角色

image.png

因此,细胞状态是 LSTM 解决长距离依赖的关键发明。


GRU 与 LSTM 相比有哪些简化?它的重置门和更新门分别对应什么功能?

GRU 的简化

image.png

image.png

对比:GRU 参数更少,计算更高效,在许多任务上表现与 LSTM 相当。但 LSTM 由于有独立的细胞状态和更精细的门控,在某些需要严格长期记忆的任务上可能更优。


LSTM 和 GRU 在不同任务上的表现差异通常由什么决定?如何选择?

表现差异的决定因素

  • 数据量与序列长度:对于非常长的序列,LSTM 的独立细胞状态可能提供更稳定的记忆,表现略优。但在中等长度序列上,两者差距很小。

  • 任务复杂度:更复杂的序列建模(如语言建模、机器翻译)中,LSTM 的额外门控能力可能带来小幅度提升。简单任务上 GRU 更容易训练,效果相当。

  • 计算资源:GRU 参数量约为 LSTM 的 3/4,训练和推理更快,适合资源受限场景。

  • 超参数调优:GRU 对某些超参数不那么敏感,鲁棒性较好。具体任务上,经过充分调优的 LSTM 常能达到略高的上限,但边际收益可能不如直接使用更多数据或更大模型。

如何选择

  • 如果追求极致性能且有足够计算资源,优先尝试 LSTM(或双向 LSTM)。

  • 若需要快速实验、部署到移动端或模型尺寸受限,GRU 是更好的默认选择。

  • 实践中,两者经常互换使用。许多 NLP 库(如 PyTorch)提供 LSTM 和 GRU 模块,可方便替换对比。

  • 当梯度消失问题显著时,两者表现均远优于 vanilla RNN,但具体选哪个往往通过验证集比较决定。


双向 RNN 的原理是什么?它如何同时利用过去和未来的信息?

原理

image.png

这个表示包含了整个序列的上下文:正向 RNN 捕捉左侧历史,反向 RNN 捕捉右侧未来信息。

训练与推理

训练时,需要完整的序列输入,可以并行计算正向和反向 RNN。但在测试时,如果序列完整可用(如文本分类、序列标注),同样可以双向运行。对于在线预测(仅有过去信息),无法使用反向 RNN,因为未来不可见。

适用场景

  • 序列标注(POS、NER):每个词的标签可能依赖前后文,双向非常有效。

  • 机器翻译(编码器部分):源语言完整可见,可使用双向 RNN 获得更好的源句表示。

  • 语音识别:整句语音可双向处理提升准确率。

它使得模型在每个时间步都能利用全局信息,极大提升了序列理解任务的性能。


双向 RNN 是否适用于在线或因果预测?为什么?

不适用于在线或因果预测

image.png

因果预测的替代

  • 使用单向 RNN(Forward RNN)确保只依赖过去信息。

  • 对于需要右侧上下文但允许一定延迟的场合,可以使用“受限双向 RNN”,即仅使用固定长度的小未来窗口,但严格意义上不是纯因果。

所以,因果预测必须保证模型当前时刻的输出只与当前及过去输入有关,双向 RNN 违反此约束。


堆叠多层 RNN 能增强什么能力?会带来什么问题?

增强的能力

  • 层次化特征学习:较低层 RNN 可能学习到短时间尺度的局部模式(如短语结构),较高层能整合这些模式形成更抽象、长时间尺度的表示(如句子语义)。

  • 更大容量:增加深度可扩展模型参数和计算量,理论上能拟合更复杂的序列映射。

  • 更好的性能:在许多任务(如机器翻译、语音识别)中,深度 RNN 相比浅层有明显精度提升。

带来的问题

  • 梯度消失/爆炸加剧:不仅在时间上,在层与层之间也会发生梯度衰减或爆炸。虽然 LSTM/GRU 缓解了时间维度的消失,但层间仍可能传播困难。

  • 训练困难:收敛变慢,需要更多技巧,如使用残差连接(Deep RNN 加入跨层 shortcut)、批归一化/层归一化(在层间或时间上),以及合适的参数初始化。

  • 过拟合风险增加:参数大幅增加,需要更多数据或正则化(Dropout,尤其变分 Dropout)。

  • 计算量激增:深度增加导致训练和推理时间显著变长,难以并行化(时间步串行)。

缓解措施

  • 使用 Highway RNN 或 Residual RNN 增加层间跳跃连接。

  • 使用层归一化稳定隐藏状态。

  • 使用适当的正则化(Recurrent Dropout、Zoneout)。

  • 当层数很深时,可能考虑替代架构(如 Transformer)来获得更深且易训练的模型。


为什么现代的 NLP 任务中 RNN 逐渐被 Transformer 取代?RNN 还有哪些不可替代的场景?

被取代的原因

  1. 并行化能力:Transformer 的自注意力在整个序列上同时计算,无循环依赖,可充分利用现代 GPU/TPU 的并行处理能力。而 RNN 受时间步串行限制,训练和推理速度慢。

  2. 长距离依赖建模:自注意力直接连接任意两个位置,梯度路径短,没有 RNN 的梯度消失问题,更易捕获超长距离依赖。

  3. 性能上限高:大规模预训练 Transformer 模型(BERT, GPT, T5 等)在几乎所有 NLP 任务上都达到了前所未有的性能,推动领域范式转变。

  4. 多模态统一性:Transformer 架构可轻松应用于文本、图像、语音等,无需针对序列改变核心结构,便于构建多模态基础模型。

RNN 不可替代(或仍有优势)的场景

  1. 流式处理与在线学习:RNN 天然能以恒定内存逐个元素处理无限长序列,适合实时语音识别、在线翻译、传感器数据分析等需要因果推理且延迟极低的场景。Transformer 自注意力需要维护整个序列,计算量随长度增长,难以在线流式处理(虽有 chunk/streaming 变体,但复杂度仍高于 RNN)。

  2. 资源极度受限的嵌入式系统:微控制器、IoT 设备可能仅能运行极简模型,小型 RNN(如 LSTM/GRU)内存占用和计算量远低于 Transformer,且无需存储大量键值缓存。

  3. 生物启发的计算与时序动态:在计算神经科学、动力系统建模中,RNN 作为连续时间动力系统的离散近似,其递归特性更贴近真实神经元处理序列的方式,常用于认知建模、脑机接口等。

  4. 时间序列预测:在金融、气象等传统时间序列领域,RNN 及其变体(如 DeepAR、TCN)依然广泛使用,因为序列通常很长,且需要逐步递归预测未来多步,RNN 结构自然匹配。

  5. 模型压缩与蒸馏:小型的 RNN 容易蒸馏为更简单的状态机或规则系统,用于可解释性或极低资源场景。

  6. 强化学习中的策略网络:在部分可观测环境中,RNN 可作为记忆组件处理历史观测,由于其递归性质,容易集成到策略梯度算法中。

综上,RNN 在需要恒定状态、流式推断、超低资源或生物合理性的领域仍具价值,而 Transformer 主导了大规模批处理和追求极致性能的离线任务。两者未来可能进一步结合,例如用轻量级 RNN 作为 Transformer 中的局部模块或流式接口。


在处理极长序列(如数千个时间步)时,RNN/LSTM 面临什么挑战?

  • 梯度消失与爆炸的残余风险:尽管LSTM的门控机制大幅缓解了梯度消失,但在数千个时间步的序列中,当遗忘门无法保持接近1时,连乘效应仍可能导致梯度指数级衰减或爆炸。而且梯度爆炸虽可裁剪,但消失依然会使极早期的信号无法有效学习。

  • 计算时间和内存随序列长度线性增长:RNN/LSTM 必须按时间步串行计算,无法并行。反向传播时,BPTT 需要保存整个序列的中间隐藏状态和细胞状态,内存占用与序列长度成正比。对于数千步的序列,即便 batch size 很小,显存也可能耗尽。

  • 训练速度极慢:由于时间步之间严格依赖,无法利用GPU的并行性加速序列维度的计算,导致每个迭代耗时巨大。

  • 长期记忆容量饱和:LSTM的细胞状态维度固定,当序列极长且信息丰富时,固定大小的记忆单元可能无法保留所有重要信息,发生“记忆覆盖”或“遗忘”,尽管门控可调节,但容量瓶颈仍存在。

  • 优化困难:损失曲面在极长序列下变得更加崎岖,即使使用自适应优化器,收敛也可能不稳定,需要精心设计学习率调度和梯度裁剪。

  • 数值问题:大量的浮点运算累积可能产生数值溢出或下溢,尤其是涉及sigmoid和tanh的多次运算。

应对策略:截断BPTT、使用跳跃连接(如IndRNN)、引入Transformer或状态空间模型、增大细胞状态维度、更好的初始化、层归一化等。


什么是“长期依赖”问题?LSTM 真的完全解决了它吗?

长期依赖问题

在序列建模中,某个时间步的输出可能依赖于很远的输入。例如,预测一段文本后续的词,可能需要几百个词之前的上下文。对RNN而言,由于梯度消失,信号在时间上反向传播时呈指数衰减,导致网络几乎无法学习到跨越长距离的依赖性。

LSTM 是否完全解决?

LSTM 通过细胞状态和门控机制,提供了一条可以让梯度相对无损传播的线性路径,能够捕捉比普通RNN长得多的依赖(通常到几百步)。但它并未完全解决长期依赖问题,原因如下:

  • 遗忘门和输入门的饱和仍然会导致梯度衰减。若遗忘门偶尔关闭(接近0),该时刻之前的信息就会被截断。

  • 记忆容量有限:细胞状态是固定长度的向量,不能无限存储历史细节。

  • 对超长序列(数千步),即使是LSTM也会出现梯度消失或记忆遗忘。

  • 实验证明,在需要精确回忆极远信息的任务(如复制任务、长距离问答)上,LSTM 的表现仍会随着序列长度增加而下降,而注意力机制可以直接寻址,效果更好。

因此,LSTM 只是显著缓解了长期依赖,并没有根本解决任意长度依赖的问题。真正突破这一限制的是基于注意力机制的Transformer,它能直接访问所有时间步的信息。


如何实现一个简单的 RNN 单元?使用 PyTorch 写出核心代码。

以下手动实现一个vanilla RNN单元,包含前向传播和权重初始化。

import torch
import torch.nn as nn

class SimpleRNNCell(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 输入到隐藏的权重
        self.W_ih = nn.Parameter(torch.randn(hidden_size, input_size) * 0.01)
        # 隐藏到隐藏的权重
        self.W_hh = nn.Parameter(torch.randn(hidden_size, hidden_size) * 0.01)
        # 偏置
        self.b_h = nn.Parameter(torch.zeros(hidden_size))

    def forward(self, x, h_prev):
        # x: (batch, input_size)
        # h_prev: (batch, hidden_size)
        # 线性变换
        linear = torch.matmul(x, self.W_ih.t()) + torch.matmul(h_prev, self.W_hh.t()) + self.b_h
        h_next = torch.tanh(linear)
        return h_next

# 循环使用
class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=1):
        super().__init__()
        self.num_layers = num_layers
        self.hidden_size = hidden_size
        self.cells = nn.ModuleList([
            SimpleRNNCell(input_size if i == 0 else hidden_size, hidden_size)
            for i in range(num_layers)
        ])

    def forward(self, x, h0=None):
        # x: (seq_len, batch, input_size)
        seq_len, batch, _ = x.shape
        if h0 is None:
            h = [torch.zeros(batch, self.hidden_size, device=x.device) for _ in range(self.num_layers)]
        else:
            h = list(h0)  # list of (batch, hidden_size)
        outputs = []
        for t in range(seq_len):
            inp = x[t]
            for layer in range(self.num_layers):
                h[layer] = self.cells[layer](inp, h[layer])
                inp = h[layer]
            outputs.append(h[-1].unsqueeze(0))
        output = torch.cat(outputs, dim=0)  # (seq_len, batch, hidden_size)
        return output, torch.stack(h, dim=0)  # hidden: (num_layers, batch, hidden_size)

实际应用中直接使用 nn.RNN 即可,其内部经过cuDNN优化,速度更快。


为什么 RNN 中常用 Tanh 作为激活函数,而 CNN 中用 ReLU?

RNN 中用 Tanh 的原因

  • 有界性防止爆炸:Tanh 输出范围 [−1,1],在递归计算中,若使用无界的 ReLU,激活值可能随时间步累积而爆炸,因为循环权重乘以激活值会不断放大。Tanh 通过压缩每一轮的输出,避免了激活发散。

  • 对称性与零中心:Tanh 均值为 0,有利于梯度传播。对于 RNN 的递归状态,零中心分布可缓解偏置偏移。

  • 导数特性:Tanh 导数最大为 1,配合良好的权重初始化可使梯度在多个时间步内保持稳定(虽然仍可能消失,但比 Sigmoid 好)。

  • 早期实验表明,ReLU 在 RNN 中容易导致数值不稳定和梯度爆炸(若不配合恰当初始化和裁剪),因此 Tanh 成为 vanilla RNN 和 LSTM 内部激活的默认选择。

CNN 中用 ReLU 的原因

  • 非递归结构:CNN 前馈计算没有循环,激活爆炸风险较低,且 Batch Normalization 能稳定分布。

  • 稀疏性和缓解梯度消失:ReLU 在正区间梯度恒为 1,有助于深层网络中的梯度流动,加速收敛。

  • 计算简单:ReLU 仅为分段线性,推理和梯度计算更快。

注意:现代 RNN 变体如 IndRNN 使用 ReLU 配合特定初始化,可以成功训练。LSTM 的门使用 sigmoid(输出 0-1),内部候选状态使用 tanh,即结合了两种激活。


LSTM 中的“peephole connection”(窥视孔连接)是什么?有什么作用?

定义

image.png

image.png

作用

  • 增强门控的精确性:让门控直接了解细胞状态的内存内容,使其决策更加精细。比如,当细胞状态包含重要信息时,遗忘门可以更好地决定是否保留;输出门可以根据当前状态决定输出哪些部分。

  • 改善对时间序列细微模式的捕捉,在某些任务中(如语音识别)能带来小幅性能提升。

  • 增加额外的参数(每个门多一个与细胞状态维度相同的权重矩阵),计算复杂度稍有增加。

注意:标准 LSTM 通常不包含窥视孔连接,它属于可选的扩展。许多主流框架(如 PyTorch)默认不开启窥视孔。


如何对 LSTM 进行 dropout 正则化?要注意什么?

方法

  1. 逐层 Dropout:在 LSTM 层之后(或之前)应用 nn.Dropout,作用于隐藏状态的输出。这是最简单的方式,仅对层间连接正则化。

  2. 循环 Dropout(Variational Dropout):在时间步维度上共享相同的 dropout 掩码。具体来说,对 LSTM 的输入、隐藏状态或循环权重应用 dropout,且同一个样本在不同时间步使用完全相同的掩码。这样可避免在时间维度上引入不一致的噪声破坏循环动态。原始论文由 Gal & Ghahramani (2016) 提出,PyTorch 的 nn.LSTM 支持 dropout 参数(仅当层数≥2时生效),这个 dropout 是应用于除最后一层外的所有 LSTM 层输出的,且它是变分 dropout 的一种近似?PyTorch 的实现是在每一层 LSTM 的输出(即下一层的输入)上施加 dropout,同一个序列的时间步使用相同的 dropout 掩码吗?实际上 PyTorch 文档指出“如果 LSTM 的 dropout 参数非零,则在除最后一层外的每层 LSTM 输出上引入 Dropout 层”,并未明确是否为变分 dropout。经查,PyTorch 1.12 及以后,LSTMdropout 是在每个时间步独立采样掩码,这可能破坏时间结构。若要真正的变分 dropout,需要自定义实现。

  3. Zoneout:随机保留上一时间步的隐藏状态(或细胞状态)而不是将其置零,这可以看作一种特殊的 dropout,对 RNN 有效。

image.png

注意事项

  • 不要在时间维度上随意改变掩码,否则会破坏 RNN 的时序依赖学习。应使用变分 dropout 或确保掩码在时间上恒定。

  • 若使用 PyTorch 内置 dropout 参数,注意它只对多层 LSTM 的层间输出做 dropout,而非循环连接内部。

  • 可以结合使用 LSTM 之后的逐层 Dropout 和 Zoneout。

  • Dropout 会减慢收敛,需要适当增加训练周期或调整学习率。


什么是 Layer Normalization 在 LSTM 中的应用?它与 BN 相比,在 LSTM 中为何更合适?

Layer Normalization (LN) 在 LSTM 中的应用

LN 对每个样本的所有隐藏单元进行归一化,独立于 batch。在 LSTM 中,LN 可以加在:

image.png

为什么 LN 比 BN 更合适

  • 不依赖 batch 统计量:RNN 的序列长度可能不同,且 batch 内样本序列长度各异(需 padding),BN 在时间维度的统计极不稳定且无法正确处理变长序列。LN 对每个时间步、每个样本独立归一化,不受 batch size 和序列长度影响。

  • 训练与测试一致:BN 在测试时使用训练集全局统计量,而 RNN 的测试可能面对单样本或不同长度,全局统计量容易偏移。LN 训练和测试完全一致。

  • 梯度传播:LN 可以稳定循环状态,缓解梯度消失/爆炸,让深层 LSTM 训练更稳定。

  • 易于实现:在自定义 RNN 单元中插入 LN 层即可,无须像 BN 那样维护运行均值和方差。

因此,LayerNorm LSTM 在序列模型中广泛使用,也是 Transformer 的基础组件。


解释 RNN 为什么不能并行化训练?这与 Transformer 的并行性形成对比。

RNN 的串行依赖

image.png

对比

  • RNN:训练时间复杂度 O(序列长度),不可并行,GPU 利用率低。

  • Transformer:训练时间复杂度 O(序列长度²),但可通过矩阵运算高度并行,实际速度远快于 RNN,尤其是中等长度序列。

RNN 在推理时也有此限制,但可以通过状态传递维持低延迟流式处理,Transformer 推理则需缓存键值但计算量更大。


什么是“截断 BPTT”(Truncated BPTT)?它如何处理长序列训练?

截断 BPTT 是为了解决长序列训练时 BPTT 计算量和内存过大的问题。其思想是:将长序列切割成若干较短的子序列,每个子序列内部进行完整的 BPTT,但梯度不再跨子序列传播。具体:

  • 将序列分成固定长度 k 的块(例如 k=100)。

  • 正向传播时,仍保持隐藏状态从前一块传递到下一块,以维持状态连续性。

  • 反向传播时,仅对当前块内的 k 步计算梯度,并在块边界处截断,不将梯度传回更早的块。

  • 参数更新在每个块之后(或累积几个块后)执行,隐藏状态则作为下一块的初始状态。

如何处理长序列训练

  • 降低内存占用:只需存储当前块的时间步中间变量,而不是整个序列。

  • 允许模型处理任意长的序列,理论上无限长,只要逐块推进隐藏状态。

  • 虽然截断了梯度流,但状态仍能跨块传递信息,模型可以学习跨块依赖(通过状态而非梯度)。

代价:梯度不能流过远超 k 步的依赖,使得学习真正长期依赖变得困难,但通常 k 可设为数百,已足够许多任务。这是一种计算和内存折中方案。


RNN 的隐藏状态维度选择有何讲究?过小或过大有什么影响?

隐藏状态维度 是 RNN/LSTM 的关键超参数,决定了记忆容量和模型复杂度。

过小

  • 容量不足,无法捕捉序列中的复杂模式和长期依赖,模型欠拟合,训练和测试损失都高。

  • 信息瓶颈:隐藏状态需要编码历史信息,维度太小会导致重要信息被丢弃,尤其对词汇量大或变化丰富的数据。

  • 可能影响梯度流,但主要问题是表示能力有限。

过大

image.png

选择原则

  • 根据任务复杂度、数据集大小、序列长度来权衡。常见初始值:128、256、512,大型任务可能到1024或更多。

  • 可通过验证集调参,观察性能饱和点。

  • 对于层数较多的 LSTM,通常隐藏维度不必过大,深度可提供更多抽象。

  • 资源受限时,适度降低维度并增加层数可能比单层大维度更有效。


如何初始化 RNN/LSTM 的权重?正交初始化对 RNN 有何好处?

常用初始化

image.png

实践:很多实现会使用 torch.nn.init.orthogonal_ 来初始化 LSTM 的所有权重,并设置遗忘门偏置为 1。


比较 LSTM 与 IndRNN(独立循环神经网络)的不同。

IndRNN (Independently Recurrent Neural Network) 由 Li et al. (2018) 提出,对传统 RNN 做了结构上的简化。

  • 循环连接方式

image.png


什么是“语言模型”?如何用 RNN 构建一个字符级语言模型?

语言模型

image.png

语言模型的目标是最大化训练文本的似然。

字符级语言模型

以字符为基本单位(而非词),建模字符序列的概率。它需要预测下一个字符。

用 RNN 构建字符级语言模型的步骤:

  1. 数据准备:收集文本语料,构建字符表(所有出现的唯一字符,如 a-z, 标点等),分配索引。将文本转为整数序列。

  2. 模型结构:

  3. 输入层:将字符索引映射为可学习的嵌入向量(Embedding)。
  4. 循环层:一层或多层 LSTM/GRU,接收嵌入序列,输出隐藏状态序列。
  5. 输出层:线性层将隐藏状态映射到字符表大小的 logits,经过 softmax 得到下一个字符的概率分布。

  6. 训练:

  7. 将输入序列和目标序列错位一个字符:输入为 "hell",目标为 "ello"(预测下一个字符)。
  8. 使用交叉熵损失计算每个时间步的预测误差,求和或取平均。
  9. BPTT 训练(或截断 BPTT),更新参数。

  10. 采样生成:

  11. 给定一个起始字符或序列,将其输入 RNN,得到下一个字符的概率分布,依据此分布采样一个字符。
  12. 将采样字符作为下一步的输入,重复过程生成任意长度的文本。

PyTorch 简单示例代码:

class CharRNN(nn.Module):
    def __init__(self, vocab_size, emb_size, hidden_size, num_layers):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, emb_size)
        self.lstm = nn.LSTM(emb_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, vocab_size)

    def forward(self, x, hidden=None):
        # x: (batch, seq_len)
        emb = self.embedding(x)
        out, hidden = self.lstm(emb, hidden)
        logits = self.fc(out)  # (batch, seq_len, vocab_size)
        return logits, hidden

从数学上证明,LSTM 的门控机制可以学习恒等映射,从而缓解梯度消失。

核心思想:如果 LSTM 的所有门控学会了正确的模式,可以让信息无损地流过许多时间步,相当于恒等映射。我们通过证明细胞状态的梯度流近似为 1 来实现。

LSTM 细胞状态更新:

image.png

结论:数学上,只要存在一条恒等映射路径(细胞状态),梯度就可绕过非线性压缩直接回传,而门控机制赋予网络学习这种映射的能力。这就是 LSTM 缓解梯度消失的理论基础。