From 5b430683975186f055fb1954928cd0ca43830bf4 Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Mon, 15 Nov 2021 18:29:53 -0800 Subject: [PATCH 1/8] attention-cues polish done --- .../attention-cues.md | 114 ++++++++++++++---- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/chapter_attention-mechanisms/attention-cues.md b/chapter_attention-mechanisms/attention-cues.md index e9c7b79d3..b79cf323d 100644 --- a/chapter_attention-mechanisms/attention-cues.md +++ b/chapter_attention-mechanisms/attention-cues.md @@ -1,44 +1,103 @@ # 注意力提示 :label:`sec_attention-cues` -感谢你对本书的关注,因为注意力是一种稀缺的资源:此刻你正在阅读这本书而忽略了其他的书。因此,你的注意力是用机会成本(与金钱类似)来支付的。为了确保你现在投入的注意力是值得的,我们尽全力(全部的注意力)创作一本好书。 - -自经济学研究稀缺资源分配以来,我们正处在注意力经济时代,即人类的注意力被视为可以交换的、有限的、有价值的且稀缺的商品。许多商业模式也被开发出来去利用这一点。在音乐或视频流媒体服务上,我们要么消耗注意力在广告上,要么付钱来隐藏广告。为了在网络游戏世界的成长,我们要么消耗注意力在战斗中,从而帮助吸引新的玩家,要么付钱立即变得强大。没什么是免费的。 - -总而言之,注意力在我们的环境中是稀缺的,而环境中的信息却并不少。在检查视觉场景时,我们的视觉神经系统大约每秒收到$10^8$位的信息,这远远超过了大脑能够完全处理的水平。幸运的是,我们的祖先已经从经验(也称为数据)中认识到*并非感官的所有输入都是一样的*。在整个人类历史中,这种只将注意力引向感兴趣的一小部分信息的能力,使我们的大脑能够更明智地分配资源来生存、成长和社交,例如发现天敌、找寻食物和伴侣。 +感谢你对本书的关注,因为你的注意力是一种稀缺的资源: +此刻你正在阅读本书(而忽略了其他的书), +因此你的注意力是用机会成本(与金钱类似)来支付的。 +为了确保你现在投入的注意力是值得的, +我们尽全力(全部的注意力)创作一本好书。 + +自经济学研究稀缺资源分配以来,我们正处在“注意力经济”时代, +即人类的注意力被视为可以交换的、有限的、有价值的且稀缺的商品。 +许多商业模式也被开发出来去利用这一点: +在音乐或视频流媒体服务上,我们要么消耗注意力在广告上,要么付钱来隐藏广告; +为了在网络游戏世界的成长,我们要么消耗注意力在游戏战斗中, +从而帮助吸引新的玩家,要么付钱立即变得强大。 +总之,注意力不是免费的。 + +注意力是稀缺的,而环境中的干扰注意力的信息却并不少。 +比如我们的视觉神经系统大约每秒收到$10^8$位的信息, +这远远超过了大脑能够完全处理的水平。 +幸运的是,我们的祖先已经从经验(也称为数据)中认识到 +“并非感官的所有输入都是一样的”。 +在整个人类历史中,这种只将注意力引向感兴趣的一小部分信息的能力, +使我们的大脑能够更明智地分配资源来生存、成长和社交, +例如发现天敌、找寻食物和伴侣。 ## 生物学中的注意力提示 -为了解释我们的注意力是如何在视觉世界中展开的,一个双组件(two-component)的框架应运而生,并得到了普及。这个框架的出现可以追溯到19世纪90年代的威廉·詹姆斯,他被认为是“美国心理学之父” :cite:`James.2007`。在这个框架中,受试者基于*非自主性提示*和*自主性提示*有选择地引导注意力的焦点。 - -非自主性提示是基于环境中物体的突出性和易见性。想象一下,你面前有五个物品:一份报纸、一篇研究论文、一杯咖啡、一本笔记本和一本书,就像 :numref:`fig_eye-coffee`。所有纸制品都是黑白印刷的,但咖啡杯是红色的。换句话说,这杯咖啡在这种视觉环境中是突出和显眼的,自动而且不由自主地引起人们的注意。所以你把fovea(黄斑中心,视力最敏锐的地方)放到咖啡上,如 :numref:`fig_eye-coffee`所示。 - -![使用基于突出性的非自主性提示(红杯子,而非纸张),注意力不自主地指向了咖啡。](../img/eye-coffee.svg) +注意力是如何应用于视觉世界中的呢? +我们从当今十分普及的*双组件*(two-component)的框架开始讲起: +这个框架的出现可以追溯到19世纪90年代的威廉·詹姆斯, +他被认为是“美国心理学之父” :cite:`James.2007`。 +在这个框架中,受试者基于*非自主性提示*和*自主性提示* +有选择地引导注意力的焦点。 + +非自主性提示是基于环境中物体的突出性和易见性。 +想象一下,假如你面前有五个物品: +一份报纸、一篇研究论文、一杯咖啡、一本笔记本和一本书, +就像 :numref:`fig_eye-coffee`。 +所有纸制品都是黑白印刷的,但咖啡杯是红色的。 +换句话说,这个咖啡杯在这种视觉环境中是突出和显眼的, +不由自主地引起人们的注意。 +所以你把视力最敏锐的地方放到咖啡上, +如 :numref:`fig_eye-coffee`所示。 + +![由于突出性的非自主性提示(红杯子),注意力不自主地指向了咖啡杯](../img/eye-coffee.svg) :width:`400px` :label:`fig_eye-coffee` -喝咖啡后,你会变得兴奋并想读书。所以你转过头,重新聚焦你的眼睛,然后看看书,就像 :numref:`fig_eye-book`中描述那样。与 :numref:`fig_eye-coffee`中由于突出性导致选择会偏向于咖啡不同,此时选择书是受到了认知和意识的控制,因此注意力在基于自主性提示去辅助选择时将更为谨慎。受试者的主观意愿推动,选择的力量也就更强大。 +喝咖啡后,你会变得兴奋并想读书。 +所以你转过头,重新聚焦你的眼睛,然后看看书, +就像 :numref:`fig_eye-book`中描述那样。 +与 :numref:`fig_eye-coffee`中由于突出性导致的选择不同, +此时选择书是受到了认知和意识的控制, +因此注意力在基于自主性提示去辅助选择时将更为谨慎。 +受试者的主观意愿推动,选择的力量也就更强大。 -![通过使用依赖于任务的意志提示(想读一本书),注意力被自主引导的书上。](../img/eye-book.svg) +![依赖于任务的意志提示(想读一本书),注意力被自主引导到书上](../img/eye-book.svg) :width:`400px` :label:`fig_eye-book` ## 查询、键和值 -自主性的与非自主性的注意力提示解释了注意力的方式,下面我们将描述设计注意力机制时的框架,框架中合并这两个注意力提示来设计注意力机制。 - -首先,考虑一个相对简单的状况,即只使用非自主性提示。要想将选择偏向于感官输入,我们可以简单地使用参数化的全连接层,甚至是非参数化的最大汇聚层或平均汇聚层。 - -因此,通过是否包含自主性提示将注意力机制与全连接层或汇聚层区别开来。在注意力机制的背景下,我们将自主性提示称为*查询*(Queries)。给定任何查询,注意力机制通过*注意力汇聚*(attention pooling)将选择偏向于*感官输入*(sensory inputs,例如中间特征表示)。在注意力机制的背景下,这些感官输入被称为*值*(Values)。更通俗的解释,每个值都与一个*键*(Keys)配对,这可以想象为感官输入的非自主提示。如 :numref:`fig_qkv`所示,我们可以设计注意力汇聚,以便给定的查询(自主性提示)可以与键(非自主性提示)进行交互,这将引导将选择偏向于值(感官输入)。 - -![注意力机制通过注意力汇聚将 *查询*(自主性提示)和 *键*(非自主性提示)结合在一起,实现对 *值*(感官输入)的选择倾向。](../img/qkv.svg) +自主性的与非自主性的注意力提示解释了人类的注意力的方式, +下面我们看看如何通过这两种注意力提示, +用神经网络来设计注意力机制的框架, + +首先,考虑一个相对简单的状况, +即只使用非自主性提示。 +要想将选择偏向于感官输入, +我们可以简单地使用参数化的全连接层, +甚至是非参数化的最大汇聚层或平均汇聚层。 + +因此,“是否包含自主性提示”将注意力机制 +与全连接层或汇聚层区别开来。 +在注意力机制的背景下,我们将自主性提示称为*查询*(query)。 +给定任何查询,注意力机制通过*注意力汇聚*(attention pooling) +将选择引导至*感官输入*(sensory inputs,例如中间特征表示)。 +在注意力机制中,这些感官输入被称为*值*(value)。 +更通俗的解释,每个值都与一个*键*(key)配对, +这可以想象为感官输入的非自主提示。 +如 :numref:`fig_qkv`所示,我们可以设计注意力汇聚, +以便给定的查询(自主性提示)可以与键(非自主性提示)进行匹配, +这将引导得出最匹配的值(感官输入)。 + +![注意力机制通过注意力汇聚将*查询*(自主性提示)和*键*(非自主性提示)结合在一起,实现对*值*(感官输入)的选择倾向](../img/qkv.svg) :label:`fig_qkv` -注意,注意力机制的设计有许多替代方案。例如,我们可以设计一个不可微的注意力模型,该模型可以使用强化学习方法 :cite:`Mnih.Heess.Graves.ea.2014`进行训练。鉴于上面所提框架在 :numref:`fig_qkv`中的主导地位,因此这个框架下的模型将成为本章我们关注的中心。 +鉴于上面所提框架在 :numref:`fig_qkv`中的主导地位, +因此这个框架下的模型将成为本章的中心。 +然而,注意力机制的设计有许多替代方案。 +例如,我们可以设计一个不可微的注意力模型, +该模型可以使用强化学习方法 :cite:`Mnih.Heess.Graves.ea.2014`进行训练。 ## 注意力的可视化 -平均汇聚层可以被视为输入的加权平均值,其中各输入的权重是一样的。实际上,注意力汇聚得到的是加权平均的总和值,其中权重是在给定的查询和不同的键之间计算得出的。 +平均汇聚层可以被视为输入的加权平均值, +其中各输入的权重是一样的。 +实际上,注意力汇聚得到的是加权平均的总和值, +其中权重是在给定的查询和不同的键之间计算得出的。 ```{.python .input} from d2l import mxnet as d2l @@ -58,14 +117,16 @@ from d2l import tensorflow as d2l import tensorflow as tf ``` -为了可视化注意力权重,我们定义了`show_heatmaps`函数。其输入`matrices`的形状是(要显示的行数,要显示的列数,查询的数目,键的数目)。 +为了可视化注意力权重,我们定义了`show_heatmaps`函数。 +其输入`matrices`的形状是 +(要显示的行数,要显示的列数,查询的数目,键的数目)。 ```{.python .input} #@tab all #@save def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), cmap='Reds'): - """显示矩阵的热图。""" + """显示矩阵热图""" d2l.use_svg_display() num_rows, num_cols = matrices.shape[0], matrices.shape[1] fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize, @@ -82,7 +143,8 @@ def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5), fig.colorbar(pcm, ax=axes, shrink=0.6); ``` -我们使用一个简单的例子进行演示。在本例子中,仅当查询和键相同时,注意力权重为1,否则为0。 +下面我们使用一个简单的例子进行演示。 +在本例子中,仅当查询和键相同时,注意力权重为1,否则为0。 ```{.python .input} #@tab all @@ -90,12 +152,12 @@ attention_weights = d2l.reshape(d2l.eye(10), (1, 1, 10, 10)) show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries') ``` -在后面的章节中,我们将经常调用此函数来显示注意力权重。 +在后面的章节中,我们将经常调用`show_heatmaps`函数来显示注意力权重。 ## 小结 * 人类的注意力是有限的、有价值和稀缺的资源。 -* 受试者使用非自主性和自主性提示有选择性地引导注意力。前者基于突出性,后者则依赖于任务。 +* 受试者使用非自主性和自主性提示有选择性地引导注意力。前者基于突出性,后者则依赖于意识。 * 注意力机制与全连接层或者汇聚层的区别源于增加的自主提示。 * 由于包含了自主性提示,注意力机制与全连接的层或汇聚层不同。 * 注意力机制通过注意力汇聚使选择偏向于值(感官输入),其中包含查询(自主性提示)和键(非自主性提示)。键和值是成对的。 From 7b2afe284ee832a8d5bee66b2d5261872c2c57d9 Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Mon, 15 Nov 2021 19:53:25 -0800 Subject: [PATCH 2/8] attention-scoring-functions --- .../attention-scoring-functions.md | 184 ++++++++++++------ 1 file changed, 128 insertions(+), 56 deletions(-) diff --git a/chapter_attention-mechanisms/attention-scoring-functions.md b/chapter_attention-mechanisms/attention-scoring-functions.md index 6ffb39465..f1b9e16cb 100644 --- a/chapter_attention-mechanisms/attention-scoring-functions.md +++ b/chapter_attention-mechanisms/attention-scoring-functions.md @@ -1,24 +1,45 @@ # 注意力评分函数 :label:`sec_attention-scoring-functions` -在 :numref:`sec_nadaraya-watson`中,我们使用高斯核来对查询和键之间的关系建模。可以将 :eqref:`eq_nadaraya-watson-gaussian`中的高斯核的指数部分视为*注意力评分函数*(attention scoring function),简称*评分函数*(scoring function),然后把这个函数的输出结果输入到softmax函数中进行运算。通过上述步骤,我们将得到与键对应的值的概率分布(即注意力权重)。最后,注意力汇聚的输出就是基于这些注意力权重的值的加权和。 - -从宏观来看,可以使用上述算法来实现 :numref:`fig_qkv`中的注意力机制框架。 :numref:`fig_attention_output`说明了如何将注意力汇聚的输出计算成为值的加权和,其中$a$表示注意力评分函数。由于注意力权重是概率分布,因此加权和其本质上是加权平均值。 - -![计算注意力汇聚的输出为值的加权和。](../img/attention-output.svg) +在 :numref:`sec_nadaraya-watson`中, +我们使用高斯核来对查询和键之间的关系建模。 +我们可以将 :eqref:`eq_nadaraya-watson-gaussian`中的 +高斯核指数部分视为*注意力评分函数*(attention scoring function), +简称*评分函数*(scoring function), +然后把这个函数的输出结果输入到softmax函数中进行运算。 +通过上述步骤,我们将得到与键对应的值的概率分布(即注意力权重)。 +最后,注意力汇聚的输出就是基于这些注意力权重的值的加权和。 + +从宏观来看,我们可以使用上述算法来实现 + :numref:`fig_qkv`中的注意力机制框架。 + :numref:`fig_attention_output`说明了 +如何将注意力汇聚的输出计算成为值的加权和, +其中$a$表示注意力评分函数。 +由于注意力权重是概率分布, +因此加权和其本质上是加权平均值。 + +![计算注意力汇聚的输出为值的加权和](../img/attention-output.svg) :label:`fig_attention_output` -用数学语言描述,假设有一个查询$\mathbf{q} \in \mathbb{R}^q$和$m$个“键-值”对$(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)$,其中$\mathbf{k}_i \in \mathbb{R}^k$,$\mathbf{v}_i \in \mathbb{R}^v$。注意力汇聚函数$f$就被表示成值的加权和: +用数学语言描述,假设有一个查询 +$\mathbf{q} \in \mathbb{R}^q$和 +$m$个“键-值”对 +$(\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)$, +其中$\mathbf{k}_i \in \mathbb{R}^k$,$\mathbf{v}_i \in \mathbb{R}^v$。 +注意力汇聚函数$f$就被表示成值的加权和: $$f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)) = \sum_{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i \in \mathbb{R}^v,$$ :eqlabel:`eq_attn-pooling` -其中查询$\mathbf{q}$和键$\mathbf{k}_i$的注意力权重(标量)是通过注意力评分函数$a$ 将两个向量映射成标量,再经过softmax运算得到的: +其中查询$\mathbf{q}$和键$\mathbf{k}_i$的注意力权重(标量) +是通过注意力评分函数$a$ 将两个向量映射成标量, +再经过softmax运算得到的: $$\alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(a(\mathbf{q}, \mathbf{k}_i))}{\sum_{j=1}^m \exp(a(\mathbf{q}, \mathbf{k}_j))} \in \mathbb{R}.$$ :eqlabel:`eq_attn-scoring-alpha` -正如我们所看到的,选择不同的注意力评分函数$a$会导致不同的注意力汇聚操作。在本节中,我们将介绍两个流行的评分函数,稍后将用他们来实现更复杂的注意力机制。 +正如我们所看到的,选择不同的注意力评分函数$a$会导致不同的注意力汇聚操作。 +在本节中,我们将介绍两个流行的评分函数,稍后将用他们来实现更复杂的注意力机制。 ```{.python .input} import math @@ -45,13 +66,22 @@ import tensorflow as tf ## [**掩蔽softmax操作**] -正如上面提到的,softmax操作用于输出一个概率分布作为注意力权重。在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。例如,为了在 :numref:`sec_machine_translation`中高效处理小批量数据集,某些文本序列被填充了没有意义的特殊词元。为了仅将有意义的词元作为值去获取注意力汇聚,可以指定一个有效序列长度(即词元的个数),以便在计算softmax时过滤掉超出指定范围的位置。通过这种方式,我们可以在下面的`masked_softmax`函数中实现这样的*掩蔽softmax操作*(masked softmax operation),其中任何超出有效长度的位置都被掩蔽并置为0。 +正如上面提到的,softmax操作用于输出一个概率分布作为注意力权重。 +在某些情况下,并非所有的值都应该被纳入到注意力汇聚中。 +例如,为了在 :numref:`sec_machine_translation`中高效处理小批量数据集, +某些文本序列被填充了没有意义的特殊词元。 +为了仅将有意义的词元作为值来获取注意力汇聚, +我们可以指定一个有效序列长度(即词元的个数), +以便在计算softmax时过滤掉超出指定范围的位置。 +通过这种方式,我们可以在下面的`masked_softmax`函数中 +实现这样的*掩蔽softmax操作*(masked softmax operation), +其中任何超出有效长度的位置都被掩蔽并置为0。 ```{.python .input} #@save def masked_softmax(X, valid_lens): """通过在最后一个轴上掩蔽元素来执行 softmax 操作""" - # `X`: 3D张量, `valid_lens`: 1D或2D 张量 + # `X`: 3D张量,`valid_lens`: 1D或2D 张量 if valid_lens is None: return npx.softmax(X) else: @@ -60,7 +90,7 @@ def masked_softmax(X, valid_lens): valid_lens = valid_lens.repeat(shape[1]) else: valid_lens = valid_lens.reshape(-1) - # 在最后的轴上,被掩蔽的元素使用一个非常大的负值替换,从而其 softmax (指数)输出为 0 + # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 X = npx.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, True, value=-1e6, axis=1) return npx.softmax(X).reshape(shape) @@ -71,7 +101,7 @@ def masked_softmax(X, valid_lens): #@save def masked_softmax(X, valid_lens): """通过在最后一个轴上掩蔽元素来执行 softmax 操作""" - # `X`: 3D张量, `valid_lens`: 1D或2D 张量 + # `X`: 3D张量,`valid_lens`: 1D或2D 张量 if valid_lens is None: return nn.functional.softmax(X, dim=-1) else: @@ -80,7 +110,7 @@ def masked_softmax(X, valid_lens): valid_lens = torch.repeat_interleave(valid_lens, shape[1]) else: valid_lens = valid_lens.reshape(-1) - # 在最后的轴上,被掩蔽的元素使用一个非常大的负值替换,从而其 softmax (指数)输出为 0 + # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens, value=-1e6) return nn.functional.softmax(X.reshape(shape), dim=-1) @@ -91,7 +121,7 @@ def masked_softmax(X, valid_lens): #@save def masked_softmax(X, valid_lens): """通过在最后一个轴上掩蔽元素来执行 softmax 操作""" - # `X`: 3D张量, `valid_lens`: 1D或2D 张量 + # `X`: 3D张量,`valid_lens`: 1D或2D 张量 if valid_lens is None: return tf.nn.softmax(X, axis=-1) else: @@ -101,12 +131,16 @@ def masked_softmax(X, valid_lens): else: valid_lens = tf.reshape(valid_lens, shape=-1) - # 在最后的轴上,被掩蔽的元素使用一个非常大的负值替换,从而其 softmax (指数)输出为 0 - X = d2l.sequence_mask(tf.reshape(X, shape=(-1, shape[-1])), valid_lens, value=-1e6) + # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0 + X = d2l.sequence_mask(tf.reshape(X, shape=(-1, shape[-1])), + valid_lens, value=-1e6) return tf.nn.softmax(tf.reshape(X, shape=shape), axis=-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])) @@ -117,7 +151,12 @@ masked_softmax(np.random.uniform(size=(2, 2, 4)), d2l.tensor([2, 3])) masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3])) ``` -同样,我们也可以使用二维张量为矩阵样本中的每一行指定有效长度。 +```{.python .input} +#@tab tensorflow +masked_softmax(tf.random.uniform(shape=(2, 2, 4)), tf.constant([2, 3])) +``` + +同样,我们也可以使用二维张量,为矩阵样本中的每一行指定有效长度。 ```{.python .input} masked_softmax(np.random.uniform(size=(2, 2, 4)), @@ -137,12 +176,24 @@ masked_softmax(tf.random.uniform(shape=(2, 2, 4)), tf.constant([2, 3])) ## [**加性注意力**] :label:`subsec_additive-attention` -一般来说,当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数。给定查询$\mathbf{q} \in \mathbb{R}^q$和键$\mathbf{k} \in \mathbb{R}^k$,*加性注意力*(additive attention)的评分函数为 +一般来说,当查询和键是不同长度的矢量时, +我们可以使用加性注意力作为评分函数。 +给定查询$\mathbf{q} \in \mathbb{R}^q$和 +键$\mathbf{k} \in \mathbb{R}^k$, +*加性注意力*(additive attention)的评分函数为 $$a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R},$$ :eqlabel:`eq_additive-attn` -其中可学习的参数是$\mathbf W_q\in\mathbb R^{h\times q}$、$\mathbf W_k\in\mathbb R^{h\times k}$和$\mathbf w_v\in\mathbb R^{h}$。如 :eqref:`eq_additive-attn`所示,将查询和键连结起来后输入到一个多层感知机(MLP)中,感知机包含一个隐藏层,其隐藏单元数是一个超参数$h$。通过使用$\tanh$作为激活函数,并且禁用偏置项,我们将在下面实现加性注意力。 +其中可学习的参数是$\mathbf W_q\in\mathbb R^{h\times q}$、 +$\mathbf W_k\in\mathbb R^{h\times k}$和 +$\mathbf w_v\in\mathbb R^{h}$。 +如 :eqref:`eq_additive-attn`所示, +将查询和键连结起来后输入到一个多层感知机(MLP)中, +感知机包含一个隐藏层,其隐藏单元数是一个超参数$h$。 +通过使用$\tanh$作为激活函数,并且禁用偏置项。 + +下面我们来实现加性注意力。 ```{.python .input} #@save @@ -159,16 +210,17 @@ class AdditiveAttention(nn.Block): def forward(self, queries, keys, values, valid_lens): queries, keys = self.W_q(queries), self.W_k(keys) # 在维度扩展后, - # `queries` 的形状:(`batch_size`, 查询的个数, 1, `num_hidden`) - # `key` 的形状:(`batch_size`, 1, “键-值”对的个数, `num_hiddens`) + # `queries` 的形状:(`batch_size`,查询的个数,1,`num_hidden`) + # `key` 的形状:(`batch_size`,1,“键-值”对的个数,`num_hiddens`) # 使用广播的方式进行求和 - features = np.expand_dims(queries, axis=2) + np.expand_dims(keys, axis=1) + features = np.expand_dims(queries, axis=2) + np.expand_dims( + keys, axis=1) features = np.tanh(features) # `self.w_v` 仅有一个输出,因此从形状中移除最后那个维度。 - # `scores` 的形状:(`batch_size`, 查询的个数, “键-值”对的个数) + # `scores` 的形状:(`batch_size`,查询的个数,“键-值”对的个数) scores = np.squeeze(self.w_v(features), axis=-1) self.attention_weights = masked_softmax(scores, valid_lens) - # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) + # `values` 的形状:(`batch_size`,“键-值”对的个数,值的维度) return npx.batch_dot(self.dropout(self.attention_weights), values) ``` @@ -187,16 +239,16 @@ class AdditiveAttention(nn.Module): def forward(self, queries, keys, values, valid_lens): queries, keys = self.W_q(queries), self.W_k(keys) # 在维度扩展后, - # `queries` 的形状:(`batch_size`, 查询的个数, 1, `num_hidden`) - # `key` 的形状:(`batch_size`, 1, “键-值”对的个数, `num_hiddens`) + # `queries` 的形状:(`batch_size`,查询的个数,1,`num_hidden`) + # `key` 的形状:(`batch_size`,1,“键-值”对的个数,`num_hiddens`) # 使用广播方式进行求和 features = queries.unsqueeze(2) + keys.unsqueeze(1) features = torch.tanh(features) # `self.w_v` 仅有一个输出,因此从形状中移除最后那个维度。 - # `scores` 的形状:(`batch_size`, 查询的个数, “键-值”对的个数) + # `scores` 的形状:(`batch_size`,查询的个数,“键-值”对的个数) scores = self.w_v(features).squeeze(-1) self.attention_weights = masked_softmax(scores, valid_lens) - # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) + # `values` 的形状:(`batch_size`,“键-值”对的个数,值的维度) return torch.bmm(self.dropout(self.attention_weights), values) ``` @@ -215,22 +267,25 @@ class AdditiveAttention(tf.keras.layers.Layer): def call(self, queries, keys, values, valid_lens, **kwargs): queries, keys = self.W_q(queries), self.W_k(keys) # 在维度扩展后, - # `queries` 的形状:(`batch_size`, 查询的个数, 1, `num_hidden`) - # `key` 的形状:(`batch_size`, 1, “键-值”对的个数, `num_hiddens`) + # `queries` 的形状:(`batch_size`,查询的个数,1,`num_hidden`) + # `key` 的形状:(`batch_size`,1,“键-值”对的个数,`num_hiddens`) # 使用广播方式进行求和 features = tf.expand_dims(queries, axis=2) + tf.expand_dims( keys, axis=1) features = tf.nn.tanh(features) # `self.w_v` 仅有一个输出,因此从形状中移除最后那个维度。 - # `scores` 的形状:(`batch_size`, 查询的个数, “键-值”对的个数) + # `scores` 的形状:(`batch_size`,查询的个数,“键-值”对的个数) scores = tf.squeeze(self.w_v(features), axis=-1) self.attention_weights = masked_softmax(scores, valid_lens) - # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) + # `values` 的形状:(`batch_size`,“键-值”对的个数,值的维度) return tf.matmul(self.dropout( self.attention_weights, **kwargs), 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)) @@ -271,7 +326,8 @@ attention(queries, keys, values, valid_lens, training=False) ``` -尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的,所以[**注意力权重**]是均匀的,由指定的有效长度决定。 +尽管加性注意力包含了可学习的参数,但由于本例子中每个键都是相同的, +所以[**注意力权重**]是均匀的,由指定的有效长度决定。 ```{.python .input} #@tab all @@ -281,11 +337,24 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), ## [**缩放点积注意力**] -使用点积可以得到计算效率更高的评分函数。但是点积操作要求查询和键具有相同的长度$d$。假设查询和键的所有元素都是独立的随机变量,并且都满足零均值和单位方差。那么两个向量的点积的均值为$0$,方差为$d$。为确保无论向量长度如何,点积的方差在不考虑向量长度的情况下仍然是$1$,则可以使用*缩放点积注意力*(scaled dot-product attention)评分函数: - -$$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}$$ - -将点积除以$\sqrt{d}$。在实践中,我们通常从小批量的角度来考虑提高效率,例如基于$n$个查询和$m$个键-值对计算注意力,其中查询和键的长度为$d$,值的长度为$v$。查询$\mathbf Q\in\mathbb R^{n\times d}$、键$\mathbf K\in\mathbb R^{m\times d}$和值$\mathbf V\in\mathbb R^{m\times v}$的缩放点积注意力是 +使用点积可以得到计算效率更高的评分函数, +但是点积操作要求查询和键具有相同的长度$d$。 +假设查询和键的所有元素都是独立的随机变量, +并且都满足零均值和单位方差, +那么两个向量的点积的均值为$0$,方差为$d$。 +为确保无论向量长度如何, +点积的方差在不考虑向量长度的情况下仍然是$1$, +我们将点积除以$\sqrt{d}$, +则*缩放点积注意力*(scaled dot-product attention)评分函数为: + +$$a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}.$$ + +在实践中,我们通常从小批量的角度来考虑提高效率, +例如基于$n$个查询和$m$个键-值对计算注意力, +其中查询和键的长度为$d$,值的长度为$v$。 +查询$\mathbf Q\in\mathbb R^{n\times d}$、 +键$\mathbf K\in\mathbb R^{m\times d}$和 +值$\mathbf V\in\mathbb R^{m\times v}$的缩放点积注意力是: $$ \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}.$$ :eqlabel:`eq_softmax_QK_V` @@ -300,10 +369,10 @@ class DotProductAttention(nn.Block): super(DotProductAttention, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) - # `queries` 的形状:(`batch_size`, 查询的个数, `d`) - # `keys` 的形状:(`batch_size`, “键-值”对的个数, `d`) - # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) - # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`, 查询的个数) + # `queries` 的形状:(`batch_size`,查询的个数,`d`) + # `keys` 的形状:(`batch_size`,“键-值”对的个数,`d`) + # `values` 的形状:(`batch_size`,“键-值”对的个数,值的维度) + # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`,查询的个数) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] # 设置 `transpose_b=True` 为了交换 `keys` 的最后两个维度 @@ -321,10 +390,10 @@ class DotProductAttention(nn.Module): super(DotProductAttention, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) - # `queries` 的形状:(`batch_size`, 查询的个数, `d`) - # `keys` 的形状:(`batch_size`, “键-值”对的个数, `d`) - # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) - # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`, 查询的个数) + # `queries` 的形状:(`batch_size`,查询的个数,`d`) + # `keys` 的形状:(`batch_size`,“键-值”对的个数,`d`) + # `values` 的形状:(`batch_size`,“键-值”对的个数,值的维度) + # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`,查询的个数) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] # 设置 `transpose_b=True` 为了交换 `keys` 的最后两个维度 @@ -342,10 +411,10 @@ class DotProductAttention(tf.keras.layers.Layer): super().__init__(**kwargs) self.dropout = tf.keras.layers.Dropout(dropout) - # `queries` 的形状:(`batch_size`, 查询的个数, `d`) - # `keys` 的形状:(`batch_size`, “键-值”对的个数, `d`) - # `values` 的形状:(`batch_size`, “键-值”对的个数, 值的维度) - # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`, 查询的个数) + # `queries` 的形状:(`batch_size`,查询的个数,`d`) + # `keys` 的形状:(`batch_size`,“键-值”对的个数,`d`) + # `values` 的形状:(`batch_size`,“键-值”对的个数,值的维度) + # `valid_lens` 的形状: (`batch_size`,) 或者 (`batch_size`,查询的个数) def call(self, queries, keys, values, valid_lens, **kwargs): d = queries.shape[-1] scores = tf.matmul(queries, keys, transpose_b=True)/tf.math.sqrt( @@ -354,7 +423,9 @@ class DotProductAttention(tf.keras.layers.Layer): return tf.matmul(self.dropout(self.attention_weights, **kwargs), values) ``` -为了[**演示上述的`DotProductAttention`类**],我们使用了与先前加性注意力例子中相同的键、值和有效长度。对于点积操作,令查询的特征维度与键的特征维度大小相同。 +为了[**演示上述的`DotProductAttention`类**], +我们使用与先前加性注意力例子中相同的键、值和有效长度。 +对于点积操作,我们令查询的特征维度与键的特征维度大小相同。 ```{.python .input} queries = d2l.normal(0, 1, (2, 1, 2)) @@ -378,7 +449,8 @@ attention = DotProductAttention(dropout=0.5) attention(queries, keys, values, valid_lens, training=False) ``` -与加性注意力演示相同,由于键包含的是相同的元素,而这些元素无法通过任何查询进行区分,因此获得了[**均匀的注意力权重**]。 +与加性注意力演示相同,由于键包含的是相同的元素, +而这些元素无法通过任何查询进行区分,因此获得了[**均匀的注意力权重**]。 ```{.python .input} #@tab all @@ -388,13 +460,13 @@ d2l.show_heatmaps(d2l.reshape(attention.attention_weights, (1, 1, 2, 10)), ## 小结 -* 可以将注意力汇聚的输出计算作为值的加权平均,选择不同的注意力评分函数会带来不同的注意力汇聚操作。 +* 将注意力汇聚的输出计算可以作为值的加权平均,选择不同的注意力评分函数会带来不同的注意力汇聚操作。 * 当查询和键是不同长度的矢量时,可以使用可加性注意力评分函数。当它们的长度相同时,使用缩放的“点-积”注意力评分函数的计算效率更高。 ## 练习 1. 修改小例子中的键,并且可视化注意力权重。可加性注意力和缩放的“点-积”注意力是否仍然产生相同的结果?为什么? -1. 只使用矩阵乘法,您能否为具有不同矢量长度的查询和键设计新的评分函数? +1. 只使用矩阵乘法,你能否为具有不同矢量长度的查询和键设计新的评分函数? 1. 当查询和键具有相同的矢量长度时,矢量求和作为评分函数是否比“点-积”更好?为什么? :begin_tab:`mxnet` From d9ab1cee5eed0ce47b07d277e72dca70fbc13f63 Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Mon, 15 Nov 2021 19:53:48 -0800 Subject: [PATCH 3/8] nadaraya-waston polish done --- .../nadaraya-waston.md | 189 ++++++++++++------ 1 file changed, 130 insertions(+), 59 deletions(-) diff --git a/chapter_attention-mechanisms/nadaraya-waston.md b/chapter_attention-mechanisms/nadaraya-waston.md index efc3d839f..6f664e67e 100644 --- a/chapter_attention-mechanisms/nadaraya-waston.md +++ b/chapter_attention-mechanisms/nadaraya-waston.md @@ -1,7 +1,13 @@ # 注意力汇聚:Nadaraya-Watson 核回归 :label:`sec_nadaraya-watson` -现在知道了 :numref:`fig_qkv`框架下的注意力机制的主要成分。回顾一下,查询(自主提示)和键(非自主提示)之间的交互形成了**注意力汇聚**(attention pooling)。注意力汇聚有选择地聚合了值(感官输入)以生成最终的输出。在本节中,我们将介绍注意力汇聚的更多细节,以便从宏观上了解注意力机制在实践中的运作方式。具体来说,1964年提出的Nadaraya-Watson核回归模型是一个简单但完整的例子,可以用于演示具有注意力机制的机器学习。 +上节我们介绍了框架下的注意力机制的主要成分 :numref:`fig_qkv`: +查询(自主提示)和键(非自主提示)之间的交互形成了注意力汇聚, +注意力汇聚有选择地聚合了值(感官输入)以生成最终的输出。 +在本节中,我们将介绍注意力汇聚的更多细节, +以便从宏观上了解注意力机制在实践中的运作方式。 +具体来说,1964年提出的Nadaraya-Watson核回归模型 +是一个简单但完整的例子,可以用于演示具有注意力机制的机器学习。 ```{.python .input} from d2l import mxnet as d2l @@ -27,23 +33,29 @@ tf.random.set_seed(seed=1322) ## [**生成数据集**] -简单起见,考虑下面这个回归问题:给定的成对的“输入-输出”数据集$\{(x_1, y_1), \ldots, (x_n, y_n)\}$,如何学习$f$来预测任意新输入$x$的输出$\hat{y} = f(x)$? +简单起见,考虑下面这个回归问题: +给定的成对的“输入-输出”数据集 +$\{(x_1, y_1), \ldots, (x_n, y_n)\}$, +如何学习$f$来预测任意新输入$x$的输出$\hat{y} = f(x)$? -根据下面的非线性函数生成一个人工数据集,其中加入的噪声项为$\epsilon$: +根据下面的非线性函数生成一个人工数据集, +其中加入的噪声项为$\epsilon$: $$y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon,$$ -其中$\epsilon$服从均值为$0$和标准差为$0.5$的正态分布。我们生成了$50$个训练样本和$50$个测试样本。为了更好地可视化之后的注意力模式,输入的训练样本将进行排序。 +其中$\epsilon$服从均值为$0$和标准差为$0.5$的正态分布。 +我们生成了$50$个训练样本和$50$个测试样本。 +为了更好地可视化之后的注意力模式,我们将训练样本进行排序。 ```{.python .input} n_train = 50 # 训练样本数 -x_train = np.sort(d2l.rand(n_train) * 5) # 训练样本的输入 +x_train = np.sort(d2l.rand(n_train) * 5) # 排序后的训练样本 ``` ```{.python .input} #@tab pytorch n_train = 50 # 训练样本数 -x_train, _ = torch.sort(d2l.rand(n_train) * 5) # 训练样本的输入 +x_train, _ = torch.sort(d2l.rand(n_train) * 5) # 排序后的训练样本 ``` ```{.python .input} @@ -86,7 +98,9 @@ n_test = len(x_test) # 测试样本数 n_test ``` -下面的函数将绘制所有的训练样本(样本由圆圈表示)、不带噪声项的真实数据生成函数$f$(标记为“Truth”)和学习得到的预测函数(标记为“Pred”)。 +下面的函数将绘制所有的训练样本(样本由圆圈表示), +不带噪声项的真实数据生成函数$f$(标记为“Truth”), +以及学习得到的预测函数(标记为“Pred”)。 ```{.python .input} #@tab all @@ -98,12 +112,14 @@ def plot_kernel_reg(y_hat): ## 平均汇聚 -先使用可能是这个世界上“最愚蠢”的估计器来解决回归问题:基于平均汇聚来计算所有训练样本输出值的平均值: +我们先使用最简单的估计器来解决回归问题: +基于平均汇聚来计算所有训练样本输出值的平均值: $$f(x) = \frac{1}{n}\sum_{i=1}^n y_i,$$ :eqlabel:`eq_avg-pooling` -如下图所示,这个估计器确实不够聪明。 +如下图所示,这个估计器确实不够聪明: +真实函数$f$(“Truth”)和预测函数(“Pred”)相差很大。 ```{.python .input} y_hat = y_train.mean().repeat(n_test) @@ -124,30 +140,57 @@ plot_kernel_reg(y_hat) ## [**非参数注意力汇聚**] -显然,平均汇聚忽略了输入$x_i$。于是Nadaraya :cite:`Nadaraya.1964`和Watson :cite:`Watson.1964`提出了一个更好的想法,根据输入的位置对输出$y_i$进行加权: +显然,平均汇聚忽略了输入$x_i$。 +于是Nadaraya :cite:`Nadaraya.1964`和 +Watson :cite:`Watson.1964`提出了一个更好的想法, +根据输入的位置对输出$y_i$进行加权: $$f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i,$$ :eqlabel:`eq_nadaraya-watson` -其中$K$是*核*(kernel)。公式 :eqref:`eq_nadaraya-watson`所描述的估计器被称为*Nadaraya-Watson核回归*(Nadaraya-Watson kernel regression)。在这里我们不会深入讨论核函数的细节。回想一下 :numref:`fig_qkv`中的注意力机制框架,我们可以从注意力机制的角度重写 :eqref:`eq_nadaraya-watson` 成为一个更加通用的*注意力汇聚*(attention pooling)公式: +其中$K$是*核*(kernel)。 +公式 :eqref:`eq_nadaraya-watson`所描述的估计器被称为 +*Nadaraya-Watson核回归*(Nadaraya-Watson kernel regression)。 +这里我们不会深入讨论核函数的细节, +但受此启发, +我们可以从 :numref:`fig_qkv`中的注意力机制框架的角度 +重写 :eqref:`eq_nadaraya-watson`, +成为一个更加通用的*注意力汇聚*(attention pooling)公式: $$f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i,$$ :eqlabel:`eq_attn-pooling` -其中$x$是查询,$(x_i, y_i)$是键值对。比较 :eqref:`eq_attn-pooling`和 :eqref:`eq_avg-pooling`,注意力汇聚是$y_i$的加权平均。将查询$x$和键$x_i$之间的关系建模为*注意力权重*(attention weight)$\alpha(x, x_i)$,如 :eqref:`eq_attn-pooling`所示,这个权重将被分配给每一个对应值$y_i$。对于任何查询,模型在所有键值对上的注意力权重都是一个有效的概率分布:它们是非负数的,并且总和为1。 +其中$x$是查询,$(x_i, y_i)$是键值对。 +比较 :eqref:`eq_attn-pooling`和 :eqref:`eq_avg-pooling`, +注意力汇聚是$y_i$的加权平均。 +将查询$x$和键$x_i$之间的关系建模为 +*注意力权重*(attention weight)$\alpha(x, x_i)$, +如 :eqref:`eq_attn-pooling`所示, +这个权重将被分配给每一个对应值$y_i$。 +对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布: +它们是非负的,并且总和为1。 -为了更好地理解注意力汇聚,仅考虑一个*高斯核*(Gaussian kernel),其定义为: +为了更好地理解注意力汇聚, +我们考虑一个*高斯核*(Gaussian kernel),其定义为: $$K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}).$$ -将高斯核代入 :eqref:`eq_attn-pooling`和 :eqref:`eq_nadaraya-watson`可以得到: +将高斯核代入 :eqref:`eq_attn-pooling`和 + :eqref:`eq_nadaraya-watson`可以得到: $$\begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}(x - x_i)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}(x - x_j)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}(x - x_i)^2\right) y_i. \end{aligned}$$ :eqlabel:`eq_nadaraya-watson-gaussian` -在 :eqref:`eq_nadaraya-watson-gaussian`中,如果一个键$x_i$越是接近给定的查询$x$,那么分配给这个键对应值$y_i$的注意力权重就会越大,也就是“获得了更多的注意力”。 +在 :eqref:`eq_nadaraya-watson-gaussian`中, +如果一个键$x_i$越是接近给定的查询$x$, +那么分配给这个键对应值$y_i$的注意力权重就会越大, +也就“获得了更多的注意力”。 -值得注意的是,Nadaraya-Watson核回归是一个非参数模型。因此, :eqref:`eq_nadaraya-watson-gaussian`是*非参数的注意力汇聚*(nonparametric attention pooling)的例子。接下来,我们将基于这个非参数的注意力汇聚模型来绘制预测结果。结果是预测线是平滑的,并且比平均汇聚产生的线更接近真实。 +值得注意的是,Nadaraya-Watson核回归是一个非参数模型。 +因此, :eqref:`eq_nadaraya-watson-gaussian`是 +*非参数的注意力汇聚*(nonparametric attention pooling)模型。 +接下来,我们将基于这个非参数的注意力汇聚模型来绘制预测结果。 +你会发现新的模型预测线是平滑的,并且比平均汇聚的预测更接近真实。 ```{.python .input} # `X_repeat` 的形状: (`n_test`, `n_train`), @@ -187,7 +230,10 @@ y_hat = tf.matmul(attention_weights, tf.expand_dims(y_train, axis=1)) plot_kernel_reg(y_hat) ``` -现在,让我们来观察注意力的权重。这里测试数据的输入相当于查询,而训练数据的输入相当于键。因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近,注意力汇聚的[**注意力权重**]就越高。 +现在,我们来观察注意力的权重。 +这里测试数据的输入相当于查询,而训练数据的输入相当于键。 +因为两个输入都是经过排序的,因此由观察可知“查询-键”对越接近, +注意力汇聚的[**注意力权重**]就越高。 ```{.python .input} d2l.show_heatmaps(np.expand_dims(np.expand_dims(attention_weights, 0), 0), @@ -204,29 +250,43 @@ d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0), ```{.python .input} #@tab tensorflow -d2l.show_heatmaps(tf.expand_dims(tf.expand_dims(attention_weights, axis=0), axis=0), +d2l.show_heatmaps(tf.expand_dims( + tf.expand_dims(attention_weights, axis=0), axis=0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') ``` ## [**带参数注意力汇聚**] -非参数的Nadaraya-Watson核回归具有*一致性*(consistency)的优点:如果有足够的数据,此模型会收敛到最优结果。尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。 +非参数的Nadaraya-Watson核回归具有*一致性*(consistency)的优点: +如果有足够的数据,此模型会收敛到最优结果。 +尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。 -例如,与 :eqref:`eq_nadaraya-watson-gaussian`略有不同,在下面的查询$x$和键$x_i$之间的距离乘以可学习参数$w$: +例如,与 :eqref:`eq_nadaraya-watson-gaussian`略有不同, +在下面的查询$x$和键$x_i$之间的距离乘以可学习参数$w$: $$\begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_j)w)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.\end{aligned}$$ :eqlabel:`eq_nadaraya-watson-gaussian-para` -在本节的余下部分,我们将通过训练这个模型 :eqref:`eq_nadaraya-watson-gaussian-para`来学习注意力汇聚的参数。 +在本节的余下部分,我们将通过训练这个模型 + :eqref:`eq_nadaraya-watson-gaussian-para`来学习注意力汇聚的参数。 ### 批量矩阵乘法 :label:`subsec_batch_dot` -为了更有效地计算小批量数据的注意力,我们可以利用深度学习开发框架中提供的批量矩阵乘法。 +为了更有效地计算小批量数据的注意力, +我们可以利用深度学习开发框架中提供的批量矩阵乘法。 -假设第一个小批量数据包含$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)) @@ -272,7 +332,9 @@ tf.matmul(tf.expand_dims(weights, axis=1), tf.expand_dims(values, axis=-1)).nump ### 定义模型 -基于 :eqref:`eq_nadaraya-watson-gaussian-para`中的[**带参数的注意力汇聚**],使用小批量矩阵乘法,定义Nadaraya-Watson核回归的带参数版本为: +基于 :eqref:`eq_nadaraya-watson-gaussian-para`中的 +[**带参数的注意力汇聚**],使用小批量矩阵乘法, +定义Nadaraya-Watson核回归的带参数版本为: ```{.python .input} class NWKernelRegression(nn.Block): @@ -281,12 +343,12 @@ class NWKernelRegression(nn.Block): self.w = self.params.get('w', shape=(1,)) def forward(self, queries, keys, values): - # `queries` 和 `attention_weights` 的形状为 (查询数, “键-值”对数) + # `queries` 和 `attention_weights` 的形状为 (查询数,“键-值”对数) queries = d2l.reshape( queries.repeat(keys.shape[1]), (-1, keys.shape[1])) self.attention_weights = npx.softmax( -((queries - keys) * self.w.data())**2 / 2) - # `values` 的形状为 (查询数, “键-值”对数) + # `values` 的形状为 (查询数,“键-值”对数) return npx.batch_dot(np.expand_dims(self.attention_weights, 1), np.expand_dims(values, -1)).reshape(-1) ``` @@ -299,12 +361,12 @@ class NWKernelRegression(nn.Module): self.w = nn.Parameter(torch.rand((1,), requires_grad=True)) def forward(self, queries, keys, values): - # `queries` 和 `attention_weights` 的形状为 (查询个数, “键-值”对个数) + # `queries` 和 `attention_weights` 的形状为 (查询个数,“键-值”对个数) queries = d2l.reshape( queries.repeat_interleave(keys.shape[1]), (-1, keys.shape[1])) self.attention_weights = nn.functional.softmax( -((queries - keys) * self.w)**2 / 2, dim=1) - # `values` 的形状为 (查询个数, “键-值”对个数) + # `values` 的形状为 (查询个数,“键-值”对个数) return torch.bmm(self.attention_weights.unsqueeze(1), values.unsqueeze(-1)).reshape(-1) ``` @@ -318,58 +380,61 @@ class NWKernelRegression(tf.keras.layers.Layer): def call(self, queries, keys, values, **kwargs): # 对于训练,“查询”是`x_train`。“键”是每个点的训练数据的距离。“值”为'y_train'。 - # `queries` 和 `attention_weights` 的形状为 (查询个数, “键-值”对个数) + # `queries` 和 `attention_weights` 的形状为 (查询个数,“键-值”对个数) queries = tf.repeat(tf.expand_dims(queries, axis=1), repeats=keys.shape[1], axis=1) self.attention_weights = tf.nn.softmax(-((queries - keys) * self.w)**2 /2, axis =1) - # `values` 的形状为 (查询个数, “键-值”对个数) + # `values` 的形状为 (查询个数,“键-值”对个数) return tf.squeeze(tf.matmul(tf.expand_dims(self.attention_weights, axis=1), tf.expand_dims(values, axis=-1))) ``` ### 训练 -接下来,[**将训练数据集变换为键和值**]用于训练注意力模型。在带参数的注意力汇聚模型中,任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算,从而得到其对应的预测输出。 +接下来,[**将训练数据集变换为键和值**]用于训练注意力模型。 +在带参数的注意力汇聚模型中, +任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算, +从而得到其对应的预测输出。 ```{.python .input} -# `X_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输入 +# `X_tile` 的形状: (`n_train`,`n_train`),每一行都包含着相同的训练输入 X_tile = np.tile(x_train, (n_train, 1)) -# `Y_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输出 +# `Y_tile` 的形状: (`n_train`,`n_train`),每一行都包含着相同的训练输出 Y_tile = np.tile(y_train, (n_train, 1)) -# `keys` 的形状: ('n_train', 'n_train' - 1) +# `keys` 的形状: ('n_train','n_train' - 1) keys = d2l.reshape(X_tile[(1 - d2l.eye(n_train)).astype('bool')], (n_train, -1)) -# `values` 的形状: ('n_train', 'n_train' - 1) +# `values` 的形状: ('n_train','n_train' - 1) values = d2l.reshape(Y_tile[(1 - d2l.eye(n_train)).astype('bool')], (n_train, -1)) ``` ```{.python .input} #@tab pytorch -# `X_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输入 +# `X_tile` 的形状: (`n_train`,`n_train`),每一行都包含着相同的训练输入 X_tile = x_train.repeat((n_train, 1)) -# `Y_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输出 +# `Y_tile` 的形状: (`n_train`,`n_train`),每一行都包含着相同的训练输出 Y_tile = y_train.repeat((n_train, 1)) -# `keys` 的形状: ('n_train', 'n_train' - 1) +# `keys` 的形状: ('n_train','n_train' - 1) keys = d2l.reshape(X_tile[(1 - d2l.eye(n_train)).type(torch.bool)], (n_train, -1)) -# `values` 的形状: ('n_train', 'n_train' - 1) +# `values` 的形状: ('n_train','n_train' - 1) values = d2l.reshape(Y_tile[(1 - d2l.eye(n_train)).type(torch.bool)], (n_train, -1)) ``` ```{.python .input} #@tab tensorflow -# `X_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输入 +# `X_tile` 的形状: (`n_train`,`n_train`),每一行都包含着相同的训练输入 X_tile = tf.repeat(tf.expand_dims(x_train, axis=0), repeats=n_train, axis=0) -# `Y_tile` 的形状: (`n_train`, `n_train`), 每一行都包含着相同的训练输出 +# `Y_tile` 的形状: (`n_train`,`n_train`),每一行都包含着相同的训练输出 Y_tile = tf.repeat(tf.expand_dims(y_train, axis=0), repeats=n_train, axis=0) -# `keys` 的形状: ('n_train', 'n_train' - 1) +# `keys` 的形状: ('n_train','n_train' - 1) keys = tf.reshape(X_tile[tf.cast(1 - tf.eye(n_train), dtype=tf.bool)], shape=(n_train, -1)) -# `values` 的形状: ('n_train', 'n_train' - 1) +# `values` 的形状: ('n_train','n_train' - 1) values = tf.reshape(Y_tile[tf.cast(1 - tf.eye(n_train), dtype=tf.bool)], shape=(n_train, -1)) ``` -[**训练带参数的注意力汇聚模型**]时使用平方损失函数和随机梯度下降。 +[**训练带参数的注意力汇聚模型**]时,使用平方损失函数和随机梯度下降。 ```{.python .input} net = NWKernelRegression() @@ -414,20 +479,21 @@ animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5]) for epoch in range(5): with tf.GradientTape() as t: - loss = loss_object(y_train, net(x_train, keys, values))/2 * len(y_train) # 与d2l书保持一致 + loss = loss_object(y_train, net(x_train, keys, values))/2 * len(y_train) grads = t.gradient(loss, net.trainable_variables) optimizer.apply_gradients(zip(grads, net.trainable_variables)) print(f'epoch {epoch + 1}, loss {float(loss):.6f}') animator.add(epoch + 1, float(loss)) ``` - -训练完带参数的注意力汇聚模型后,我们发现,在尝试拟合带噪声的训练数据时,[**预测结果绘制**]的线不如之前非参数模型的线平滑。 +如下所示,训练完带参数的注意力汇聚模型后,我们发现: +在尝试拟合带噪声的训练数据时, +[**预测结果绘制**]的线不如之前非参数模型的平滑。 ```{.python .input} -# `keys` 的形状: (`n_test`, `n_train`), 每一行包含着相同的训练输入(例如:相同的键) +# `keys` 的形状: (`n_test`,`n_train`),每一行包含着相同的训练输入(例如,相同的键) keys = np.tile(x_train, (n_test, 1)) -# `value` 的形状: (`n_test`, `n_train`) +# `value` 的形状: (`n_test`,`n_train`) values = np.tile(y_train, (n_test, 1)) y_hat = net(x_test, keys, values) plot_kernel_reg(y_hat) @@ -435,9 +501,9 @@ plot_kernel_reg(y_hat) ```{.python .input} #@tab pytorch -# `keys` 的形状: (`n_test`, `n_train`), 每一行包含着相同的训练输入(例如:相同的键) +# `keys` 的形状: (`n_test`,`n_train`),每一行包含着相同的训练输入(例如,相同的键) keys = x_train.repeat((n_test, 1)) -# `value` 的形状: (`n_test`, `n_train`) +# `value` 的形状: (`n_test`,`n_train`) values = y_train.repeat((n_test, 1)) y_hat = net(x_test, keys, values).unsqueeze(1).detach() plot_kernel_reg(y_hat) @@ -445,19 +511,23 @@ plot_kernel_reg(y_hat) ```{.python .input} #@tab tensorflow -# `keys` 的形状: (`n_test`, `n_train`), 每一行包含着相同的训练输入(例如:相同的键) +# `keys` 的形状: (`n_test`,`n_train`),每一行包含着相同的训练输入(例如,相同的键) keys = tf.repeat(tf.expand_dims(x_train, axis=0), repeats=n_test, axis=0) -# `value` 的形状: (`n_test`, `n_train`) +# `value` 的形状: (`n_test`,`n_train`) values = tf.repeat(tf.expand_dims(y_train, axis=0), repeats=n_test, axis=0) y_hat = net(x_test, keys, values) plot_kernel_reg(y_hat) ``` - -与非参数的注意力汇聚模型相比,带参数的模型加入可学习的参数后,在输出结果的绘制图上,[**曲线在注意力权重较大的区域变得更不平滑**]。 +为什么新的模型更不平滑了呢? +我们看一下输出结果的绘制图: +与非参数的注意力汇聚模型相比, +带参数的模型加入可学习的参数后, +[**曲线在注意力权重较大的区域变得更不平滑**]。 ```{.python .input} -d2l.show_heatmaps(np.expand_dims(np.expand_dims(net.attention_weights, 0), 0), +d2l.show_heatmaps(np.expand_dims( + np.expand_dims(net.attention_weights, 0), 0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') ``` @@ -471,20 +541,21 @@ d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0), ```{.python .input} #@tab tensorflow -d2l.show_heatmaps(tf.expand_dims(tf.expand_dims(net.attention_weights, axis=0), axis=0), +d2l.show_heatmaps(tf.expand_dims( + tf.expand_dims(net.attention_weights, axis=0), axis=0), xlabel='Sorted training inputs', ylabel='Sorted testing inputs') ``` ## 小结 -* Nadaraya-Watson核回归是具有注意力机制的机器学习的一个例子。 +* Nadaraya-Watson核回归是具有注意力机制的机器学习范例。 * Nadaraya-Watson核回归的注意力汇聚是对训练数据中输出的加权平均。从注意力的角度来看,分配给每个值的注意力权重取决于将值所对应的键和查询作为输入的函数。 * 注意力汇聚可以分为非参数型和带参数型。 ## 练习 -1. 增加训练数据的样本数量。能否得到更好的非参数的Nadaraya-Watson核回归模型? +1. 增加训练数据的样本数量,你能否得到更好的非参数的Nadaraya-Watson核回归模型? 1. 在带参数的注意力汇聚的实验中学习得到的参数$w$的价值是什么?为什么在可视化注意力权重时,它会使加权区域更加尖锐? 1. 如何将超参数添加到非参数的Nadaraya-Watson核回归中以实现更好地预测结果? 1. 为本节的核回归设计一个新的带参数的注意力汇聚模型。训练这个新模型并可视化其注意力权重。 From c43e906555a0bd8937efbf639c2c8cfe7e267afb Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Sat, 20 Nov 2021 09:26:00 -0800 Subject: [PATCH 4/8] bahdanau polish --- .../bahdanau-attention.md | 90 ++++++++++++++----- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/chapter_attention-mechanisms/bahdanau-attention.md b/chapter_attention-mechanisms/bahdanau-attention.md index 3289e54b4..233ee6c97 100644 --- a/chapter_attention-mechanisms/bahdanau-attention.md +++ b/chapter_attention-mechanisms/bahdanau-attention.md @@ -1,21 +1,46 @@ # Bahdanau 注意力 :label:`sec_seq2seq_attention` -我们在 :numref:`sec_seq2seq`中探讨了机器翻译问题,在那里我们设计了一个基于两个循环神经网络的编码器-解码器架构,用于序列到序列学习。具体来说,循环神经网络编码器将可变长度序列转换为固定形状的上下文变量,然后循环神经网络解码器根据生成的词元和上下文变量按词元生成输出(目标)序列词元。但是,即使并非所有输入(源)词元都对解码某个词元都有用,但在每个解码步骤中仍使用编码整个输入序列的*相同*上下文变量。 - -在为给定文本序列生成手写的挑战中,格雷夫斯设计了一种可微注意力模型,将文本字符与更长的笔迹对齐,其中对齐方式仅向一个方向移动 :cite:`Graves.2013`。受学习对齐想法的启发,Bahdanau等人提出了一个没有严格的单向对齐限制 :cite:`Bahdanau.Cho.Bengio.2014`的可微注意力模型。在预测词元时,如果不是所有输入词元都相关,模型将仅对齐(或参与)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力集中的输出来实现的。 +我们在 :numref:`sec_seq2seq`中探讨了机器翻译问题: +通过设计一个基于两个循环神经网络的编码器-解码器架构, +用于序列到序列学习。 +具体来说,循环神经网络编码器将长度可变的序列转换为固定形状的上下文变量, +然后循环神经网络解码器根据生成的词元和上下文变量 +按词元生成输出(目标)序列词元。 +然而,即使并非所有输入(源)词元都对解码某个词元都有用, +在每个解码步骤中仍使用编码*相同*的上下文变量。 +有什么方法能改变上下文变量呢? + +我们试着从 :cite:`Graves.2013`中找到灵感: +在为给定文本序列生成手写的挑战中, +Graves设计了一种可微注意力模型, +将文本字符与更长的笔迹对齐, +其中对齐方式仅向一个方向移动。 +受学习对齐想法的启发,Bahdanau等人提出了一个没有严格单向对齐限制的 +可微注意力模型 :cite:`Bahdanau.Cho.Bengio.2014`。 +在预测词元时,如果不是所有输入词元都相关,模型将仅对齐(或参与)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力集中的输出来实现的。 ## 模型 -在下面描述Bahdanau注意力对循环神经网络编码器的关注时,我们将遵循 :numref:`sec_seq2seq`中的相同符号表达。新的基于注意力的模型与 :numref:`sec_seq2seq`中的模型相同,只不过 :eqref:`eq_seq2seq_s_t`中的上下文变量$\mathbf{c}$在任何解码时间步$t'$都会被$\mathbf{c}_{t'}$替换。假设输入序列中有$T$个词元,解码时间步$t'$的上下文变量是注意力集中的输出: +下面描述的Bahdanau注意力模型 +将遵循 :numref:`sec_seq2seq`中的相同符号表达。 +这个新的基于注意力的模型与 :numref:`sec_seq2seq`中的模型相同, +只不过 :eqref:`eq_seq2seq_s_t`中的上下文变量$\mathbf{c}$ +在任何解码时间步$t'$都会被$\mathbf{c}_{t'}$替换。 +假设输入序列中有$T$个词元, +解码时间步$t'$的上下文变量是注意力集中的输出: $$\mathbf{c}_{t'} = \sum_{t=1}^T \alpha(\mathbf{s}_{t' - 1}, \mathbf{h}_t) \mathbf{h}_t,$$ -其中,时间步$t' - 1$时的解码器隐状态$\mathbf{s}_{t' - 1}$是查询,编码器隐状态$\mathbf{h}_t$既是键,也是值,注意力权重$\alpha$是使用 :eqref:`eq_attn-scoring-alpha`所定义的加性注意力打分函数计算的。 +其中,时间步$t' - 1$时的解码器隐状态$\mathbf{s}_{t' - 1}$是查询, +编码器隐状态$\mathbf{h}_t$既是键,也是值, +注意力权重$\alpha$是使用 :eqref:`eq_attn-scoring-alpha` +所定义的加性注意力打分函数计算的。 -与 :numref:`fig_seq2seq_details`中的循环神经网络编码器-解码器架构略有不同, :numref:`fig_s2s_attention_details`描述了Bahdanau注意力的架构。 +与 :numref:`fig_seq2seq_details`中的循环神经网络编码器-解码器架构略有不同, + :numref:`fig_s2s_attention_details`描述了Bahdanau注意力的架构。 -![在一个带有Bahdanau注意力的循环神经网络编码器-解码器模型中的层。](../img/seq2seq-attention-details.svg) +![一个带有Bahdanau注意力的循环神经网络编码器-解码器模型](../img/seq2seq-attention-details.svg) :label:`fig_s2s_attention_details` ```{.python .input} @@ -40,13 +65,16 @@ import tensorflow as tf ## 定义注意力解码器 -要用Bahdanau注意力实现循环神经网络编码器-解码器,我们只需重新定义解码器即可。为了更方便地显示学习的注意力权重,以下`AttentionDecoder`类定义了[**带有注意力机制的解码器基本接口**]。 +下面我们看看如何定义Bahdanau注意力,实现循环神经网络编码器-解码器? +其实,我们只需重新定义解码器即可。 +为了更方便地显示学习的注意力权重, +以下`AttentionDecoder`类定义了[**带有注意力机制解码器的基本接口**]。 ```{.python .input} #@tab all #@save class AttentionDecoder(d2l.Decoder): - """带有注意力机制的解码器基本接口""" + """带有注意力机制解码器的基本接口""" def __init__(self, **kwargs): super(AttentionDecoder, self).__init__(**kwargs) @@ -55,7 +83,16 @@ class AttentionDecoder(d2l.Decoder): raise NotImplementedError ``` -接下来,让我们在接下来的`Seq2SeqAttentionDecoder`类中[**实现带有Bahdanau注意力的循环神经网络解码器**]。初始化解码器的状态(1)编码器在所有时间步的最终层隐状态(作为注意力的键和值);(2)最后一个时间步的编码器全层隐状态(初始化解码器的隐状态);(3)编码器有效长度(排除在注意力池中填充词元)。在每个解码时间步骤中,解码器上一个时间步的最终层隐状态将用作关注的查询。因此,注意力输出和输入嵌入都连结为循环神经网络解码器的输入。 +接下来,让我们在接下来的`Seq2SeqAttentionDecoder`类中 +[**实现带有Bahdanau注意力的循环神经网络解码器**]。 +首先,我们初始化解码器的状态,需要下面的输入: + +1. 编码器在所有时间步的最终层隐状态,将作为注意力的键和值; +1. 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态; +1. 编码器有效长度(排除在注意力池中填充词元)。 + +在每个解码时间步骤中,解码器上一个时间步的最终层隐状态将用作查询。 +因此,注意力输出和输入嵌入都连结为循环神经网络解码器的输入。 ```{.python .input} class Seq2SeqAttentionDecoder(AttentionDecoder): @@ -68,9 +105,8 @@ class Seq2SeqAttentionDecoder(AttentionDecoder): self.dense = nn.Dense(vocab_size, flatten=False) def init_state(self, enc_outputs, enc_valid_lens, *args): - # `outputs`的形状为 (`num_steps`, `batch_size`, `num_hiddens`). - # `hidden_state[0]`的形状为 (`num_layers`, `batch_size`, - # `num_hiddens`) + # `outputs`的形状为 (`num_steps`,`batch_size`,`num_hiddens`) + # `hidden_state[0]`的形状为 (`num_layers`,`batch_size`,`num_hiddens`) outputs, hidden_state = enc_outputs return (outputs.swapaxes(0, 1), hidden_state, enc_valid_lens) @@ -120,9 +156,8 @@ class Seq2SeqAttentionDecoder(AttentionDecoder): self.dense = nn.Linear(num_hiddens, vocab_size) def init_state(self, enc_outputs, enc_valid_lens, *args): - # `enc_outputs`的形状为 (`batch_size`, `num_steps`, `num_hiddens`). - # `hidden_state`的形状为 (`num_layers`, `batch_size`, - # `num_hiddens`) + # `enc_outputs`的形状为 (`batch_size`,`num_steps`,`num_hiddens`). + # `hidden_state`的形状为 (`num_layers`,`batch_size`,`num_hiddens`) outputs, hidden_state = enc_outputs return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens) @@ -169,13 +204,14 @@ class Seq2SeqAttentionDecoder(AttentionDecoder): self.rnn = tf.keras.layers.RNN(tf.keras.layers.StackedRNNCells( [tf.keras.layers.GRUCell(num_hiddens, dropout=dropout) for _ in range(num_layers)]), - return_sequences=True, return_state=True) + return_sequences=True, + return_state=True) self.dense = tf.keras.layers.Dense(vocab_size) def init_state(self, enc_outputs, enc_valid_lens, *args): - # `outputs`的形状为 (`num_steps`, `batch_size`, `num_hiddens`). - # `hidden_state[0]`的形状为 (`num_layers`, `batch_size`, - # `num_hiddens`) + # `outputs`的形状为 (`num_steps`,`batch_size`,`num_hiddens`) + # `hidden_state[0]`的形状为 (`num_layers`,`batch_size`,`num_hiddens`) + outputs, hidden_state = enc_outputs return (outputs, hidden_state, enc_valid_lens) @@ -253,7 +289,11 @@ output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape ## [**训练**] -与 :numref:`sec_seq2seq_training`类似,我们在这里指定超参数,实例化一个带有Bahdanau注意力的编码器和解码器,并对这个模型进行机器翻译训练。由于新增的注意力机制,这项训练要比没有注意力机制的 :numref:`sec_seq2seq_training`慢得多。 +与 :numref:`sec_seq2seq_training`类似, +我们在这里指定超参数,实例化一个带有Bahdanau注意力的编码器和解码器, +并对这个模型进行机器翻译训练。 +由于新增的注意力机制,训练要比没有注意力机制的 + :numref:`sec_seq2seq_training`慢得多。 ```{.python .input} #@tab all @@ -301,7 +341,9 @@ attention_weights = d2l.reshape( (1, 1, -1, num_steps)) ``` -训练结束后通过[**可视化注意力权重**],我们可以看到,每个查询都会在键值对上分配不同的权重。它显示,在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。 +训练结束后,下面我们通过[**可视化注意力权重**] +你会发现,每个查询都会在键值对上分配不同的权重,这说明 +在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。 ```{.python .input} # 加上一个包含序列结束词元 @@ -328,12 +370,12 @@ d2l.show_heatmaps(attention_weights[:, :, :, :len(engs[-1].split()) + 1], ## 小结 * 在预测词元时,如果不是所有输入词元都是相关的,那么具有Bahdanau注意力的循环神经网络编码器-解码器会有选择地统计输入序列的不同部分。这是通过将上下文变量视为加性注意力池化的输出来实现的。 -* 在循环神经网络编码器-解码器中,Bahdanau注意力将上一个时间步的解码器隐状态视为查询,在所有时间步的编码器隐状态同时视为键和值。 +* 在循环神经网络编码器-解码器中,Bahdanau注意力将上一时间步的解码器隐状态视为查询,在所有时间步的编码器隐状态同时视为键和值。 ## 练习 1. 在实验中用LSTM替换GRU。 -1. 修改实验以将加性注意力打分函数替换为缩放点积注意力。它如何影响训练效率? +1. 修改实验以将加性注意力打分函数替换为缩放点积注意力,它如何影响训练效率? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/347) From cc4c2d9a5d03ad92f776a9bfd91c262873c8a867 Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Sat, 20 Nov 2021 09:26:23 -0800 Subject: [PATCH 5/8] multihead polish --- .../multihead-attention.md | 138 ++++++++++++------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/chapter_attention-mechanisms/multihead-attention.md b/chapter_attention-mechanisms/multihead-attention.md index 4d2f8a51e..414bec4b4 100644 --- a/chapter_attention-mechanisms/multihead-attention.md +++ b/chapter_attention-mechanisms/multihead-attention.md @@ -1,24 +1,55 @@ # 多头注意力 :label:`sec_multihead-attention` -在实践中,当给定相同的查询、键和值的集合时,我们希望模型可以基于相同的注意力机制学习到不同的行为,然后将不同的行为作为知识组合起来,例如捕获序列内各种范围的依赖关系(例如,短距离依赖和长距离依赖)。因此,允许注意力机制组合使用查询、键和值的不同*子空间表示*(representation subspaces)可能是有益的。 - -为此,与使用单独一个注意力汇聚不同,我们可以用独立学习得到的$h$组不同的*线性投影*(linear projections)来变换查询、键和值。然后,这$h$组变换后的查询、键和值将并行地送到注意力汇聚中。最后,将这$h$个注意力汇聚的输出拼接在一起,并且通过另一个可以学习的线性投影进行变换,以产生最终输出。这种设计被称为*多头注意力*,其中$h$个注意力汇聚输出中的每一个输出都被称作一个*头*(head) :cite:`Vaswani.Shazeer.Parmar.ea.2017`。 :numref:`fig_multi-head-attention`展示了使用全连接层来实现可学习的线性变换的多头注意力。 - -![多头注意力,多个头连结然后线性变换。](../img/multi-head-attention.svg) +在实践中,当给定相同的查询、键和值的集合时, +我们希望模型可以基于相同的注意力机制学习到不同的行为, +然后将不同的行为作为知识组合起来, +捕获序列内各种范围的依赖关系 +(例如,短距离依赖和长距离依赖关系)。 +因此,允许注意力机制组合使用查询、键和值的不同 +*子空间表示*(representation subspaces)可能是有益的。 + +为此,与其只使用单独一个注意力汇聚, +我们可以用独立学习得到的$h$组不同的 +*线性投影*(linear projections)来变换查询、键和值。 +然后,这$h$组变换后的查询、键和值将并行地送到注意力汇聚中。 +最后,将这$h$个注意力汇聚的输出拼接在一起, +并且通过另一个可以学习的线性投影进行变换, +以产生最终输出。 +这种设计被称为*多头注意力*(multihead attention) + :cite:`Vaswani.Shazeer.Parmar.ea.2017`。 +对于$h$个注意力汇聚输出,每一个注意力汇聚都被称作一个*头*(head)。 + :numref:`fig_multi-head-attention` +展示了使用全连接层来实现可学习的线性变换的多头注意力。 + +![多头注意力:多个头连结然后线性变换](../img/multi-head-attention.svg) :label:`fig_multi-head-attention` ## 模型 -在实现多头注意力之前,让我们用数学语言将这个模型形式化地描述出来。给定查询$\mathbf{q} \in \mathbb{R}^{d_q}$、键$\mathbf{k} \in \mathbb{R}^{d_k}$和值$\mathbf{v} \in \mathbb{R}^{d_v}$,每个注意力头$\mathbf{h}_i$($i = 1, \ldots, h$)的计算方法为: +在实现多头注意力之前,让我们用数学语言将这个模型形式化地描述出来。 +给定查询$\mathbf{q} \in \mathbb{R}^{d_q}$、 +键$\mathbf{k} \in \mathbb{R}^{d_k}$和 +值$\mathbf{v} \in \mathbb{R}^{d_v}$, +每个注意力头$\mathbf{h}_i$($i = 1, \ldots, h$)的计算方法为: $$\mathbf{h}_i = f(\mathbf W_i^{(q)}\mathbf q, \mathbf W_i^{(k)}\mathbf k,\mathbf W_i^{(v)}\mathbf v) \in \mathbb R^{p_v},$$ -其中,可学习的参数包括$\mathbf W_i^{(q)}\in\mathbb R^{p_q\times d_q}$、$\mathbf W_i^{(k)}\in\mathbb R^{p_k\times d_k}$和$\mathbf W_i^{(v)}\in\mathbb R^{p_v\times d_v}$,以及代表注意力汇聚的函数$f$。$f$可以是 :numref:`sec_attention-scoring-functions`中的加性注意力和缩放点积注意力。多头注意力的输出需要经过另一个线性转换,它对应着$h$个头连结后的结果,因此其可学习参数是$\mathbf W_o\in\mathbb R^{p_o\times h p_v}$: +其中,可学习的参数包括 +$\mathbf W_i^{(q)}\in\mathbb R^{p_q\times d_q}$、 +$\mathbf W_i^{(k)}\in\mathbb R^{p_k\times d_k}$和 +$\mathbf W_i^{(v)}\in\mathbb R^{p_v\times d_v}$, +以及代表注意力汇聚的函数$f$。 +$f$可以是 :numref:`sec_attention-scoring-functions`中的 +加性注意力和缩放点积注意力。 +多头注意力的输出需要经过另一个线性转换, +它对应着$h$个头连结后的结果,因此其可学习参数是 +$\mathbf W_o\in\mathbb R^{p_o\times h p_v}$: $$\mathbf W_o \begin{bmatrix}\mathbf h_1\\\vdots\\\mathbf h_h\end{bmatrix} \in \mathbb{R}^{p_o}.$$ -基于这种设计,每个头都可能会关注输入的不同部分。可以表示比简单加权平均值更复杂的函数。 +基于这种设计,每个头都可能会关注输入的不同部分, +可以表示比简单加权平均值更复杂的函数。 ```{.python .input} from d2l import mxnet as d2l @@ -44,7 +75,13 @@ import tensorflow as tf ## 实现 -在实现过程中,我们[**选择缩放点积注意力作为每一个注意力头**]。为了避免计算代价和参数代价的大幅增长,我们设定$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 @@ -61,12 +98,12 @@ class MultiHeadAttention(nn.Block): self.W_o = nn.Dense(num_hiddens, use_bias=use_bias, flatten=False) def forward(self, queries, keys, values, valid_lens): - # `queries`, `keys`, `values` 的形状: - # (`batch_size`, 查询或者“键-值”对的个数, `num_hiddens`) + # `queries`,`keys`,`values` 的形状: + # (`batch_size`,查询或者“键-值”对的个数,`num_hiddens`) # `valid_lens` 的形状: - # (`batch_size`,) 或 (`batch_size`, 查询的个数) - # 经过变换后,输出的 `queries`, `keys`, `values` 的形状: - # (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, + # (`batch_size`,) 或 (`batch_size`,查询的个数) + # 经过变换后,输出的 `queries`,`keys`,`values` 的形状: + # (`batch_size` * `num_heads`,查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) queries = transpose_qkv(self.W_q(queries), self.num_heads) keys = transpose_qkv(self.W_k(keys), self.num_heads) @@ -77,11 +114,11 @@ class MultiHeadAttention(nn.Block): # 然后如此复制第二项,然后诸如此类。 valid_lens = valid_lens.repeat(self.num_heads, axis=0) - # `output` 的形状: (`batch_size` * `num_heads`, 查询的个数, + # `output` 的形状: (`batch_size` * `num_heads`,查询的个数, # `num_hiddens` / `num_heads`) output = self.attention(queries, keys, values, valid_lens) - # `output_concat` 的形状: (`batch_size`, 查询的个数, `num_hiddens`) + # `output_concat` 的形状: (`batch_size`,查询的个数,`num_hiddens`) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) ``` @@ -102,12 +139,12 @@ class MultiHeadAttention(nn.Module): self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias) def forward(self, queries, keys, values, valid_lens): - # `queries`, `keys`, `values` 的形状: - # (`batch_size`, 查询或者“键-值”对的个数, `num_hiddens`) + # `queries`,`keys`,`values` 的形状: + # (`batch_size`,查询或者“键-值”对的个数,`num_hiddens`) # `valid_lens` 的形状: - # (`batch_size`,) 或 (`batch_size`, 查询的个数) - # 经过变换后,输出的 `queries`, `keys`, `values` 的形状: - # (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, + # (`batch_size`,) 或 (`batch_size`,查询的个数) + # 经过变换后,输出的 `queries`,`keys`,`values` 的形状: + # (`batch_size` * `num_heads`,查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) queries = transpose_qkv(self.W_q(queries), self.num_heads) keys = transpose_qkv(self.W_k(keys), self.num_heads) @@ -119,11 +156,11 @@ class MultiHeadAttention(nn.Module): valid_lens = torch.repeat_interleave( valid_lens, repeats=self.num_heads, dim=0) - # `output` 的形状: (`batch_size` * `num_heads`, 查询的个数, + # `output` 的形状: (`batch_size` * `num_heads`,查询的个数, # `num_hiddens` / `num_heads`) output = self.attention(queries, keys, values, valid_lens) - # `output_concat` 的形状: (`batch_size`, 查询的个数, `num_hiddens`) + # `output_concat` 的形状: (`batch_size`,查询的个数,`num_hiddens`) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) ``` @@ -144,12 +181,12 @@ class MultiHeadAttention(tf.keras.layers.Layer): self.W_o = tf.keras.layers.Dense(num_hiddens, use_bias=bias) def call(self, queries, keys, values, valid_lens, **kwargs): - # `queries`, `keys`, `values` 的形状: - # (`batch_size`, 查询或者“键-值”对的个数, `num_hiddens`) + # `queries`,`keys`,`values` 的形状: + # (`batch_size`,查询或者“键-值”对的个数,`num_hiddens`) # `valid_lens` 的形状: - # (`batch_size`,) 或 (`batch_size`, 查询的个数) - # 经过变换后,输出的 `queries`, `keys`, `values` 的形状: - # (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, + # (`batch_size`,) 或 (`batch_size`,查询的个数) + # 经过变换后,输出的 `queries`,`keys`,`values` 的形状: + # (`batch_size` * `num_heads`,查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) queries = transpose_qkv(self.W_q(queries), self.num_heads) keys = transpose_qkv(self.W_k(keys), self.num_heads) @@ -160,31 +197,33 @@ class MultiHeadAttention(tf.keras.layers.Layer): # 然后如此复制第二项,然后诸如此类。 valid_lens = tf.repeat(valid_lens, repeats=self.num_heads, axis=0) - # `output` 的形状: (`batch_size` * `num_heads`, 查询的个数, + # `output` 的形状: (`batch_size` * `num_heads`,查询的个数, # `num_hiddens` / `num_heads`) output = self.attention(queries, keys, values, valid_lens, **kwargs) - # `output_concat` 的形状: (`batch_size`, 查询的个数, `num_hiddens`) + # `output_concat` 的形状: (`batch_size`,查询的个数,`num_hiddens`) output_concat = transpose_output(output, self.num_heads) return self.W_o(output_concat) ``` -为了能够[**使多个头并行计算**],上面的`MultiHeadAttention`类使用了下面定义的两个转置函数。具体来说,`transpose_output`函数反转了`transpose_qkv`函数的操作。 +为了能够[**使多个头并行计算**], +上面的`MultiHeadAttention`类将使用下面定义的两个转置函数。 +具体来说,`transpose_output`函数反转了`transpose_qkv`函数的操作。 ```{.python .input} #@save def transpose_qkv(X, num_heads): """为了多注意力头的并行计算而变换形状。""" - # 输入 `X` 的形状: (`batch_size`, 查询或者“键-值”对的个数, `num_hiddens`). - # 输出 `X` 的形状: (`batch_size`, 查询或者“键-值”对的个数, `num_heads`, + # 输入 `X` 的形状: (`batch_size`,查询或者“键-值”对的个数,`num_hiddens`) + # 输出 `X` 的形状: (`batch_size`,查询或者“键-值”对的个数,`num_heads`, # `num_hiddens` / `num_heads`) X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) - # 输出 `X` 的形状: (`batch_size`, `num_heads`, 查询或者“键-值”对的个数, + # 输出 `X` 的形状: (`batch_size`,`num_heads`,查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) X = X.transpose(0, 2, 1, 3) - # `output` 的形状: (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, + # 最终输出的形状: (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) return X.reshape(-1, X.shape[2], X.shape[3]) @@ -202,16 +241,16 @@ def transpose_output(X, num_heads): #@save def transpose_qkv(X, num_heads): """为了多注意力头的并行计算而变换形状。""" - # 输入 `X` 的形状: (`batch_size`, 查询或者“键-值”对的个数, `num_hiddens`). - # 输出 `X` 的形状: (`batch_size`, 查询或者“键-值”对的个数, `num_heads`, + # 输入 `X` 的形状: (`batch_size`,查询或者“键-值”对的个数,`num_hiddens`) + # 输出 `X` 的形状: (`batch_size`,查询或者“键-值”对的个数,`num_heads`, # `num_hiddens` / `num_heads`) X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) - # 输出 `X` 的形状: (`batch_size`, `num_heads`, 查询或者“键-值”对的个数, + # 输出 `X` 的形状: (`batch_size`,`num_heads`,查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) X = X.permute(0, 2, 1, 3) - # `output` 的形状: (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, + # 最终输出的形状: (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) return X.reshape(-1, X.shape[2], X.shape[3]) @@ -229,16 +268,16 @@ def transpose_output(X, num_heads): #@save def transpose_qkv(X, num_heads): """为了多注意力头的并行计算而变换形状。""" - # 输入 `X` 的形状: (`batch_size`, 查询或者“键-值”对的个数, `num_hiddens`). - # 输出 `X` 的形状: (`batch_size`, 查询或者“键-值”对的个数, `num_heads`, + # 输入 `X` 的形状: (`batch_size`,查询或者“键-值”对的个数,`num_hiddens`) + # 输出 `X` 的形状: (`batch_size`,查询或者“键-值”对的个数,`num_heads`, # `num_hiddens` / `num_heads`) X = tf.reshape(X, shape=(X.shape[0], X.shape[1], num_heads, -1)) - # 输出 `X` 的形状: (`batch_size`, `num_heads`, 查询或者“键-值”对的个数, + # 输出 `X` 的形状: (`batch_size`,`num_heads`,查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) X = tf.transpose(X, perm=(0, 2, 1, 3)) - # `output` 的形状: (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, + # 最终输出的形状: (`batch_size` * `num_heads`, 查询或者“键-值”对的个数, # `num_hiddens` / `num_heads`) return tf.reshape(X, shape=(-1, X.shape[2], X.shape[3])) @@ -251,7 +290,8 @@ def transpose_output(X, num_heads): return tf.reshape(X, shape=(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 @@ -276,7 +316,8 @@ attention = MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens, ```{.python .input} #@tab mxnet, pytorch -batch_size, num_queries, num_kvpairs, valid_lens = 2, 4, 6, d2l.tensor([3, 2]) +batch_size, num_queries = 2, 4 +num_kvpairs, valid_lens = 6, d2l.tensor([3, 2]) X = d2l.ones((batch_size, num_queries, num_hiddens)) Y = d2l.ones((batch_size, num_kvpairs, num_hiddens)) attention(X, Y, Y, valid_lens).shape @@ -284,7 +325,8 @@ attention(X, Y, Y, valid_lens).shape ```{.python .input} #@tab tensorflow -batch_size, num_queries, num_kvpairs, valid_lens = 2, 4, 6, d2l.tensor([3, 2]) +batch_size, num_queries = 2, 4 +num_kvpairs, valid_lens = 6, d2l.tensor([3, 2]) X = tf.ones((batch_size, num_queries, num_hiddens)) Y = tf.ones((batch_size, num_kvpairs, num_hiddens)) attention(X, Y, Y, valid_lens, training=False).shape @@ -292,13 +334,13 @@ attention(X, Y, Y, valid_lens, training=False).shape ## 小结 -* 多头注意力融合了来自于相同的注意力汇聚产生的不同的知识,这些知识的不同来源于相同的查询、键和值的不同的子空间表示。 +* 多头注意力融合了来自于多个注意力汇聚的不同知识,这些知识的不同来源于相同的查询、键和值的不同的子空间表示。 * 基于适当的张量操作,可以实现多头注意力的并行计算。 ## 练习 1. 分别可视化这个实验中的多个头的注意力权重。 -1. 假设我们已经拥有一个完成训练的基于多头注意力的模型,现在希望修剪最不重要的注意力头以提高预测速度。应该如何设计实验来衡量注意力头的重要性? +1. 假设我们有一个完成训练的基于多头注意力的模型,现在希望修剪最不重要的注意力头以提高预测速度。如何设计实验来衡量注意力头的重要性呢? :begin_tab:`mxnet` [Discussions](https://discuss.d2l.ai/t/1634) From 1746785168b1d2d6c26d31acdadf1d343a9935ef Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Sat, 20 Nov 2021 10:09:55 -0800 Subject: [PATCH 6/8] index polish done --- chapter_attention-mechanisms/index.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/chapter_attention-mechanisms/index.md b/chapter_attention-mechanisms/index.md index fd2cb9e7e..3c75ee8e2 100644 --- a/chapter_attention-mechanisms/index.md +++ b/chapter_attention-mechanisms/index.md @@ -1,13 +1,29 @@ # 注意力机制 :label:`chap_attention` -灵长类动物的视觉系统中的视神经接受了大量的感官输入。这些感官输入远远超过了大脑能够完全处理的程度。幸运的是,并非所有刺激的影响都是相等的。意识的聚集和专注使灵长类动物能够在复杂的视觉环境中将注意力引向感兴趣的物体,例如猎物和天敌。只关注一小部分信息的能力对进化富有意义,使人类得以生存和成功。 +灵长类动物的视觉系统接受了大量的感官输入, +这些感官输入远远超过了大脑能够完全处理的程度。 +然而,并非所有刺激的影响都是相等的。 +意识的聚集和专注使灵长类动物能够在复杂的视觉环境中将注意力引向感兴趣的物体,例如猎物和天敌。 +只关注一小部分信息的能力对进化更加有意义,使人类得以生存和成功。 -自19世纪以来,科学家们一直在研究认知神经科学领域的注意力。在本章中,我们将首先回顾一个热门框架,解释如何在视觉场景中展开注意力。受此框架中的*注意力提示*(attention cues)的启发,我们将设计能够利用这些注意力提示的模型。1964年的Nadaraya-Waston核回归(kernel regression)正是具有*注意力机制*(attention mechanisms)的机器学习的简单演示。 +自19世纪以来,科学家们一直致力于研究认知神经科学领域的注意力。 +本章的很多章节将涉及到这些研究: +我们将首先回顾一个经典注意力框架,解释如何在视觉场景中展开注意力。 +受此框架中的*注意力提示*(attention cues)的启发, +我们将设计能够利用这些注意力提示的模型。 +1964年的Nadaraya-Waston核回归(kernel regression)正是具有 +*注意力机制*(attention mechanism)的机器学习的简单演示。 -然后,我们继续介绍的是注意力函数,它们在深度学习的注意力模型设计中被广泛使用。具体来说,我们将展示如何使用这些函数来设计*Bahdanau注意力*。Bahdanau注意力是深度学习中的具有突破性价值的注意力模型,它是双向对齐的并且可以微分。 +然后,我们继续介绍的是注意力函数,它们在深度学习的注意力模型设计中被广泛使用。 +具体来说,我们将展示如何使用这些函数来设计*Bahdanau注意力*。 +Bahdanau注意力是深度学习中的具有突破性价值的注意力模型,它双向对齐并且可以微分。 -最后,我们将描述仅仅基于注意力机制的*transformer*架构,该架构中使用了*多头注意力*(multi-head attention)和*自注意力*(self-attention)设计。自2017年被构想出来,transformer一直都普遍存在于现代的深度学习应用中,例如语言、视觉、语音和强化学习领域。 +最后,我们将描述仅仅基于注意力机制的*transformer*架构, +该架构中使用了*多头注意力*(multi-head attention) +和*自注意力*(self-attention)。 +自2017年横空出世,transformer一直都普遍存在于现代的深度学习应用中, +例如语言、视觉、语音和强化学习领域。 ```toc :maxdepth: 2 From a2da5585b9994a6e9bbfbddefbf32f2ea8c9c83d Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Sat, 20 Nov 2021 19:32:32 -0800 Subject: [PATCH 7/8] self attention polish --- .../self-attention-and-positional-encoding.md | 104 ++++++++++++++---- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/chapter_attention-mechanisms/self-attention-and-positional-encoding.md b/chapter_attention-mechanisms/self-attention-and-positional-encoding.md index 484e4805f..1770c6ecc 100644 --- a/chapter_attention-mechanisms/self-attention-and-positional-encoding.md +++ b/chapter_attention-mechanisms/self-attention-and-positional-encoding.md @@ -1,8 +1,15 @@ # 自注意力和位置编码 :label:`sec_self-attention-and-positional-encoding` -在深度学习中,我们经常使用卷积神经网络(CNN)或循环神经网络(RNN)对序列进行编码。现在想象一下,有了注意力机制之后,我们将词元序列输入注意力池化中,以便同一组词元同时充当查询、键和值。具体来说,每个查询都会关注所有的键-值对并生成一个注意力输出。由于查询、键和值来自同一组输入,因此被称为 -*自注意力*(self-attention :cite:`Lin.Feng.Santos.ea.2017,Vaswani.Shazeer.Parmar.ea.2017`,也被称为*内部注意力*(intra-attention) :cite:`Cheng.Dong.Lapata.2016,Parikh.Tackstrom.Das.ea.2016,Paulus.Xiong.Socher.2017`。在本节中,我们将讨论使用自注意力进行序列编码,包括使用序列的顺序作为补充信息。 +在深度学习中,我们经常使用卷积神经网络(CNN)或循环神经网络(RNN)对序列进行编码。 +想象一下,有了注意力机制之后,我们将词元序列输入注意力池化中, +以便同一组词元同时充当查询、键和值。 +具体来说,每个查询都会关注所有的键-值对并生成一个注意力输出。 +由于查询、键和值来自同一组输入,因此被称为 +*自注意力*(self-attention) + :cite:`Lin.Feng.Santos.ea.2017,Vaswani.Shazeer.Parmar.ea.2017`, +也被称为*内部注意力*(intra-attention) :cite:`Cheng.Dong.Lapata.2016,Parikh.Tackstrom.Das.ea.2016,Paulus.Xiong.Socher.2017`。 +在本节中,我们将使用自注意力进行序列编码,以及如何使用序列的顺序作为补充信息。 ```{.python .input} from d2l import mxnet as d2l @@ -29,11 +36,17 @@ import tensorflow as tf ## [**自注意力**] -给定一个由词元组成的输入序列$\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$,其中: +给定一个由词元组成的输入序列$\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$,其中: $$\mathbf{y}_i = f(\mathbf{x}_i, (\mathbf{x}_1, \mathbf{x}_1), \ldots, (\mathbf{x}_n, \mathbf{x}_n)) \in \mathbb{R}^d$$ -根据 :eqref:`eq_attn-pooling`中定义的注意力池化函数$f$。下面的代码片段是基于多头注意力对一个张量完成自注意力的计算,张量的形状为(批量大小, 时间步的数目或词元序列的长度, $d$)。输出与输入的张量形状相同。 +根据 :eqref:`eq_attn-pooling`中定义的注意力池化函数$f$。 +下面的代码片段是基于多头注意力对一个张量完成自注意力的计算, +张量的形状为(批量大小,时间步的数目或词元序列的长度,$d$)。 +输出与输入的张量形状相同。 ```{.python .input} num_hiddens, num_heads = 100, 5 @@ -75,28 +88,61 @@ attention(X, X, X, valid_lens, training=False).shape 让我们比较下面几个架构,目标都是将由$n$个词元组成的序列映射到另一个长度相等的序列,其中的每个输入词元或输出词元都由$d$维向量表示。具体来说,我们将比较的是卷积神经网络、循环神经网络和自注意力这几个架构的计算复杂性、顺序操作和最大路径长度。请注意,顺序操作会妨碍并行计算,而任意的序列位置组合之间的路径越短,则能更轻松地学习序列中的远距离依赖关系 :cite:`Hochreiter.Bengio.Frasconi.ea.2001`。 -![比较卷积神经网络(填充词元被忽略)、循环神经网络和自注意力三种架构。](../img/cnn-rnn-self-attention.svg) +![比较卷积神经网络(填充词元被忽略)、循环神经网络和自注意力三种架构](../img/cnn-rnn-self-attention.svg) :label:`fig_cnn-rnn-self-attention` -考虑一个卷积核大小为$k$的卷积层。我们将在后面的章节中提供关于使用卷积神经网络处理序列的更多详细信息。目前,我们只需要知道,由于序列长度是$n$,输入和输出的通道数量都是$d$,所以卷积层的计算复杂度为$\mathcal{O}(knd^2)$。如 :numref:`fig_cnn-rnn-self-attention`所示,卷积神经网络是分层的,因此为有$\mathcal{O}(1)$个顺序操作,最大路径长度为$\mathcal{O}(n/k)$。例如,$\mathbf{x}_1$和$\mathbf{x}_5$处于 :numref:`fig_cnn-rnn-self-attention`中卷积核大小为3的双层卷积神经网络的感受野内。 - -当更新循环神经网络的隐状态时,$d \times d$权重矩阵和$d$维隐状态的乘法计算复杂度为$\mathcal{O}(d^2)$。由于序列长度为$n$,因此循环神经网络层的计算复杂度为$\mathcal{O}(nd^2)$。根据 :numref:`fig_cnn-rnn-self-attention`,有$\mathcal{O}(n)$个顺序操作无法并行化,最大路径长度也是$\mathcal{O}(n)$。 - -在自注意力中,查询、键和值都是$n \times d$矩阵。考虑 :eqref:`eq_softmax_QK_V`中缩放的”点-积“注意力,其中$n \times d$矩阵乘以$d \times n$矩阵,然后输出的$n \times n$矩阵乘以$n \times d$矩阵。因此,自注意力具有$\mathcal{O}(n^2d)$计算复杂性。正如我们在 :numref:`fig_cnn-rnn-self-attention`中看到的那样,每个词元都通过自注意力直接连接到任何其他词元。因此,有$\mathcal{O}(1)$个顺序操作可以并行计算,最大路径长度也是$\mathcal{O}(1)$。 - -总而言之,卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。 +考虑一个卷积核大小为$k$的卷积层。 +我们将在后面的章节中提供关于使用卷积神经网络处理序列的更多详细信息。 +目前,我们只需要知道,由于序列长度是$n$,输入和输出的通道数量都是$d$, +所以卷积层的计算复杂度为$\mathcal{O}(knd^2)$。 +如 :numref:`fig_cnn-rnn-self-attention`所示, +卷积神经网络是分层的,因此为有$\mathcal{O}(1)$个顺序操作, +最大路径长度为$\mathcal{O}(n/k)$。 +例如,$\mathbf{x}_1$和$\mathbf{x}_5$处于 + :numref:`fig_cnn-rnn-self-attention`中卷积核大小为3的双层卷积神经网络的感受野内。 + +当更新循环神经网络的隐状态时, +$d \times d$权重矩阵和$d$维隐状态的乘法计算复杂度为$\mathcal{O}(d^2)$。 +由于序列长度为$n$,因此循环神经网络层的计算复杂度为$\mathcal{O}(nd^2)$。 +根据 :numref:`fig_cnn-rnn-self-attention`, +有$\mathcal{O}(n)$个顺序操作无法并行化,最大路径长度也是$\mathcal{O}(n)$。 + +在自注意力中,查询、键和值都是$n \times d$矩阵。 +考虑 :eqref:`eq_softmax_QK_V`中缩放的”点-积“注意力, +其中$n \times d$矩阵乘以$d \times n$矩阵。 +之后输出的$n \times n$矩阵乘以$n \times d$矩阵。 +因此,自注意力具有$\mathcal{O}(n^2d)$计算复杂性。 +正如我们在 :numref:`fig_cnn-rnn-self-attention`中看到的那样, +每个词元都通过自注意力直接连接到任何其他词元。 +因此,有$\mathcal{O}(1)$个顺序操作可以并行计算, +最大路径长度也是$\mathcal{O}(1)$。 + +总而言之,卷积神经网络和自注意力都拥有并行计算的优势, +而且自注意力的最大路径长度最短。 +但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。 ## [**位置编码**] :label:`subsec_positional-encoding` -在处理词元序列时,循环神经网络是逐个的重复地处理词元的,而自注意力则因为并行计算而放弃了顺序操作。为了使用序列的顺序信息,我们通过在输入表示中添加*位置编码*(positional encoding)来注入绝对的或相对的位置信息。位置编码可以通过学习得到也可以直接固定得到。接下来,我们描述的是基于正弦函数和余弦函数的固定位置编码 :cite:`Vaswani.Shazeer.Parmar.ea.2017`。 +在处理词元序列时,循环神经网络是逐个的重复地处理词元的, +而自注意力则因为并行计算而放弃了顺序操作。 +为了使用序列的顺序信息,我们通过在输入表示中添加 +*位置编码*(positional encoding)来注入绝对的或相对的位置信息。 +位置编码可以通过学习得到也可以直接固定得到。 +接下来,我们描述的是基于正弦函数和余弦函数的固定位置编码 + :cite:`Vaswani.Shazeer.Parmar.ea.2017`。 -假设输入表示$\mathbf{X} \in \mathbb{R}^{n \times d}$包含一个序列中$n$个词元的$d$维嵌入表示。位置编码使用相同形状的位置嵌入矩阵$\mathbf{P} \in \mathbb{R}^{n \times d}$输出$\mathbf{X} + \mathbf{P}$,矩阵第$i$行、第$2j$列和$2j$列上的元素为: +假设输入表示$\mathbf{X} \in \mathbb{R}^{n \times d}$ +包含一个序列中$n$个词元的$d$维嵌入表示。 +位置编码使用相同形状的位置嵌入矩阵 +$\mathbf{P} \in \mathbb{R}^{n \times d}$输出$\mathbf{X} + \mathbf{P}$, +矩阵第$i$行、第$2j$列和$2j$列上的元素为: $$\begin{aligned} p_{i, 2j} &= \sin\left(\frac{i}{10000^{2j/d}}\right),\\p_{i, 2j+1} &= \cos\left(\frac{i}{10000^{2j/d}}\right).\end{aligned}$$ :eqlabel:`eq_positional-encoding-def` -乍一看,这种基于三角函数的设计看起来很奇怪。在解释这个设计之前,让我们先在下面的`PositionalEncoding`类中实现它。 +乍一看,这种基于三角函数的设计看起来很奇怪。 +在解释这个设计之前,让我们先在下面的`PositionalEncoding`类中实现它。 ```{.python .input} #@save @@ -159,7 +205,10 @@ class PositionalEncoding(tf.keras.layers.Layer): return self.dropout(X, **kwargs) ``` -在位置嵌入矩阵$\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 @@ -194,15 +243,20 @@ d2l.plot(np.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)', ### 绝对位置信息 -为了明白沿着编码维度单调降低的频率与绝对位置信息的关系,让我们打印出$0, 1, \ldots, 7$的[**二进制表示**]形式。正如我们所看到的,每个数字、每两个数字和每四个数字上的比特值在第一个最低位、第二个最低位和第三个最低位上分别交替。 +为了明白沿着编码维度单调降低的频率与绝对位置信息的关系, +让我们打印出$0, 1, \ldots, 7$的[**二进制表示**]形式。 +正如我们所看到的,每个数字、每两个数字和每四个数字上的比特值 +在第一个最低位、第二个最低位和第三个最低位上分别交替。 ```{.python .input} #@tab all for i in range(8): - print(f'{i} in binary is {i:>03b}') + print(f'{i}的二进制是:{i:>03b}') ``` -在二进制表示中,较高比特位的交替频率低于较低比特位,与下面的热图所示相似,只是位置编码通过使用三角函数[**在编码维度上降低频率**]。由于输出是浮点数,因此此类连续表示比二进制表示法更节省空间。 +在二进制表示中,较高比特位的交替频率低于较低比特位, +与下面的热图所示相似,只是位置编码通过使用三角函数[**在编码维度上降低频率**]。 +由于输出是浮点数,因此此类连续表示比二进制表示法更节省空间。 ```{.python .input} P = np.expand_dims(np.expand_dims(P[0, :, :], 0), 0) @@ -226,9 +280,15 @@ d2l.show_heatmaps(P, xlabel='Column (encoding dimension)', ### 相对位置信息 -除了捕获绝对位置信息之外,上述的位置编码还允许模型学习得到输入序列中相对位置信息。这是因为对于任何确定的位置偏移$\delta$,位置$i + \delta$处的位置编码可以线性投影位置$i$处的位置编码来表示。 +除了捕获绝对位置信息之外,上述的位置编码还允许模型学习得到输入序列中相对位置信息。 +这是因为对于任何确定的位置偏移$\delta$,位置$i + \delta$处 +的位置编码可以线性投影位置$i$处的位置编码来表示。 -这种投影的数学解释是,令$\omega_j = 1/10000^{2j/d}$,对于任何确定的位置偏移$\delta$, :eqref:`eq_positional-encoding-def`中的任何一对$(p_{i, 2j}, p_{i, 2j+1})$都可以线性投影到$(p_{i+\delta, 2j}, p_{i+\delta, 2j+1})$: +这种投影的数学解释是,令$\omega_j = 1/10000^{2j/d}$, +对于任何确定的位置偏移$\delta$, + :eqref:`eq_positional-encoding-def`中的任何一对 +$(p_{i, 2j}, p_{i, 2j+1})$都可以线性投影到 +$(p_{i+\delta, 2j}, p_{i+\delta, 2j+1})$: $$\begin{aligned} &\begin{bmatrix} \cos(\delta \omega_j) & \sin(\delta \omega_j) \\ -\sin(\delta \omega_j) & \cos(\delta \omega_j) \\ \end{bmatrix} @@ -245,7 +305,7 @@ $2\times 2$投影矩阵不依赖于任何位置的索引$i$。 * 在自注意力中,查询、键和值都来自同一组输入。 * 卷积神经网络和自注意力都拥有并行计算的优势,而且自注意力的最大路径长度最短。但是因为其计算复杂度是关于序列长度的二次方,所以在很长的序列中计算会非常慢。 -* 为了使用序列的顺序信息,我们可以通过在输入表示中添加位置编码来注入绝对的或相对的位置信息。 +* 为了使用序列的顺序信息,我们可以通过在输入表示中添加位置编码,来注入绝对的或相对的位置信息。 ## 练习 From 3b99c6f41aedb00c4510a10508fa8f03371f1ff1 Mon Sep 17 00:00:00 2001 From: Rachel Hu Date: Sat, 20 Nov 2021 19:32:46 -0800 Subject: [PATCH 8/8] transformer polish --- chapter_attention-mechanisms/transformer.md | 49 ++++++++++++--------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/chapter_attention-mechanisms/transformer.md b/chapter_attention-mechanisms/transformer.md index f683ea57c..7e476eaa3 100644 --- a/chapter_attention-mechanisms/transformer.md +++ b/chapter_attention-mechanisms/transformer.md @@ -7,7 +7,7 @@ Transformer作为编码器-解码器架构的一个实例,其整体架构图在 :numref:`fig_transformer`中展示。正如所见到的,transformer是由编码器和解码器组成的。与 :numref:`fig_s2s_attention_details`中基于Bahdanau注意力实现的序列到序列的学习相比,transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的*嵌入*(embedding)表示将加上*位置编码*(positional encoding),再分别输入到编码器和解码器中。 -![transformer 架构](../img/transformer.svg) +![transformer架构](../img/transformer.svg) :width:`500px` :label:`fig_transformer` @@ -45,7 +45,7 @@ import tensorflow as tf ## [**基于位置的前馈网络**] -基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是*基于位置的*(positionwise)的原因。在下面的实现中,输入`X`的形状(批量大小、时间步数或序列长度、隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小、时间步数、`ffn_num_outputs`)的输出张量。 +基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是*基于位置的*(positionwise)的原因。在下面的实现中,输入`X`的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,`ffn_num_outputs`)的输出张量。 ```{.python .input} #@save @@ -115,7 +115,7 @@ ffn(tf.ones((2, 3, 4)))[0] ## 残差连接和层规范化 -现在让我们关注 :numref:`fig_transformer`中的“**加法和规范化**(add&norm)”组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。 +现在让我们关注 :numref:`fig_transformer`中的“*加法和规范化*(add&norm)”组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。 在 :numref:`sec_batch_norm`中,我们解释了在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化和批量规范化的目标相同,但层规范化是基于特征维度进规范化。尽管批量规范化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。 @@ -129,7 +129,7 @@ bn.initialize() X = d2l.tensor([[1, 2], [2, 3]]) # 在训练模式下计算 `X` 的均值和方差 with autograd.record(): - print('layer norm:', ln(X), '\nbatch norm:', bn(X)) + print('层规范化:', ln(X), '\n批量规范化:', bn(X)) ``` ```{.python .input} @@ -154,7 +154,7 @@ print('layer norm:', ln(X), '\nbatch norm:', bn(X, training=True)) ```{.python .input} #@save class AddNorm(nn.Block): - """残差连接后进行层规范化。""" + """残差连接后进行层规范化""" def __init__(self, dropout, **kwargs): super(AddNorm, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) @@ -168,7 +168,7 @@ class AddNorm(nn.Block): #@tab pytorch #@save class AddNorm(nn.Module): - """残差连接后进行层规范化。""" + """残差连接后进行层规范化""" def __init__(self, normalized_shape, dropout, **kwargs): super(AddNorm, self).__init__(**kwargs) self.dropout = nn.Dropout(dropout) @@ -182,7 +182,7 @@ class AddNorm(nn.Module): #@tab tensorflow #@save class AddNorm(tf.keras.layers.Layer): - """残差连接后进行层规范化。""" + """残差连接后进行层规范化""" def __init__(self, normalized_shape, dropout, **kwargs): super().__init__(**kwargs) self.dropout = tf.keras.layers.Dropout(dropout) @@ -202,14 +202,14 @@ add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape ```{.python .input} #@tab pytorch -add_norm = AddNorm([3, 4], 0.5) # Normalized_shape是input.size()[1:] +add_norm = AddNorm([3, 4], 0.5) add_norm.eval() add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape ``` ```{.python .input} #@tab tensorflow -add_norm = AddNorm([1, 2], 0.5) # Normalized_shape是 [i for i in range(len(input.shape))][1:] +add_norm = AddNorm([1, 2], 0.5) add_norm(tf.ones((2, 3, 4)), tf.ones((2, 3, 4)), training=False).shape ``` @@ -220,7 +220,7 @@ add_norm(tf.ones((2, 3, 4)), tf.ones((2, 3, 4)), training=False).shape ```{.python .input} #@save class EncoderBlock(nn.Block): - """transformer编码器块。""" + """transformer编码器块""" def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, use_bias=False, **kwargs): super(EncoderBlock, self).__init__(**kwargs) @@ -239,7 +239,7 @@ class EncoderBlock(nn.Block): #@tab pytorch #@save class EncoderBlock(nn.Module): - """transformer编码器块。""" + """transformer编码器块""" def __init__(self, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, dropout, use_bias=False, **kwargs): @@ -261,7 +261,7 @@ class EncoderBlock(nn.Module): #@tab tensorflow #@save class EncoderBlock(tf.keras.layers.Layer): - """transformer编码器块。""" + """transformer编码器块""" def __init__(self, key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_hiddens, num_heads, dropout, bias=False, **kwargs): super().__init__(**kwargs) @@ -398,7 +398,8 @@ 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) @@ -424,7 +425,7 @@ encoder(tf.ones((2, 100)), valid_lens, training=False).shape 如 :numref:`fig_transformer`所示,[**transformer解码器也是由多个相同的层组成**]。在`DecoderBlock`类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。 -正如在本节前面所述,在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于**序列到序列模型**(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数`dec_valid_lens`,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。 +正如在本节前面所述,在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于*序列到序列模型*(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数`dec_valid_lens`,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。 ```{.python .input} class DecoderBlock(nn.Block): @@ -587,6 +588,14 @@ state = [encoder_blk(X, valid_lens), valid_lens, [None]] decoder_blk(X, state)[0].shape ``` +```{.python .input} +#@tab tensorflow +decoder_blk = DecoderBlock(24, 24, 24, 24, [1, 2], 48, 8, 0.5, 0) +X = tf.ones((2, 100, 24)) +state = [encoder_blk(X, valid_lens), valid_lens, [None]] +decoder_blk(X, state, training=False)[0].shape +``` + 现在我们构建了由`num_layers`个`DecoderBlock`实例组成的完整的[**transformer解码器**]。最后,通过一个全连接层计算所有`vocab_size`个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。 ```{.python .input} @@ -785,7 +794,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 @@ -821,7 +830,7 @@ dec_attention_weights_2d = [d2l.tensor(head[0]).tolist() dec_attention_weights_filled = d2l.tensor( pd.DataFrame(dec_attention_weights_2d).fillna(0.0).values) dec_attention_weights = d2l.reshape(dec_attention_weights_filled, - (-1, 2, num_layers, num_heads, num_steps)) + (-1, 2, num_layers, num_heads, num_steps)) dec_self_attention_weights, dec_inter_attention_weights = \ dec_attention_weights.transpose(1, 2, 3, 0, 4) dec_self_attention_weights.shape, dec_inter_attention_weights.shape @@ -835,7 +844,7 @@ dec_attention_weights_2d = [head[0].tolist() dec_attention_weights_filled = d2l.tensor( pd.DataFrame(dec_attention_weights_2d).fillna(0.0).values) dec_attention_weights = d2l.reshape(dec_attention_weights_filled, - (-1, 2, num_layers, num_heads, num_steps)) + (-1, 2, num_layers, num_heads, num_steps)) dec_self_attention_weights, dec_inter_attention_weights = \ dec_attention_weights.permute(1, 2, 3, 0, 4) dec_self_attention_weights.shape, dec_inter_attention_weights.shape @@ -882,9 +891,9 @@ d2l.show_heatmaps( ## 小结 * transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。 -* 在transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器还必须通过掩蔽机制来保留自回归属性。 -* transformer中的残差连接和层规范化是训练非常深度的模型的重要工具。 -* transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有的序列位置的表示进行转换。 +* 在transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。 +* transformer中的残差连接和层规范化是训练非常深度模型的重要工具。 +* transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。 ## 练习