Published on

Attention Intro

Authors
  • avatar
    Name
    Guming
    Twitter

注意力机制

什么是注意力机制?

  • 注意力机制最先源于计算机视觉领域,其核心思想为当我们关注一张图片,我们往往无需看清楚全部内容而仅将注意力集中在重点部分即可。 而在自然语言处理领域,也可以通过将重点注意力集中在一个或几个 token,从而取得更高效高质的计算效果
本地图片1
  • 人类的视觉注意力使我们能够专注于某个具有“高分辨率”(即看黄色框中的尖耳朵)的区域,同时以“低分辨率”(即看看雪地背景和服装如何?)感知周围图像,然后相应地调整焦点或进行推理。 给定图像的一小块区域,其余像素提供了显示该区域的线索。我们期望在黄色框中看到一个尖耳朵,因为我们已经看到狗鼻子,底部的毛衣和毯子不如那些狗狗特征那么有帮助

同样地,我们也可以解释一个句子或邻近语境中词语之间的关系。当我们看到“吃”时,我们预期很快会遇到一个食物相关的词语。

进一步理解,注意力可以广泛地解释为一个重要性权重的向量:为了预测或推断一个元素,例如图像中的一个像素或句子中的一个词语,我们使用注意力向量来估计它与其他元素的相关性强度,并将它们通过注意力向量加权的值之和作为目标值的近似

理解注意力机制

注意力机制有三个核心变量:Query(查询值)、Key(键值)和 Value(真值)。

以字典为例,逐步分析注意力机制的计算公式是如何得到的,从而深入理解注意力机制。首先,有这样一个字典:

{
    "apple":10,
    "banana":5,
    "chair":2
}

字典的键就是注意力机制中的键值 Key,而字典的值就是真值 Value。字典支持我们进行精确的字符串匹配,例如,如果我们想要查找的值也就是查询值 Query 为“apple”,那么我们可以直接通过将 Query 与 Key 做匹配来得到对应的 Value

但是,如果我们想要匹配的 Query 是一个包含多个 Key 的概念呢?例如,我们想要查找“fruit”,此时,我们应该将 apple 和 banana 都匹配到,但不能匹配到 water Key 对应的 Value 进行组合得到最终的 Value。

例如,当我们的 Query 为“fruit”,我们可以分别给三个 Key 赋予如下的权重:

{
    "apple":0.6,
    "banana":0.4,
    "chair":0.0
}

那么,最终查询到的值应该是:

value=0.610+0.45+02=8value = 0.6 * 10 + 0.4 * 5 + 0 * 2 = 8

给不同 Key 所赋予的不同权重,就是我们所说的注意力分数,也就是为了查询到 Query,我们应该赋予给每一个 Key 多少注意力。Key 与 Query 相关性越高,则其所应该赋予的注意力权重就越大。

但是,我们如何能够找到一个合理的、能够计算出正确的注意力分数的方法呢?答案是词向量

词向量和相似度(插播)

通过下面的示例加强理解

假设我们用一个数字向量来表示一个单词。理想情况下,向量中的值应该以某种方式捕捉它所代表的单词的含义。例如,假设我们有以下词向量(在二维空间中可视化)

本地图片1

相似的词语聚集在一起。水果聚集在右上角,蔬菜聚集在左上角,家具聚集在底部。事实上,蔬菜和水果的聚类比家具聚类更接近彼此,因为它们是更紧密相关的事物。

给定单词“king”、“queen”、“man”和“woman”及其各自的向量表征

vking,vqueen,vman,vwomen\boldsymbol{v}_{\text{king}}, \boldsymbol{v}_{\text{queen}}, \boldsymbol{v}_{\text{man}}, \boldsymbol{v}_{\text{women}}

进一步假定

vqueenvwoman+vmanvking\boldsymbol{v}_{\text{queen}} - \boldsymbol{v}_{\text{woman}} + \boldsymbol{v}_{\text{man}} \sim \boldsymbol{v}_{\text{king}}

“女王”的向量减去“女人”加上“男人”应该会得到一个与“国王”的向量相似的向量

但是两个向量相似究竟意味着什么呢?在水果/蔬菜的例子中,我们使用距离(特别是欧氏距离)作为相似性的度量

但我们同样也可以用点积来进行度量:

vw=iviwiv·w = \sum_{i}v_iw_i

根据词向量的定义,语义相似的两个词对应的词向量的点积应该大于0,而语义不相似的词向量点积应该小于0。

这些词向量究竟从何而来呢?在神经网络中,它们通常来自某种学习到的Embedding。也就是说,最初词向量只是随机数,但随着神经网络的训练,它们的值会被调整,从而越来越好地表征单词。神经网络是如何学习这些更好的表征的呢?这超出了本博文的讨论范围,你需要学习深度学习入门课程才能理解。现在,我们只需要接受词向量的存在,以及它们能够以某种方式捕捉单词的含义。

下面继续注意力分数

假设我们的 Query 为“fruit”,对应的词向量为 qq ;我们的 Key 对应的词向量为 k=[vapplevbananavchair]k = [v_{apple} v_{banana} v_{chair}] ,则我们可以计算 Query 和每一个键的相似程度:

x=qKTx = qK^T

此处的 K 即为将所有 Key 对应的词向量堆叠形成的矩阵。基于矩阵乘法的定义,x 即为 q 与每一个 k 值的点积。现在我们得到的 x 即反映了 Query 和每一个 Key 的相似程度,我们再通过一个 Softmax 层将其转化为和为 1 的权重:

softmax(x)i=exijexj\text{softmax}(x)_i = \frac{e^{xi}}{\sum_{j}e^{x_j}}

得到的向量就能够反映 Query 和每一个 Key 的相似程度,同时又相加权重为 1,也就是我们的注意力分数了。最后,我们再将得到的注意力分数和值向量做对应乘积即可。根据上述过程,我们就可以得到注意力机制计算的基本公式:

attention(Q,K,V)=softmax(qKT)vattention(Q,K,V) = softmax(qK^T)v

👇是代码示例

import numpy as np

def get_word_vector(word, d_k=8):
    """Hypothetical mapping that returns a word vector of size
    d_k for the given word. For demonstrative purposes, we initialize
    this vector randomly, but in practice this would come from a learned
    embedding or some kind of latent representation."""
    return np.random.normal(size=(d_k,))

def softmax(x):
    # assumes x is a vector
    return np.exp(x) / np.sum(np.exp(x))

def attention(q, K, v):
    # assumes q is a vector of shape (d_k)
    # assumes K is a matrix of shape (n_k, d_k)
    # assumes v is a vector of shape (n_k)
    return softmax(q @ K.T) @ v

def kv_lookup(query, keys, values):
    return attention(
        q = get_word_vector(query),
        K = np.array([get_word_vector(key) for key in keys]),
        v = values,
    )

# returns some float number
print(kv_lookup("fruit", ["apple", "banana", "chair"], [10, 5, 2]))

👆就是使用点积计算注意力分数的示例代码

此时的值还是一个标量,同时,我们此次只查询了一个 Query。我们可以将值转化为维度为 dvd_v 的向量,同时一次性查询多个 Query,同样将多个 Query 对应的词向量堆叠在一起形成矩阵 Q,得到公式:

attention(Q,K,V)=softmax(QKT)Vattention(Q,K,V) = softmax(QK^T)V

目前,我们离标准的注意力机制公式还差最后一步。在上一个公式中,如果 Q 和 K 对应的维度 dkd_k 比较大,softmax 放缩时就非常容易受影响,使不同值之间的差异较大,从而影响梯度的稳定性。因此,我们要将 Q 和 K 乘积的结果做一个放缩:

attention(Q,K,V)=softmax(QKTdk)Vattention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V

这也就是注意力机制的核心计算公式了

代码:

import numpy as np

def softmax(x):
    # assumes x is a matrix and we want to take the softmax along each row
    # (which is achieved using axis=-1 and keepdims=True)
    return np.exp(x) / np.sum(np.exp(x), axis=-1, keepdims=True)

def attention(Q, K, V):
    # assumes Q is a matrix of shape (n_q, d_k)
    # assumes K is a matrix of shape (n_k, d_k)
    # assumes v is a matrix of shape (n_k, d_v)
    # output is a matrix of shape (n_q, d_v)
    d_k = K.shape[-1]
    return softmax(Q @ K.T / np.sqrt(d_k)) @ V