深入理解 GRU 及其 PyTorch 实现¶
本文介绍了 GRU 的网络结构,梳理了 GRU 的前向传播关系,即 \(t-1\) 时间步的隐状态 \(h_{t-1}\)、\(t\) 时间步的输入 \(x_t\)、更新门 \(z_t\)、重置门 \(r_t\) 和 \(t\) 时间步的隐状态输出 \(h_t\) 之间是如何转换的。为了更好地理解 GRU,本文给出了各个张量的维数大小关系的数值示例。最后,本文提供了使用 PyTorch 实现一个 2 层 GRU 模型的代码。
Gated Recurrent Unit (GRU) 是由 Cho, et al. (2014)1 提出的一种循环神经网络结构,它的目的是缓解标准的循环神经网络(RNN)存在的梯度消失问题。GRU 和 LSTM 有着相似的设计思路,并且在一些情况下,两者确实能够得到同样好的效果。本文将介绍 GRU 的网络结构,对 RNN 和 LSTM 的介绍,可以参考 How Recurrent Neural Networks work 和 Understanding LSTM Networks。
GRU 的网络结构¶
GRU 可以看作是改进版本的 RNN,它可以缓解标准 RNN 存在的梯度消失问题。具体来说,GRU 是通过更新门(Update Gate)和重置门(Reset Gate)来记忆长期信息的。下图是 GRU 的整体网络结构。
GRU 在不同时间步的网络参数是相同的
在一个循环神经网络(RNN)中, RNN 在不同时间步的参数通常是相同的。每个 RNN 块都由相同的权重矩阵和偏置向量组成,这些参数在整个网络中共享。
RNN 中的每个时间步都使用相同的参数进行计算,这意味着网络在处理序列数据时具有相同的结构和参数。这种共享参数的方式使得 RNN 能够处理任意长度的序列,并且能够捕捉序列中的时间相关性。
放大来看,一个 GRU 块的内部结构为:
符号说明
哈达姆乘积
\(\odot\) 表示哈达姆乘积,即对应元素相乘。
接下来我们将 GRU 的内部结构进行拆解,并用数值示例理解各张量的维数大小关系与运算关系。我们假设各时间步的输入 \(x\) 的维数均为 \(30\times1\),各隐藏层输出 \(h\) 的维数均为 \(64\times 1\)。
在 PyTorch 中,输入 \(x\) 的维数用 input_size
表示,隐藏层的维数用 hidden_size
表示。
更新门¶
结构图¶
更新门对应的结构是:
公式¶
时间步 \(t\) 的输入 \(x_t\),与 \(t-1\) 步的隐藏层输出 \(h_{t-1}\),分别与各自的权重矩阵相乘后再相加,最后输入到 Sigmoid 激活函数中,就得到了更新门。
由于后续 \(z_t\) 需要与 \(h_{t-1}\) 进行对应元素相乘(哈达姆乘积),因此 \(z_t\) 和 \(h_{t-1}\) 的维数相同,也是 \(64\times 1\)。
因此,\(W^{(z)}\) 的维数是 \(64 \times 30\),\(U^{(z)}\) 的维数是 \(64 \times 64\)。
正确理解各张量的维数
关于 GRU 网络中各张量的维数,可以参考 https://stats.stackexchange.com/a/501784/。
作用¶
更新门的作用是帮助模型判断:应该将多少历史信息带到未来。我们将在最终输出部分深入理解更新门的作用(\(z_t\) 中的元素越接近于 1,则模型会倾向于保留更多的历史信息,而更少用最新的输入 \(x_t\))。现在我们只需要记住更新门 \(z_t\) 的公式即可。
重置门¶
结构图¶
重置门对应的结构是:
公式¶
时间步 \(t\) 的输入 \(x_t\),与 \(t-1\) 步的隐藏层输出 \(h_{t-1}\),分别与各自的权重矩阵相乘后再相加,最后输入到 Sigmoid 激活函数中,就得到了重置门。 注意,虽然计算重置门的过程与计算更新门的过程很相似,但两者使用的权重矩阵并不相同。
与上文的维数关系类似,由于后续 \(r_t\) 需要与 \(U h_{t-1}\) 进行对应元素相乘(哈达姆乘积),因此 \(r_t\) 的维数是 \(64 \times 1\)。
因此,\(W^{(r)}\) 的维数是 \(64 \times 30\),\(U^{(r)}\) 的维数是 \(64 \times 64\)。
作用¶
重置门的作用是帮助模型判断:应该保留多少历史信息。在公式 \(\eqref{3}\) 中可以看到,\(r_t\) 中的元素越接近于 1,则模型会倾向于保留更多的历史信息。
候选隐状态¶
结构图¶
公式¶
接下来我们将看到重置门究竟是如何影响最终输出的。在得到重置门 \(r_t\) 后,我们利用如下公式得到候选隐状态:
如果模型倾向于保留更少的历史信息,则可以将 \(r_t\) 中的元素更新为接近于 0。
与上文分析类似,\(W\) 的维数是 \(64 \times 30\),\(U\) 的维数是 \(64 \times 64\)。
作用¶
候选隐状态将会与 \(1-z_t\) 相乘后作为模型最终输出的一部分。
最终输出¶
结构图¶
公式¶
最终,模型通过更新门决定:从候选隐状态获取多少信息,以及从上一时间步的隐状态获取多少信息。
- 更新门 \(z_t\) 首先与上一时间步的隐状态 \(h_{t-1}\) 进行逐元素相乘,即 \(z_t \odot h_{t-1}\)。
- \(1-z_t\) 与候选隐状态 \(h_t^{\prime}\) 进行逐元素相乘,即 \((1-z_t) \odot h_t^{\prime}\)。
- 将上述两步的结果相加,即得到最终输出,作为 \(t\) 时间步的隐状态输出。
公式 \(\eqref{4}\) 中,所有张量的维数都是 \(64\times 64\)。
作用¶
\(t\) 时间步的隐状态输出,可以作为 \(t+1\) 时间步的隐状态输入。
PyTorch 实现¶
本节我们将用 PyTorch 实现一个 2 层的 GRU 模型,其结构如下图所示:
下面是一个实现 2 层 GRU 模型的代码示例,其中输入维度为 30,隐藏层维度为 64,同时设置batch_first=True
:
import torch
import torch.nn as nn
class GRUModel(nn.Module):
def __init__(
self,
input_size,
hidden_size,
num_layers,
output_size,
):
super(GRUModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.gru = nn.GRU(
input_size,
hidden_size,
num_layers, # (1)!
batch_first=True, # (2)!
)
self.fc = nn.Linear(hidden_size, output_size) # output_size 为输出的维度
def forward(self, input):
output, _ = self.gru(input)
output = self.fc(output[:, -1, :]) # 取最后一个时间步的输出
return output
# 创建模型实例
input_size = 30
hidden_size = 64
num_layers = 2
output_size = 10 # 假设全连接层的输出维度为 10
model = GRUModel(
input_size,
hidden_size,
num_layers,
output_size,
)
-
当使用 GRU 模型时,有时候我们可能需要多层的 GRU 结构以增加模型的表达能力。在 PyTorch 中,我们可以通过设置
num_layers
参数来实现多层 GRU。 -
通过设置
batch_first=True
,我们可以在输入数据中将批次维度放在第一个维度,这样更符合常见的数据形状。官方文档:batch_first – If
True
, then the input and output tensors are provided as (batch, seq, feature) instead of (seq, batch, feature). Note that this does not apply to hidden or cell states. See the Inputs/Outputs sections below for details. Default:False
更多代码示例可以参考 PyTorch GRU 的官方文档。
-
Cho K, Van Merriënboer B, Gulcehre C, et al. Learning phrase representations using RNN encoder-decoder for statistical machine translation[J]. arXiv preprint arXiv:1406.1078, 2014. ↩