为了全面理解循环神经网络如何处理序列信息,将数据随时间在网络中的流动形象化是很有益的。虽然我们经常将RNN单元绘制为一个指向自身的循环连接,但为了分析和理解训练过程,我们可以将这个循环沿输入序列的时间步“展开”。设想你有一个输入序列 $x = (x_1, x_2, ..., x_T)$。当这个序列输入到RNN中时,网络在每个时间步 $t$ 逐个元素 ($x_t$) 处理它。主要之处在于隐藏状态 $h_t$,它在每个时间步得到更新,并将先前时间步的信息向前传递。让我们将这个展开过程形象化:digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", color="#adb5bd", fontcolor="#495057"]; edge [fontname="sans-serif", color="#868e96"]; subgraph cluster_0 { label = "时间 t-1"; style=dashed; color="#ced4da"; xt_1 [label="x₍ₜ₋₁₎", shape=circle, style=filled, color="#a5d8ff", fontcolor="#1c7ed6"]; ht_1 [label="h₍ₜ₋₁₎", style=filled, color="#ffec99", fontcolor="#f59f00"]; yt_1 [label="y₍ₜ₋₁₎", shape=circle, style=filled, color="#b2f2bb", fontcolor="#37b24d"]; cell_1 [label=<RNN 单元<BR/><FONT POINT-SIZE="10">参数 W, U, V</FONT>>, style=filled, color="#e9ecef"]; xt_1 -> cell_1 [label="输入"]; ht_prev [label="h₍ₜ₋₂₎", shape=plaintext, fontcolor="#adb5bd"]; // Placeholder for previous state ht_prev -> cell_1 [label="前一个状态"]; cell_1 -> ht_1 [label="新状态"]; cell_1 -> yt_1 [label="输出"]; } subgraph cluster_1 { label = "时间 t"; style=dashed; color="#ced4da"; xt [label="xₜ", shape=circle, style=filled, color="#a5d8ff", fontcolor="#1c7ed6"]; ht [label="hₜ", style=filled, color="#ffe066", fontcolor="#f59f00"]; yt [label="yₜ", shape=circle, style=filled, color="#8ce99a", fontcolor="#37b24d"]; cell_t [label=<RNN 单元<BR/><FONT POINT-SIZE="10">参数 W, U, V</FONT>>, style=filled, color="#e9ecef"]; xt -> cell_t [label="输入"]; cell_t -> ht [label="新状态"]; cell_t -> yt [label="输出"]; } subgraph cluster_2 { label = "时间 t+1"; style=dashed; color="#ced4da"; xt_p1 [label="x₍ₜ₊₁₎", shape=circle, style=filled, color="#a5d8ff", fontcolor="#1c7ed6"]; ht_p1 [label="h₍ₜ₊₁₎", style=filled, color="#ffd43b", fontcolor="#f59f00"]; yt_p1 [label="y₍ₜ₊₁₎", shape=circle, style=filled, color="#69db7c", fontcolor="#37b24d"]; cell_p1 [label=<RNN 单元<BR/><FONT POINT-SIZE="10">参数 W, U, V</FONT>>, style=filled, color="#e9ecef"]; xt_p1 -> cell_p1 [label="输入"]; cell_p1 -> ht_p1 [label="新状态"]; cell_p1 -> yt_p1 [label="输出"]; } ht_1 -> cell_t [label="前一个状态"]; ht -> cell_p1 [label="前一个状态"]; // 如有需要,用于对齐的虚拟节点 dummy1 [shape=point, width=0]; dummy2 [shape=point, width=0]; xt_1 -> dummy1 [style=invis]; xt -> dummy2 [style=invis]; // 添加参数标签 // W_xh 在 x 和单元格之间,为清晰起见省略,由输入箭头暗示 // W_hh 在 h_prev 和单元格之间,省略,由前一个状态箭头暗示 // W_hy 在单元格和 y 之间,省略,由输出箭头暗示 } 一个随时间展开的RNN。相同的RNN单元,具有相同的权重(W、U、V分别对应$W_{xh}$、$W_{hh}$和$W_{hy}$),在每个时间步应用。隐藏状态($h$)充当记忆体,将一个时间步连接到下一个时间步。在这个展开视图中:输入传播: 在每个时间步 $t$,相应的输入元素 $x_t$ 被输入到该时间步的RNN单元中。状态传播: 来自前一个时间步的隐藏状态 $h_{t-1}$ 也被输入到时间步 $t$ 的单元中。对于第一个时间步($t=1$),通常使用一个初始隐藏状态 $h_0$,它通常被初始化为零。计算: 在时间 $t$ 的单元内部,输入 $x_t$ 和前一个状态 $h_{t-1}$ 结合权重矩阵和激活函数 $f$ 来计算新的隐藏状态 $h_t$: $$h_t = f(W_{hh}h_{t-1} + W_{xh}x_t + b_h)$$输出生成: 新的隐藏状态 $h_t$ 通常用于计算当前时间步的输出 $y_t$,使用另一个权重矩阵和激活函数 $g$: $$y_t = g(W_{hy}h_t + b_y)$$ (注意:根据具体的任务,可能不会在每个时间步生成或需要输出。)迭代: 这个过程在下一个时间步 $t+1$ 重复。刚刚计算出的隐藏状态 $h_t$ 成为 $t+1$ 处计算的“前一个隐藏状态”。这种展开清楚地展示了在任何给定时间步的隐藏状态 $h_t$ 如何通过隐藏状态链($h_0, h_1, ..., h_{t-1}$)成为当前输入 $x_t$ 和所有先前输入($x_1, ..., x_{t-1}$)的函数。这种隐藏状态的链式连接就是RNN保持对迄今已处理序列的上下文或“记忆”的方式。由展开视图说明的一个重要之处是参数共享。请注意,相同的权重矩阵($W_{xh}, W_{hh}, W_{hy}$)和偏置($b_h, b_y$)在每个时间步的RNN单元内部使用。网络学习一组参数,并在整个序列中重复应用。这使得模型高效,并能够处理不同长度的序列。理解这种信息流动和展开的思路是理解RNN如何训练的基础。跨时间步的依赖性意味着在较晚时间步计算的误差需要通过这个展开结构反向传播,以更新共享权重。这个过程被称为时间反向传播(BPTT),我们将在接下来考察它。