循环神经网络(RNN)一步步处理序列,并使用其隐藏状态传递信息。那么网络是怎样学习的呢?就像前馈网络一样,RNN通过根据其产生的误差调整权重来进行学习。对此的标准算法是反向传播。然而,RNN中的循环连接和共享权重需要进行修改:沿时间的反向传播(BPTT)。设想您向RNN输入一个长度为$T$的序列。如前所述,网络在每一步执行相同的计算,使用相同的权重集合($W_{hh}, W_{xh}, W_{hy}, b_h, b_y$)。为训练网络,我们首先需要计算一个损失,它衡量网络预测值($y_1, y_2, ..., y_T$)与该序列真实目标值之间的距离。此损失通常在整个序列上计算,常为每个时间步损失的总和或平均值。BPTT的主要思路是应用微积分的链式法则,与标准反向传播一样,但要跨时间操作序列进行。为形象地说明这一点,可以想象将RNN针对特定输入序列进行“展开”。这会形成一个大型的、类似前馈的网络,其中每个时间步对应一个层。重要的一点是,权重在这些“层”之间是共享的。让我们考虑梯度计算。最终时间步$T$的误差直接取决于隐藏状态$h_T$以及权重$W_{hy}$和$b_y$。时间步$T-1$的误差取决于$h_{T-1}$,并可能通过循环连接($h_T$取决于$h_{T-1}$)影响时间步$T$的误差。BPTT通过计算总损失相对于每个时间步的输出和隐藏状态的梯度来运行,从最后一步$T$开始,并向后移动到第一步$t=1$。损失$L$相对于时间$t$隐藏状态的梯度,表示为$\frac{\partial L}{\partial h_t}$,依赖于两点:$h_t$如何直接影响输出$y_t$(从而影响时间步$t$的损失)。$h_t$如何影响下一个隐藏状态$h_{t+1}$(从而影响所有后续步骤$t+1, ..., T$的损失)。第二点是“沿时间”的部分。梯度信号通过循环权重矩阵$W_{hh}$从$h_{t+1}$向后流向$h_t$。在数学上,这涉及到如下项:$$ \frac{\partial h_{t+1}}{\partial h_t} = \frac{\partial f(W_{hh}h_{t} + W_{xh}x_{t+1} + b_h)}{\partial h_t} $$这种传播一步步向后进行。digraph BPTT { rankdir=LR; node [shape=box, style=rounded, fontname="helvetica", fontsize=10]; edge [fontname="helvetica", fontsize=9]; subgraph cluster_T { label = "时间 T"; bgcolor="#e9ecef"; ht [label="h_T", shape=ellipse, style=filled, fillcolor="#a5d8ff"]; yt [label="y_T", shape=ellipse, style=filled, fillcolor="#b2f2bb"]; LossT [label="损失_T", shape=diamond, style=filled, fillcolor="#ffc9c9"]; ht -> yt; yt -> LossT; } subgraph cluster_T_minus_1 { label = "时间 T-1"; bgcolor="#e9ecef"; htm1 [label="h_{T-1}", shape=ellipse, style=filled, fillcolor="#a5d8ff"]; ytm1 [label="y_{T-1}", shape=ellipse, style=filled, fillcolor="#b2f2bb"]; LossTm1 [label="损失_{T-1}", shape=diamond, style=filled, fillcolor="#ffc9c9"]; htm1 -> ytm1; ytm1 -> LossTm1; } subgraph cluster_dots { label = "..."; bgcolor="#e9ecef"; node [shape=plaintext]; dots [label="..."]; } subgraph cluster_1 { label = "时间 1"; bgcolor="#e9ecef"; h1 [label="h_1", shape=ellipse, style=filled, fillcolor="#a5d8ff"]; y1 [label="y_1", shape=ellipse, style=filled, fillcolor="#b2f2bb"]; Loss1 [label="损失_1", shape=diamond, style=filled, fillcolor="#ffc9c9"]; h1 -> y1; y1 -> Loss1; } htm1 -> ht [label=" W_hh", color="#4263eb"]; dots -> htm1 [label=" W_hh", color="#4263eb"]; h1 -> dots [label=" W_hh", color="#4263eb", style=invis]; // Placeholder edge for layout // Gradient flow edges (backward) LossT -> yt [dir=back, color="#f03e3e", style=dashed, constraint=false, label=" dL/dy_T"]; yt -> ht [dir=back, color="#f03e3e", style=dashed, constraint=false, label=" dy_T/dh_T"]; LossT -> ht [dir=back, color="#f03e3e", style=dashed, penwidth=1.5, label=" dL/dh_T"]; // Direct path gradient sum LossTm1 -> ytm1 [dir=back, color="#f03e3e", style=dashed, constraint=false, label=" dL/dy_{T-1}"]; ytm1 -> htm1 [dir=back, color="#f03e3e", style=dashed, constraint=false, label=" dy_{T-1}/dh_{T-1}"]; ht -> htm1 [dir=back, color="#f03e3e", style=dashed, penwidth=1.5, label=" dh_T/dh_{T-1} \n (通过 W_hh)"]; LossTm1 -> htm1 [dir=back, color="#f03e3e", style=dashed, penwidth=1.5, label=" dL/dh_{T-1}"]; // Direct path gradient sum htm1 -> dots [dir=back, color="#f03e3e", style=dashed, penwidth=1.5, label=" ..."]; dots -> h1 [dir=back, color="#f03e3e", style=dashed, penwidth=1.5, label=" ..."]; Loss1 -> y1 [dir=back, color="#f03e3e", style=dashed, constraint=false, label=" dL/dy_1"]; y1 -> h1 [dir=back, color="#f03e3e", style=dashed, constraint=false, label=" dy_1/dh_1"]; Loss1 -> h1 [dir=back, color="#f03e3e", style=dashed, penwidth=1.5, label=" dL/dh_1"]; // Direct path gradient sum // Total Loss aggregation TotalLoss [label="总损失\n L = sum(损失_t)", shape=doubleoctagon, style=filled, fillcolor="#fab005"]; LossT -> TotalLoss [style=dotted]; LossTm1 -> TotalLoss [style=dotted]; Loss1 -> TotalLoss [style=dotted]; // Weights (representation) Weights [label="共享权重\n(W_hh, W_xh, ...)", shape=cylinder, style=filled, fillcolor="#ced4da", fontsize=9]; ht -> Weights [dir=back, color="#7048e8", style=dashed, constraint=false, label=" dL/dW"]; htm1 -> Weights [dir=back, color="#7048e8", style=dashed, constraint=false]; h1 -> Weights [dir=back, color="#7048e8", style=dashed, constraint=false]; }BPTT的反向传播过程。梯度(虚线红色)从每个时间步的损失流回,通过网络输出($y_t$)和隐藏状态($h_t$)。重要的是,梯度也通过循环连接(通过$W_{hh}$)向后流动,影响早期时间步的梯度计算。相对于共享权重(紫色线)的梯度在所有时间步累积。BPTT的一个重要方面源于共享权重。由于在每个时间步都使用相同的权重矩阵($W_{hh}, W_{xh}, W_{hy}$)和偏置向量($b_h, b_y$),因此为特定权重计算的梯度需要考虑其在整个序列中的作用。因此,共享参数(例如$W_{hh}$)的最终梯度是相对于其在每个时间步$t=1, ..., T$的使用所计算的梯度之和。$$ \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^{T} \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{hh}} \quad \text{(在反向传播过程中计算)} $$$W_{xh}$、$W_{hy}$、$b_h$和$b_y$的计算方式类似。一旦这些总梯度计算完毕,就会执行标准的梯度下降更新(或其变体之一,如Adam或RMSprop)来调整网络参数。$$ W \leftarrow W - \eta \frac{\partial L}{\partial W} $$这里$W$代表任何共享参数,而$\eta$是学习率。尽管BPTT允许我们训练RNN,但这种在可能很长的序列上传播梯度的过程并非没有困难。随着梯度信号在许多时间步中向后传播,它可能呈指数级缩小至零(梯度消失)或呈指数级增大(梯度爆炸)。我们将在第4章检验这些训练上的难题及其影响。目前,主要观点是BPTT扩展了反向传播,以处理循环架构中固有的时间依赖性和共享参数。下一节将更仔细地讨论展开网络以促进此过程的实际方法。