经典循环神经网络(一)RNN及其在歌词数据集上的应用
1 RNN概述
在深度学习兴起之前,NLP领域一直是统计模型的天下,例如词对齐算法GIZA++,统计机器翻译开源框架MOSES等等。在语言模型方向,n-gram是当时最为流行的语言模型方法。n-gram的问题是其捕捉句子中长期依赖的能力非常有限。另外n-gram算法过于简单,其是否有能力取得令人信服的效果的确要打一个大的问号。
循环神经网络是为更好地处理时序信息而设计的。它引入状态变量来存储过去的信息,并⽤其与当前的输入共同决定当前的输出
。
循环神经网络常用于处理序列数据,如⼀段文字或声音、购物或观影的顺序,甚⾄是图像中的⼀行或⼀列像素。因此,循环神经网络有着极为广泛的实际应用,如语言模型、文本分类、机器翻译、语音识别、图像分析等。
1.1 RNN理解
下面图像,来自台大李宏毅老师课件
1.1.1 RNN的引入
对于2句话,都有Taipei这个词,但是一个是目的地,一个是出发地
如果神经网络有记忆力,能够根据上下文对同样的input词汇产生不同的输出,我们就能解决这个问题
像下面两句话,同样输入Taipei,一个输出“目的地”,一个输出“出发地”
arrive Taipei on November 2
leave Taipei on November 2
1.1.2 结合例子理解
首先,网络接收第一个单词(arrive)的输入,经过网络得到一个输出,并保存隐藏层的输出。
然后接收第二个单词(Taipei)的输入,经过网络得到输出,……,用同样的网络结构不断重复这个行为
所以当两句不同的话输入的时候,一个Taipei前面是leave,一个是arrive,而这两个的vector是不一样的,所以存在memory中的值不同,这样就会得到不同的输出。
更一般地,网络可以不止有一个隐藏层,而是有许多个隐藏层,每个单词输入的时候,各个隐藏层都会考虑之前存在memory中的值。
1.2 RNN网络架构
上图展示了循环神经网络在三个相邻时间步的计算逻辑。
其中:
隐状态中
X
t
W
x
h
+
H
t
−
1
W
h
h
的计算,相当于
X
t
和
H
t
−
1
的拼接与
W
x
h
和
W
h
h
的拼接的矩阵乘法。
隐状态中 X_tW_{xh} + H_{t-1}W_{hh}的计算,相当于X_t和H_{t-1}的拼接与W_{xh}和W_{hh}的拼接的矩阵乘法。
隐状态中XtWxh+Ht−1Whh的计算,相当于Xt和Ht−1的拼接与Wxh和Whh的拼接的矩阵乘法。
这里使用代码验证一下:
RNN的实现非常简单,如下:
'''
定义了如何在【⼀个时间步内】计算隐状态和输出。
循环神经⽹络模型通过inputs最外层的维度实现循环,以便逐时间步更新⼩批量数据的隐状态H。
此外,这⾥使⽤tanh函数作为激活函数。
'''
def rnn(inputs, state, params):
"""
:param inputs: inputs的形状:(时间步数量,批量⼤⼩,词表⼤⼩)
:param state: 隐状态
:param params: 初始化的权重及偏置参数
:return:
"""
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# X的形状:(批量⼤⼩,词表⼤⼩)
for X in inputs:
# 隐藏层激活函数是tanh。
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h) # 隐变量state
# 输出层没有激活函数,就是隐变量的线性变换
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
2 RNN在歌词数据集上的应用
2.1 歌词数据集的预处理
歌词数据集下载地址:https://gitee.com/inkiinki/data20201205/blob/master/Data20201205/jaychou_lyrics.txt.zip
2.1.1 读取歌词数据集
这是⼀个相当小的语料库,但足够我们测试,现实中的文档集合可能会包含数十亿个单词。
import collections
import random
import torch
import re
# 读取歌词数据集
def read_jaychou(file_loc = '../data/jaychou_lyrics.txt'):
with open(file=file_loc, mode='r', encoding='utf8') as f:
lines = f.readlines()
return lines
lines = read_jaychou()
print(f'⽂本总⾏数: {len(lines)}')
print(lines[0])
print(lines[1])
⽂本总⾏数: 5819
想要有直升机
想要和你飞到宇宙去
2.1.2 词元化
tokenize函数将文本行列表(lines)作为输入,列表中的每个元素是⼀个文本序列(如⼀条文本
行)。每个文本序列又被拆分成⼀个词元列表,词元(token)是文本的基本单位。最后,返回⼀个由词元列
表组成的列表。这里为了方便,一个中文字为一个词元。
def tokenize(lines):
tokens = []
for line in lines:
line = line.replace("\n", " ").replace("\r", " ")
fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>',
'\?', '@', '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”',
'“', ]
line = re.sub("|".join(fileters), "", line)
tokens.append(list(line))
return tokens
2.1.3 词表
-
我们先将训练集中的所有文档合并在⼀起,对它们的唯⼀词元进行统计,得到的统计结果称之为语料(corpus)。
-
然后根据每个唯⼀词元的出现频率,为其分配⼀个数字索引。很少出现的词元通常被移除,这可以降低复杂性。
-
另外,语料库中不存在或已删除的任何词元都将映射到⼀个特定的未知词元“”。
-
我们可以选择增加⼀个列表,用于保存那些被保留的词元,例如:填充词元(“”);序列开始词元(“”);序列结束词元(“”)。
def count_corpus(tokens):
"""统计词元的频率"""
# 这⾥的tokens是1D列表或2D列表
if len(tokens) == 0 or isinstance(tokens[0], list):
# 将词元列表展平成⼀个列表
tokens = [token for line in tokens for token in line]
return collections.Counter(tokens)
class Vocab:
"""⽂本词表类"""
def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
if tokens is None:
tokens = []
if reserved_tokens is None:
reserved_tokens = []
# 按出现频率排序
counter = count_corpus(tokens)
self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True)
# 未知词元的索引为0
self.idx_to_token = ['<unk>'] + reserved_tokens
self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)}
for token, freq in self._token_freqs:
if freq < min_freq:
break
if token not in self.token_to_idx:
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token) - 1
self.vocab_size = len(self.idx_to_token)
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self, tokens):
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk)
return [self.__getitem__(token) for token in tokens]
def to_tokens(self, indices):
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
return [self.idx_to_token[index] for index in indices]
@property
def unk(self): # 未知词元的索引为0
return 0
@property
def token_freqs(self):
return self._token_freqs
vocab = Vocab(tokens)
print(vocab.vocab_size)
print(vocab.idx_to_token[:10])
print(list(vocab.token_to_idx.items())[:10])
最后,将上面3步进行封装
def load_corpus_jaychou(max_tokens=-1):
"""返回歌词数据集的词元索引列表和词表"""
lines = read_jaychou()
tokens = tokenize(lines)
vocab = Vocab(tokens)
# 将所有文本行展平到一个列表中
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens > 0:
corpus = corpus[:max_tokens]
return corpus, vocab
2.1.4 读取序列数据
随机采样
'''
随机采样:
在随机采样中,每个样本都是在原始的⻓序列上任意捕获的⼦序列。
1、在迭代过程中,来⾃两个相邻的、随机的、⼩批量中的⼦序列不⼀定在原始序列上相邻。
例如:
[15., 16., 17., 18., 19.] 和 [5., 6., 7., 8., 9.]
2、对于语⾔建模,⽬标是基于到⽬前为⽌我们看到的词元来预测下⼀个词元,因此标签label是移位了⼀个词元的原始序列。
例如:
[15., 16., 17., 18., 19.]对应的标签label是[16., 17., 18., 19., 20.]
下⾯的代码每次可以从数据中随机⽣成⼀个⼩批量。
参数batch_size指定了每个⼩批量中⼦序列样本的数⽬
参数num_steps是每个⼦序列中预定义的时间步数
'''
def seq_data_iter_random(corpus, batch_size, num_steps):
"""使⽤随机抽样⽣成⼀个⼩批量⼦序列"""
# 从随机偏移量开始对序列进⾏分区,随机范围包括num_steps-1
corpus = corpus[random.randint(0, num_steps - 1):]
# 减去1,是因为我们需要考虑标签
num_subseqs = (len(corpus) - 1) // num_steps
# ⻓度为num_steps的⼦序列的起始索引
initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
# 在随机抽样的迭代过程中,
# 来自两个相邻的、随机的、⼩批量中的⼦序列不⼀定在原始序列上相邻
random.shuffle(initial_indices)
def data(pos):
# 返回从pos位置开始的⻓度为num_steps的序列
return corpus[pos: pos + num_steps]
num_batches = num_subseqs // batch_size
for i in range(0, batch_size * num_batches, batch_size):
# 在这⾥,initial_indices包含⼦序列的随机起始索引
initial_indices_per_batch = initial_indices[i: i + batch_size]
X = [data(j) for j in initial_indices_per_batch]
Y = [data(j + 1) for j in initial_indices_per_batch]
yield torch.tensor(X), torch.tensor(Y)
顺序分区
'''
顺序分区:
在迭代过程中,除了对原始序列可以随机抽样外,我们还可以保证
两个相邻的⼩批量中的⼦序列在原始序列上也是相邻的。
例如:
[ 0., 1., 2., 3., 4.] 和 [ 5., 6., 7., 8., 9.]
'''
def seq_data_iter_sequential(corpus, batch_size, num_steps):
"""使用顺序分区生成⼀个小批量子序列"""
# 从随机偏移量开始划分序列
offset = random.randint(0, num_steps)
num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
Xs = torch.tensor(corpus[offset: offset + num_tokens])
Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])
Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
num_batches = Xs.shape[1] // num_steps
for i in range(0, num_steps * num_batches, num_steps):
X = Xs[:, i: i + num_steps]
Y = Ys[:, i: i + num_steps]
yield X, Y
将上⾯的两个采样函数包装到⼀个类中,以便稍后可以将其用作数据迭代器
class SeqDataLoader:
"""加载序列数据的迭代器"""
def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
if use_random_iter:
self.data_iter_fn = seq_data_iter_random
else:
self.data_iter_fn = seq_data_iter_sequential
self.corpus, self.vocab = load_corpus_jaychou(max_tokens)
self.batch_size, self.num_steps = batch_size, num_steps
def __iter__(self):
return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)
将预处理全部进行封装
def load_data_jaychou(batch_size, num_steps, use_random_iter=False, max_tokens=10000):
"""返回歌词数据集的迭代器和词表"""
data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens)
return data_iter, data_iter.vocab
2.2 RNN的简单实现
2.2.1 加载歌词数据集
import torch
from torch import nn
import math
from _0_Vocab import load_data_jaychou
batch_size, num_steps = 32, 35
train_iter, vocab = load_data_jaychou(batch_size, num_steps)
2.2.2 封装通用的RNN模型
import torch.nn as nn
import torch
from torch.nn import functional as F
class RNNModel(nn.Module):
"""循环神经网络模型"""
def __init__(self, rnn_layer, 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是双向的,num_directions应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
def forward(self, inputs, state):
# inputs的shape为(batch_size,num_steps)
# one-hot编码后,shape变为 (num_steps, batch_size, 词表大小)
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
# Y的shape(num_steps, batch_size, 隐藏单元数)
# state的shape(num_layers, batch_size, 隐藏单元数)
Y, state = self.rnn(X, state)
# 全连接层
# 首先,将Y的形状改为(时间步数*批量⼤⼩, 隐藏单元数)
# 它的输出形状是(时间步数*批量⼤⼩,词表大小)。
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, device, batch_size=1):
if not isinstance(self.rnn, nn.LSTM):
# 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)
)
if __name__ == '__main__':
# 1、构造⼀个具有256个隐藏单元的单隐藏层的循环神经⽹络层rnn_layer
num_hiddens = 256
rnn_layer = nn.RNN(input_size=28, hidden_size=num_hiddens,num_layers=1,bidirectional=True)
# 2、构建rnn模型,词表大小为28
net = RNNModel(rnn_layer, vocab_size=28)
print(net)
# 3、构建测试数据
num_steps = 10
batch_size = 5
x = torch.rand(size=(batch_size, num_steps))
state = net.begin_state(batch_size=x.shape[0], device='cpu')
print(net(x, state)[0].shape)
print(net(x, state)[1].shape)
RNNModel(
(rnn): RNN(28, 256, bidirectional=True)
(linear): Linear(in_features=512, out_features=28, bias=True)
)
torch.Size([50, 28])
torch.Size([2, 5, 256])
2.2.3 定义rnn模型
num_hiddens = 256
# 构造⼀个具有256个隐藏单元的单隐藏层的循环神经网络层rnn_layer
rnn_layer = nn.RNN(len(vocab), num_hiddens)
# 使用张量来初始化隐状态,
# 它的形状是(隐藏层数,批量大小,隐藏单元数)
state = torch.zeros((1, batch_size, num_hiddens))
print(state.shape)
'''
通过⼀个隐状态和⼀个输⼊,我们就可以⽤更新后的隐状态计算输出。
需要强调的是,rnn_layer的“输出”(Y)不涉及输出层的计算:
它是指每个时间步的隐状态,这些隐状态可以⽤作后续输出层的输⼊。
'''
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape
torch.Size([1, 32, 256])
(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))
from _0_RNNModel import RNNModel
def try_gpu(i=0):
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')
device = try_gpu()
net = RNNModel(rnn_layer, vocab_size=vocab.vocab_size)
net = net.to(device)
在训练前,先预测一波
'''
首先,net(get_input(), state)目的就是用【前一次预测得到的输出】和【前一次时刻的隐藏状态】 得到 【当前时刻的输出和当前时刻的隐藏状态】。
然后第一次for循环,意在得到一个比较准确的,可供进行prefix这句话以后的字的预测的一个初始时刻隐藏状态,所以使用了完整的一句话去得到。
比如说 `你好不好` 是一句话,传入作为 `prefix` ,然后用 `你` 初始化 `outputs` 作为第0时刻的输出,
首先预热整句话,从第1时刻开始,循环 `好不好` 来获得初始隐藏状态 `state` ,这期间连续的更新state直至最后一个 `好` 字;
第1时刻默认用0时刻输出 `你` 和0时刻默认的 `state` 输入模型得到预测结果和1时刻的隐藏状态 `state` ,
不用接收模型使用 `你` 预测得到的结果,因为标准答案 `好` 可以直接作为下一时刻的输入。
剩下的循环以此类推。
所以第二个for循环,才是正式做预测,预测 `你好不好` 这句话的后面 `num_preds` 个词是什么,
所以初始时刻就传入 `你好不好` 的最后一个词 `好` 和之前预热的 `state` 隐藏状态,得到第4时刻的预测结果 `y` 和当前时刻隐藏状态。
'''
def predict_ch(prefix, num_preds, net, vocab, device):
"""在prefix后⽣成新字符"""
state = net.begin_state(batch_size=1, device=device)
outputs = [ vocab[str(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): # 预测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_ch('分开', 10, net, vocab, device)
'分开万枯幅枯幅掉氧枯氧枯'
2.2.4 模型的训练
梯度裁剪
对于长度为T的序列,我们在迭代中计算这T个时间步上的梯度,将会在反向传播过程中产生长度为O(T)的
矩阵乘法链。当T较大时,它可能导致数值不稳定,例如可能导致梯度爆炸或梯度消失。因此,
循环神经网络模型往往需要额外的方式来支持稳定训练。梯度裁剪提供了⼀个快速修复梯度爆炸的方法。
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
# ||g||
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
评价指标——困惑度
我们可以通过⼀个序列中所有的n个词元的交叉熵损失的平均值来衡量。
由于历史原因,自然语言处理的科学家更喜欢使用⼀个叫做困惑度(perplexity)的量。其实,就是交叉熵损失的平均值的指数。
• 在最好的情况下,模型总是完美地估计标签词元的概率为1。在这种情况下,模型的困惑度为1。
• 在最坏的情况下,模型总是预测标签词元的概率为0。在这种情况下,困惑度是正无穷大。
from AccumulatorClass import Accumulator
from AnimatorClass import Animator
from TimerClass import Timer
'''
循环神经⽹络模型的训练函数既⽀持从零开始实现,也可以使⽤⾼级API来实现。
1. 序列数据的不同采样⽅法(随机采样和顺序分区)将导致隐状态初始化的差异。
2. 在更新模型参数之前裁剪梯度。这样的操作的⽬的是:即使训练过程中某个点上发⽣了梯度爆炸,也能保证模型不会发散。
3. ⽤困惑度来评价模型。
'''
def train_epoch(net, train_iter, loss, updater, device, use_random_iter):
"""训练网络⼀个迭代周期"""
state, timer = None, Timer()
metric = 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对于nn.GRU是个张量
state.detach_()
else:
# state对于nn.LSTM或对于我们从零开始实现的模型是个张量
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:
# 在训练函数中,如果没有用torch提供的优化器,就先不进行梯度清零
# 自定义的updater。在更新参数后,也清零了。
l.backward()
grad_clipping(net, 1) # 裁剪梯度后,再进行梯度更新
# 因为已经调⽤了mean函数
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
def train(net, train_iter, vocab, lr, num_epochs, device,use_random_iter=False):
"""训练模型"""
loss = nn.CrossEntropyLoss()
animator = 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: sgd(net.params, lr, batch_size)
predict = lambda prefix: predict_ch(prefix, 50, net, vocab, device)
# 训练和预测
for epoch in range(num_epochs):
ppl, speed = train_epoch(net, train_iter, loss, updater, device, use_random_iter)
if (epoch + 1) % 10 == 0:
print(predict('分开'))
animator.add(epoch + 1, [ppl])
print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
print(predict('分开'))
print(predict('不分开'))
num_epochs, lr = 1000, 1
train(net, train_iter, vocab, lr, num_epochs, device)
2.3 RNN的手动实现
2.3.1 加载歌词数据集
import torch
from torch import nn
import math
from _0_Vocab import load_data_jaychou
from torch.nn import functional as F
batch_size, num_steps = 32, 35
train_iter, vocab = load_data_jaychou(batch_size, num_steps)
2.3.2 初始化模型参数
'''
隐藏单元数num_hiddens是⼀个可调的超参数。
注意:
当训练语⾔模型时,输⼊和输出来⾃相同的词表。因此,它们具有相同的维度,即词表的⼤⼩。
'''
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) # 随机初始化为0
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device) # 随机初始化为0
# 附加梯度
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
2.3.3 创建rnn模型
'''
init_rnn_state函数在初始化时返回隐状态。
这个函数的返回是⼀个张量,张量全⽤0填充,形状为(批量⼤⼩,隐藏单元数)
'''
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
'''
定义了如何在【⼀个时间步内】计算隐状态和输出。
循环神经⽹络模型通过inputs最外层的维度实现循环,以便逐时间步更新⼩批量数据的隐状态H。
此外,这⾥使⽤tanh函数作为激活函数。
'''
def rnn(inputs, state, params):
"""
:param inputs: inputs的形状:(时间步数量,批量⼤⼩,词表⼤⼩)
:param state: 隐状态
:param params: 初始化的权重及偏置参数
:return:
"""
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# X的形状:(批量⼤⼩,词表⼤⼩)
for X in inputs:
# 隐藏层激活函数是tanh。
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h) # 隐变量state
# 输出层没有激活函数,就是隐变量的线性变换
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
封装通用函数
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):
'''
如果第0维是批量大小,那么假如每次取一个样本 即(x1 x2 x3...)
这样根据rnn计算规则,先算预测的x2 然后后才能算x3, 这样只能一个个算,不便于程序的并行执行。
如果第0纬换成了时间序列,那么每拿一个出来,得到的就是(第一个x1 第二个x1...)这些是可以并行计算的,
转置一下的好处就是把能够并行计算的东西 使得他们在空间存储上尽量相邻 提高计算效率。
'''
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)
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, try_gpu(), get_params,init_rnn_state, rnn)
2.3.4 预测
预测函数和2.2一致
# 鉴于我们还没有训练⽹络,它会⽣成荒谬的预测结果。
predict_ch('分开', 10, net, vocab, try_gpu())
2.3.5 模型的训练
训练函数和2.2一致
# 因为我们在数据集中只使⽤了10000个词元,所以模型需要更多的迭代周期来更好地收敛。
num_epochs, lr = 1000, 1
train(net, train_iter, vocab, lr, num_epochs, try_gpu())