参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战
这个分为两部分,手写RNN是用最基础的语法实现RNN,尽量不用PyTorch的模块,用来清理思路的。下一节的RNN简介实现才是快速手写RNN。
one-hot 独热编码
在写RNN之前先会议一下独热码。
独热码是只有0和1的向量,独热嘛,肯定只有一个“热”,所以只有一个是1,用来区分不同的元素。
-
举个例子用独热码来区分物体。
-
Q:001是谁?
A:是■
-
Q:juejin?
A:0001
-
那怎么用pytorch写呢?
使用pytorch自带的one_hot函数可以直接生成one hot向量。
torch.nn.functional.one_hot(tensor, num_classes=- 1)
- Parameters
- tensor (LongTensor) – class values of any shape
- num_classes (int) – one hot向量的长度,如果不设置则默认是-1,会根据前一个参数自动调整长度。
-
from torch.nn import functional as F >>> F.one_hot(torch.arange(5)) tensor([[1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]) >>> F.one_hot(torch.arange(5) % 3) tensor([[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 0], [0, 1, 0]]) >>> F.one_hot(torch.arange(5) % 3, num_classes=5) tensor([[1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], [1, 0, 0, 0, 0], [0, 1, 0, 0, 0]]) >>> F.one_hot(torch.tensor([0,1,4])) tensor([[1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 0, 0, 1]]) >>> F.one_hot(torch.arange(0, 6).view(3,2) % 3) tensor([[[1, 0, 0], [0, 1, 0]], [[0, 0, 1], [1, 0, 0]], [[0, 1, 0], [0, 0, 1]]])
RNN
欧克,可以开始写代码了。
模型
import math
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
设置批量大小和读取的时间步长。
加载数据集,这里返回参数多了一个vocab,就是之前简单预处理之后的数据。
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
# 隐藏层参数
W_xh = normal((num_inputs, num_hiddens))
W_hh = normal((num_hiddens, num_hiddens))
b_h = torch.zeros(num_hiddens, device=device)
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
- 参数:
- vocab size即one hot向量的长度
- num hiddens隐藏状态的长度
- device 这里开始考虑是在CPU还是GPU上跑数据
normal函数,因为要重复初始化,所以定义一个normal函数- 初始化各层的weight和bias。也没啥东西,就是相对于多层感知机多了一个隐状态。回去看下图就知道网络结构了。
层感知机,把它扭过来旋转一下方向。
再加上时间序列。就变为下图:
看着上图肯定会想,第一个h没有啊,所以要给他手写一个。
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
init_rnn_state函数的返回是一个张量,张量全用0填充,形状为(批量大小, 隐藏单元数)。
def rnn(inputs, state, params):
# `inputs`的形状:(`时间步数量`,`批量大小`,`词表大小`)
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# `X`的形状:(`批量大小`,`词表大小`)
for X in inputs:
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
这个函数是在一个时间步内计算隐藏状态和输出。可以看作以前的forward函数。
初始化各层权重偏置,初始化初始状态的隐状态,初始化输出。
这里的激活函数用的是tanh函数。
最后返回计算结果和当前的隐状态。
返回结果拼了一下,拼接之后行数变为批量大小×时间步。
class RNNModelScratch:
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)
定义一个类将前边的整合起来,从零开始实现的循环神经网络模型。
__init__就是用前边的函数进行初始化__call__调用,可以看作是forward,待会儿传入rnn函数 函数中对X进行了转置,因为传入的X是批量*时间不长,转置之后将时间步长挪到前边,变为时间步长*批量大小,这样才能适应rnn函数的计算begin_state初始化初始状态,待会儿传入init_rnn_state函数
def grad_clipping(net, theta):
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
这是个梯度剪裁函数,用于缓解RNN梯度消失或者梯度爆炸。梯度剪裁使其符合
训练
训练模型一个迭代周期:
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2) # 训练损失之和, 词元数量
for X, Y in train_iter:
if state is None or use_random_iter:
# 在第一次迭代或使用随机抽样时初始化`state`
state = net.begin_state(batch_size=X.shape[0], device=device)
else:
if isinstance(net, nn.Module) and not isinstance(state, tuple):
# `state`对于`nn.GRU`是个张量
state.detach_()
else:
# `state`对于`nn.LSTM`或对于我们从零开始实现的模型是个张量
for s in state:
s.detach_()
y = Y.T.reshape(-1)
X, y = X.to(device), y.to(device)
y_hat, state = net(X, state)
l = loss(y_hat, y.long()).mean()
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
l.backward()
grad_clipping(net, 1)
updater.step()
else:
l.backward()
grad_clipping(net, 1)
# 因为已经调用了`mean`函数
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
定义训练模型:
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
legend=['train'], xlim=[10, num_epochs])
# 初始化
if isinstance(net, nn.Module):
updater = torch.optim.SGD(net.parameters(), lr)
else:
updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
# 训练和预测
for epoch in range(num_epochs):
ppl, speed = train_epoch_ch8(
net, train_iter, loss, updater, device, use_random_iter)
if (epoch + 1) % 10 == 0:
print(predict('time traveller'))
animator.add(epoch + 1, [ppl])
print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))
测试一下我们的结果:
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())
最后结果大概长这样:
-
《动手学深度学习》系列更多可以看这里:《动手学深度学习》专栏(juejin.cn)
-
笔记Github地址:DeepLearningNotes/d2l(github.com)
还在更新中…………