卷積漲點論文 | Asymmetric Convolution ACNet | ICCV | 2019

文章原創來自做者的微信公衆號:【機器學習煉丹術】。交流羣氛圍超好,我但願能夠建議一個:當一我的遇到問題的時候,有這樣一個平臺能夠快速討論並解答,目前已經1羣已經滿員啦,2羣歡迎你的到來哦。加入羣惟一的要求就是,你對AI有興趣。加個人微信我邀請進羣cyx645016617。python

  • 論文名稱:「ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Blocks」
  • 論文連接:https://arxiv.org/abs/1908.03930
  • 模型縮寫:ACNet

0 個人理解

這個ACNet是一個不錯的對於卷積核結構的一個創新。總的來講是一個值得在CNN模型中嘗試的trick,至於有沒有效果還得看緣分。不過這個trick的聽同行來講,算是一個好的trick,因此值得嘗試。微信

這個trick的代價是增長了訓練階段的時間和參數,可是並不會增長推理階段的時長,也不會增長最終模型的參數網絡

1 論文講解

這個方法挺簡單了,能夠用這一張圖來展現:
機器學習

煉丹兄帶你理解這圖:ide

  • 圖片分爲左右兩個部分,左邊是訓練階段的ACNet,右邊是部署的模型,能夠理解爲測試推理階段;
  • 通常3x3的卷積,其實就是左圖中第一行的那個卷積,ACNet的創新在於3x3的卷積的側面並行了1x3和3x1兩個矩形卷積核的卷積。能夠理解爲,任何一個卷積網絡中,原本的一個3x3的卷積層,假如使用ACNet的方法,就會變成3哥卷積層並行的一個結構。
  • 三個卷積層的輸出結構相加,就是這個這個AC卷積層的輸出特徵圖了
  • 爲何說,測試階段模型的參數沒有增長呢?這不是多了兩個卷積層,那參數怎麼會不增長呢?從右邊的圖能夠看到,這三個卷積核其實能夠合併成一個卷積核,因此其實acnet是徹底等價於通常的卷積模型的。

我的的理解,通常的模型也是有可能訓練出ACNet的效果的,由於二者的參數徹底等價。可是ACNet多是由於強化了橫向和縱向的特徵,因此會取得更好的效果。而且這個至關於,給卷積核增長了一層限制,卷積核的每個參數再也不是同等中重要的,中心更爲重要。由於增長了限制,可能也會避免過擬合。這是我的從實驗中獲得的一些猜測和思考。學習

下面看一下另一篇文章的解釋,看得懂的朋友能夠驗證本身理解的是否正確:
測試

2 訓練代碼

我先寫一個用通常卷積的很是簡單的分類網絡:.net

class Net(nn.Module):    
    def __init__(self):
        super(Net, self).__init__()
          
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
          
        self.classifier = nn.Sequential(
            nn.Dropout(p = 0.5),
            nn.Linear(64 * 7 * 7, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(p = 0.5),
            nn.Linear(512, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(p = 0.5),
            nn.Linear(512, 10),
        )                

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        
        return x

下面我來把這個網絡轉成使用ACNet的結構,先構建一個acblock來代替卷積:code

class ACConv2d(nn.Module):
    def __init__(self,in_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=True):
        super(ACConv2d,self).__init__()
        self.conv = nn.Conv2d(in_channels,out_channels,kernel_size=kernel_size,
                             stride=stride,padding=padding,bias=True)
        self.ac1 = nn.Conv2d(in_channels,out_channels,kernel_size=(1,kernel_size),
                            stride=stride,padding=(0,padding),bias=True)
        self.ac2 = nn.Conv2d(in_channels,out_channels,kernel_size=(kernel_size,1),
                            stride=stride,padding=(padding,0),bias=True)
        
    def forward(self,x):
        ac1 = self.ac1(x)
        ac2 = self.ac2(x)
        x = self.conv(x)
        return (ac1+ac2+x)/3

而後把網路中的nn.Conv2d替換成ACConv2d便可:orm

class ACNet(nn.Module):    
    def __init__(self):
        super(ACNet, self).__init__()
          
        self.features = nn.Sequential(
            ACConv2d(1, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            ACConv2d(32, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            ACConv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            ACConv2d(64, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
          
        self.classifier = nn.Sequential(
            nn.Dropout(p = 0.5),
            nn.Linear(64 * 7 * 7, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(p = 0.5),
            nn.Linear(512, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            nn.Dropout(p = 0.5),
            nn.Linear(512, 10),
        )  

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

3 效果及緣由

效果上看,模型在ImageNet上是有必定的效果的。爲何會有這樣的提高呢?論文中給出了一種解釋,由於1x3和3x1的卷積覈對於豎直翻轉和水平翻轉是有魯棒性的。看下圖:

特徵圖豎直翻轉以後,對於1x3的卷積核的特徵並無影響,可是3x3的卷積核中的特徵已經發生改變。同理,3x1的卷積覈對於水平翻轉也有魯棒性。

這個翻轉魯棒性是一種解釋,下面還有另一種解釋:

這部分的緣由我的理解是來自梯度差別化,原來只有一個[公式]卷積層,梯度能夠看出一份,而添加了1x3和3x1卷積層後,部分位置的梯度變爲2份和3份,也是更加細化了。並且理論上能夠融合無數個卷積層不斷逼近現有網絡的效果極限,融合方式不限於相加(訓練和推理階段一致便可),融合的卷積層也不限於1x3或3x1尺寸。

我把這個方法用在我MNIST數據集的識別上,不過沒有什麼效果哈哈。但願未來能夠個人項目有提高效果,是一個值得嘗試的trick,歡迎你們收藏點贊。

4 改進

最後,若是你耐心看到這裏,而且對以前的內容加以思考,就會發現,我寫的ac卷積,並無實如今推理過程的卷積核融合。我後來完善了一下代碼,當調用model.eval()後,acconv卷積就會融合成一個卷積層,而不是3個並行的卷積層:

class ACConv2d(nn.Module):
    def __init__(self,in_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=False):
        super(ACConv2d,self).__init__()
        self.bias = bias
        self.conv = nn.Conv2d(in_channels,out_channels,kernel_size=kernel_size,
                             stride=stride,padding=padding,bias=bias)
        self.ac1 = nn.Conv2d(in_channels,out_channels,kernel_size=(1,kernel_size),
                            stride=stride,padding=(0,padding),bias=bias)
        self.ac2 = nn.Conv2d(in_channels,out_channels,kernel_size=(kernel_size,1),
                            stride=stride,padding=(padding,0),bias=bias)
        self.fusedconv = nn.Conv2d(in_channels,out_channels,kernel_size=kernel_size,
                                 stride=stride,padding=padding,bias=bias)
    def forward(self,x):
        if self.training:
            ac1 = self.ac1(x)
            ac2 = self.ac2(x)
            x = self.conv(x)
            return (ac1+ac2+x)/3
        else:
            x = self.fusedconv(x)
            return x
        
    def train(self,mode=True):
        super().train(mode=mode)
        if mode is False:
            weight = self.conv.weight.cpu().detach().numpy()
            weight[:,:,1:2,:] = weight[:,:,1:2,:] + self.ac1.weight.cpu().detach().numpy()
            weight[:,:,:,1:2] = weight[:,:,:,1:2] + self.ac2.weight.cpu().detach().numpy()
            self.fusedconv.weight = torch.nn.Parameter(torch.FloatTensor(weight/3))
            if self.bias:
            	bias = self.conv.bias.cpu().detach().numpy()+self.conv.ac1.cpu().detach().numpy()+self.conv.ac2.cpu().detach().numpy()
                self.fusedconv.bias = torch.nn.Parameter(torch.FloatTensor(bias/3))
            if torch.cuda.is_available():
                self.fusedconv = self.fusedconv.cuda()

感謝各位的閱讀,喜歡的能夠點個「贊」和「在看」!
參考文章:

  1. https://arxiv.org/pdf/1908.03930.pdf
  2. https://zhuanlan.zhihu.com/p/131282789
  3. https://blog.csdn.net/u014380165/article/details/103916114
  4. https://arxiv.org/abs/1908.03930
相關文章
相關標籤/搜索