循环神经网络逐步处理序列信息,同时保持内部记忆。实现此功能的基本构件是简单RNN单元。可将RNN单元视为在序列中每个时间步运行的计算引擎。对于输入序列中的每个元素 $x_t$(在时间步 $t$),该单元接收两个输入:当前输入元素 $x_t$。来自上一时间步的隐藏状态 $h_{t-1}$。利用这些输入,该单元执行计算以生成两个输出:新的隐藏状态 $h_t$。此状态捕获当前输入 $x_t$ 的信息,以及来自所有先前步骤的相关上下文(概括于 $h_{t-1}$ 中)。它作为网络的记忆,并传递给下一时间步。可选输出 $y_t$。这是网络专门为当前时间步 $t$ 生成的预测或结果。是否在每一步都产生输出取决于具体任务(例如,预测下一个单词每步需要一个输出,而情感分类可能只需要在整个序列处理完后得到一个输出)。RNN单元内部简单RNN单元内的核心计算包含结合当前输入和先前隐藏状态,使用学习到的权重和激活函数。本章引言展示了基本方程:$$h_t = f(W_{hh}h_{t-1} + W_{xh}x_t + b_h)$$ $$y_t = g(W_{hy}h_t + b_y)$$我们来逐一分析:$W_{xh}$: 一个将输入 $x_t$ 进行转换的权重矩阵。$W_{hh}$: 一个将先前隐藏状态 $h_{t-1}$ 进行转换的权重矩阵。这是循环权重矩阵,捕获过去信息的影响。$b_h$: 一个添加到隐藏状态计算中的偏置向量。$f$: 隐藏状态的激活函数。这里的常见选择是双曲正切 (tanh),它将值压缩到 [-1, 1] 的范围。这有助于调节信息流并减轻一些梯度问题(尽管不能完全解决,正如我们稍后将了解的)。$W_{hy}$: 一个将当前隐藏状态 $h_t$ 进行转换以产生输出 $y_t$ 的权重矩阵。$b_y$: 一个添加到输出计算中的偏置向量。$g$: 输出 $y_t$ 的激活函数。$g$ 的选择取决于任务(例如,分类使用 softmax,回归使用线性函数)。此架构的一个重要特点是权重共享。相同的权重矩阵 ($W_{xh}$, $W_{hh}$, $W_{hy}$) 和偏置 ($b_h$, $b_y$) 在每个时间步都使用。这意味着网络学习一套参数来明确如何处理输入元素并更新其记忆,无论该元素在序列中的位置如何。这使得RNN在参数上具有高效性,并且能够泛化处理不同长度的序列。单元和循环的可视化我们可以可视化单个RNN单元及其连接:digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#495057", fillcolor="#e9ecef", style="filled,rounded"]; edge [fontname="sans-serif", color="#495057"]; subgraph cluster_cell { label = "RNN单元 (时间步 t)"; style=dashed; color="#adb5bd"; node [shape=box, style=rounded, fillcolor="#a5d8ff"]; cell [label=<<b>h</b><sub>t</sub> = f(W<sub>hh</sub><b>h</b><sub>t-1</sub> + W<sub>xh</sub><b>x</b><sub>t</sub> + b<sub>h</sub>)<br/><b>y</b><sub>t</sub> = g(W<sub>hy</sub><b>h</b><sub>t</sub> + b<sub>y</sub>)>]; } xt [label=<<b>x</b><sub>t</sub> (输入)>, shape=plaintext, fontcolor="#1c7ed6"]; ht_prev [label=<<b>h</b><sub>t-1</sub> (上一状态)>, shape=plaintext, fontcolor="#ae3ec9"]; ht [label=<<b>h</b><sub>t</sub> (新状态)>, shape=plaintext, fontcolor="#ae3ec9"]; yt [label=<<b>y</b><sub>t</sub> (输出)>, shape=plaintext, fontcolor="#1098ad"]; xt -> cell [label=" W_xh", fontcolor="#868e96"]; ht_prev -> cell [label=" W_hh", fontcolor="#868e96"]; cell -> ht; cell -> yt [label=" W_hy", fontcolor="#868e96"]; }单个RNN单元处理输入 $x_t$ 和先前状态 $h_{t-1}$,以计算得到新状态 $h_t$ 和输出 $y_t$。权重矩阵 ($W_{xh}$, $W_{hh}$, $W_{hy}$) 在这些转换过程中应用。真正的作用体现在将这些单元链接起来,形成循环连接。在时间步 $t$ 计算得到的隐藏状态 $h_t$ 成为时间步 $t+1$ 单元的输入 $h_{t-1}$。这种时间上的“展开”使得信息能够流经序列:digraph G { rankdir=LR; node [shape=box, style="rounded,filled", fontname="sans-serif", color="#495057", fillcolor="#a5d8ff"]; edge [fontname="sans-serif", color="#495057"]; subgraph cluster_unrolled { label = "RNN在时间上展开"; style=dashed; color="#adb5bd"; x0 [label=<<b>x</b><sub>0</sub>>, shape=plaintext, fontcolor="#1c7ed6"]; x1 [label=<<b>x</b><sub>1</sub>>, shape=plaintext, fontcolor="#1c7ed6"]; x2 [label=<<b>x</b><sub>2</sub>>, shape=plaintext, fontcolor="#1c7ed6"]; h_init [label=<<b>h</b><sub>-1</sub>>, shape=plaintext, fontcolor="#ae3ec9"]; // 初始状态 rnn0 [label="RNN单元"]; rnn1 [label="RNN单元"]; rnn2 [label="RNN单元"]; y0 [label=<<b>y</b><sub>0</sub>>, shape=plaintext, fontcolor="#1098ad"]; y1 [label=<<b>y</b><sub>1</sub>>, shape=plaintext, fontcolor="#1098ad"]; y2 [label=<<b>y</b><sub>2</sub>>, shape=plaintext, fontcolor="#1098ad"]; h0 [label=<<b>h</b><sub>0</sub>>, shape=plaintext, fontcolor="#ae3ec9"]; h1 [label=<<b>h</b><sub>1</sub>>, shape=plaintext, fontcolor="#ae3ec9"]; h2 [label=<<b>h</b><sub>2</sub>>, shape=plaintext, fontcolor="#ae3ec9"]; // 时间步 0 x0 -> rnn0; h_init -> rnn0; rnn0 -> h0; rnn0 -> y0; // 时间步 1 x1 -> rnn1; h0 -> rnn1 [constraint=true]; // 隐藏状态连接 rnn1 -> h1; rnn1 -> y1; // 时间步 2 x2 -> rnn2; h1 -> rnn2 [constraint=true]; // 隐藏状态连接 rnn2 -> h2; rnn2 -> y2; // 使输入/输出垂直对齐 { rank=same; x0; rnn0; y0 } { rank=same; x1; rnn1; y1; h0 } { rank=same; x2; rnn2; y2; h1 } { rank=same; h2 } // 对齐最终隐藏状态 // 如需要,可使用不可见边进行更好布局 // x0 -> x1 [style=invis]; x1 -> x2 [style=invis]; // y0 -> y1 [style=invis]; y1 -> y2 [style=invis]; } }一个RNN在三个时间步上展开。每个时间步由单元计算得到的隐藏状态 ($h_t$) 作为输入传递给下一时间步 ($t+1$) 的单元。每个时间步都使用相同的单元参数(权重 $W$ 和偏置 $b$)。这种展开视图很有帮助,尤其是在思考梯度在训练期间如何反向流动(时间反向传播)时,我们将在接下来讨论这个。然而,请记住,在实际应用中,是相同的单元结构(具有相同的权重)被重复应用,而不是每个时间步都有不同的副本。这种结构使得RNN能够使用固定数量的参数处理任意长度的序列。