《动手学深度学习》学习笔记 第8章 循环神经网络

本系列为《动手学深度学习》学习笔记

书籍链接:动手学深度学习

笔记是从第四章开始,前面三章为基础知识,有需要的可以自己去看看

关于本系列笔记: 书里为了让读者更好的理解,有大篇幅的描述性的文字,内容很多,笔记只保留主要内容,同时也是对之前知识的查漏补缺

《动手学深度学习》学习笔记 第4章 多层感知机
《动手学深度学习》学习笔记 第5章 深度学习计算
《动手学深度学习》学习笔记 第6章 卷积神经网络
《动手学深度学习》学习笔记 第7章 现代卷积神经网络
《动手学深度学习》学习笔记 第8章 循环神经网络
《动手学深度学习》学习笔记 第9章 现代循环神经网络

8. 循环神经网络

  到目前为止我们默认数据都来自于某种分布,并且所有样本都是独立同分布的(independently and identically distributed,i.i.d.)。然而,大多数的数据并非如此。

  如果说卷积神经网络可以有效地处理空间信息,那么本章的**循环神经网络(recurrent neural network, RNN)**则可以更好地处理序列信息。

8.1 序列模型

8.1.1 统计工具

  处理序列数据需要统计工具和新的深度神经网络架构。为了简单起见,以 图8.1.1所示的股票价格(富时100指数)为例。
在这里插入图片描述
图8.1.1: 近30年的富时100指数
  其中,用 x t x_t xt表示价格,即在时间步(time step) t ∈ Z + t ∈ Z^+ tZ+时,观察到的价格 x t x_t xt t t t对于本文中的序列通常是离散的,并在整数或其子集上变化)

  假设一个交易员想在 t t t日的股市中表现良好,于是通过以下途径预测 x t x_t xt
x t ∼ P ( x t ∣ x t − 1 , . . . , x 1 ) . ( 8.1.1 ) x_t ∼ P(xt | x_{t−1}, . . . , x_1). (8.1.1) xtP(xtxt1,...,x1).(8.1.1)

自回归模型

  主要问题:输入数据的数量,输入 x t − 1 , . . . , x 1 x_{t−1}, . . . , x_1 xt1,...,x1本身因t而异。也就是说,输入数据的数量会随着遇到的数据量的增加
而增加,因此需要一个近似方法来使这个计算变得容易处理。

  如何有效估计 P ( x t ∣ x t − 1 , . . . , x 1 ) P(x_t | x_{t−1}, . . . , x_1) P(xtxt1,...,x1)? 简单地说,它归结为以下两种策略:

  • 第一种策略,假设在现实情况下很长的序列 x t − 1 , . . . , x 1 x_{t−1}, . . . ,x_1 xt1,...,x1可能是不必要的,只需要满足某个长度为 τ τ τ的时间跨度,即使用观测序列 x t − 1 , . . . , x t − τ x_{t−1}, . . . , x_{t−τ} xt1,...,xtτ。这样的好处就是参数的数量总是不变的,至少在 t > τ t >τ t>τ时如此。这种模型被称为自回归模型(autoregressive models),因为它们是对自己执行回归。
  • 第二种策略,如 图8.1.2所示,保留一些对过去观测的总结 h t h_t ht,并且同时更新预测 x ^ t \hat{x}_t x^t总结 h t h_t ht。这就产生了基于 x ^ t = P ( x t ∣ h t ) \hat{x}t =P(x_t | h_t) x^t=P(xtht)估计 x t x_t xt,以及公式 h t = g ( h t − 1 , x t − 1 ) h_t = g(h_{t−1}, x_{t−1}) ht=g(ht1,xt1)更新的模型。由于 h t h_t ht从未被观测到,这类模型也被称为隐变量自回归模型(latent autoregressive models)
    在这里插入图片描述

图8.1.2: 隐变量自回归模型

  这两种情况都有一个显而易见的问题:如何生成训练数据?

  一个经典方法是使用历史观测来预测下一个未来观测。显然,我们并不指望时间会停滞不前。然而,一个常见的假设是虽然特定值xt可能会改变,但是序列本身的动力学(可以理解为变化趋势或者变化)不会改变。这样的假设是合理的,因为新的动力学一定受新的数据影响,而我们不可能用目前所掌握的数据来预测新的动力学。统计学家称不变的动力学为静止的(stationary)。因此,整个序列的估计值都将通过以下的方式获得:

P ( x 1 , . . . , x T ) = ∏ t = 1 T P ( x t ∣ x 1 , . . . , x T ) P( x_1, . . . , x_T) = \prod \limits_{t=1}^TP( x_t|x_1, . . . , x_T) P(x1,...,xT)=t=1TP(xtx1,...,xT)

注意,如果处理的是离散的对象(如单词),上述的考虑仍然有效。唯一的差别是,对于离散的对象,需要使用分类器而不是回归模型来估计 P ( x t ∣ x t − 1 , . . . , x 1 ) P(x_t | x_{t−1}, . . . , x_1) P(xtxt1,...,x1)

马尔可夫模型

  回想一下,在自回归模型的近似法中,我们使用 x t − 1 , . . . , x t − τ x_{t−1}, . . . , x_{t−τ} xt1,...,xtτ 而不是 x t − 1 , . . . , x 1 x_{t−1}, . . . , x_1 xt1,...,x1来估计 x t x_t xt。只要这种是近似精确的,就说序列满足马尔可夫条件(Markov condition)。特别是,如果 τ = 1 τ = 1 τ=1,得到一个 一阶马尔可夫模型(first‐order Markov model) P ( x ) P(x) P(x)由下式给出:
P ( x 1 , . . . , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 ) 当 P ( x 1 ∣ x 0 ) = P ( x 1 ) P( x_1, . . . , x_T) = \prod \limits_{t=1}^TP( x_t|x_{t-1})当P(x_1|x_0)=P(x_1) P(x1,...,xT)=t=1TP(xtxt1)Px1x0=P(x1)

  当假设 x t x_t xt仅是离散值时,使用动态规划可以沿着马尔可夫链精确地计算结果。例如,可以高效地计算 P ( x t + 1 ∣ x t − 1 ) P(x_{t+1} | x_{t−1}) P(xt+1xt1)
在这里插入图片描述
  利用这一事实,只需要考虑过去观察中的一个非常短的历史: P ( x t + 1 ∣ x t , x t − 1 ) = P ( x t + 1 ∣ x t ) P(x_{t+1} | x_t, x_{t−1}) = P(x_{t+1} | x_t) P(xt+1xt,xt1)=P(xt+1xt)。隐马尔可夫模型中的动态规划超出了本节的范围(将在 9.4节再次遇到),而动态规划这些计算工具已经在控制算法和强化学习算法广泛使用。

因果关系

  原则上,可以将 P ( x 1 , . . . , x T ) P(x_1, . . . , x_T ) P(x1,...,xT)倒序展开。基于条件概率公式,可以写出:
P ( x 1 , . . . , x T ) = ∏ t = T 1 P ( x t ∣ x t + 1 , . . . , x T ) P( x_1, . . . , x_T) = \prod \limits_{t=T}^1 P( x_t|x_{t+1}, . . . , x_T) P(x1,...,xT)=t=T1P(xtxt+1,...,xT)
  事实上,如果基于一个马尔可夫模型,还可以得到一个反向的条件概率分布。

  然而,在许多情况下,数据存在一个自然的方向,即在时间上是前进的。很明显,未来的事件不能影响过去。因此,如果我们改变 x t x_t xt,可能会影响未来发生的事情 x t + 1 x_{t+1} xt+1,但不能反过来(也就是说,如果我们改变 x t x_t xt,基于过去事件得到的分布不会改变。)

  因此,解释 P ( x t + 1 ∣ x t ) P(x_{t+1} | x_t) P(xt+1xt)应该比解释 P ( x t ∣ x t + 1 ) P(x_t | x_{t+1}) P(xtxt+1)更容易。例如,在某些情况下,对于某些可加性噪声 ϵ ϵ ϵ,显然可以找到 x t + 1 = f ( x t ) + ϵ x_{t+1} = f(x_t) + ϵ xt+1=f(xt)+ϵ,而反之则不行 (Hoyer et al., 2009)。

8.1.2 训练

  了解了上述统计工具后,在实践中尝试一下!

  首先,生成一些数据:使用正弦函数和一些可加性噪声来生成序列数据,时间步为1, 2, . . . , 1000。

%matplotlib inline
import torch
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))

在这里插入图片描述
  接下来,将这个序列转换为特征-标签(feature‐label)对。基于嵌入维度 τ τ τ,将数据映射为数据对 y t = x t y_t = x_t yt=xt x t = [ x t − τ , . . . , x t − 1 ] x_t = [x_{t−τ} , . . . , x_{t−1}] xt=[xtτ,...,xt1]。这比数据样本少了 τ τ τ个( x 0 到 x τ x_0到x_τ x0xτ),因为我们没有足够的历史记录来描述前τ个数据样本。一个简单的解决办法是:

  • 如果拥有足够长的序列就丢弃这几项;
  • 另一个方法是用零填充序列。

  在这里,我们仅使用前600个 “特征-标签” 对进行训练。

tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):features[:, i] = x[i: T - tau + i]labels = x[tau:].reshape((-1, 1))
batch_size, n_train = 16, 600# 只有前n_train个样本用于训练
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),batch_size, is_train=True)

  其中:features[:, i] = x[i: T - tau + i] 就是根据 x t = [ x t − τ , . . . , x t − 1 ] x_t = [x_{t−τ} , . . . , x_{t−1}] xt=[xtτ,...,xt1]生成的数据特征

  在这里,使用一个相当简单的架构训练模型:拥有两个全连接层的多层感知机ReLU激活函数平方损失

# 初始化网络权重的函数
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 net
# 平方损失。注意:MSELoss计算平方误差时不带系数1/2
loss = 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)
==========================================================
epoch 1, loss: 0.076846
epoch 2, loss: 0.056340
epoch 3, loss: 0.053779
epoch 4, loss: 0.056320
epoch 5, loss: 0.051650

8.1.3 预测

  由于训练损失很小,因此期望模型能有很好的工作效果。这在实践中意味着什么?

  首先是检查模型预测下一个时间步的能力,也就是单步预测(one‐step‐ahead prediction)。

onestep_preds = net(features)
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))

在这里插入图片描述

  单步预测效果不错。即使这些预测的时间步超过了 600 + 4 ( n t r a i n + t a u ) 600 + 4(n_train + tau) 600+4ntrain+tau,其结果看起来仍然是可信的。

  然而有一个小问题:如果数据观察序列的时间步只到604,需要一步一步地向前迈进:
x ^ 605 = f ( x 601 , x 602 , x 603 , x 604 ) , . . . ( 8.1.6 ) \hat{x}_{605} = f(x_{601}, x_{602}, x_{603}, x_{604}),. . .(8.1.6) x^605=f(x601,x602,x603,x604),...(8.1.6)
x ^ 606 = f ( x 602 , x 603 , x 604 ) , x 605 , . . . ( 8.1.6 ) \hat{x}_{606} = f(x_{602}, x_{603}, x_{604}),x_{605}, . . .(8.1.6) x^606=f(x602,x603,x604),x605,...(8.1.6)
… … …… ……

  通常,对于直到 x t x_t xt的观测序列,其在时间步 t + k t + k t+k处的预测输出 x ^ t + k \hat{x}_{t+k} x^t+k 称为 k k k步预测(k‐step‐ahead‐prediction)
由于我们的观察已经到了 x 604 x_{604} x604,它的 k k k步预测是 x ^ 604 + k \hat{x}_{604+k} x^604+k

  换句话说,必须使用我们自己的预测(而不是原始数据)来进行多步预测。 让我们看看效果如何。

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))

在这里插入图片描述

  如上面的例子所示,绿线的预测显然并不理想。经过几个预测步骤之后,预测的结果很快就会衰减到一个常数。

为什么这个算法效果这么差呢? 事实是由于错误的累积

  • 假设在步骤1之后,积累了一些错误 ϵ 1 = ϵ ^ + c ϵ 1 ϵ_1 = \hat{ϵ}+ cϵ_1 ϵ1=ϵ^+cϵ1
  • 于是,步骤2的输入被扰动了 ϵ 1 ϵ1 ϵ1,结果积累的误差是依照次序的 ϵ 2 = ¯ ϵ + c ϵ 1 ϵ2 = ¯ϵ + cϵ1 ϵ2=¯ϵ+1(其中 c c c为某个常数)
  • 后面的预测误差依此类推。

因此误差可能会相当快地偏离真实的观测结果。(例如,未来24小时的天气预报往往相当准确,但超过这一点,精度就会迅速下降)。

我们将在本章及后续章节中讨论如何改进这一点。

  基于 k = 1 , 4 , 16 , 64 k = 1, 4, 16, 64 k=1,4,16,64,通过对整个序列预测的计算,更仔细地看一下k步预测的困难。

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))

在这里插入图片描述
  以上例子清楚地说明了当我们试图预测更远的未来时,预测的质量是如何变化的。虽然“4步预测”看起来仍然不错,但超过这个跨度的任何预测几乎都是无用的。

小结

  • 内插法(在现有观测值之间进行估计)和外推法(对超出已知观测范围进行预测)在实践的难度上差别很大。因此,对于所拥有的序列数据,在训练时始终要尊重其时间顺序,即最好不要基于未来的数据进行训练
  • 序列模型的估计需要专门的统计工具,两种较流行的选择是自回归模型隐变量自回归模型
  • 对于时间是向前推进的因果模型,正向估计通常比反向估计更容易
  • 对于直到时间步t的观测序列,其在时间步t +k的预测输出是“k步预测”。随着我们对预测时间k值的增加,会造成误差的快速累积和预测质量的极速下降。

8.2 文本预处理

  对于序列数据处理问题,在 8.1节中评估了所需的统计工具和预测时面临的挑战。
  本节中,将解析文本的常见预处理步骤。这些步骤通常包括:

  1. 将文本作为字符串加载到内存中。
  2. 将字符串拆分为词元(如单词和字符)。
  3. 建立一个词表,将拆分的词元映射到数字索引。
  4. 将文本转换为数字索引序列,方便模型操作。
import collections
import re
from d2l import torch as d2l

8.2.1 读取数据集

  首先,从H.G.Well的时光机器99中加载文本。这是一个相当小的语料库,只有30000多个单词,而现实中的文档集合可能会包含数十亿个单词。(下面的函数将数据集读取到由多条文本行组成的列表中,其中每条文本行都是一个字符串。为简单起见,在这里忽略了标点符号和字母大写。)

#@save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt','090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine(): #@save"""将时间机器数据集加载到文本行的列表中"""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]lines = read_time_machine()
print(f'# 文本总行数: {len(lines)}')
print(lines[0])
print(lines[10])
==============================================================
Downloading ../data/timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt...
# 文本总行数: 3221
the time machine by h g wells
twinkled and his usually pale face was flushed and animated the

8.2.2 词元化

下面的tokenize函数

  • 文本行列表(lines) 作为输入,列表中的每个元素一个文本序列(如一条文本行)。
  • 每个文本序列又被拆分成一个词元列表词元(token) 是文本的基本单位。
  • 最后,返回一个由词元列表组成的列表,其中的每个词元都是一个字符串(string)。
def tokenize(lines, token='word'): #@save"""将文本行拆分为单词或字符词元"""if 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])['the', 'time', 'machine', 'by', 'h', 'g', 'wells']
[]
[]
[]
[]
['i']
[]
[]
['the', 'time', 'traveller', 'for', 'so', 'it', 'will', 'be', 'convenient', 'to', 'speak', 'of', 'him']
['was', 'expounding', 'a', 'recondite', 'matter', 'to', 'us', 'his', 'grey', 'eyes', 'shone', 'and']
['twinkled', 'and', 'his', 'usually', 'pale', 'face', 'was', 'flushed', 'and', 'animated', 'the']

8.2.3 词表

  词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。

  现在,让我们构建一个字典,通常也叫做词表(vocabulary),用来将字符串类型的词元映射到从0开始的数字索引中。

  • 先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计,得到的统计结果称之为语料(corpus)
  • 然后根据每个唯一词元的出现频率,为其分配一个数字索引。很少出现的词元通常被移除,这可以降低复杂性。
  • 另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“”

  可以选择增加一个列表,用于保存那些被保留的词元,例如:填充词元(“”);序列开始词元(“”);序列结束词元(“”)。

class Vocab: #@save"""文本词表"""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)# 未知词元的索引为0self.idx_to_token = ['<unk>'] + reserved_tokensself.token_to_idx = {token: idxfor 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) - 1def __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]@propertydef unk(self): # 未知词元的索引为0return 0@propertydef token_freqs(self):return self._token_freqsdef 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)

  首先使用时光机器数据集作为语料库来构建词表,然后打印前几个高频词元及其索引。

vocab = Vocab(tokens)print(list(vocab.token_to_idx.items())[:10])[('<unk>', 0), ('the', 1), ('i', 2), ('and', 3), ('of', 4), ('a', 5), ('to', 6), ('was', 7), ('in', 8),('that', 9)]

  现在,可以将每一条文本行转换成一个数字索引列表。

for i in [0, 10]:print('文本:', tokens[i])print('索引:', vocab[tokens[i]])=======================================================================
文本: ['the', 'time', 'machine', 'by', 'h', 'g', 'wells']
索引: [1, 19, 50, 40, 2183, 2184, 400]
文本: ['twinkled', 'and', 'his', 'usually', 'pale', 'face', 'was', 'flushed', 'and','animated', 'the']
索引: [2186, 3, 25, 1044, 362, 113, 7, 1421, 3, 1045, 1]

8.2.4 整合所有功能

  在使用上述函数时,将所有功能打包到load_corpus_time_machine函数中,该函数返回corpus(词元索引列表)vocab(时光机器语料库的词表)

  在这里所做的改变是:

  1. 为了简化后面章节中的训练,我们使用字符(而不是单词)实现文本词元化;
  2. 时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的corpus仅处理为单个列表,而不是使用多词元列表构成的一个列表。
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, vocab
corpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)
===================================================================
(170580, 28)

小结

  • 文本是序列数据的一种最常见的形式之一。
  • 为了对文本进行预处理,我们通常将文本拆分为词元,构建词表将词元字符串映射为数字索引,并将文本数据转换为词元索引以供模型操作。

8.3 语言模型和数据集

  在 8.2节中,了解了如何将文本数据映射为词元,以及将这些词元可以视为一系列离散的观测,例如单词或字符。假设长度为T的文本序列中的词元依次为 x 1 , . . . , x T x_1, . . . , x_T x1,...,xT。于是, x t ( 1 ≤ t ≤ T ) x_t(1 ≤ t ≤ T) xt1tT可以被认为是文本序列在时间步t处观测或标签。在给定这样的文本序列时,语言模型(language model)的目标是估计序列的联合概率
P ( x 1 , . . . , x T ) . ( 8.3.1 ) P(x_1, . . . , x_T ). (8.3.1) P(x1,...,xT).(8.3.1)

8.3.1 学习语言模型

  我们面对的问题是如何对一个文档,甚至是一个词元序列进行建模。假设在单词级别对文本数据进行词元化,可以依靠在 8.1节中对序列模型的分析。让我们从基本概率规则开始:
P ( x 1 , . . . , x T ) = ∏ t = 1 T P ( x t ∣ x 1 , . . . , x T ) P( x_1, . . . , x_T) = \prod \limits_{t=1}^TP( x_t|x_1, . . . , x_T) P(x1,...,xT)=t=1TP(xtx1,...,xT)
  例如,包含了四个单词的一个文本序列的概率是:
P ( d e e p , l e a r n i n g , i s , f u n ) = P ( d e e p ) P ( l e a r n i n g ∣ d e e p ) P ( i s ∣ d e e p , l e a r n i n g ) P ( f u n ∣ d e e p , l e a r n i n g , i s ) . ( 8.3.3 ) P(deep, learning, is,fun) = P(deep)P(learning | deep)P(is | deep, learning)P(fun | deep, learning, is).(8.3.3) P(deep,learning,is,fun)=P(deep)P(learningdeep)P(isdeep,learning)P(fundeep,learning,is).(8.3.3)
  为了训练语言模型,我们需要计算单词的概率,以及给定前面几个单词后出现某个单词的条件概率。这些概率本质上就是语言模型的参数。

  这里,我们假设训练数据集是一个大型的文本语料库。比如,维基百科的所有条目、古登堡计划101,或者所有发布在网络上的文本。训练数据集中词的概率可以根据给定词的相对词频来计算。

  例如,可以将估计值 P ^ ( d e e p ) \hat{P}(deep) P^(deep) 计算为任何以单词“deep”开头的句子的概率。一种(稍稍不太精确的)方法是统计单词“deep”在数据集中的出现次数,然后将其除以整个语料库中的单词总数。特别是对于频繁出现的单词,这种方法效果不错。

  接下来,可以尝试估计

P ^ ( l e a r n i n g ∣ d e e p ) = n ( d e e p , l e a r n i n g ) n ( d e e p ) \hat{P}(learning | deep) = \frac{n(deep, learning)}{n(deep)} P^(learningdeep)=n(deep)n(deep,learning)

  不幸的是,由于连续单词对 “deep learning” 的出现频率要低得多,所以估计这类单词正确的概率要困难得多。特别是对于一些不常见的单词组合,要想找到足够的出现次数来获得准确的估计可能都不容易。除非我们提供某种解决方案,来将这些单词组合指定为非零计数,否则将无法在语言模型中使用它们。

  一种常见的策略是执行某种形式的拉普拉斯平滑(Laplace smoothing),具体方法是在所有计数中添加一个小常量。用 n n n表示训练集中的单词总数,用 m m m表示唯一单词的数量。
在这里插入图片描述

8.3.2 马尔可夫模型与n元语法

  回想在 8.1节中对马尔可夫模型的讨论,并且将其应用于语言建模。如果 P ( x t + 1 ∣ x t , . . . , x 1 ) = P ( x t + 1 ∣ x t ) P(xt+1 | xt, . . . , x1) = P(xt+1 | xt) P(xt+1∣xt,...,x1)=P(xt+1∣xt),则序列上的分布满足
阶马尔可夫性质
阶数越高,对应的依赖关系就越长

  这种性质推导出了许多可以应用于序列建模的近似公式:
在这里插入图片描述
  通常,涉及一个、两个和三个变量的概率公式分别被称为 一元语法(unigram)二元语法(bigram)三元语法(trigram) 模型。下面,我们将学习如何去设计更好的模型。

8.3.3 自然语言统计

  根据 8.2节中介绍的时光机器数据集构建词表,并打印前10个
最常用的(频率最高的)单词。

import random
import torch
from d2l import torch as d2l
tokens = d2l.tokenize(d2l.read_time_machine())
# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)
vocab.token_freqs[:10]
==========================
[('the', 2261),
('i', 1267),
('and', 1245),
('of', 1155),
('a', 816),
('to', 695),
('was', 552),
('in', 541),
('that', 443),
('my', 440)]

  正如所看到的,最流行的词看起来很无聊,这些词通常被称为停用词(stop words),因此可以被过滤掉。尽管如此,它们本身仍然是有意义的,我们仍然会在模型中使用它们。

  此外,还有个明显的问题是词频衰减的速度相当地快。例如,最常用单词的词频对比,第10个还不到第1个的1/5。

freqs = [freq for token, freq in vocab.token_freqs]d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',xscale='log', yscale='log')

在这里插入图片描述

  通过此图我们可以发现:词频以一种明确的方式迅速衰减:单词的频率满足齐普夫定律(Zipf’s law),即第i个最常用
单词的频率ni为:
n i ∝ 1 i α n_i ∝\frac{1}{i^α} niiα1
等价于
l o g n i = − α l o g i + c , log n_i = −α log i + c, logni=αlogi+c,
其中 α α α是刻画分布的指数, c c c是常数。

  这告诉我们想要通过计数统计和平滑来建模单词是不可行的,因为这样建模的结果会大大高估尾部单词的频率,也就是所谓的不常用单词。

  直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法。

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'])

在这里插入图片描述

  1. 除了一元语法词,单词序列似乎也遵循齐普夫定律,尽管公式 (8.3.7)中的指数α更小(指数的大小受序列长度的影响);
  2. 词表中n元组的数量并没有那么大,这说明语言中存在相当多的结构,这些结构给了我们应用模型的希望;
  3. 很多n元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。作为代替,我们将使用基于深度学习的模型。

8.3.4 读取长序列数据

  假设我们将使用神经网络来训练语言模型,模型中的网络一次处理一个小批量序列(小批量序列:具有预定义长度(例如n个时间步))。
  首先,由于文本序列可以是任意长的,例如整本《时光机器》(The Time Machine),可以选择任意偏移量来指示初始位置。
在这里插入图片描述
图8.3.1: 分割文本时,不同的偏移量会导致不同的子序列

  如果只选择一个偏移量,那么用于训练网络的、所有可能的子序列的覆盖范围将是有限的。因此,可以从随机偏移量开始划分序
列,以同时获得覆盖性(coverage)随机性(randomness)

  下面,将描述如何实现随机采样(random sampling)顺序分区(sequential partitioning) 策略。

随机采样

  在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元,因此标签是移位了一个词元的原始序列

  下面的代码每次可以从数据中随机生成一个小批量。

  • 参数batch_size指定了每个小批量中子序列样本的数目,
  • 参数num_steps是每个子序列中预定义的时间步数。
def seq_data_iter_random(corpus, batch_size, num_steps): #@save"""使用随机抽样生成一个小批量子序列"""# 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1corpus = 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_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]yield torch.tensor(X), torch.tensor(Y)

  下面生成一个从0到34的序列。批量大小(num_steps)为2,时间步数(num_steps)为5,这意味着可以生成 ⌊(35 − 1)/5⌋ = 6个“特征-标签”子序列对。

my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):print('X: ', X, '\nY:', Y)
---------------------------------------------------------------
X: tensor([[13, 14, 15, 16, 17],[28, 29, 30, 31, 32]])
Y: tensor([[14, 15, 16, 17, 18],[29, 30, 31, 32, 33]])
X: tensor([[ 3, 4, 5, 6, 7],[18, 19, 20, 21, 22]])
Y: tensor([[ 4, 5, 6, 7, 8],[19, 20, 21, 22, 23]])
X: tensor([[ 8, 9, 10, 11, 12],[23, 24, 25, 26, 27]])
Y: tensor([[ 9, 10, 11, 12, 13],[24, 25, 26, 27, 28]])

顺序分区

  在迭代过程中,还可以保证两个相邻的小批量中的子序列在原始序列上也是相邻的。这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。

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, Y

  基于相同的设置,通过顺序分区读取每个小批量的子序列的特征X和标签Y。通过将它们打印出来可以发现:迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。

for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
print('X: ', X, '\nY:', Y)
X: tensor([[ 0, 1, 2, 3, 4],[17, 18, 19, 20, 21]])
Y: tensor([[ 1, 2, 3, 4, 5],[18, 19, 20, 21, 22]])
X: tensor([[ 5, 6, 7, 8, 9],[22, 23, 24, 25, 26]])
Y: tensor([[ 6, 7, 8, 9, 10],[23, 24, 25, 26, 27]])
X: tensor([[10, 11, 12, 13, 14],[27, 28, 29, 30, 31]])
Y: tensor([[11, 12, 13, 14, 15],[28, 29, 30, 31, 32]])

  将上面的两个采样函数包装到一个类中

class 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)

  定义一个函数load_data_time_machine,它同时返回数据迭代器和词表。

def load_data_time_machine(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

小结

  • 语言模型是自然语言处理的关键。
  • n元语法通过截断相关性,为处理长序列提供了一种实用的模型。
  • 齐普夫定律支配着单词的分布,这个分布不仅适用于一元语法,还适用于其他n元语法。
  • 通过拉普拉斯平滑法可以有效地处理结构丰富而频率不足的低频词词组
  • 读取长序列的主要方式是随机采样顺序分区。在迭代过程中,后者可以保证来自两个相邻的小批量中的子序列在原始序列上也是相邻的。

8.4-8.7为最初循环神经网络的实现过程,算是基础和发展历程,有助于理解下一章9.现代循环神经网络。不过其中的方法大多被弃用或者被优化为现代循环神经网络了,所以这里就不再更新这部分内容了,可以直接看下一章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/630386.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue3中动态组件使用

一&#xff0c;动态组件使用&#xff1a; 应用场景&#xff1a;动态绑定或切换组件 应用Vue3碎片&#xff1a; is 1.使用 a.组件A <div class"layout-base"><Button>红茶</Button> </div>a.组件B <div class"layout-base"&g…

【MATLAB】SVMD_LSTM神经网络时序预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 SVMD-LSTM神经网络时序预测算法是一种结合了单变量经验模态分解&#xff08;Singular Value Decomposition&#xff0c;SVD&#xff09;和长短期记忆神经网络&#xff08;LSTM&#xff09…

第二百七十二回

文章目录 1. 概念介绍2. 方法与类型2.1 使用方法2.2 常见类型 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何加载本地图片"相关的内容&#xff0c;本章回中将介绍如何获取文件类型.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回…

23.实战演练--个人主页

<?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"><applicationandroid:allowBackup"true"an…

【项目实战】Postgresql数据库中出现锁表如何解决

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 系列专栏目录 [Java项目…

Unity之触发器

目录 &#x1f4d5;一、触发器概念 &#x1f4d5;二、碰撞与触发的区别 &#x1f4d5;三、触发器小实例 一、触发器概念 第一次玩侠盗猎车手是在小学&#xff0c;从那以后就开启了我的五星好市民之路。 下面是小编在小破站截的图&#xff0c;这是罪恶都市最开始的地方&a…

MCU和MPU有什么区别

大家好&#xff0c;今天给大家介绍MCU和MPU有什么区别&#xff0c;文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 MCU&#xff08;Microcontroller Unit&#xff09;和MPU&#xff08;Micro…

蓝桥杯备赛 day 2 —— 二分算法(C/C++,零基础,配图)

目录 &#x1f308;前言&#xff1a; &#x1f4c1; 二分的概念 &#x1f4c1; 整数二分 &#x1f4c1; 二分的模板 &#x1f4c1; 习题 &#x1f4c1; 总结 &#x1f308;前言&#xff1a; 这篇文章主要是准备蓝桥杯竞赛同学所写&#xff0c;为你更好准备蓝桥杯比赛涉及…

从0到1:实验室设备借用小程序开发笔记

概论 实验室设备借用小程序&#xff0c;适合各大高校&#xff0c;科技园区&#xff0c;大型企业集团的实验室设备借用流程, 通过数字化的手段进一步提升相关单位设备保障水平&#xff0c;规范实验室和设备管理&#xff0c;用户通过手机小程序扫描设备的二维码&#xff0c;可以…

深入解析:如何使用Java、SpringBoot、Vue.js和MySQL构建课表管理系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

VS代码生成工具ReSharper v2023.3正式发布——支持C# 12

实质上&#xff0c;ReSharper特征可用于C#&#xff0c;VB.net&#xff0c;XML&#xff0c;Asp.net&#xff0c;XAML&#xff0c;和构建脚本。 使用ReSharper&#xff0c;你可以进行深度代码分析&#xff0c;智能代码协助&#xff0c;实时错误代码高亮显示&#xff0c;解决方案范…

JavaScript基础(27)_内联样式的获取和修改、获取元素当前显示的样式

内联样式的获取和修改 获取元素的内联样式&#xff1a; 语法&#xff1a;元素.style.样式名 注意&#xff1a;通过style属性设置和读取的都是内联样式&#xff0c;无法读取样式表中的样式。 修改元素的内联样式&#xff1a; 语法&#xff1a;元素.style.样式名 样式值比如…

并发编程之深入理解AQS

目录 什么是AQS&#xff1f; AQS的特性 AQS总结 什么是AQS&#xff1f; java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为&#xff0c;比如等待队列、条件队列、独占获取、共享获取等&#xff0c;而这些行为的抽象就是基于AbstractQueuedSynchronizer&a…

WordPress后台仪表盘自定义添加删除概览项目插件Glance That

成功搭建WordPress站点&#xff0c;登录后台后可以在“仪表盘 – 概览”中看到包括多少篇文章、多少个页面、多少条评论和当前WordPress版本号及所使用的主题。具体如下图所示&#xff1a; 但是如果我们的WordPress站点还有自定义文章类型&#xff0c;也想在概览中显示出来应该…

【经验分享】美赛报名以及注册方法-以2024年美赛为例

1 进入美赛官网 首先点击COMAP的官网链接&#xff1a; https://www.comap.com/然后选择Contests目录下的MCM/ICM 选择 Learn More and Register 然后选择 register for contest 接下来开始注册环节&#xff0c;注册分为两个步骤&#xff1a;顾问&#xff08;指导教师&#xf…

Jsqlparser简单学习

文章目录 学习链接模块访问者模式parser模块statement模块Expression模块deparser模块 测试TestDropTestSelectTestSelectVisitor 学习链接 java设计模式&#xff1a;访问者模式 github使用示例参考 测试 JSqlParser使用示例 JSqlParse&#xff08;一&#xff09;基本增删改…

GitHub API使用--获取GitHub topic

目录标题 技术简介申请token简单使用使用Java调用获取GitHub topic总结 技术简介 GitHub API是一个功能强大的工具&#xff0c;为开发者提供了访问和操作GitHub平台上资源的途径。无论是构建个人工具&#xff0c;集成自动化流程&#xff0c;还是开发应用程序&#xff0c;GitHu…

【Git】任何位置查看git日志

需求 现需要查看指定项目中的某个文件的 Git 日志。如有 项目代码 jflowable &#xff0c;需要查看其下文件 D:\z_workspace\jflowable\src\main\java\com\xzbd\jflowable\controller\TestController.java 的日志。 分析 一般的思路是&#xff0c;进入 jflowable 项目&#…

ES 之索引和文档

本文主要介绍ES中的数据组成结构单元。 一、文档(Document) 1、概念 ES的数据存储单元是面向文档的&#xff0c;文档是所有数据存储&#xff0c;搜索的最小单元。 你可以把ES中的文档对应成mysql中的一条条数据记录。到时候你存进ES的数据就是一个个文档。 文档存入ES是序列…

BRC20通证的诞生与未来展望!如何导入bitget教程

BRC-20通证是什么&#xff1f; 嘿&#xff01;你知道BRC-20通证吗&#xff1f;这可是比特币区块链上的超级明星&#xff01;它们不依赖智能合约&#xff0c;而是把JSON代码刻在聪上&#xff0c;聪可是比特币的最小单位哦&#xff01;就像在比特币的乐高积木上盖房子&#xff0…