持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
注意:
-
我一共要写两篇文章来讲解两种写GRU的方法,一种是手写实现,一种是直接调用pytorch自带的GRU。
-
本文使用jupyter notebook写的代码,和pycharmh有一点不一样。比如
x
可以直接输出变量,但是在pycharm中需要使用print(x)
才可以。
自己写
要注意,自己写的会和pytorch的有有出入,毕竟人家是经过优化的,所以同样的数据使用我们自己写的训练速度会很慢。这只是带你熟悉流程的。
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
-
调用其他的包。
-
设定batch-size批量大小和时间步的长度num_step。这里需要注意时间步的长度是你一个要处理的序列的时间步有多少个。
-
使用之前我们实现过的加载时光机器数据集。获得数据集的迭代器和词汇表的长度,这里为了方便,使用的是char进行分割,也就是说词汇表是a~z以及空格和<unk>。
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
看代码之前记得回顾一下GRU的计算公式:
重置门:Rt=σ(XtWxr+Ht−1Whr+br),更新门:Zt=σ(XtWxz+Ht−1Whz+bz),候选隐藏状态:H~t=tanh(XtWxh+(Rt⊙Ht−1)Whh+bh)隐藏状态:Ht=Zt⊙Ht−1+(1−Zt)⊙H~tbegin{aligned}
&重置门:&mathbf{R}_t = sigma(mathbf{X}_t mathbf{W}_{xr} + mathbf{H}_{t-1} mathbf{W}_{hr} + mathbf{b}_r),
&更新门:&mathbf{Z}_t = sigma(mathbf{X}_t mathbf{W}_{xz} + mathbf{H}_{t-1} mathbf{W}_{hz} + mathbf{b}_z),
&候选隐藏状态:&tilde{mathbf{H}}_t = tanh(mathbf{X}_t mathbf{W}_{xh} + left(mathbf{R}_t odot mathbf{H}_{t-1}right) mathbf{W}_{hh} + mathbf{b}_h)
&隐藏状态:&mathbf{H}_t = mathbf{Z}_t odot mathbf{H}_{t-1} + (1 – mathbf{Z}_t) odot tilde{mathbf{H}}_t
end{aligned}
这一步是初始化模型参数:实例化与更新门、重置门、候选隐藏状态和输出层相关的所有权重和偏置。
-
num_hiddens
定义隐藏单元的数量, -
normal
函数用于从标准差为 0.01 的高斯分布中随机生成权重。 -
three
函数用于给更新门、重置门、候选隐藏状态初始化权重和偏执,一次更新仨就叫three了。 -
后边三个
w,w,b = three()
分别对应更新门、重置门、候选隐藏状态的初始化。 -
w_hq
和b_q
是初始化隐藏层到输出层的权重和偏执。 -
params
将参数整理到一起,为其附加梯度。
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
这个是隐状态初始化函数。将其初始化为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,)
这是定义GRU的计算,这里就不重复写公式了,往上翻一下子看看公式。
-
开始是用
params
设定gru的参数。 -
H
获取初始的隐藏状态 -
for循环就是对其进行计算。对照公式一目了然的东西。
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,
init_gru_state, gru)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
这段代码是对我们手写GRU的训练和预测测试,会输出预测结果和训练过程的可视化。给你们看个好玩的,仔细看输出的句子和图像。随着训练句子会一直改变,困惑度也一直下降。在CPU上会训练很久,所以要等好长一会儿让他跑完500个epoch。
训练结束后,会分别打印输出训练集的困惑度和前缀“time traveler”和“traveler”的预测序列上的困惑度。因为是随机初始化,所以每次运行的结果都不太一样,我就不贴运行结果出来了。
调用人家的
import torch
from torch import nn
from d2l import torch as d2l
from torch.nn import functional as F
依旧是熟悉的配方熟悉的导包操作。
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
这里和之前没区别,设定一些超参数并加载数据集。
-
设定batch-size批量大小和时间步的长度num_step。这里需要注意时间步的长度是你一个要处理的序列的时间步有多少个。
-
使用之前我们实现过的加载时光机器数据集。获得数据集的迭代器和词汇表的长度,这里为了方便,使用的是char进行分割,也就是说词汇表是a~z以及空格和<unk>。
class GRUModel(nn.Module):
def __init__(self, gru_layer, vocab_size, **kwargs):
super(GRUModel, self).__init__(**kwargs)
self.gru = gru_layer
self.vocab_size = vocab_size
self.num_hiddens = self.gru.hidden_size
if not self.gru.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
def forward(self, inputs, state):
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
Y, state = self.gru(X, state)
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, device, batch_size=1):
return torch.zeros((self.num_directions * self.gru.num_layers,
batch_size, self.num_hiddens),
device=device)
RNN、GRU、LSTM一脉相承,用的类的都差不多,这个是适用于RNN和GRU的,但是不适用于LSTM。LSTM可以看我之后的文章,或者看前边的简洁实现RNN循环神经网络 实现的那个RNNModule类,那个类是涵盖了RNN、GRU、LSTM的通用模型。
-
__init__
初始化这个类,这个类是继承了nn.Module
的。-
self.gru
设定计算层是GRU层,这里是需要参数的,你在下一段代码中会传入nn.GRU
。 -
self.vocab_size
设定字典的大小,这里大小是28,因为我们使用的是字母进行分词,所以其中只有a~z
26个字母外加<unk>
(空格和unknown)。 -
self.num_hiddens
设置隐藏层的大小。普通的RNN是隐藏层,在这里是带隐状态的隐藏层。不是说有隐状态之后就没隐藏层了。 -
if-else语句是设定GRU是单双向的。
-
-
forward
定义前向传播网络。这里不用我们自己来实现计算过程了,
nn.GRU
会直接给我们计算。但是我们依旧需要对数据进行一下才操作。-
首先是将输入转化为对应的one-hot向量,这里
F
看前边导包部分,是使用nn.functional
。torch.float32
再将其类型转化为float。
-
Y
和state
是计算隐状态的,注意 在这里Y
不是 输出。这里Y是输出全部的隐状态,state是输出最后一个时间步的隐状态。 -
output
是用于存储输出的。
-
-
begin_state
是进行初始化。
这里初始化和RNN初始化一样,都是初始化为一个零张量。之后可以留意一下LSTM,LSTM是返回一个元组,元组中有两个张量。
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 100, 1
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = GRUModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
设定一些基础数值:
-
vocab_size
词典长度 -
num_hiddens
隐藏层向量的长度 -
device
在CPU还是GPU上执行 -
num_epochs
训练的epoch数量 -
lr
学习率learning rate
GRU层直接使用nn.GRU
。
之后对其进行训练并测试。输出训练集的困惑度和前缀“time traveler”和“traveler”的预测序列结果。