前馈网络是独立处理输入的。然而,许多问题都涉及序列数据,其中顺序很重要,并且先前项的背景信息会影响当前项。例如,理解一个句子、预测股价或转录语音。每个词、价格点或声音片段都依赖于它之前的内容。标准的前馈网络缺乏一种内在机制来‘记住’序列中的过去信息。这就是循环神经网络(RNN)的作用所在。它们通过引入循环的构想,专门设计用于处理序列数据。记忆的构想:隐状态RNN 的决定性特征是其内部循环。在处理序列的每一步,网络不仅考虑当前输入,还会考虑它从先前步骤中保留下来的信息。这些保留的信息存储在所谓的隐状态中。设想你正在阅读一个句子。你不会孤立地处理每个词。你对当前词的把握会受到你已阅读词语的很大影响。RNN 中的隐状态就像这种运行中的总结或背景信息。它捕获了序列中先前元素的相关信息。逐步处理序列RNN 一次处理序列中的一个元素(或“时间步”)时。对于每个时间步 $t$:它接收该时间步的输入,我们称之为 $x_t$。它还接收来自前一个时间步的隐状态 $h_{t-1}$。它使用一组学习到的权重结合 $x_t$ 和 $h_{t-1}$,以计算新的隐状态 $h_t$。这个新的隐状态现在包含了从所有步骤直到 $t$ 的信息。可选地,它可以为当前时间步生成一个输出 $y_t$,这通常基于隐状态 $h_t$。重要的是,每个时间步都使用相同的一组权重(结合输入和先前状态以及生成输出的规则)。这种权重共享使得 RNN 效率高,并能使其将模式推广到不同长度的序列。可视化循环:时间上的展开通常,通过在时间上“展开”RNN 会有所帮助。我们可以绘制一条链来表示网络在每个时间步的状态,而不是绘制循环。digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#495057", fillcolor="#e9ecef", style="filled, rounded"]; edge [color="#495057"]; subgraph cluster_t_minus_1 { label = "时间 t-1"; style=dashed; color="#adb5bd"; ht_minus_1 [label="h(t-1)", shape=ellipse, color="#7048e8", fillcolor="#d0bfff"]; xt_minus_1 [label="x(t-1)"]; yt_minus_1 [label="y(t-1)", shape=ellipse, color="#1098ad", fillcolor="#99e9f2", style="filled, rounded"]; cell_t_minus_1 [label="RNN 单元", shape=box, color="#f76707", style="filled, rounded", fillcolor="#ffd8a8"]; xt_minus_1 -> cell_t_minus_1; ht_minus_1 -> cell_t_minus_1; cell_t_minus_1 -> yt_minus_1; placeholder_in [shape=point, width=0.01, height=0.01, label=""]; placeholder_in -> ht_minus_1 [label=" ...", arrowhead=none]; } subgraph cluster_t { label = "时间 t"; style=dashed; color="#adb5bd"; ht [label="h(t)", shape=ellipse, color="#7048e8", fillcolor="#d0bfff"]; xt [label="x(t)"]; yt [label="y(t)", shape=ellipse, color="#1098ad", fillcolor="#99e9f2", style="filled, rounded"]; cell_t [label="RNN 单元", shape=box, color="#f76707", style="filled, rounded", fillcolor="#ffd8a8"]; xt -> cell_t; cell_t -> ht; cell_t -> yt; } subgraph cluster_t_plus_1 { label = "时间 t+1"; style=dashed; color="#adb5bd"; ht_plus_1 [label="h(t+1)", shape=ellipse, color="#7048e8", fillcolor="#d0bfff"]; xt_plus_1 [label="x(t+1)"]; yt_plus_1 [label="y(t+1)", shape=ellipse, color="#1098ad", fillcolor="#99e9f2", style="filled, rounded"]; cell_t_plus_1 [label="RNN 单元", shape=box, color="#f76707", style="filled, rounded", fillcolor="#ffd8a8"]; xt_plus_1 -> cell_t_plus_1; ht_plus_1 -> cell_t_plus_1 [style=invis]; // Hides the arrow but keeps layout cell_t_plus_1 -> yt_plus_1; placeholder_out [shape=point, width=0.01, height=0.01, label=""]; ht_plus_1 -> placeholder_out [label=" ...", arrowhead=none]; } cell_t_minus_1 -> ht [constraint=false]; // 跨时间步的循环连接 ht -> cell_t_plus_1; // 循环连接 }一个在时间上“展开”的 RNN。相同的 RNN 单元(代表共享权重)处理输入 $x_t$ 和先前的隐状态 $h_{t-1}$,以生成新的隐状态 $h_t$ 和可选的输出 $y_t$。隐状态从一个时间步传递到下一个时间步。从数学角度看,简单 RNN 单元在时间步 $t$ 内的核心计算通常表示为:计算新的隐状态 $h_t$: $$ h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h) $$计算输出 $y_t$: $$ y_t = W_{hy} h_t + b_y $$这里:$x_t$ 是时间步 $t$ 的输入。$h_{t-1}$ 是来自前一个时间步的隐状态。$h_t$ 是时间步 $t$ 的新隐状态。$y_t$ 是时间步 $t$ 的输出。$W_{hh}$、$W_{xh}$ 和 $W_{hy}$ 是在训练期间学习到的权重矩阵。它们分别代表了先前隐状态、当前输入和当前隐状态的影响程度。这些权重在所有时间步之间是共享的。$b_h$ 和 $b_y$ 是偏置向量,也是学习得到的。$\tanh$ 是双曲正切激活函数,常用于简单的 RNN 中以引入非线性。根据具体任务,输出层可以使用其他激活函数(例如,用于分类的 Softmax)。重要之处在于 $h_t$ 的循环公式,它同时依赖于当前输入 $x_t$ 和先前的隐状态 $h_{t-1}$。正是这种依赖性赋予了 RNN 记忆能力。RNN 的应用场景RNN 在处理序列模式的任务中表现出色:自然语言处理(NLP): 语言建模(预测下一个词)、机器翻译、情感分析、文本生成。语音识别: 将口语音频转换为文本。时间序列分析: 预测股价、天气预报、传感器数据分析。视频分析: 理解视频帧中随时间发生的动作。挑战与后续尽管功能强大,但像上面描述的简单 RNN 在学习长距离依赖时可能会遇到困难。来自早期时间步的信息在通过多个步骤传播时可能会被稀释或丢失,这个问题通常被称为梯度消失问题。反之,梯度有时可能会变得过大,这被称为梯度爆炸问题。这些挑战促成了更精密的循环架构的发展,如长短期记忆(LSTM)和门控循环单元(GRU),它们使用门控机制来更好地控制信息流和记忆。本章稍后将简要提及这些内容。目前,掌握循环的核心思想、隐状态的作用以及逐步处理过程就足够了。在接下来的部分中,我们将了解如何使用 PyTorch 的 nn.RNN 模块实现一个基本的 RNN。