# 循环神经网络
循环神经网络(recurrent neural networks,RNNs) 是具有隐状态的神经网络。隐藏层和隐状态指的是两个截然不同的概念,隐藏层是在从输入到输出的路径上(以观测角度来理解)的隐藏的层, 而隐状态则是在给定步骤所做的任何事情(以技术角度来定义)的输入, 并且这些状态只能通过先前时间步的数据来计算。
我们初始化循环神经网络模型的模型参数。 隐藏单元数num_hiddens
是一个可调的超参数。 当训练语言模型时,输入和输出来自相同的词表。 因此,它们具有相同的维度,即词表的大小。
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)
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 # 生成指定形状 shape 的随机数张量
# 隐藏层参数
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] # 所有参数都被添加到 params 列表中。
for param in params: # 通过循环,所有参数的 requires_grad 属性被设置为 True,这表示这些参数将用于计算梯度,以便进行反向传播和训练神经网络。
param.requires_grad_(True)
return params
初始化循环神经网络(RNN)的隐藏状态。
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), ) # 张量的形状是 (batch_size, num_hiddens),其中每一行表示一个样本的隐藏状态。
def rnn(inputs, state, params): # inputs 是输入数据的一个序列,其形状为 (时间步数量,批量大小,词表大小)。
# 在时间序列中,每个时间步都有一个输入,时间步数量 表示时间序列的长度,批量大小 表示同时处理的样本数量,词表大小 表示词汇表的大小。
# state 是一个包含一个张量的元组 (H,),其中 H 是隐藏状态张量。params 是包含RNN模型参数的列表
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) # 更新隐藏状态 H
Y = torch.mm(H, W_hq) + b_q # 计算输出 Y
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,) # 执行RNN模型的前向传播,从输入数据中生成输出序列,并更新隐藏状态以在时间步之间传递信息。、
建一个类来包装这些函数, 并存储从零开始实现的循环神经网络模型的参数。
class RNNModelScratch:
def __init__(self, vocab_size, num_hiddens, device, # vocab_size:词汇表的大小,表示输入和输出的词汇表大小。num_hiddens:隐藏层中的隐藏单元数量。
get_params, init_state, forward_fn): # get_params:一个函数,用于获取 RNN 模型的参数。init_state:一个函数,用于初始化 RNN 模型的隐藏状态。forward_fn:一个函数,用于执行 RNN 模型的前向传播。
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 和 RNN 模型的当前状态 state,并返回 RNN 模型的输出结果。
X = F.one_hot(X.T, self.vocab_size).type(torch.float32) # X 被独热编码成浮点数类型,然后传递给 forward_fn 来执行前向传播。
return self.forward_fn(X, state, self.params) # 然后传递给 forward_fn 来执行前向传播。
def begin_state(self, batch_size, device): # 用于初始化 RNN 模型的隐藏状态。
return self.init_state(batch_size, self.num_hiddens, device)
# 预测
def predict_ch8(prefix, num_preds, net, vocab, device): # prefix 是输入的前缀字符串,用于初始化生成过程。num_preds 是要生成的字符的数量。vocab 包含词汇表信息,允许将模型输出的整数转换回字符。
"""在prefix后面生成新字符"""
state = net.begin_state(batch_size=1, device=device) # 初始化 RNN 模型的隐藏状态 state。这里使用批处理大小为 1,因为我们一次生成一个字符
outputs = [vocab[prefix[0]]] # outputs 是一个列表,用于存储生成的字符序列。初始时,它包含前缀字符串的第一个字符,即 vocab[prefix[0]]。
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1)) # get_input 是一个函数,用于获取下一个时间步的输入。它返回一个形状为 (1, 1) 的张量,其中 1 表示批处理大小和序列长度。
for y in prefix[1:]: # 预热期 我们通过将每个字符作为输入传递给 RNN 模型来更新隐藏状态,但不存储输出。这有助于模型适应前缀字符串。
_, state = net(get_input(), state)
outputs.append(vocab[y])
for _ in range(num_preds): # 预测num_preds步
y, state = net(get_input(), state) # 使用 get_input() 获取下一个输入字符
outputs.append(int(y.argmax(dim=1).reshape(1))) # 择输出中概率最高的字符作为预测字符,并将其整数索引添加到 outputs 列表中。
return ''.join([vocab.idx_to_token[i] for i in outputs]) # 将生成的整数字符序列转换回字符,并使用 join 函数将它们连接起来,形成生成的字符串。
# 梯度裁剪
def grad_clipping(net, theta): # net 是输入的神经网络模型,theta 是梯度裁剪的阈值,用于限制梯度的范数。
if isinstance(net, nn.Module): # 如果 net 是 nn.Module 类型,它将获取模型中所有具有梯度的参数,并将其存储在 params 列表中。如果 net 不是 nn.Module 类型,它将使用 net.params 来获取参数。
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)) # 算所有参数的梯度的范数 norm,这是所有参数梯度的二范数之和。
if norm > theta: # 如果 norm 大于指定的阈值 theta,则梯度裁剪将被执行。对于每个参数,函数将梯度值按比例缩放,以确保梯度的范数不超过阈值 theta。
for param in params:
param.grad[:] *= theta / norm
# 训练
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
# train_iter 是用于训练的数据迭代器,它产生输入数据 X 和目标数据 Y。loss 是损失函数,用于衡量模型的预测与实际目标的差距。updater 是用于更新模型参数的优化器。
# use_random_iter 是一个布尔值,指示是否使用随机迭代。如果设置为 True,则在每个迭代周期的第一个迭代中将初始化隐藏状态,否则将在每个迭代中使用先前迭代的最终隐藏状态。
"""训练网络一个迭代周期(定义见第8章)"""
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 的类型,以确保它是可以进行梯度计算的张量
# state对于nn.GRU是个张量
state.detach_()
else:
# state对于nn.LSTM或对于我们从零开始实现的模型是个张量
for s in state:
s.detach_()
y = Y.T.reshape(-1) # 将目标数据 Y 转置后展平为一维张量 y。
X, y = X.to(device), y.to(device)
y_hat, state = net(X, state) # 使用神经网络模型 net 对输入数据进行前向传播,得到预测结果 y_hat 和更新后的隐藏状态 state。
l = loss(y_hat, y.long()).mean()
if isinstance(updater, torch.optim.Optimizer): # 如果 updater 是 PyTorch 的优化器,清零优化器的梯度,然后执行反向传播和参数更新。否则,直接执行梯度裁剪和自定义的参数更新操作。
updater.zero_grad()
l.backward()
grad_clipping(net, 1) # 函数执行梯度裁剪,将梯度限制在阈值 1 内。
updater.step() # 调用 updater.step() 来更新模型的参数。
else:
l.backward()
grad_clipping(net, 1)
# 因为已经调用了mean函数
updater(batch_size=1)
metric.add(l * y.numel(), y.numel()) # 使用累加器 metric 更新训练损失之和和处理的词元数量
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):
"""训练模型(定义见第8章)"""
loss = nn.CrossEntropyLoss() # 定义了交叉熵损失函数 loss,用于度量模型的预测与实际目标的差距。
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) # 用于生成文本预测。这个函数接受一个前缀字符串,并生成模型生成的文本序列,长度为 50 个字符。
# 训练和预测
for epoch in range(num_epochs):
ppl, speed = train_epoch_ch8( # 调用 train_epoch_ch8 函数,用于训练一个迭代周期,并获取每个词元上的指数损失 ppl 和处理速度 speed。
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())
# 循环神经网络的简洁实现
构造一个具有256个隐藏单元的单隐藏层的循环神经网络层rnn_layer
。
num_hiddens = 256 # len(vocab):这是词汇表的大小,也就是词汇表中不同字符的数量。RNN层的输入维度将等于词汇表的大小,因为它将处理字符级别的文本数据。
rnn_layer = nn.RNN(len(vocab), num_hiddens) # RNN层配置为具有256个隐藏单元。
使用张量来初始化隐状态,它的形状是(隐藏层数,批量大小,隐藏单元数)。
rnn_layer
的“输出”(Y
)不涉及输出层的计算: 它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。
X = torch.rand(size=(num_steps, batch_size, len(vocab))) # X 的随机张量,其形状为 (num_steps, batch_size, len(vocab))。这个张量用于表示输入数据,num_steps 是时间步的数量,表示模型将在多少个时间步内处理输入数据。batch_size 是批量大小,表示在每个时间步处理多少个样本。len(vocab) 是词汇表的大小,表示输入数据的词表大小。在这里,它代表了字符级别的词汇表的大小。
Y, state_new = rnn_layer(X, state) # Y 是输出张量,它包含 RNN 层在每个时间步上生成的输出。state_new 是更新后的 RNN 状态,它包含 RNN 层在处理完输入数据后的最终状态。
Y.shape, state_new.shape
接下来创建一个RNNModel。
#@save
class RNNModel(nn.Module):
"""循环神经网络模型"""
def __init__(self, rnn_layer, vocab_size, **kwargs): # rnn_layer:RNN层,它定义了模型的循环结构。vocab_size:词汇表的大小,表示模型的输出词汇表大小。**kwargs:其他可选参数。
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 根据RNN层是否是双向的来选择线性变换层的输出维度。
if not self.rnn.bidirectional:
self.num_directions = 1 # 如果RNN不是双向的,则设置 self.num_directions 为 1,然后创建一个线性层 self.linear,将隐藏状态映射到词汇表的大小。
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2 # 如果RNN是双向的,则 self.num_directions 设置为 2,并且线性层 self.linear 的输入维度为 self.num_hiddens * 2,以将双向RNN的隐藏状态映射到词汇表的大小。
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
def forward(self, inputs, state): # inputs:输入数据,通常是文本序列。state:RNN的隐藏状态,用于存储和传递RNN的内部状态。
X = F.one_hot(inputs.T.long(), self.vocab_size) # 首先将输入转置,self.vocab_size 表示词汇表的大小。然后将其转化为独热编码。
X = X.to(torch.float32)
Y, state = self.rnn(X, state) # 将转换后的输入数据 X 和隐藏状态 state 传递给RNN层 self.rnn 进行前向传播。Y 包含RNN层在每个时间步上生成的输出,state 存储更新后的RNN状态。
output = self.linear(Y.reshape((-1, Y.shape[-1]))) # Y 的形状被调整为 (时间步数*批量大小, 隐藏单元数),然后通过线性变换映射到 (时间步数*批量大小, 词汇表大小) 的输出。
return output, state
def begin_state(self, device, batch_size=1): # 创建RNN的初始状态。
if not isinstance(self.rnn, nn.LSTM): # 首先检查RNN层的类型,如果RNN层不是LSTM类型,则创建一个全零的张量作为隐藏状态,并根据 self.num_directions 和 self.rnn.num_layers 确定张量的形状。
# nn.GRU以张量作为隐状态
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device)
else:
# 如果是 nn.LSTM,则返回元组形式的初始状态。
return (torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device),
torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device))
然后我们进行训练,训练的代码即是上面的train_ch8
与之前从零开始实现相比,我们的困惑度下降了,花费的时间也减少了