注意力机制(Attention and Transformers) #
Transformer架构已成为几乎所有NLP任务的核心模型。处理NLP任务的默认方法是选择一个预训练的Transformer模型(如BERT、ELECTRA、RoBERTa或Longformer),根据下游任务调整输出层并进行微调。此外,Transformer架构也已成为视觉任务(如图像识别、目标检测、语义分割和超分辨率)的默认选择,并在语音识别、强化学习以及图神经网络中表现出竞争力。
Transformer模型的 核心是Attention机制,最初被设计为增强编码器-解码器RNN在序列到序列(seq2seq)任务中的表现(如机器翻译)。在传统的seq2seq模型中,编码器将输入压缩为固定长度的向量再传递给解码器。Attention的直觉是,解码器在每个时间步可以动态关注输入序列的不同部分,而不是使用单一的固定表示。
Bahdanau等(2014)提出了一种简单的Attention机制,允许解码器在每个时间步选择性地关注输入序列的某些部分。具体而言,编码器生成与输入序列长度相等的表示;解码器通过一个控制机制,从这些表示中计算加权和(context vector)作为输入。这些权重表示了 解码器在某一时间步对输入序列每个元素的“关注程度”,并通过可微分的方式学习。
Attention最初作为增强RNN的机制,在机器翻译任务中表现优异,并展示了对跨语言词义对齐的解释性。然而,Vaswani等(2017)提出了完全基于Attention的Transformer架构,彻底摒弃了循环结构。Transformer通过Attention机制捕获输入和输出序列中所有元素之间的关系,在性能上超过了传统架构。到2018年,Transformer在大多数NLP任务中成为主流。
此外,NLP领域逐渐采用大规模预训练的模式:在庞大的通用语料上进行自监督预训练,然后使用下游任务数据进行微调。这种预训练-微调范式进一步拉大了Transformer与传统架构的性能差距。如今,基于Transformer的预训练模型(如GPT系列)被称为 基础模型(Foundation Models),其广泛应用标志着Transformer的全面崛起。
注意力机制中的查询 (Query)、键 (Key) 和值 (Value) #
目前接触过的网络模型大多依赖于 固定大小的输入。例如,ImageNet中的图像尺寸固定为特定像素大小,而卷积神经网络(CNNs)针对这种固定尺寸进行了优化。在自然语言处理中,循环神经网络(RNNs)的输入通常也是固定的,若输入大小可变,则通过逐步处理每个token或设计专用卷积核来解决。然而,当输入序列长度不定且信息含量变化时(如文本生成任务中),这些方法会带来显著问题,尤其在长序列中,网络难以跟踪已经生成或处理过的内容。
这种问题可以与数据库的运作方式类比。数据库通常是由 键-值 (key-value) 对组成的集合,例如: \(\{(“Zhang”, “Aston”), (“Lipton”, “Zachary”), (“Li”, “Mu”), (“Smola”, “Alex”)\}\) 。键是姓氏,值是名字。查询可以通过提供键来找到对应的值(如查询“Li”返回“Mu”)。这一简单示例表明:
- 查询可以在不同大小的数据库上有效运行。
- 相同的查询根据数据库内容可能 返回不同结果。
- 数据库操作的“代码”(如精确匹配、近似匹配)可以非常简洁,无需压缩或简化数据库即可有效运行。
这种理念引出了深度学习中的重要概念:注意力机制(Attention Mechanism)。它的核心思想是将 输入看作键-值对的数据库,并基于查询计算注意力权重 (attention weights)。假设数据库包含 \(\mathcal{D} \stackrel{\textrm{def}}{=} \{(\mathbf{k}_1, \mathbf{v}_1), \ldots (\mathbf{k}_m, \mathbf{v}_m)\}\) 个键值对,键和值分别记为 \(k_m\) 和 \(v_m\) ,查询记为 \(q\) 。关于的 \(\mathcal{D}\) 的注意力机制定义如下:
\[ \textrm{Attention}(\mathbf{q}, \mathcal{D}) \stackrel{\textrm{def}}{=} \sum_{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i, \]其中, \(\alpha(\mathbf{q}, \mathbf{k}_i) \in \mathbb{R}(i = 1, \ldots)\) 。这一操作称为 注意力池化(Attention Pooling),其关键点如下:
- 当权重 \(\alpha\) 较大时,机制对对应的值 \(v_i\) 赋予更多关注。
- 结果 \(\textrm{Attention}(\mathbf{q}, \mathcal{D})\) 是数据库中值的线性组合。
查询(Query) 是当前模型试图“寻找”或“关注”的目标。它是一个向量,代表了你想匹配的信息。 Example:在机器翻译任务中,假如模型正在翻译句子时生成一个新单词,例如“猫”(cat)。此时的查询向量可以看作是模型生成“猫”时,试图从输入句子中找出哪些词与“猫”相关的信息。
键(key) 是所有潜在匹配目标的特征表示。每个键对应于输入中的一个元素,表示这个元素的特性或身份。 Example:如果输入句子是“我喜欢吃鱼”,每个单词“我”“喜欢”“吃”“鱼”都有一个对应的键向量,表示它们的特性或含义,比如语义信息、位置等。
值(value) 是和键一起存储的信息,也是最终被提取的信息。注意力机制的目标是通过查询和键找到最相关的值。 Example:仍以“我喜欢吃鱼”为例,每个单词的值向量可以看作它承载的具体信息,比如“我”的值是表示主语身份的信息,“鱼”的值是关于“鱼”的语义内容。
Example: 假设我们在阅读一篇文章,想要找出与“健康饮食”相关的信息:
- 查询 (Query):你正在脑中思考“健康饮食”这个主题。
- 键 (Key):文章中的每个段落都携带一个特性,比如它是讲“运动”、还是“饮食”、或者是“健康习惯”。
- 值 (Value):段落具体的内容。
通过查询和键的匹配,注意力机制会计算出每个段落与“健康饮食”的相关性,最终提取出最相关段落的信息。
权重 \(\alpha(\mathbf{q}, \mathbf{k}_i)\) 的特殊情况:
非负权重:若所有 \(\alpha(\mathbf{q}, \mathbf{k}_i) \geq 0\) ,输出在值的凸锥中。
权重归一化:若 \(\sum_{i=1}^n \alpha(\mathbf{q}, \mathbf{k}_i) = 1\) ,且 \(\alpha(\mathbf{q}, \mathbf{k}_i) \geq 0\) ,输出为值的凸组合。
确保权重总和为 1 的常见策略是通过以下方式使它们 Normalize: \(\alpha(\mathbf{q}, \mathbf{k}_i) = \frac{\alpha(\mathbf{q}, \mathbf{k}_i)}{{\sum_j} \alpha(\mathbf{q}, \mathbf{k}_j)}\) 。 为了确保权重也是非负的,我们可以添加指数化: \(\alpha(\mathbf{q}, \mathbf{k}_i) = \frac{\exp(a(\mathbf{q}, \mathbf{k}_i))}{\sum_j \exp(a(\mathbf{q}, \mathbf{k}_j))}\) 。
单一选择:若 \(\alpha(\mathbf{q}, \mathbf{k}_i) = 1\) (其余为0),等同于传统的数据库精确查询。
平均池化:若 \(\alpha(\mathbf{q}, \mathbf{k}_i)\) 均相等,即 \(\alpha(\mathbf{q}, \mathbf{k}_i) = 1/n\) ,等同于对所有值进行平均。
注意力机制如何工作?
- 对查询和每个键计算相似度 \(\text{Similarity} = \alpha(\mathbf{q}, \mathbf{k}_i)\)
- 对这些相似度进行归一化(通常使用 Softmax 函数) \(\alpha(\mathbf{q}, \mathbf{k}_i) = \frac{\exp(a(\mathbf{q}, \mathbf{k}_i))}{\sum_j \exp(a(\mathbf{q}, \mathbf{k}_j))}\) 。归一化后的结果称为注意力权重(Attention Weights)。
- 将注意力权重与对应的值相乘,得到一个加权求和结果。这个结果就是当前查询的输出: \(\textrm{Attention}(\mathbf{q}, \mathcal{D}) \stackrel{\textrm{def}}{=} \sum_{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i\) 。
虽然上述注意力机制可微分且适用于深度学习,但也有非可微的注意力模型(例如使用强化学习训练)。现代研究多集中于这种可微机制的变体。
通过注意力机制,网络可以高效操作任意大小的键值对集合,而无需改变操作方式。这种灵活性和参数高效性使其成为深度学习中重要的工具。
Note: 为什么计算机能在注意力(Attention)中正确判断两个词语的相似性?
在 Transformer 之前,Word2Vec / GloVe 等词嵌入方法就已经利用了一个关键思想:两个在相似上下文中出现的词,应该有相似的 embedding。 e.g. “dog” 和 “cat” 经常出现在 “I love my ___” 这样的上下文中,所以它们的 embedding 可能很接近。
Transformer 使用的是更先进的 自注意力(Self-Attention),它不是直接定义相似度,而是让神经网络自己学习“什么是相似”。在提升模型表现,调整 weight 的过程中,模型利用自注意力(Self-Attention)最终学会了:哪些 Query 和哪些 Key 需要匹配。
查询 (Query)、键 (Key) 和值 (Value)如何获得 #
在 Transformer 的注意力机制中,Query (Q)、Key (K) 和 Value (V) 都是从输入嵌入(embedding)中线性变换得到的。它们的计算方式如下: \[ Q = X W_Q, \quad K = X W_K, \quad V = X W_V \]
其中:
- \(X\) :输入数据(通常是词嵌入矩阵)。
- \(W_Q, W_K, W_V\) :可训练的权重矩阵,用于投影输入数据到 Query、Key 和 Value 空间。
- \(Q, K, V\) :注意力计算所需的 Query、Key 和 Value 矩阵。
- 得到 \(Q, K, V\) 的过程 相当于经历了一次线性变换。Attention不直接使用 \(X\) 而是使用经过矩阵乘法生成的这三个矩阵,因为使用三个可训练的参数矩阵,可增强模型的拟合能力。

通过相似性实现注意力池化(Attention Pooling by Similarity) #
在了解了注意力机制的核心组件后,可以将其应用于经典的回归与分类任务,例如基于核密度估计(Kernel Density Estimation, KDE)的方法。这种方法核心是通过相似性核函数(Similarity Kernel)将查询(Query)与键(Key)关联。常见的核函数形式包括: \[ \begin{split}\begin{aligned} \alpha(\mathbf{q}, \mathbf{k}) & = \exp\left(-\frac{1}{2} \|\mathbf{q} - \mathbf{k}\|^2 \right) && \textrm{Gaussian;} \\ \alpha(\mathbf{q}, \mathbf{k}) & = 1 \textrm{ if } \|\mathbf{q} - \mathbf{k}\| \leq 1 && \textrm{Boxcar;} \\ \alpha(\mathbf{q}, \mathbf{k}) & = \mathop{\mathrm{max}}\left(0, 1 - \|\mathbf{q} - \mathbf{k}\|\right) && \textrm{Epanechikov.} \end{aligned}\end{split} \]
Note: Kernel(核函数) 是一种用来度量 数据之间相似度 的数学工具。它帮助我们将 数据映射到一个更高维度的空间,在这个空间中,数据的结构可能变得更加容易理解和操作。
Example: 假设有两个水果,分别是苹果和橙子。如果你只看果实的直径(一个特征),你可能很难区分这两种水果,因为它们的尺寸可能非常接近。但是如果你使用一个“核函数”来 考虑更多的信息,比如水果的颜色、口感、质地等,你就能更准确地判断它们的区别。
核函数选择是启发式的,可根据实际需求调整。例如,可以全局或按坐标单独调整核函数带宽 width。此外,这种方法无需训练,直接基于观测值和核函数即可进行预测。核函数最终会导致统一的计算公式,适用于回归和分类: \[ f(\mathbf{q}) = \sum_i \mathbf{v}_i \frac{\alpha(\mathbf{q}, \mathbf{k}_i)}{\sum_j \alpha(\mathbf{q}, \mathbf{k}_j)} \]
Note: 核函数如何工作?
- 相似度度量:核函数会对两个数据点之间的相似度进行度量,常见的核函数有线性核、高斯核(RBF)、多项式核等。它们会根据 数据的特征来计算一个值,表示两个数据点的相似度。这个值可以帮助模型决定如何结合不同的数据点来做出预测。
- 高维空间映射:有时,数据本身并不在一个容易分类或处理的空间中。例如,数据可能是非线性分布的。如果你将数据映射到一个更高维度的空间,这些数据可能变得更加分离或更容易分类。核函数帮助你“隐式”地将数据映射到高维空间,而不需要显式地进行转换,这样既高效又省去了计算高维空间坐标的麻烦。
Nadaraya-Watson核回归 #
在注意力池化(attention pooling)的背景下,Nadaraya-Watson核回归 提供了一种简单的方法来计算核回归估计。首先,我们计算训练特征(covariates)和验证特征之间的核,然后对其进行归一化。将归一化后的核权重与训练标签相乘,即可获得估计值。在这里,每个验证特征作为查询(query),每个训练特征-标签对作为键值对(key-value pair),而计算得到的归一化相对核权重即为注意力权重。
\[ f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i, \]其中 \(K\) 是核(kernel)。为了更好地理解注意力汇聚,下面考虑一个高斯核(Gaussian kernel),其定义为: \[ K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}) \] 将高斯核代入可以得到: \[ \begin{split}\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}\end{split} \]
如果一个键 \(x_i\) 越是接近给定的查询 \(x\) ,那么分配给这个键对应值的 注意力权重就会越大,也就“获得了更多的注意力”。
值得注意的是,Nadaraya-Watson核回归是一个 非参数模型。 因此,这是 非参数的注意力汇聚(nonparametric attention pooling)模型。
Nadaraya-Watson核回归的核心思想可以总结为:
- 计算相似度(通过kernel):
- 对每个需要预测的输入(query),计算它与数据集中所有输入(keys)的相似度。
- 使用核函数(如高斯核、Boxcar核等)来度量这种相似度。
- 归一化:
- 将所有相似度值归一化,使它们的总和为1。这一步确保了 注意力权重(attention weights)是一个概率分布。
- 加权平均:
- 对于数据集中的每个输出值(value),根据归一化后的注意力权重对其进行加权。
- 最终的预测值是所有加权值的总和。
非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点:如果有足够的数据,此模型会收敛到最优结果。尽管如此,我们还是可以轻松地将 可学习的参数集成到注意力汇聚中 。例如,在下面的查询 \(x\) 和键 \(x_i\) 之间的距离乘以可学习参数 \(w\) : \[ \begin{split}\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}\end{split} \]
注意力评分函数(Attention Scoring Functions) #
上一部分使用了高斯核来对查询和键之间的关系建模。其中的 高斯核指数部分可以视为 注意力评分函数(Attention scoring function),简称 评分函数(Scoring function)。然而,与点积(dot product)相比,距离函数的计算开销稍大。因此,许多研究重点放在如何简化注意力评分函数(Attention scoring function)的计算上,同时通过 Softmax 操作确保注意力权重(Attention weights)为非负。
重新回顾高斯核的注意力函数: \[ a(\mathbf{q}, \mathbf{k}_i) = -\frac{1}{2} \|\mathbf{q} - \mathbf{k}_i\|^2 = \mathbf{q}^\top \mathbf{k}_i -\frac{1}{2} \|\mathbf{k}_i\|^2 -\frac{1}{2} \|\mathbf{q}\|^2. \]
- 在公式中,常数 \(c\) 只与查询 \(q\) 有关,对所有键值对 \((q, k_i)\) 都相同。将权重归一化为概率分布,可以直接消除 \(c\) 的影响。
- 如果键 \(k_i\) 由层归一化(Layer Normalization)生成,其范数(Norm)通常是常数。因此,忽略 \(k_i\) 范数的影响对结果几乎没有变化。
所以这里的函数变为点积操作: \[ a(\mathbf{q}, \mathbf{k}_i)=\mathbf{q}^\top \mathbf{k} \]
接下来,为控制指数函数中参数的量级,我们对点积进行缩放:
- 假设 \(\mathbf{q} \in \mathbb{R}^d\) 和 \(\mathbf{k}_i \in \mathbb{R}^d\) 的元素是独立同分布的随机变量,均值为0,方差为1,则点积 \(q \cdot k\) 的均值为0,方差为 \(d\) (向量长度)。
- 为了使点积的方差与向量长度无关,我们将点积缩放为: \[ a(\mathbf{q}, \mathbf{k}_i) = \mathbf{q}^\top \mathbf{k}_i / \sqrt{d}. \] 随后通过 Softmax 归一化: \[ \alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(\mathbf{q}^\top \mathbf{k}_i / \sqrt{d})}{\sum_{j=1} \exp(\mathbf{q}^\top \mathbf{k}_j / \sqrt{d})}. \]
点积注意力(Dot Product Attention)在Transformers 等模型中被广泛应用,其特点是通过缩放点积和softmax操作来控制参数的范围并归一化权重。
Note:为什么需要对点积进行缩放?
由于点积的方差与 d成正比,因此,如果我们增加维度 d,点积的数值会变得更大。将点积作为输入传递给Softmax函数时,Softmax对大数值特别敏感,因为它会根据输入的相对大小来计算概率值。如果点积值变得非常大,Softmax会让其中一些值的输出接近1,而其他值接近0,这会 导致计算不稳定或梯度消失等问题。
此外,点积的大小还会影响梯度的分布。如果点积的方差过大,会使得梯度变得不均衡,从而影响模型的训练过程,导致收敛速度变慢或无法有效收敛。这个缩放的主要目的是 将点积的方差控制在一个合理的范围,使其不随向量维度的增加而变得过大。
关于点积的理解 #
在 Self-Attention 机制中,相似性本质上是由 点积(Dot Product) 计算得出的,它用于衡量词向量(embedding)之间的关系。 \[ x \cdot y = x_0 y_0 + x_1 y_1 + \dots + x_n y_n \] 点乘的几何意义是: \(x\) 在 \(y\) 方向上的投影再与 \(y\) 相乘,反映了两个向量的相似度。点乘结果越大,表示两个向量越相似。
一个矩阵 \(X\) 由 \(n\) 行向量组成。比如,我们可以将某一行向量 \(x_i\) 理解成一个词的词向量,共有 \(n\) 个行向量组成 \(n×n\) 的方形矩阵:
\[ X = \begin{bmatrix} x_0 \\ x_1 \\ \vdots \\ x_n \end{bmatrix}, X^\top = \begin{bmatrix} x_0^\top & x_1^\top & \dots & x_n^\top \end{bmatrix} \]矩阵相乘 \(XX^\top\) 计算如下:
\[ XX^\top = \begin{bmatrix} x_0 \cdot x_0 & x_0 \cdot x_1 & \dots & x_0 \cdot x_n \\ x_1 \cdot x_0 & x_1 \cdot x_1 & \dots & x_1 \cdot x_n \\ \vdots & \vdots & \ddots & \vdots \\ x_n \cdot x_0 & x_n \cdot x_1 & \dots & x_n \cdot x_n \end{bmatrix} \]以 \(XX^\top\) 中的第一行第一列元素为例,其实是向量 \(x_0\) 与 \(x_0\) 自身做点乘,其实就是 \(x_0\) 自身与自身的相似度,那第一行第二列元素就是 \(x_0\) 与 \(x_1\) 之间的相似度。
下面以词向量矩阵为例,这个矩阵中,每行为一个词的词向量。矩阵与自身的转置相乘,生成了目标矩阵,目标矩阵其实就是一个词的词向量与各个词的词向量的相似度。
如果再加上Softmax呢?Softmax的作用是对向量做归一化,那么就是对相似度的归一化,得到了一个归一化之后的权重矩阵,矩阵中,某个值的权重越大,表示相似度越高。
掩码Softmax操作(Masked Softmax Operation) #
在序列模型中,处理不同长度的序列是常见需求。当序列被打包到同一个小批量中时,较短的序列通常需要用 填充符(dummy tokens) 补齐。填充符不携带实际含义,因此在计算注意力权重时,需要屏蔽这些填充符。这种 屏蔽操作称为 掩码Softmax操作。其实现原理如下:
- 将超出有效长度部分的值 \(\mathbf{v}_i\) 设置为零。
- 将注意力权重中的这些无效部分设为一个较大的负值(如 \(-10^{6}\) ),这样它们在梯度计算和实际值中被忽略。
- 此方法避免了复杂的条件语句(if-else),充分利用了GPU优化的线性代数操作,即使在计算上略有冗余,也能提高效率。
批量矩阵乘法(Batch Matrix Multiplication, BMM) #
在注意力机制中,批量矩阵乘法是一种常用操作,特别是在处理查询(queries)、键(keys)和值(values)的情况下。例如,假设有以下矩阵定义: \[ \begin{split}\mathbf{Q} = [\mathbf{Q}_1, \mathbf{Q}_2, \ldots, \mathbf{Q}_n] \in \mathbb{R}^{n \times a \times b}, \\ \mathbf{K} = [\mathbf{K}_1, \mathbf{K}_2, \ldots, \mathbf{K}_n] \in \mathbb{R}^{n \times b \times c}.\end{split} \]
批量矩阵乘法(BMM)的作用是按批量逐元素地计算矩阵乘法,例如: \[ \textrm{BMM}(\mathbf{Q}, \mathbf{K}) = [\mathbf{Q}_1 \mathbf{K}_1, \mathbf{Q}_2 \mathbf{K}_2, \ldots, \mathbf{Q}_n \mathbf{K}_n] \in \mathbb{R}^{n \times a \times c}. \] 批量矩阵乘法能够高效地并行处理小批量的查询、键和值矩阵,是注意力机制计算中的核心操作。
Scaled Dot Product Attention(缩放点积注意力机制) #
缩放点积注意力机制是基于点积注意力的一种优化方法。在标准点积注意力中,查询(query)和键(key)的 向量长度需要一致,记为 \(d\) 。如果查询和键的向量长度不一致,可以通过引入一个矩阵 \(M\) 来将两者 \(\mathbf{q}^\top \mathbf{k}\) 映射到相同的空间 \(\mathbf{q}^\top \mathbf{M} \mathbf{k}\) 。
为了确保点积操作的数学意义和计算的合理性,查询和键必须有相同的维度(即相同的长度)。如果它们的维度不一致,点积就无法进行,因为维度不匹配意味着没有明确的一一对应的元素可以进行乘法操作。
矩阵 M 本身并不固定,而是在模型训练时 通过学习来调整的,以便最适合任务需求。
为了提高计算效率,通常在小批量(minibatch)中计算注意力。现在假设查询(query)和键(key)的向量长度一致,对于 \(n\) 个查询(queries)和 \(m\) 个键值对(key-value pairs),假设查询和键的维度为 \(d\) ,值(values)的维度为 \(v\) 。关于查询(query) \(\mathbf Q\in\mathbb R^{n\times d}\) ,键(key) \(\mathbf K\in\mathbb R^{m\times d}\) 和值(values) \(\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}. \]
关于维度: 在模型中,每个词(如 “The”、“cat” 等)被表示为一个向量,假设每个词的表示是一个 64 维的向量。
- 查询向量(Query):如果你正在对“cat”进行查询,查询向量 q 是该词的表示(64 维向量),表示你想要寻找与“cat”相关的词。
- 键向量(Key):同样,其他词(比如“sat”,“on”)也有键向量,表示它们的特征,维度也是 64。
所以当查询(query)和键(key)Embedding 向量的维度不同时,我们需要引入一个矩阵 M 来将它们的维度统一,使得点积操作能够正常进行。
为了避免模型过拟合,在计算注意力输出时 通常会应用 dropout 进行正则化。
加性注意力(Additive Attention) #
当查询(query)和键(key)的维度不同,可以通过使用矩阵进行维度匹配 \(\mathbf{q}^\top \mathbf{M} \mathbf{k}\) ,或者使用 加性注意力作为评分函数。加性注意力的一个优势是它的“加性”特性,这可以带来一些计算上的节省。在加性注意力中,给定一个查询向量 \(\mathbf{q} \in \mathbb{R}^q\) 和一个键向量 \(\mathbf{k} \in \mathbb{R}^k\) ,其评分函数定义为: \[ a(\mathbf q, \mathbf k) = \mathbf w_v^\top \textrm{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R}, \]
其中 \(\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}\) 是可学习的参数。这个评分函数的输出被输入到 softmax 函数中,以确保其非负性和归一化。一个等价的解释是,查询和键被连接在一起,作为输入传递给一个具有单隐藏层的多层感知机(MLP)。在该过程中,我们使用激活函数 \(\tanh\) ,并且不使用偏置项。加性注意力的计算可以通过以下方式实现:
- 首先,查询和键被合并。
- 然后通过 MLP 进行处理,最后通过 softmax 获得归一化的注意力权重。
Note: Dot-Product Attention 相当于直接计算 Query 和 Key 之间的角度相似性(点积衡量相似性)。Additive Attention 则相当于 让一个小型神经网络学习 Query 和 Key 之间的相似性,它不局限于点积运算。
- Dot-Product Attention 适合高维向量(如 Transformer,通常 d > 64)。因为点积运算可以在 GPU 上优化为 矩阵乘法,并且在高维度时,点积仍然能很好地区分不同的向量。
- Additive Attention 适合低维向量(如早期的 Seq2Seq 结构,通常 d < 64)。Additive Attention 在高维下的计算量更大,并且收益不明显。
Bahdanau 注意力(The Bahdanau Attention Mechanism) #
在机器翻译任务中,传统的序列到序列(Sequence-to-Sequence)架构通过编码器(Encoder)将一个变长的输入序列转换为固定形状的上下文变量(Context Variable),然后解码器(Decoder)基于该上下文变量逐步生成目标序列的每个词元(Token)。这种方法的核心是依赖编码器生成的中间状态(State)作为解码器生成翻译序列的唯一信息来源。
然而,这种方法在处理较短的输入序列时是可行的,但对于较长的序列(如书的章节或长句子)则难以胜任。原因在于,固定维度的中间状态无法容纳所有重要的信息,导致解码器在生成长句或复杂句子时容易失败。
Bahdanau等人(2014)提出了一种无单向限制的可微分注意力机制(Bahdanau Attention Mechanism)。该机制的核心思想是:当预测目标序列中的一个词元时,如果输入序列中的某些部分与该词元的生成无关,模型只会关注(Align)那些与当前预测相关的部分。这种选择性关注会用于更新当前状态,然后生成下一个词元。
在传统的 seq2seq 模型中,encoder 的作用是提炼输入序列的信息,将长度不一的输入序列压缩成一个固定维度的隐藏状态(hidden state),作为输入序列的全局表示。Encoder 在每个时间步 t 都会生成一个隐藏状态 h_t,通常只使用最后一个时间步的隐藏状态 h_T 作为输入序列的全局表示。 这个隐藏状态在理想情况下认为包含了输入序列的全部信息,用于传递给 decoder。在解码阶段,decoder 依赖于上一个时间步的状态,同时利用 encoder 提供的隐藏状态中浓缩的信息来辅助生成目标序列。然而,当输入序列过长时,encoder 的隐藏状态很容易因为 信息压缩的限制而丢失部分重要细节,从而导致 decoder 无法充分利用输入信息,最终表现不佳。
基于序列到序列(sequence-to-sequence)架构定义注意力机制的模型。Bahdanau 注意力的关键思想是动态更新上下文变量 \(\mathbf{c}_{t'}\) ,使其不仅依赖于源句子的编码器隐藏状态 \(\mathbf{h}_t\) ,还结合已经生成的目标文本解码器隐藏状态 \(\mathbf{s}_{t'-1}\) 。这种方法动态调整上下文,使其更灵活地适应每个解码步骤 \(T\) 。
\[ \mathbf{c}_{t'} = \sum_{t=1}^{T} \alpha(\mathbf{s}_{t' - 1}, \mathbf{h}_{t}) \mathbf{h}_{t}. \]- 其中,解码器的隐藏状态 \(\mathbf{s}_{t'-1}\) 被用作查询(query)。
- 编码器隐藏状态 \(\mathbf{h}_t\) 同时作为键(key)和值(value)。
注意力权重 \(\alpha\) 使用加性注意力(additive attention)得分函数计算,公式为: \[ \alpha(\mathbf{s}_{t' - 1}, \mathbf{h}_{t}) = \mathbf w_v^\top \textrm{tanh}(\mathbf W_q\mathbf{s}_{t' - 1} + \mathbf W_k \mathbf{h}_{t}) \in \mathbb{R}, \]
多头注意力机制(Multi-Head Attention) #
在实际应用中,给定相同的查询(queries)、键(keys)和值(values),我们可能希望模型能够结合来自相同注意力机制的不同表现,例如捕获序列中不同范围的依赖关系(如短期依赖与长期依赖)。因此,让注意力机制同时使用查询、键和值的不同表示子空间可能是有益的。
为此,首先 不是进行单一的注意力聚合,而是对查询、键和值进行独立学习的 线性投影(linear projections)。接下来,将这些投影后的查询、键和值并行地输入到注意力聚合中。最终,这些注意力聚合的输出会被连接(concatenate)在一起,并通过另一个学习到的线性投影生成最终的输出。这个设计被称为多头注意力(multi-head attention),每一个注意力聚合输出被称为一个“头”(head)。该设计使用全连接层(fully connected layers)来执行可学习的线性变换。
Note: 输入的 Q(查询向量)、K(键向量)、V(值向量)本质上都是相同的词嵌入(embedding word X),但是它们分别经过了 不同的线性变换 来得到不同的 Q, K, V,以实现注意力机制的计算。每个注意力头都分配一个独立的 W_q^i, W_k^i, W_v^i,这样不同的注意力头会有不同的 Q, K, V,使得不同的头学习不同的注意力模式,可以关注不同类型的关系(如语法关系、语义关系、长距离依赖等)。
在实现多头注意力(multi-head attention)之前,我们先对其进行数学公式化。给定查询(query) \(\mathbf{q} \in \mathbb{R}^{d_q}\) ,键(key) \(\mathbf{k} \in \mathbb{R}^{d_k}\) ,和值(value) \(\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}\) 是可学习的参数,注意力池化(Attention Pooling)指的是之前提到的加性注意力(additive attention)或缩放点积注意力(scaled dot product attention)。多头注意力的输出需要经过另一个线性转换,它对应着 \(h\) 个头连结后的结果,因此其可学习参数是 \(\mathbf W_o\in\mathbb R^{p_o\times h p_v}\) : \[ \begin{split}\mathbf W_o \begin{bmatrix}\mathbf h_1\\\vdots\\\mathbf h_h\end{bmatrix} \in \mathbb{R}^{p_o}.\end{split} \] 通过这种设计,每个注意力头可以关注输入的不同部分,从而表达比简单加权平均更复杂的函数。
- 多头注意力机制(Multi-Head Attention)代码示例
import torch
import torch.nn as nn
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads # 每个 head 的维度
# 线性变换层(W_q, W_k, W_v)
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
# 输出变换
self.W_o = nn.Linear(d_model, d_model)
def attention(self, Q, K, V):
""" 计算注意力得分并加权求和(省略实现)"""
pass
def forward(self, Q, K, V):
batch_size, seq_len, _ = Q.shape
# 线性变换
Q = self.W_q(Q) # [B, L, d_model]
K = self.W_k(K)
V = self.W_v(V)
# 拆分成多个 Head
Q = Q.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) # [B, H, L, d_k]
K = K.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
# 计算注意力
output = self.attention(Q, K, V) # [B, H, L, d_k]
# 重新拼接多头
output = output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
# 线性变换输出
return self.W_o(output)
自注意力和位置编码(Self-Attention and Positional Encoding) #
在深度学习中,传统上我们通常使用卷积神经网络(CNN)或循环神经网络(RNN)来编码序列。随着注意力机制的引入,可以设想将一系列的tokens(标记)输入到注意力机制中。在每一步中,每个token都会有自己的查询(query)、键(key)和值(value)。在计算token在下一层的表示时,token可以通过它的查询向量(query vector)去关注其他token(根据它们的键向量进行匹配)。通过计算查询-键的兼容性得分,可以为每个token计算出一个表示,通过对其他tokens进行适当的加权求和。
因为每个token都会去关注其他token(与解码器步骤只关注编码器步骤的情况不同),这种架构通常被称为 自注意力模型(self-attention model),也有一些地方称之为内部注意力模型(intra-attention model)。
Note:注意力机制和自注意力(Self-Attention) :注意力机制是一种广义的方法,用于让模型在处理输入数据时,重点关注最相关的信息,而非所有信息都等权处理。Self-Attention 是注意力机制的一种特殊形式,专门用于序列数据(文本、音频、视频等)。它的关键特点是:查询、键和值都来自同一个输入序列。
- 普通 Attention:Q 和 K 来自不同地方(如机器翻译的解码器对编码器的关注)。
- Self-Attention:Q、K、V 都来自同一输入(如 BERT 计算句子内部单词关系)。
给定一个由词元组成的输入序列 \(\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 \]其中 \(f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i\) 。
自注意力机制(Self-Attention)通过并行计算取代了RNN逐步处理序列的方式,但 它本身不保留输入序列的顺序信息。当输入序列的顺序对模型结果至关重要时,需要引入额外的信息来表示序列顺序。解决方案是为每个token添加一个表示其位置的信息,称为 位置编码(Positional Encoding)。位置编码可以是预定义的(固定的)或通过学习得到的。
对于固定位置编码,最常见的方法是基于正弦(sine)和余弦(cosine)函数的编码方案。假设输入表示 \(\mathbf{X} \in \mathbb{R}^{n \times d}\) 是一个序列的 \(n \times d\) 矩阵,其中 \(n\) 是序列长度, \(d\) 是嵌入维度。位置编码使用相同形状的位置嵌入矩阵 \(\mathbf{P} \in \mathbb{R}^{n \times d}\) 输出 \(\mathbf{X} + \mathbf{P}\) ,其元素按以下公式生成:
\[ \begin{split}\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}\end{split} \]Note:Positional Encoding(位置编码)可以直接与 word embedding 逐元素相加(element-wise addition) :Word embedding 主要表示单词的语义信息。Positional Encoding 提供额外的位置信息,以弥补 Transformer 结构中缺少序列顺序感的问题。相加的效果 是让同一个词(如 “apple”)在不同的位置有略微不同的表示,但仍保留其主要的语义信息。Positional Encoding 并不需要通过训练来学习,它是固定的、基于位置的函数,因此不干扰原本的语义信息。
设计背后的逻辑:
正弦和余弦的频率变化: 在位置编码矩阵 \(\mathbf{P}\) 中:
- \(2j\) 和 \(2j+1\) 列的频率随维度 \(j\) 单调递减。
- 频率降低的特性使得不同的维度捕获了不同粒度的位置信息。
类比于二进制表示,高位的切换频率低,低位的切换频率高。正弦和余弦函数用浮点数表示位置,提供了比离散二进制表示更高效的空间利用率。
绝对位置信息: 每个位置的 编码是序列中绝对位置信息的函数。例如,通过绘制热图,可以观察到不同维度的频率变化模式,这种模式类似于二进制位的切换频率。
相对位置信息的线性可投影性: 除了绝对位置信息,这种设计还允许模型学习相对位置的偏移。
- 对于固定偏移量 \(\delta\) ,位置 \(i+\delta\) 的编码 \(\mathbf{P}_{i+\delta}\) 可以通过位置 \(i\) 的编码 \(\mathbf{P}_{i}\) 通过线性投影得到。 \[ \begin{split}\begin{aligned} &\begin{bmatrix} \cos(\delta \omega_j) & \sin(\delta \omega_j) \\ -\sin(\delta \omega_j) & \cos(\delta \omega_j) \\ \end{bmatrix} \begin{bmatrix} p_{i, 2j} \\ p_{i, 2j+1} \\ \end{bmatrix}\\ =&\begin{bmatrix} \cos(\delta \omega_j) \sin(i \omega_j) + \sin(\delta \omega_j) \cos(i \omega_j) \\ -\sin(\delta \omega_j) \sin(i \omega_j) + \cos(\delta \omega_j) \cos(i \omega_j) \\ \end{bmatrix}\\ =&\begin{bmatrix} \sin\left((i+\delta) \omega_j\right) \\ \cos\left((i+\delta) \omega_j\right) \\ \end{bmatrix}\\ =& \begin{bmatrix} p_{i+\delta, 2j} \\ p_{i+\delta, 2j+1} \\ \end{bmatrix}, \end{aligned}\end{split} \] 这种线性可投影性使模型能够轻松捕捉序列中元素之间的相对位置关系。
- 位置编码(Positional Encoding)代码示例
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
# 创建一个最大长度为 max_len 的 Positional Encoding 矩阵
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).float().unsqueeze(1) # 位置索引,大小为 (max_len, 1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)) # 用于缩放
pe[:, 0::2] = torch.sin(position * div_term) # 偶数位置使用 sin
pe[:, 1::2] = torch.cos(position * div_term) # 奇数位置使用 cos
pe = pe.unsqueeze(0).transpose(0, 1) # 调整维度,大小为 (1, max_len, d_model)
self.register_buffer('pe', pe) # 注册为 buffer,以便在模型保存时不被更新
def forward(self, x):
# 输入 x 的形状为 (batch_size, seq_len, d_model)
return x + self.pe[:x.size(1), :]
Transformer #
Transformer架构的核心优势在于其自注意力机制(self-attention),这一机制既支持并行计算,又能实现较短的最大路径长度。因此,自注意力机制特别适合用于设计深度架构。与早期的自注意力模型不同,Transformer模型 完全基于注意力机制,去除了卷积层和循环层的依赖。最初,Transformer是为文本数据的序列到序列学习(sequence-to-sequence learning)设计的,但如今它已经广泛应用于语言、视觉、语音和强化学习等多个现代深度学习领域。
Transformer模型是一种 Encoder-Decoder 架构。与Bahdanau注意力机制不同,Transformer的输入(源序列)和输出(目标序列)在送入编码器和解码器之前,会与 位置编码(positional encoding)相加。这种结构的编码器和解码器都基于 自注意力机制(self-attention),并通过堆叠多个模块来实现。
具体来说,Transformer的编码器由多个相同的层堆叠而成,每一层包含两个子层:第一个是 多头自注意力(multi-head self-attention),第二个是 逐位置的前馈网络(positionwise feed-forward network)。在编码器的自注意力机制中,查询(queries)、键(keys)和值(values)都来自前一层的输出。每个子层都使用 残差连接(residual connection) 设计,并在其后进行 层归一化(layer normalization),确保模型的训练更稳定。最终,编码器为输入序列的每个位置输出一个 d-维向量表示。
Note: 在 Encoder 中的 Multi-Head Attention,每个 token(即单词的 embedding)都会和其他所有 token 进行注意力计算,因此它的 输入 包含:查询(Query, Q),键(Key, K),值(Value, V)。经过 多头注意力计算后,每个 token 得到一个新的表示向量,它结合了整个输入序列的信息,并包含不同注意力头的综合信息。如果输入是一个长度为 L 的句子(即 L 个 token),那么经过 Multi-Head Attention 之后,输出仍然是 L 个 token,每个 token 都得到了一个新的表示向量。
Transformer的解码器与编码器类似,也是由多个相同的层组成,包含残差连接和层归一化。除了与编码器相同的两个子层外,解码器还加入了一个额外的子层,称为 编码器-解码器注意力(encoder-decoder attention)。在这个子层中,查询来自解码器自注意力子层的输出,而键和值来自编码器的输出。解码器中的自注意力机制中,查询、键和值都来自前一层的输出,但每个位置只能关注解码器中当前位置之前的所有位置,从而保留了自回归(autoregressive)特性,确保 预测仅依赖于已生成的输出标记。
位置化前馈网络(Positionwise Feed-Forward Networks) #
位置化前馈网络(Positionwise Feed-Forward Networks)使用 相同的多层感知机(MLP)来转换序列中每个位置的表示。这意味着对于序列中每个位置,都会应用相同的前馈神经网络,因此称之为位置化(Positionwise)。在实现中,输入张量 \(X\) 的形状为 (批次大小, 序列长度, 隐藏单元数或特征维度),通过一个两层的MLP进行转换,得到输出张量,形状为 (批次大小, 序列长度, 输出数量)。
在 Transformer Encoder 或 Decoder 的每一层中,Positionwise FFN 主要由两个 全连接层 (Linear Layers) 和一个 非线性激活函数 (ReLU 或 GELU) 组成:
\[ \text{FFN}(x) = \max(0, x W_1 + b_1) W_2 + b_2 \]第一层线性变换 (Linear Transformation 1):
\(h = x W_1 + b_1\) 。 \(W_1\) 形状为 ( \(d_{\text{model}}, d_{\text{ff}}\) ) ,通常 \(d_{\text{ff}} \gg d_{\text{model}}\) 。这个层的作用是 升维,让 token 表示进入一个更高维的空间。
激活函数 (ReLU / GELU):
\(h{\prime} = \text{ReLU}(h)\) 。引入非线性,使得模型具有更强的表达能力。
第二层线性变换 (Linear Transformation 2):
\(y = h{\prime} W_2 + b_2\) 。 \(W_2\) 形状为 ( \(d_{\text{ff}}, d_{\text{model}}\) ) ,作用是 降维,回到原来的 \(d_{\text{model}}\) 维度。所以 FFN 处理完后,序列长度 L 仍然不变,只是每个 token 的表示变得更复杂。
Note: 每个 token 经过 Self-Attention 计算后,得到的输出向量会被 独立地 传入 FFN,这个过程 不会跨 token 共享计算,即 每个位置的 token 独立通过相同的前馈网络 进行转换,这就是 “Position-wise”(逐位置)这个名字的由来。
FFN 的作用
- 增加非线性变换能力: Self-Attention 主要依赖于加权求和,属于 线性变换,需要 FFN 提供 非线性激活,增强模型的表达能力。
- 局部特征变换(逐 token 处理): FFN 在每个 token 位置上独立运行,不会跨 token 交互信息(这一点与 Self-Attention 相反),因此它可以视为 每个 token 进行独立的特征变换。
- 扩大表示空间: d_ff 通常比 d_model 大很多,可以理解为在一个高维空间中对 token 进行投影和变换,再映射回原始维度,类似于一个 bottleneck 结构。
- 位置化前馈网络(Positionwise Feed-Forward Networks)代码示例
import torch
import torch.nn as nn
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff):
super(PositionwiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ff) # 第一层变换到高维
self.relu = nn.ReLU() # 非线性激活
self.fc2 = nn.Linear(d_ff, d_model) # 变换回原维度
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
残差连接和层规范化(Residual Connection and Layer Normalization) #
残差连接通过将输入信号直接传递到下一层,有效避免了深度网络训练中的梯度消失问题。层归一化与批量归一化(batch normalization)类似,不过 层归一化是沿特征维度进行归一化,而批量归一化是在一个小批次(minibatch)内部进行归一化。这种归一化方式使得层归一化具有尺度独立性和批次大小独立性的优势,尤其在自然语言处理任务中,输入序列的长度通常是可变的,因此层归一化比批量归一化更为有效。残差连接(Residual Connection)和层归一化(Layer Normalization)的核心公式如下:
\[ \text{Output} = \text{LayerNorm}(X + \text{SubLayer}(X)) \]Note: Batch Normalization(BN)在 batch 维度 上归一化,每个特征维度独立计算均值和方差,统计 batch 内的样本均值 和方差。Layer Normalization(LN)在 特征维度(d_model) 归一化,每个 token 计算均值和方差,统计 单个 token 内 的特征均值 和方差。
层归一化的实现方法是对每一层的特征进行归一化,而不是像批量归一化那样跨批次进行。需要注意的是,残差连接要求两个输入的形状相同,才能确保在执行加法操作后输出张量的形状不变。此外,为了提高模型的泛化能力,还可以在该过程中加入Dropout(随机失活),以起到正则化的作用。
Note: 为什么 Transformer 不能用 Batch Normalization(BN)?
在 Transformer 里,我们希望 每个 token 可以单独处理,而不是依赖 batch,但是,Batch Normalization 需要 batch 维度的统计信息(均值、方差),这意味着 batch size 变化时 BN 计算不稳定。在 NLP 任务中,序列长度可能不同,导致 BN 在不同 batch 计算均值、方差时变化过大,影响模型稳定性。
Layer Normalization 只对每个 token 的 hidden state 归一化,不依赖 batch 维度,所以:
- ✅ 可以处理变长序列,适用于 NLP 任务
- ✅ 不会因为 batch size 变化而影响模型稳定性
- ✅ 支持 Transformer 的并行计算,不影响推理效率
- 残差连接和层规范化(Residual Connection and Layer Normalization)代码示例
import torch
import torch.nn as nn
class TransformerLayer(nn.Module):
def __init__(self, d_model, d_ff, num_heads):
super(TransformerLayer, self).__init__()
self.attn = nn.MultiheadAttention(embed_dim=d_model, num_heads=num_heads)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model)
)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
def forward(self, x):
# Multi-Head Attention + Residual Connection + LayerNorm
attn_output, _ = self.attn(x, x, x)
x = self.norm1(x + attn_output)
# Position-wise Feed-Forward Network + Residual Connection + LayerNorm
ffn_output = self.ffn(x)
x = self.norm2(x + ffn_output)
return x
Encoder #
在学习Transformer编码器时,首先需要理解Transformer编码器的基本结构。Transformer编码器由多个子层组成,其中包括多头自注意力机制(multi-head self-attention)和逐位置前馈网络(positionwise feed-forward networks)。每个子层都采用残差连接(residual connection),并通过层归一化(layer normalization)进行处理。
值得注意的是,Transformer 编码器中的每一层都不会改变输入的形状。
在实现Transformer编码器时,我们通常 堆叠多个TransformerEncoderBlock。每个TransformerEncoderBlock由上述的多头自注意力和前馈网络组成。为了将输入嵌入与位置编码相加,我们 使用固定的可学习的位置编码,其值始终介于0和1之间,并将输入嵌入的值乘以嵌入维度的平方根进行重新缩放。
最终,Transformer编码器的输出形状为(批大小,时间步数,隐藏层维度)。
Note: 为什么 堆叠多个 TransformerEncoderBlock ?
- 每个 TransformerEncoderBlock 都可以看作是一个特征提取器。通过堆叠多个 Block,模型能够从输入数据中提取出多层次的特征。随着层数的增加,模型能够捕捉更复杂的语义关系和全局依赖:
- 浅层特征:语法、局部依赖。
- 中层特征:句法结构、短距离语义。
- 深层特征:全局语义、长距离依赖、抽象概念。
- 每个 TransformerEncoderBlock 都包含一个自注意力机制和一个前馈神经网络(FFN),这些模块引入了非线性变换。通过堆叠多个 Block,模型可以逐步组合这些非线性变换,从而学习到更复杂的函数映射。深度模型(更多层)通常具有更强的表达能力,能够拟合更复杂的模式。
- 虽然自注意力机制理论上可以捕捉任意距离的依赖关系,但在实际中,单层的注意力机制可能仍然有限。通过堆叠多个 Block,模型可以在不同层次上反复处理信息,从而更好地捕捉长距离依赖。
- Transformer Encoder 代码示例
class TransformerEncoder(nn.Module):
def __init__(self, num_layers, d_model, d_ff, num_heads):
super(TransformerEncoder, self).__init__()
# Stack multiple TransformerLayer blocks
self.layers = nn.ModuleList([
TransformerLayer(d_model, d_ff, num_heads) for _ in range(num_layers)
])
def forward(self, x):
# Pass input through each TransformerLayer
for layer in self.layers:
x = layer(x)
return x
Decoder #
Transformer解码器由多个相同的层(layers)组成,每一层包括三个子层(sublayers):解码器自注意力(decoder self-attention)、编码器-解码器注意力(encoder-decoder attention) 和 逐位置前馈网络(positionwise feed-forward network)。每个子层都使用残差连接(residual connection)并紧接着进行层归一化(layer normalization)。
子层 1:解码器自注意力: 在解码器自注意力(masked multi-head decoder self-attention)中,查询(queries)、键(keys) 和 值(values) 全部来自上一层解码器的输出。
- 训练时:序列到序列模型的输出序列所有位置的标记(tokens)都是已知的,解码器可以同时使用这些标记。
- 预测时:输出序列按逐标记生成(token by token),即在任意解码时间步,解码器只能使用已生成的标记。为了确保解码器的自回归特性(autoregression),解码器的掩码自注意力使用 dec_valid_lens,限制查询仅能关注当前位置及之前的位置。
在这一步过程中, \(Q、K、V\) 全部来自目标序列的嵌入表示(即 Decoder 自身的输入),与 Encoder 的输出无关。这一层的目的是让解码器 捕捉目标序列内部的依赖关系(例如语法结构、语义一致性),类似于 Encoder 的自注意力层捕捉输入序列的依赖关系。
第一个子层解码器自注意力(Masked Multi-Head Decoder Self-Attention)的主要作用是让解码器在生成目标序列时,能够关注到已经生成的部分序列,同时避免“偷看”未来的信息。
- 捕捉目标序列的内部依赖关系:解码器自注意力通过自注意力机制,让目标序列中的每个 token 能够关注到序列中的其他 token。
- 确保自回归特性(Autoregressive Property):在生成目标序列时,解码器是自回归的,即每个 token 的生成依赖于之前已经生成的 token。解码器自注意力通过掩码(mask) 机制,确保在生成第 t 个 token 时,只能关注到第 1 到第 t−1 个 token,而不能“偷看”未来的 token。
- 解码器自注意力主要关注目标序列内部的依赖关系。后续的编码器-解码器注意力则关注目标序列与输入序列之间的对齐关系(例如在翻译任务中,目标语言的某个词与源语言的哪些词相关)。
子层 2:编码器-解码器注意力: 在编码器-解码器注意力中,查询来自解码器,键和值来自编码器的输出。为了支持缩放点积(scaled dot product)操作和残差连接中的加法操作,解码器的特征维度(num_hiddens)与编码器相同。这一部分的主要作用有:
- 对齐目标序列与输入序列:在生成目标序列的每个位置时,解码器需要知道输入序列中哪些部分是相关的。
- 融合输入序列的上下文信息:解码器不仅需要理解目标序列的内部依赖(通过解码器自注意力),还需要结合输入序列的语义信息(通过编码器-解码器注意力)
- 动态权重分配:通过注意力机制,模型可以为输入序列中的每个位置分配不同的权重(即重要性),从而动态决定哪些输入信息对当前生成的目标词更关键。
子层 2:编码器-解码器注意力中, \(Q\) (Query):来自 解码器的当前状态(目标序列的嵌入表示)即“我需要关注什么”。 \(K\) (Key) 和 \(V\) (Value):来自 编码器的输出(即源序列的编码表示)。Key 表示源序列的特征,用于与 Query 计算相似度。Value 表示源序列的实际内容,用于加权求和。
子层 3:逐位置前馈网络: 逐位置前馈网络应用于每个时间步,独立处理序列中每个位置的特征。他的作用和 encoder 中的 Position-wise Feed-Forward Network 类似:
- 非线性变换:将注意力机制输出的特征映射到更高维的空间,捕捉更复杂的模式。将注意力机制输出的特征映射到更高维的空间,捕捉更复杂的模式。
- 特征提取:通过两层全连接网络(线性变换 + 激活函数 + 线性变换),提取每个位置的局部特征。FFN 可以看作是对注意力机制输出的特征进行“精炼”,增强模型的表达能力。
- 位置独立性:FFN 对序列中的每个位置独立处理,不依赖其他位置的信息。这种设计使得 FFN 能够专注于每个位置的局部特征,同时保持模型的并行性。
整个解码器由多个 TransformerDecoderBlock 实例堆叠而成。最终通过一个全连接层预测所有可能的输出标记(vocab_size)。解码器的自注意力权重(self-attention weights)和编码器-解码器注意力权重(encoder-decoder attention weights) 都被存储下来,便于可视化分析。
- Transformer Decoder 代码示例
import torch
import torch.nn as nn
import torch.nn.functional as F
class TransformerDecoderBlock(nn.Module):
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
# 解码器自注意力
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.norm1 = nn.LayerNorm(d_model)
# 编码器-解码器注意力
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.norm2 = nn.LayerNorm(d_model)
# 逐位置前馈网络
self.ffn = PositionwiseFFN(d_model, d_ff)
self.norm3 = nn.LayerNorm(d_model)
def forward(self, x, encoder_output, tgt_mask):
# 解码器自注意力
attn_output = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + attn_output)
# 编码器-解码器注意力
cross_attn_output = self.cross_attn(x, encoder_output, encoder_output)
x = self.norm2(x + cross_attn_output)
# 逐位置前馈网络
ffn_output = self.ffn(x)
x = self.norm3(x + ffn_output)
return x
class TransformerDecoder(nn.Module):
def __init__(self, num_layers, d_model, num_heads, d_ff, vocab_size):
super().__init__()
self.layers = nn.ModuleList([
TransformerDecoderBlock(d_model, num_heads, d_ff) for _ in range(num_layers)
])
self.fc_out = nn.Linear(d_model, vocab_size) # 全连接层,映射到词汇表大小
def forward(self, x, encoder_output, tgt_mask):
for layer in self.layers:
x = layer(x, encoder_output, tgt_mask)
logits = self.fc_out(x) # 输出 logits
return logits
Transformer在视觉领域的应用 #
Transformer最初是为序列到序列的学习任务(如机器翻译)设计的架构,随后迅速成为自然语言处理(NLP)领域的首选模型。虽然Transformer在NLP领域表现出色,但在计算机视觉领域,卷积神经网络(CNN)长期占据主导地位。
Ramachandran等人(2019)首次提出用自注意力(Self-Attention)替代卷积操作的方案,但由于需要特定的注意力模式,其模型在硬件加速器上难以扩展。随后,Cordonnier等人从理论上证明,自注意力可以学习出类似于卷积的行为。在实践中,将图像分割成小的patch(图像块)作为输入,但过小的patch尺寸限制了模型对高分辨率图像的适用性。
为克服这一限制,视觉Transformer(Vision Transformers, ViTs)直接从图像中提取patch,并将其输入到Transformer编码器中以获取全局表示,最终用于分类任务(Dosovitskiy等人,2021)。与CNN相比,Transformer在扩展性方面表现更优异:当模型规模和数据集增大时,ViTs在性能上显著超过了ResNet。
视觉Transformer(Vision Transformer, ViT)的模型架构由三个主要部分组成:图像切分模块(stem)、基于多层Transformer编码器的主体(body)、以及将全局表示转换为输出标签的头部模块(head)。
对于高度为 \(h\) 、宽度为 \(w\) 、通道数为 \(c\) 的输入图像,假设切分的 patch(小块)的高度和宽度均为 \(p\) ,图像将被切分为 \(m = hw/p^2\) 个 patch。每个 patch 被展平为长度为 \(cp^2\) 的向量。这些展平后的图像 patch 可以被视为与文本序列中的 tokens 类似,并由 Transformer 编码器处理。
此外,一个特殊的 <cls>
(class)token被加入到这些展平图像 patch 前,形成 \(m+1\)
个向量的序列。每个向量通过线性投影后,与可学习的 位置嵌入(positional embeddings)相加,作为 Transformer编码器的输入。多层 Transformer 编码器将这些输入向量转换为 相同数量且长度一致的输出向量表示。其工作方式与原始Transformer编码器一致,唯一区别在于归一化的位置有所调整。
在这个过程中,<cls>
token通过自注意力(self-attention)机制关注所有图像 patch。最终,Transformer编码器输出的 <cls>
token表示被进一步转换为输出标签,完成分类任务。
Patch Embedding(Patch 嵌入) #
在Vision Transformer(ViT)中,图像切分为 patch(小块)并将这些展平的 patch 线性投影的过程可以 简化为一次卷积操作(convolution operation)。卷积核的大小和步幅均设为 patch 大小。在实现中,假设输入图像的高度和宽度为 \(\text{img\_size}\) ,输出将包含 \((\text{img\_size} // \text{patch\_size})^2\) 个patch,并通过线性投影生成长度为 \(\text{num\_hiddens}\) 的向量。
Vision Transformer Encoder(编码器) #
ViT编码器的多层感知机(MLP)与传统Transformer编码器的逐位置前馈网络(Positionwise FFN)略有不同:
- 激活函数使用Gaussian Error Linear Unit(GELU),相比ReLU更平滑。
- 在MLP的全连接层输出中应用了dropout进行正则化(regularization)。
ViT编码器模块遵循 预归一化(pre-normalization) 设计:在多头注意力(multi-head attention)或 MLP 之前进行归一化,而不是传统“残差连接后归一化(add & norm)”的后归一化设计。这种设计被证明可以提升Transformer的训练效率和效果。
与传统Transformer相同,ViT编码器模块不会改变输入的形状。
整体结构 #
ViT的前向传播过程包括以下步骤:
- 输入图像先经过PatchEmbedding模块,输出与
<cls>
token嵌入拼接后,与可学习的位置嵌入相加,再应用dropout处理。 - 结果传入 Transformer 编码器,由 \(\text{num\_blks}\) 个 ViTBlock 堆叠而成。
- 编码器输出的
<cls>
token表示通过网络头部进行投影,得到最终输出。
在小规模数据集(如Fashion-MNIST)上,ViT的表现不如ResNet,这甚至在ImageNet(120万图像)数据集上也有类似情况。这是因为 Transformer 缺乏卷积网络的平移不变性(translation invariance)和局部性(locality)等特性。然而,当训练更大的模型并使用大规模数据集(如3亿张图像)时,ViT在图像分类任务中明显优于ResNet,展现出其在扩展性上的优势。
ViT的引入改变了图像数据建模的网络设计方式。在ImageNet数据集上,然而,由于自注意力机制的二次复杂性(quadratic complexity),Transformer架构对高分辨率图像的处理效率较低。为了解决这一问题,Swin Transformer通过降低复杂性并引入类似卷积的先验知识,使Transformer能够应用于超越图像分类的广泛视觉任务。