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

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 都会以机器翻译任务为例子讲解,机器翻译任务是指将一种语言转换得到另一种语言,例如英语翻译为中文任务。从最上层来看,如下所示:

Untitled

1.1 早期 seq2seq

机器翻译是一个历史悠久的问题,本质可以理解为序列转序列问题,也就是我们常说的 seq2seq 结构,也可以称为 encoder-decoder 结构,如下所示:

Untitled

encoder 和 decoder 在早期一般是 RNN 模块 (因为其可以捕获时序信息),后来引入了 LSTM 或者 GRU 模块,不管内部组件是啥,其核心思想都是通过 Encoder 编码成一个表示向量,即上下文编码向量,然后交给 Decoder 来进行解码,翻译成目标语言。一个采用典型 RNN 进行编码码翻译的可视化图如下:

Untitled

可以看出,其解码过程是顺序进行,每次仅解码出一个单词。对于 CV 领域初学者来说,RNN 模块构建的 seq2seq 算法,理解到这个程度就可以了,不需要深入探讨如何进行训练。但是上述结构其实有缺陷,具体来说是:

通俗理解是编码器与解码器的连接点仅仅是编码单元输出的隐含向量,其包含的信息有限,对于一些复杂任务可能信息不够,如要翻译的句子较长时,一个上下文向量可能存不下那么多信息,就会造成翻译精度的下降。

1.2 基于 attention 的 seq2seq

基于上述缺陷进而提出带有注意力机制 Attention 的 seq2seq,同样可以应用于 RNN、LSTM 或者 GRU 模块中。注意力机制 Attention 对人类来说非常好理解,假设给定一张图片,我们会自动聚焦到一些关键信息位置,而不需要逐行扫描全图。此处的 attention 也是同一个意思,其本质是对输入的自适应加权,结合 cv 领域的 senet 中的 se 模块就能够理解了。

Untitled

se 模块最终是学习出一个 1x1xc 的向量,然后逐通道乘以原始输入,从而对特征图的每个通道进行加权即通道注意力,对 attention 进行抽象,不管啥领域其机制都可以归纳为下图:

Untitled

将 Query(通常是向量) 和 4 个 Key(和 Q 长度相同的向量) 分别计算相似性,然后经过 softmax 得到 q 和 4 个 key 相似性的概率权重分布,然后对应权重乘以 Value(和 Q 长度相同的向量),最后相加即可得到包含注意力的 attention 值输出,理解上应该不难。 举个简单例子说明:

以上就是完整的注意力机制,采用我心中的标准 Query 去和被标签化的所有店铺 Keys 一一比对,此时就可以得到我的 Query 在每个店铺中的匹配情况,最终去不同店铺买不同东西的过程就是权重和 Values 加权求和过程。简要代码如下:

# 假设q是(1,N,512),N就是最大标签化后的list长度,k是(1,M,512),M可以等于N,也可以不相等
# (1,N,512) x (1,512,M)-->(1,N,M)
attn = torch.matmul(q, k.transpose(2, 3))
# softmax转化为概率,输出(1,N,M),表示q中每个n和每个m的相关性
attn=F.softmax(attn, dim=-1)
# (1,N,M) x (1,M,512)-->(1,N,512),V和k的shape相同
output = torch.matmul(attn, v)

带有 attention 的 RNN 模块组成的 ser2seq, 解码时候可视化如下:

Untitled

在没有 attention 时候,不同解码阶段都仅仅利用了同一个编码层的最后一个隐含输出,加入 attention 后可以通过在每个解码时间步输入的都是不同的上下文向量,以上图为例,解码阶段会将第一个开启解码标志 (也就是 Q)与编码器的每一个时间步的隐含状态 (一系列 Key 和 Value) 进行点乘计算相似性得到每一时间步的相似性分数,然后通过 softmax 转化为概率分布,然后将概率分布和对应位置向量进行加权求和得到新的上下文向量,最后输入解码器中进行解码输出,其详细解码可视化如下:

Untitled

通过上述简单的 attention 引入,可以将机器翻译性能大幅提升,引入 attention 有以下几个好处:

1.3 基于 transformer 的 seq2seq

基于 attention 的 seq2seq 的结构虽然说解决了很多问题,但是其依然存在不足:

最大问题应该是无法并行训练,不利于大规模快速训练和部署,也不利于整个算法领域发展,故在 Attention Is All You Need 论文中抛弃了传统的 CNN 和 RNN,将 attention 机制发挥到底,整个网络结构完全是由 Attention 机制组成,这是一个比较大的进步。

google 所提基于 transformer 的 seq2seq 整体结构如下所示:

Untitled

其包括 6 个结构完全相同的编码器,和 6 个结构完全相同的解码器,其中每个编码器和解码器设计思想完全相同,只不过由于任务不同而有些许区别,整体详细结构如下所示:

第一眼看有点复杂,其中 N=6,由于基于 transformer 的翻译任务已经转化为分类任务 (目标翻译句子有多长,那么就有多少个分类样本),故在解码器最后会引入 fc+softmax 层进行概率输出,训练也比较简单,直接采用 ce loss 即可,对于采用大量数据训练好的预训练模型,下游任务仅仅需要训练 fc 层即可。上述结构看起来有点复杂,一个稍微抽象点的图示如下:

Untitled

看起来比基于 RNN 或者其余结构构建的 seq2seq 简单很多。下面结合代码和原理进行深入分析。

1.4 transformer 深入分析

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

下面按照 编码器输入数据处理 -> 编码器运行 -> 解码器输入数据处理 -> 解码器运行 -> 分类 head 的实际运行流程进行讲解。

1.4.1 编码器输入数据处理

(1) 源单词嵌入

以上面翻译任务为例,原始待翻译输入是三个单词:

Untitled

输入是三个单词,为了能够将文本内容输入到网络中肯定需要进行向量化 (不然单词如何计算?),具体是采用 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) 网络自动学习

self.pos_embedding = nn.Parameter(torch.randn(1, N, 512))

比较简单,因为位置编码向量需要和输入嵌入 (b,N,512) 相加,所以其 shape 为 (1,N,512) 表示 N 个位置,每个位置采用 512 长度向量进行编码

(b) 自己定义规则

自定义规则做法非常多,论文中采用的是 sin-cos 规则,具体做法是:

pos 即 $0 \sim N$, i 是 $0-511$

def get_position_angle_vec(position):
    # hid_j是0-511,d_hid是512,position表示单词位置0~N-1
    return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]

# 每个单词位置0~N-1都可以编码得到512长度的向量
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
# 偶数列进行sin
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  # dim 2i
# 奇数列进行cos
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])  # dim 2i+1

上面例子的可视化如下:

Untitled

如此编码的优点是能够扩展到未知的序列长度,例如前向时候有特别长的句子,其可视化如下:

作者为啥要设计如此复杂的编码规则?原因是 sin 和 cos 的如下特性:

$$ \left\{ \begin{array} { l } \sin ( \alpha + \beta ) = \sin \alpha \cos \beta + \cos \alpha \sin \beta \\ \cos ( \alpha + \beta ) = \cos \alpha \cos \beta - \sin \alpha \sin \beta \end{array} \right. $$

可以将 $PE(pos + k)$ 用 $PE(pos)$ 进行线性表出:

$$ \left\{ \begin{array} { l } P E ( p o s + k , 2 i ) = P E ( p o s , 2 i ) \times P E ( k , 2 i + 1 ) + P E ( p o s , 2 i + 1 ) \times P E ( k , 2 i ) \\ P E ( p o s + k , 2 i + 1 ) = P E ( p o s , 2 i + 1 ) \times P E ( k , 2 i + 1 ) - P E ( p o s , 2 i ) \times P E ( k , 2 i ) \end{array} \right. $$

假设 $k=1$,那么下一个位置的编码向量可以由前面的编码向量线性表示,等价于以一种非常容易学会的方式告诉了网络单词之间的绝对位置,让模型能够轻松学习到相对位置信息。 注意编码方式不是唯一的,将单词嵌入向量和位置编码向量相加就可以得到编码器的真正输入了,其输出 shape 是 $(b,N,512)$。

1.4.2 编码器前向过程

编码器由两部分组成:自注意力层和前馈神经网络层。

Untitled

其前向可视化如下:

Untitled

注意上图没有绘制出单词嵌入向量和位置编码向量相加过程,但是是存在的。

(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 提供了可视化工具,如下所示:

Untitled

上述是从宏观角度思考,如果从输入输出流角度思考,也比较容易:

Untitled

假设我们现在要翻译上述两个单词,首先将单词进行编码,和位置编码向量相加,得到自注意力层输入 $X$, 其 shape 为 $(b,N,512)$;然后定义三个可学习矩阵 $W^Q,W^K,W^V$(通过 nn.Linear 实现),其 shape 为 $(512,M)$,一般 $M$ 等于前面维度 $512$,从而计算后维度不变;将 $X$ 和矩阵 $W^Q,W^K,W^V$ 相乘,得到 $QKV$ 输出,shape 为 $(b,N,M)$;然后将 $Q$ 和 $K$ 进行点乘计算向量相似性,结果的shape为 $(b, N, N)$;采用 softmax 转换为概率分布;将概率分布和 $V$ 进行加权求和即可, 结果的shape为$(b, N, M)$。

实际上代码层面采用矩阵实现非常简单:

Untitled

上面的操作很不错,但是还有改进空间,论文中又增加一种叫做 “多头” 注意力(“multi-headed” attention)的机制进一步完善了自注意力层,并在两方面提高了注意力层的性能: