1.从零开始写 RNN 循环神经网络
1.1 运行环境
- pytorch1.9.1 GPU版
- pycharm 2021
- 李沐动手学深度学习 d2l 包
注:参考李沐书籍,这里只是做代码注释
1.2 代码及注释
# -*- coding: utf-8 -*-
# @Project: zc
# @Author: ZhangChu
# @File name: RNN-ZC
# @Create time: 2021/10/29 5:56
# 1.导入数据库
import math
import matplotlib.pyplot as plt
import torch
from d2l import torch as d2l
from torch.nn import functional as F
import torch.nn
from torch import nn
# 2. 将文本转换成词向量
batch_size = 32 # 批量大小
num_steps = 35 # 每次看多长的序列,时间维度 T
# vocab 作用:可以将一个整数的 index 转换成相对应的词
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
X = torch.arange(10).reshape((2, 5))
# 注:F.one_hot(张量, 类别数 )
# 小批量数据形状是:(批量大小,时间步数);
# 为了方便后续的迭代计算,将时间放在前面,这样就可以以时间 X_t 为序号来处理每个样本
# 我们需要将此转置为:(时间步数, 批量大小,每个样本的特征长度)
# 3. 初始化循环神经网络模型的参数
# 功能:
# 初始化参数:返回一些可以学习的参数
# RNN模型在训练时候,一般隐变量 num_hiddens 是可调超参数,
# 参数:input:
# vocab_size :词汇表的大小,输入和输出都是同一个词汇表,故相同
# num_hiddens : 隐单元的个数(超参数)
# device : 尽量选择网络模型在GPU
#
# 参数:output:
# params = [W_xh,W_hh,b_h,W_hq,b_q]
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size # 词向量的大小等于输入,输出
def normal(shape): # 从一个均值为0*0.01,方差为1*0.01的正太分布中随机抽取给定大小的张量值
return torch.randn(size=shape, device=device) * 0.01
# 公式1: H_t = φ(X_t * W_xh + H_t-1 * W_hh + b_h ) 隐藏层
# 公式2; O_t = H_t * W_hq + b_q 输出层,注:无激活函数
# 注: 如果把 W_hh 这行代码取消掉,就相当于具有一个单隐藏层的 MLP
# RNN 与 MLP 的区别就仅仅加了一个 W_hh 这一层
W_xh = normal((num_inputs, num_hiddens)) # 输入 X 映射到隐藏变量 H 的矩阵
W_hh = normal((num_hiddens, num_hiddens)) # H_t-1 时刻的隐藏变量映射到下一个时刻 H_t
b_h = torch.zeros(num_hiddens, device=device) # 每个隐藏元的偏置 b_h 应该跟隐藏数大小一致
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
# 4. 一个 init_rnn_state 函数在初始化时返回隐藏状态
# 为了定义循环神经⽹络模型,我们⾸先需要⼀个init_rnn_state函数在初始化时返回隐藏状态。函数的返
# 回是⼀个张量,张量全⽤0填充,形状为(批量⼤小, 隐藏单元数)。在后⾯的章节中将会遇到隐藏状态包含多
# 个变量的情况,而使⽤元组可以处理地更容易些。
# 在 t=0 时刻的时候,我们是不知道初始的隐藏单元状态,所以我们需要初始化为张量0
def init_rnn_state(batch_size, num_hiddens, device):
"""
:param batch_size: 批量大小
:param num_hiddens: 隐藏单元数
:param device: GPU设置
:return: (值全为0的张量,) 注:一个元祖,RNN只有一个张量初始化,为了后续 LSTM 我们写成元祖更方便
"""
return (torch.zeros((batch_size, num_hiddens), device=device),)
# 5. 定义 RNN 的 forward 函数
def rnn(inputs, state, params):
"""
注:input 的形状 : (时间步数量,批量大小,词表大小)
功能: 给定一个小批量,将里面的时间步都计算一遍,得到输出定义了如何在一个时间步内计算隐藏状态和输出
:param inputs: X_0 到 X_t 所有的输入
:param state: 提供过来的初始化隐藏状态
:param params: 可学习的参数
:return: 1.所有时刻的状态输出,二维矩阵:(批量大小*时间长度,vocab_size)2.H_t+1的隐状态
"""
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
output = []
# x 的形状为:(批量大小,词表大小)
# 公式1: H_t = φ(X_t * W_xh + H_t-1 * W_hh + b_h ) 隐藏层
# 公式2; O_t = H_t * W_hq + b_q 输出层,注:无激活函数
for x in inputs: # 以时间步为序号进行提取,得到 X_t 时刻的(批量大小,词表大小)
H = torch.tanh(torch.mm(x, W_xh) + torch.mm(H, W_hh) + b_h)
Y = torch.mm(H, W_hq) + b_q
output.append(Y)
return torch.cat(output, dim=0), (H,)
# 6. 定义 RNN 的类,将之前的函数封装到一个类里面
class RNNModelScratch:
"""从零开始实现的循环神经网络"""
"""
:param vocab_size : 词表大小
:param num_hiddens :隐藏单元数
:param device: GPU设置
:param get_params : 获取参数
:param init_state : 初始化隐藏单元状态
:param forward_fn : 定义好的RNN函数,forward 函数
"""
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 =(批量大小,时间步数(句子的长度))
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)
# 7. 我们检查输出是否具有正确的形状,例如,是否保证了隐藏状态的维数保持不变。
num_hiddens = 512
# 设置隐藏单元数大小为512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(),
get_params, init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.to(d2l.try_gpu()), state)
# 8. 定义预测函数来生成 prefix 之后的新字符
def predict_ch8(prefix, num_preds, net, vocab, device):
"""
功能:在 prefix 后面生成新字符
:param prefix: 给定一段词
:param num_preds: 预测多少个词, 这里我们只预测一个字符
:param net: 训练好的模型
:param vocab: 预测值可以映射到真实的字符
:param device: GPU设置
:return:
"""
# state 生成初始化的状态
state = net.begin_state(batch_size=1, device=d2l.try_gpu())
outputs = [vocab[prefix[0]]] # 将prefix的第一个词拿出来,放到 vocab 中,得到相对应的下标
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
# 将输出(outputs)中最后的一个词放入到输入(inputs)中
for y in prefix[1:]: # 此循环是为了初始化
_, state = net(get_input(), state) # 初始化状态即可,前面一个返回值忽略掉
outputs.append(vocab[y]) # 此时 y 是真是的值,不是预测的值
for _ in range(num_preds): # 此循环是为了预测,每次就 get_input 输入,得到输出预测值 y 和 state状态值
y, state = net(get_input(), state) # y = (1,vocab_size)的向量
outputs.append(int(y.argmax(dim=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
# 9. 梯度剪裁,防止梯度爆炸的有效方式
def grad_clipping(net, theta):
"""
裁剪梯度: g <- min(1, theta/|g|)g
:param net: 网络模型
:param theta: 参数
:return:
"""
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))
# 1. 将单层里面的参数的梯度求平方,求和
# 2. 将所有的层的梯度平方和来求和后再开根号得到范数
# 3. 如果梯度大于给定的 theta , 那么我们就将梯度直接改写 param.grad[:] *= theta / norm
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
# 10.训练模型一个迭代周期
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
"""
训练模型一个迭代周期
:param net: 训练的模型
:param train_iter: 训练的迭代
:param loss: 损失函数
:param updater: 优化器
:param device: GPU
:param use_random_iter: 判断是否是随机采样还是顺序采样
:return:
"""
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2) # 训练损失之和, 词元数量
for X, Y in train_iter:
if state is None or use_random_iter:
state = net.begin_state(batch_size=X.shape[0], device=device)
else:
if isinstance(net, nn.Module) and not isinstance(state, tuple):
state.detach_()
else:
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)
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
# 11. 定义训练模型函数
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
"""训练模型(定义见第8章)。"""
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 = 600, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), use_random_iter=True)
plt.figure()
plt.show()
1.3 运行结果
困惑度 1.4, 115179.0 词元/秒 cuda:0
time travellerit s against reason said filbywan a lubu foce time
traveller after the pauserequired for the proper assimilati