门控循环神经网络grnn学写笔记


GRU LSTM

当时间步数较大或者时间步较小时,循环神经网络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。通常由于这个原因,循环神经网络在实际中较难捕捉时间序列中时间步距离较大的依赖关系。

门控循环神经网络(gated recurrent neural network)的提出,正是为了更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可以学习的门来控制信息的流动。其中,门控循环单元(gated recurrent unit,GRU)是一种常用的门控循环神经网络 。另一种常用的门控循环神经网络是长短期记忆(long short-term memory,LSTM)。

1、GRU

RNN存在的问题:梯度较容易出现衰减或爆炸(BPTT)
?控循环神经?络:捕捉时间序列中时间步距离较?的依赖关系

RNN
在这里插入图片描述

Ht=?(XtWxh+Ht?1Whh+bh)
H_{t} = ?(X_{t}W_{xh} + H_{t-1}W_{hh} + b_{h})

Ht?=?(Xt?Wxh?+Ht?1?Whh?+bh?)
GRU:
在这里插入图片描述

Rt=σ(XtWxr+Ht?1Whr+br)Zt=σ(XtWxz+Ht?1Whz+bz)H~t=tanh(XtWxh+(RtHt?1)Whh+bh)Ht=ZtHt?1+(1?Zt)H~t
R_{t} = σ(X_tW_{xr} + H_{t?1}W_{hr} + b_r)\\ Z_{t} = σ(X_tW_{xz} + H_{t?1}W_{hz} + b_z)\\ \widetilde{H}_t = tanh(X_tW_{xh} + (R_t ⊙H_{t?1})W_{hh} + b_h)\\H_t = Z_t⊙H_{t?1} + (1?Z_t)⊙\widetilde{H}_t

Rt?=σ(Xt?Wxr?+Ht?1?Whr?+br?)Zt?=σ(Xt?Wxz?+Ht?1?Whz?+bz?)Ht?=tanh(Xt?Wxh?+(Rt?⊙Ht?1?)Whh?+bh?)Ht?=Zt?⊙Ht?1?+(1?Zt?)⊙Ht?

  • 最后每个门单元还要有一个输出,即把Ht输出到一个output上。假如是做一个q分类任务,Ht的形状是(n,h)那么需要Whq和b1q权重和偏置来得到输出。h为隐藏单元个数,即num_hiddens。

  • 重置?有助于捕捉时间序列?短期的依赖关系。

  • 更新?有助于捕捉时间序列??期的依赖关系。

部分代码(只需在前边从零构造的RNN中添加门单元的代码即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F
import ... as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

(corpus_indices, char_to_idx, idx_to_char, vocab_size) = d2l.load_data_jay_lyrics()

#初始化参数
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)

def get_params():  
    def _one(shape):
        ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32) #正态分布
        return torch.nn.Parameter(ts, requires_grad=True)
    def _three():
        return (_one((num_inputs, num_hiddens)),
                _one((num_hiddens, num_hiddens)),
                torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
     
    W_xz, W_hz, b_z = _three()  # 更新门参数
    W_xr, W_hr, b_r = _three()  # 重置门参数
    W_xh, W_hh, b_h = _three()  # 候选隐藏状态参数
   
    # 输出层参数
    W_hq = _one((num_hiddens, num_outputs))
    b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
    return nn.ParameterList([W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q])

def init_gru_state(batch_size, num_hiddens, device):   #隐藏状态初始化
    return (torch.zeros((batch_size, num_hiddens), device=device), )

GRU模型 加了个门控循环单元(gated recurrent unit)

1
2
3
4
5
6
7
8
9
10
11
12
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(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
        R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
        H_tilda = torch.tanh(torch.matmul(X, W_xh) + R * torch.matmul(H, W_hh) + b_h)
        H = Z * H + (1 - Z) * H_tilda
        Y = torch.matmul(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H,)

然后训练模型即可

1
2
3
4
5
6
7
8
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']

d2l.train_and_predict_rnn(gru, get_params, init_gru_state, num_hiddens,
                          vocab_size, device, corpus_indices, idx_to_char,
                          char_to_idx, False, num_epochs, num_steps, lr,
                          clipping_theta, batch_size, pred_period, pred_len,
                          prefixes)  #第一个参数传入gru即可

GRU简洁实现

在PyTorch中我们直接调用nn模块中的GRU类即可。

1
2
3
4
5
6
7
lr = 1e-2 # 注意调整学习率
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
                                corpus_indices, idx_to_char, char_to_idx,
                                num_epochs, num_steps, lr, clipping_theta,
                                batch_size, pred_period, pred_len, prefixes)

小结

  • 门控循环神经网络可以更好地捕捉时间序列中时间步距离较大的依赖关系。
  • 门控循环单元引入了门的概念,从而修改了循环神经网络中隐藏状态的计算方式。它包括重置门、更新门、候选隐藏状态和隐藏状态。
  • 重置门有助于捕捉时间序列里短期的依赖关系。
  • 更新门有助于捕捉时间序列里长期的依赖关系

2、LSTM

LSTM 中引入了3个门,即输入门(input gate)、遗忘门(forget gate)和输出门(output gate),以及与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一种特殊的隐藏状态),从而记录额外的信息。

遗忘门:控制上一时间步的记忆细胞 输入门:控制当前时间步的输入
输出门:控制从记忆细胞到隐藏状态
记忆细胞:?种特殊的隐藏状态的信息的流动

LSTM结构图如下
在这里插入图片描述

It=σ(XtWxi+Ht?1Whi+bi)Ft=σ(XtWxf+Ht?1Whf+bf)Ot=σ(XtWxo+Ht?1Who+bo)C~t=tanh(XtWxc+Ht?1Whc+bc)Ct=FtCt?1+ItC~tHt=Ottanh(Ct)
I_t = σ(X_tW_{xi} + H_{t?1}W_{hi} + b_i) \\F_t = σ(X_tW_{xf} + H_{t?1}W_{hf} + b_f)\\O_t = σ(X_tW_{xo} + H_{t?1}W_{ho} + b_o)\\\widetilde{C}_t = tanh(X_tW_{xc} + H_{t?1}W_{hc} + b_c)\\C_t = F_t ⊙C_{t?1} + I_t ⊙\widetilde{C}_t\\H_t = O_t⊙tanh(C_t)

It?=σ(Xt?Wxi?+Ht?1?Whi?+bi?)Ft?=σ(Xt?Wxf?+Ht?1?Whf?+bf?)Ot?=σ(Xt?Wxo?+Ht?1?Who?+bo?)Ct?=tanh(Xt?Wxc?+Ht?1?Whc?+bc?)Ct?=Ft?⊙Ct?1?+It?⊙Ct?Ht?=Ot?⊙tanh(Ct?)
最后每个门单元还要有一个输出,即把Ht输出到一个output上。假如是做一个q分类任务,Ht的形状是(n,h)那么需要Whq和b1q权重和偏置来得到输出。h为隐藏单元个数,即num_hiddens。(和上边讲的GRU最后一样)

每个门单元 ==> 隐藏状态和记忆细胞 、输出(只和隐藏状态有关)

我们先介绍如何从零开始实现长短期记忆(部分关键代码)。

初始化参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)

def get_params():
    def _one(shape):
        ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
        return torch.nn.Parameter(ts, requires_grad=True)
    def _three():
        return (_one((num_inputs, num_hiddens)),
                _one((num_hiddens, num_hiddens)),
                torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
   
    W_xi, W_hi, b_i = _three()  # 输入门参数
    W_xf, W_hf, b_f = _three()  # 遗忘门参数
    W_xo, W_ho, b_o = _three()  # 输出门参数
    W_xc, W_hc, b_c = _three()  # 候选记忆细胞参数
   
    # 输出层参数
    W_hq = _one((num_hiddens, num_outputs))
    b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
    return nn.ParameterList([W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q])

#在初始化函数中,长短期记忆的隐藏状态需要返回额外的形状为(批量大小, 隐藏单元个数)的值为0的记忆细胞。
def init_lstm_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),
            torch.zeros((batch_size, num_hiddens), device=device))

LSTM模型

下面根据长短期记忆的计算表达式定义模型。需要注意的是,只有隐藏状态Ht会传递到输出层,而记忆细胞Ct不参与输出层的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i)
        F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f)
        O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o)
        C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * C.tanh()
        Y = torch.matmul(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H, C)

训练模型

我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。

1
2
3
4
5
6
7
8
9
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
#我们每过40个迭代周期便根据当前训练的模型创作一段歌词。

d2l.train_and_predict_rnn(lstm, get_params, init_lstm_state, num_hiddens,
                          vocab_size, device, corpus_indices, idx_to_char,
                          char_to_idx, False, num_epochs, num_steps, lr,
                          clipping_theta, batch_size, pred_period, pred_len,
                          prefixes)

用Pytorch简洁实现

在Gluon中我们可以直接调用rnn模块中的LSTM类。

1
2
3
4
5
6
7
8
9
10
11
12
num_hiddens=256
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '不分开']
#我们每过40个迭代周期便根据当前训练的模型创作一段歌词。

lr = 1e-2 # 注意调整学习率
lstm_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(lstm_layer, vocab_size)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
                                corpus_indices, idx_to_char, char_to_idx,
                                num_epochs, num_steps, lr, clipping_theta,
                                batch_size, pred_period, pred_len, prefixes)

小结

  • 长短期记忆的隐藏层输出包括隐藏状态和记忆细胞。只有隐藏状态会传递到输出层。
  • 长短期记忆的输入门、遗忘门和输出门可以控制信息的流动。
  • 长短期记忆可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。