Transformer结构及其应用

Apr 25, 2024
1 views
NLP

取代RNN——Transformer

在介绍Transformer前我们来回顾一下RNN的结构

image

对RNN有一定了解的话,一定会知道,RNN有两个很明显的问题

  • 效率问题:需要逐个词进行处理,后一个词要等到前一个词的隐状态输出以后才能开始处理
  • 如果传递距离过长还会有梯度消失、梯度爆炸和遗忘问题
    为了缓解传递间的梯度和遗忘问题,设计了各种各样的RNN cell,最著名的两个就是LSTM和GRU了

LSTM (Long Short Term Memory)

image

GRU (Gated Recurrent Unit)

image

但是,引用网上一个博主的比喻,这么做就像是在给马车换车轮,为什么不直接换成汽车呢?

于是就有了Transformer。Transformer 是Google Brain 2017的提出的一篇工作,它针对RNN的弱点进行重新设计,解决了RNN效率问题和传递中的缺陷等,在很多问题上都超过了RNN的表现。Transfromer的基本结构如下图所示,它是一个N进N出的结构,也就是说每个Transformer单元相当于一层的RNN层,接收一整个句子所有词作为输入,然后为句子中的每个词都做出一个输出。但是与RNN不同的是,Transformer能够同时处理句子中的所有词,并且任意两个词之间的操作距离都是1,这么一来就很好地解决了上面提到的RNN的效率问题和距离问题。

image

每个Transformer单元都有两个最重要的子层,分别是Self-Attention层与Feed Forward层,后面会对这两个层的详细结构做介绍。文章使用Transformer搭建了一个类似Seq2Seq的语言翻译模型,并为Encoder与Decoder设计了两种不同的Transformer结构。

image

Decoder Transformer相对于Encoder Transformer多了一个Encoder-Decoder Attention层,用来接收来自于Encoder的输出作为参数。最终只要按照下图的方式堆叠,就可以完成Transformer Seq2Seq的结构搭建。

image

举个例子介绍下如何使用这个Transformer Seq2Seq做翻译

  • 首先,Transformer对原语言的句子进行编码,得到memory。
  • 第一次解码时输入只有一个标志,表示句子的开始。
  • 解码器通过这个唯一的输入得到的唯一的输出,用于预测句子的第一个词。
  • 第二次解码,将第一次的输出Append到输入中,输入就变成了和句子的第一个词(ground truth或上一步的预测),解码生成的第二个输出用于预测句子的第二个词。以此类推(过程与Seq2Seq非常类似)
    了解了Transformer的大致结构以及如何用它来完成翻译任务后,接下来就看看Transformer的详细结构:

image

核心组件就是上面所提到的Self-Attention和Feed Forward Networks,但还有很多其他细节,接下来我们就开始逐个结构的来解读Transformer。

Self Attention

Self Attention就是句子中的某个词对于本身的所有词做一次Attention。算出每个词对于这个词的权重,然后将这个词表示为所有词的加权和。每一次的Self Attention操作,就像是为每个词做了一次Convolution操作或Aggregation操作。具体操作如下:

首先,每个词都要通过三个矩阵\(W_q, W_k, W_v\) 进行一次线性变化,一分为三,生成每个词自己的query, key, vector三个向量。以一个词为中心进行Self Attention时,都是用这个词的key向量与每个词的query向量做点积,再通过Softmax归一化出权重。然后通过这些权重算出所有词的vector的加权和,作为这个词的输出。具体过程如下图所示

image

归一化之前需要通过除以向量的维度\(d_k\)来进行标准化,所以最终Self Attention用矩阵变换的方式可以表示为

\[ Q = XW_Q\\K=XW_K\\V=XW_V\\ Attention(Q,K,V)=Softmax(\frac{QK^T}{\sqrt{d_k}})V \]

最终每个Self Attention接受n个词向量的输入,输出n个Aggregated的向量。

上文提到Encoder中的Self Attention与Decoder中的有所不同,Encoder中的Q、K、V全部来自于上一层单元的输出,而Decoder只有Q来自于上一个Decoder单元的输出,K与V都来自于Encoder最后一层的输出。也就是说,Decoder是要通过当前状态与Encoder的输出算出权重后,将Encoder的编码加权得到下一层的状态。

Masked Attention

通过观察上面的结构图我们还可以发现Decoder与Encoder的另外一个不同,就是每个Decoder单元的输入层,要先经过一个Masked Attention层。那么Masked的与普通版本的Attention有什么区别呢?

Encoder因为要编码整个句子,所以每个词都需要考虑上下文的关系。所以每个词在计算的过程中都是可以看到句子中所有的词的。但是Decoder与Seq2Seq中的解码器类似,每个词都只能看到前面词的状态,所以是一个单向的Self-Attention结构。

Masked Attention的实现也非常简单,只要在普通的Self Attention的Softmax步骤之前,与(&)上一个下三角矩阵M就好了

\[ Attention(Q,K,V)=Softmax(\frac{QK^T\odot M}{\sqrt{d_k}})V \]

Multi-Head Attention

Multi-Head Attention就是将上述的Attention做h遍,然后将h个输出进行concat得到最终的输出。这样做可以很好地提高算法的稳定性,在很多Attention相关的工作中都有相关的应用。Transformer的实现中,为了提高Multi-Head的效率,将W扩大了h倍,然后通过view(reshape)和transpose操作将相同词的不同head的k、q、v排列在一起进行同时计算,完成计算后再次通过reshape和transpose完成拼接,相当于对于所有的head进行了一个并行处理。

$$
MultiHead(Q, K, V ) = Concat(head_1, ..., head_h)W^O\
\ \ \ \ \ \ \ \ where\ head_i = Attention(Q , K , V)

$$

Position-wise Feed Forward Networks

Encoder中和Decoder中经过Attention之后输出的n个向量(这里n是词的个数)都分别的输入到一个全连接层中,完成一个逐个位置的前馈网络。

\[ FFN(x)=max(0, xW_1+b_1)W_2+b_2 \]

image

Add & Norm

是一个残差网络,将一层的输入与其标准化后的输出进行相加即可。Transformer中每一个Self Attention层与FFN层后面都会连一个Add & Norm层。

image

Positional Encoding

一种做法就是分配一个0到1之间的数值给每个时间步,其中,0表示第一个词,1表示最后一个词。这种方法虽然简单,但会带来很多问题。其中一个就是你无法知道在一个特定区间范围内到底存在多少个单词。换句话说,不同句子之间的时间步差值没有任何的意义。

另一种做法就是线性分配一个数值给每个时间步。也就是,1分配给第一个词,2分配给第二个词,以此类推。这种方法带来的问题是,不仅这些数值会变得非常大,而且模型也会遇到一些比训练中的所有句子都要长的句子。此外,数据集中不一定在所有数值上都会包含相对应长度的句子,也就是模型很有可能没有看到过任何一个这样的长度的样本句子,这会严重影响模型的泛化能力。

因此,一种好的位置编码方案需要满足以下几条要求:

  • 它能为每个时间步输出一个独一无二的编码;
  • 不同长度的句子之间,任何两个时间步之间的距离应该保持一致;
  • 模型应该能毫不费力地泛化到更长的句子。它的值应该是有界的;
  • 它必须是确定性的。
    Transformer的作者们提出了一个简单但非常创新的位置编码方法,能够满足上述所有的要求。首先,这种编码不是单一的一个数值,而是包含句子中特定位置信息的d 维向量(非常像词向量)。第二,这种编码没有整合进模型,而是用这个向量让每个词具有它在句子中的位置的信息。换句话说,通过注入词的顺序信息来增强模型输入。

相对位置的线性关系: 正弦曲线函数的位置编码的另一个特点是,它能让模型毫不费力地关注相对位置信息。这里引用原文的一段话:

We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset \(k\), \(\text{PE}_{\text{pos}+k}\) can be represented as a linear function of \(\text{PE}_{\text{pos}}\)

由于Transformer中既不存在RNN,也不同于CNN,句子里的所有词都被同等的看待,所以词之间就没有了先后关系。换句话说,很可能会带上和词袋模型相同的不足。为了解决这个问题,Transformer提出了Positional Encoding的方案,就是给每个输入的词向量叠加一个固定的向量来表示它的位置。文中使用的Positional Encoding如下

\[ PE(pos,2i)=sin(pos/10000^{2i/d_{model}})\\ PE(pos,2i+1)=cos(pos/10000^{2i/d_{model}}) \]

其中\(pos\)是词在句子中的位置,\(i\) 是词向量中第i位,即将每个词的词向量为一行进行叠加,然后针对每一列都叠加上一个相位不同或波长逐渐增大的波,以此来唯一区分位置。

Transformer 工作流程

Transformer的工作流程就是上面介绍的每一个子流程的拼接

  • 输入的词向量首先叠加上Positional Encoding,然后输入至Transformer内
  • 每个Encoder Transformer会进行一次Multi-head self attention->Add & Normalize->FFN->Add & Normalize流程,然后将输出输入至下一个Encoder中
  • 最后一个Encoder的输出将会作为memory保留
  • 每个Decoder Transformer会进行一次Masked Multi-head self attention->Multi-head self attention->Add & Normalize->FFN->Add & Normalize流程,其中Multi-head self attention时的K、V至来自于Encoder的memory。根据任务要求输出需要的最后一层Embedding。
  • Transformer的输出向量可以用来做各种下游任务

具体实现可以参考这里:

🔖 https://nlp.seas.harvard.edu/2018/04/03/attention.html

transformer机器翻译任务具体训练和推理过程

推理过程:

以“我爱你”到“I love you”为例,对于transformer来讲,在翻译“我爱你”这句话时,先将其进行embedding和encoding送入encoder(包含n层encoder layers),encoder layer之间完全是串联关系,最终在第n层encoder layer得到k,v。

对于decoder来讲,t0时刻先输入起始符(S),通过n层decoder layer(均计算masked自注意力与交互注意力(由decoder得到的q与encoder输出的k,v计算)),最终在第n层decoder layer得到当前的预测结果projection(可以理解成“I love you”中的“I”);在t1时刻,将t0时刻得到的输出“I”输入至decoder,重复t0的过程(计算masked自注意力与交互注意力),最终在第n层decoder layer得到输出“love”。最后经过多次计算,得到完整输出结果“I love you E”(E为终止符,由“you”输入decoder得到),是一个自回归的过程

训练过程:

仍以“我爱你”到“I love you”为例,在训练过程中,encoder输入的是待翻译句子“我爱你”,得到k,v;decoder输入的是“S I love you”(S位起始符);最终计算loss的label是“I love you E”。

所谓的mask其实就是一个上斜为1的矩阵,如下图所示

image

对于mask,0为可以看到的字,1为看不到的字。在此具体问题中,当计算自注意力的字是S时,就看不到后面的“I”,“love”,“you”;计算z自注意力的字时“I”时,就看不到后面的“love”,“you”,以此类推。

对于decoder中的交互注意力来讲,q,k,v(k,v是encoder)均来自输入的一整段话。我们可以看到,在由q,k相乘结果再与v相乘前,先对\(qk^T\)进行了softmax计算,那么在进行softmax前,mask就发挥作用了,decoder会根据mask将对应位置的值设置为无穷小, 比如\(-1e^{10}\),这样在计算softmax时,会使其失去作用(趋近于0),进而在与v相乘时,也就忽略了v中对应的 “love” 和 “you” 的部分。

以“S I love you”为例,如果我们想对“I”求交互注意力,那么我们应该让此时的注意力机制看不到后面的“love”和“you”,此时对应了上文中mask矩阵的第二行,即让“love”和“you”在qkt中对应的值置为无穷小,这样在做softmax的时候,就可以忽略“love”和“you”的作用,也就是在一定程度上实现了“看不到”后面的“love”和“you”的作用

以上就是decoder在训练过程中,mask所起到的作用,最后decoder得到预测输出,与label的“I love you E”计算loss,使得其输出逼近label,最终得到训练好的模型。

单向二阶段训练模型——OpenAI GPT

GPT(Generative Pre-Training),是OpenAI在2018年提出的模型,利用Transformer模型来解决各种自然语言问题,例如分类、推理、问答、相似度等应用的模型。GPT采用了Pre-training + Fine-tuning的训练模式,使得大量无标记的数据得以利用,大大提高了这些问题的效果。

GPT就是利用Transformer进行自然语言各种任务的尝试之一,主要有以下三个要点

  • Pre-Training的方式
  • 单向Transformer模型
  • Fine-Tuning与不同输入数据结构的变化
    如果已经理解了Transformer的原理,那么只需要再搞懂上面的三个内容就能够对GPT有更深的认识。

Pre-Training 训练方式

很多机器学习任务都需要带标签的数据集作为输入完成。但是我们身边存在大量没有标注的数据,例如文本、图片、代码等等。标注这些数据需要花费大量的人力和时间,标注的速度远远不及数据产生的速度,所以带有标签的数据往往只占有总数据集很小的一部分。随着算力的不断提高,计算机能够处理的数据量逐渐增大。如果不能很好利用这些无标签的数据就显得很浪费。

所以半监督学习和预训练+微调的二阶段模式整变得越来越受欢迎。最常见的二阶段方法就是Word2Vec,使用大量无标记的文本训练出带有一定语义信息的词向量,然后将这些词向量作为下游机器学习任务的输入,就能够大大提高下游模型的泛化能力。

但是Word2Vec有一个问题,就是单个单词只能有一个Embedding。这样一来,一词多义就不能很好地进行表示。

单向Transformer结构

OpenAI GPT采用了单向Transformer完成了这项预训练任务。