文章原創來自作者的微信公眾號:【機器學習煉丹術】。交流群氛圍超好,我希望可以建議一個:當一個人遇到問題的時候,有這樣一個平臺可以快速討論並解答,目前已經1群已經滿員啦,2群歡迎你的到來哦。加入群唯一的要求就是,你對AI有興趣。加我的微信我邀請進群cyx645016617。
- 論文名稱:“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 論文講解
這個方法挺簡單了,可以用這一張圖來展示:
煉丹兄帶你理解這圖:
- 圖片分為左右兩個部分,左邊是訓練階段的ACNet,右邊是部署的模型,可以理解為測試推理階段;
- 一般3x3的卷積,其實就是左圖中第一行的那個卷積,ACNet的創新在於3x3的卷積的側面並行了1x3和3x1兩個矩形卷積核的卷積。可以理解為,任何一個卷積網路中,本來的一個3x3的卷積層,假如使用ACNet的方法,就會變成3哥卷積層並行的一個結構。
- 三個卷積層的輸出結構相加,就是這個這個AC卷積層的輸出特徵圖了
- 為什麼說,測試階段模型的引數沒有增加呢?這不是多了兩個卷積層,那引數怎麼會不增加呢?從右邊的圖可以看到,這三個卷積核其實可以合併成一個卷積核,所以其實acnet是完全等價於一般的卷積模型的。
個人的理解,一般的模型也是有可能訓練出ACNet的效果的,因為兩者的引數完全等價。但是ACNet可能是因為強化了橫向和縱向的特徵,所以會取得更好的效果。並且這個相當於,給卷積核增加了一層限制,卷積核的每一個引數不再是同等中重要的,中心更為重要。因為增加了限制,可能也會避免過擬合。這是個人從實驗中得到的一些猜想和思考。
下面看一下另外一篇文章的解釋,看得懂的朋友可以驗證自己理解的是否正確:
2 訓練程式碼
我先寫一個用一般卷積的非常簡單的分類網路:
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來代替卷積:
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
即可:
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()
感謝各位的閱讀,喜歡的可以點個“贊”和“在看”!
參考文章: