本文是伯克利與谷歌的研究在Convolution+Transformer組合方面一個探索,它採用混合方式同時利用了CNN的特徵提取能力、transformer的內容自注意力與位置自注意力機制,取得了優於純CNN(如ResNet、EfficientNet)或者自注意力(如ViT、DeiT)的性能。所提方案在ImageNet上取得了84.7%的top1精度;在COCO數據集上,基於MaskR-CNN取得了44.4%的MaskAP與49.7%的BoxAP指標。本文值得對Transformer感興趣的同學深入研究一番,文中各種實驗分析相當的詳細。
Abstract本文提出了一種概念簡單但強有力的骨幹網絡BoTNet,它集成了自注意力機制並用於多個計算機視覺任務(包含圖像分類、目標檢測、實例分割)。通過簡單的採用全局自注意力模塊替換ResNet的最後三個Bottleneck模塊中的卷積,而無需其他改動即可得到BoTNet,相比baseline,所提方法可以在目標檢測與實例分割任務上取得顯著的性能提升,同時還可以降低參數量,代價為少量的推理延遲。
通過BoTNet的設計,本文同時指出:帶自注意力模塊的Bottleneck模塊可以視作Transformer模塊。無需任何技巧,基於Mask R-CNNN框架,BoTNet在COCO實例分割任務上取得了44.4%的Mask AP與49.7%的Box AP指標,超越了之前由ResNeSt的最佳指標。最後,本文還針對圖像分類任務進行了BoTNet的自適應設計,在ImageNet數據集上取得了84.7%的top1精度,同時比EfficientNe快2.33倍(硬體平臺為TPU-v3)。
Method本文的的思路非常簡單:採用多頭注意力(Multi-Head Self Attention, MHSA)模塊替換Bottleneck中的。而MHSA的結構示意圖見下圖。
一般來說,ResNet包含4個階段,即[c2, c3, c4, c5],它們分別對應stride=[4,8,16,32]尺寸的特徵。[c2,c3,c4,c5]分別由多個Bottleneck模塊構成,比如ResNet50中的數量分別為[3,4,6,3]。ResNet50與BoTNet50的結構信息對比見下表。
本文與現有自注意力機制方法的一個區別在於:以Vit、SAN等為代表的自注意力機制參與到ResNet的各個階段的Bottleneck中取了;而BoTNet則只是參與到c5階段的Bottleneck。為什麼會這種設計呢?原因分析如下:
本文的目的是採用注意力提升實例分割模型的性能,而實例分割模塊的輸入通常比較大(比如
為更好的使得注意力機制存在位置相關性,基於Transformer的架構引入了Position Encoding。而之前趙恆爽、賈佳亞等人提出的SAN方案已經證實了位置編碼在視覺任務中的有效性,故而本文所設計的BoT模塊中同樣引入了與位置相關的自注意力機制。
最後,我們附上不同配置的網絡架構示意圖,見下圖。
Experiments為更好的說明所提方案的有效性,我們在實例分割、目標檢測等任務上對其進行了論證。
我們先來看一下COCO實例分割上的效果,結果見上表。從上表可以看到:在12epoch訓練周期下,相比ResNet50,BoTNet50取得了有效的性能提升;而更長周期的訓練可以得到更為顯著的性能改善。
與此同時,為說明位置編碼的重要性,本文對其進行了消融實驗分析,結果見上表。可以看到:內容注意力機制可以取得0.6AP性能提升,而位置注意力機制可以取得1.0AP性能提升;而兩者的組合則可以取得1.5AP性能提升。這說明內容與位置注意力具有互補性。
此外,本文還針對「為什麼要替換c5中所有Bottleneck的
在上述基礎上,作者還引入了多尺度jitter配置,可以看到BoT50還可以進一步的加劇性能提升。
另外,作者還將所提方法與非局部注意力機制進行了對比,結果見上表。可以看到:BoT模塊帶來的性能提升要比NL模塊的提升更大;此外還可以看到:同時替換c4與c5還可以取得進一步的性能提升。
最後,我們再來看一下在圖像分類任務上所提方法與其他方案(包含SENet、EfficientNet、ViT、DeiT)的性能對比,結果見上表與下表。可以看到:BoTNet取得了比DeiT-384更好的結果,這說明:混合模型可以同時利用卷積與自注意力的特性,取得優於純注意力模型的效果。
全文到此結束,更多消融實驗分析建議查看原文,強烈建議感興趣的同學去研讀一下原文。
Code另外吐槽一點:本文尚未提供預訓練模型,不過提供了Tensorflow和Pytorch實現(Pytorch為第三方)。這裡提供一下核心的code,實現如下。
def MHSA(featuremap, pos_enc_type='relative', use_pos=True): q = group_pointwise( featuremap, proj_factor=1, name='q_proj', heads=heads, target_dimension=bottleneck_dimension) k = group_pointwise( featuremap, proj_factor=1, name='k_proj', heads=heads, target_dimension=bottleneck_dimension) v = group_pointwise( featuremap, proj_factor=1, name='v_proj', heads=heads, target_dimension=bottleneck_dimension) assert pos_enc_type in ['relative', 'absolute'] o = relpos_self_attention( q=q, k=k, v=v, relative=use_pos, fold_heads=True) return o
def group_pointwise( featuremap, proj_factor=1, name='grouppoint', heads=4, target_dimension=None): """1x1 conv with heads.""" with tf.variable_scope(name, reuse=tf.AUTO_REUSE): in_channels = featuremap.shape[-1] if target_dimension is not None: proj_channels = target_dimension // proj_factor else: proj_channels = in_channels // proj_factor w = tf.get_variable( 'w', [in_channels, heads, proj_channels // heads], dtype=featuremap.dtype, initializer=tf.random_normal_initializer(stddev=0.01)) out = tf.einsum('bHWD,Dhd->bhHWd', featuremap, w) return out
def relative_logits(q): """Compute relative position enc logits.""" with tf.variable_scope('relative', reuse=tf.AUTO_REUSE): bs, heads, h, w, dim = q.shape int_dim = dim.value
rel_emb_w = tf.get_variable( 'r_width', shape=(2*w - 1, dim), dtype=q.dtype, initializer=tf.random_normal_initializer(int_dim**-0.5)) rel_logits_w = relative_logits_1d( q=q, rel_k=rel_emb_w, transpose_mask=[0, 1, 2, 4, 3, 5])
rel_emb_h = tf.get_variable( 'r_height', shape=(2*h - 1, dim), dtype=q.dtype, initializer=tf.random_normal_initializer(int_dim**-0.5)) rel_logits_h = relative_logits_1d( q=tf.transpose(q, [0, 1, 3, 2, 4]), rel_k=rel_emb_h, transpose_mask=[0, 1, 4, 2, 5, 3]) return rel_logits_h + rel_logits_w
def relpos_self_attention( *, q, k, v, relative=True, fold_heads=False): """2D self-attention with rel-pos. Add option to fold heads.""" bs, heads, h, w, dim = q.shape int_dim = dim.value q = q * (dim ** -0.5) logits = tf.einsum('bhHWd,bhPQd->bhHWPQ', q, k) if relative: logits += relative_logits(q) weights = tf.reshape(logits, [-1, heads, h, w, h * w]) weights = tf.nn.softmax(weights) weights = tf.reshape(weights, [-1, heads, h, w, h, w]) attn_out = tf.einsum('bhHWPQ,bhPQd->bHWhd', weights, v) if fold_heads: attn_out = tf.reshape(attn_out, [-1, h, w, heads * dim]) return attn_out下載在極市平臺公眾號後臺回覆:BoTNet,即可獲得論文與code下載連結(註:暫無預訓練模型)。