未验证 提交 af7c00d4 编写于 作者: G goldmermaid 提交者: GitHub

Merge pull request #1010 from goldmermaid/polishch8

[polish] chapter 8 peer review
......@@ -50,13 +50,6 @@
我们可以最大限度地减少在设备之间传输数据的时间。
例如,当在带有GPU的服务器上训练神经网络时,
我们通常希望模型的参数在GPU上。
接下来,我们需要确认安装了GPU版本的PyTorch。
如果已经安装了PyTorch的CPU版本,我们需要先卸载它。
例如,使用`pip uninstall torch`命令,
然后根据你的CUDA版本安装相应的PyTorch版本。
例如,假设你安装了CUDA10.0,你可以通过`pip install torch-cu100`
安装支持CUDA10.0的PyTorch版本。
:end_tab:
要运行此部分中的程序,至少需要两个GPU。
......
# 循环神经网络
:label:`chap_rnn`
到目前为止,我们遇到了两种类型的数据:表格数据和图像数据。
对于后者,我们设计了专门的神经网络架构来利用数据的规律。
换句话说,如果我们拥有一张图像,图像中的内容看起来就像模拟电视时代的测试图,那么对图像中的像素位置进行重排,就会对图像中的内容推断造成极大的困难。
到目前为止,我们遇到过两种类型的数据:表格数据和图像数据。
对于图像数据,我们设计了专门的卷积神经网络架构来为这类特殊的数据结构建模。
换句话说,如果我们拥有一张图像,我们需要有效地利用其像素位置,
假若我们对图像中的像素位置进行重排,就会对图像中内容的推断造成极大的困难。
最重要的是,到目前为止我们默认数据都来自于某种分布,并且所有样本都是独立同分布的(independently and identically distributed,i.i.d.)。
最重要的是,到目前为止我们默认数据都来自于某种分布,
并且所有样本都是独立同分布的
(independently and identically distributed,i.i.d.)。
然而,大多数的数据并非如此。
例如,文章中的单词是按顺序写的,如果顺序被随机地重排,就很难理解文章原始的意思。
同样,视频中的图像帧、对话中的音频信号以及网站上的浏览行为都是有顺序的。
因此,针对此类数据而设计特定模型,可能效果会更好。
另一个问题来自这样一个事实:我们不仅仅可以接收一个序列作为输入,而是还可能期望继续猜测这个序列的后续。
另一个问题来自这样一个事实:
我们不仅仅可以接收一个序列作为输入,而是还可能期望继续猜测这个序列的后续。
例如,一个任务可以是继续预测$2, 4, 6, 8, 10, \ldots$。
这在时间序列分析中是相当常见的,可以用来预测股市的波动、患者的体温曲线或者赛车所需的加速度。
这在时间序列分析中是相当常见的,可以用来预测股市的波动、
患者的体温曲线或者赛车所需的加速度。
同理,我们需要能够处理这些数据的特定模型。
简言之,卷积神经网络可以有效地处理空间信息,*循环神经网络*(recurrent neural network,RNN)这种设计可以更好地处理序列信息。
简言之,如果说卷积神经网络可以有效地处理空间信息,
那么本章的*循环神经网络*(recurrent neural network,RNN)则可以更好地处理序列信息。
循环神经网络通过引入状态变量存储过去的信息和当前的输入,从而可以确定当前的输出。
许多使用循环网络的例子都是基于文本数据的,因此我们将在本章中重点介绍语言模型。
......
# 循环神经网络的简洁实现
:label:`sec_rnn-concise`
虽然 :numref:`sec_rnn_scratch`对了解循环神经网络的实现方式具有指导意义,但并不方便。本节将展示如何使用深度学习框架的高级API提供的函数更有效地实现相同的语言模型。我们仍然从读取时光机器数据集开始。
虽然 :numref:`sec_rnn_scratch`
对了解循环神经网络的实现方式具有指导意义,但并不方便。
本节将展示如何使用深度学习框架的高级API提供的函数更有效地实现相同的语言模型。
我们仍然从读取时光机器数据集开始。
```{.python .input}
from d2l import mxnet as d2l
......@@ -34,7 +37,10 @@ train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
## [**定义模型**]
高级API提供了循环神经网络的实现。我们构造了一个具有256个隐藏单元的单隐藏层的循环神经网络层`rnn_layer`。事实上,我们还没有讨论多层的意义——这将在 :numref:`sec_deep_rnn`中介绍。现在,仅需要将多层理解为一层循环神经网络的输出被用作下一层循环神经网络的输入就足够了。
高级API提供了循环神经网络的实现。
我们构造一个具有256个隐藏单元的单隐藏层的循环神经网络层`rnn_layer`
事实上,我们还没有讨论多层循环神经网络的意义(这将在 :numref:`sec_deep_rnn`中介绍)。
现在,你仅需要将多层理解为一层循环神经网络的输出被用作下一层循环神经网络的输入就足够了。
```{.python .input}
num_hiddens = 256
......@@ -58,7 +64,10 @@ rnn_layer = tf.keras.layers.RNN(rnn_cell, time_major=True,
```
:begin_tab:`mxnet`
初始化隐状态是简单的,只需要调用成员函数`begin_state`即可。函数将返回一个列表(`state`),列表中包含了初始隐状态用于小批量数据中的每个样本,其形状为(隐藏层数,批量大小,隐藏单元数)。对于以后要介绍的一些模型(例如长-短期记忆网络),这样的列表还会包含其他信息。
初始化隐状态是简单的,只需要调用成员函数`begin_state`即可。
函数将返回一个列表(`state`),列表中包含了初始隐状态用于小批量数据中的每个样本,
其形状为(隐藏层数,批量大小,隐藏单元数)。
对于以后要介绍的一些模型(例如长-短期记忆网络),这样的列表还会包含其他信息。
:end_tab:
:begin_tab:`pytorch`
......@@ -82,10 +91,16 @@ state = rnn_cell.get_initial_state(batch_size=batch_size, dtype=tf.float32)
state.shape
```
[**通过一个隐状态和一个输入,我们就可以用更新后的隐状态计算输出。**]需要强调的是,`rnn_layer`的“输出”(`Y`)不涉及输出层的计算:它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。
[**通过一个隐状态和一个输入,我们就可以用更新后的隐状态计算输出。**]
需要强调的是,`rnn_layer`的“输出”(`Y`)不涉及输出层的计算:
它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。
:begin_tab:`mxnet`
此外,`rnn_layer`返回的更新后的隐状态(`state_new`)是指小批量数据的最后时间步的隐状态。这个隐状态可以用来初始化顺序分区中一个迭代周期内下一个小批量数据的隐状态。对于多个隐藏层,每一层的隐状态将存储在(`state_new`)变量中。至于稍后要介绍的某些模型(例如,长-短期记忆),此变量还包含其他信息。
此外,`rnn_layer`返回的更新后的隐状态(`state_new`
是指小批量数据的最后时间步的隐状态。
这个隐状态可以用来初始化顺序分区中一个迭代周期内下一个小批量数据的隐状态。
对于多个隐藏层,每一层的隐状态将存储在(`state_new`)变量中。
至于稍后要介绍的某些模型(例如,长-短期记忆),此变量还包含其他信息。
:end_tab:
```{.python .input}
......@@ -108,7 +123,9 @@ Y, state_new = rnn_layer(X, state)
Y.shape, len(state_new), state_new[0].shape
```
与 :numref:`sec_rnn_scratch`类似,[**我们为一个完整的循环神经网络模型定义了一个`RNNModel`类**]。注意,`rnn_layer`只包含隐藏的循环层,我们还需要创建一个单独的输出层。
与 :numref:`sec_rnn_scratch`类似,
[**我们为一个完整的循环神经网络模型定义了一个`RNNModel`类**]。
注意,`rnn_layer`只包含隐藏的循环层,我们还需要创建一个单独的输出层。
```{.python .input}
#@save
......@@ -123,8 +140,8 @@ class RNNModel(nn.Block):
def forward(self, inputs, state):
X = npx.one_hot(inputs.T, self.vocab_size)
Y, state = self.rnn(X, state)
# 全连接层首先将`Y`的形状改为(`时间步数`*`批量大小`, `隐藏单元数`)
# 它的输出形状是 (`时间步数`*`批量大小`, `词表大小`)
# 全连接层首先将`Y`的形状改为(`时间步数`*`批量大小`, `隐藏单元数`)
# 它的输出形状是 (`时间步数`*`批量大小`, `词表大小`)
output = self.dense(Y.reshape(-1, Y.shape[-1]))
return output, state
......@@ -142,7 +159,7 @@ class RNNModel(nn.Module):
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 如果RNN是双向的(之后将介绍),`num_directions`应该是2,否则应该是1
# 如果RNN是双向的(之后将介绍),`num_directions`应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
......@@ -154,7 +171,7 @@ class RNNModel(nn.Module):
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
Y, state = self.rnn(X, state)
# 全连接层首先将`Y`的形状改为(`时间步数`*`批量大小`, `隐藏单元数`)
# 全连接层首先将`Y`的形状改为(`时间步数`*`批量大小`, `隐藏单元数`)
# 它的输出形状是 (`时间步数`*`批量大小`, `词表大小`)。
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state
......@@ -187,7 +204,7 @@ class RNNModel(tf.keras.layers.Layer):
def call(self, inputs, state):
X = tf.one_hot(tf.transpose(inputs), self.vocab_size)
# Later RNN like `tf.keras.layers.LSTMCell` return more than two values
# rnn返回两个以上的值
Y, *state = self.rnn(X, state)
output = self.dense(tf.reshape(Y, (-1, Y.shape[-1])))
return output, state
......@@ -225,7 +242,9 @@ with strategy.scope():
d2l.predict_ch8('time traveller', 10, net, vocab)
```
很明显,这种模型根本不能输出好的结果。接下来,我们使用 :numref:`sec_rnn_scratch`中定义的超参数调用`train_ch8`,并且[**使用高级API训练模型**]。
很明显,这种模型根本不能输出好的结果。
接下来,我们使用 :numref:`sec_rnn_scratch`
定义的超参数调用`train_ch8`,并且[**使用高级API训练模型**]。
```{.python .input}
num_epochs, lr = 500, 1
......@@ -244,19 +263,20 @@ num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, strategy)
```
与上一节相比,由于深度学习框架的高级API对代码进行了更多的优化,该模型在较短的时间内实现了较低的困惑度。
与上一节相比,由于深度学习框架的高级API对代码进行了更多的优化,
该模型在较短的时间内达到了较低的困惑度。
## 小结
* 深度学习框架的高级API提供了循环神经网络层的实现。
* 高级API的循环神经网络层返回一个输出和一个更新后的隐状态,这个输出不涉及输出层的计算
* 使用高级API实现的循环神经网络相比从零开始实现可以得到更快的训练速度
* 高级API的循环神经网络层返回一个输出和一个更新后的隐状态,我们还需要计算整个模型的输出层
* 相比从零开始实现的循环神经网络,使用高级API实现可以加速训练
## 练习
1. 你能使用高级API使循环神经网络模型过拟合吗?
1. 尝试使用高级API,你能使循环神经网络模型过拟合吗?
1. 如果在循环神经网络模型中增加隐藏层的数量会发生什么?你能使模型正常工作吗?
1. 使用循环神经网络实现 :numref:`sec_sequence`的自回归模型。
1. 尝试使用循环神经网络实现 :numref:`sec_sequence`的自回归模型。
:begin_tab:`mxnet`
[Discussions](https://discuss.d2l.ai/t/2105)
......
# 文本预处理
:label:`sec_text_preprocessing`
对于序列数据处理问题,我们回顾和评估了使用的统计工具和预测时面临的挑战。
对于序列数据处理问题,我们在 :numref:`sec_sequence`
评估了所需的统计工具和预测时面临的挑战。
这样的数据存在许多种形式,文本是最常见例子之一。
例如,一篇文章可以简单地看作是一串单词序列,甚至是一串字符序列。
本节中,我们将专门解释文本的常见预处理步骤。
本节中,我们将解析文本的常见预处理步骤。
这些步骤通常包括:
1. 将文本作为字符串加载到内存中。
......@@ -35,7 +36,8 @@ import re
## 读取数据集
首先,我们从H.G.Well的[时光机器](https://www.gutenberg.org/ebooks/35)中加载文本。
这是一个相当小的语料库,只有30000多个单词,但足够我们小试牛刀,而现实中的文档集合可能会包含数十亿个单词。
这是一个相当小的语料库,只有30000多个单词,但足够我们小试牛刀,
而现实中的文档集合可能会包含数十亿个单词。
下面的函数(**将数据集读取到由多条文本行组成的列表中**),其中每条文本行都是一个字符串。
为简单起见,我们在这里忽略了标点符号和字母大写。
......@@ -46,27 +48,28 @@ d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
'090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine(): #@save
"""Load the time machine dataset into a list of text lines."""
"""将时间机器数据集加载到文本行的列表中"""
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'# text lines: {len(lines)}')
print(f'# 文本总行数: {len(lines)}')
print(lines[0])
print(lines[10])
```
## 词元化
下面的`tokenize`函数将文本行列表(`lines`)作为输入,列表中的每个元素是一个文本序列(如一条文本行)。
下面的`tokenize`函数将文本行列表(`lines`)作为输入,
列表中的每个元素是一个文本序列(如一条文本行)。
[**每个文本序列又被拆分成一个词元列表**],*词元*(token)是文本的基本单位。
最后,返回一个由词元列表组成的列表,其中的每个词元都是一个字符串(string)。
```{.python .input}
#@tab all
def tokenize(lines, token='word'): #@save
"""将文本行拆分为单词或字符词元"""
"""将文本行拆分为单词或字符词元"""
if token == 'word':
return [line.split() for line in lines]
elif token == 'char':
......@@ -82,12 +85,17 @@ for i in range(11):
## 词表
词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。
现在,让我们[**构建一个字典,通常也叫做*词表*(vocabulary),用来将字符串类型的词元映射到从$0$开始的数字索引中**]。
我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计,得到的统计结果称之为*语料*(corpus)。
现在,让我们[**构建一个字典,通常也叫做*词表*(vocabulary),
用来将字符串类型的词元映射到从$0$开始的数字索引中**]。
我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计,
得到的统计结果称之为*语料*(corpus)。
然后根据每个唯一词元的出现频率,为其分配一个数字索引。
很少出现的词元通常被移除,这可以降低复杂性。
另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“<unk>”。
我们可以选择增加一个列表,用于保存那些被保留的词元,例如:填充词元(“<pad>”);序列开始词元(“<bos>”);序列结束词元(“<eos>”)。
我们可以选择增加一个列表,用于保存那些被保留的词元,
例如:填充词元(“<pad>”);
序列开始词元(“<bos>”);
序列结束词元(“<eos>”)。
```{.python .input}
#@tab all
......@@ -128,18 +136,18 @@ class Vocab: #@save
return [self.idx_to_token[index] for index in indices]
@property
def unk(self): # Index for the unknown token
def unk(self): # 未知词元的索引为0
return 0
@property
def token_freqs(self): # Index for the unknown token
def token_freqs(self):
return self._token_freqs
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)
```
......@@ -157,21 +165,23 @@ print(list(vocab.token_to_idx.items())[:10])
```{.python .input}
#@tab all
for i in [0, 10]:
print('words:', tokens[i])
print('indices:', vocab[tokens[i]])
print('文本:', tokens[i])
print('索引:', vocab[tokens[i]])
```
## 整合所有功能
在使用上述函数时,我们[**将所有功能打包到`load_corpus_time_machine`函数中**],该函数返回`corpus`(词元索引列表)和`vocab`(时光机器语料库的词表)。
在使用上述函数时,我们[**将所有功能打包到`load_corpus_time_machine`函数中**],
该函数返回`corpus`(词元索引列表)和`vocab`(时光机器语料库的词表)。
我们在这里所做的改变是:
1. 为了简化后面章节中的训练,我们使用字符(而不是单词)实现文本词元化;
2. 时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的`corpus`仅处理为单个列表,而不是使用多个词元列表构成的一个列表。
1. 时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的`corpus`仅处理为单个列表,而不是使用多词元列表构成的一个列表。
```{.python .input}
#@tab all
def load_corpus_time_machine(max_tokens=-1): #@save
"""返回时光机器数据集的词元索引列表和词表"""
"""返回时光机器数据集的词元索引列表和词表"""
lines = read_time_machine()
tokens = tokenize(lines, 'char')
vocab = Vocab(tokens)
......@@ -188,12 +198,12 @@ len(corpus), len(vocab)
## 小结
* 文本是序列数据的一种重要形式
* 文本是序列数据的一种最常见的形式之一
* 为了对文本进行预处理,我们通常将文本拆分为词元,构建词表将词元字符串映射为数字索引,并将文本数据转换为词元索引以供模型操作。
## 练习
1. 词元化是一个关键的预处理步骤,它因语言而异尝试找到另外三种常用的词元化文本的方法。
1. 词元化是一个关键的预处理步骤,它因语言而异尝试找到另外三种常用的词元化文本的方法。
1. 在本节的实验中,将文本词元为单词和更改`Vocab`实例的`min_freq`参数。这对词表大小有何影响?
:begin_tab:`mxnet`
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册