专栏:神经网络复现目录
门控循环神经网络(Gated Recurrent Neural Network,简称“门控循环神经网络”或“门循环神经网络”)是一种改进的循环神经网络(RNN)架构。它包含了一些门控机制,可以更好地捕捉时间序列数据中的长期依赖关系。
门控循环神经网络最早由Hochreiter和Schmidhuber在1997年提出,但是由于当时缺乏计算能力和数据集,它并没有得到广泛应用。后来,在2014年,Cho等人提出了一种简化版的门控循环神经网络,即GRU,它比传统的门控循环神经网络更易于训练和实现,并且在很多任务上取得了优秀的结果。
门控循环神经网络通过使用门控单元来控制信息的流动。这些门控单元允许网络选择性地从输入中选择性地忽略一些信息,或者从过去的状态中选择性地记忆一些信息。这样就可以更好地捕捉时间序列数据中的长期依赖关系,从而提高模型的性能。
门控循环单元与普通的循环神经网络之间的关键区别在于: 前者支持隐状态的门控。 这意味着模型有专门的机制来确定应该何时更新隐状态, 以及应该何时重置隐状态。 这些机制是可学习的,并且能够解决了上面列出的问题。 例如,如果第一个词元非常重要, 模型将学会在第一次观测之后不更新隐状态。 同样,模型也可以学会跳过不相关的临时观测。 最后,模型还将学会在需要的时候重置隐状态。 下面我们将详细讨论各类门控。
重置门(reset gate)是门控循环神经网络(GRU)中的一种门控机制。重置门的作用是决定网络是否忽略之前的状态信息,从而控制信息的流动。
具体来说,在GRU中,每个时间步都有一个重置门,用一个sigmoid函数来计算,其输出值在0和1之间。当重置门的输出接近于1时,表示网络需要从之前的状态中获取更多的信息;当重置门的输出接近于0时,表示网络需要更加依赖当前的输入信息。因此,重置门可以让网络选择性地忘记或记住之前的状态信息。
重置门的计算方式如下:
R t = σ ( X t W x r + H t − 1 W h r + b r ) R_t=\sigma(X_tW_{xr}+H_{t-1}W_{hr}+b_r) Rt=σ(XtWxr+Ht−1Whr+br)
其中, X t X_t Xt表示当前的输入, H t − 1 H_{t-1} Ht−1表示上一个时间步的隐藏状态, W x r W_{xr} Wxr、 W h r W_{hr} Whr和 b r b_r br是可学习的权重参数, σ \sigma σ是sigmoid函数。 R t R_t Rt表示重置门的输出。
更新门(update gate)是门控循环神经网络(GRU)中的一种门控机制。更新门的作用是控制模型是否记住之前的状态信息,以及如何将新的输入信息与之前的状态信息进行结合。
具体来说,在GRU中,每个时间步都有一个更新门,用一个sigmoid函数来计算,其输出值在0和1之间。当更新门的输出接近于1时,表示网络需要完全记住之前的状态信息;当更新门的输出接近于0时,表示网络完全忽略之前的状态信息,只依赖于当前的输入信息。因此,更新门可以让网络选择性地记住或忘记之前的状态信息。
更新门的计算方式如下:
Z t = σ ( X t W x z + H t − 1 W h z + b z ) Z_t=\sigma(X_tW_{xz}+H_{t-1}W_{hz}+b_z) Zt=σ(XtWxz+Ht−1Whz+bz)
其中, X t X_t Xt表示当前的输入, H t − 1 H_{t-1} Ht−1表示上一个时间步的隐藏状态, W x z W_{xz} Wxz、 W h z W_{hz} Whz和 b z b_z bz是可学习的权重参数, σ \sigma σ是sigmoid函数。 R t R_t Rt表示重置门的输出。
下图说明了更新门和重置门的计算流程
在门控循环神经网络(GRU)中,候选隐状态(candidate hidden state)是在更新门和重置门的作用下计算得到的一种新的隐状态。
具体来说,在GRU中,首先通过重置门计算一个重置向量 R t R_t Rt,用于控制当前时刻的输入信息与上一时刻的隐状态的结合程度。然后,通过将 r t r_t rt与上一时刻的隐状态 H t − 1 H_{t-1} Ht−1相乘,得到一个重置的上一时刻隐状态 R t ⊙ H t − 1 R_t\odot H_{t-1} Rt⊙Ht−1,它会与当前时刻的输入信息 x t x_t xt一起作为新的输入信息传递给一个tanh激活函数,计算得到一个候选隐状态 h ~ t \tilde{h}_t h~t:
H ~ t = tanh ( X t W x h + ( R t ⊙ H t − 1 ) W h h + b h ) \tilde{H}t = \tanh(X_tW_{xh}+(R_t\odot H_{t-1})W_{hh}+b_h) H~t=tanh(XtWxh+(Rt⊙Ht−1)Whh+bh)
其中, W x h W_{xh} Wxh、 W h h W_{hh} Whh和 b h b_h bh是可学习的参数, ⊙ \odot ⊙表示逐元素相乘。
上述的计算结果只是候选隐状态,我们仍然需要结合更新门 Z t Z_t Zt的效果,这一步确定的隐状态 H t H_t Ht在多大程度上来自旧的状态 H t − 1 H_{t-1} Ht−1和新的候选状态 H t ~ \tilde{H_t} Ht~, 这就得出了门控循环单元的最终更新公式:
H t = Z t ⊙ H t − 1 + ( 1 − Z t ) ⊙ H t ~ H_t = Z_t\odot H_{t-1}+(1-Z_t)\odot \tilde{H_{t}} Ht=Zt⊙Ht−1+(1−Zt)⊙Ht~
每当更新门接近1时,模型就倾向只保留旧状态。 此时,来自 X t X_t Xt的信息基本上被忽略, 从而有效地跳过了依赖链条中的时间步 t t t。 相反,当更新门接近0时, 新的隐状态就会接近候选隐状态。 这些设计可以帮助我们处理循环神经网络中的梯度消失问题, 并更好地捕获时间步距离很长的序列的依赖关系。 例如,如果整个子序列的所有时间步的更新门都接近于1, 则无论序列的长度如何,在序列起始时间步的旧隐状态都将很容易保留并传递到序列结束。
GRU的数学公式为:
R t = σ ( X t W x r + H t − 1 W h r + b r ) R_t=\sigma(X_tW_{xr}+H_{t-1}W_{hr}+b_r) Rt=σ(XtWxr+Ht−1Whr+br)
Z t = σ ( X t W x z + H t − 1 W h z + b z ) Z_t=\sigma(X_tW_{xz}+H_{t-1}W_{hz}+b_z) Zt=σ(XtWxz+Ht−1Whz+bz)
H ~ t = tanh ( X t W x h + ( R t ⊙ H t − 1 ) W h h + b h ) \tilde{H}t = \tanh(X_tW_{xh}+(R_t\odot H_{t-1})W_{hh}+b_h) H~t=tanh(XtWxh+(Rt⊙Ht−1)Whh+bh)
H t = Z t ⊙ H t − 1 + ( 1 − Z t ) ⊙ H t ~ H_t = Z_t\odot H_{t-1}+(1-Z_t)\odot \tilde{H_{t}} Ht=Zt⊙Ht−1+(1−Zt)⊙Ht~
总之,门控循环单元具有以下两个显著特征:
重置门有助于捕获序列中的短期依赖关系;
更新门有助于捕获序列中的长期依赖关系。
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device)*0.01
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))
W_xz, W_hz, b_z = three() # 更新门参数
W_xr, W_hr, b_r = three() # 重置门参数
W_xh, W_hh, b_h = three() # 候选隐状态参数
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
这段代码定义了一个函数 get_params,该函数接受三个参数:vocab_size,num_hiddens 和 device,并返回一组神经网络参数,这些参数用于实现一个门控循环单元 (GRU) 模型。
函数中定义了三个辅助函数:
normal(shape):返回一个形状为 shape 的张量,张量中的元素服从均值为 0、标准差为 0.01 的正态分布。
three():返回一个三元组,包含三个形状为 (num_inputs, num_hiddens) 或 (num_hiddens,) 的张量。这些张量将用于定义门控循环单元模型中的更新门、重置门和候选隐状态。
在 get_params 函数中,首先通过将 num_inputs 和 num_outputs 设置为 vocab_size,来确定输入和输出层的大小。接下来,使用 three() 函数三次,分别定义了更新门、重置门和候选隐状态的参数。每个门都包括两个权重矩阵 W_xz 和 W_hz、一个偏置向量 b_z。其中,W_xz 和 W_hz 分别是输入和隐藏状态的权重矩阵,b_z 是偏置向量。这些参数都是随机初始化的,以确保模型的多样性和可训练性。
接下来,定义输出层参数 W_hq 和 b_q,用于将隐藏状态映射到输出。这里的 W_hq 是一个 (num_hiddens, num_outputs) 的权重矩阵,用于将隐藏状态映射到输出空间;b_q 是一个形状为 (num_outputs,) 的偏置向量。
最后,将所有参数设置为需要计算梯度,返回所有参数作为一个列表。这些参数将被用于训练门控循环单元模型。
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
这里返回的是时间序列的初始化,即初始将 H 1 H_1 H1、 H 2 H_2 H2、 H t H_t Ht初始化为0
接下来我们准备定义门控循环单元模型, 模型的架构与基本的循环神经网络单元是相同的, 只是权重更新公式更为复杂。
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = H @ W_hq + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
class RNNModelScratch: #@save
def __init__(self, vocab_size, num_hiddens, device,
get_params, init_state, forward_fn):
self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
self.params = get_params(vocab_size, num_hiddens, device)
self.init_state, self.forward_fn = init_state, forward_fn
def __call__(self, X, state):
X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
return self.forward_fn(X, state, self.params)
def begin_state(self, batch_size, device):
return self.init_state(batch_size, self.num_hiddens, device)
class GRU(nn.Module):
def __init__(self, feature_size, hidden_size, num_layers, output_size):
super(GRU, self).__init__()
self.hidden_size = hidden_size # 隐层大小
self.num_layers = num_layers # gru层数
# feature_size为特征维度,就是每个时间点对应的特征数量,这里为1
self.gru = nn.GRU(feature_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden=None):
batch_size = x.shape[0] # 获取批次大小
# 初始化隐层状态
if hidden is None:
h_0 = x.data.new(self.num_layers, batch_size, self.hidden_size).fill_(0).float()
else:
h_0 = hidden
# GRU运算
output, h_0 = self.gru(x, h_0)
# 获取GRU输出的维度信息
batch_size, timestep, hidden_size = output.shape
# 将output变成 batch_size * timestep, hidden_dim
output = output.reshape(-1, hidden_size)
# 全连接层
output = self.fc(output) # 形状为batch_size * timestep, 1
# 转换维度,用于输出
output = output.reshape(timestep, batch_size, -1)
# 我们只需要返回最后一个时间片的数据即可
return output[-1]