# 一个简单的多层感知机 defget_net(): net = nn.Sequential(nn.Linear(4, 10), nn.ReLU(), nn.Linear(10, 1)) net.apply(init_weights) return net
# 平方损失 loss = nn.MSELoss(reduction='none')
deftrain(net, train_iter, loss, epochs, lr): trainer = torch.optim.Adam(net.parameters(), lr) for epoch inrange(epochs): for X, y in train_iter: trainer.zero_grad() l = loss(net(X), y) l.sum().backward() trainer.step() print(f'epoch {epoch + 1}, ' f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
net = get_net() train(net, train_iter, loss, 5, 0.01)
我们根据前 600 对训练数据,考察模型的序列预测能力。对于直到 xt 的观测序列,其在时间 t+k 处的预测输出 x^t+k 称为 k 步预测。对于单步预测(k=1)而言,我们的预测都是基于已有的真实数据 X 做出的下一步预测,而对于 k>1 的情况,我们需要借助自己模型的预测值去预测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
max_steps = 64
features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
for i inrange(tau): features[:, i] = x[i: i + T - tau - max_steps + 1]
for i inrange(tau, tau + max_steps): features[:, i] = net(features[:, i - tau:i]).reshape(-1)
steps = (1, 4, 16, 64) d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps], [features[:, (tau + i - 1)].detach().numpy() for i in steps], 'time', 'x', legend=[f'{i}-step preds'for i in steps], xlim=[5, 1000], figsize=(6, 3))
# 正则表达式读取 defread_time_machine(): withopen(d2l.download('time_machine'), 'r') as f: lines = f.readlines() return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
例如,英文文本序列 I am a boy. 的 Token 列表可以表示为 ['I', 'am', 'a', 'boy', '.'] ;中文文本序列 姬你太美! 的 Token 列表可以表示为 ['姬', '你', '太', '美', '!']。这里只是举个例子,真实的 Token 的划分遵循一套特殊的算法,后续文章会介绍。
1 2 3 4 5 6 7 8 9 10 11
# token 化处理 deftokenize(lines, token='word'): if token == 'word': return [line.split() for line in lines] elif token == 'char': return [list(line) for line in lines] else: print('错误:未知词元类型:' + token)
def__getitem__(self, tokens): ifnotisinstance(tokens, (list, tuple)): returnself.token_to_idx.get(tokens, self.unk) return [self.__getitem__(token) for token in tokens]
defto_tokens(self, indices): ifnotisinstance(indices, (list, tuple)): returnself.idx_to_token[indices] return [self.idx_to_token[index] for index in indices]
# 统计词元频率 defcount_corpus(tokens): # 这里的tokens是1D列表或2D列表 iflen(tokens) == 0orisinstance(tokens[0], list): # 将词元列表展平成一个列表 tokens = [token for line in tokens for token in line] return collections.Counter(tokens)
我们尝试获取 The Time Machine 的语料库和词库大小。
1 2 3 4 5 6 7 8 9 10 11 12 13
defload_corpus_time_machine(max_tokens=-1): #@save lines = read_time_machine() # 这里采用字符 Token 化 tokens = tokenize(lines, 'char') 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
Bengio 等人首先提出了使用神经网络进行语言建模的概念。在一个循环神经网络模型中,设小批量大小为1,批量中的文本序列为一个单词。为了简化后续部分的训练,我们考虑使用字符级语言模型(Character-level Language Model),将文本词元化为字符而不是单词。下图演示了如何通过基于字符级语言建模的循环神经网络, 使用当前的和先前的字符预测下一个字符。
实践中,我们使用批量大小为 m>1,每个词元由一个 n 维向量表示,因此输入 X∈Rm×n。
困惑度
最后,让我们讨论如何度量语言模型的质量,这将在后续部分中用于评估基于循环神经网络的模型。信息论知识表明,一个长度为 T 序列的交叉熵可以表示为
H=−T1t=1∑TlogP(xt∣x1⋯xt−1)
定义其困惑度(Perplexity)为
PP=2H
困惑度的最好的理解是“下一个词元的实际选择数的调和平均数”,其等价形式为
PP=P(x1,x2,⋯,xT)−T1
如果困惑度较低,说明语言模型对序列的预测较为准确
如果困惑度较高,说明模型在预测下一个词时存在较大的不确定性,性能较差
RNN代码实现
模型定义
在本节,我们将从零开始搭建一个循环神经网络。以 The Time Machine 文本序列为训练集,引入必要的库。
%matplotlib inline import math import torch import random import re from torch import nn from torch.nn import functional as F from d2l import torch as d2l from torch.utils import data
defread_time_machine(filepath='timemachine.txt'): withopen(filepath, 'r', encoding='utf-8') as f: lines = f.read().split('\n') return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
deftokenize(lines, token='char'): if token == 'char': return [list(line) for line in lines] elif token == 'word': return [line.split() for line in lines] else: raise ValueError(f'Unknown token type: {token}')
classVocab: def__init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens isNone: tokens = [] if reserved_tokens isNone: reserved_tokens = [] if tokens andisinstance(tokens[0], list): tokens = [t for line in tokens for t in line] counter = {} for t in tokens: counter[t] = counter.get(t, 0) + 1 self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) self.idx_to_token = ['<unk>'] + reserved_tokens self.token_to_idx = {t: i for i, t inenumerate(self.idx_to_token)} for token, freq inself._token_freqs: if freq < min_freq: break if token notinself.token_to_idx: self.idx_to_token.append(token) self.token_to_idx[token] = len(self.idx_to_token) - 1
def__len__(self): returnlen(self.idx_to_token)
def__getitem__(self, tokens): ifnotisinstance(tokens, (list, tuple)): returnself.token_to_idx.get(tokens, 0) return [self.__getitem__(t) for t in tokens]
defto_tokens(self, indices): ifnotisinstance(indices, (list, tuple)): returnself.idx_to_token[indices] return [self.idx_to_token[i] for i in indices]
defseq_data_iter_random(corpus, batch_size, num_steps): corpus = corpus[random.randint(0, num_steps - 1):] num_seqs = (len(corpus) - 1) // num_steps initial_indices = list(range(0, num_seqs * num_steps, num_steps)) random.shuffle(initial_indices) for i inrange(0, len(initial_indices) - batch_size + 1, batch_size): batch_idx = initial_indices[i: i + batch_size] X = torch.tensor([corpus[j: j + num_steps] for j in batch_idx]) Y = torch.tensor([corpus[j + 1: j + 1 + num_steps] for j in batch_idx]) yield X, Y
defseq_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 inrange(0, num_steps * num_batches, num_steps): X = Xs[:, i: i + num_steps] Y = Ys[:, i: i + num_steps] yield X, Y
def_load_corpus(self, max_tokens): lines = read_time_machine() tokens = tokenize(lines, token='char') vocab = Vocab(tokens) corpus = [vocab[t] for line in tokens for t in line] if max_tokens > 0: corpus = corpus[:max_tokens] return corpus, vocab
defpredict(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 _ inrange(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])
当我们的目标函数 f 具有良好的性质时,比如其对常数 L>0 是 Lipschitz 连续的,那么有
∣f(x)−f(y)∣⩽L∥x−y∥
此时应用梯度下降,对于梯度更新量 ηg 有
∣f(x)−f(x−ηg)∣⩽Lη∥g∥
这表明我们不会观察到超过 Lη∥g∥ 的变化,这表示了两件事情
当朝着好的方向更新时,它限制了取得进展的速度
当朝着坏的方向更新时,它限制了事情变糟的程度
现在考虑梯度爆炸的问题,当我们的目标函数 f 满足上述性质时(一般都是满足的),我们可以减小学习率 η 来防止梯度过大。然而,当学习率过小时,我们的算法是很难收敛的,此时需要采取一个更聪明的策略——梯度裁剪。给定一个超参数 θ,定义裁剪后的梯度 g^ 为
g^←min{1,∥g∥θ}g
这就保证了裁剪后的梯度 g^ 的范数一定不超过 θ,并且和 g 保持同向。梯度裁剪还有一个值得拥有的副作用, 即限制了任何给定的小批量数据(以及其中任何给定的样本)对参数向量的影响,这赋予了模型一定程度的稳定性。梯度裁剪的代码如下
1 2 3 4 5 6 7 8 9
defgrad_clipping(net, theta): ifisinstance(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
由于循环神经网络和MLP的差别,我们的单epoch训练方式有三个不同之处
序列数据的不同采样方法将导致隐状态初始化的差异
使用随机抽样时,因为每个样本都是在随机位置抽样的, 因此需要为每个迭代周期重新初始化隐状态
使用顺序分区时,在每个迭代周期的开始位置初始化隐状态。由于下一个小批量数据中的第 i 个子序列样本与当前第 i 个子序列样本相邻,因此当前小批量数据最后一个样本的隐状态,将用于初始化下一个小批量数据第一个样本的隐状态
困惑度 1.0, 67212.6 词元/秒 cuda:0 time traveller for so it will be convenient to speak of himwas e travelleryou can show black is white by argument said filby