参考资料:
参考博客:
1.简介
【机器学习】详解 RNN 介绍了 RNN,当时间步数较大或者时间步较小时,RNN 的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。因此,RNN 在实际中较难捕捉时间序列中时间步距离较大的依赖关系。
门控循环神经网络 (Gated Recurrent Neural Network,GRNN) 的提出,旨在更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可学习的门来控制信息的流动。其中,门控循环单元 (Gated Recurrent Unit,GRU) 是一种常用的 GRNN。GRU 对 【机器学习】详解 LSTM 介绍的 LSTM 做了很多简化,同时却保持着和 LSTM 相同的效果。
2.GRU网络结构
3.Pytroch实现GRU
3.1 通过GRU层实现
通过PyTorch提供的集成好的GRU层进行实现,简单快捷,但丧失了一些灵活性。
class GRUModel(nn.Module):
def __init__(self, input_num, hidden_num, output_num):
super(GRUModel, self).__init__()
self.hidden_size = hidden_num
# 这里设置了 batch_first=True, 所以应该 inputs = inputs.view(inputs.shape[0], -1, inputs.shape[1])
# 针对时间序列预测问题,相当于将时间步(seq_len)设置为 1。
self.GRU_layer = nn.GRU(input_size=input_num, hidden_size=hidden_num, batch_first=True)
self.output_linear = nn.Linear(hidden_num, output_num)
self.hidden = None
def forward(self, x):
# h_n of shape (num_layers * num_directions, batch, hidden_size)
# 这里不用显式地传入隐层状态 self.hidden
x, self.hidden = self.GRU_layer(x)
x = self.output_linear(x)
return x, self.hidden
3.2 通过GRUCell实现
这种实现方法与方法一的不同之处主要有两点:
- GRUCell需要显式地传入隐藏层状态。
- 方法一当中GRU层要求输入数据
(x)
具有三个维度,即(seq_len, batch, input_size)
,分别代表序列长度/时间步、batch size、输入特征维数;而GRUCell的输入形状则和一般的神经网络相同,为(batch, input_size)
。
class GRUModel(nn.Module):
def __init__(self, input_num, hidden_num, output_num):
super(GRUModel, self).__init__()
self.hidden_size = hidden_num
self.grucell = nn.GRUCell(input_num, hidden_num)
self.out_linear = nn.Linear(hidden_num, output_num)
def forward(self, x, hid):
if hid is None:
hid = torch.randn(x.shape[0], self.hidden_size)
next_hid = self.grucell(x, hid) # 需要传入隐藏层状态
y = self.out_linear(next_hid)
return y, next_hid.detach() # detach()和detach_()都可以使用
这里需要对forward()
函数的第二个返回值 next_hid.detach()
做一些说明。首先看一下PyTorch官方文档当中对于detach()
和detach_()
方法的介绍:
detach():
Returns a new Tensor, detached from the current graph. The result will never require gradient.detach_():
Detaches the Tensor from the graph that created it, making it a leaf. Views cannot be detached in-place.
这两种方法有一个相似的作用,就是将张量从创造它的计算图当中分离出来。下图是一个在不对返回值中隐层状态进行detach()
操作时GRU网络计算过程的示意图。
从图中可以比较清晰地看出,不同于 X X X 是在每一步前向传播(图中黑线)开始时由外部提供的(称为计算图的一个叶子),隐层状态 H H H 是通过上一步的前向传播产生的。在这种情况下,在进行反向传播时,梯度计算就会一直追溯到该网络的初始状态 H 0 H_0 H0 和 X 1 X_1 X1 。而正确的情况应当是反向传播过程到达上一隐藏层状态 H H H 后即停止,将 H H H 和 X X X同等地作为叶子节点。如下图所示。
3.3 通过自定义Cell实现
GRUCell的内部实际上是实现了以下计算过程:
所以我们可以通过自定义的方式来实现GRUCell,并根据自己的想法来定义新的循环网络单元。
class GRUCell(nn.Module):
"""自定义GRUCell"""
def __init__(self, input_size, hidden_size):
super(TestGRUCell, self).__init__()
# 输入变量的线性变换过程是 x @ W.T + b (@代表矩阵乘法, .T代表矩阵转置)
# in2hid_w 的原始形状应是 (hidden_size, input_size), 为了编程的方便, 这里改成(input_size, hidden_size)
lb, ub = -sqrt(1/hidden_size), sqrt(1/hidden_size)
self.in2hid_w = nn.ParameterList([self.__init(lb, ub, input_size, hidden_size) for _ in range(3)])
self.hid2hid_w = nn.ParameterList([self.__init(lb, ub, hidden_size, hidden_size) for _ in range(3)])
self.in2hid_b = nn.ParameterList([self.__init(lb, ub, hidden_size) for _ in range(3)])
self.hid2hid_b = nn.ParameterList([self.__init(lb, ub, hidden_size) for _ in range(3)])
@staticmethod
def __init(low, upper, dim1, dim2=None):
if dim2 is None:
return nn.Parameter(torch.rand(dim1) * (upper - low) + low) # 按照官方的初始化方法来初始化网络参数
else:
return nn.Parameter(torch.rand(dim1, dim2) * (upper - low) + low)
def forward(self, x, hid):
r = torch.sigmoid(torch.mm(x, self.in2hid_w[0]) + self.in2hid_b[0] +
torch.mm(hid, self.hid2hid_w[0]) + self.hid2hid_b[0])
z = torch.sigmoid(torch.mm(x, self.in2hid_w[1]) + self.in2hid_b[1] +
torch.mm(hid, self.hid2hid_w[1]) + self.hid2hid_b[1])
n = torch.tanh(torch.mm(x, self.in2hid_w[2]) + self.in2hid_b[2] +
torch.mul(r, (torch.mm(hid, self.hid2hid_w[2]) + self.hid2hid_b[2])))
next_hid = torch.mul((1 - z), n) + torch.mul(z, hid)
return next_hid
定义好GRUCell后,结合方法二就可以定义出GRU网络了。