“正如本章引言中所述,许多问题都涉及序列数据,其中元素的顺序对理解信息起着决定性的作用。试想理解一个句子、预测某人将要输入的下一个词、分析随时间变化的股市走势,或者判读机器的传感器读数。标准的前馈神经网络,例如我们见过的全连接网络或卷积神经网络,都是独立处理输入的。它们在处理当前输入时,不会固有地保留过去输入的信息,这对于序列任务是一个显著的限制。”在处理像文本、语音或时间序列这样的顺序数据时,标准神经网络的一个主要限制是它们无法保持对先前输入的信息。循环神经网络(RNN)正是为了解决这一限制而设计的。RNN的主要思想是循环:网络对序列中的每个元素执行相同的操作,但每个元素的输出不仅取决于当前输入,还取决于先前元素的结果。这是通过在网络结构中引入一个“循环”来实现的。隐藏状态:网络中的记忆想象一下逐词处理一个句子。为了把握“猫追老鼠,然后它跑了”这句话中“它”的含义,你需要记住“它”指的是猫还是老鼠。RNN通过一个内部的隐藏状态(通常表示为$h$)来完成这种上下文保留。在序列中的每一步$t$(例如,处理第$t$个词或第$t$个时间点),RNN接收两个输入:当前输入元素$x_t$。来自上一步的隐藏状态$h_{t-1}$。然后它计算新的隐藏状态$h_t$,并可选地计算一个输出$y_t$。重点在于,$h_t$的计算使用了$h_{t-1}$。这形成了一个依赖链,其中来自早期步骤的信息可以通过隐藏状态在序列中传递。这使得网络能够拥有某种“记忆”,保留来自过去元素的上下文信息。时间步$t$时隐藏状态的更新规则可以表示为:$$ h_t = f(W_{hh}h_{t-1} + W_{xh}x_t + b_h) $$时间步$t$的输出(如果每一步都需要)可以表示为:$$ y_t = g(W_{hy}h_t + b_y) $$此处:$x_t$是时间步$t$的输入。$h_{t-1}$是来自前一时间步$t-1$的隐藏状态。$h_t$是时间步$t$的新隐藏状态。$y_t$是时间步$t$的输出。$W_{xh}$、$W_{hh}$和$W_{hy}$是权重矩阵(分别为输入到隐藏层、隐藏层到隐藏层以及隐藏层到输出层)。$b_h$和$b_y$是偏置向量。$f$和$g$是激活函数(例如,对于$f$可以是tanh或relu,对于$g$根据任务可以是softmax或sigmoid)。在所有时间步中,使用相同的权重集($W_{xh}$、$W_{hh}$、$W_{hy}$)和偏置($b_h$、$b_y$)。这种参数共享使得RNN高效,并能够泛化序列中不同位置的模式。展开RNN为了更好地观察信息的流动,通常会将RNN循环按序列长度“展开”。想象序列有$T$个时间步。展开意味着创建网络的$T$个副本,每个时间步一个,并将一个时间步的隐藏状态输出连接到下一个时间步的隐藏状态输入。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=dashed; color="#adb5bd"; h_prev [label="h(t-1)", shape=ellipse, fillcolor="#a5d8ff"]; x_prev [label="x(t-1)", shape=circle, fillcolor="#ffec99"]; rnn_prev [label="RNN 单元"]; y_prev [label="y(t-1)", shape=ellipse, fillcolor="#b2f2bb"]; x_prev -> rnn_prev; h_prev -> rnn_prev; rnn_prev -> y_prev; } subgraph cluster_1 { label = "时间步 t"; style=dashed; color="#adb5bd"; h_curr [label="h(t)", shape=ellipse, fillcolor="#a5d8ff"]; x_curr [label="x(t)", shape=circle, fillcolor="#ffec99"]; rnn_curr [label="RNN 单元"]; y_curr [label="y(t)", shape=ellipse, fillcolor="#b2f2bb"]; x_curr -> rnn_curr; h_curr -> rnn_curr [style=invis]; // 用于对齐的不可见边 rnn_curr -> y_curr; rnn_curr -> h_curr; } subgraph cluster_2 { label = "时间步 t+1"; style=dashed; color="#adb5bd"; h_next [label="h(t+1)", shape=ellipse, fillcolor="#a5d8ff"]; x_next [label="x(t+1)", shape=circle, fillcolor="#ffec99"]; rnn_next [label="RNN 单元"]; y_next [label="y(t+1)", shape=ellipse, fillcolor="#b2f2bb"]; x_next -> rnn_next; h_next -> rnn_next [style=invis]; // 用于对齐的不可见边 rnn_next -> y_next; rnn_next -> h_next; } // 步骤间的连接 rnn_prev -> h_curr [label=" 状态传递", constraint=false]; h_curr -> rnn_next [label=" 状态传递", constraint=false]; // 用于对齐的不可见边 x_prev -> x_curr [style=invis]; x_curr -> x_next [style=invis]; y_prev -> y_curr [style=invis]; y_curr -> y_next [style=invis]; }一个随时间展开的RNN。每个RNN 单元块表示在不同时间步应用相同的一组权重。隐藏状态h将信息从一步传递到下一步。输入x和输出y在每一步发生。这种展开视图使得梯度在反向传播(通过时间的反向传播,即BPTT)过程中如何流动更加清晰,以及为什么捕捉长程依赖有时会比较困难,这会导致像梯度消失问题,我们将在后面讨论。输入和输出情景RNN在处理不同的序列输入/输出关系方面很灵活:多对一: 序列输入,单个输出(例如,句子的情感分类)。一对多: 单个输入,序列输出(例如,图像描述生成——输入图像,输出词序列)。多对多(对齐): 序列输入,序列输出且输入/输出长度匹配(例如,句子中每个词的词性标注)。多对多(延迟): 序列输入,序列输出,其中输出在一些延迟或处理完整个输入后开始(例如,机器翻译)。核心RNN思想是这些不同模式的依据。在Keras中,SimpleRNN、LSTM和GRU等层实现了这种循环行为。我们将在下一节中说明如何使用这些层,从基本的SimpleRNN开始。