From c1326b1029736deb62ede313a4f8925167223559 Mon Sep 17 00:00:00 2001 From: goldmermaid Date: Mon, 19 Jul 2021 00:02:21 -0700 Subject: [PATCH] [slides] attention (#906) * [slides] attention * polish --- .../attention-scoring-functions.md | 16 ++++----- .../bahdanau-attention.md | 12 +++---- .../multihead-attention.md | 6 ++-- .../nadaraya-waston.md | 22 ++++++------ .../self-attention-and-positional-encoding.md | 10 +++--- chapter_attention-mechanisms/transformer.md | 34 +++++++++---------- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/chapter_attention-mechanisms/attention-scoring-functions.md b/chapter_attention-mechanisms/attention-scoring-functions.md index 64e336254..26f095d04 100644 --- a/chapter_attention-mechanisms/attention-scoring-functions.md +++ b/chapter_attention-mechanisms/attention-scoring-functions.md @@ -36,7 +36,7 @@ import torch from torch import nn ``` -## 遮蔽 softmax 操作 +## [**遮蔽softmax操作**] 正如上面提到的,softmax 运算用于输出一个概率分布作为注意力权重。在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。例如,为了在 :numref:`sec_machine_translation` 中高效处理小批量数据集,某些文本序列被填充了没有意义的特殊标记。为了仅将有意义的标记作为值去获取注意力汇聚,可以指定一个有效序列长度(即标记的个数),以便在计算 softmax 时过滤掉超出指定范围的位置。通过这种方式,我们可以在下面的 `masked_softmax` 函数中实现这样的 *遮蔽 softmax 操作*(masked softmax operation),其中任何超出有效长度的位置都被遮蔽并置为0。 @@ -79,7 +79,7 @@ def masked_softmax(X, valid_lens): return nn.functional.softmax(X.reshape(shape), dim=-1) ``` -为了演示此函数是如何工作的,考虑由两个 $2 \times 4$ 矩阵表示的样本,这两个样本的有效长度分别为 $2$ 和 $3$。经过遮蔽 softmax 操作,超出有效长度的值都被遮蔽为0。 +为了[**演示此函数是如何工作**]的,考虑由两个 $2 \times 4$ 矩阵表示的样本,这两个样本的有效长度分别为 $2$ 和 $3$。经过遮蔽 softmax 操作,超出有效长度的值都被遮蔽为0。 ```{.python .input} masked_softmax(np.random.uniform(size=(2, 2, 4)), d2l.tensor([2, 3])) @@ -102,7 +102,7 @@ masked_softmax(np.random.uniform(size=(2, 2, 4)), masked_softmax(torch.rand(2, 2, 4), d2l.tensor([[1, 3], [2, 4]])) ``` -## 加性注意力 +## [**加性注意力**] :label:`subsec_additive-attention` 一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为打分函数。给定查询 $\mathbf{q} \in \mathbb{R}^q$ 和键 $\mathbf{k} \in \mathbb{R}^k$,*加性注意力*(additive attention) 的打分函数为 @@ -168,7 +168,7 @@ class AdditiveAttention(nn.Module): return torch.bmm(self.dropout(self.attention_weights), values) ``` -让我们用一个小子来演示上面的 `AdditiveAttention` 类,其中查询、键和值的形状为(批量大小、步数或标记序列长度、特征大小),实际输出为 $(2,1,20)$、$(2,10,2)$ 和 $(2,10,4)$。注意力汇聚输出的形状为(批量大小、查询的步数、值的维度)。 +让我们用一个小例子来[**演示上面的`AdditiveAttention`类**],其中查询、键和值的形状为(批量大小、步数或标记序列长度、特征大小),实际输出为 $(2,1,20)$、$(2,10,2)$ 和 $(2,10,4)$。注意力汇聚输出的形状为(批量大小、查询的步数、值的维度)。 ```{.python .input} queries, keys = d2l.normal(0, 1, (2, 1, 20)), d2l.ones((2, 10, 2)) @@ -195,7 +195,7 @@ attention.eval() attention(queries, keys, values, valid_lens) ``` -尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的,所以注意力权重是均匀的,由指定的有效长度决定。 +尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的,所以[**注意力权重**]是均匀的,由指定的有效长度决定。 ```{.python .input} #@tab all @@ -203,7 +203,7 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), xlabel='Keys', ylabel='Queries') ``` -## 缩放点积注意力 +## [**缩放点积注意力**] 使用点积可以得到计算效率更高的打分函数。但是点积操作要求查询和键具有相同的长度 $d$。假设查询和键的所有元素都是独立的随机变量,并且都满足均值为 $0$ 和方差为 $1$。那么两个向量的点积的均值为 $0$,方差为 $d$。为确保无论向量长度如何,点积的方差在不考虑向量长度的情况下仍然是 $1$,则可以使用 *缩放点积注意力*(scaled dot-product attention) 打分函数: @@ -257,7 +257,7 @@ class DotProductAttention(nn.Module): return torch.bmm(self.dropout(self.attention_weights), values) ``` -为了演示上述的 `DotProductAttention` 类,我们使用了与先前加性注意力例子中相同的键、值和有效长度。对于点积操作,令查询的特征维度与键的特征维度大小相同。 +为了[**演示上述的`DotProductAttention`类**],我们使用了与先前加性注意力例子中相同的键、值和有效长度。对于点积操作,令查询的特征维度与键的特征维度大小相同。 ```{.python .input} queries = d2l.normal(0, 1, (2, 1, 2)) @@ -274,7 +274,7 @@ attention.eval() attention(queries, keys, values, valid_lens) ``` -与加性注意力演示相同,由于键包含的是相同的元素,而这些元素无法通过任何查询进行区分,因此获得了均匀的注意力权重。 +与加性注意力演示相同,由于键包含的是相同的元素,而这些元素无法通过任何查询进行区分,因此获得了[**均匀的注意力权重**]。 ```{.python .input} #@tab all diff --git a/chapter_attention-mechanisms/bahdanau-attention.md b/chapter_attention-mechanisms/bahdanau-attention.md index 149c1ac81..11dab7024 100644 --- a/chapter_attention-mechanisms/bahdanau-attention.md +++ b/chapter_attention-mechanisms/bahdanau-attention.md @@ -34,7 +34,7 @@ from torch import nn ## 定义注意力解码器 -要用 Bahdanau 注意力实现循环神经网络编码器-解码器,我们只需重新定义解码器即可。为了更方便地显示学习的注意力权重,以下 `AttentionDecoder` 类定义了带有注意力机制的解码器基本接口。 +要用 Bahdanau 注意力实现循环神经网络编码器-解码器,我们只需重新定义解码器即可。为了更方便地显示学习的注意力权重,以下 `AttentionDecoder` 类定义了[**带有注意力机制的解码器基本接口**]。 ```{.python .input} #@tab all @@ -49,7 +49,7 @@ class AttentionDecoder(d2l.Decoder): raise NotImplementedError ``` -接下来,让我们在接下来的 `Seq2SeqAttentionDecoder` 类中实现带有 Bahdanau 注意力的循环神经网络解码器。初始化解码器的状态 (1)编码器在所有时间步的最终层隐藏状态(作为注意力的键和值);(2)最后一个时间步的编码器全层隐藏状态(初始化解码器的隐藏状态);(3)编码器有效长度(排除在注意力池中填充标记)。在每个解码时间步骤中,解码器上一个时间步的最终层隐藏状态将用作关注的查询。因此,注意力输出和输入嵌入都连接为循环神经网络解码器的输入。 +接下来,让我们在接下来的 `Seq2SeqAttentionDecoder` 类中[**实现带有Bahdanau注意力的循环神经网络解码器**]。初始化解码器的状态 (1)编码器在所有时间步的最终层隐藏状态(作为注意力的键和值);(2)最后一个时间步的编码器全层隐藏状态(初始化解码器的隐藏状态);(3)编码器有效长度(排除在注意力池中填充标记)。在每个解码时间步骤中,解码器上一个时间步的最终层隐藏状态将用作关注的查询。因此,注意力输出和输入嵌入都连接为循环神经网络解码器的输入。 ```{.python .input} class Seq2SeqAttentionDecoder(AttentionDecoder): @@ -151,7 +151,7 @@ class Seq2SeqAttentionDecoder(AttentionDecoder): return self._attention_weights ``` -接下来,我们使用包含 7 个时间步的 4 个序列输入的小批量测试我们实现的 Bahdanau 注意力解码器。 +接下来,我们使用包含 7 个时间步的 4 个序列输入的小批量[**测试Bahdanau 注意力解码器**]。 ```{.python .input} encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, @@ -180,7 +180,7 @@ output, state = decoder(X, state) output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape ``` -## 训练 +## [**训练**] 与 :numref:`sec_seq2seq_training` 类似,我们在这里指定超参数,实例化一个带有 Bahdanau 注意力的编码器和解码器,并对这个模型进行机器翻译训练。由于新增的注意力机制,这项训练要比没有注意力机制的 :numref:`sec_seq2seq_training` 慢得多。 @@ -199,7 +199,7 @@ net = d2l.EncoderDecoder(encoder, decoder) d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ``` -模型训练后,我们用它将几个英语句子翻译成法语并计算它们的 BLEU 分数。 +模型训练后,我们用它[**将几个英语句子翻译成法语**]并计算它们的 BLEU 分数。 ```{.python .input} #@tab all @@ -219,7 +219,7 @@ attention_weights = d2l.reshape( (1, 1, -1, num_steps)) ``` -训练结束后通过可视化注意力权重,我们可以看到,每个查询都会在键值对上分配不同的权重。它显示,在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。 +训练结束后通过[**可视化注意力权重**],我们可以看到,每个查询都会在键值对上分配不同的权重。它显示,在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。 ```{.python .input} # 加上一个包含序列结束标记 diff --git a/chapter_attention-mechanisms/multihead-attention.md b/chapter_attention-mechanisms/multihead-attention.md index 7d31f8280..672b7c9da 100644 --- a/chapter_attention-mechanisms/multihead-attention.md +++ b/chapter_attention-mechanisms/multihead-attention.md @@ -38,7 +38,7 @@ from torch import nn ## 实现 -在实现过程中,我们选择了缩放点积注意力作为每一个注意力头。为了避免计算成本和参数数量的大幅增长,我们设定 $p_q = p_k = p_v = p_o / h$。值得注意的是,如果我们将查询、键和值的线性变换的输出数量设置为 $p_q h = p_k h = p_v h = p_o$,则可以并行计算 $h$ 个头。在下面的实现中,$p_o$ 是通过参数 `num_hiddens` 指定的。 +在实现过程中,我们[**选择缩放点积注意力作为每一个注意力头**]。为了避免计算成本和参数数量的大幅增长,我们设定 $p_q = p_k = p_v = p_o / h$。值得注意的是,如果我们将查询、键和值的线性变换的输出数量设置为 $p_q h = p_k h = p_v h = p_o$,则可以并行计算 $h$ 个头。在下面的实现中,$p_o$ 是通过参数 `num_hiddens` 指定的。 ```{.python .input} #@save @@ -120,7 +120,7 @@ class MultiHeadAttention(nn.Module): return self.W_o(output_concat) ``` -为了允许多个头的并行计算,上面的 `MultiHeadAttention` 类使用了下面定义的两个转置函数。具体来说,`transpose_output` 函数反转了 `transpose_qkv` 函数的操作。 +为了能够[**使多个头并行计算**],上面的 `MultiHeadAttention` 类使用了下面定义的两个转置函数。具体来说,`transpose_output` 函数反转了 `transpose_qkv` 函数的操作。 ```{.python .input} #@save @@ -173,7 +173,7 @@ def transpose_output(X, num_heads): return X.reshape(X.shape[0], X.shape[1], -1) ``` -让我们使用键和值相同的小例子来测试我们编写的 `MultiHeadAttention` 类。多头注意力输出的形状是 (`batch_size`, `num_queries`, `num_hiddens`)。 +让我们使用键和值相同的小例子来[**测试**]我们编写的 `MultiHeadAttention` 类。多头注意力输出的形状是 (`batch_size`, `num_queries`, `num_hiddens`)。 ```{.python .input} num_hiddens, num_heads = 100, 5 diff --git a/chapter_attention-mechanisms/nadaraya-waston.md b/chapter_attention-mechanisms/nadaraya-waston.md index 95d464dca..f36602f01 100644 --- a/chapter_attention-mechanisms/nadaraya-waston.md +++ b/chapter_attention-mechanisms/nadaraya-waston.md @@ -18,7 +18,7 @@ import torch from torch import nn ``` -## 生成数据集 +## [**生成数据集**] 简单起见,考虑下面这个回归问题:给定的成对的“输入-输出”数据集 $\{(x_1, y_1), \ldots, (x_n, y_n)\}$,如何学习 $f$ 来预测任意新输入 $x$ 的输出 $\hat{y} = f(x)$? @@ -81,7 +81,7 @@ y_hat = torch.repeat_interleave(y_train.mean(), n_test) plot_kernel_reg(y_hat) ``` -## 非参数的注意力汇聚 +## [**非参数注意力汇聚**] 显然,平均汇聚忽略了输入 $x_i$。于是 Nadaraya :cite:`Nadaraya.1964` 和 Waston :cite:`Watson.1964` 提出了一个更好的想法,根据输入的位置对输出 $y_i$ 进行加权: @@ -133,7 +133,7 @@ y_hat = d2l.matmul(attention_weights, y_train) plot_kernel_reg(y_hat) ``` -现在,让我们来观察注意力的权重。这里测试数据的输入相当于查询,而训练数据的输入相当于键。因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近,注意力汇聚的注意力权重就越高。 +现在,让我们来观察注意力的权重。这里测试数据的输入相当于查询,而训练数据的输入相当于键。因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近,注意力汇聚的[**注意力权重**]就越高。 ```{.python .input} d2l.show_heatmaps(np.expand_dims(np.expand_dims(attention_weights, 0), 0), @@ -148,7 +148,7 @@ d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), ylabel='Sorted testing inputs') ``` -## 带参数的注意力汇聚 +## [**带参数注意力汇聚**] 非参数的 Nadaraya-Watson 核回归具有 *一致性*(consistency) 的优点:如果有足够的数据,此模型会收敛到最优结果。尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。 @@ -165,7 +165,7 @@ $$\begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac 为了更有效地计算小批量数据的注意力,我们可以利用深度学习开发框架中提供的批量矩阵乘法。 -假设第一个小批量数据包含 $n$ 个矩阵 $\mathbf{X}_1,\ldots, \mathbf{X}_n$,形状为 $a\times b$,第二个小批量包含 $n$ 个矩阵 $\mathbf{Y}_1, \ldots, \mathbf{Y}_n$,形状为 $b\times c$。它们的批量矩阵乘法得到 $n$ 个矩阵 $\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n$,形状为 $a\times c$。因此,假定两个张量的形状分别是 $(n,a,b)$ 和 $(n,b,c)$ ,它们的批量矩阵乘法输出的形状为 $(n,a,c)$。 +假设第一个小批量数据包含 $n$ 个矩阵 $\mathbf{X}_1,\ldots, \mathbf{X}_n$,形状为 $a\times b$,第二个小批量包含 $n$ 个矩阵 $\mathbf{Y}_1, \ldots, \mathbf{Y}_n$,形状为 $b\times c$。它们的批量矩阵乘法得到 $n$ 个矩阵 $\mathbf{X}_1\mathbf{Y}_1, \ldots, \mathbf{X}_n\mathbf{Y}_n$,形状为 $a\times c$。因此,[**假定两个张量的形状分别是 $(n,a,b)$ 和 $(n,b,c)$ ,它们的批量矩阵乘法输出的形状为 $(n,a,c)$**]。 ```{.python .input} X = d2l.ones((2, 1, 4)) @@ -180,7 +180,7 @@ Y = d2l.ones((2, 4, 6)) torch.bmm(X, Y).shape ``` -在注意力机制的背景中,我们可以使用小批量矩阵乘法来计算小批量数据中值的加权平均。 +在注意力机制的背景中,我们可以[**使用小批量矩阵乘法来计算小批量数据中的加权平均值**]。 ```{.python .input} weights = d2l.ones((2, 10)) * 0.1 @@ -197,7 +197,7 @@ torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1)) ### 定义模型 -基于 :eqref:`eq_nadaraya-waston-gaussian-para` 中的带参数的注意力汇聚,使用小批量矩阵乘法,定义 Nadaraya-Watson 核回归的带参数版本为: +基于 :eqref:`eq_nadaraya-waston-gaussian-para` 中的[**带参数的注意力汇聚**],使用小批量矩阵乘法,定义 Nadaraya-Watson 核回归的带参数版本为: ```{.python .input} class NWKernelRegression(nn.Block): @@ -236,7 +236,7 @@ class NWKernelRegression(nn.Module): ### 训练模型 -接下来,将训练数据集转换为键和值用于训练注意力模型。在带参数的注意力汇聚模型中,任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算,从而得到其对应的预测输出。 +接下来,[**将训练数据集转换为键和值**]用于训练注意力模型。在带参数的注意力汇聚模型中,任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算,从而得到其对应的预测输出。 ```{.python .input} # `X_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输入 @@ -265,7 +265,7 @@ values = d2l.reshape(Y_tile[(1 - d2l.eye(n_train)).type(torch.bool)], (n_train, -1)) ``` -训练带参数的注意力汇聚模型时使用平方损失函数和随机梯度下降。 +[**训练带参数的注意力汇聚模型**]时使用平方损失函数和随机梯度下降。 ```{.python .input} net = NWKernelRegression() @@ -301,7 +301,7 @@ for epoch in range(5): animator.add(epoch + 1, float(l.sum())) ``` -训练完带参数的注意力汇聚模型后,我们发现,在尝试拟合带噪声的训练数据时,预测结果绘制的线不如之前非参数模型的线平滑。 +训练完带参数的注意力汇聚模型后,我们发现,在尝试拟合带噪声的训练数据时,[**预测结果绘制**]的线不如之前非参数模型的线平滑。 ```{.python .input} # `keys` 的形状: (`n_test`, `n_train`), 每一行包含着相同的训练输入(例如:相同的键) @@ -322,7 +322,7 @@ y_hat = net(x_test, keys, values).unsqueeze(1).detach() plot_kernel_reg(y_hat) ``` -与非参数的注意力汇聚模型相比,带参数的模型加入可学习的参数后,在输出结果的绘制图上,曲线在注意力权重较大的区域变得更不平滑。 +与非参数的注意力汇聚模型相比,带参数的模型加入可学习的参数后,在输出结果的绘制图上,[**曲线在注意力权重较大的区域变得更不平滑**]。 ```{.python .input} d2l.show_heatmaps(np.expand_dims(np.expand_dims(net.attention_weights, 0), 0), diff --git a/chapter_attention-mechanisms/self-attention-and-positional-encoding.md b/chapter_attention-mechanisms/self-attention-and-positional-encoding.md index 4c903cec3..7235293a8 100644 --- a/chapter_attention-mechanisms/self-attention-and-positional-encoding.md +++ b/chapter_attention-mechanisms/self-attention-and-positional-encoding.md @@ -20,7 +20,7 @@ import torch from torch import nn ``` -## 自注意力 +## [**自注意力**] 给定一个由标记组成的输入序列 $\mathbf{x}_1, \ldots, \mathbf{x}_n$,其中任意 $\mathbf{x}_i \in \mathbb{R}^d$ ($1 \leq i \leq n$)。该序列的自注意力输出为一个长度相同的序列 $\mathbf{y}_1, \ldots, \mathbf{y}_n$,其中: @@ -65,7 +65,7 @@ attention(X, X, X, valid_lens).shape 总而言之,卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。 -## 位置编码 +## [**位置编码**] :label:`subsec_positional-encoding` 在处理标记序列时,循环神经网络是逐个的重复地处理标记的,而自注意力则因为并行计算而放弃了顺序操作。为了使用序列的顺序信息,我们通过在输入表示中添加 *位置编码*(positional encoding)来注入绝对的或相对的位置信息。位置编码可以通过学习得到也可以直接固定得到。接下来,我们描述的是基于正弦函数和余弦函数的固定位置编码 :cite:`Vaswani.Shazeer.Parmar.ea.2017` 。 @@ -115,7 +115,7 @@ class PositionalEncoding(nn.Module): return self.dropout(X) ``` -在位置嵌入矩阵 $\mathbf{P}$ 中,行代表标记在序列中的位置,列代表位置编码的不同维度。在下面的例子中,我们可以看到位置嵌入矩阵的 第$6$列 和 第$7$列的频率高于 第$8$ 列和 第$9$ 列。第$6$列 和 第$7$列之间的偏移量(第$8$ 列和 第$9$ 列相同)是由于正弦函数和余弦函数的交替。 +在位置嵌入矩阵 $\mathbf{P}$ 中,[**行代表标记在序列中的位置,列代表位置编码的不同维度**]。在下面的例子中,我们可以看到位置嵌入矩阵的 第$6$列 和 第$7$列的频率高于 第$8$ 列和 第$9$ 列。第$6$列 和 第$7$列之间的偏移量(第$8$ 列和 第$9$ 列相同)是由于正弦函数和余弦函数的交替。 ```{.python .input} encoding_dim, num_steps = 32, 60 @@ -140,7 +140,7 @@ d2l.plot(d2l.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)', ### 绝对位置信息 -为了明白沿着编码维度单调降低的频率与绝对位置信息的关系,让我们打印出 $0, 1, \ldots, 7$ 的二进制表示形式。正如我们所看到的,每个数字、每两个数字和每四个数字上的比特值在第一个最低位、第二个最低位和第三个最低位上分别交替。 +为了明白沿着编码维度单调降低的频率与绝对位置信息的关系,让我们打印出 $0, 1, \ldots, 7$ 的[**二进制表示**]形式。正如我们所看到的,每个数字、每两个数字和每四个数字上的比特值在第一个最低位、第二个最低位和第三个最低位上分别交替。 ```{.python .input} #@tab all @@ -148,7 +148,7 @@ for i in range(8): print(f'{i} in binary is {i:>03b}') ``` -在二进制表示中,较高比特位的交替频率低于较低比特位,与下面的热图所示相似,只是位置编码通过使用三角函数在编码维度上降低频率。由于输出是浮点数,因此此类连续表示比二进制表示法更节省空间。 +在二进制表示中,较高比特位的交替频率低于较低比特位,与下面的热图所示相似,只是位置编码通过使用三角函数[**在编码维度上降低频率**]。由于输出是浮点数,因此此类连续表示比二进制表示法更节省空间。 ```{.python .input} P = np.expand_dims(np.expand_dims(P[0, :, :], 0), 0) diff --git a/chapter_attention-mechanisms/transformer.md b/chapter_attention-mechanisms/transformer.md index bc3cf776f..2327d7527 100644 --- a/chapter_attention-mechanisms/transformer.md +++ b/chapter_attention-mechanisms/transformer.md @@ -35,7 +35,7 @@ import torch from torch import nn ``` -## 基于位置的前馈网络 +## [**基于位置的前馈网络**] 基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是 *基于位置的*(positionwise)的原因。在下面的实现中,输入 `X` 的形状(批量大小、时间步数或序列长度、隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小、时间步数、`ffn_num_outputs`)的输出张量。 @@ -67,7 +67,7 @@ class PositionWiseFFN(nn.Module): return self.dense2(self.relu(self.dense1(X))) ``` -下面的例子显示,张量的最里层维度的尺寸会改变成基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。 +下面的例子显示,[**改变张量的最里层维度的尺寸**],会改变成基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。 ```{.python .input} ffn = PositionWiseFFN(4, 8) @@ -88,7 +88,7 @@ ffn(d2l.ones((2, 3, 4)))[0] 在 :numref:`sec_batch_norm` 中,我们解释了在一个小批量的样本内基于批量标准化对数据进行重新中心化和重新缩放的调整。层归一化和批量归一化的目标相同,但层归一化是基于特征维度进行归一化。尽管批量归一化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量归一化通常不如层归一化的效果好。 -以下代码段对比了不同维度的层归一化和批量归一化的归一化效果。 +以下代码[**对比不同维度的层归一化和批量归一化的效果**]。 ```{.python .input} ln = nn.LayerNorm() @@ -110,7 +110,7 @@ X = d2l.tensor([[1, 2], [2, 3]], dtype=torch.float32) print('layer norm:', ln(X), '\nbatch norm:', bn(X)) ``` -现在我们可以使用残差连接和层归一化来实现 `AddNorm` 类。Dropout 也被作为正则化方法使用。 +现在我们可以[**使用残差连接和层归一化**]来实现 `AddNorm` 类。Dropout 也被作为正则化方法使用。 ```{.python .input} #@save @@ -137,7 +137,7 @@ class AddNorm(nn.Module): return self.ln(self.dropout(Y) + X) ``` -残差连接要求两个输入的形状相同,以便在加法操作后输出张量的形状也相同。 +残差连接要求两个输入的形状相同,以便[**加法操作后输出张量的形状相同**]。 ```{.python .input} add_norm = AddNorm(0.5) @@ -154,7 +154,7 @@ add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape ## 编码器 -有了组成 Transformer 编码器的基础组件,现在可以先实现编码器中的一个层。下面的 `EncoderBlock` 类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层归一化。 +有了组成 Transformer 编码器的基础组件,现在可以先[**实现编码器中的一个层**]。下面的 `EncoderBlock` 类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层归一化。 ```{.python .input} #@save @@ -194,7 +194,7 @@ class EncoderBlock(nn.Module): return self.addnorm2(Y, self.ffn(Y)) ``` -正如我们所看到的,Transformer 编码器中的任何层都不会改变其输入的形状。 +正如我们所看到的,[**Transformer编码器中的任何层都不会改变其输入的形状**]。 ```{.python .input} X = d2l.ones((2, 100, 24)) @@ -213,7 +213,7 @@ encoder_blk.eval() encoder_blk(X, valid_lens).shape ``` -在实现下面的 Transformer 编码器的代码中,我们堆叠了 `num_layers` 个 `EncoderBlock` 类的实例。由于我们使用的是值范围在 $-1$ 和 $1$ 之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。 +在实现下面的[**Transformer编码器**]的代码中,我们堆叠了 `num_layers` 个 `EncoderBlock` 类的实例。由于我们使用的是值范围在 $-1$ 和 $1$ 之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。 ```{.python .input} #@save @@ -274,7 +274,7 @@ class TransformerEncoder(d2l.Encoder): return X ``` -下面我们指定了超参数来创建一个两层的 Transformer 编码器。Transformer 编码器输出的形状是(批量大小、时间步的数目、`num_hiddens`)。 +下面我们指定了超参数来[**创建一个两层的Transformer编码器**]。Transformer 编码器输出的形状是(批量大小、时间步的数目、`num_hiddens`)。 ```{.python .input} encoder = TransformerEncoder(200, 24, 48, 8, 2, 0.5) @@ -292,7 +292,7 @@ encoder(d2l.ones((2, 100), dtype=torch.long), valid_lens).shape ## 解码器 -如 :numref:`fig_transformer` 所示,Transformer 解码器也是由多个相同的层组成。在 `DecoderBlock` 类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层归一化围绕。 +如 :numref:`fig_transformer` 所示,[**Transformer解码器也是由多个相同的层组成**]。在 `DecoderBlock` 类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层归一化围绕。 正如在本节前面所述,在遮蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于 **序列到序列模型** (sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的标记都是已知的;然而,在预测阶段,其输出序列的标记是逐个生成的。因此,在任何解码器时间步中,只有生成的标记才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其遮蔽自注意力设定了参数 `dec_valid_lens`,以便任何查询都只会与解码器中所有已经生成标记的位置(即直到该查询位置为止)进行注意力计算。 @@ -392,7 +392,7 @@ class DecoderBlock(nn.Module): return self.addnorm3(Z, self.ffn(Z)), state ``` -为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,编码器和解码器的特征维度都是 `num_hiddens`。 +为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,[**编码器和解码器的特征维度都是`num_hiddens`。**] ```{.python .input} decoder_blk = DecoderBlock(24, 48, 8, 0.5, 0) @@ -411,7 +411,7 @@ state = [encoder_blk(X, valid_lens), valid_lens, [None]] decoder_blk(X, state)[0].shape ``` -现在我们构建了由 `num_layers` 个 `DecoderBlock` 实例组成的完整的 Transformer 解码器。最后,通过一个全连接层计算所有 `vocab_size` 个可能的输出标记的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。 +现在我们构建了由 `num_layers` 个 `DecoderBlock` 实例组成的完整的[**Transformer解码器**]。最后,通过一个全连接层计算所有 `vocab_size` 个可能的输出标记的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。 ```{.python .input} class TransformerDecoder(d2l.AttentionDecoder): @@ -490,7 +490,7 @@ class TransformerDecoder(d2l.AttentionDecoder): return self._attention_weights ``` -## 训练 +## [**训练**] 依照 Transformer 结构来实例化编码器-解码器模型。在这里,指定 Transformer 的编码器和解码器都是 2 层,都使用 4 头注意力。与 :numref:`sec_seq2seq_training` 类似,为了进行序列到序列的学习,我们在“英语-法语”机器翻译数据集上训练 Transformer 模型。 @@ -533,7 +533,7 @@ net = d2l.EncoderDecoder(encoder, decoder) d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device) ``` -训练结束后,使用 Transformer 模型将一些英语句子翻译成法语,并且计算它们的 BLEU 分数。 +训练结束后,使用 Transformer 模型[**将一些英语句子翻译成法语**],并且计算它们的 BLEU 分数。 ```{.python .input} #@tab all @@ -546,7 +546,7 @@ for eng, fra in zip(engs, fras): f'bleu {d2l.bleu(translation, fra, k=2):.3f}') ``` -当进行最后一个英语到法语的句子翻译工作时,让我们对 Transformer 的注意力权重进行可视化。编码器自注意力权重的形状为 (编码器层数, 注意力头数, `num_steps`或查询的数目, `num_steps` 或“键-值”对的数目) 。 +当进行最后一个英语到法语的句子翻译工作时,让我们[**可视化Transformer 的注意力权重**]。编码器自注意力权重的形状为 (编码器层数, 注意力头数, `num_steps`或查询的数目, `num_steps` 或“键-值”对的数目) 。 ```{.python .input} #@tab all @@ -572,7 +572,7 @@ d2l.show_heatmaps( figsize=(7, 3.5)) ``` -为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。例如,我们用零填充被遮蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以 *序列开始标记*(beginning-of-sequence, BOS)打头,再与后续输出的标记共同组成序列。 +[**为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。**]例如,我们用零填充被遮蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以 *序列开始标记*(beginning-of-sequence, BOS)打头,再与后续输出的标记共同组成序列。 ```{.python .input} dec_attention_weights_2d = [d2l.tensor(head[0]).tolist() @@ -612,7 +612,7 @@ d2l.show_heatmaps( titles=['Head %d' % i for i in range(1, 5)], figsize=(7, 3.5)) ``` -与编码器的自注意力的情况类似,通过指定输入序列的有效长度,输出序列的查询就不会与输入序列中填充位置的标记进行注意力计算。 +与编码器的自注意力的情况类似,通过指定输入序列的有效长度,[**输出序列的查询不会与输入序列中填充位置的标记进行注意力计算**]。 ```{.python .input} #@tab all