注意力机制能够显著提高神经机器翻译任务的性能。本文将会看一看Transformer—加速训练注意力模型的方法。Transformers在很多特定任务上已经优于Google神经机器翻译模型了。不过其最大的优点在于它的并行化训练。
Transformer模型:
编码组件是一系列编码器的堆叠(文章中是6个编码器的堆叠——没什么特别的,你也可以试试其他的数字)。解码部分也是同样的堆叠数。
编码器在结构上都是一样的(但是它们不共享权重)。每个都可以分解成两个子模块:
编码器的输入首先流经self-attention层,该层有助于编码器对特定单词编码时查看输入序列的其他单词。本文后面将会详细介绍self-attention。
Self-attention层的输出被送入前馈神经网络。完全相同的前馈神经网络独立应用在每个位置。
解码器也具有这两层,但是这两层中间还插入了attention层,能帮助解码器注意输入句子的相关部分(和seq2seq模型的attention相同)。
上面我们看到了模型的主要部件,我们现在开始研究各种向量/张量以及他们如何在这些组件中流动来将训练好的模型的输入转换为输出。
和传统NLP任务一样,我们首先使用词嵌入来将每个输入单词转换为向量。
词嵌入仅发生在最底部的编码器中。所有编码器的共同之处是他们接收元素大小为512的向量列表——在最底部的编码器中这恰好是词嵌入后的大小,而在其他的编码器中这恰好是其下面编码器输出的大小。这个列表大小是我们设置的超参数----基本上它就是训练集中最长句子的大小。在输入序列中进行词嵌入后,每一个输入都将会流过编码器的两个层。
这里我们看到Transformer一个重要特性,每个位置的单词在经过编码器时流经自己的路径。self-attention层中这些路径之间有依赖关系。然而前馈层并不具有这些依赖关系,所以各种路径在流经前馈层时可以并行执行。下面我们将例子中句子换为更短的句子来看一下每个编码器中的子层发生了什么。
开始编码
上面提到过,编码器接受向量列表作为输入。编码器将向量列表传入self-attention层,之后进入前馈神经网络,然后再输出到下一个编码器。
更高的视角看self-attention
句子中“it”指的是什么?指street还是说animal呢?对人来说很简单的问题,对机器却很复杂。
当模型处理单词“it”时,self-attention 就可以使它指代“animal”。
当模型处理每个单词时(输入序列中每个位置),self-attention使得它可以查看输入序列的其他位置以便于更好的编码该单词。
如果你熟悉RNN,考虑一下如何维护隐藏层状态来更好的结合已经处理的先前的单词/向量与目前正在处理的单词/向量。Transformer使用self-attention来将其他相关单词的“理解”融入到目前正在处理的单词。
Self-Attention 细节
首先我们看看如何使用向量计算self-Attention,之后再研究它如何实现的———使用矩阵实现呗。
计算self-attention的 第一步 需要从每个编码器的输入向量(这个例子中是每个词的词嵌入表示)创建三个向量。因此,对于每个单词,我们创建一个Query向量,一个Key向量和一个Value向量。这些向量是通过将词嵌入(embedding)乘以在训练过程中训练的三个矩阵来创建的。
注意,这些新创建的向量的维度小于词嵌入向量(embedding vector)。它们(新创建的向量)的维度是64,而词嵌入和编码器的输入输出向量的维度是512。它们不必更小,这是一种架构选择,可以使多头注意力(multiheaded attention)计算不变。
那么,究竟什么是“query”,“key”,“value”向量呢?
计算self-attention的 第二步 是计算得分(score 权重)。假设我们正在计算例子中第一个单词"Thinking"的self-attention。我们需要根据这个词对输入句子的每个词进行评分。当我们在某个位置编码单词时,分数决定了对输入句子的其他部分放置多少的焦点(注意力)。
这里的分数是通过将"query"向量与我们正在评分的单词的“key”向量做点积来得到。所以如果我们计算位置#1处的单词的self-attention,第一个得分就是就是q1和k1的点积。第二个得分是q1和k2的点积。
第三第四 是将分数除以8(论文中使用**“Key”向量维数的平方根**—64。这可以有更稳定的梯度。实际上还可以有其他可能的值,这里使用默认值),然后经过一个softmax操作后输出结果。Softmax可以将分数归一化,这样使得结果都是正数并且加起来等于1。
softmax后的分数决定了每个单词在这个位置被表达了多少。很明显该位置的这个词具有最高的softmax分数,但是有时候关注与当前单词相关的其它词更有用。
第五步 将每个值向量乘以softmax得分(准备将他们相加)。直觉上讲需要保持我们关注的单词的值不变,忽略掉不相关的单词(比如可以将它们乘以0.001这样的小数字)。
第六步 对加权值向量求和。这样就产生了在这个位置的self-attention的输出(对于第一个单词)。
这就是self-attention计算。得到的向量可以送往前馈神经网络。然而在真正的实现中,计算过程通过矩阵计算来进行,以便加快计算。现在我们已经清楚了单词级别的计算过程。
Self-Attention的矩阵计算
第一步 是计算Query, Key, Value矩阵。通过将词嵌入整合到矩阵X中,并将其乘以我们训练过的权重矩阵(WQ,WK,WV)来实现。
最后 ,由于我们在处理矩阵,我们可以将步骤2到步骤6合并为一个公式来计算self-attention层的输出。
来自https://blog.csdn.net/qq_44689178/article/details/123691354
多头注意力机制
这篇文章通过增加一种称为“多头”注意力的机制完善了self-attention层。这通过两种方式改善了注意力层的性能:
1、它扩展了模型关注不同位置的能力。在上面的例子中,Z1包含了每个其他编码的一点,但它可能由实际的单词本身支配。翻译句子:“The animal didn’t cross the street because it was too tired”,我们很想知道这里的“it”指代什么? 这时候会很有帮助。
2、它给予attention层多个“表达子空间”。接下来会看到多头注意力有多组Query/ Key /Value权重矩阵(Transformer使用了8组注意力头,所以这里我们为每个编码器/解码器设置了8组),而不是简单的一组。每组集合都是随机初始化。之后在训练中每组用于将词嵌入(或来自较低层编码器/解码器的输出)映射到不同的表达子空间。
如果我们进行上面概述提到的相同的self-attention计算,在8个不同的时间使用8个不同的权重矩阵,最终将会得到8个不同的Z矩阵。
这就有点麻烦了。因为前馈神经网络层并不是期望8个矩阵,而是需要一个矩阵(每个单词一个向量)。所以我们需要将这8个矩阵整合成一个矩阵。
怎么办?我们将8个矩阵连接起来然后乘以一个单独的矩阵WO。
这就是多头注意力的全部内容。这仅是一小部分的矩阵。我们把这些矩阵都放到一个图解中,更容易总览全局:
现在我们已经粗略了解了注意力头了,我们重新审视之前的例子,看看在例子中编码单词“it”的时候,不同的注意力头关注在哪里?
然而当我们把所有注意力头都在图上画出来时,可能就有点难以理解了:
使用位置编码表示序列顺序
到目前为止我们还未考虑输入序列中单词顺序的问题。为解决这个问题,Transformer为每个输入的词嵌入增加了一个向量。这些向量遵循模型学习到的特定模式,这有助于确定每个单词的位置,或者学习到不同单词之间的距离。直觉告诉我们,将这些值添加到词嵌入之中可以在计算点积注意和将词嵌入映射到 Q/K/V 向量时提供有意义的距离信息。
All need is the attention的描述:由于我们的模型不包含递归性和卷积,为了使模型利用序列的顺序,我们必须注入一些关于序列中标记的相对或绝对位置的信息。为此,我们在编码器和解码器堆栈的底部的输入嵌入中添加“位置编码”。位置编码与嵌入具有相同的维数d,因此两者可以求和。有许多位置编码的选择,被学习到的和固定的。
如果假定词嵌入维度为4,那真实的位置编码(维度也是4,方便相加)如下:
这种模式究竟看起来如何呢?
下图中,每一行对应一个向量的位置编码。因此第一行就是输入序列中第一个单词的词嵌入向量。每行包含512个值—每个值介于-1到1之间。这里我们进行了涂色,使模式可见。注意:该例子中共20个词(行),词嵌入向量维度为512维(列),位置编码也有512维。不要被上个例子中的4个迷惑了。
位置编码的公式:
其中pos是位置,i是维度。也就是说,**位置编码的每个维度都对应于一个正弦曲线。**波长形成了一个从2π到10000·2π的几何级数。我们选择这个函数是因为我们假设它允许模型容易地学习相对位置,因为对于任何固定的偏移量k,PEpos+k可以表示为PEpos的线性函数。
你可以在 get_timing_signal_1d() 函数中看到用于生成位置编码的代码。这并不是生成位置编码的唯一方式。然而,它的优点在于可以扩展到看不见的序列长度(eg. 如果要翻译的句子的长度远长于训练集中最长的句子)。
残差连接
需要注意一下:编码器架构中每个编码器中每个子层(self-attention, ffnn)都在其周围有残差连接,之后就是层标准化(layer-normalization)步骤。
如果将向量和self-attention层的标准化操作可视化,它会如下所示:
这也适用于解码器的子层。如果我们想看到堆叠两个编码器和解码器的Transformer,它将如下所示:
解码器
我们已经介绍了编码器的的大部分概念,相信大家都知道解码器如何工作的。现在我们看一下它们是如何协同工作的。
编码器开始处理输入序列。然后将顶部编码器的输出变换为一组注意力向量K和V。这些将在每个解码器的“encoder-decoder attention” 层使用,这有助于解码器集中注意力在输入序列的合适位置:
接下来的步骤会一直重复此过程,直到遇到结束符。下一时间步骤中,每个步骤的输出被发送到其底部解码器中,解码器就像编码器那样弹出他们的解码结果。就像对编码器输入所做的那样,我们对解码器输入中嵌入位置进行编码来指示每个词的位置。
解码器中的self-attention层与编码器中的操作方式略有不同:
在解码器中,仅允许 self-attention层 关注输出序列中较早的位置。这是通过在计算self-attention中softmax步骤前屏蔽未来位置(将它们设置为-inf)实现的。
“Encoder-Decoder Attention” 层就像多头注意力(multiheader self-attention)一样工作———而**“Encoder-Decoder Attention” 层从其下面的层创建其Queries矩阵,并从编码器堆栈的输出中获取Keys和Values矩阵。**
最后的线性层和softmax层
解码器堆叠(decoder stack)输出浮点数向量。如何将其转换为一个单词?这就是最后Softmax层后线性层的工作了。
线性层是一个简单的全连接神经网络,它将解码器堆叠(decoder stack)产生的向量映射到一个更大更大的向量中去,这个向量称为logits向量。
假设模型有10000个单独的英文单词(模型的“输出词汇表”),这是从训练集中学到的。这使得logits向量有10,000个单元的宽度 ———每个单元对应一个唯一单词的得分。这样就解释了线性层后面的模型输出。
softmax层将这些分数转化为概率(全部为正数,加起来为1.0)。选择具有最高概率的单元,并将与其相关的单词作为本时间步的输出。
当我们在标记的训练集上训练的时候,我们可以将它的输出与真实的标签进行对比。
可视化理解一下,当我们假设输出词汇仅包含6个单词(“a”, “am”, “i”, “thanks”, “student”, “”)。
一旦定义了输出词汇表,就可以使用相同宽度(大小)的向量来表示词汇表中的每个单词了。这就是one-hot编码。例如,可以使用如下向量来表示单词“am”:
损失函数
假设我们正在训练模型,这是训练一个简单例子的第一步,比如将“merci”翻译为“thanks”。
我们如何理解这个翻译任务?这意味着我们希望输出一个指向“thanks”的概率分布。但是模型还未训练好,它输出极有可能是这个样子:
怎么来比较两个概率分布呢?可以简单地一个减去另一个。更多详细信息,就需要看一下 交叉熵 和 KL散度。
注意,这是一个过度简化的例子。更现实一点的是,我们将使用更长一些的句子而不是单个单词。比如:输入:“je suis étudiant” ,期望输出:“I am a student”。这意味着我们希望模型能够输出一个如下的连续概率分布:
每个概率分布都被表达成宽度为 vocab_size 的向量。(在我们这个玩具模型中是6。现实一点的数字往往是3,000或10,000)
第一个概率分布在 与单词”I“相关联的单元处 有最高概率。
第二个概率分布在 与单词”am“相关联的单元处 有最高概率。
以此类推,直到第五个输出分布表示 ‘’ ,这个符号也与10,000个元素的单词表中某个单元相关联。
在足够大的数据集中训练模型足够长的时间后,我们希望生成的概率分布如下所示:
期望通过训练,模型会输出我们期望的正确翻译。不过这也并不能说明什么———如果这个短语是训练集的一部分的话(参考:交叉验证)。注意,每个位置即便不是该时间步的输出,它也会获得一点的概率值——这就是softmax有用的地方。现在因为模型一次产生一个输出,我们可以假设模型从该概率分布中选择具有最高概率的单词并丢弃其他可能的单词。这种方法称为贪婪解码。另一种方法是保持住该词的前两个候选(比如是“I”和“a”),在下一步解码中运行模型两次:一旦假设第一个输出位置是单词“I”,另一次假设输出位置是“me”,考虑#1和#2位置,保留错误较少的那个候选版本… 这种方法称为“集束搜索(beam search)”,在这个例子中,beam_size是2(因为我们比较了两个位置#1,#2后给出的结果),top_beams也是2(因为我们保留了2个词)。这都是试验中可以尝试的超参数。
以下来自视频 https://www.youtube.com/watch?v=TQQlZhbC5ps
编解码过程详解实例:英语翻译成法语
RNN就像一个传送带,需要顺序依次输入每个单词;而transformer可以同时输入多个单词,因为内部是并行的;
嵌入(假设是英语)与位置编码相加,形成输入向量;编码器经过多头注意力机制得到attention向量;attention向量经过前向神经网络并行将每一个attention vector转化成下一层编码器或者解码器可以消化的格式,再经过层归一化提高训练效率。
解码器:
将已经输出的法语经过embedding得到嵌入,和来自编码器的英语的向量,一起经过一个编解码attention层;
为对应的英语和法语生成相似的attention向量;
刚刚被翻译出来的法语的嵌入被作为output embedding,和位置编码相加后送入多头注意力机制,masked是指训练的时候后面未翻译的单词的标签被置为0,前向线性层是为了把向量映射成法语单词数,softmax将其转换为人类可以解释的概率分布,最终单词是与总体概率最高的单词相对应。在多个句子步长上执行该操作,直到解码出结束标志。
因为output embedding只能用已经翻译出来的法语句子的部分,所以把标签里后面的部分遮住;
归一化可以加快模型的训练速度。
以下:原来Transformer就是一种图神经网络,这个概念你清楚吗?
https://m.thepaper.cn/baijiahao_6255298
将长句 S 中的第 i 个单词的隐藏特征 h 从 ℓ 层更新至ℓ+1 层:
其中 j∈S 为句子中单词的集合,Qℓ、Kℓ、V^ℓ为可学习的线性权重(分别表示注意力计算的 Query、Key 以及 Value)。针对句子中每个单词的并行执行注意力机制,从而在 one shot 中(在 RNNs 转换器上的另外一点,逐字地更新特征)获取它们的更新特征。
考虑到 h_j^l; ∀j∈S 句中 h_i^l 和其他词的特征,通过点积计算每对(i,j)的注意力权重,然后在所有 j 上计算出 softmax。最后通过所有 h_j^l 的权重进行相应的加权,得到更新后的单词特征 h_i^l+1。
多头注意力机制
让点积注意力机制发挥作用是被证明较为棘手:糟糕的随机初始化可能会破坏学习过程的稳定性,此情况可以通过并行执行多头注意力将结果连接起来,从而克服这个问题(而每个「head」都有单独的可学习权重):
其中 Qk,ℓ、Kk,ℓ、V^k,ℓ是第 K 个注意力 head 的可学习权重,O^ℓ 是向下的投影,用以匹配 h_i^l+1 和 h_i^l 跨层的维度。
此外,多头允许注意力机制从本质上做「对冲」,从上一层看不同的转换或隐藏特征方面。
尺度问题和前向传播子层
一个推动 Transformer 的关键问题是**词的特征在经过了注意力机制后可能会有不同的尺度:**1)这可能是因为在相加之后,有些词有非常高或分布注意力权重 w_ij;2)在独立特征/向量输入阶段,将多个注意力头级联(每个注意力头都可能输出不同尺度的值),最终会导致最后的向量 h_i^ℓ+1 有不同的值。根据传统的 ML 思路,似乎增加一个归一化层是个合理的选择。
Transformer 克服了这一点,因为它使用了 LayerNorm,可以在特征层级归一和学习一个仿射变化。此外,Transformer 使用平方根来缩放点乘规模。
最终,Transformer 的作者还提出了另一个小窍门,用来控制尺度——一个有着特殊架构的、位置级别的双层全连接层。在多头注意力之后,它们会使用可学习权重来映射 h_i^ℓ+1 到一个更高维度。这其中使用了 ReLU 非线性,然后再将它映射会原有的维度,并使用另一个归一化操作。
在很多深度网络中,Transformer 架构是可以调整的,使得 NLP 社区可以从模型参数量和数据层面提升其规模。而残差连接也是堆栈 Transformer 层的关键。
GNN 构建图展示
图神经网络(GNNs)或图卷积网络(GCNs)在图形数据中建立节点和边的表示。通过邻域聚合(或者信息传递)来实现这一点,每一个节点从其相邻处收集特征,用以更新其相邻本地图结构的表示。堆叠多个 GNN 层能够使模型在整个图中传播每个节点的特征,从相邻处扩散到相邻处的相邻处,等等。
以社交网络为例:由 GNN 产生的节点特征可以用于预测,比如识别最有影响力的成员或提供潜在的联系。
在最基本的形式中,GNNs 更新了第一层节点 i 的隐藏特征 h,并通过节点自身特征 h_i^l 的非线性转换添加到每个相邻节点 j∈N(i) 的特征 h_i^l 集合中:
其中 Ul,Vl 是 GNN 层的可学习矩阵,类似于 ReLU 的非线性矩阵。
领域 j 节点 j∈N(i) 上的和可以用其他输入大小不变的聚合函数来代替,如简单的均值/最大值或者是其他更有效的函数,比如通过注意机制得到的加权和。
如果我们采用邻域聚合的多个并行 head,用注意机制(即加权和)代替邻域 J 上的和,我们就得到了图注意网络(GAT)。添加正则化和前馈 MLP 就得到了 Graph Transformer。
https://blog.csdn.net/qq_45836365/article/details/122757107
句子就是全连接词语的图
为了让 Transformer 和图神经网络的关系更直接,我们可以将一个句子想象为一个全连接图,每个词都和其余的词相连接。现在,我们使用图神经网路来构建每个节点(词)的特征,这是之后可以在其他 NLP 任务中用到的。
广义来说,这其实就是 Transformer 所做的事情。它们实际上就是有着多头注意力(作为集群聚合函数:neighbourhood aggregation function)的 GNN。标准的 GNN 从局部集群节点 j∈N(i) 中聚合特征,而 Transformer 则将整个句子 S 视为一个局部集群,在每个层中从每个词 j∈S 获得聚合特征。
重要的是,各种针对问题的技巧,如位置编码、masked 聚合、规划学习率和额外的预训练——对于 Transformer 的成功很重要,但是很少在 GNN 中见到。同时,从 GNN 的角度来看 Transformer,可以帮助我们在架构上舍弃很多无用的部分。
可以从 Transformers 和 GNN 学到什么?
现在我们已经建立起了 Transformers 与 GNN 之间的联系,那么以下一些问题也就随之而来:
全连接图对于 NLP 来说是最佳的输入格式吗?
在统计型 NLP 和 ML 出现之前,诺姆·乔姆斯基等语言学家着重创建语言结构的形式化理论,如语法树/图等。Tree-LSTM 就是其中一种尝试,但 Transformers 或者 GNN 架构是否能够更好地拉近语言学理论和统计型 NLP 呢?这又是一个问题。
如何学习长期依赖(long-term dependency)
全连接图的另一个问题是它们会使得学习单词之间的长期依赖变得困难。这仅仅取决于全连接图中的边数如何随着节点数而呈平方地扩展,例如在包含 n 个单词的句子中,Transformer 或者 GNN 将计算 n^2 个单词对。n 数越大,计算愈加困难。
NLP 社区对于长序列和依赖的观点很有趣,即为了获得更好的 Transformers,我们可以在考虑输入大小的时候执行稀疏或自适应的注意力机制,在每一层添加递归或压缩,以及使用局部敏感哈希来实现有效注意力。
所以,融合 GNN 社区的一些观点有可能收获显著的效果,例如用于句子图稀疏化的二元分区(binary partitioning)似乎就是一个不错的方法。