思路:
这段代码实现了一个循环神经网络(RNN)模型,并使用训练数据对其进行训练。
代码流程:
1.导入所需的库:
import math
import torch
from torch import nn
from torch.nn import functional as F
from main import load_data_time_machine
import matplotlib.pyplot as plt
这部分代码导入了需要使用的库,包括数学库math
、PyTorch库、PyTorch的nn模块、PyTorch的functional模块(用于定义激活函数)、自定义的load_data_time_machine
函数和绘图库matplotlib.pyplot
。
2.设置训练所需的超参数和加载训练数据,加载训练数据具体看:函数流程
batch_size, num_steps = 32, 35
train_iter, vocab = load_data_time_machine(batch_size, num_steps)
这部分代码调用load_data_time_machine
函数加载《时间机器》数据集,并设置了批量大小(batch_size)和时间步数(num_steps)。同时,返回了训练数据迭代器和词汇表。
3.定义梯度更新函数 sgd
:
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
这个函数接受模型参数、学习率(lr)和批量大小(batch_size),并根据SGD算法更新模型参数。对于每个参数,使用梯度下降公式进行更新,并将梯度清零。
4.定义累加器类 Accumulator
:
class Accumulator:
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
这个类用于在n个变量上进行累加操作。它具有add
方法,可以将输入的参数与内部数据进行累加。同时,还提供了reset
方法用于将累加的数据重置为零,并提供了__getitem__
方法用于获取累加的结果。
5.定义模型参数初始化函数 get_params
:
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
这部分代码定义了一个函数get_params
,用于初始化模型的参数。函数接受词汇表大小(vocab_size)、隐藏单元数量(num_hiddens)和设备(device)作为输入。
在函数内部,首先将输入和输出的数量设置为词汇表大小(num_inputs = num_outputs = vocab_size)。
然后定义了一个辅助函数normal
,用于生成服从标准正态分布的随机数,并将其乘以0.01,以限制参数的初始值范围。
接下来,使用normal
函数生成了参数W_xh
、W_hh
、b_h
、W_hq
和b_q
,它们分别表示输入到隐藏层的权重、隐藏层到隐藏层的权重、隐藏层的偏置、隐藏层到输出层的权重和输出层的偏置。这些参数都被初始化为随机值,并使用torch.zeros
生成的全零张量作为偏置。
最后,将这些参数存储在列表params
中,并将它们的requires_grad
属性设置为True
,以便在训练过程中计算梯度。最后,将参数列表返回。
6.定义 RNN 的初始状态函数 init_rnn_state
和 RNN 前向传播函数 rnn
:
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),)
def rnn(inputs, state, params):
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
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,)
这个函数接受批量大小(batch_size)、隐藏单元数量(num_hiddens)和设备(device)作为输入。
在函数内部,使用torch.zeros
生成一个形状为(batch_size, num_hiddens)的全零张量,表示初始化的隐藏状态。由于循环神经网络的隐藏状态是一个元组,所以将这个张量放入元组中并返回。
RNN这个函数接受输入序列(inputs)、隐藏状态(state)和模型参数(params)作为输入。
在函数内部,首先从参数列表中解包出各个参数,包括输入到隐藏层的权重W_xh
、隐藏层到隐藏层的权重W_hh
、隐藏层的偏置b_h
、隐藏层到输出层的权重W_hq
和输出层的偏置b_q
。同时,从隐藏状态中解包出隐藏层的输出H
。
然后,定义一个空列表outputs
,用于存储每个时间步的输出结果。
接下来,使用一个循环遍历输入序列中的每个时间步。对于每个时间步的输入X
,通过以下步骤计算隐藏层的输出和输出层的输出:
- 计算隐藏层的输出:使用
torch.mm
函数计算输入到隐藏层的线性变换,然后将其与隐藏层的权重W_hh
相乘的结果进行相加,并加上隐藏层的偏置b_h
,最后通过torch.tanh
函数进行激活。这一步骤更新了隐藏状态。 - 计算输出层的输出:将隐藏层的输出与输出层的权重
W_hq
相乘,并加上输出层的偏置b_q
。
将输出结果Y
添加到outputs
列表中。
最后,使用torch.cat
函数将所有输出结果在维度0上进行拼接,并将更新后的隐藏状态返回。
7.定义从零开始实现的 RNN 模型类 RNNModelScratch
:
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__
方法中,接受词汇表大小(vocab_size)、隐藏单元数量(num_hiddens)、设备(device)、获取参数的函数(get_params)、初始化隐藏状态的函数(init_state)和循环神经网络的前向传播函数(forward_fn)作为输入。在初始化过程中,调用get_params
函数获取模型的参数,并将其存储在params
属性中。
__call__
方法用于执行模型的前向传播。它接受输入序列X
和隐藏状态state
作
为输入序列X
和隐藏状态state
作为输入。
在方法内部,首先使用F.one_hot
函数将输入序列X
进行独热编码,将其转换为形状为(序列长度,词汇表大小)的浮点张量。这是为了将输入转换为适合模型计算的格式。
然后,调用forward_fn
函数执行循环神经网络的前向传播。传入独热编码后的输入序列X
、隐藏状态state
和模型的参数params
。forward_fn
函数在前面的代码中定义为rnn
函数。
最后,将前向传播的结果返回。begin_state
方法用于初始化隐藏状态。它接受批量大小(batch_size)和设备(device)作为输入。
在方法内部,调用init_state
函数初始化隐藏状态,并将其返回。
这样,RNNModelScratch
类封装了从零开始实现的循环神经网络模型,并提供了初始化、前向传播和初始化隐藏状态的方法,方便使用和调用。
8.创建 RNN 模型实例 net
:
num_hiddens = 512
device = torch.device('cuda')
net = RNNModelScratch(len(vocab), num_hiddens, device, get_params,
init_rnn_state, rnn)
9.定义生成预测结果的函数 predict_ch8
:
def predict_ch8(prefix, num_preds, net, vocab, device):
state = net.begin_state(batch_size=1, device=device)
outputs = [vocab[prefix[0]]]
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
for y in prefix[1:]:
_, state = net(get_input(), state)
outputs.append(vocab[y])
for _ in range(num_preds):
y, state = net(get_input(), state)
outputs.append(int(y.argmax(dim=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
predict_ch8
函数用于生成给定前缀的新字符。它接受前缀(prefix)、要生成的字符数量(num_preds)、模型(net)、词汇表(vocab)和设备(device)作为输入。首先,它使用net.begin_state
方法生成隐藏状态。然后,它使用给定的前缀进行预测,将最近预测的值作为输入,并将预测结果添加到输出列表中。最后,它将输出列表中的索引转换为对应的字符,并将它们连接起来形成一个字符串,作为最终的预测结果。
10.定义梯度裁剪函数 grad_clipping
:
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
grad_clipping
函数用于裁剪梯度。它接受模型(net)和裁剪阈值(theta)作为输入。如果模型是nn.Module
的实例,它会获取模型的参数;否则,它会获取模型的params
属性中的参数。然后,它计算所有参数梯度的范数,并将其与阈值进行比较。如果范数大于阈值,则将所有参数的梯度进行缩放,使范数不超过阈值。
11.定义训练一个迭代周期的函数 train_epoch_ch8
:
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
"""训练模型一个迭代周期(定义见第8章)。"""
state = None
metric = 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]
train_epoch_ch8
函数用于训练模型的一个迭代周期。它接受模型(net)、训练数据迭代器(train_iter)、损失函数(loss)、更新器(updater)、设备(device)和是否使用随机迭代器(use_random_iter)作为输入。在每个迭代步骤中,它首先初始化隐藏状态。然后,将输入数据和标签转移到设备上。接下来,它通过调用模型进行前向传播,得到预测结果和更新后的隐藏状态。然后,计算损失并进行反向传播。如果更新器是torch.optim.Optimizer
的实例,它将执行优化器的相关操作;否则,它将调用自定义的更新器函数。最后,它计算并返回困惑度(perplexity)和标记数量。
12.定义训练模型的函数 train_ch8
:
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
"""训练模型(定义见第8章)。"""
loss = nn.CrossEntropyLoss()
plt.xlabel('epoch')
plt.ylabel('perplexity')
plt.xlim(10, num_epochs)
if isinstance(net, nn.Module):
updater = torch.optim.SGD(net.parameters(), lr)
else:
updater = lambda batch_size: 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'))
plt.plot(epoch + 1, ppl, 'bo')
# print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))
train_ch8
函数用于训练模型。它接受模型(net)、训练数据迭代器(train_iter)、词汇表(vocab)、学习率(lr)、迭代周期数(num_epochs)、设备(device)和是否使用随机迭代器(use_random_iter)作为输入。在函数内部,它定义了损失函数(交叉熵损失)和更新器(使用随机梯度下降)。然后,它使用循环迭代指定的周期数,在每个周期中调用train_epoch_ch8
函数进行模型训练,并记录困惑度和速度。如果当前周期是10的倍数,它会打印生成给定前缀的预测结果,并将困惑度绘制到图表中。最后,它打印最终的困惑度、速度和预测结果。
13.设置训练的迭代次数和学习率,并调用 train_ch8
函数开始训练:
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=True)
最后一部分代码调用了train_ch8
函数来训练模型。它传入了模型(net)、训练数据迭代器(train_iter)、词汇表(vocab)、学习率(lr)、迭代周期数(num_epochs)、设备(device)和是否使用随机迭代器(use_random_iter=True)。这将开始模型的训练过程,并输出每个周期的困惑度、速度和预测结果。
结果:
1.使用顺序提取:
困惑度 1.0, 8960.0 标记/秒 cuda
time traveller for so it will be convenient to speak of him was
traveller with a slight accession of cheerfulness really th
困惑度为1,说明此时效果已经达到最好,但受模型本身的限制,无法达到更高的水平
2.使用随机提取:
困惑度 1.4, 8960.0 标记/秒 cuda
time traveller smiled are you sure we can move freely in space r
traveller came back and filby s anecdote collapsedthe thing
随机提取效果稍微要差一些,但随机性更高,不容易过拟合。