Transformer 将注意力发挥到极致

作者: 莫烦 编辑: 莫烦 2020-08-28

学习资料:

怎么了

我们了解到注意力是一件好事,在之前RNN+Attention 架构中, 我们看到了当模型使用注意力关注被处理的输入信息时,模型的训练效果得到了很好的提升。既然注意力效果这么好,那RNN encode的信息还有那么重要吗? 我们能不能直接绕过RNN,来直接在词向量阶段就开始使用注意力?我制作的这个短片简介 很好的解释了这样一种新方式的好处。简而言之有这么两种方案:在理解一句话时,

  1. 我们可以选择先读一遍,基于读过之后的理解上,再为后续处理分配不同的注意力;或者
  2. 我们不通读,而是跳着读关键词,直接用注意力方法找出并运用这些关键词

rnn and direct attention comparison

研究发现,第二种方法在语言的理解上能够更上一筹,而且在同等量级的网络规模上,要比第一种方法快很多。 而且基于第二种方法再拓展一点。我理解的句子的时候可以不仅仅只过目一遍,我还可以像多层RNN一样,在理解的基础上再次理解。 只是这次,不会像RNN那样,每次都对通句加深理解,而是一遍又一遍地注意到句子的不同部分,提炼对句子更深层的理解。

attention layer

这次介绍的Transformer模型就带领学术界将attention玩出花,玩到更好的效果。

全都是注意

Transformer这个模型的论文题很网红,叫作Attention Is All You Need,你说这些作者都能找到这么短小精炼的话题,不去做网红真的可惜了。 不过说回来,如果一个模型里面如果全都是注意力,那么怎么设计比较好呢?

model struct

这是论文里面的图,如果是深度学习玩家,看到这种模型感觉起来好像也没有很复杂。但是注意到,这个图只画了一层attention,还有一个Nx,它可是要扩展成N个这种结构呀。 一般来说N的取值从个位数到十位数不等,大于20的话,一般的电脑恐怕就吃不消了,GPU的内存都不够用的。

新手深度学习玩家,看不懂不要紧,因为莫烦从来只会把复杂的问题说简单。

Transformer 这种模型是一种 seq2seq 模型,是为了解决生成语言的问题。它也有一个 Encoder-Decoder 结构,只是它不像RNN中的 Encoder-Decoder。 之后我们将要介绍的 BERT 就是这个 Transformer 的Encoder部分,GPT就是它的 Decoder 部分,目前我们可以这样理解,后续我们会介绍他们之间的差别点。

transformer encoder decoder

主要目的都一样,为了完成语言的理解和任务的输出,使用Encoder对语言信息进行压缩和提炼,然后用Decoder产生相对的内容。 详细说明的话,Encoder 负责仔细阅读,一遍一遍地阅读,每一遍阅读都是重新使用注意力关注到上次的理解,对上次的理解进行再一次转义。 Decoder 任务同Seq2Seq 的 decoder 任务一样,同时接收Encoder的理解和之前预测的结果信息,生成下一步的预测结果

总算到了最最最重要的地方了,这也是 Transformer 的核心点,它的 attention 是怎么做的呢?

transformer qkv

上面是论文中的原图,看懵逼了?没问题,莫烦帮你抽象化。它关注的有三种东西,Query, Key, Value。有的同学可能在别的论文中发现过这种结构, 我最开始看论文的时候总是弄不清这三个东西的关系。所以我给你画个图,你可能好理解一点。 其实做这件事的核心目的是快速准确地找到核心内容,换句话说:用我的搜索(Query)找到关键内容(Key),在关键内容上花时间花功夫(Value)。

transformer qkv

想象这是一个相亲画面,我有我心中有个喜欢女孩的样子,我会按照这个心目中的形象浏览各女孩的照片,如果一个女生样貌很像我心中的样子,我就注意这个人, 并安排一段稍微长一点的时间阅读她的详细材料,反之我就安排少一点时间看她的材料。这样我就能将注意力放在我认为满足条件的候选人身上了。 我心中女神的样子就是Query,我拿着它(Query)去和所有的候选人(Key)做对比,得到一个要注意的程度(attention), 根据这个程度判断我要花多久时间仔细阅读候选人的材料(Value)。 这就是Transformer的注意力方式。

为了增强注意力的能力,Transformer还做了一件事:注意力修改成了注意力注意力注意力注意力。哈哈哈,这叫做多头注意力(Multi-Head Attention)。 论文中的原图长这样。

transformer multihead

有看不懂啦?有什么关系,反正莫烦还是会用更简单的图解释,不是吗?

transformer multihead

其实多头注意力指的就是在同一层做注意力计算的时候,我多搞几次注意力。有点像我同时找了多个人帮我注意一下,这几个人帮我一轮一轮注意+理解之后, 我在汇总所有人的理解,统一判断。有点三个臭皮匠赛过诸葛亮的意思。

最后一个我想提到的重点是Decoder怎么样拿到Encoder对句子的理解的?或者Encoder是怎么样引起Decoder的注意的? 在理解这个问题之前,我们需要知道Encoder和Decoder都存在注意力,Encoder里的的注意力叫做自注意力(self-attention), 因为Encoder在这个时候只是自己和自己玩,自己捣鼓一句话的意思。而Decoder说:你把你捣鼓到的意思借我参考一下吧。 这时Self-attention在transformer中的意义才被凸显出来。

transformer decoder

在Decoding时,decoder会向encoder借一下Key和Value,Decoder自己可以提供Query(已经预测出来的token)。使用我们刚刚提到的K,Q,V结合方式计算。 不过这张图里面还有些细节没有提到,比如 Decoder 先要经过Masked attention再和encoder的K,V结合,然后还有有一个feed forward计算,还要计算残差。 因为这些比起怎么Attention,都略显不那么重要,一个个深入的话,这篇文章就太长了。不过我可以简单提一下。

  • Masked attention: 不让decoder在训练的时候用后文的信息生成前文的信息;
  • Feed forward: 这个encoder,decoder都有,做一下非线性处理;
  • 残差计算:这个也是encoder和decoder都有,为了更有效的backpropagation。

翻译

在这节内容中,我还是以翻译为例。延续前几次用到日期翻译的例子, 我们知道在翻译的模型中,实际上是要构建一个Encoder,一个Decoder。这节内容我们就是让Decoder在生成语言的时候,也注意到Encoder的对应部分。

# 中文的 "年-月-日" -> "day/month/year"
"98-02-26" -> "26/Feb/1998"

今天的例子有很多种不同的attention,也有很多不同的结果,我们先剧透一个比较重要的,encoder-decoder交织的attention:

encoder decoder attention res matrix

encoder decoder attention res line

对于这些结果的解释,我们下面再说。 对比前几期内容,Transformer的翻译任务收敛得也是很快的。

step:  0 | time: 0.55 | loss: 3.7659 | target:  17/Jun/1996 | inference:  SepOctOct<EOS>Sep5<EOS>5AprJul
step:  50 | time: 7.62 | loss: 1.1756 | target:  19/May/1996 | inference:  19/1/1999<EOS>
step:  100 | time: 8.46 | loss: 0.7199 | target:  09/Mar/1995 | inference:  19/Jan/1999<EOS>
step:  150 | time: 7.54 | loss: 0.3360 | target:  17/Jul/1996 | inference:  17/Jan/1996<EOS>
step:  200 | time: 7.58 | loss: 0.0793 | target:  24/Sep/2022 | inference:  24/Sep/2022<EOS>
...
step:  500 | time: 7.80 | loss: 0.0053 | target:  31/Mar/2024 | inference:  31/Mar/2024<EOS>
step:  550 | time: 8.88 | loss: 0.0026 | target:  22/Jan/1997 | inference:  22/Jan/1997<EOS>

秀代码

今天的全部的代码有300+行,对于深度研究的朋友应该不算多,但是对于教学算偏多了,我在这里教学时会调重点。如果你习惯直接看全部代码, 请点击这里

我代码的组织形式是这样,我将encoder,decoder,attention等层全部都抽象出来,代码也极度简化了,希望能方便你理解。

class MultiHead:
    ...
    def scaled_dot_product_attention(self, q, k, v, mask=None):
        ...
    def call(self, q, k, v, mask):
        # 处理一下 q k v
        o = self.scaled_dot_product_attention(q, k, v, mask)
        return o

class PositionWiseFFN:
    # 主要为了重新定义一下结果的 shape,方便传入下一层 layer

上面两个功能是Encoder和Decoder都会用到的功能,所以我们统一写一下。下面是encoder和decoder layer怎么组装这些功能。

class EncodeLayer:
    def __init__(self):
        self.mh = MultiHead()
        self.ffn = PositionWiseFFN()

    def call(self, xz):
        attn = self.mh.call(xz, xz, xz, ...)    # multi head attention
        o1 = attn + xz                          # 残差
        ffn = self.ffn.call(o1)                 # 非线性
        o = ffn + o1                            # 残差
        return o

class DecoderLayer:
    def __init__(self):
        self.mh1 = MultiHead()
        self.mh2 = MultiHead()
        self.ffn = PositionWiseFFN()
    
    def call(self, yz, xz):
        attn = self.mh1.call(yz, yz, yz, ...)   # decoder 的 multi head attention
        o1 = attn + yz                          # 残差
        attn = self.mh2.call(o1, xz, xz, ...)   # encoder + decoder 的混合 multi head attention
        o2 = attn + o1                          # 残差
        ffn = self.ffn.call(o2)                 # 非线性
        o = ffn + o2                            # 残差
        return o

上面这些步骤是用来组成Encoder和Decoder的每一层layer的。 里面包含了mutli head attention的计算、残差计算、encoder+decoder混合attention、非线性处理等计算。 接下来我们将要把layer加到 encoder和decoder当中去。

class Encoder:
    def __init__(self):
        self.ls = [EncodeLayer() for _ in range(n)]
    
    def call(self, xz):
        for l in self.ls:
            xz = l.call(xz)
        return xz

class Decoder:
    def __init__(self):
        self.ls = [DecodeLayer() for _ in range(n)]
    
    def call(self, yz, xz):
        for l in self.ls:
            yz = l.call(yz, xz)
        return yz

Encoder只管好自己就行,Decoder需要拿着Encoder给出来的xz,一起计算。最后我们把它们整进Transformer。

class Transformer:
    def __init__(self):
        self.embed = PositionEmbedding(max_len, model_dim, n_vocab)
        self.encoder = Encoder(n_head, model_dim, drop_rate, n_layer)
        self.decoder = Decoder(n_head, model_dim, drop_rate, n_layer)
        self.o = keras.layers.Dense(n_vocab)
    
    def call(self, x, y):
        x_embed, y_embed = self.embed(x), self.embed(y)
        encoded_z = self.encoder.call(x_embed, ...)
        decoded_z = self.decoder.call(y_embed, encoded_z, ...)
        o = self.o(decoded_z)
        return o

这就是整个Transformer的框架啦。说简单也简单(我帮你简化了), 说难也难(真实代码还是有300+行)。 按照这个框架写一些训练代码,你的程序就能跑起来了。

结果讨论

最重要的,还是encoder和decoder配合的结果。我用矩阵和连线的方法分开给你展示。 可视化代码你也可以拿去随意使用。

encoder decoder matrix

在论文里,应该经常看到上面这种图,我们看到最后一层layer3, 这个就是decoder在结合encoder信息后的attention,生成的预测结果。我们很明显可以看到中英文日期对应的点上, 注意力都非常大。

换一种角度来看,我们再用连线可以更加明显的看出来这样的关系。

encoder decoder line

下面我们再来看看decoder,encoder各自的self-attention.

encoder self attention

Encoder 的 self-attention 看不出来太多信息,因为我们这个数据集在自注意上并不是很强调,X的语句内部没有多少相关的信息。所以训练出来的encoder self-attention 并不明显。

decoder self attention

反而是decoder的self-attention还是有些信息的。因为decoder在做self-attention时, 实际上还是会多多少少接收到encoder attention的影响。因为encoder的attention信息被传输过来了。

还有一个有意思的点,不知道你们发现没,decoder的attention图,为什么是一个三角形? 原因在我在上面提到的,预测时,不能让后文的信息影响到前文,就会用一个look_ahead_mask将后文的信息给遮盖住。这个mask长成这样:

look ahead mask

那问题又来了,为什么这个mask不是一个对角的三角形呢?原因是有些句子没那么长,也可以一起mask掉,我把这种叫做 pad_mask,像下图这样。

pad mask

最后,还有一个问题,transformer的attention不像RNN,它没有捕捉到文字序列上的时序信息。那我们怎么让模型知道一句话的顺序呢? 这个有多种做法,比如让模型仔细学一个position embedding,或者你给一个有规律的position embedding就好了。positional embedding 的代码你就直接在我github上搜PositionEmbedding吧。 下面展示的是可视化出来的Position Embedding:

positional embedding

总结

好了,这就差不多了,总算把这么复杂的transformer给肢解了,费了老大力。理解了transformer,知道了模型是怎么玩注意力的,后面的当前很流行的 GPTBert就能迎刃而解~

降低知识传递的门槛

莫烦的对教育的态度是: 降低知识传递的门槛,不希望给"学习"设置金钱障碍。 这是我花大量业余时间贡献 免费 AI分享的原因。 通过 【赞助】 能及时让我看到你对 【莫烦态度】 的认同,我也更有理由坚持下去。

如果你当前目标是找工作或者转行AI,想接受更加丰富的教学资源、培训辅导体验,我想推荐我的朋友 七月在线 给你, 通过这个 【莫烦Python为你提供的注册链接】, 你将可以获得莫烦专门为你协商的课程优惠券。祝你找/换工作顺利~