本文内容目录

  • 序列模型
  • 文本预处理
  • 语言模型和数据集
  • 循环神经网络
  • RNN的从零开始实现
  • RNN的简洁实现
  • 通过时间反向传播
  • 门控循环单元GRU
  • 长短期记忆网络(LSTM)
  • 深度循环神经网络
  • 双向循环神经网络
  • 机器翻译与数据集
  • 编码器-解码器架构
  • 序列到序列学习(Seq2Seq)
  • 束搜索

李沐老师在本章节中主要讲解了关于循环神经网络和编码器-解码器的内容,主要关注于实用方向,对于其理论并没有深挖,如果感兴趣可以看我这两篇博客,是李宏毅老师的学习笔记,对理论部分进行了较详细的说明:
【机器学习】李宏毅——Recurrent Neural Network(循环神经网络)
【机器学习】李宏毅——AE自编码器(Auto-encoder)

序列模型

在现实生活中很多数据都是有时序结构的,那么对于时序结构的研究也是必要的。

一般对于时序结构而言,在第t个时间点的观察值xtx_txt是与前面t-1个时刻的观察值有关的,但反过来在现实中不一定可行,即:

那么我们对条件概率进行建模,即:

那么如果能够学习到模型f,及概率计算方法p,就可以进行预测了。

那么针对这个问题,有两种比较常见的研究方法:


马尔科夫假设

因为前面我们的叙述是当前时刻的观察值跟前面所有时刻的观察值相关,那么马尔科夫假设就是假定当前数据只和过去τ\tauτ个数据点相关,这样函数f的输入就从不定长转换为了定长,因此就方便很多:

那么就可以用一个简单的MLP来实现。


潜变量模型

即引入潜变量hth_tht来表示过去信息ht=f(x1,...,xt−1)h_t=f(x_1,...,x_{t-1})ht=f(x1,...,xt1),那么xt=p(xt∣ht)x_t=p(x_t \mid h_t)xt=p(xtht)


具体老师通过了一个小例子来为我们展示了训练以及预测:

import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2lT = 1000  # 总共产生1000个点
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))  # 加上噪音
# d2l.plot(time, [x], 'time', 'x', xlim=[1, 1000], figsize=(6, 3))tau = 4
features = torch.zeros((T - tau, tau))  # 因为前tau个之间没有tau个可以作为输入
for i in range(tau):features[:, i] = x[i: T - tau + i]  # 例如第0列就是每个数据的前面第4个
labels = x[tau:].reshape((-1, 1))  # 从第4个往后都是前面tau个造成的输出了batch_size, n_train = 16, 600
# 用前600个样本来训练,然后后面400个完成预测任务
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),batch_size, is_train=True)# 初始化网络权重的函数
def init_weights(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)# 一个简单的多层感知机
def get_net():net = nn.Sequential(nn.Linear(4, 10),nn.ReLU(),nn.Linear(10, 1))net.apply(init_weights)return netloss = nn.MSELoss(reduction='none')def train(net, train_iter, loss, epochs, lr):trainer = torch.optim.Adam(net.parameters(), lr)for epoch in range(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)onestep_preds = net(features)
# 单步预测,就是每次都给定4个真实值来让你预测下一个
# 注意这里采用detach是将本来含有梯度的变量,复制一个不含有梯度,不过都是指向同一个数值
# 不含有梯度是因为画图不需要计算梯度,防止在画图中发生计算过程而改变梯度
d2l.plot([time, time[tau:]],[x.detach().numpy(), onestep_preds.detach().numpy()], 'time','x', legend=['data', '1-step preds'], xlim=[1, 1000],figsize=(6, 3))
plt.show()
# 多步预测,只知道600个,然后可以结合真实数据预测到604个,那么后面都是靠预测值来预测
multistep_preds = torch.zeros(T)
multistep_preds[: n_train + tau] = x[: n_train + tau]
for i in range(n_train + tau, T):multistep_preds[i] = net(multistep_preds[i - tau:i].reshape((1, -1)))d2l.plot([time, time[tau:], time[n_train + tau:]],[x.detach().numpy(), onestep_preds.detach().numpy(),multistep_preds[n_train + tau:].detach().numpy()], 'time','x', legend=['data', '1-step preds', 'multistep preds'],xlim=[1, 1000], figsize=(6, 3))
plt.show()
max_steps = 64
features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# 列i(i<tau)是来自x的观测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(tau):features[:, i] = x[i: i + T - tau - max_steps + 1]# 列i(i>=tau)是来自(i-tau+1)步的预测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(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))plt.show()

可以看到单步预测的结果还是很精确的。

但是在多步预测时,我们如果只给前面600个数据点,然后让其预测后面400个,就结果差的很离谱

可以看到,多步预测为1,4,16的结果都还算可以,但是增加到64时就出现了明显的差异。


小结

  • 时序模型中,当前数据跟之前观察到的数据相关
  • 自回归模型使用自身过去数据来预测未来
  • 马尔科夫模型假设当前只跟最近少数数据相关,从而简化模型
  • 潜变量模型使用潜变量来概括历史信息

文本预处理

该章节主要是介绍了对于一个简单文本文件的处理,生成可以用来使用的数据集

import collections
import re
from d2l import torch as d2l# @save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt','090b5e7e70c295757f55df93cb0a180b9691891a')# 下载数据集def read_time_machine():with open(d2l.download('time_machine'), 'r') as f:lines = f.readlines()return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]# 就是将除了A-Z和a-z,还有空格,其他的符号都去掉,再去掉回车,再转成小写lines = read_time_machine()
print(f'# 文本总行数:{len(lines)}')
print(lines[0])
print(lines[10])def tokenize(lines, token='word'):  # @saveif token == 'word':return [line.split() for line in lines]elif token == 'char':return [list(line) for line in lines]else:print('错误:未知词元类型:' + token)tokens = tokenize(lines)
for i in range(11):print(tokens[i])def count_corpus(tokens):  # @save"""统计词元的频率"""# 这里的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_tokensself.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:breakif token not in self.token_to_idx:self.idx_to_token.append(token)self.token_to_idx[token] = len(self.idx_to_token) - 1# 不断添加进去词汇并建立和位置之间的索引关系def __len__(self):return len(self.idx_to_token)@propertydef unk(self):  # 未知词元的索引为0,装饰器,可以直接self.unk,不用加括号return 0def __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]@propertydef token_freqs(self):return self._token_freqsvocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])def load_corpus_time_machine(max_tokens=-1):  # @save"""返回时光机器数据集的词元索引列表和词表"""lines = read_time_machine()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, vocabcorpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)

语言模型和数据集

语言模型是指,给定文本序列x1,...,xTx_1,...,x_Tx1,...,xT,其目标是估计联合概率p(x1,...,xT)p(x_1,...,x_T)p(x1,...,xT),也就是该文本序列出现的概率。

那么假设序列长度为2,那我们可以使用计数的方法,简单计算为:
p(x,x′)=p(x)p(x′∣x)=n(x)nalln(x,x′)n(x)p(x,x^{\prime})=p(x)p(x^{\prime}\mid x)=\frac{n(x)}{n_{all}}\frac{n(x,x^{\prime})}{n(x)} p(x,x)=p(x)p(xx)=nalln(x)n(x)n(x,x)
那么继续拓展序列长度也可以采用类似的计数方法。

但是如果序列长度太长,如果文本量不够大的情况下可能会出现n(x1,...,xT)≤1n(x_1,...,x_T)\leq 1n(x1,...,xT)1的情况,那么就可以用马尔科夫假设来缓解这个问题:

  • 一元语法:p(x1,x2,x3,x4)=p(x1)p(x2)p(x3)p(x4)p(x_1,x_2,x_3,x_4)=p(x_1)p(x_2)p(x_3)p(x_4)p(x1,x2,x3,x4)=p(x1)p(x2)p(x3)p(x4)
  • 二元语法:p(x1,x2,x3,x4)=p(x1)p(x2∣x1)p(x3∣x2)p(x4∣x3)p(x_1,x_2,x_3,x_4)=p(x_1)p(x_2\mid x_1)p(x_3\mid x_2)p(x_4\mid x_3)p(x1,x2,x3,x4)=p(x1)p(x2x1)p(x3x2)p(x4x3)
  • 三元语法:p(x1,x2,x3,x4)=p(x1)p(x2∣x1)p(x3∣x1,x2)p(x4∣x2,x3)p(x_1,x_2,x_3,x_4)=p(x_1)p(x_2\mid x_1)p(x_3\mid x_1,x_2)p(x_4\mid x_2,x_3)p(x1,x2,x3,x4)=p(x1)p(x2x1)p(x3x1,x2)p(x4x2,x3)

代码为:

import random
import torch
from d2l import torch as d2l
from matplotlib import pyplot as plttokens = d2l.tokenize(d2l.read_time_machine())
# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)  # 计算频率得到的词汇列表freqs = [freq for token, freq in vocab.token_freqs]  # 将频率变化画出来
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log')
plt.show()  # 以上这是单个单词的情况# 我们来看看连续的两个单词和三个单词的情况,即二元语法和三元语法
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
trigram_tokens = [triple for triple in zip(corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs]
d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x',ylabel='frequency: n(x)', xscale='log', yscale='log',legend=['unigram', 'bigram', 'trigram'])
plt.show()  # 画出来对比# 下面我们对一个很长的文本序列,随机在上面采样得到我们指定长度的子序列,方便我们输入到模型中
def seq_data_iter_random(corpus, batch_size, num_steps):  # @save"""使用随机抽样生成一个小批量子序列"""# 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1corpus = corpus[random.randint(0, num_steps - 1):]# 因为长度为num_steps是肯定的,那我们如果每次都从0开始,那么例如2-7这种就得不到# 因此每次都随机的初始点开始就可以保证我们能够采样得到不同的数据# 减去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_sizefor 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]# 这里解释一下,一开始我认为应该输入序列x之后我们要输出x之后的下一个单词,因此认为y应该为长度为1# 但是实际上在训练时我们并不是5个丢进去,然后生成1个出来# 我们是丢进去第一个,然后生成第二个,然后结合1,2的真实标签,去预测第三个,以此类推# 直到后面结合5个去预测第6个yield torch.tensor(X), torch.tensor(Y)# 这个函数是让相邻两个小批量中的子序列在原始序列上是相邻的
def seq_data_iter_sequential(corpus, batch_size, num_steps):  # @save"""使用顺序分区生成一个小批量子序列"""# 从随机偏移量开始划分序列offset = random.randint(0, num_steps)num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_sizeXs = 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_stepsfor i in range(0, num_steps * num_batches, num_steps):X = Xs[:, i: i + num_steps]Y = Ys[:, i: i + num_steps]yield X, Yclass SeqDataLoader:  # @save"""加载序列数据的迭代器"""def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):if use_random_iter:self.data_iter_fn = d2l.seq_data_iter_randomelse:self.data_iter_fn = d2l.seq_data_iter_sequentialself.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)self.batch_size, self.num_steps = batch_size, num_stepsdef __iter__(self):return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)# 封装,同时返回数据迭代器和词汇表
def load_data_time_machine(batch_size, num_steps,  #@saveuse_random_iter=False, max_tokens=10000):"""返回时光机器数据集的迭代器和词表"""data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens)return data_iter, data_iter.vocab


循环神经网络

其模型可以用下图来表示:

即中间的隐变量,是用来捕获并保留数序列直到其当前时间步的历史信息,其内部原理为:

  • 更新隐藏状态:ht=ϕ(Whhht−1+Whxxt−1+bh)\pmb{h}_t=\phi(\pmb{W}_{hh}\pmb{h}_{t-1}+\pmb{W}_{hx}\pmb{x}_{t-1}+\pmb{b}_h)hht=ϕ(WWhhhht1+WWhxxxt1+bbh)
  • 输出:ot=(Whoht+bo)\pmb{o}_t=(\pmb{W}_{ho}\pmb{h}_t+\pmb{b}_o)oot=(WWhohht+bbo)

例如在t1t_1t1时刻输入x1=x_1=x1=“你”,那么我们希望它能够能够计算得到h1h_1h1并得到输出o1o_1o1=“好”,然后接下来输入为x2x_2x2=“好”,我们希望o2o_2o2="世"等等。


而衡量一个句子的质量,使用的是困惑度,其内部使用平均交叉熵来实现:
π=1n∑i=1n−log⁡p(xt∣xt−1,...,x1)\pi=\frac{1}{n}\sum_{i=1}^n-\log p(x_t\mid x_{t-1},...,x_1) π=n1i=1nlogp(xtxt1,...,x1)
注意这里指的是根据现有已知的x1,...,xt−1x_{1},...,x_{t-1}x1,...,xt1的情况(都是真实标签),我们能够预测出正确结果xtx_txt的概率,那么如果每次都能够正确预测,就是p=1,那么log=0。而常见的是用exp⁡(π)\exp(\pi)exp(π)来表达,因此1代表完美,无穷大为最差情况


下一个知识点是梯度裁剪

为了防止在迭代过程中计算T个时间步上的梯度时由于不断叠加而产生的数值不稳定的情况,而引入梯度裁剪:将所有层的梯度拼成一个向量g,那么如果该向量的L2范数超过了设定值θ\thetaθ就将其进行修正,修正为θ\thetaθ值,即:
g←min⁡(1,θ∥g∥)g\pmb{g}\leftarrow \min (1,\frac{\theta}{\Vert \pmb{g} \Vert})\pmb{g} ggmin(1,ggθ)gg


RNN有非常多的应用场景:


小结

  • RNN的输出取决于当前输入和前一时刻的隐变量
  • 应用到语言模型中时,RNN根据当前词预测下一次时刻词
  • 通常使用困惑度来衡量语言模型的好坏

RNN的从零开始实现

完成代码如下,需要注意的地方和讲解的地方都在注释中了。

import math
import torch
from matplotlib import pyplot as plt
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 接下来引入独热编码的使用
# print(F.one_hot(torch.tensor([0, 2]), len(vocab)))
# 第一个参数0和2,代表我有两个编码,第一个编码在0的位置取1,第二个在2的位置取1,而长度就是第二个参数# 而我们每次采样得到都是批量大小*时间步数,将每个取值(标量)转换为独热编码就是三维
# 批量大小*时间步数*独热编码,那为了方便,我们将维度转换为时间步数*批量大小再去变成独热编码
# 这样每个时刻的数值就连在一起了方便使用,如下:
X = torch.arange(10).reshape((2, 5))  # 批量为2,时间步长为5
# print(F.one_hot(X.T, 28).shape)  # 输出为5,2,28# 初始化模型参数
def get_params(vocab_size, num_hiddens, device):num_inputs = num_outputs = vocab_size# 因为输入是一个字符,就是1个独特编码,输出是预测的下一个字符也是独热编码,因此长度都是独特编码的长度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# 下面是对RNN模型的定义
# 定义初始隐藏层的状态
def init_rnn_state(batch_size, num_hiddens, device):# 这里用元组的原因是为了和后面LSTM统一return (torch.zeros((batch_size, num_hiddens), device=device), )def rnn(inputs, state, params):# inputs的形状:(时间步数量,批量大小,词表大小)W_xh, W_hh, b_h, W_hq, b_q = paramsH, = state  # 注意state是元组,第二个参数我们暂时不要,所有不用接受,但是要有逗号,否则H是元组outputs = []# X的形状:(批量大小,词表大小),这就是我们前面转置的原因,方便对同一时间步的输入做预测for X in inputs:H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)  # 更新HY = torch.mm(H, W_hq) + b_q  # 对Y做出预测outputs.append(Y)  # 这里outputs是数组,长度为时间步数量,每个元素都是批量大小*词表大小# 那个下面对output进行堆叠,就是将时间步维度去掉,行数为(时间步*批量大小),列为词表大小return torch.cat(outputs, dim=0), (H,)# 用类来封装这些函数
class RNNModelScratch: #@save"""从零开始实现的循环神经网络模型"""def __init__(self, vocab_size, num_hiddens, device,get_params, init_state, forward_fn):self.vocab_size, self.num_hiddens = vocab_size, num_hiddensself.params = get_params(vocab_size, num_hiddens, device)# 下面这两个其实是函数,第一个就是刚才初始化隐状态的函数,第二个就是rnn函数进行前向计算self.init_state, self.forward_fn = init_state, forward_fndef __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)# 检查输出是否具有正确的形状
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.to(d2l.try_gpu()), state)
print(Y.shape, "\n",len(new_state),"\n", new_state[0].shape)# 定义预测函数
def predict_ch8(prefix, num_preds, net, vocab, device):  #@save"""在prefix后面生成新字符"""# prefix是用户提供的一个包含多个字符的字符串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))# 上是一个匿名函数,可以在每次outputs更新后都调用outputs的最后一个元素for y in prefix[1:]:  # 预热期,此时不做预测,我们用这些字符不断来更新state_, state = net(get_input(), state)outputs.append(vocab[y])  # 将下一个待作为输入的转换为数字进入for _ in range(num_preds):  # 预测num_preds步y, state = net(get_input(), state)  # 预测并更新stateoutputs.append(int(y.argmax(dim=1).reshape(1)))  # 这就是将预测的放入,并作为下一次的输入return ''.join([vocab.idx_to_token[i] for i in outputs])  # 拼接成字符print(predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu()))  # 看看效果# 梯度裁剪
def grad_clipping(net, theta):  #@save"""裁剪梯度"""if isinstance(net, nn.Module):params = [p for p in net.parameters() if p.requires_grad]# 取出那些需要更新的梯度else:params = net.paramsnorm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))if norm > theta:for param in params:param.grad[:] *= theta / norm  # 对梯度进行修剪# 训练模型
#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):"""训练网络一个迭代周期(定义见第8章)"""state, timer = None, d2l.Timer()metric = d2l.Accumulator(2)  # 训练损失之和,词元数量for X, Y in train_iter:if state is None or use_random_iter:# 如果用的是打乱的,那么后一个小批量和前一个小批量的样本之间并不是连接在一起的# 那么它们的隐变量不存在关系,所以必须初始化# 在第一次迭代或使用随机抽样时初始化statestate = net.begin_state(batch_size=X.shape[0], device=device)else:  # 否则的话,我们就可以沿用上次计算完的隐变量,只不过detach是断掉链式求导,我们现在隐变量是数值了,跟之前的没有关系了if isinstance(net, nn.Module) and not isinstance(state, tuple):# state对于nn.GRU是个张量,这部分可以认为我们将state变换为常数# 那么梯度更新时就不会再和前面批次的梯度进行相乘,这里就直接断掉梯度的链式法则了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: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()#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,use_random_iter=False):"""训练模型(定义见第8章)"""loss = nn.CrossEntropyLoss()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)  # 预测函数# 训练和预测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'))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())
plt.show()
# 随机采用
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,init_rnn_state, rnn)
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),use_random_iter=True)
plt.show()

困惑度 1.0, 50189.8 词元/秒 cuda:0
time traveller for so it will be convenient to speak of himwas e
traveller with a slight accession ofcheerfulness really thi

困惑度 1.5, 47149.4 词元/秒 cuda:0
time traveller proceeded anyreal body must have extension in fou
traveller held in his hand was a glitteringmetallic furmime

小结

  • 我们可以训练一个基于循环神经网络的字符级语言模型,根据用户提供的文本的前缀 生成后续文本
  • 一个简单的循环神经网络语言模型包括输入编码、循环神经网络模型和输出生成
  • 循环神经网络模型在训练以前需要初始化状态,不过随机抽样和顺序划分使用初始化方法不同
  • 当使用顺序划分时,我们需要分离梯度以减少计算量(detach)
  • 在进行任何预测之前,模型通过预热期进行自我更新(获得比初始值更好的隐状态,训练只是修改参数,并没有改状态)
  • 梯度裁剪可以防止梯度爆炸,但不能应对梯度消失

RNN的简洁实现

import torch
from matplotlib import pyplot as plt
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 定义模型
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)  # 直接调用模型
# 初始化隐状态
state = torch.zeros((1, batch_size, num_hiddens))
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
# 这里要注意的是rnn_layer的输出Y并不是我们想要的预测变量!而是隐状态!里面只进行了隐状态的计算而已# 完成的RNN模型
#@save
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_sizeself.num_hiddens = self.rnn.hidden_size# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1if not self.rnn.bidirectional:self.num_directions = 1self.linear = nn.Linear(self.num_hiddens, self.vocab_size)  # 输出层计算Yelse:self.num_directions = 2self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)def forward(self, inputs, state):X = F.one_hot(inputs.T.long(), self.vocab_size)X = X.to(torch.float32)Y, state = self.rnn(X, state)# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)# 它的输出形状是(时间步数*批量大小,词表大小)。output = self.linear(Y.reshape((-1, Y.shape[-1])))return output, statedef 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))# 训练与预测
device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.3, 390784.5 tokens/sec on cuda:0
time travellerit s against reatou dimensions of space generally
traveller pus he iryed it apredinnen it a mamul redoun abs

小结

  • 深度学习框架的高级API提供了RNN层的实现
  • 高级API的RNN层返回一个输出和一个更新后的隐状态,我们还需要另外一个线型层来计算整个模型的输出
  • 相比从零开始实现的RNN,使用高级API实现可以加速训练

通过时间反向传播

在RNN中,前向传播的计算相对简单,但是其通过时间反向传播实际上要求我们将RNN每次对一个时间步进行展开,以获得模型变量和参数之间的依赖关系,然后基于链式法则去应用放反向传播计算和存储梯度,这就导致当时间长度T较大时,可能依赖关系会相当长

假设RNN可表示为:
ht=f(xt,ht−1,wh)ot=g(ht,wo)损失函数为:L(x1,..,xT,y1,...,yT,wh,wo)=1T∑t=1Tl(yt,ot)h_t=f(x_t, h_{t-1},w_h)\\ o_t=g(h_t,w_o)\\ 损失函数为:L(x_1,..,x_T,y_1,...,y_T,w_h,w_o)=\frac{1}{T}\sum_{t=1}^Tl(y_t,o_t) ht=f(xt,ht1,wh)ot=g(ht,wo)损失函数为:L(x1,..,xT,y1,...,yT,wh,wo)=T1t=1Tl(yt,ot)
那么在计算梯度时:
∂L∂wh=1T∑t=1T∂l(yt,ot)∂wh=1T∑t=1T∂l(yt,ot)∂ot∂g(ht,wo)∂ht∂ht∂wh\frac{\partial L}{\partial w_h}=\frac{1}{T}\sum_{t=1}^T \frac{\partial l(y_t,o_t)}{\partial w_h}\\ =\frac{1}{T}\sum_{t=1}^T\frac{\partial l(y_t,o_t)}{\partial o_t}\frac{\partial g(h_t,w_o)}{\partial h_t}\frac{\partial h_t}{\partial w_h} whL=T1t=1Twhl(yt,ot)=T1t=1Totl(yt,ot)htg(ht,wo)whht
上述计算中最麻烦的是第三个,因为hth_tht不仅依赖于whw_hwh,还依赖于ht−1h_{t-1}ht1,而ht−1h_{t-1}ht1也依赖于whw_hwh,这样就会不停计算下去,即:
∂ht∂wh=∂f(xt,ht−1,wh)∂wh+∑i=1t−1(∏j=i+1t∂f(xj,hj−1,wh)∂hj−1)∂f(xi,hi−1,wh)∂wh\frac{\partial h_t}{\partial w_h}=\frac{\partial f(x_t,h_{t-1},w_h)}{\partial w_h}+\sum_{i=1}^{t-1}(\prod_{j=i+1}^t \frac{\partial f(x_j,h_{j-1},w_h)}{\partial h_{j-1}})\frac{\partial f(x_i,h_{i-1},w_h)}{\partial w_h} whht=whf(xt,ht1,wh)+i=1t1(j=i+1thj1f(xj,hj1,wh))whf(xi,hi1,wh)
那么如果采用上述完成的链式计算,当t很大时这个链就会变得很长,难以计算。具体有以下几种办法:


完全计算

显然最简单的思想当然是直接计算,但是这样非常缓慢,并且很可能会发生梯度爆炸,因为初始条件的额微小变化就可能因为连乘而带给结果巨大的影响,就类似于蝴蝶效应,这是不可取的。


截断时间步

可以在τ\tauτ步后截断上述的求和运算,即将链式法则终止于∂ht−τ∂wh\frac{\partial h_{t-\tau}}{\partial w_h}whhtτ,这样通常被称为截断的通过时间反向传播。这么做会导致模型主要侧重于短期影响,而不是长期影响,它会将估计值偏向更简单和更稳定的模型


随机截断

引入一个随机变量来代替∂ht∂wh\frac{\partial h_t}{\partial w_h}whht,即定义P(ξt=0)=1−πt,P(ξt=πt−1)=πtP(\xi_t=0)=1-\pi_t,P(\xi_t=\pi_t^{-1})=\pi_tP(ξt=0)=1πt,P(ξt=πt1)=πt,那么E[ξt]=1E[\xi_t]=1E[ξt]=1,令:
zt=∂f(xt,ht−1,wh)∂wh+ξt∂f(xt,ht−1,wh)∂ht−1∂ht−1∂whz_t=\frac{\partial f(x_t,h_{t-1},w_h)}{\partial w_h}+\xi_t \frac{\partial f(x_t,h_{t-1},w_h)}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial w_h} zt=whf(xt,ht1,wh)+ξtht1f(xt,ht1,wh)whht1
那么可以推导出E[zt]=∂ht∂whE[z_t]=\frac{\partial h_t}{\partial w_h}E[zt]=whht,这就导致了不同长度的截断,

门控循环单元GRU

这个机制是引入了重置门和更新门来更好地控制时序信息的传递,具体如下:

可以看到其中Rt、ZtR_t、Z_tRtZt分别称为重置门和更新门,那么Zt=1Z_t=1Zt=1时,Ht=Ht−1H_t=H_{t-1}Ht=Ht1,相当于信息完全不更新直接传递过去;而当Zt=0,Rt=0Z_t=0,R_t=0Zt=0,Rt=0时,相当于此时不关注Ht−1H_{t-1}Ht1的信息,截断时序的传递,那就相当于初始化了

注意这里Rt和Ht−1R_t和H_{t-1}RtHt1之间的计算是按照元素相乘。

import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 初始化模型参数,这部分和RNN不同
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.01def three():  # 用这个函数可以减少重复写return (normal((num_inputs, num_hiddens)),normal((num_hiddens, num_hiddens)),torch.zeros(num_hiddens, device=device))W_xz, W_hz, b_z = three()  # 更新门参数W_xr, W_hr, b_r = three()  # 重置门参数W_xh, W_hh, b_h = three()  # 候选隐状态参数# 输出层参数W_hq = normal((num_hiddens, num_outputs))b_q = torch.zeros(num_outputs, device=device)# 附加梯度params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]for param in params:param.requires_grad_(True)return params# 初始化隐状态
def init_gru_state(batch_size, num_hiddens, device):return (torch.zeros((batch_size, num_hiddens), device=device),)# 定义模型
def gru(inputs, state, params):W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params  # 获取参数H, = state  # 隐状态outputs = []for X in inputs:Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)  # 计算更新门,@是矩阵乘法R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)  # 注意这里R*H是按元素H = Z * H + (1 - Z) * H_tilda  # 这里也是按元素Y = H @ W_hq + b_q  # 输出outputs.append(Y)return torch.cat(outputs, dim=0), (H,)  # 同样是叠在一起vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,init_gru_state, gru)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.1, 16015.2 tokens/sec on cuda:0
time traveller for so it will be convenient to speak of himwas e
traveller for so it will be convenient to speak of himwas e

那么GRU的简洁实现也很简单:

num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))  # 封装成model的同时会加上线型层
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.0, 256679.5 tokens/sec on 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

可以看到我们调用高级API比从零实现快很多。


小结

  • 门控循环神经网络可以更好地捕获时间步距离很长的序列上的依赖关系
  • 重置门有助于捕获系列中的短期依赖关系
  • 更新门有助于捕获序列中的长期依赖关系
  • 重置门打开时,门控循环单元包含基本循环神经网络;更新门被打开时,门控循环单元可以跳过子序列

长短期记忆网络(LSTM)

这一部分老师讲得比较简单,更关注于实现方面,那么关于LSTM的比较全面的介绍内容可以观看李宏毅老师的课程中相关章节,或者阅读我这篇博客[此处]([机器学习]李宏毅——Recurrent Neural Network(循环神经网络)_FavoriteStar的博客-CSDN博客)。

LSTM的结构具体如下:

它最主要的特点就是引入了三个门控以及另外一个状态CtC_tCt来更好地存储和控制信息,三个门控分别为:

  • 输入门:决定是否忽略输入数据
  • 忘记门:将数值朝零减少
  • 输出门:决定是否使用隐状态

It=σ(XtWxi+Ht−1Whi+bi)Ft=σ(XtWxf+Ht−1Whf+bf)Ot=σ(XtWxo+Ht−1Who+bo)C~t=tanh⁡(XtWxc+Ht−1Whc+bc)Ct=Ft⊙Ct−1+It⊙C~tHt=Ot⊙tanh⁡(Ct)I_t=\sigma(X_tW_{xi}+H_{t-1}W_{hi}+b_i)\\ F_t=\sigma(X_tW_{xf}+H_{t-1}W_{hf}+b_f)\\ O_t=\sigma(X_tW_{xo}+H_{t-1}W_{ho}+b_o)\\ \tilde{C}_t=\tanh(X_tW_{xc}+H_{t-1}W_{hc}+b_c)\\ C_t=F_t\odot C_{t-1}+I_t\odot \tilde{C}_t\\ H_t=O_t\odot \tanh(C_t) It=σ(XtWxi+Ht1Whi+bi)Ft=σ(XtWxf+Ht1Whf+bf)Ot=σ(XtWxo+Ht1Who+bo)C~t=tanh(XtWxc+Ht1Whc+bc)Ct=FtCt1+ItC~tHt=Ottanh(Ct)

具体讲解可以看我上述提到的那篇博客,讲得比较仔细。

import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 初始化模型参数
def get_lstm_params(vocab_size, num_hiddens, device):num_inputs = num_outputs = vocab_sizedef normal(shape):return torch.randn(size=shape, device=device)*0.01def three():return (normal((num_inputs, num_hiddens)),normal((num_hiddens, num_hiddens)),torch.zeros(num_hiddens, device=device))W_xi, W_hi, b_i = three()  # 输入门参数W_xf, W_hf, b_f = three()  # 遗忘门参数W_xo, W_ho, b_o = three()  # 输出门参数W_xc, W_hc, b_c = three()  # 候选记忆元参数# 输出层参数W_hq = normal((num_hiddens, num_outputs))b_q = torch.zeros(num_outputs, device=device)# 附加梯度params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,b_c, W_hq, b_q]for param in params:param.requires_grad_(True)return params# 初始化隐状态,这部分就是两个了
def init_lstm_state(batch_size, num_hiddens, device):return (torch.zeros((batch_size, num_hiddens), device=device),torch.zeros((batch_size, num_hiddens), device=device))# 定义模型
def lstm(inputs, state, params):[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,W_hq, b_q] = params  # 获取参数(H, C) = state  # 获取隐状态outputs = []for X in inputs:I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)C = F * C + I * C_tildaH = O * torch.tanh(C)Y = (H @ W_hq) + b_q  # 计算输出outputs.append(Y)return torch.cat(outputs, dim=0), (H, C)vocab_size, num_hiddens, device = len(vocab), 512, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.1, 13369.1 tokens/sec on cuda:0
time traveller well the wild the urais diff me time srivelly are
travelleryou can show black is white by argument said filby

下面是简洁实现:

num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))  # 同样会补上输出的线型层实现
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.0, 147043.6 tokens/sec on cuda:0
time travelleryou can show black is white by argument said filby
travelleryou can show black is white by argument said filby

调用高级API的速度是从零实现是十倍往上。


小结

  • LSTM有三种类型的门:输入门、遗忘门和输出门
  • LSTM隐藏层输出包括隐状态和记忆元,只有隐状态会传递到输出层,而记忆元完全属于内部信息
  • LSTM可以缓解梯度消失和梯度爆炸的问题,因此多次使用到tanh将输出映射到[-1,1]之间,具体可以看我那篇博客最后。

深度循环神经网络

为了能够获得更多的非线性以及更强的表示能力,我们可以在深度上拓展循环神经网络:

这部分还是很简单很好理解的,对于GRU和LSTM同样可以采用。

import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)vocab_size, num_hiddens, num_layers = len(vocab), 256, 2  # 指定隐藏层的层数
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)  # 第三个参数指定隐藏层数目
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.0, 128068.2 tokens/sec on cuda:0
time travelleryou can show black is white by argument said filby
travelleryou can show black is white by argument said filby

小结

  • 在深度循环神经网络中,隐状态的信息被传递到当前层的下一时间步和下一层的当前时间步
  • 有许多不同风格的深度循环神经网络,如LSTM、GRU、RNN等,这些模型都可以用深度学习框架的高级API实现
  • 总体而言,深度循环神经网络需要大量的调参(如学习率和修剪) 来确保合适的收敛,模型的初始化也需要谨慎。

双向循环神经网络

之前的模型都是观察历史的数据来预测未来的数据,但是如果是在一些填空之类的任务中,未来的信息对这个空也是至关重要的:

因此双向循环神经网络就是可以观察未来的信息,它拥有一个前向RNN隐层和一个反向RNN隐层,然后输出层的输入是这两个层隐状态的合并,如下:

虽然这在训练的时候是没问题的,但是这种模型不能用于做预测任务,因此它无法得知未来的信息,这会造成很糟糕的结果。它最主要的用处是用来对序列进行特征抽取,因为它能够观察到未来的信息,因此特征抽取会更加全面

import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2l# 加载数据
batch_size, num_steps, device = 32, 35, d2l.try_gpu()
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 通过设置“bidirective=True”来定义双向LSTM模型
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, bidirectional=True)
model = d2l.RNNModel(lstm_layer, len(vocab))  # 里面已经设置了当为双向时线性层会不同
model = model.to(device)
# 训练模型
num_epochs, lr = 500, 1
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
plt.show()

perplexity 1.1, 76187.7 tokens/sec on cuda:0
time travellerererererererererererererererererererererererererer
travellerererererererererererererererererererererererererer

可以看到预测的效果是极差的。


小结

  • 在双向循环神经网络中,每个时间步的隐状态由当前时间步的前后数据同时决定。
  • 双向循环神经网络与概率图模型中的“前向-后向”算法具有相似性。
  • 双向循环神经网络主要用于序列编码和给定双向上下文的观测估计。
  • 由于梯度链更长,因此双向循环神经网络的训练代价非常高。

机器翻译与数据集

import os
import torch
from d2l import torch as d2l#@save
from matplotlib import pyplot as pltd2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip','94646ad1522d915e7b0f9296181140edcf86a4f5')#@save
def read_data_nmt():"""载入“英语-法语”数据集"""data_dir = d2l.download_extract('fra-eng')with open(os.path.join(data_dir, 'fra.txt'), 'r',encoding='utf-8') as f:return f.read()raw_text = read_data_nmt()
print(raw_text[:75])#@save
def preprocess_nmt(text):"""预处理“英语-法语”数据集"""def no_space(char, prev_char):return char in set(',.!?') and prev_char != ' '# 使用空格替换不间断空格# 使用小写字母替换大写字母text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()  # 将utf-8中半角全角空格都换成空格# 在单词和标点符号之间插入空格out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else charfor i, char in enumerate(text)]return ''.join(out)text = preprocess_nmt(raw_text)
print(text[:80])#@save
def tokenize_nmt(text, num_examples=None):"""词元化“英语-法语”数据数据集"""source, target = [], []for i, line in enumerate(text.split('\n')):if num_examples and i > num_examples:breakparts = line.split('\t')  # 按照制表符将英文和法文分开if len(parts) == 2:  # 说明前面是英文,后面是法文source.append(parts[0].split(' '))  # 按照我们前面插入的空格来划分target.append(parts[1].split(' '))return source, targetsource, target = tokenize_nmt(text)
print(source[:6], target[:6])def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):"""绘制列表长度对的直方图"""d2l.set_figsize()_, _, patches = d2l.plt.hist([[len(l) for l in xlist], [len(l) for l in ylist]])d2l.plt.xlabel(xlabel)d2l.plt.ylabel(ylabel)for patch in patches[1].patches:patch.set_hatch('/')d2l.plt.legend(legend)show_list_len_pair_hist(['source', 'target'], '# tokens per sequence','count', source, target)
plt.show()src_vocab = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])
# 转换成词表,然后加入一些特殊的词,分别是填充、开始、结尾
print(len(src_vocab))#@save
def truncate_pad(line, num_steps, padding_token):  # 这是为了保证我们的输入都是等长的"""截断或填充文本序列"""if len(line) > num_steps:  # 如果这个句子的长度大于设定长度,我们就截断return line[:num_steps]  # 截断return line + [padding_token] * (num_steps - len(line))  # 如果小于就进行填充print(truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>']))#@save
def build_array_nmt(lines, vocab, num_steps):"""将机器翻译的文本序列转换成小批量"""lines = [vocab[l] for l in lines]  # 将文本转换为向量lines = [l + [vocab['<eos>']] for l in lines]  # 每一个都要加上结尾符array = torch.tensor([truncate_pad(l, num_steps, vocab['<pad>']) for l in lines])# 进行填充或截断valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)# 这是把每个句子除填充外的有效长度都标注出来,之后计算会用到return array, valid_len#@save
def load_data_nmt(batch_size, num_steps, num_examples=600):"""返回翻译数据集的迭代器和词表"""text = preprocess_nmt(read_data_nmt())  # 预处理source, target = tokenize_nmt(text, num_examples)  # 生成英文和法文两部分# 转成词典src_vocab = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])tgt_vocab = d2l.Vocab(target, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)data_iter = d2l.load_array(data_arrays, batch_size)  # 一次迭代含有4个变量return data_iter, src_vocab, tgt_vocabtrain_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:print('X:', X.type(torch.int32))print('X的有效长度:', X_valid_len)print('Y:', Y.type(torch.int32))print('Y的有效长度:', Y_valid_len)break

Go. Va !
Hi.  Salut !
Run!    Cours !
Run!    Courez !
Who?    Qui ?
Wow!    Ça alors !go .   va !
hi . salut !
run !   cours !
run !   courez !
who ?   qui ?
wow !   ça alors !
[['go', '.'], ['hi', '.'], ['run', '!'], ['run', '!'], ['who', '?'], ['wow', '!']] [['va', '!'], ['salut', '!'], ['cours', '!'], ['courez', '!'], ['qui', '?'], ['ça', 'alors', '!']]
10012
[47, 4, 1, 1, 1, 1, 1, 1, 1, 1]
X: tensor([[  7,   0,   4,   3,   1,   1,   1,   1],[118,  55,   4,   3,   1,   1,   1,   1]], dtype=torch.int32)
X的有效长度: tensor([4, 4])
Y: tensor([[6, 7, 0, 4, 3, 1, 1, 1],[0, 4, 3, 1, 1, 1, 1, 1]], dtype=torch.int32)
Y的有效长度: tensor([5, 3])

小结

  • 机器翻译指的是将文本序列从一种语言自动翻译成另一种语言。
  • 使用单词级词元化时的词表大小,将明显大于使用字符级词元化时的词表大小。为了缓解这一问题,我们可以将低频词元视为相同的未知词元。
  • 通过截断和填充文本序列,可以保证所有的文本序列都具有相同的长度,以便以小批量的方式加载。

编码器-解码器架构

这是一个很重要的模型。因为机器翻译是序列转换模型中的一个核心问题,其输入和输出都是长度可变的序列。那么为了处理这种类型的结构,我们便使用到了编码器-解码器架构。

首先是编码器,它接受一个长度可变的序列作为输入,然后将其转换为具有固定形状的编码状态;然后是解码器,它将固定形状的编码状态映射到长度可变的序列,如下:

对于AE自编码器的介绍可以看我这篇博客,讲得比较仔细,有助于理解这种结构。

from torch import nn#@save
class Encoder(nn.Module):"""编码器-解码器架构的基本编码器接口"""def __init__(self, **kwargs):super(Encoder, self).__init__(**kwargs)def forward(self, X, *args):raise NotImplementedError#@save
class Decoder(nn.Module):"""编码器-解码器架构的基本解码器接口"""def __init__(self, **kwargs):super(Decoder, self).__init__(**kwargs)def init_state(self, enc_outputs, *args):  # 这部分就是编码后的状态raise NotImplementedErrordef forward(self, X, state):raise NotImplementedError#@save
class EncoderDecoder(nn.Module):"""编码器-解码器架构的基类"""def __init__(self, encoder, decoder, **kwargs):super(EncoderDecoder, self).__init__(**kwargs)self.encoder = encoderself.decoder = decoderdef forward(self, enc_X, dec_X, *args):enc_outputs = self.encoder(enc_X, *args)  # 计算编码后的值dec_state = self.decoder.init_state(enc_outputs, *args)return self.decoder(dec_X, dec_state)  # 解码

小结

  • 编码器-解码器架构可以将长度可变的序列作为输入和输出,因此适用于机器翻译等序列转换问题。
  • 编码器将长度可变的序列作为输入,并将其转换为具有固定形状的编码状态。
  • 解码器将具有固定形状的编码状态映射为长度可变的序列。

序列到序列学习(Seq2Seq)

这种任务就是给定一个序列,我们希望将其变换为另一个序列,最典型的应用就是机器翻译,它给定一个源语言的句子并将其翻译为目标语言。那么这就要求给定句子的长度是可变的,而且翻译后的句子可以有不同的长度。

那么这个任务最开始用的是编码器-解码器架构来做的:

且编码器和解码器用的都是RNN的模型。

编码器中RNN使用长度可变的序列作为输入,将其转换为固定形状的隐状态,此时输入序列的信息都被编码到隐状态中;然后将该编码器的最后一个隐状态作为解码器的初始隐状态,解码器的RNN根据该初始隐状态和自己的输入,开始进行预测

那么这种架构在训练和预测的时候有所不同,在训练时解码器的输入一直都是正确的预测结果,而在预测的时候解码器的输入就是本身预测的上一个结果,不一定正确

而因为现在我们不仅仅是预测字母,我们是预测整个句子,因此需要一个新的衡量指标来量化预测句子的好坏。常用的是BLEU,其具体如下:

pnp_npn是预测中所有n-gram的精度,例如真实序列ABCDEF和预测序列ABBCD,那么p1p_1p1就是预测序列中单个单元(A,B,B,C,D)在真实序列中是否出现,可以看到总共有4个出现了(B只出现1次)因此p1=45p_1=\frac{4}{5}p1=54,同理p2=34p_2=\frac{3}{4}p2=43p3=13p_3=\frac{1}{3}p3=31p4=0p_4=0p4=0

而BLEU的定义如下:
exp⁡(min⁡(0,1−lenlabellenpred))∏n=1kpn12n\exp \bigg( \min \Big( 0,~~1-\frac{len_{label}}{len_{pred}} \Big) \bigg)\prod_{n=1}^k p_n^{\frac{1}{2^n}} exp(min(0,1lenpredlenlabel))n=1kpn2n1
其中指数项是为了惩罚过短的预测,因此如果我只预测单个单元,那么只要其出现了我所有的pnp_npn(也就是p1p_1p1)就是1了,但这是不行的。第二项因为p都是小于1的,因此较长的匹配其指数(12n\frac{1}{2^n}2n1)会较小,因此可以认为其具有更大的权重


import collections
import math
import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2l# @save
class Seq2SeqEncoder(d2l.Encoder):"""用于序列到序列学习的循环神经网络编码器"""def __init__(self, vocab_size, embed_size, num_hiddens,num_layers, dropout=0, **kwargs):super(Seq2SeqEncoder, self).__init__(**kwargs)# 嵌入层self.embedding = nn.Embedding(vocab_size, embed_size)# 词嵌入,将文字自动转换成词向量self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout)# 因为已经转换为词向量,因此输入为词向量的长度def forward(self, X, *args):# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X)  # 先转换为词向量# 在循环神经网络模型中,第一个轴对应于时间步X = X.permute(1, 0, 2)  # 转换为时间步*批量大小*长度# 如果未提及状态,则默认为0output, state = self.rnn(X)# output的形状:(num_steps,batch_size,num_hiddens),# 因为有多层,它可以认为是最后一层的所有时间步的隐状态输出# state的形状:(num_layers,batch_size,num_hiddens)# 它是所有层的最后一个时间步的隐状态输出return output, stateclass Seq2SeqDecoder(d2l.Decoder):"""用于序列到序列学习的循环神经网络解码器"""def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout=0, **kwargs):super(Seq2SeqDecoder, self).__init__(**kwargs)self.embedding = nn.Embedding(vocab_size, embed_size)self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,dropout=dropout)# 因为下面做了拼接处理,因此这里输入的维度为embed_size+num_hiddensself.dense = nn.Linear(num_hiddens, vocab_size)def init_state(self, enc_outputs, *args):return enc_outputs[1]  # 里面有output,state,【1】就是把state拿出来def forward(self, X, state):# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X).permute(1, 0, 2)# 广播context,使其具有与X相同的num_steps  state[-1]是前面最后一层的最后一个隐状态context = state[-1].repeat(X.shape[0], 1, 1)# context的维度为num_steps,1,num_hiddensX_and_context = torch.cat((X, context), 2)# 将它们拼在一起,即输入embed_size+num_hiddens# 这里可以认为是:我觉得单纯的隐状态的传递不够,我再将最后一层的最后一个隐状态# 和我的第一个输入拼在一起,我觉得它浓缩了很多信息,也一起来作为输入output, state = self.rnn(X_and_context, state)output = self.dense(output).permute(1, 0, 2)# output的形状:(batch_size,num_steps,vocab_size)# state的形状:(num_layers,batch_size,num_hiddens)return output, state# @save
def sequence_mask(X, valid_len, value=0):  # 该函数生成mask并进行遮挡"""在序列中屏蔽不相关的项"""maxlen = X.size(1)  # 取出X中的第一维度的数量mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)[None, :] < valid_len[:, None]# arange生成一个一维的tensor,[None,:]是将其变成二维的,1*maxlen的tensor# 而valid_len是长度为max_len的向量,[:,None]就变成了max_len*1的tensor# 然后小于就会触发广播机制,例如max_len=4,那么arange生成的[[1,2,3,4]]就会广播# 变成4行,每一行都是[1,2,3,4],那么将每一列和valid_len这个列比较# 因为valid_len中的元素就是多少个有效的,那么假设2,就是前两个为true,后两个为false# 这样就可以将其提取出来了X[~mask] = valuereturn X# @save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):"""带遮蔽的softmax交叉熵损失函数"""# pred的形状:(batch_size,num_steps,vocab_size)# label的形状:(batch_size,num_steps)# valid_len的形状:(batch_size,)def forward(self, pred, label, valid_len):weights = torch.ones_like(label)weights = sequence_mask(weights, valid_len)self.reduction = 'none'  # 不对求出来的损失求和、平均等操作unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)  # 这里维度转换为torch本身的要求weighted_loss = (unweighted_loss * weights).mean(dim=1)  # 按元素相乘return weighted_loss# @save
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):"""训练序列到序列模型"""def xavier_init_weights(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)if type(m) == nn.GRU:for param in m._flat_weights_names:if "weight" in param:nn.init.xavier_uniform_(m._parameters[param])net.apply(xavier_init_weights)net.to(device)optimizer = torch.optim.Adam(net.parameters(), lr=lr)loss = MaskedSoftmaxCELoss()net.train()  # 开启训练模式animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[10, num_epochs])for epoch in range(num_epochs):timer = d2l.Timer()metric = d2l.Accumulator(2)  # 训练损失总和,词元数量for batch in data_iter:optimizer.zero_grad()  # 清空梯度X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]# 英文、英文有效长度、法文、法文有效长度bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0], device=device).reshape(-1, 1)  # 转换为相同纬度# Y是法文,在训练时是作为解码器的输入的,然后我们需要一个开始标注# 因此我们将Y的最后一个单词去掉,再在第一个前面加上一个开始标志bos# 这样我们强制让它学习bos去预测第一个词,而最后一个词它不会用来做预测,因此在预测时它去掉没关系# 那之后在真正预测的时候,我们就只需要给解码器第一个为bos,后面它自己生成的拿来做输入就可以dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学Y_hat, _ = net(X, dec_input, X_valid_len)  # 这个模型的第一个参数的编码器输入,第二个是解码器输入# 第三个是编码器输入的有效长度l = loss(Y_hat, Y, Y_valid_len)  # 这里就是用原来的Y去和预测的做损失l.sum().backward()  # 损失函数的标量进行“反向传播”d2l.grad_clipping(net, 1)  # 梯度裁剪num_tokens = Y_valid_len.sum()optimizer.step()with torch.no_grad():metric.add(l.sum(), num_tokens)if (epoch + 1) % 10 == 0:animator.add(epoch + 1, (metric[0] / metric[1],))print(f'loss{metric[0] / metric[1]:.3f},{metric[1] / timer.stop():.1f}'f'tokens/sec on{str(device)}')embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10  # 10是句子最长为10,超过裁剪,不足就补充
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
plt.show()# @save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps, device,save_attention_weights=False):"""序列到序列模型的预测"""# 在预测时将net设置为评估模式net.eval()# 将输入的句子变成小写再按空格分隔再加上结尾符,并且都经过src_vocab这个类转换成向量了src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]# 该句子的有效长度enc_valid_len = torch.tensor([len(src_tokens)], device=device)# 对该句子检查长度进行填充或者裁剪src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])# 添加批量轴,将src_tokens添加上批量这个维度,因此变成批量*时间步*vocabsizeenc_X = torch.unsqueeze(torch.tensor(src_tokens, dtype=torch.long, device=device),dim=0)# 计算encoder的输出enc_outputs = net.encoder(enc_X, enc_valid_len)# 计算decoder应该接受的初始状态dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)# 添加批量轴,因为现在是预测因此decoer的输入只有一个<bos>,那么为它添加一个批量轴dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)  # 增加一个维度output_seq, attention_weight_seq = [], []for _ in range(num_steps):Y, dec_state = net.decoder(dec_X, dec_state)  # 输出和隐状态# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入dec_X = Y.argmax(dim=2)  # 更新输入pred = dec_X.squeeze(dim=0).type(torch.int32).item()  # 去除批量这个维度# 保存注意力权重(稍后讨论)if save_attention_weights:attention_weight_seq.append(net.decoder.attention_weights)# 一旦序列结束词元被预测,输出序列的生成就完成了if pred == tgt_vocab['<eos>']:breakoutput_seq.append(pred)return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seqdef bleu(pred_seq, label_seq, k):  #@save"""计算BLEU"""pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')len_pred, len_label = len(pred_tokens), len(label_tokens)score = math.exp(min(0, 1 - len_label / len_pred))for n in range(1, k + 1):num_matches, label_subs = 0, collections.defaultdict(int)for i in range(len_label - n + 1):# 这个循环是将真实序列中的各种长度的连续词汇都变成词典计数label_subs[' '.join(label_tokens[i: i + n])] += 1for i in range(len_pred - n + 1):if label_subs[' '.join(pred_tokens[i: i + n])] > 0:num_matches += 1  # 这里判断出来预测序列中有对应的n-gramlabel_subs[' '.join(pred_tokens[i: i + n])] -= 1  # 要减一score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))return scoreengs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng}=>{translation}, bleu{bleu(translation, fra, k=2):.3f}')

loss 0.019, 12068.0 tokens/sec on cuda:0
go . => va !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est riche ., bleu 0.658
i'm home . => je suis chez moi chez moi chez moi juste ., bleu 0.537

小结

  • 根据“编码器-解码器”架构的设计, 我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。
  • 在实现编码器和解码器时,我们可以使用多层循环神经网络。
  • 我们可以使用遮蔽来过滤不相关的计算,例如在计算损失时。
  • 在“编码器-解码器”训练中,强制教学方法将原始输出序列(而非预测结果)输入解码器。
  • BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的n元语法的匹配度来评估预测。

束搜索

在前面的预测之中,我们采用的策略是贪心策略,也就是每一次预测的时候都是选择当前概率最大的来作为结果。那么贪心策略的最终结果通常不是最优的,然后穷举搜索计算复杂度太大了,因此有另一种方法为束搜索来进行改进。

束搜索有一种关键的参数为束宽kkk。在时间步1,也就是根据<bos>做第一次预测时,我们不止是选取概率最大的那个来进行输出,而是选择具有最高概率的kkk个词元,例如下图中我们在第一个时间步选择到了A和C。那么在之后的时间步中,就会基于上一个时间步所选择的kkk个候选序列,来从k∣Y∣k\vert Y\vertkY个可能中挑选出具有最高条件概率的kkk个候选输出序列:

并且,我们不止是考虑最终得到的长序列,而是考虑在选择过程中选择到的各个序列,即A,C,AB,CE,ABD,CED这两个序列。对它们的评估我们采用以下公式进行计算:
1Lαlog⁡P(y1,...,yL)=1Lα∑t′=1Llog⁡P(yt′∣y1,...,yt′−1)\frac{1}{L^{\alpha}}\log P(y_1,...,y_L)=\frac{1}{L^{\alpha}}\sum_{t^{\prime}=1}^L \log P(y_{t^{\prime}}\mid y_1,...,y_{t^{\prime}-1}) Lα1logP(y1,...,yL)=Lα1t=1LlogP(yty1,...,yt1)
其中L为序列的长度,α\alphaα常取0.75,这部分是为了中和长短序列的差距,因为短序列乘的概率少总是会大一点,因此用这部分来进行中和,相当于给选择短序列加入了一定的惩罚。

束搜索的时间复杂度为O(k∣Y∣T)O(k\vert Y\vert T)O(kYT)


小结

  • 序列搜索策略包括贪心搜索、穷举搜索和束搜索。
  • 贪心搜索所选取序列的计算量最小,但精度相对较低。
  • 穷举搜索所选取序列的精度最高,但计算量最大。
  • 束搜索通过灵活选择束宽,在正确率和计算代价之间进行权衡。

【动手学深度学习】李沐——循环神经网络相关推荐

  1. 动手学深度学习(李沐)的pytorch版本(包含代码和PDF版本)

    目录 网址(特别适合自学) 说明: 代码与PDF版 网址(特别适合自学) 传送门 界面一览: 说明:   github上一个项目将<动手学深度学习>从mxnet改为了pytorch实现.感 ...

  2. 《动手学深度学习》task3_3 循环神经网络进阶

    目录 GRU GRU 重置门和更新门 候选隐藏状态 隐藏状态 GRU的实现 载入数据集 初始化参数 GRU模型 训练模型 简洁实现 LSTM 长短期记忆 输入门.遗忘门和输出门 候选记忆细胞 记忆细胞 ...

  3. 动手学深度学习PyTorch版-循环神经网络基础

    循环神经网络基础 从零开始实现循环神经网络 import torch import torch.nn as nn import time import math import sys sys.path ...

  4. 动手学深度学习(十一) NLP循环神经网络

    循环神经网络 本节介绍循环神经网络,下图展示了如何基于循环神经网络实现语言模型.我们的目的是基于当前的输入与过去的输入序列,预测序列的下一个字符.循环神经网络引入一个隐藏变量,用表示在时间步的值.的计 ...

  5. 假设训练数据集中有10万个词,四元语法需要存储多少词频和多词相邻频率?《动手学深度学习 李沐》 转

    假设训练数据集中有10万个词,四元语法需要存储多少词频和多词相邻频率? 循环神经网络 Notes 1. 语言模型 语言模型(language model)是自然语言处理的重要技术.自然语言处理中最常见 ...

  6. pytorch卷积神经网络_知识干货-动手学深度学习(pytorch)-06 卷积神经网络基础

    卷积神经网络基础 本节我们介绍卷积神经网络的基础概念,主要是卷积层和池化层,并解释填充.步幅.输入通道和输出通道的含义. 二维卷积层 本节介绍的是最常见的二维卷积层,常用于处理图像数据. 二维互相关运 ...

  7. 【动手学深度学习】(task123)注意力机制剖析

    note 将注意力汇聚的输出计算可以作为值的加权平均,选择不同的注意力评分函数会带来不同的注意力汇聚操作. 当查询和键是不同长度的矢量时,可以使用可加性注意力评分函数.当它们的长度相同时,使用缩放的& ...

  8. 资源 | 李沐等人开源中文书《动手学深度学习》预览版上线

    来源:机器之心 本文约2000字,建议阅读10分钟. 本文为大家介绍了一本交互式深度学习书籍. 近日,由 Aston Zhang.李沐等人所著图书<动手学深度学习>放出了在线预览版,以供读 ...

  9. 【深度学习】李沐《动手学深度学习》的PyTorch实现已完成

    这个项目是中文版<动手学深度学习>中的代码进行整理,用Pytorch实现,是目前全网最全的Pytorch版本. 项目作者:吴振宇博士 简介   Dive-Into-Deep-Learnin ...

  10. 推荐:李沐开源新作,一起来《动手学深度学习》

    来源 /Datawhale 图文 / 静修   排版 / 家豪 [导读]<动手学深度学习>这本书由亚马逊首席科学家李沐,亚马逊应用科学家阿斯顿·张等大师合作打造,沉淀三年完成.本书采用交互 ...

最新文章

  1. OSChina 周二乱弹 ——假期综合症
  2. 人力资源学python有意义吗-python爬虫抖音 个人资料 仅供学习参考 切勿用于商业...
  3. 去除 计算机里面的百度云管家,WIN7如何彻底清除“百度云管家”图标或残留文件?...
  4. muduo学习笔记 - 第3章 多线程服务器的适合场合与常用编程模型
  5. 在UnitTest中读取*.config文件的郁闷
  6. 为AWT的机器人创建DSL
  7. pytorch中gather函数的理解
  8. 告诉各位为如何学习linux系统
  9. 视频技术系列 - 2020年超高清视频技术创新实践
  10. 力扣——字符串转换整数(atoi)
  11. python flask的request模块以及在flask编程中遇到的坑
  12. ubuntu下的qt程序移植至ARM开发板
  13. mysql完成字符串分割
  14. linux大文件分割与合并
  15. 有意思的DCDC工作原理
  16. python 分词字典的词性_NLP注2“自定义词性与词典实现”,笔记,字典,的
  17. android+国家区号api,android国际区号选择器
  18. 【数据库】PostgreSQL简介
  19. python3 get爬取网页标题、链接和链接的数字ID
  20. UnityPlayerActivity详解

热门文章

  1. 最新小程序反编译详细教程,亲测可用
  2. 阿里云DDoS防护产品介绍
  3. java基础知识和JDBC
  4. java jmf 教程_JMF入门(Java Media Framework)
  5. JAVA实现诗词_基于jsp的古诗词网站-JavaEE实现古诗词网站 - java项目源码
  6. 二十一世纪大学英语读写基础教程学习笔记(原文)——5 - Shipwrecked in Antarctica(沉船南极洲)
  7. MapReduce中名字的通俗解释--故事会
  8. 梯度下降和随机梯度下降
  9. 【SEO优化】SEO应该是我们现在理解的这样吗?
  10. Excel如何一键删除重复行?Leo老师告诉你