目录
一、简介
二、门控循环神经网络的原理
2.1 GRU 的两个重大改进
2.2 GRU 的前向计算公式
2.3 GRU 连接图示
2.4 GRU 整体流程
三、 门控循环神经网络的实现
3.1 读取数据集
3.2 初始化模型参数
3.3 定义模型
3.4 训练模型并创作歌词
3.5 基于 PyTorch 的 API 实现
一、简介
【机器学习】详解 RNN 介绍了 RNN,当时间步数较大或者时间步较小时,RNN 的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。因此,RNN 在实际中较难捕捉时间序列中时间步距离较大的依赖关系。
门控循环神经网络 (Gated Recurrent Neural Network,GRNN) 的提出,旨在更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可学习的门来控制信息的流动。其中,门控循环单元 (Gated Recurrent Unit,GRU) 是一种常用的 GRNN。GRU 对【机器学习】详解 LSTM 介绍的 LSTM 做了很多简化,同时却保持着和 LSTM 相同的效果。
二、门控循环神经网络的原理
2.1 GRU 的两个重大改进
- 将三个门:输入门、遗忘门、输出门变为两个门:更新门 (Update Gate) 和 重置门 (Reset Gate)
- 将 (候选) 单元状态 与 隐藏状态 (输出) 合并,即只有 当前时刻候选隐藏状态 和 当前时刻隐藏状态
- 更新门 控制 前一时刻 的隐藏状态输出 及 当前时刻 的隐藏状态输入 有多少流入当前时刻 的 隐藏状态 中 (以下公式 4),更新门的值越大,流入信息越多。更新门有助于捕捉时间序列里的 长期依赖关系。
- 重置门 控制 前一时刻 的隐藏状态输出 有多少流入当前时刻 的 候选隐藏状态 中 (以下公式 3),重置门的值越小,流入信息越少 / 先前信息遗忘得越多。重置门有助于捕捉时间序列里的 短期依赖关系。
2.2 GRU 的前向计算公式
- 公式 3:当前时间步重置门 与上一时间步隐藏状态 按元素乘法 时,若重置门中的元素值接近 0,那么意味着重置对应隐藏状态元素为 0,即大部分丢弃上一时间步的隐藏状态 ;若重置门中的元素值接近 1,那么表示大部分保留上一时间步的隐藏状态 。然后,将按元素乘法的结果与当前时间步的隐藏状态输入 相加,再通过含激活函数 tanh 的全连接层计算出候选隐藏状态 ,其所有元素的值域为 [−1,1]。由公式 3 可知,重置门控制了上一时间步的隐藏状态 如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。
- 公式 4:更新门可以 控制当前时间步的隐藏状态 应如何被包含当前时间步信息的候选隐藏状态 所更新。假设先前的更新门 在好几个时间步 到当前时间步 () 之间一直近似为 ( 就总近似为 )。那么,在时间步 到 之间的输入信息几乎没有流入时间步 的隐藏状态 。实际上,这可视为是 较早时刻 的隐藏状态 一直随时间保存并传递至当前时间步。该设计可应对 RNN 中的梯度衰减问题,并更好地捕捉时间序列中长期依赖关系 (长期:时间步距离/跨度较大)。
2.3 GRU 连接图示
2.4 GRU 整体流程
GRU 使用两个门来调控长期信息的流动和遗忘,其参数更少,训练更快 (张量操作少了) 且更容易,效果相当。但孰优孰劣还取决于具体场景和应用,通过尝试以确定哪一者更合适。
三、 门控循环神经网络的实现
3.1 读取数据集
为了实现并展示 GRU,使用周杰伦歌词数据集来训练模型作词。这里除门控循环单元以外的实现已在先前系列中介绍过。以下为读取数据集部分。
import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch 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()
3.2 初始化模型参数
下面的代码对模型参数进行初始化。超参数 num_hiddens 定义了隐藏单元的个数。
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])
3.3 定义模型
下面的代码定义隐藏状态初始化函数 init_gru_state,它返回由一个形状为 (批量大小, 隐藏单元个数) 的值为 0 的Tensor 组成的元组。
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
下面根据 GRU 的计算表达式定义模型。
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:
# Eq.1
Z = torch.sigmoid(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
# Eq.2
R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
# Eq.3
H_tilda = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(R * H, W_hh) + b_h)
# Eq.4
H = Z * H + (1 - Z) * H_tilda
# 输出层
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
3.4 训练模型并创作歌词
我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀 “分开” 和 “不分开” 分别创作长度为 50 个字符的一段歌词。
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(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)
输出:
epoch 40, perplexity 149.477598, time 1.08 sec
- 分开 我不不你 我想你你的爱我 你不你的让我 你不你的让我 你不你的让我 你不你的让我 你不你的让我 你
- 不分开 我想你你的让我 你不你的让我 你不你的让我 你不你的让我 你不你的让我 你不你的让我 你不你的让我
epoch 80, perplexity 31.689210, time 1.10 sec
- 分开 我想要你 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不
- 不分开 我想要你 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不要再想 我不
epoch 120, perplexity 4.866115, time 1.08 sec
- 分开 我想要这样牵着你的手不放开 爱过 让我来的肩膀 一起好酒 你来了这节秋 后知后觉 我该好好生活 我
- 不分开 你已经不了我不要 我不要再想你 我不要再想你 我不要再想你 不知不觉 我跟了这节奏 后知后觉 又过
epoch 160, perplexity 1.442282, time 1.51 sec
- 分开 我一定好生忧 唱着歌 一直走 我想就这样牵着你的手不放开 爱可不可以简简单单没有伤害 你 靠着我的
- 不分开 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好生活 我该好好生活
3.5 基于 PyTorch 的 API 实现
在 PyTorch 中直接调用 nn 模块中的 GRU 类即可快速使用:
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)
输出:
epoch 40, perplexity 1.022157, time 1.02 sec
- 分开手牵手 一步两步三步四步望著天 看星星 一颗两颗三颗四颗 连成线背著背默默许下心愿 看远方的星是否听
- 不分开暴风圈来不及逃 我不能再想 我不能再想 我不 我不 我不能 爱情走的太快就像龙卷风 不能承受我已无处
epoch 80, perplexity 1.014535, time 1.04 sec
- 分开始想像 爸和妈当年的模样 说著一口吴侬软语的姑娘缓缓走过外滩 消失的 旧时光 一九四三 在回忆 的路
- 不分开始爱像 不知不觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好
epoch 120, perplexity 1.147843, time 1.04 sec
- 分开都靠我 你拿着球不投 又不会掩护我 选你这种队友 瞎透了我 说你说 分数怎么停留 所有回忆对着我进攻
- 不分开球我有多烦恼多 牧草有没有危险 一场梦 我面对我 甩开球我满腔的怒火 我想揍你已经很久 别想躲 说你
epoch 160, perplexity 1.018370, time 1.05 sec
- 分开爱上你 那场悲剧 是你完美演出的一场戏 宁愿心碎哭泣 再狠狠忘记 你爱过我的证据 让晶莹的泪滴 闪烁
- 不分开始 担心今天的你过得好不好 整个画面是你 想你想的睡不著 嘴嘟嘟那可爱的模样 还有在你身上香香的味道
参考资料:
零基础入门深度学习(6) - 长短时记忆网络(LSTM) - 作业部落 Cmd Markdown 编辑阅读器