激励自注意力

为什么我们需要查询、键和值?

…self-self-attention?

本文的目标是解释 transformers 中的自注意机制是如何工作的,以及该机制的设计初衷。

我们将从讨论语言理解模型需要具备的能力开始,接着构建自注意机制。在这个过程中,我们将发现为什么我们需要查询、键和值来以一种自然的方式建模单词之间的关系,而 QKV 注意机制是实现这一目标最简单的方法之一。

本文最适合已经接触过 transformers 和自注意机制的读者,但对于熟悉一些基本线性代数的任何人都可以理解。对于那些希望更好地理解 transformers 的人,我会很乐意推荐您阅读这篇博客文章。

所有图片均由作者提供。

Transformers 经常出现在序列到序列建模任务中,例如语言翻译或更显著的句子完成。然而,我认为从序列建模的问题,特别是语言理解问题开始思考会更容易。

因此,这是一个我们想要理解的句子:

让我们仔细考虑一下我们如何理解这个句子。

  • Evan 的狗 Riley… 从这个信息中,我们知道 Riley 是狗的名字,而 Evan 拥有 Riley。
  • …很活跃… 很简单,“活跃”指的是狗 Riley,影响了我们对 Riley 的印象。
  • …她从不停止移动。这个有趣。 “她” 指的是 Riley,因为狗是第一个短语的主语。这告诉我们 Evan 的狗 Riley 是雌性的,之前由于常见的中性狗名“Riley”,这一点曾经是模糊的。 “从不停止移动” 是一组略微复杂的单词,详细说明了“活跃”。

关键在于,为了建立对句子的理解,我们不断考虑单词之间的关系,以增强它们的意义。

在机器学习社区中,通过存在另一个单词 b 来增强单词 a 的含义的过程,俗称为“a 关注 b”,即单词 a 关注单词 b。

An arrow a => b indicates that ” a attends to b ”

因此,如果我们希望机器学习模型能够理解语言,我们可能希望模型具有一个单词关注另一个单词并以某种方式相应地更新其含义的能力。

这正是我们希望在三个部分中模拟的能力,这个机制被称为(自)注意机制。

接下来,我将提出一些问题,这些问题将以斜体形式出现。我强烈建议读者在继续之前停下来考虑一分钟。

第一部分

现在,让我们关注“狗”和“Riley”之间的关系。单词“狗”强烈影响单词“Riley”的含义,因此我们希望“Riley”关注“狗”,所以这里的目标是以某种方式相应地更新单词“Riley”的含义。

为了让这个例子更具体化,假设我们从每个词的向量表示开始,每个向量的长度为n,基于对该词的无上下文理解。我们将假设这个向量空间相当有组织,这意味着在意义上更相似的单词与更靠近的向量相关联。

因此,我们有两个向量,v_dogv_Riley,它们捕捉了两个词的含义。

我们如何使用v_dog更新v_Riley的值,以获得一个新的“Riley”单词的值,其中包含“dog”的含义?

我们不想完全用v_dog替换v_Riley的值,因此让我们假设我们使用v_Rileyv_dog的线性组合作为v_Riley的新值:

v_Riley = get_value('Riley')v_dog = get_value('dog')ratio = .75v_Riley = (ratio * v_Riley) + ((1-ratio) * v_dog)

这似乎可以,我们已经将“dog”单词的一些含义嵌入到“Riley”单词中。

现在我们想尝试将此形式的注意力应用于整个句子,通过将每个单词的向量表示更新为每个其他单词的向量表示。

这里出了什么问题?

核心问题是我们不知道哪些单词应该具有其他单词的含义。我们也希望了解每个单词的值应该对每个其他单词的贡献有多大。

第二部分

好吧。所以我们需要知道两个单词应该有多相关。

是尝试第二次的时候了。

我重新设计了我们的向量数据库,使每个单词实际上具有两个关联向量。第一个是我们之前拥有的相同值向量,仍然由v表示。此外,我们现在有由k表示的单位向量,存储一些词语关系的概念。具体来说,如果两个k向量彼此接近,这意味着与这些单词相关联的值可能会影响彼此的含义。

使用我们的新kv向量,我们如何修改我们以前的方案,以一种尊重两个单词相关性的方式更新v_Riley的值,使用v_dog

让我们像以前一样继续用线性组合业务,但仅当两个的k向量在嵌入空间中接近时。更好的是,我们可以使用两个k向量的点积(它们是单位向量,因此范围从0-1)告诉我们应该如何更新v_Rileyv_dog

v_Riley,v_dog = get_value('Riley'),get_value('dog')k_Riley,k_dog = get_key('Riley'),get_key('dog')relevance = k_Riley · k_dog # 点积v_Riley = (relevance) * v_Riley + (1 - relevance) * v_dog

这有点奇怪,因为如果相关性为1,v_Riley完全被v_dog替换,但让我们先忽略它。

我想更换思考方式,将此类思想应用于整个序列时会发生什么。通过k的点积,“Riley”单词将与每个其他单词具有相关值。因此,也许我们可以根据点积值的值按比例更新每个单词的值。为简单起见,让我们将其与自身的点积一起包括,作为保留自身值的一种方式。

sentence = "Evan's dog Riley is so hyper, she never stops moving"words = sentence.split()# 获取值列表values = get_values(words)# 顺便说一下,这就是k的意思keys = get_keys(words)# 获取riley的关联键riley_index = words.index('Riley')riley_key = keys[riley_index]# 生成“Riley”与每个其他单词的相关性relevances = [riley_key · key for key in keys] # 仍然假装python有·# 将相关性规范化为总和为1relevances /= sum(relevances)# 按比例取值的线性组合,加权因素为相关性v_Riley = relevances · values

好的,现在已经足够好了。

但是我再次声称,这种方法存在问题。不是我们的任何想法都被错误地实现了,而是这种方法与我们实际思考单词之间关系的方式之间存在根本性的差异。

如果这篇文章有任何一个地方,我真的非常认为你应该停下来思考一下,那就是这里。即使你们中的一些人认为自己完全理解了注意力。我们的方法有什么问题?

提示

单词之间的关系本质上是不对称的!“Riley”对“dog”的关注方式不同于“dog”对“Riley”的关注方式。这更重要的是,“Riley”指的是一只狗,而不是一个人,比狗的名字重要得多。

相反,点积是一种对称操作,这意味着在我们目前的设置中,如果a关注b,那么b对a的关注同样强烈!实际上,这有些不正确,因为我们正在规范化相关性得分,但重点是单词应该有以不对称的方式关注,即使其他标记保持不变。

第三部分

我们快要完成了!最后,问题变成了:

我们如何最自然地扩展我们当前的设置,以允许不对称关系?

我们还能用一种矢量类型做什么?我们仍然有我们的值向量v,和我们的关系向量k。现在我们有另一个向量q,用于每个标记。

我们如何修改我们的设置并使用q来实现我们想要的不对称关系?

那些熟悉自我关注如何工作的人此时有望傻笑。

当“dog”关注“Riley”时,我们可以将查询q_Riley与关键字k_dog进行点积,而不是计算相关性k_dog · k_Riley。当计算另一种方式时,我们将使用q_dog · k_Riley ——不对称关联!

这里是所有东西在一起,一次计算每个值的更新!

sentence = "Evan's dog Riley is so hyper, she never stops moving"words = sentence.split()seq_len = len(words)# obtain arrays of queries, keys, and values, each of shape (seq_len, n)Q = array(get_queries(words))K = array(get_keys(words))V = array(get_values(words))relevances = Q @ K.Tnormalized_relevances = relevances / relevances.sum(axis=1)new_V = normalized_relevances @ V

这基本上就是自我关注!

我留下了一些细节,但重要的思想都在这里。

总之,我们从值向量(v)开始,以表示每个单词的含义,但很快发现我们需要关键向量(k)来解释单词之间的关系。最后,为了正确地模拟单词关系的不对称性质,我们引入了查询向量(q)。它几乎感觉就像如果我们只允许点积和这样的东西,三是每个单词需要适当模拟单词之间关系的最小向量数。

本文的目的是以一种比传统的算法优先方法更少压力的方式揭示自我关注机制。我希望从这个更加语言驱动的角度出发,查询-关键字-值设计的优雅和简单之处可以得到展示。

我留下的一些细节:

  • 我们不是为每个标记存储3个向量,而是存储一个单一的嵌入向量,我们可以从中提取我们的qk向量。提取过程只是一个线性投影。
  • 在这个设置中,从技术上讲,每个单词都不知道句子中其他单词在哪里。自我关注实际上是一个集合操作。因此,我们需要嵌入位置知识,通常通过将位置向量添加到嵌入向量来完成。由于变压器应该允许任意长度的序列,因此这不是完全微不足道的。如何在实践中工作超出了本文的范围。
  • 单个自我关注层只允许我们表示两个单词之间的关系。但通过组合自我关注层,我们可以模拟单词之间的更高级别关系。由于自我关注层的输出与原始序列具有相同的序列长度,因此这意味着我们可以将它们组合在一起。事实上,变压器块只是自我关注层后面跟着位置逐层前馈块。堆叠几百个这些,支付几百万美元,你就有了一个LLM! 🙂
OpenAI表明需要512个变压器块才能理解后半部分。