Recurrent Neural Network (RNN)
循环神经网络 (RNN) 会逐步读取序列,并向前传递隐藏状态。涵盖其循环机制、训练、变体和代码。
循环神经网络(RNN)是一种神经网络,它会逐个处理序列中的元素,并将每一步的隐藏状态传递到下一步,因此任意一步的输出都可能取决于此前读入的所有内容。每一步都使用相同的权重,因此同一个网络可以处理任意长度的序列,从一句话到数小时的传感器读数都适用。在 2010 年代后期,Transformer取代 RNN,成为大多数语言任务的主流架构;在此之前,RNN 曾是自然语言处理(NLP)、语音识别和时间序列分析的主要架构。RNN 的门控变体仍常用于流式处理和端侧模型;此外,循环结构也通过 Mamba 等状态空间模型(SSMs)重新回归。
RNN 的工作原理#
简单 RNN 也称 Elman 网络,得名于 Jeffrey Elman 1990 年发表的论文。它按以下公式更新隐藏状态:hₜ = tanh(Wxₜ + Uhₜ₋₁ + b),并且可以在每一步输出 yₜ = Vhₜ + c。隐藏状态是一个固定大小的向量,用来概括当前为止的序列;tanh将其值保持在 −1 和 1 之间,再将其反馈回网络。将每一步的网络副本画出来后,循环会展开成一个深层前馈网络,其中各层共享相同权重:
下面的代码片段会对一个序列运行 PyTorch 的 nn.RNN,并通过普通循环重复相同的循环计算:
import torch
from torch import nn
torch.manual_seed(0)
rnn = nn.RNN(input_size=3, hidden_size=4, batch_first=True)
x = torch.randn(1, 6, 3) # one sequence: 6 time steps, 3 features each
# PyTorch runs the whole sequence; output holds the hidden state after every step
output, h_n = rnn(x)
# The same recurrence by hand, reusing one set of weights at every step
h = torch.zeros(1, 4)
for t in range(x.shape[1]):
h = torch.tanh(x[:, t] @ rnn.weight_ih_l0.T + rnn.bias_ih_l0 + h @ rnn.weight_hh_l0.T + rnn.bias_hh_l0)
print(f"Output shape: {tuple(output.shape)}, final state shape: {tuple(h_n.shape)}")
print(f"Hand-written loop matches nn.RNN: {torch.allclose(h, h_n[0], atol=1e-6)}")代码会输出 Output shape: (1, 6, 4), final state shape: (1, 1, 4) 和 Hand-written loop matches nn.RNN: True。每个时间步的输出都包含一个 4 维隐藏状态;循环只需一个输入权重矩阵和一个前一状态权重矩阵,就能复现最终状态。
序列任务#
相同的循环结构适用于输入和输出长度各不相同的任务:
- 多对一:读取整个序列,并根据最终状态对序列进行分类,例如情感分析或为一段传感器数据打标签。
- 多对多:在每一步都输出结果,例如为句子中的每个词或音频中的每一帧打标签。
- 序列到序列:编码器 RNN 读取输入,解码器 RNN 生成长度不同的输出;早期神经机器翻译以及其他序列到序列模型都采用了这种设计。
- 一对多:根据单个输入生成序列,例如早期图像字幕生成模型根据图像的特征向量生成字幕。
双向 RNN 会添加第二个反向读取序列的 RNN,并在每一步合并两个状态,因此每个输出都能利用过去和未来的上下文。堆叠式 RNN 会将一层的隐藏状态作为输入序列传给下一层。
通过时间反向传播进行训练#
RNN 使用通过时间反向传播(BPTT)进行训练:网络沿序列展开,损失在各步求和,然后在展开后的计算图中执行反向传播。从时间步 T 传到时间步 t 的梯度会经过 T − t 个循环层副本,并且每次都会乘以 U 和 tanh 的导数。如果这些因子大多小于 1,梯度就会按几何级数缩小,这就是 Yoshua Bengio、Patrice Simard 和 Paolo Frasconi 于 1994 年分析的梯度消失问题。如果这些因子大于 1,梯度就会按几何级数增大,这就是梯度爆炸问题。下面的代码片段会测量长度为 50 的序列中,每个输入对最终隐藏状态的影响:
import torch
from torch import nn
torch.manual_seed(0)
rnn = nn.RNN(input_size=8, hidden_size=32, batch_first=True)
x = torch.randn(1, 50, 8, requires_grad=True) # one 50-step sequence
# Backpropagate from the final hidden state to the input at every step
output, h_n = rnn(x)
h_n.sum().backward()
grad_norms = x.grad[0].norm(dim=1)
for distance in [0, 10, 20, 30, 40]:
print(f"Input {distance:>2} steps before the end: gradient {grad_norms[-1 - distance]:.1e}")它会在结束前的 0、10、20、30 和 40 个时间步,分别输出输入 1.6e+00、3.4e-03、6.2e-06、1.2e-08 和 1.1e-11 的梯度。每十个时间步,梯度就会缩小约三个数量级,因此训练几乎无法调整权重来利用几十个时间步之前的输入。Razvan Pascanu 和同事在 2013 年针对 RNN 分析的梯度裁剪会限制梯度爆炸,而截断 BPTT 则将反向传播限制在固定窗口内,以控制内存使用。梯度消失需要改变架构,而 LSTM 和 GRU 的门控机制正是为此提供了解决方案。
RNN 与相关架构的比较#
- LSTM 和 GRU:带门控的 RNN,通过加法状态更新,让信息和梯度能够经过多个时间步。在实际使用中,“RNN”通常既指简单 RNN,也指这两种架构。
- Transformer:直接关注所有更早的位置,而不是将历史信息压缩到一个状态中,并且可以并行训练所有位置。其注意力计算成本会随序列长度增长,而 RNN 每增加一个时间步,计算和内存开销都相同。
- 卷积神经网络 (CNN):使用固定大小的滤波器混合相邻位置,在图像中作用于空间维度,或通过 1D 卷积作用于时间维度;其感受野由网络深度决定。
- 状态空间模型:Mamba 等线性递归模型可以通过扫描操作并行训练,并在推理时像 RNN 一样逐步运行。液态神经网络是连续时间 RNN,其神经元遵循微分方程。
应用场景#
- 键盘预测:Gboard 的下一个词预测模型采用耦合输入-遗忘门 LSTM,这是一种更轻量的 LSTM 变体;Google 研究人员在 2018 年介绍了该模型如何通过用户手机上的联邦学习进行训练。
- 需求预测:Amazon 的 DeepAR 在多个相关时间序列上训练一个自回归 RNN,并为每个未来值输出概率分布。
- 流式音频:唤醒词检测器和降噪器使用小型循环层逐帧处理音频,使手机和耳机上的内存占用与延迟保持稳定。
- 目标跟踪:跟踪器会为每个目标在连续帧之间维护一个状态,这正是 RNN 隐藏状态的作用。YOLO26 的跟踪模式内置的所有跟踪器——从 ByteTrack 和 BoT-SORT 到默认的 TrackTrack——都会使用卡尔曼滤波器更新该状态;卡尔曼滤波器是一种线性递归,用手动设定的恒定速度方程取代学习得到的权重。










