Transformer 将注意力发挥到极致
学习资料:
- 本节的全部代码
- 代码依赖的 utils.py 和 visual.py 在这里找到
- 我制作的 自然语言处理注意力机制 短片简介
- 我制作的 自然语言处理注意力升级版 短片简介
- Transformer 论文:Attention Is All You Need
怎么了¶
我们了解到注意力是一件好事,在之前RNN+Attention 架构中, 我们看到了当模型使用注意力关注被处理的输入信息时,模型的训练效果得到了很好的提升。既然注意力效果这么好,那RNN encode的信息还有那么重要吗? 我们能不能直接绕过RNN,来直接在词向量阶段就开始使用注意力?我制作的这个短片简介 很好的解释了这样一种新方式的好处。简而言之有这么两种方案:在理解一句话时,
- 我们可以选择先读一遍,基于读过之后的理解上,再为后续处理分配不同的注意力;或者
- 我们不通读,而是跳着读关键词,直接用注意力方法找出并运用这些关键词。
研究发现,第二种方法在语言的理解上能够更上一筹,而且在同等量级的网络规模上,要比第一种方法快很多。 而且基于第二种方法再拓展一点。我理解的句子的时候可以不仅仅只过目一遍,我还可以像多层RNN一样,在理解的基础上再次理解。 只是这次,不会像RNN那样,每次都对通句加深理解,而是一遍又一遍地注意到句子的不同部分,提炼对句子更深层的理解。
这次介绍的Transformer模型就带领学术界将attention玩出花,玩到更好的效果。
全都是注意¶
Transformer这个模型的论文题很网红,叫作Attention Is All You Need
,你说这些作者都能找到这么短小精炼的话题,不去做网红真的可惜了。 不过说回来,如果一个模型里面如果全都是注意力,那么怎么设计比较好呢?
这是论文里面的图,如果是深度学习玩家,看到这种模型感觉起来好像也没有很复杂。但是注意到,这个图只画了一层attention,还有一个Nx
,它可是要扩展成N个这种结构呀。 一般来说N的取值从个位数到十位数不等,大于20的话,一般的电脑恐怕就吃不消了,GPU的内存都不够用的。
新手深度学习玩家,看不懂不要紧,因为莫烦从来只会把复杂的问题说简单。
Transformer 这种模型是一种 seq2seq 模型,是为了解决生成语言的问题。它也有一个 Encoder-Decoder 结构,只是它不像RNN中的 Encoder-Decoder。 之后我们将要介绍的 BERT 就是这个 Transformer 的Encoder部分,GPT就是它的 Decoder 部分,目前我们可以这样理解,后续我们会介绍他们之间的差别点。
主要目的都一样,为了完成语言的理解和任务的输出,使用Encoder对语言信息进行压缩和提炼,然后用Decoder产生相对的内容。 详细说明的话,Encoder 负责仔细阅读,一遍一遍地阅读,每一遍阅读都是重新使用注意力关注到上次的理解,对上次的理解进行再一次转义。 Decoder 任务同Seq2Seq 的 decoder 任务一样,同时接收Encoder的理解和之前预测的结果信息,生成下一步的预测结果
总算到了最最最重要的地方了,这也是 Transformer 的核心点,它的 attention 是怎么做的呢?
上面是论文中的原图,看懵逼了?没问题,莫烦帮你抽象化。它关注的有三种东西,Query, Key, Value。有的同学可能在别的论文中发现过这种结构, 我最开始看论文的时候总是弄不清这三个东西的关系。所以我给你画个图,你可能好理解一点。 其实做这件事的核心目的是快速准确地找到核心内容,换句话说:用我的搜索(Query)找到关键内容(Key),在关键内容上花时间花功夫(Value)。
想象这是一个相亲画面,我有我心中有个喜欢女孩的样子,我会按照这个心目中的形象浏览各女孩的照片,如果一个女生样貌很像我心中的样子,我就注意这个人, 并安排一段稍微长一点的时间阅读她的详细材料,反之我就安排少一点时间看她的材料。这样我就能将注意力
放在我认为满足条件的候选人身上了。 我心中女神的样子就是Query,我拿着它(Query)去和所有的候选人(Key)做对比,得到一个要注意的程度(attention), 根据这个程度判断我要花多久时间仔细阅读候选人的材料(Value)。 这就是Transformer的注意力方式。
为了增强注意力
的能力,Transformer还做了一件事:从注意力
修改成了注意力注意力注意力注意力
。哈哈哈,这叫做多头注意力(Multi-Head Attention)。 论文中的原图长这样。
有看不懂啦?有什么关系,反正莫烦还是会用更简单的图解释,不是吗?
其实多头注意力
指的就是在同一层做注意力计算的时候,我多搞几次注意力。有点像我同时找了多个人帮我注意一下,这几个人帮我一轮一轮注意+理解之后, 我在汇总所有人的理解,统一判断。有点三个臭皮匠赛过诸葛亮
的意思。
最后一个我想提到的重点是Decoder怎么样拿到Encoder对句子的理解的?或者Encoder是怎么样引起Decoder的注意的? 在理解这个问题之前,我们需要知道Encoder和Decoder都存在注意力,Encoder里的的注意力叫做自注意力(self-attention), 因为Encoder在这个时候只是自己和自己玩,自己捣鼓一句话的意思。而Decoder说:你把你捣鼓到的意思借我参考一下吧。 这时Self-attention在transformer中的意义才被凸显出来。
在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:
对于这些结果的解释,我们下面再说。 对比前几期内容,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等层全部都抽象出来,代码也极度简化了,希望能方便你理解。
上面两个功能是Encoder和Decoder都会用到的功能,所以我们统一写一下。下面是encoder和decoder layer怎么组装这些功能。
上面这些步骤是用来组成Encoder和Decoder的每一层layer的。 里面包含了multi-head attention的计算、残差计算、encoder+decoder混合attention、非线性处理等计算。 接下来我们将要把layer加到 encoder和decoder当中去。
Encoder只管好自己就行,Decoder需要拿着Encoder给出来的xz
,一起计算。最后我们把它们整进Transformer。
这就是整个Transformer的框架啦。说简单也简单(我帮你简化了), 说难也难(真实代码还是有300+行)。 按照这个框架写一些训练代码,你的程序就能跑起来了。
结果讨论¶
最重要的,还是encoder和decoder配合的结果。我用矩阵和连线的方法分开给你展示。 可视化代码你也可以拿去随意使用。
在论文里,应该经常看到上面这种图,我们看到最后一层layer3, 这个就是decoder在结合encoder信息后的attention,生成的预测结果。我们很明显可以看到中英文日期对应的点上, 注意力都非常大。
换一种角度来看,我们再用连线可以更加明显的看出来这样的关系。
下面我们再来看看decoder,encoder各自的self-attention.
Encoder 的 self-attention 看不出来太多信息,因为我们这个数据集在自注意上并不是很强调,X的语句内部没有多少相关的信息。所以训练出来的encoder self-attention 并不明显。
反而是decoder的self-attention还是有些信息的。因为decoder在做self-attention时, 实际上还是会多多少少接收到encoder attention的影响。因为encoder的attention信息被传输过来了。
还有一个有意思的点,不知道你们发现没,decoder的attention图,为什么是一个三角形? 原因在我在上面提到的,预测时,不能让后文的信息影响到前文,就会用一个look_ahead_mask
将后文的信息给遮盖住。这个mask长成这样:
那问题又来了,为什么这个mask不是一个对角的三角形呢?原因是有些句子没那么长,也可以一起mask掉,我把这种叫做 pad_mask
,像下图这样。
最后,还有一个问题,transformer的attention不像RNN,它没有捕捉到文字序列上的时序信息。那我们怎么让模型知道一句话的顺序呢? 这个有多种做法,比如让模型仔细学一个position embedding,或者你给一个有规律的position embedding就好了。positional embedding 的代码你就直接在我github上搜PositionEmbedding
吧。 下面展示的是可视化出来的Position Embedding:
总结¶
好了,这就差不多了,总算把这么复杂的transformer给肢解
了,费了老大力。理解了transformer,知道了模型是怎么玩注意力的,后面的当前很流行的 GPT,Bert就能迎刃而解~