本节将使用nn来更简洁地实现基于循环神经网络的语言模型。首先,我们读取周杰伦专辑歌词数据集。
import d2ltorch as d2lt
import math
import torch
from torch import nn, optim, utils
import time
(corpus_indices, char_to_idx, idx_to_char,
vocab_size) = d2lt.load_data_jay_lyrics()
nn的RNN
模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层rnn_layer
,并对权重做初始化。
num_hiddens = 256
rnn_layer = nn.RNN(vocab_size, num_hiddens)
接下来调用rnn_layer
时如果没有传入隐藏状态hx
,会使用torch.zeros
函数初始化hx
。它是一个形状为(隐藏层个数, 批量大小, 隐藏单元个数)的tensor
。
与上一节中实现的循环神经网络不同,这里rnn_layer
的输入形状为(时间步数, 批量大小, 输入个数)。其中输入个数即one-hot向量长度(词典大小)。此外,rnn_layer
作为PyTorch的nn.RNN
实例,在前向计算后会分别返回输出和隐藏状态,其中输出指的是隐藏层在各个时间步上计算并输出的隐藏状态,它们通常作为后续输出层的输入。需要强调的是,该“输出”本身并不涉及输出层计算,形状为(时间步数, 批量大小, 隐藏单元个数)。而nn.RNN
实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中;对于像长短期记忆这样的循环神经网络,该变量还会包含其他信息。我们会在本章的后面介绍长短期记忆和深度循环神经网络。
batch_size = 2
num_steps = 35
X = torch.rand(num_steps, batch_size, vocab_size)
Y, state_new = rnn_layer(X)
Y.shape, len(state_new), state_new[0].shape
(torch.Size([35, 2, 256]), 1, torch.Size([2, 256]))
接下来我们继承Module
类来定义一个完整的循环神经网络。它首先将输入数据使用one-hot向量表示后输入到rnn_layer
中,然后使用全连接输出层得到输出。输出个数等于词典大小vocab_size
。
#本类已保存在d2ltorch包中方便以后使用
class RNNModel(nn.Module):
def __init__(self, rnn_layer, num_hiddens, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.linear = nn.Linear(num_hiddens, vocab_size)
def forward(self, inputs, state=None):
# 将输入转置成(num_steps, batch_size)后获取one-hot向量表示
X = torch.stack(d2lt.to_onehot(inputs, self.vocab_size, inputs.device))
Y, state = self.rnn(X, state)
# 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens),它的输出
# 形状为(num_steps * batch_size, vocab_size)
output = self.linear(Y.reshape(-1, Y.shape[-1]))
return output, state
同上一节一样,下面定义一个预测函数。这里的实现区别在于前向计算和初始化隐藏状态的函数接口。
# 本函数已保存在d2ltorch包中方便以后使用
def predict_rnn_nn(prefix, num_chars, model, vocab_size, device, idx_to_char,
char_to_idx):
# 使用model的成员函数来初始化隐藏状态
state = None
output = [char_to_idx[prefix[0]]]
with torch.no_grad():
for t in range(num_chars + len(prefix) - 1):
X = torch.Tensor([output[-1]]).to(device).reshape(1, 1)
(Y, state) = model(X, state) # 前向传播不需要传入模型参数
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(int(Y.argmax(dim=1).item()))
return ''.join([idx_to_char[i] for i in output])
让我们使用权重为随机值的模型来预测一次。
device = d2lt.try_gpu()
model = RNNModel(rnn_layer, num_hiddens, vocab_size)
model.to(device)
predict_rnn_nn('分开', 10, model, vocab_size, device, idx_to_char, char_to_idx)
'分开每杰疗每杰每杰疗每杰'
接下来实现训练函数。算法同上一节的一样,但这里只使用了相邻采样来读取数据。
# 本函数已保存在d2ltorch包中方便以后使用
def train_and_predict_rnn_nn(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes):
loss = nn.CrossEntropyLoss()
model.to(device)
d2lt.params_init(model, init=nn.init.normal_, mean=0.01)
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0, weight_decay=0)
for epoch in range(num_epochs):
l_sum, n, start = 0.0, 0, time.time()
data_iter = d2lt.data_iter_consecutive(
corpus_indices, batch_size, num_steps, device)
state = None
for X, Y in data_iter:
optimizer.zero_grad()
if not state is None:
if isinstance(state, tuple):
for s in state:
s.detach_()
else:
state.detach_()
(output, state) = model(X, state)
y = Y.long().t().flatten()
l = loss(output, y)
l.backward()
# 梯度裁剪
nn.utils.clip_grad_norm_(model.parameters(), clipping_theta)
optimizer.step()
l_sum += l.data.mean().item() * torch.numel(y.data)
n += torch.numel(y.data)
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, math.exp(l_sum / n), time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn_nn(
prefix, pred_len, model, vocab_size, device, idx_to_char,
char_to_idx))
使用和上一节实验中一样的超参数来训练模型。
num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
train_and_predict_rnn_nn(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes)
epoch 50, perplexity 1.401943, time 0.09 sec - 分开 我有你寻身边 我池手 我有植身米 我想想想想你 我 我有多烦恼 没有你烦我有多烦恼多难熬 穿在 - 不分开 我有样遇还我是你的想很久 黄想要有点寻天 你想这样没有你烦的我有多烦恼 没有你烦我有多烦恼多难 epoch 100, perplexity 1.190715, time 0.08 sec - 分开 我不要再想你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再想 我不能再想 我不能够不远 - 不分开 我有多烦恼多难熬 穿过云层 我试著努力向你奔跑 爱才送到 你却已在别人怀抱 就是开不了口让她知道 epoch 150, perplexity 1.149434, time 0.08 sec - 分开 我有你寄叹息 如果我遇见你是一场悲剧 我想我这辈子注定一个人演戏 最后再一个人慢慢的回忆 没有了过 - 不分开 我有人有危险 一再重演 一根我不抽的菸 一放好多年 它一直在身边 干什么 干什么 我打开任督二脉 epoch 200, perplexity 1.162889, time 0.09 sec - 分开的开始 能 在隐着我 在 着 所 在的着我 肩膀 你 在我胸口睡著 像这样的生活 我爱你 你爱我 - 不分开的话像语言暴力 我已无能为力的提起 决定中断熟悉 然后在这里 不限日期 然后将过去 慢慢温习 让我爱 epoch 250, perplexity 1.130298, time 0.09 sec - 分开 我不 我不要再想你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再想 我不 我不 我不 我 - 不分开 我不 我不要再想 我不 我不 我不要再想你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再