循环神经网络复习2-seq2seq,attention,self-attention(transform)

本文深入解析Seq2Seq模型的局限性及其解决方案——Attention机制,进而介绍革命性的Transformer模型,详细阐述Self-Attention机制如何克服传统RNN的缺陷,实现高效并行计算,同时涵盖位置编码、多头注意力等关键技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

循环神经网络复习2-seq2seq,attention,self-attention(transform)

seq2seq的弊端:

  • 1.输入信息被压缩到一个向量中去,但随着长度的增加,前面的信息减少的越来越多
  • 2.逆序输入可以缓解上面的问题,(所以有了双向循环神经网络)
  • 3.中间信息还是可能会丢失,主要原因是,没有学到输入输出中间的对应关系

这里说的寻找对应关系,就是attention模型的初衷。

下面是attention的公式,有五个,但核心只有一个是最后3和4,这两个公式:
先列出变量
编码端的隐状态变量 h 1 , h 2 , h 3 . . . h T h_{1},h_{2},h_{3}...h_{T} h1,h2,h3...hT,
解码端的隐状态变量 s 1 , s 2 , s 3 . . . s T s_{1},s_{2},s_{3}...s_{T} s1,s2,s3...sT,
解码端的输出变量 y 1 , y 2 , y 3 . . . y T y_{1},y_{2},y_{3}...y_{T} y1,y2,y3...yT,
解码端获取的语境向量 c 1 , c 2 , c 3 . . . c T c_{1},c_{2},c_{3}...c_{T} c1,c2,c3...cT,
解码端的注意力权重参数 α i j \alpha_{ij} αij
下 标 i 是 解 码 端 的 第 i 个 时 间 步 , 下 标 j 是 编 码 端 的 第 j 个 时 间 步 下标i是解码端的第i个时间步,下标j是编码端的第j个时间步 iijj.
公式:
s i = f ( s i − 1 , y i − 1 , c i ) s_i=f(s_{i-1},y_{i-1},c_i) si=f(si1,yi1,ci)第一个公式,
p ( y i ∣ y 1 , y 2 , . . . , y i − 1 ) = g ( y i − 1 , s i , c i ) p(y_i|y_1,y_2,...,y_{i-1})=g(y_{i-1},s_i,c_i) p(yiy1,y2,...,yi1)=g(yi1,si,ci)第二个公式,
c i = ∑ i = 1 T α i j h j c_i=\sum_{i=1}^{T}\alpha_{ij}h_j ci=i=1Tαijhj第三个公式,
α i j = e x p ( e i j ) ∑ j = 1 T e x p ( e i j ) \alpha_{ij}=\dfrac{exp(e_{ij})}{\sum_{j=1}^{T}exp(e_{ij})} αij=j=1Texp(eij)exp(eij)第四个公式,
e i j = a ( s i − 1 , h j ) e_{ij}=a(s_{i-1},h_j) eij=a(si1,hj)第五个公式,

最后一个公式,往往就是求向量相似度的余弦相似度函数,(也有别的运算,也具有表中两个向量的相关程度即可)

综上:我们看到了
attention模型的初衷是:寻找对应关系。
具体做法是:对解码端的隐状态变量 s i − 1 s_{i-1} si1和之前编码端的所有隐状态变量 h 1 , h 2 . . . h i . . . h T h_{1},h_{2}...h_{i}...h_{T} h1,h2...hi...hT,求了一个具有相似度味道的变量 e i j e_{ij} eij,并把这些 e i j e_{ij} eij,(解码时间步 i i i固定,编码时间步 j j j可变)利用softmax的归一化公式,得到解码端的注意力权重参数 α i j \alpha_{ij} αij,再把解码端的注意力权重参数 α i j \alpha_{ij} αij作用在每一个编码端的隐状态变量 h j h_{j} hj之上,累加求和得到了动态可变的解码端语境向量 c i c_{i} ci.
可见,attention机制最终得到的是,动态可变的解码端语境向量 c i c_{i} ci.
而未加attention机制,只有一个全局固定的编码端的上下文表征向量压缩向量 c c c.

物理上的解释就是:
会考虑每个输入词和当前的输出词之间的对齐关系(对应关系),对齐越好的词,会有越大的权重,对生成当前输出词的影响也就越大。
那什么叫对齐越好呢?一种解释是位置对齐,而明显太人为直观了,对代码程序中就是,如果两个向量完全一样,那么两个向量完全对齐,把对齐的概念貌似转成了相似的概念。

如果只使用一个方向的循环神经网络来计算隐状态,那么 h i h_i hi只包含了 x 0 x_0 x0 x i xi xi的信息,相当于在 α i j α_{ij} αij这里丢失了 x i x_i xi后面的词的信息。而使用双向循环神经网络进行建模,第 i i i个输入词对应的隐状态包含了和,前者编码 x 0 x_0 x0 x i x_i xi的信息,后者编码 x i x_i xi及之后所有词的信息,防止了前后文信息的丢失.
在这里插入图片描述

这样 e i j = a ( s i − 1 , h j ) e_{ij}=a(s_{i-1},h_j) eij=a(si1,hj)公式就会有两方面,
a ( s i − 1 , ← h j ) a(s_{i-1}, \leftarrow{h_j}) a(si1,hj)(原谅弱鸡打不出向左的上标箭头)
a ( s i − 1 , → h j ) a(s_{i-1}, \rightarrow{h_j}) a(si1,hj) ( a ( s i − 1 , h j ⃗ ) a(s_{i-1}, \vec{h_j}) a(si1,hj ))

下面就是self-attention了

说self-attention其实就是说transform模型,两者基本是一个意思。
下面的复习,大概基于这篇博文
起源:2017年,google的一篇很近的文章,《attention is all you need》
这里transform是google 在这篇文章对新框架命的名字,
新框架的技术是self-attention
新框架的亮点是decode和encode端都不用rnn这一类的了(rnn,lstm,gru都不用)
反而回到用前向神经网络,难道是退步吗?不,因为在进前向神经网络之前,经过了self-attention的操作。
这里隐含了一点,我们知道当初用rnn代替前向神经网络是因为rnn这一类的了能学习输入的序列性(前后性依赖),现在改成了self-attention+前向神经网络,这不就暗示着self-attention也能学习到输入的序列性(前后性依赖)吗?

transform 这个框架是seq框架的升级版,也是由一个encoder,一个decoder组成。区别就是encoder和decoder都不用rnn了,而换成了多个self-attention

所以大致列出区别如下:
传统seq2seq:

  • encoder:RNN
  • 中间联系:输入的压缩表征,一个固定的 c c c
  • decoder:RNN

带attention机制的seq2seq:

  • encoder:RNN
  • 中间联系:输入的压缩表征,动态的 c i = ∑ i = 1 T α i j h j c_i=\sum_{i=1}^{T}\alpha_{ij}h_j ci=i=1Tαijhj α i j = e x p ( e i j ) ∑ j = 1 T e x p ( e i j ) \alpha_{ij}=\dfrac{exp(e_{ij})}{\sum_{j=1}^{T}exp(e_{ij})} αij=j=1Texp(eij)exp(eij) e i j = a ( s i − 1 , h j ) e_{ij}=a(s_{i-1},h_j) eij=a(si1,hj)
  • decoder:RNN

带self-attention机制的transform:

  • encoder:self-attention
  • 中间联系:Encoder-Decoder Attention,类似带attention机制的seq2seq的中间部分
  • decoder:self-attention

行,讲到这里,就会觉得还是得粘图,不然讲不下去了。

在这里插入图片描述
整体架构确实不简单,但如果抽象的看,其实还是还是一个seq2seq结果,高度简化了,就是这样了:
在这里插入图片描述

Encoder的输出和decoder的结合如下,即最后一个encoder的输出将和每一层的decoder进行结合:

在这里插入图片描述
好了,现在我们可以主要关注每一层的encoder和每一层的decoder的内部结构了,如下图:

在这里插入图片描述
可以看到,就是前面说的,
Encoder的每一层有两个操作,self-attention 和前向神经网络,
而Decoder的每一层有三个操作,分别是self-attention、encoder-decoder attention 以及前向神经网络操作。
这里的Self-Attention和Encoder-Decoder Attention都是用的是Multi-Head Attention机制,这也是我们本文重点讲解的地方。(Multi-Head Attention机制 就是多份数据而已)

在介绍之前,我们先介绍下我们的数据,经过处理之后,数据如下:
在这里插入图片描述
很简单,上面部分是我们的x,也就是encoder的输入,下面部分是y,也就是decoder的输入,这是一个机器翻译的数据,x中的每一个id代表一个语言中的单词id,y中的每一个id代表另一种语言中的单词id。后面为0的部分是填充部分,代表这个句子的长度没有达到我们设置的最大长度,进行补齐。

Attention回顾

attention 中的公式如下:
c i = ∑ i = 1 T α i j h j c_i=\sum_{i=1}^{T}\alpha_{ij}h_j ci=i=1Tαijhj α i j = e x p ( e i j ) ∑ j = 1 T e x p ( e i j ) \alpha_{ij}=\dfrac{exp(e_{ij})}{\sum_{j=1}^{T}exp(e_{ij})} αij=j=1Texp(eij)exp(eij) e i j = a ( s i − 1 , h j ) e_{ij}=a(s_{i-1},h_j) eij=a(si1,hj)

Attention其实就是计算一种相关程度
attention的高度抽象,可以借助三个变量query,key,value。表示为将query(Q)和key-value pairs映射到输出上。其中query、每个key、每个value都是向量。输出是V中所有values的加权,其中权重是由Query和每个key计算出来的。
所以对于上面的三个公式来说:
query: s i − 1 s_{i-1} si1
key: h 1 , h 2 , h 3 . . . h T h_{1},h_{2},h_{3}...h_{T} h1,h2,h3...hT
value: h 1 , h 2 , h 3 . . . h T h_{1},h_{2},h_{3}...h_{T} h1,h2,h3...hT.
能看到这里,key=value.
不要奇怪,后面可能还能看到query=key=value,哈哈

Self-Attention

Self attention这个单词看起来好像每个人都知道是什么意思,但实质上他是算法领域中新出的概念,出处:Attention is All You Need self attention的原理。

下面我们先介绍如何用向量的方式来计算self attention,然后再来看看它是如何使用矩阵来实现的。
第一步:计算self attention的第一步是从每个Encoder的输入向量上创建3个向量(在这个情况下,对每个单词做词嵌入)。所以,对于每个单词,我们创建一个Query向量,一个Key向量和一个Value向量。这些向量是通过词嵌入乘以我们训练过程中创建的3个训练矩阵(Q,K,V)而产生的。

注意这些新向量的维度比嵌入向量小。我们知道嵌入向量的维度为512,而这里的新向量的维度只有64维。新向量并不是必须小一些,这是网络架构上的选择需要适当降低系统的计算量。(这里除了嵌入的词向量还有位置向量拼接)
在这里插入图片描述

我们将 X 1 X_1 X1 X 2 X_2 X2乘以 W Q W^{Q} WQ的权重矩阵得到新向量 q 1 q_1 q1 q 2 q_2 q2,既是“query”的向量。同理,最终我们可以对输入句子的每个单词创建“query”,“key”,“value”的新向量表示形式。

那么“query”,“key”,“value”是什么向量呢?有什么用呢?这些向量的概念是很抽象,但是它确实有助于计算注意力。不过先不用纠结去理解它,后面的的内容,会帮助你理解的。

第二步:计算self attention的第二步是计算得分。以上图为例,假设我们在计算第一个单词“thinking”的self attention。我们需要根据这个单词对输入句子的每个单词进行评分。当我们在某个位置编码单词时,分数决定了对输入句子的其他单词的关照程度。

通过将query向量和key向量点积来对相应的单词打分。所以,如果我们处理开始位置的的self attention,则第一个分数为 q 1 q_{1} q1 k 1 k_{1} k1的点积,第二个分数为 q 2 q_{2} q2 k 2 k_{2} k2的点积。如下图
在这里插入图片描述

第三步和第四步的计算,是将第二部的得分除以8( d k \sqrt{d_{k}} dk )(论文中使用key向量的维度是64维,其平方根=8,这样可以使得训练过程中具有更稳定的梯度。这个 d k \sqrt{d_{k}} dk 并不是唯一值,经验所得)。然后再将得到的输出通过softmax函数标准化,使得最后的列表和为1。

在这里插入图片描述
**这个softmax的分数决定了当前单词在每个句子中每个单词位置的表示程度。**很明显,当前单词对应句子中此单词所在位置的softmax的分数最高,但是,有时候attention机制也能关注到此单词外的其他单词,这很有用。

第五步是将每个Value向量乘以softmax后的得分。这里实际上的意义在于保存对当前词的关注度不变的情况下,降低对不相关词的关注。

第六步是 累加加权值的向量。 这会在此位置产生self-attention层的输出(对于第一个单词) z 1 z_1 z1
在这里插入图片描述

总结self-attention的计算过程,(单词级别)就是得到一个我们可以放到前馈神经网络的矢量 z 1 z_1 z1,前馈神经网络的输入就是 z 1 , z 2 , z 3 . . . z T z_{1},z_{2},z_{3}...z_{T} z1,z2,z3...zT。 然而在实际的实现过程中,该计算会以矩阵的形式完成,以便更快地处理。下面我们来看看Self-Attention的矩阵计算方式。

Matrix Calculation of Self-Attention

第一步是去计算Query,Key和Value矩阵。我们将词嵌入转化成矩阵X中,并将其乘以我们训练的权值矩阵( W Q , W K , W V W^{Q},W^{K},W^{V} WQ,WK,WV
在这里插入图片描述

X矩阵中的每一行对应于输入句子中的一个单词。 我们看到的X每一行的方框数实际上是词嵌入的维度,图中所示的和论文中是有差距的。X(图中的4个方框论文中为512个)和q / k / v向量(图中的3个方框论文中为64个)

最后,由于我们正在处理矩阵,我们可以在一个公式中浓缩前面步骤2到6来计算self attention层的输出。
在这里插入图片描述

Multi-Head Attention

通过使用“Multi-headed”的机制来进一步完善self attention层。“Multi-headed”(这种多份数据为什么有用呢?)
主要通过下面2中方式改善了attention层的性能:

  • 1 . 它拓展了模型关注不同位置的能力。在上面例子中可以看出,”The animal didn’t cross the street because it was too tired”,我们的attention机制计算出“it”指代的为“animal”,这在对语言的理解过程中是很有用的。
  • 2.它为attention层提供了多个“representation subspaces”子空间。由下图可以看到,在self attention中,我们有多个个Query / Key / Value权重矩阵(Transformer使用8个attention heads)。这些集合中的每个矩阵都是随机初始化生成的。然后通过训练,用于将词嵌入(或者来自较低Encoder/Decoder的矢量)投影到不同的“representation subspaces(表示子空间)”中。(类似于CNN的conv卷积层)。

通过multi-headed attention,我们为每个“header”都独立维护一套Q/K/V的权值矩阵。然后我们还是如之前单词级别的计算过程一样处理这些数据。
如果对上面的例子做同样的self attention计算,而因为我们有8头attention,所以我们会在八个时间点并行的去计算这些不同的权值矩阵,但最后结束时,我们会得到8个不同的矩阵。
在这里插入图片描述
我们知道在self-attention后面紧跟着的是前馈神经网络,而前馈神经网络接受的是单个矩阵向量,而不是8个矩阵。聪明的我们自然想到一种办法,把这8个矩阵压缩成一个矩阵,再乘以一个矩阵即可。
在这里插入图片描述
这样multi-headed self attention的全部内容就介绍完了。之前可能都是一些过程的图解,现在我将这些过程连接在一起,用一个整体的框图来表示一下计算的过程,希望可以加深理解。
在这里插入图片描述

位置向量

tansform中除了self-attention机制,考虑的还更多,例如考虑了位置向量。
回忆我们最开始讲的,传统seq2seq中的rnn一个考虑就是,rnn能考虑传统前向数据网络不能解决的输入序列性(前后顺序性)。
这里self-attention似乎只考虑了对应关系,相似关系,不能关注到位置关系,可能输入的位置不同,self-attentinon得到的结果仍一样,所以transform中,考虑了如何来解决输入序列中单词顺序的方法。
为了解决这个问题,transformer为每个输入单词的词嵌入上添加了一个新向量-位置向量。这些位置编码向量有固定的生成方式,所以获取他们是很方便的,但是这些信息确是很有用的,他们能捕捉每个单词的位置,或者序列中不同单词之间的距离。将这些信息也添加到词嵌入中,然后与Q/K/V向量点击,获得的attention就有了距离的信息了。

在这里插入图片描述
为了让模型捕捉到单词的顺序信息,我们添加位置编码向量信息(POSITIONAL ENCODING)-位置编码向量不需要训练,它有一个规则的产生方式。
如果我们的嵌入维度为4,那么实际上的位置编码就如下图所示:
在这里插入图片描述
那么生成位置向量需要遵循怎样的规则呢?

观察下面的图形,每一行都代表着对一个矢量的位置编码。因此第一行就是我们输入序列中第一个字的嵌入向量,每行都包含512个值,每个值介于1和-1之间。我们用颜色来表示1,-1之间的值,这样方便可视化的方式表现出来:
在这里插入图片描述
这是一个20个字(行)的(512)列位置编码示例。你会发现它咋中心位置被分为了2半,这是因为左半部分的值是一由一个正弦函数生成的,而右半部分是由另一个函数(余弦)生成。然后将它们连接起来形成每个位置编码矢量。

位置编码的公式在论文(3.5节)中有描述。你也可以在中查看用于生成位置编码的代码get_timing_signal_1d()。这不是位置编码的唯一可能方法。然而,它具有能够扩展到看不见的序列长度的优点(例如,如果我们训练的模型被要求翻译的句子比我们训练集中的任何句子都长)。

Layer-Normalization

这一节介绍的是encoder过程中的每个self-attention层的左右连接情况,我们称这个为:layer-normalization 步骤。这是深度学习中的常规操作,有加快收敛的作用,一般认为BN在cnn等模型中表现好,LN在序列性问题,rnn中比较的好。
在这里插入图片描述
在进一步探索其内部计算方式,我们可以将上面图层可视化为下图:
在这里插入图片描述
这里可能和我们想的有点不一样,因为LN不是直接对self-attention得到的输出Z向量来做的,而是对self-attention得到的输出Z向量和输入向量X一起做的,有点学习残差的味道吧。

Decoder的子层也是同样的,如果我们想做堆叠了2个Encoder和2个Decoder的Transformer,那么它可视化就会如下图所示:
在这里插入图片描述

All Processes

我们已经基本介绍完了Encoder的大多数概念,我们基本上也可以预知Decoder是怎么工作的。现在我们来仔细探讨下Decoder的数据计算原理,当序列输入时,Encoder开始工作,最后在其顶层的Encoder输出矢量组成的列表,然后我们将其转化为一组attention的集合( K e n c d e c , V e n c d e c K_{encdec},V_{encdec} Kencdec,Vencdec)。( K e n c d e c , V e n c d e c K_{encdec},V_{encdec} Kencdec,Vencdec)将带入每个Decoder的“encoder-decoder attention”层中去计算(这样有助于decoder捕获输入序列的位置信息)。
在这里插入图片描述
完成encoder阶段后,我们开始decoder阶段,decoder阶段中的每个步骤输出来自输出序列的元素(在这种情况下为英语翻译句子)。
上面实际上已经是应用的阶段了,我们并没有提到decoder端的训练,那我们decoder端训练阶段是如何的呢?
我们以下图的步骤进行训练,直到输出一个特殊的符号,表示已经完成了。 The output of each step is fed to the bottom decoder in the next time step, and the decoders bubble up their decoding results just like the encoders did. 对于Decoder,和Encoder一样,我们在每个Decoder的输入做词嵌入并添加上表示每个字位置的位置编码。
Decoder中的self attention与Encoder的self attention略有不同:

在Decoder中,在进行self attention操作时只能关注输出序列中的较早的位置(不能未卜先知吧)。具体实现是通过在解码端self attention计算中的softmax步骤之前屏蔽了特征位置(设置为 -inf)来完成的。

“Encoder-Decoder Attention”层的工作方式与"Multi-Headed Self-Attention"一样,只是它从下面的层创建其Query矩阵,并在Encoder堆栈的输出中获取Key和Value的矩阵。
也就是说 “Encoder-Decoder Attention”中的K,V是编码器给的,而 Q当然是解码器自己的东西。

Decoder的输出是浮点数的向量列表。我们是如何将其变成一个单词的呢?(这和其他网络没什么区别,都需要这样做)
这就是最终的线性层和softmax层所做的工作。线性层是一个简单的全连接神经网络,它是由Decoder堆栈产生的向量投影到一个更大,更大的向量中(Vocabulary_size)”,称为对数向量。

假设实验中我们的模型从训练数据集上总共学习到1万个英语单词(“Output Vocabulary”)。这对应的Logits矢量也有1万个长度-每一段表示了一个唯一单词的得分。在线性层之后是一个softmax层,softmax将这些分数转换为概率。选取概率最高的索引,然后通过这个索引找到对应的单词作为输出。

在这里插入图片描述
至此,循环神经网络复习2-seq2seq,attention,self-attention(transform) 已经讲解完毕,

### Self-Attention Encoder with Transformer Decoder Code Implementation In the context of implementing a self-attention mechanism within an encoder and utilizing a transformer-based decoder, one can follow this structured approach to build such models using PyTorch. The architecture leverages the power of transformers by incorporating multi-head attention mechanisms that allow each position in the encoder or decoder sequence to attend over all positions in the previous layer's output. The following Python code demonstrates how to implement a simple version of a self-attention encoder connected to a transformer decoder: ```python import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000): super().__init__() self.dropout = nn.Dropout(p=dropout) position = torch.arange(max_len).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)) pe = torch.zeros(max_len, 1, d_model) pe[:, 0, 0::2] = torch.sin(position * div_term) pe[:, 0, 1::2] = torch.cos(position * div_term) self.register_buffer('pe', pe) def forward(self, x): """ Args: x: Tensor, shape [seq_len, batch_size, embedding_dim] """ x = x + self.pe[:x.size(0)] return self.dropout(x) class TransformerModel(nn.Module): def __init__(self, ntoken: int, d_model: int, nhead: int, d_hid: int, nlayers: int, dropout: float = 0.5): super().__init__() from torch.nn import TransformerEncoder, TransformerEncoderLayer self.model_type = 'Transformer' self.pos_encoder = PositionalEncoding(d_model, dropout) encoder_layers = TransformerEncoderLayer(d_model, nhead, d_hid, dropout) self.transformer_encoder = TransformerEncoder(encoder_layers, nlayers) # For demonstration purposes only; actual vocab size should be defined based on dataset. self.encoder = nn.Embedding(ntoken, d_model) self.d_model = d_model # Define transformer decoder layers here similarly to encoder but use `nn.TransformerDecoder`. decoder_layer = nn.TransformerDecoderLayer(d_model=d_model, nhead=nhead) self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=nlayers) self.init_weights() def init_weights(self) -> None: initrange = 0.1 self.encoder.weight.data.uniform_(-initrange, initrange) def forward(self, src: torch.Tensor, tgt: torch.Tensor, src_mask: torch.Tensor = None, tgt_mask: torch.Tensor = None) -> torch.Tensor: """ Args: src: Tensor, shape [seq_len, batch_size] tgt: Tensor, shape [target_seq_len, batch_size] Returns: output Tensor of shape [target_seq_len, batch_size, ntoken] """ src = self.encoder(src) * math.sqrt(self.d_model) src = self.pos_encoder(src) memory = self.transformer_encoder(src, src_mask) output = self.decoder(tgt=tgt, memory=memory, tgt_mask=tgt_mask) return output ``` This implementation includes both positional encoding and embeddings added together before being passed through multiple layers of encoders and decoders[^1]. Note that while training would typically involve more components like optimizers, loss functions, etc., these have been omitted for clarity.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值