Date: 2020/07/25
Coder: CW
Foreword:
本文是该系列的重点之一,通过对DETR中Transformer部分的代码解析,你就会知道Transformer是如何在目标检测领域work的了,并且你还可以自己动手实践一番,是不是很诱人?哈哈哈!Transformer在这里与NLP的流程类似,主要包括 Embedding、Encoder 和 Decoder,如果对Transformer基本原理不了解的朋友们,可以阅读下Transformer系列的这几篇文章:
Transformer 修炼之道(一)、Input Embedding
之前写以上这几篇文章正是为该系列作铺垫,以便不熟悉Transformer原理的朋友们快速了解下相关知识点。
Outline
I. Encoder
II. Query Embedding
III. Decoder
IV. Transformer (Combine Encoder with Decoder)
Encoder
Encoder通常有6层,每层结构相同,这里使用_get_clones()方法将结构相同的层复制(注意是deepcopy)多次,返回一个nn.ModuleList实例。
_get_clones()方法一行代码就能KO掉。
Encoder的前向过程即循环调用每层的前向过程,前一层的输出作为后一层的输入。最后,若指定了需要归一化,那么就对最后一层的输出作归一化。
这里注意下几个输入参数的意义,CW 已为大家在下图中作了注释。
Encoder的每层是下图这个TransformerEncoderLayer的一个实例,其主要由 多头自注意力层(Multi-Head Self-Attention) 和 前向反馈层(FFN)构成,另外还包含了层归一化、激活层、Dropout层以及残差连接。
_get_activation_fn()方法根据输入参数指定的激活方式返回对应的激活层,默认是ReLU。
这里归一化使用了nn.LayerNorm,即层归一化。与BatchNorm(批归一化)不同,后者是在通道这个维度上进行归一化,而前者可以指定对最后的哪几个维度进行归一化。另外,其可学习参数是对应做归一化的那些维度上的每个元素的,而非批归一化那种一个通道共享一对标量。还有一点与批归一化不同,就是其在测试过程中也会计算统计量。
官方给出的说明如下:
Unlike Batch Normalization and Instance Normalization, which applies scalar scale and bias for each entire channel/plane with the :attr:`affine` option, Layer Normalization applies per-element scale and bias with :attr:`elementwise_affine`.
This layer uses statistics computed from input data in both training and evaluation modes.
EncoderLayer的前向过程分为两种情况,一种是在输入多头自注意力层和前向反馈层前先进行归一化,另一种则是在这两个层输出后再进行归一化操作。
先进行归一化的前向过程如下图。
self.self_attn是nn.MultiheadAttention的实例,其前向过程返回两部分,第一个是自注意力层的输出,第二个是自注意力权重,因此这里取了输出索引为0的部分即代表自注意力层的输出。
另外,这里的key_padding_mask对应上述Encoder的src_key_padding_mask,是backbone最后一层输出特征图对应的mask,值为True的那些位置代表原始图像padding的部分,在生成注意力的过程中会被填充为-inf,这样最终生成注意力经过softmax时输出就趋向于0,相当于忽略不计,官方对该参数的解释如下:
key_padding_mask – if provided, specified padding elements in the key will be ignored by the attention. This is an binary mask. When the value is True, the corresponding value on the attention layer will be filled with -inf.
而src_mask是在Transformer中用来“防作弊”的,即遮住当前预测位置之后的位置,忽略这些位置,不计算与其相关的注意力权重。而这里的序列是图像特征(反而就是要计算图像各位置的全局关系),不同于NLP,因此没有这个需求,在这里也就没有使用。
还有一点,在输入多头自注意力层时需要先进行位置嵌入,即结合位置编码。注意仅对query和key实施,而value不需要。query和key是在图像特征中各个位置之间计算相关性,而value作为原图像特征,使用计算出来的相关性加权上去,得到各位置结合了全局相关性(增强/削弱)后的特征表示。
(不过好奇的童鞋也可以试着对value也进行position embedding,实验对比下效果,然后再想想怎么讲故事..)
后进行归一化的前向过程与上类似。
Query Embedding
在解析Decoder前,有必要先简要地谈谈query embedding,因为它是Decoder的主要输入之一。
query embedding 有点anchor的味道,而且是自学习的anchor,作者使用了nn.Embedding实现:
其中num_queries代表图像中有多少个目标(位置),默认是100个,对这些目标(位置)全部进行嵌入,维度映射到hidden_dim,将query_embedding的权重作为参数输入到Transformer的前向过程,使用时与position encoding的方式相同:直接相加。
而这个query embedding应该加在哪呢?当然是我们需要预测的目标(query object)咯!可是网络一开始还没有输出,我们都不知道预测目标在哪里呀,如何将它实体化?作者也不知道,于是就简单粗暴地直接将它初始化为全0,shape和query embedding 的权重一致(从而可以element-wise add)。
Decoder
Decoder的结构和Encoder十分类似。
其前向过程如下。具体操作和Encoder的也类似,只不过需要先将以下红框部分的参数梳理清楚,明白各自代表的意义,之后整个代码看起来就十分好理解了。
提一句,这里tgt_mask和memory_mask都是用于“防作弊”的,这里均未使用。
在下图中,需要注意的是intermediate中记录的是每层输出后的归一化结果,而每一层的输入是前一层输出(没有归一化)的结果。
感觉下图红框部分是“多此一举”,因为上图中本身intermediate记录的就是每层输出的归一化结果了。
另外,不知道你们发现了不,这里的实现有点“令人不舒服”。self.norm是通过初始化时传进来的参数norm(默认为None)设置的,那么self.norm就有可能是None,因此下图第一句代码也对此作了判断。但是在上图中,却在没有作判断的情况下直接码出了self.norm(output)这句,所以有可能会引发异常。
在整体项目代码中,作者在构建Decoder使始终传了norm参数,使得其不为None,因此不会引发异常。但就单独看Decoder的这部分实现来说,确实是有问题的,如果朋友们直接拿这部分去用,需要注意下这点(没想到FAIR写的代码居然也有这种level的水平,被我抓到了..)。
DecoderLayer与Encoder的实现类似,只不过多了一层 Encoder-Decoder Layer,其实质也是多头自注意力层,但是key和value来自于Encoder的输出。
DecoderLayer的前向过程也如同EncoderLayer一样分为两种情况,这里就对“后进行归一化”的情况作解析。
在第一个多头自注意层中,输入均和Encoder无关。
注意,和Encoder中一样,会对query和key进行position embedding(而value则不需要)。
到了第二个多头自注意力层,即 Encoder-Decoder Layer,key和value均来自Encoder的输出。同样地,query和key要进行位置嵌入(而value不用)。
这里自注意力层计算的相关性是目标物体与图像特征各位置的相关性,然后再把这个相关性系数加权到Encoder编码后的图像特征(value)上,相当于获得了object features的意思,更好地表征了图像中的各个物体。
通过Decoder和Encoder的实现可以发现,作者极力强调位置嵌入的作用,每次在self-attention操作时都伴随着position embedding,这是因为继承了Transformer的permute invariant特性,即对排列和位置是不care的,而我们很清楚,在detection任务中,位置信息有着举足轻重的地位!
另外,看完DecoderLayer的实现,会发现可能存在“重复归一化”的问题。当使用后归一化的前向方式时(如以上两幅图所示),每个DecoderLayer的输出是归一化后的结果,但是在Decoder的前向过程中会使用self.norm对其再进行一次归一化!
Transformer (Combine Encoder with Decoder)
将Encoder和Decoder封装在一起构成Transformer。
Transformer的前向过程如下,首先是将输入(图像特征)flatten成序列,这里的src是已经将CNN提取的特征维度映射到hidden_dim的结果。
然后就是将以上输入参数依次送进Encoder和Decoder,最后再reshape到需要的结果。
注意,tgt是与query embedding形状一直且设置为全0的结果,意为初始化需要预测的目标。因为一开始并不清楚这些目标,所以初始化为全0。其会在Decoder的各层不断被refine,相当于一个coarse-to-fine的过程,但是真正要学习的是query embedding,学习到的是整个数据集中目标物体的统计特征,而tgt在每次迭代训练(一个batch数据刚到来)时会被重新初始化为0。
(那么这里就可以思考下了,这种学习方式会不会使得模型的泛化能力受限?在遇到另一个分布差异巨大的数据集时模型的表现想必会很尴尬?不过当前深度学习中基于特定数据集训练而来的模型貌似都有这个通病,毕竟你在A分布上训练,却要在B分布(与A分布差异巨大)上测试,是不是太不善良了..)
以上有一处需要注意下,就是作者在项目代码中构建Transformer时设置了return_intermediate为True,因此Decoder会返回6层的输出结果,于是hs的第一个维度是6。
@最后
本文解析了Transformer在DETR中的实现,这部分是DETR的核心部分之一。但是还没完,经过Transformer后只能说DETR的主要工作基本做完了,其输出结果并不是最终的预测结果,还需要进行转换,后续部分的内容会在该系列后续的文章中给出,还请诸位客官稍候不知道多少刻..