专栏:神经网络复现目录

门控循环单元(GRU)

门控循环神经网络(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+Ht1Whr+br)

其中, X t X_t Xt表示当前的输入, H t − 1 H_{t-1} Ht1表示上一个时间步的隐藏状态, 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+Ht1Whz+bz)

其中, X t X_t Xt表示当前的输入, H t − 1 H_{t-1} Ht1表示上一个时间步的隐藏状态, 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} Ht1相乘,得到一个重置的上一时刻隐状态 R t ⊙ H t − 1 R_t\odot H_{t-1} RtHt1,它会与当前时刻的输入信息 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+(RtHt1)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} Ht1和新的候选状态 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=ZtHt1+(1Zt)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+Ht1Whr+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+Ht1Whz+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+(RtHt1)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=ZtHt1+(1Zt)Ht~

总之,门控循环单元具有以下两个显著特征:

  1. 重置门有助于捕获序列中的短期依赖关系;

  2. 更新门有助于捕获序列中的长期依赖关系。

从零开始实现GRU

初始化参数模型

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]
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐