3W 字长文带你轻松入门视觉 transformer

date
Apr 7, 2022
Last edited time
Mar 27, 2023 09:03 AM
status
Published
slug
3W字长文带你轻松入门视觉transformer
tags
DL
CV
summary
type
Post
Field
Plat

0 摘要

transformer 结构是 google 在 17 年的 Attention Is All You Need 论文中提出,在 NLP 的多个任务上取得了非常好的效果,可以说目前 NLP 发展都离不开 transformer。最大特点是抛弃了传统的 CNN 和 RNN,整个网络结构完全是由 Attention 机制组成。 由于其出色性能以及对下游任务的友好性或者说下游任务仅仅微调即可得到不错效果,在计算机视觉领域不断有人尝试将 transformer 引入,近期也出现了一些效果不错的尝试,典型的如目标检测领域的 detr 和可变形 detr,分类领域的 vision transformer 等等。 本文从 transformer 结构出发,结合视觉中的 transformer 成果 (具体是 vision transformer 和 detr) 进行分析,希望能够帮助 cv 领域想了解 transformer 的初学者快速入门。由于本人接触 transformer 时间也不长,也算初学者,故如果有描述或者理解错误的地方欢迎指正。
本文的大部分图来自论文、国外博客和国内翻译博客,在此一并感谢前人工作,具体链接见参考资料。本文特别长,大概有 3w 字,请先点赞收藏然后慢慢看….

1 transformer 介绍

一般讲解 transformer 都会以机器翻译任务为例子讲解,机器翻译任务是指将一种语言转换得到另一种语言,例如英语翻译为中文任务。从最上层来看,如下所示:
notion image

1.1 早期 seq2seq

机器翻译是一个历史悠久的问题,本质可以理解为序列转序列问题,也就是我们常说的 seq2seq 结构,也可以称为 encoder-decoder 结构,如下所示:
notion image
encoder 和 decoder 在早期一般是 RNN 模块 (因为其可以捕获时序信息),后来引入了 LSTM 或者 GRU 模块,不管内部组件是啥,其核心思想都是通过 Encoder 编码成一个表示向量,即上下文编码向量,然后交给 Decoder 来进行解码,翻译成目标语言。一个采用典型 RNN 进行编码码翻译的可视化图如下:
notion image
可以看出,其解码过程是顺序进行,每次仅解码出一个单词。对于 CV 领域初学者来说,RNN 模块构建的 seq2seq 算法,理解到这个程度就可以了,不需要深入探讨如何进行训练。但是上述结构其实有缺陷,具体来说是:
  • 不论输入和输出的语句长度是什么,中间的上下文向量长度都是固定的,一旦长度过长,仅仅靠一个固定长度的上下文向量明显不合理
  • 仅仅利用上下文向量解码,会有信息瓶颈,长度过长时候信息可能会丢失
通俗理解是编码器与解码器的连接点仅仅是编码单元输出的隐含向量,其包含的信息有限,对于一些复杂任务可能信息不够,如要翻译的句子较长时,一个上下文向量可能存不下那么多信息,就会造成翻译精度的下降。

1.2 基于 attention 的 seq2seq

基于上述缺陷进而提出带有注意力机制 Attention 的 seq2seq,同样可以应用于 RNN、LSTM 或者 GRU 模块中。注意力机制 Attention 对人类来说非常好理解,假设给定一张图片,我们会自动聚焦到一些关键信息位置,而不需要逐行扫描全图。此处的 attention 也是同一个意思,其本质是对输入的自适应加权,结合 cv 领域的 senet 中的 se 模块就能够理解了。
notion image
se 模块最终是学习出一个 1x1xc 的向量,然后逐通道乘以原始输入,从而对特征图的每个通道进行加权即通道注意力,对 attention 进行抽象,不管啥领域其机制都可以归纳为下图:
notion image
将 Query(通常是向量) 和 4 个 Key(和 Q 长度相同的向量) 分别计算相似性,然后经过 softmax 得到 q 和 4 个 key 相似性的概率权重分布,然后对应权重乘以 Value(和 Q 长度相同的向量),最后相加即可得到包含注意力的 attention 值输出,理解上应该不难。 举个简单例子说明:
  • 假设世界上所有小吃都可以被标签化,例如微辣、特辣、变态辣、微甜、有嚼劲….,总共有 1000 个标签,现在我想要吃的小吃是 [微辣、微甜、有嚼劲],这三个单词就是我的 Query
  • 来到东门老街一共 100 家小吃店,每个店铺卖的东西不一样,但是肯定可以被标签化,例如第一家小吃被标签化后是 [微辣、微咸], 第二家小吃被标签化后是 [特辣、微臭、特咸],第三家小吃被标签化后是 [特辣、微甜、特咸、有嚼劲],其余店铺都可以被标签化,每个店铺的标签就是 Keys, 但是每家店铺由于卖的东西不一样,单品种类也不一样,所以被标签化后每一家的标签 List 不一样长
  • Values 就是每家店铺对应的单品,例如第一家小吃的 Values 是 [烤羊肉串、炒花生]
  • 将 Query 和所有的 Keys 进行一一比对,相当于计算相似性,此时就可以知道我想买的小吃和每一家店铺的匹配情况,最后有了匹配列表,就可以去店铺里面买东西了 (Values 和相似性加权求和)。最终的情况可能是,我在第一家店铺买了烤羊肉串,然后在第 10 家店铺买了个玉米,最后在第 15 家店铺买了个烤面筋
以上就是完整的注意力机制,采用我心中的标准 Query 去和被标签化的所有店铺 Keys 一一比对,此时就可以得到我的 Query 在每个店铺中的匹配情况,最终去不同店铺买不同东西的过程就是权重和 Values 加权求和过程。简要代码如下:
带有 attention 的 RNN 模块组成的 ser2seq, 解码时候可视化如下:
notion image
在没有 attention 时候,不同解码阶段都仅仅利用了同一个编码层的最后一个隐含输出,加入 attention 后可以通过在每个解码时间步输入的都是不同的上下文向量,以上图为例,解码阶段会将第一个开启解码标志 (也就是 Q)与编码器的每一个时间步的隐含状态 (一系列 Key 和 Value) 进行点乘计算相似性得到每一时间步的相似性分数,然后通过 softmax 转化为概率分布,然后将概率分布和对应位置向量进行加权求和得到新的上下文向量,最后输入解码器中进行解码输出,其详细解码可视化如下:
notion image
通过上述简单的 attention 引入,可以将机器翻译性能大幅提升,引入 attention 有以下几个好处:
  • 注意力显著提高了机器翻译性能
  • 注意力允许解码器以不同程度的权重利用到编码器的所有信息,可以绕过瓶颈
  • 通过检查注意力分布,可以看到解码器在关注什么,可解释性强

1.3 基于 transformer 的 seq2seq

基于 attention 的 seq2seq 的结构虽然说解决了很多问题,但是其依然存在不足:
  • 不管是采用 RNN、LSTM 还是 GRU 都不利于并行训练和推理,因为相关算法只能从左向右依次计算或者从右向左依次计算
  • 长依赖信息丢失问题,顺序计算过程中信息会丢失,虽然 LSTM 号称有缓解,但是无法彻底解决
最大问题应该是无法并行训练,不利于大规模快速训练和部署,也不利于整个算法领域发展,故在 Attention Is All You Need 论文中抛弃了传统的 CNN 和 RNN,将 attention 机制发挥到底,整个网络结构完全是由 Attention 机制组成,这是一个比较大的进步。
google 所提基于 transformer 的 seq2seq 整体结构如下所示:
notion image
其包括 6 个结构完全相同的编码器,和 6 个结构完全相同的解码器,其中每个编码器和解码器设计思想完全相同,只不过由于任务不同而有些许区别,整体详细结构如下所示:
notion image
第一眼看有点复杂,其中 N=6,由于基于 transformer 的翻译任务已经转化为分类任务 (目标翻译句子有多长,那么就有多少个分类样本),故在解码器最后会引入 fc+softmax 层进行概率输出,训练也比较简单,直接采用 ce loss 即可,对于采用大量数据训练好的预训练模型,下游任务仅仅需要训练 fc 层即可。上述结构看起来有点复杂,一个稍微抽象点的图示如下:
notion image
看起来比基于 RNN 或者其余结构构建的 seq2seq 简单很多。下面结合代码和原理进行深入分析。

1.4 transformer 深入分析

前面写了一大堆,没有理解没有关系,对于 cv 初学者来说其实只需要理解 QKV 的含义和注意力机制的三个计算步骤: Q 和所有 K 计算相似性;对相似性采用 softmax 转化为概率分布;将概率分布和 V 进行一一对应相乘,最后相加得到新的和 Q 一样长的向量输出即可,重点是下面要讲的 transformer 结构。
下面按照 编码器输入数据处理 -> 编码器运行 -> 解码器输入数据处理 -> 解码器运行 -> 分类 head 的实际运行流程进行讲解。

1.4.1 编码器输入数据处理

(1) 源单词嵌入
以上面翻译任务为例,原始待翻译输入是三个单词:
notion image
输入是三个单词,为了能够将文本内容输入到网络中肯定需要进行向量化 (不然单词如何计算?),具体是采用 nlp 领域的 embedding 算法进行词嵌入,也就是常说的 Word2Vec。对于 cv 来说知道是干嘛的就行,不必了解细节。假设每个单词都可以嵌入成 512 个长度的向量,故此时输入即为 3x512,注意 Word2Vec 操作只会输入到第一个编码器中,后面的编码器接受的输入是前一个编码器输出。
为了便于组成 batch(不同训练句子单词个数肯定不一样) 进行训练,可以简单统计所有训练句子的单词个数,取最大即可,假设统计后发现待翻译句子最长是 10 个单词,那么编码器输入是 10x512,额外填充的 512 维向量可以采用固定的标志编码得到,例如 $$。
(2) 位置编码 positional encoding
采用经过单词嵌入后的向量输入到编码器中还不够,因为 transformer 内部没有类似 RNN 的循环结构,没有捕捉顺序序列的能力,或者说无论句子结构怎么打乱,transformer 都会得到类似的结果。为了解决这个问题,在编码词向量时会额外引入了位置编码 position encoding 向量表示两个单词 i 和 j 之间的距离,简单来说就是在词向量中加入了单词的位置信息
加入位置信息的方式非常多,最简单的可以是直接将绝对坐标 0,1,2 编码成 512 个长度向量即可。作者实际上提出了两种方式:
  • 网络自动学习
  • 自己定义规则
提前假设单词嵌入并且组成 batch 后,shape 为 (b,N,512),N 是序列最大长度,512 是每个单词的嵌入向量长度, b 是 batch
(a) 网络自动学习
比较简单,因为位置编码向量需要和输入嵌入 (b,N,512) 相加,所以其 shape 为 (1,N,512) 表示 N 个位置,每个位置采用 512 长度向量进行编码
(b) 自己定义规则
自定义规则做法非常多,论文中采用的是 sin-cos 规则,具体做法是:
  • 将向量 采用如下函数进行处理
pos 即 , i 是
  • 将向量的 512 维度切分为奇数行和偶数行
  • 偶数行采用 sin 函数编码,奇数行采用 cos 函数编码
  • 然后按照原始行号拼接
上面例子的可视化如下:
notion image
如此编码的优点是能够扩展到未知的序列长度,例如前向时候有特别长的句子,其可视化如下:
notion image
作者为啥要设计如此复杂的编码规则?原因是 sin 和 cos 的如下特性:
可以将 进行线性表出:
假设 ,那么下一个位置的编码向量可以由前面的编码向量线性表示,等价于以一种非常容易学会的方式告诉了网络单词之间的绝对位置,让模型能够轻松学习到相对位置信息。 注意编码方式不是唯一的,将单词嵌入向量和位置编码向量相加就可以得到编码器的真正输入了,其输出 shape 是

1.4.2 编码器前向过程

编码器由两部分组成:自注意力层和前馈神经网络层。
notion image
其前向可视化如下:
notion image
注意上图没有绘制出单词嵌入向量和位置编码向量相加过程,但是是存在的。
(1) 自注意力层
通过前面分析我们知道自注意力层其实就是 attention 操作,并且由于其 QKV 来自同一个输入,故称为自注意力层。我想大家应该能想到这里 attention 层作用,在参考资料 1 博客里面举了个简单例子来说明 attention 的作用:假设我们想要翻译的输入句子为 The animal didn’t cross the street because it was too tired,这个 “it” 在这个句子是指什么呢?它指的是 street 还是这个 animal 呢?这对于人类来说是一个简单的问题,但是对于算法则不是。当模型处理这个单词 “it” 的时候,自注意力机制会允许 “it” 与“animal”建立联系即随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。 实际上训练完成后确实如此,google 提供了可视化工具,如下所示:
notion image
上述是从宏观角度思考,如果从输入输出流角度思考,也比较容易:
notion image
假设我们现在要翻译上述两个单词,首先将单词进行编码,和位置编码向量相加,得到自注意力层输入 , 其 shape 为 ;然后定义三个可学习矩阵 (通过 nn.Linear 实现),其 shape 为 ,一般 等于前面维度 ,从而计算后维度不变;将 和矩阵 相乘,得到 输出,shape 为 ;然后将 进行点乘计算向量相似性,结果的shape为 ;采用 softmax 转换为概率分布;将概率分布和 进行加权求和即可, 结果的shape为
实际上代码层面采用矩阵实现非常简单:
notion image
上面的操作很不错,但是还有改进空间,论文中又增加一种叫做 “多头” 注意力(“multi-headed” attention)的机制进一步完善了自注意力层,并在两方面提高了注意力层的性能:
  • 它扩展了模型专注于不同位置的能力。在上面的例子中,虽然每个编码都在 z1 中有或多或少的体现,但是它可能被实际的单词本身所支配。如果我们翻译一个句子,比如 “The animal didn’t cross the street because it was too tired”,我们会想知道“it” 指的是哪个词,这时模型的 “多头” 注意机制会起到作用。
  • 它给出了注意力层的多个 “表示子空间”, 对于“多头” 注意机制,有多个查询 / 键 / 值权重矩阵集(Transformer 使用 8 个注意力头,因此我们对于每个编码器 / 解码器有 8 个矩阵集合)。
notion image
简单来说就是类似于分组操作,将输入 X 分别输入到 8 个 attention 层中,得到 8 个 Z 矩阵输出,最后对结果 concat 即可。论文图示如下:
notion image
先忽略 Mask 的作用,左边是单头 attention 操作,右边是 n 个单头 attention 构成的多头自注意力层。
代码层面非常简单,单头 attention 操作如下:
再次复习下 Multi-Head Attention 层的图示,可以发现在前面讲的内容基础上还加入了残差设计和层归一化操作,目的是为了防止梯度消失,加快收敛。
notion image
Multi-Head Attention 实现在 ScaledDotProductAttention 基础上构建:
现在 pytorch 新版本已经把 MultiHeadAttention 当做 nn 中的一个类了,可以直接调用。
(2) 前馈神经网络层
这个层就没啥说的了,非常简单:
(3) 编码层操作整体流程
可视化如下所示:
notion image
单个编码层代码如下所示:
将上述编码过程重复 n 遍即可,除了第一个模块输入是单词嵌入向量与位置编码的和外,其余编码层输入是上一个编码器输出即后面的编码器输入不需要位置编码向量。如果考虑 n 个编码器的运行过程,如下所示:
到目前为止我们就讲完了编码部分的全部流程和代码细节。现在再来看整个 transformer 算法就会感觉亲切很多了:
notion image

1.4.3 解码器输入数据处理

在分析解码器结构前先看下解码器整体结构,方便理解:
notion image
其输入数据处理也要区分第一个解码器和后续解码器,和编码器类似,第一个解码器输入不仅包括最后一个编码器输出,还需要额外的输出嵌入向量,而后续解码器输入是来自最后一个编码器输出和前面解码器输出。
(1) 目标单词嵌入
这个操作和源单词嵌入过程完全相同,维度也是 512,假设输出是 i am a student,那么需要对这 4 个单词也利用 word2vec 算法转化为 4x512 的矩阵,作为第一个解码器的单词嵌入输入。
(2) 位置编码
同样的也需要对解码器输入引入位置编码,做法和编码器部分完全相同,且将目标单词嵌入向量和位置编码向量相加即可作为第一个解码器输入。
和编码器单词嵌入不同的地方是在进行目标单词嵌入前,还需要将目标单词即是 i am a student 右移动一位,新增加的一个位置采用提前定义好的标志位 BOS_WORD 代替,现在就变成 [BOS_WORD,i,am,a,student],为啥要右移?因为解码过程和 seq2seq 一样是顺序解码的,需要提供一个开始解码标志,。不然第一个时间步的解码单词 i 是如何输出的呢?具体解码过程其实是:输入 BOS_WORD,解码器输出 i;输入前面已经解码的 BOS_WORD 和 i,解码器输出 am…,输入已经解码的 BOS_WORD、i、am、a 和 student,解码器输出解码结束标志位 EOS_WORD, 每次解码都会利用前面已经解码输出的所有单词嵌入信息
下面有个非常清晰的 gif 图,一目了然:
notion image
上图没有绘制 BOS_WORD 嵌入向量输入,然后解码出 i 单词的过程。

1.4.4 解码器前向过程

仔细观察解码器结构,其包括:带有 mask 的 MultiHeadAttention、MultiHeadAttention 和前馈神经网络层三个组件,带有 mask 的 MultiHeadAttention 和 MultiHeadAttention 结构和代码写法是完全相同,唯一区别是是否输入了 mask。
为啥要 mask?原因依然是顺序解码导致的。试想模型训练好了,开始进行翻译 (测试),其流程就是上面写的:输入 BOS_WORD,解码器输出 i;输入前面已经解码的 BOS_WORD 和 i,解码器输出 am…,输入已经解码的 BOS_WORD、i、am、a 和 student,解码器输出解码结束标志位 EOS_WORD, 每次解码都会利用前面已经解码输出的所有单词嵌入信息,这个测试过程是没有问题,但是训练时候我肯定不想采用上述顺序解码类似 rnn 即一个一个目标单词嵌入向量顺序输入训练,肯定想采用类似编码器中的矩阵并行算法,一步就把所有目标单词预测出来。要实现这个功能就可以参考编码器的操作,把目标单词嵌入向量组成矩阵一次输入即可,但是在解码 am 时候,不能利用到后面单词 a 和 student 的目标单词嵌入向量信息,否则这就是作弊 (测试时候不可能能未卜先知)。为此引入 mask,目的是构成下三角矩阵,右上角全部设置为负无穷 (相当于忽略),从而实现当解码第一个字的时候,第一个字只能与第一个字计算相关性,当解出第二个字的时候,只能计算出第二个字与第一个字和第二个字的相关性。具体是:在解码器中,自注意力层只被允许处理输出序列中更靠前的那些位置,在 softmax 步骤前,它会把后面的位置给隐去(把它们设为 - inf)。
还有个非常重要点需要知道 (看图示可以发现):解码器内部的带有 mask 的 MultiHeadAttention 的 qkv 向量输入来自目标单词嵌入或者前一个解码器输出,三者是相同的,但是后面的 MultiHeadAttention 的 qkv 向量中的 kv 来自最后一层编码器的输入,而 q 来自带有 mask 的 MultiHeadAttention 模块的输出。
关于带 mask 的注意力层写法其实就是前面提到的代码:
可视化如下:图片来源 https://zhuanlan.zhihu.com/p/44731789
notion image
整个解码器代码和编码器非常类似:
考虑 n 个解码器模块,其整体流程为:

1.4.5 分类层

在进行编码器 - 解码器后输出依然是向量,需要在后面接 fc+softmax 层进行分类训练。假设当前训练过程是翻译任务需要输出 i am a student EOS_WORD 这 5 个单词。假设我们的模型是从训练集中学习一万个不同的英语单词(我们模型的 “输出词表”)。因此 softmax 后输出为一万个单元格长度的向量,每个单元格对应某一个单词的分数,这其实就是普通多分类问题,只不过维度比较大而已。
依然以前面例子为例,假设编码器输出 shape 是 (b,100,512),经过 fc 后变成 (b,100,10000),然后对最后一个维度进行 softmax 操作,得到 bx100 个单词的概率分布,在训练过程中 bx100 个单词是知道 label 的,故可以直接采用 ce loss 进行训练。

1.4.6 前向流程

以翻译任务为例:
  • 将源单词进行嵌入,组成矩阵 (加上位置编码矩阵) 输入到 个编码器中,输出编码向量
  • 第一个解码器先输入一个 BOS_WORD 单词嵌入向量,后续解码器接受该解码器输出,结合 进行第一次解码
  • 将第一次解码单词进行嵌入,联合 BOS_WORD 单词嵌入向量构成矩阵再次输入到解码器中进行第二次解码,得到解码单词
  • 不断循环,每次的第一个解码器输入都不同,其包含了前面时间步长解码出的所有单词
  • 直到输出 EOS_WORD 表示解码结束或者强制设置最大时间步长即可
详细说明下循环解码过程:第一次解码,输入 BOS_WORD 单词嵌入向量,假设是 ,而编码器输出始终不变是 ,那么第一次解码过程是+ 位置编码作为解码器输入,解码输出是,经过 fc 层(参数 shape 是(256,10000)) 变成 (1,10000),10000 是单词总数,此时就可以解码得到第一个单词 i;接着将 BOS_WORD 和 i 都进行嵌入,得到(2,256) 输入,同样运行,输出是 (2,256),经过 fc 是(2,10000),此时不需要第一个维度输出只需要[-1,10000] 既可以解码第二个单词,后面就一直迭代直到输出结束解码标注。
这个解码过程其实就是标准的 seq2seq 流程。到目前为止就描述完了整个标准 transformer 训练和测试流程。

2 视觉领域的 transformer

在理解了标准的 transformer 后,再来看视觉领域 transformer 就会非常简单,因为在 cv 领域应用 transformer 时候大家都有一个共识:尽量不改动 transformer 结构,这样才能和 NLP 领域发展对齐,所以大家理解 cv 里面的 transformer 操作是非常简单的。

2.1 分类 vision transformer

论文题目:An Image is Worth 16x16 Words:Transformers for Image Recognition at Scale
其做法超级简单,只含有编码器模块:
notion image
本文出发点是彻底抛弃 CNN,以前的 cv 领域虽然引入 transformer,但是或多或少都用到了 cnn 或者 rnn,本文就比较纯粹了,整个算法几句话就说清楚了,下面直接分析。

2.1.1 图片分块和降维

因为 transformer 的输入需要序列,所以最简单做法就是把图片切分为 patch,然后拉成序列即可。 假设输入图片大小是 256x256,打算分成 64 个 patch,每个 patch 是 32x32 像素
这个写法是采用了爱因斯坦表达式,具体是采用了 einops 库实现,内部集成了各种算子,rearrange 就是其中一个,非常高效。不懂这种语法的请自行百度。p 就是 patch 大小,假设输入是 b,3,256,256,则 rearrange 操作是先变成 (b,3,8x32,8x32),最后变成(b,8x8,32x32x3) 即(b,64,3072),将每张图片切分成 64 个小块,每个小块长度是 32x32x3=3072,也就是说输入长度为 64 的图像序列,每个元素采用 3072 长度进行编码。
考虑到 3072 有点大,故作者先进行降维:
仔细看论文上图,可以发现假设切成 9 个块,但是最终到 transfomer 输入是 10 个向量,额外追加了一个 0 和_。为啥要追加?原因是我们现在没有解码器了,而是编码后直接就进行分类预测,那么该编码器就要负责一点点解码器功能,那就是:需要一个类似开启解码标志,非常类似于标准 transformer 解码器中输入的目标嵌入向量右移一位操作。试下如果没有额外输入,9 个块输入 9 个编码向量输出,那么对于分类任务而言,我应该取哪个输出向量进行后续分类呢?选择任何一个都说不通,所以作者追加了一个可学习嵌入向量输入。那么额外的可学习嵌入向量为啥要设计为可学习,而不是类似 nlp 中采用固定的 token 代替?个人不负责任的猜测这应该就是图片领域和 nlp 领域的差别,nlp 里面每个词其实都有具体含义,是离散的,但是图像领域没有这种真正意义上的离散 token,有的只是一堆连续特征或者图像像素,如果不设置为可学习,那还真不知道应该设置为啥内容比较合适,全 0 和全 1 也说不通。 自此现在就是变成 10 个向量输出,输出也是 10 个编码向量,然后取第 0 个编码输出进行分类预测即可。从这个角度看可以认为编码器多了一点点解码器功能。具体做法超级简单,0 就是位置编码向量,_是可学习的 patch 嵌入向量。

2.1.2 位置编码

位置编码也是必不可少的,长度应该是 1024,这里做的比较简单,没有采用 sincos 编码,而是直接设置为可学习,效果差不多
对训练好的 pos_embedding 进行可视化,如下所示:
notion image
相邻位置有相近的位置编码向量,整体呈现 2d 空间位置排布一样。
将 patch 嵌入向量和位置编码向量相加即可作为编码器输入

2.1.3 编码器前向过程

作者采用的是没有任何改动的 transformer,故没有啥说的。
假设输入是 ,那么 transformer 输出也是

2.1.4 分类 head

在编码器后接 fc 分类器 head 即可
到目前为止就全部写完了,是不是非常简单,外层整体流程为:

2.1.5 实验分析

作者得出的结论是:cv 领域应用 transformer 需要大量数据进行预训练,在同等数据量的情况下性能不然 cnn。一旦数据量上来了,对应的训练时间也会加长很多,那么就可以轻松超越 cnn。
notion image
同时应用 transformer,一个突出优点是可解释性比较强:
notion image

2.2 目标检测 detr

论文名称:End-to-End Object Detection with Transformers
detr 是 facebook 提出的引入 transformer 到目标检测领域的算法,效果很好,做法也很简单,符合其一贯的简洁优雅设计做法。
notion image
对于目标检测任务,其要求输出给定图片中所有前景物体的类别和 bbox 坐标,该任务实际上是无序集合预测问题。针对该问题,detr 做法非常简单:给定一张图片,经过 CNN 进行特征提取,然后变成特征序列输入到 transformer 的编解码器中,直接输出指定长度为 N 的无序集合,集合中每个元素包含物体类别和坐标。其中 N 表示整个数据集中图片上最多物体的数目,因为整个训练和测试都 Batch 进行,如果不设置最大输出集合数,无法进行 batch 训练,如果图片中物体不够 N 个,那么就采用 no object 填充,表示该元素是背景。
整个思想看起来非常简单,相比 faster rcnn 或者 yolo 算法那就简单太多了,因为其不需要设置先验 anchor,超参几乎没有,也不需要 nms(因为输出的无序集合没有重复情况),并且在代码程度相比 faster rcnn 那就不知道简单多少倍了,通过简单修改就可以应用于全景分割任务。可以推测,如果 transformer 真正大规模应用于 CV 领域,那么对初学者来说就是福音了,理解 transformer 就几乎等于理解了整个 cv 领域了 (当然也可能是坏事)。

2.2.1 detr 核心思想分析

相比 faster rcnn 等做法,detr 最大特点是将目标检测问题转化为无序集合预测问题。论文中特意指出 faster rcnn 这种设置一大堆 anchor,然后基于 anchor 进行分类和回归其实属于代理做法即不是最直接做法,目标检测任务就是输出无序集合,而 faster rcnn 等算法通过各种操作,并结合复杂后处理最终才得到无序集合属于绕路了,而 detr 就比较纯粹了。
尽管将 transformer 引入目标检测领域可以避免上述各种问题,但是其依然存在两个核心操作:
  • 无序集合输出的 loss 计算
  • 针对目标检测的 transformer 改进

2.2.2 detr 算法实现细节

下面结合代码和原理对其核心环节进行深入分析。

2.2.2.1 无序集合输出的 loss 计算

在分析 loss 计算前,需要先明确 N 个无序集合的 target 构建方式。作者在 coco 数据集上统计,一张图片最多标注了 63 个物体,所以 N 应该要不小于 63,作者设置的是 100。为啥要设置为 100?有人猜测是和 coco 评估指标只取前 100 个预测结果算法指标有关系。
detr 输出是包括 batchx100 个无序集合,每个集合包括类别和坐标信息。对于 coco 数据而言,作者设置类别为 91(coco 类别标注索引是 1-91, 但是实际就标注了 80 个类别),加上背景一共 92 个类别,对于坐标分支采用 4 个归一化值表征即 cxcywh 中心点、wh 坐标,然后除以图片宽高进行归一化 (没有采用复杂变换策略),故每个集合是 ,c 是长度为 92 的分类向量,b 是长度为 4 的 bbox 坐标向量。总之 detr 输出集合包括两个分支:分类分支 shape=(b,100,92),bbox 坐标分支 shape=(b,100,4),对应的 target 也是包括分类 target 和 bbox 坐标 target,如果不够 100,则采用背景填充,计算 loss 时候 bbox 分支仅仅计算有物体位置,背景集合忽略。
现在核心问题来了:输出的 bx100 个检测结果是无序的,如何和 gt bbox 计算 loss?这就需要用到经典的双边匹配算法了,也就是常说的匈牙利算法,该算法广泛应用于最优分配问题,在 bottom-up 人体姿态估计算法中进行分组操作时候也经常使用。detr 中利用匈牙利算法先进行最优一对一匹配得到匹配索引,然后对 bx100 个结果进行重排就和 gt bbox 对应上了 (对 gt bbox 进行重排也可以,没啥区别),就可以算 loss 了
匈牙利算法是一个标准优化算法,具体是组合优化算法,在 scipy.optimize.linear_sum_assignmen 函数中有实现,一行代码就可以得到最优匹配,网上解读也非常多,这里就不写细节了,该函数核心是需要输入 A 集合和 B 集合两两元素之间的连接权重,基于该重要性进行内部最优匹配,连接权重大的优先匹配
上述描述优化过程可以采用如下公式表达:
优化对象是 ,其是长度为 N 的 list,表示无序 gt bbox 集合的哪个元素和输出预测集合中的第 i 个匹配。其实简单来说就是找到最优匹配,因为在最佳匹配情况下 和最小即 loss 最小。
前面说过匈牙利算法核心是需要提供输入 A 集合和 B 集合两两元素之间的连接权重,这里就是要输入 N 个输出集合和 M 个 gt bbox 之间的关联程度,如下所示
而 Lbox 具体是:
Hungarian 意思就是匈牙利,也就是前面的 L_match,上述意思是需要计算 M 个 gt bbox 和 N 个输出集合两两之间的广义距离,距离越近表示越可能是最优匹配关系,也就是两者最密切。广义距离的计算考虑了分类分支和 bbox 分支,下面结合代码直接说明,比较简单。
在得到匹配关系后算 loss 就水到渠成了。分类分支计算 ce loss,bbox 分支计算 l1 loss+giou loss

2.2.2.2 针对目标检测的 transformer 改进

分析完训练最关键的:双边匹配 + loss 计算部分,现在需要考虑在目标检测算法中 transformer 如何设计?下面按照算法的 4 个步骤讲解。
transformer 细节如下:
notion image
(1) cnn 骨架特征提取
骨架网络可以是任何一种,作者选择 resnet50,将最后一个 stage 即 stride=32 的特征图作为编码器输入。由于 resnet 仅仅作为一个小部分且已经经过了 imagenet 预训练,故和常规操作一样,会进行如下操作:
  • resnet 中所有 BN 都固定,即采用全局均值和方差
  • resnet 的 stem 和第一个 stage 不进行参数更新,即 parameter.requires_grad_(False)
  • backbone 的学习率小于 transformer,lr_backbone=1e-05, 其余为 0.0001
假设输入是 ,则 resnet50 输出是 ,2048 比较大,为了节省计算量,先采用 卷积降维为 256, 最后转化为序列格式输入到 transformer 中,输入
(2) 编码器设计和输入
编码器结构设计没有任何改变,但是输入改变了。
a) 位置编码需要考虑 2d 空间
由于图像特征是 2d 特征,故位置嵌入向量也需要考虑 方向。前面说过编码方式可以采用 sincos,也可以设置为可学习,本文采用的依然是 sincos 模式,和前面说的一样,但是需要考虑 xy 两个方向 (前面说的序列只有 x 方向)。
可以看出对于 的 2d 图像特征,不是类似 vision transoformer 做法简单的将其拉伸为 ,然后从 进行长度为 256 的位置编码,而是考虑了 方向同时编码,每个方向各编码 维向量,这种编码方式更符合图像特定。
还有一个细节需要注意:原始 transformer 的 n 个编码器输入中,只有第一个编码器需要输入位置编码向量,但是 detr 里面对每个编码器都输入了同一个位置编码向量,论文中没有写为啥要如此修改。
b) QKV 处理逻辑不同
作者设置编码器一共 6 个,并且位置编码向量仅仅加到 QK 中,V 中没有加入位置信息,这个和原始做法不一样,原始做法是 QKV 都加上了位置编码,论文中也没有写为啥要如此修改。
其余地方就完全相同了,故代码就没必要贴了。总结下和原始 transformer 编码器不同的地方:
  • 输入编码器的位置编码需要考虑 2d 空间位置
  • 位置编码向量需要加入到每个编码器中
  • 在编码器内部位置编码仅仅和 QK 相加,V 不做任何处理
经过 6 个编码器 forward 后,输出 shape 为
c) 编码器部分整体运行流程
6 个编码器整体 forward 流程如下:
每个编码器内部运行流程如下:
(3) 解码器设计和输入
解码器结构设计没有任何改变,但是输入也改变了。
a) 新引入 Object queries
object queries(shape 是 (100,256)) 可以简单认为是输出位置编码, 其作用主要是在学习过程中提供目标对象和全局图像之间的关系, 相当于全局注意力,必不可少非常关键。代码形式上是可学习位置编码矩阵。和编码器一样,该可学习位置编码向量也会输入到每一个解码器中。我们可以尝试通俗理解:object queries 矩阵内部通过学习建模了 100 个物体之间的全局关系,例如房间里面的桌子旁边 (A 类) 一般是放椅子(B 类),而不会是放一头大象(C 类),那么在推理时候就可以利用该全局注意力更好的进行解码预测输出。
论文中指出 object queries 作用非常类似 faster rcnn 中的 anchor,只不过这里是可学习的,不是提前设置好的。
补充一个 object queries 通俗理解:假设其维度是 (100,256),在训练过程中每个格子(共 N 个) 的向量都会包括整个训练集相关的位置和类别信息,例如第 0 个格子里面存储的一定是某个空间位置的大象类别的嵌入向量,注意该大象类别嵌入向量和某一张图片的大象特征无关,而是通过训练考虑了所有图片的某个位置附近的大象编码特征,属于和位置有关的全局大象统计信息。训练完成后每个格子里面都会压缩入所有类别的图片位置相关的统计信息。现在开始测试:假设图片中有大象、狗和猫三种物体,该图片会输入到编码器中进行特征编码,假设特征没有丢失,该编码器输出的编码向量就是 KV,而 object queries 是 Q,现在通过注意力模块将 Q 和 K 计算,然后加权 V 得到解码器输出。对于第 0 个格子的 q 会和 K 中的所有向量进行计算,目的是查找某个位置附近有没有大象,如果有那么该特征就会加权输出,整个过程计算完成后就可以把编码向量中的大象、狗和猫的编码嵌入信息提取出来,然后后面接 fc 进行分类和回归就比较容易,因为特征已经对齐了。
在整个分析过程中可以总结下:object queries 在训练过程中对于 N 个格子会压缩入对应的和位置和类别相关的统计信息,在测试阶段就可以利用该 Q 去和编码特征 KV 计算加权计算,从而提出想要的对齐的特征,最后进行分类和回归。所以前面才会说 object queries 作用非常类似 faster rcnn 中的 anchor,这个 anchor 是可学习的,由于维度比较高,故可以表征的东西丰富,当然维度越高,训练时长就会越长。
b) 位置编码也需要
编码器环节采用的 sincos 位置编码向量也可以考虑引入,且该位置编码向量输入到每个解码器的第二个 Multi-Head Attention 中,后面有是否需要该位置编码的对比实验。
c) QKV 处理逻辑不同
解码器一共包括 6 个,和编码器中 QKV 一样,V 不会加入位置编码。上述说的三个操作,只要看下网络结构图就一目了然了。
d) 一次解码输出全部无序集合
和原始 transformer 顺序解码操作不同的是,detr 一次就把 N 个无序框并行输出了 (因为任务是无序集合,做成顺序推理有序输出没有很大必要)。为了说明如何实现该功能,我们需要先回忆下原始 transformer 的顺序解码过程:输入 BOS_WORD,解码器输出 i;输入前面已经解码的 BOS_WORD 和 i,解码器输出 am…,输入已经解码的 BOS_WORD、i、am、a 和 student,解码器输出解码结束标志位 EOS_WORD, 每次解码都会利用前面已经解码输出的所有单词嵌入信息。现在就是一次解码,故只需要初始化时候输入一个全 0 的查询向量 A,类似于 BOS_WORD 作用,然后第一个解码器接受该输入 A,解码输出向量作为下一个解码器输入,不断推理即可,最后一层解码输出即为我们需要的输出,不需要在第二个解码器输入时候考虑 BOS_WORD 和第一个解码器输出。
总结下和原始 transformer 解码器不同的地方:
  • 额外引入可学习的 Object queries,相当于可学习 anchor,提供全局注意力
  • 编码器采用的 sincos 位置编码向量也需要输入解码器中,并且每个解码器都输入
  • QKV 处理逻辑不同
  • 不需要顺序解码,一次即可输出 N 个无序集合
e) 解码器整体运行流程
n 个解码器整体流程如下:
内部每个解码器运行流程为:
解码器最终输出 shape 是 (6,b,100,256),6 是指 6 个解码器的输出。
(4) 分类和回归 head
在解码器输出基础上构建分类和 bbox 回归 head 即可输出检测结果,比较简单:
作者实验发现,如果对解码器的每个输出都加入辅助的分类和回归 loss,可以提升性能,故作者除了对最后一个编码层的输出进行 Loss 监督外,还对其余 5 个编码器采用了同样的 loss 监督,只不过权重设置低一点而已。
(5) 整体推理流程
基于 transformer 的 detr 算法,作者特意强调其突出优点是部署代码不超过 50 行,简单至极。
notion image
当然上面是简化代码,和实际代码不一样。具体流程是:
  • 图片输入到 resnet50 中进行特征提取, 输出
  • 通过 卷积降维,变成
  • 利用 函数计算位置编码
  • 将图像特征和位置编码向量相加,作为编码器输入,输出编码后的向量, 不变
  • 初始化全 0 的 的输出嵌入向量,结合位置编码向量和 query_embed,进行解码输出,解码器输出 shape 为,后面的解码器接受该输出,然后再次结合置编码向量和 query_embed 进行输出,不断前向
  • 将最后一个解码器输出输入到分类和回归 head 中,得到 100 个无序集合
  • 对 100 个无序集合进行后处理,主要是提取前景类别和对应的 bbox 坐标,乘上 即可得到最终坐标, 后处理代码如下:
既然训练时候对 6 个解码器输出都进行了 loss 监督,那么在测试时候也可以考虑将 6 个解码器的分类和回归分支输出结果进行 nms 合并,稍微有点性能提升。

2.2.3 实验分析

(1) 性能对比
notion image
Faster RCNN-DC5 是指的 resnet 的最后一个 stage 采用空洞率 = stride 设置代替 stride,目的是在不进行下采样基础上扩大感受野,输出特征图分辨率保持不变。+ 号代表采用了额外的技巧提升性能例如 giou、多尺度训练和 9xepoch 训练策略。可以发现 detr 效果稍微好于 faster rcnn 各种版本,证明了视觉 transformer 的潜力。但是可以发现其小物体检测能力远远低于 faster rcnn,这是一个比较大的弊端。
(2) 各个模块分析
编码器数目越多效果越好,但是计算量也会增加很多,作者最终选择的是 6。
可以发现解码器也是越多越好,还可以观察到第一个解码器输出预测效果比较差,增加第二个解码器后性能提升非常多。上图中的 NMS 操作是指既然我们每个解码层都可以输入无序集合,那么将所有解码器无序集合全部保留,然后进行 nms 得到最终输出,可以发现性能稍微有提升,特别是 AP50。
作者对比了不同类型的位置编码效果,因为 query_embed(output pos) 是必不可少的,所以该列没有进行对比实验,始终都有,最后一行效果最好,所以作者采用的就是该方案,sine at attn 表示每个注意力层都加入了 sine 位置编码,相比仅仅在 input 增加位置编码效果更好。
(3) 注意力可视化
前面说过 transformer 具有很好的可解释性,故在训练完成后最终提出了几种可视化形式
a) bbox 输出可视化
这个就比较简单了,直接对预测进行后处理即可
b) 解码器交叉注意力层权重可视化
这里指的是最后一个解码器内部的第一个 MultiheadAttention 的交叉注意力权重 (因为 KV 来自编码器,Q 来自解码器,所以叫做交叉注意力),其实就是 QK 相似性计算后然后 softmax 后的输出可视化,具体是:
c) 编码器自注意力层权重可视化
这个和解码器操作完全相同。

2.2.4 小结

detr 整体做法非常简单,基本上没有改动原始 transformer 结构,其显著优点是:不需要设置啥先验,超参也比较少,训练和部署代码相比 faster rcnn 算法简单很多,理解上也比较简单。但是其缺点是:改了编解码器的输入,在论文中也没有解释为啥要如此设计,而且很多操作都是实验对比才确定的,比较迷。算法层面训练 epoch 次数远远大于 faster rcnn(300epoch),在同等 epoch 下明显性能不如 faster rcnn,而且训练占用内存也大于 faster rcnn
整体而言,虽然效果不错,但是整个做法还是显得比较原始,很多地方感觉是尝试后得到的做法,没有很好的解释性,而且最大问题是训练 epoch 非常大和内存占用比较多,对应的就是收敛慢,期待后续作品。

3 总结

本文从 transformer 发展历程入手,并且深入介绍了 transformer 思想和实现细节;最后结合计算机视觉领域的几篇有典型代表文章进行深入分析,希望能够给 cv 领域想快速理解 transformer 的初学者一点点帮助。

4 参考资料


© Lazurite 2021 - 2024