pytorch 学习笔记(三⼗⼀):门控循环单元(GRU )
⽂章⽬录
前⾔
时间步数较⼤或者时间步较⼩时,循环神经⽹络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但⽆法解决梯度衰减的问题。通常由于这个原因,循环神经⽹络在实际中较难捕捉时间序列中时间步距离较⼤的依赖关系。
门控循环神经⽹络(gated recurrent neural network) 的提出,正是为了更好地捕捉时间序列中时间步距离较⼤的依赖关系。它通过可以学习的门来控制信息的流动。其中,门控循环单元(gated recurrent unit,GRU)是⼀种常⽤的门控循环神经⽹络。
1. 门控循环单元
下⾯将介绍门控循环单元的设计。它引⼊了重置门(reset gate)和更新门(update gate)的概念,从⽽修改了循环神经⽹络中隐藏状态计算⽅式。
1.1 重置门和更新门
如图6.4所⽰,门控循环单元中的重置门和更新门的输⼊均为当前时间步输⼊与上⼀时间步隐藏状态,输出由激活函数为sigmoid
函数的全连接层计算得到。
具体来说,假设隐藏单元个数为,给定时间步的⼩批量输⼊(样本数为,输⼊个数为)和上⼀时间步隐藏状态。重置门和更新门的计算如下:
其中和是权重参数,是偏差参数。3.8节(多层感知机)节中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,重置门和更新门中每个元素的值域都是。
1.2 候选隐藏状态
接下来,门控循环单元将计算候选隐藏状态来辅助稍后的隐藏状态计算。如图6.5所⽰,我们将当前时间步重置门的输出与上⼀时间步隐藏状态做按元素乘法(符号为)。如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上⼀时间步的隐藏状态。如果元素值接近1,那么表⽰保留上⼀时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输⼊连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为。
X t H t −1h t X ∈t R n ×d n d H ∈t −1R n ×h R ∈t R n ×h Z ∈t R n ×h R =σ(X W +H W +b ),
t t xr t −1hr r Z =σ(X W +H W +b ),
t t xz t −1hz z W ,W ∈xr xz R d ×h W ,W ∈hr hz R h ×h b ,b ∈r z R 1×h R t Z t [0,1]⊙[−1,1]
具体来说,时间步的候选隐藏状态的计算为其中和是权重参数,是偏差参数。从上⾯这个公式可以看出,重置门控制了上⼀时间步的隐藏状态如何流⼊当前时间步的候选隐藏状态。⽽上⼀时间步的隐藏
状态可能包含了时间序列截⾄上⼀时间步的全部历史信息。因此,重置门可以⽤来丢弃与预测⽆关的历史信息。
1.3 隐藏状态
最后,时间步的隐藏状态的计算使⽤当前时间步的更新门来对上⼀时间步的隐藏状态和当前时间步的候选隐藏状态
做组合:
值得注意的是,更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,如图6.6所⽰。假设更新门在时间步到()之间⼀直近似1。那么,在时间步到之间的输⼊信息⼏乎没有流⼊时间步的隐藏状态。实际上,这可以看作是较早时刻的隐藏状态⼀直通过时间保存并传递⾄当前时间步。这个设计可以应对循环神经⽹络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较⼤的依赖关系。
t ∈H ~
t R n ×h =H ~t tanh(X W +t xh R ⊙H W +(t t −1)hh b ),
h W ∈xh R d ×h W ∈hh R h ×h b ∈h R 1×h t H ∈t R n ×h Z t H t −1H ~t H =t Z ⊙t H +t −1(1−Z )⊙t .
H ~t t ′t t <′t t ′t t H t H t −1′t
我们对门控循环单元的设计稍作总结:
重置门有助于捕捉时间序列⾥短期的依赖关系;
更新门有助于捕捉时间序列⾥长期的依赖关系。
2. 读取数据集
为了实现并展⽰门控循环单元,下⾯依然使⽤周杰伦歌词数据集来训练模型作词。以下为读取数据集部分。
import numpy as np
import torch
from torch import nn, optim
functional as F
import sys
sys.path.append("..")
device = torch.device('cuda'if torch.cuda.is_available()else'cpu')
加载周杰伦歌词数据集
def load_data_jay_lyrics():
我想就这样牵着你的手不放开
"""加载周杰伦歌词数据集"""
with zipfile.ZipFile('../../data/ip')as zin:
with zin.open('')as f:
corpus_chars = f.read().decode('utf-8')
corpus_chars = place('\n',' ').replace('\r',' ')
corpus_chars = corpus_chars[0:10000]
idx_to_char =list(set(corpus_chars))
char_to_idx =dict([(char, i)for i, char in enumerate(idx_to_char)])
vocab_size =len(char_to_idx)
corpus_indices =[char_to_idx[char]for char in corpus_chars]
return corpus_indices, char_to_idx, idx_to_char, vocab_size
(corpus_indices, char_to_idx, idx_to_char, vocab_size)= load_data_jay_lyrics()
3. 从零开始实现
我们先介绍如何从零开始实现门控循环单元。
3.1 初始化模型参数
下⾯的代码对模型参数进⾏初始化。超参数num_hiddens定义了隐藏单元的个数。
num_inputs, num_hiddens, num_outputs = vocab_size,256, vocab_size
print('will use', device)
def get_params():
def_one(shape):
ts = sor(al(0,0.01, size=shape), device=device, dtype=torch.float32)
Parameter(ts, requires_grad=True)
def_three():
return(_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
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 = s(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])
3.2 定义模型
下⾯的代码定义隐藏状态初始化函数init_gru_state。同6.4节(循环神经⽹络的从零开始实现)中定义的init_rnn_state函数⼀样,它返回由⼀个形状为(批量⼤⼩, 隐藏单元个数)的值为0的Tensor组成的元组。
def init_gru_state(batch_size, num_hiddens, device):
s((batch_size, num_hiddens), device=device),)
下⾯根据门控循环单元的计算表达式定义模型。
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)+ torch.matmul(R * 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,)
3.3 训练模型并创作歌词
def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, is_random_iter, num_epochs, num_steps,
lr, clipping_theta, batch_size, pred_period,
pred_len, prefixes):
if is_random_iter:
data_iter_fn = data_iter_random
else:
data_iter_fn = data_iter_consecutive
params = get_params()
loss = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
if not is_random_iter:# 如使⽤相邻采样,在epoch开始时初始化隐藏状态
state = init_rnn_state(batch_size, num_hiddens, device)
l_sum, n, start =0.0,0, time.time()
data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
for X, Y in data_iter:
if is_random_iter:# 如使⽤随机采样,在每个⼩批量更新前初始化隐藏状态
state = init_rnn_state(batch_size, num_hiddens, device)
else:
# 否则需要使⽤detach函数从计算图分离隐藏状态, 这是为了
# 使模型参数的梯度计算只依赖⼀次迭代读取的⼩批量序列(防⽌梯度计算开销太⼤)
for s in state:
s.detach_()
inputs = to_onehot(X, vocab_size)
# outputs有num_steps个形状为(batch_size, vocab_size)的矩阵
(outputs, state)= rnn(inputs, state, params)
# 拼接之后形状为(num_steps * batch_size, vocab_size)
outputs = torch.cat(outputs, dim=0)
# Y的形状是(batch_size, num_steps),转置后再变成长度为
# batch * num_steps 的向量,这样跟输出的⾏⼀⼀对应
y = anspose(Y,0,1).contiguous().view(-1)
# 使⽤交叉熵损失计算平均分类误差
l = loss(outputs, y.long())
# 梯度清0
if params[0].grad is not None:
for param in params:
<_()
l.backward()
grad_clipping(params, clipping_theta, device)# 裁剪梯度
sgd(params, lr,1)# 因为误差已经取过均值,梯度不⽤再做平均
l_sum += l.item()* y.shape[0]
n += y.shape[0]
if(epoch +1)% pred_period ==0:
print('epoch %d, perplexity %f, time %.2f sec'%(
epoch +1, p(l_sum / n), time.time()- start))
for prefix in prefixes:
print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
我们在训练模型时只使⽤相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的⼀段歌词。
num_epochs, num_steps, batch_size, lr, clipping_theta =160,35,32,1e2,1e-2
pred_period, pred_len, prefixes =40,50,['分开','不分开']
我们每过40个迭代周期便根据当前训练的模型创作⼀段歌词。