导入环境
%matplotlib inline
import math
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
定义RNNScratch
类
class RNNScratch(d2l.Module): #@save
"""The RNN model implemented from scratch."""
def __init__(self, num_inputs, num_hiddens, sigma=0.01):
super().__init__()
self.save_hyperparameters()
# 保存超参数
self.W_xh = nn.Parameter(
torch.randn(num_inputs, num_hiddens) * sigma)
#表示连接输入层和隐藏层的权重矩阵。
self.W_hh = nn.Parameter(
torch.randn(num_hiddens, num_hiddens) * sigma)
self.b_h = nn.Parameter(torch.zeros(num_hiddens))
# 表示隐藏层的偏差向量,并使用零初始化。
RNN的前向传播
前向方法定义了如何在给定当前输入和上一个时间步的模型状态的情况下计算任何时间步的输出和隐藏状态。请注意,RNN 模型循环遍历输入的最外层维度,一次更新一个步骤的隐藏状态。这里的模型使用
@d2l.add_to_class(RNNScratch) #@save
def forward(self, inputs, state=None):
# 使用条件语句来检查是否提供了隐藏状态 state。如果没有,则使用 torch.zeros 函数初始化一个全零的隐藏状态;否则,直接使用提供的 state。
# 隐藏状态是 RNN 中一个关键的概念,它在每个时间步之间传递信息,并记录了序列数据的过去信息。
if state is None:
# Initial state with shape: (batch_size, num_hiddens)
state = torch.zeros((inputs.shape[1], self.num_hiddens),
device=inputs.device)
else:
state, = state
outputs = []
for X in inputs: # 输入形状: (num_steps, batch_size, num_inputs) 对于每个输入 计算当前的输入与隐藏层的乘积
state = torch.tanh(torch.matmul(X, self.W_xh) +
torch.matmul(state, self.W_hh) + self.b_h)
outputs.append(state)
return outputs, state
#计算出的所有时间步的输出 outputs 和最后的隐藏状态 state。
将一小批输入序列输入 RNN 模型
batch_size, num_inputs, num_hiddens, num_steps = 2, 16, 32, 100
rnn = RNNScratch(num_inputs, num_hiddens)
X = torch.ones((num_steps, batch_size, num_inputs))
outputs, state = rnn(X)
检查 RNN 模型是否产生正确形状的结果,以确保隐藏状态的维数保持不变。
ef check_len(a, n): #@save
# 检查数据长度和形状,并随后利用它们对之前计算的结果进行校验。
assert len(a) == n, f'list\'s length {len(a)} != expected length {n}'
def check_shape(a, shape): #@save
assert a.shape == shape, \
f'tensor\'s shape {a.shape} != expected shape {shape}'
#函数使用 assert 语句来检查张量 a 的形状是否等于 shape。如果不相等,它会抛出一个异常并打印一条错误信息,其中包含实际形状和期望形状。
check_len(outputs, num_steps)
check_shape(outputs[0], (batch_size, num_hiddens))
check_shape(state, (batch_size, num_hiddens))
使用RNN来训练一个语言模型
训练语言模型时,输入和输出来自相同的词汇表。因此,它们具有相同的维度,等于词汇量的大小。
class RNNLMScratch(d2l.Classifier):
def __init__(self, rnn, vocab_size, lr=0.01):
super().__init__()
self.save_hyperparameters()
self.init_params()
def init_params(self):
"""初始化模型的参数。"""
self.W_hq = nn.Parameter(
torch.randn(
self.rnn.num_hiddens, self.vocab_size) * self.rnn.sigma)
self.b_q = nn.Parameter(torch.zeros(self.vocab_size))
def training_step(self, batch):
"""执行模型的训练步骤。
batch : tuple
包含输入序列和对应标签的批次数据。
Returns:
-------
torch.Tensor
训练损失。
"""
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('ppl', torch.exp(l), train=True)
return l
def validation_step(self, batch):
"""执行模型的验证步骤。
Parameters:
----------
batch : tuple
包含输入序列和对应标签的批次数据。
Returns:
-------
torch.Tensor
验证损失。
"""
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('ppl', torch.exp(l), train=False)
return l
梯度剪裁
梯度剪裁是一种用于控制梯度大小的技术,主要用于防止训练过程中出现梯度爆炸的情况。当神经网络的参数更新过程中,梯度的数值过大,导致参数更新过大,进而导致模型不稳定或者无法收敛时,就需要采用梯度剪裁来限制梯度的大小。
@d2l.add_to_class(d2l.Trainer) #@save
def clip_gradients(self, grad_clip_val, model):# 梯度裁剪
params = [p for p in model.parameters() if p.requires_grad]
# 提取所有需要梯度更新的模型参数,
#首先对所有参数的梯度张量进行逐元素平方,然后将结果相加得到每个参数的平方梯度总和
#最后对所有参数的平方梯度总和进行求和并取平方根,得到所有参数的梯度 L2 范数。
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
# 计算所有参数梯度的 L2 范数
if norm > grad_clip_val:
for param in params:
param.grad[:] *= grad_clip_val / norm
训练语言模型
# 加载时间机器数据集,设置批次大小为 1024,序列长度为 32
data = d2l.TimeMachine(batch_size=1024, num_steps=32)
# 初始化自定义的循环神经网络模型(RNNScratch),输入维度为词汇表大小,隐藏层维度为 32
rnn = RNNScratch(num_inputs=len(data.vocab), num_hiddens=32)
# 初始化自定义的 RNN 语言模型(RNNLMScratch),传入循环神经网络模型、词汇表大小和学习率
model = RNNLMScratch(rnn, vocab_size=len(data.vocab), lr=1)
# 初始化训练器(Trainer),设置最大训练周期为 100,梯度剪裁阈值为 1,使用一个 GPU
trainer = d2l.Trainer(max_epochs=100, gradient_clip_val=1, num_gpus=1)
# 开始训练模型
trainer.fit(model, data)
预测
def predict(self, prefix, num_preds, vocab, device=None):
state, outputs = None, [vocab[prefix[0]]]
for i in range(len(prefix) + num_preds - 1):
# 将上一步预测的词转换为独热编码
X = torch.tensor([[outputs[-1]]], device=device)
embs = self.one_hot(X)
# 使用 RNN 模型进行前向计算
rnn_outputs, state = self.rnn(embs, state)
# Warm-up 阶段:添加词语前缀中的下一个词
if i < len(prefix) - 1:
outputs.append(vocab[prefix[i + 1]])
# 预测阶段:使用模型输出预测下一个词
else:
Y = self.output_layer(rnn_outputs)
outputs.append(int(Y.argmax(axis=2).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])