基本循环神经网络 (RNN) 的结构包含了循环和隐状态。与信息严格单向流动的前馈网络不同,RNNs 包含一个循环,使得来自先前步骤的信息能够保留并影响当前步骤。这种内部记忆使得它们适合处理序列数据。RNN 的核心是 RNN 单元。可以把这个单元看作是每个输入序列元素都会被重复使用的基本处理单元。在每个时间步 $t$,该单元接收两个输入:序列中的当前输入元素,$x_t$。来自上一时间步的隐状态,$h_{t-1}$。根据这些输入,该单元执行两个主要计算:计算新的隐状态,$h_t$。为当前时间步生成一个输出,$y_t$。其核心机制涉及使用特定的权重矩阵和激活函数来组合当前输入 $x_t$ 和先前隐状态 $h_{t-1}$。RNN 单元内部让我们分解一下在单个时间步 $t$ 内一个简单 RNN 单元中发生的计算:计算隐状态 ($h_t$): 新的隐状态 $h_t$ 通过对当前输入 $x_t$ 和先前隐状态 $h_{t-1}$ 应用线性变换、添加偏置,然后将结果通过非线性激活函数(通常是 tanh 或 ReLU)计算得出。公式通常为: $$ h_t = \tanh(W_{xh} x_t + W_{hh} h_{t-1} + b_h) $$ 这里:$x_t$ 是时间步 $t$ 的输入向量。$h_{t-1}$ 是来自先前时间步 $t-1$ 的隐状态向量。(对于第一个时间步 $t=0$, $h_{-1}$ 通常初始化为零向量)。$W_{xh}$ 是连接输入 $x_t$ 到隐状态的权重矩阵。$W_{hh}$ 是连接先前隐状态 $h_{t-1}$ 到当前隐状态 $h_t$ 的权重矩阵。这是与“循环”连接或循环相关的矩阵。$b_h$ 是隐状态计算的偏置向量。tanh 是激活函数的一个常见选择,它将值压缩到 -1 和 1 之间。计算输出 ($y_t$): 当前时间步的输出 $y_t$ 通常通过对新计算出的隐状态 $h_t$ 应用另一个线性变换、添加偏置,并根据具体任务(例如,如果任务是每步分类,则为 softmax 函数)可能通过另一个激活函数来计算。公式通常是: $$ y_t = \text{activation}(W_{hy} h_t + b_y) $$ 这里:$h_t$ 是当前的隐状态向量。$W_{hy}$ 是连接隐状态到输出的权重矩阵。$b_y$ 是输出计算的偏置向量。activation 是适用于该问题的输出激活函数(例如,回归任务使用恒等函数,多类别分类使用 softmax)。沿时间展开 RNN为了更好地理解 RNN 如何处理序列,我们通常会沿时间“展开”网络。这意味着将网络绘制得好像是一个深度前馈网络,其中一层对应一个时间步。digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#495057", fillcolor="#e9ecef", style=filled]; edge [fontname="sans-serif", color="#495057"]; subgraph cluster_0 { label = "时间 t-1"; style=filled; color="#dee2e6"; ht_1 [label="h(t-1)"]; xt_1 [label="x(t-1)"]; yt_1 [label="y(t-1)"]; cell_1 [label="RNN 单元", shape=ellipse, fillcolor="#a5d8ff"]; xt_1 -> cell_1; ht_1 -> cell_1 [style=dashed]; cell_1 -> yt_1; } subgraph cluster_1 { label = "时间 t"; style=filled; color="#dee2e6"; ht [label="h(t)"]; xt [label="x(t)"]; yt [label="y(t)"]; cell_t [label="RNN 单元", shape=ellipse, fillcolor="#a5d8ff"]; xt -> cell_t; ht_1 -> cell_t [label="W_hh", style=dashed, constraint=false]; cell_t -> ht; cell_t -> yt; } subgraph cluster_2 { label = "时间 t+1"; style=filled; color="#dee2e6"; ht_p1 [label="h(t+1)"]; xt_p1 [label="x(t+1)"]; yt_p1 [label="y(t+1)"]; cell_p1 [label="RNN 单元", shape=ellipse, fillcolor="#a5d8ff"]; xt_p1 -> cell_p1; ht -> cell_p1 [label="W_hh", style=dashed, constraint=false]; cell_p1 -> ht_p1; cell_p1 -> yt_p1; } ht_1 -> ht [style=invis]; // 辅助布局 ht -> ht_p1 [style=invis]; // 辅助布局 // 明确绘制显示数据流的连接 cell_1 -> ht_1 [style=dashed]; // 显示作为后续输入的输出状态 cell_t -> ht [style=dashed]; // 显示作为后续输入的输出状态 // 明确连接跨时间步的隐状态(此处为清晰起见移除 W_hh 标签) ht_1 -> cell_t [style=dashed]; ht -> cell_p1 [style=dashed]; // 指示主流程之外的共享权重 W_xh [label="W_xh (共享)", shape=plaintext, fontcolor="#1c7ed6"]; W_hh_label [label="W_hh (共享)", shape=plaintext, fontcolor="#1c7ed6"]; W_hy [label="W_hy (共享)", shape=plaintext, fontcolor="#1c7ed6"]; // 将权重标签放置在相关连接附近(大致位置) xt -> cell_t [label="W_xh", fontcolor="#1c7ed6"]; cell_t -> yt [label="W_hy", fontcolor="#1c7ed6"]; { rank=same; xt_1; xt; xt_p1; W_xh; } { rank=same; yt_1; yt; yt_p1; W_hy; } { rank=same; ht_1; ht; ht_p1; W_hh_label; } }一个沿时间展开的 RNN。每个 RNN 单元 块代表一个时间步的处理,它接收输入 $x_t$ 和先前隐状态 $h_{t-1}$ 来生成输出 $y_t$ 和下一个隐状态 $h_t$。注意隐状态 $h$ 从一个时间步传递到下一个时间步(虚线所示)。重要的一点是,相同的权重矩阵 ($W_{xh}$, $W_{hh}$, $W_{hy}$) 在所有时间步中都被使用。展开所体现的最重要方面是参数共享。权重矩阵 ($W_{xh}$, $W_{hh}$, $W_{hy}$) 和偏置向量 ($b_h$, $b_y$) 在每个时间步都是相同的。这使得模型高效,因为它不需要为每个输入位置单独设置一组参数。它学习了一种可以重复应用于整个序列的通用变换。隐状态 $h_t$ 充当网络的记忆。它捕获来自所有先前时间步 ($x_0, x_1, ..., x_{t-1}$) 的信息,并将其与当前输入 $x_t$ 结合,以影响当前输出 $y_t$ 和随后的隐状态 $h_{t+1}$。这种简单结构使得 RNN 能够建模序列内部的依赖关系,支撑着序列数据的处理。然而,正如我们接下来将看到的,这种基本架构在处理长序列时会遇到某些困难。