網絡結構搜索NAS小記

最近有了一篇最新的論壇, MobileNetV3, 想必對於MobileNetV2大家有所耳聞, 但是這個mobilenetv3是什麼鬼. 簡單來說, 就是一個自動搜索出來的升級版本MobileNet, 速度更快, 精度更高, 模型體積更小. 這無疑是非常吸引人的, 看來神經網絡架構搜索是一個十分有前景又是十分有用的東西, 這就好比, 以前只有大佬才能通過網絡稍微修改, 參數稍微修改來達到更高的精度 ( 當然我覺得也是瞎JB一頓亂改) 而現在, 你可以通過一個優化算法, 自動搜索出最優化的架構, 並且很有可能是最優解!

神經網絡架構搜索的基本思路

既然NAS這麼牛逼, 那麼這個玩意怎麼做呢? 這讓我們想起了當初最早做極限學習機的時候, 極限學習機通常是跟權重無關,而與結構有關, 因此還寫了一篇論文來研究如果通過優化極限學習機的網絡結構來達到更高的準確率. 現在想起來, 這和NAS簡直如出一轍. 但是, 既然涉及到優化問題, 你就需要你的損失函數.

其他的問題損失函數是那麼的顯而易見, 比如分類問題, 那就是你的分類損失. 可是NAS的損失函數啥呢? 當然是整個網絡的準確率.

那麼問題來了, 我們通常評估一個網絡的準確率, 是通過訓練模型, 經過幾千次上萬次迭代, 才能判斷這個模型的準確率的, 而如果我們要這羊在海量的搜索空間去搜索一個結構 ( 試想一下, 我一個stride步長不同都可以組成很多結構, 不同的連接方式組成的網絡更多 ) 這麼大的搜索空間, 你如何去優化? 順便說一句, 這和我之前做的極限學習機優化有着本質的不同, 極限學習機每次計算速度非常快, 而且不同迭代, 因此它具備搜索的可行性.

那麼我們如何來進行神經網絡的架構搜索呢?

最早的一篇應該是Googlebrain推出的Neural Architecture Search with Reinforcement Learning(ICLR 2017),參見:
https://blog.csdn.net/Lucifer_zzq/article/details/83188462

在這裏插入圖片描述
事實上, 在比較早期的NAS工作, 正式我們上面所思考的那樣, 以至於一個想法可能需要在幾百個G的TPU上運行上百天才能得出結果, 這個幾乎是除了谷歌這樣的公司以外都不切實際的做法. 但至少給我們探索出了NAS可能的三個重要步驟:

  1. 首先確定搜索空間, 而這個空間可以以一個人工設計的網絡爲起點;
  2. 然後,我們需要確定所採用的優化算法, 比如用強化學習, 進化算法, 或者貝葉斯優化等;
  3. 最後我們需要設計我們的評估方案, 如果評估搜索出來的算法是有卵用的算法.

在這裏插入圖片描述

NAS常用的方法

既然我們有了一個大概的套路, 那麼我們如何開始一個NAS的實驗呢? 我們想看看, 如何進行一個NAS的實際操作. 先來總結一下前輩們套索的路徑.

  • 基於強化學習(Reinforcement learning):如上面提到的,開創性的工作主要是2016年由MIT發表的《Designing
    Neural Network Architectures using Reinforcement
    Learning》和Google發表的《Neural Architecture Search with Reinforcement
    Learning》兩篇文章。前者提出MetaQNN,它將網絡架構搜索建模成馬爾可夫決策過程,使用RL方法(具體地,Q-learning算法)來產生CNN架構。。
  • 基於進化算法(Evolutionary algorithm):在Google的論文《Large-Scale Evolution of
    Image
    Classifiers》中,進化算法被引入來解決NAS問題,並被證明在CIFAR-10和CIFAR-100兩個數據集上能從一個簡單的初始條件開始達到高的精度。首先,網絡結構會進行編碼,稱爲DNA。演進過程中會維擴護網絡模型的集合,這些網絡模型的fitness通過它們在驗證集上的準確率給出。。
  • 基於梯度的方法(Gradient-based
    method):這是比較新的一類方法。前面提到的基於強化學習和進化算法的方法本質上都還是在離散空間中搜索,它們將目標函數看作黑盒。我們知道,如果搜索空間連續,目標函數可微,那基於梯度信息可以更有效地搜索。CMU和Google的學者在《DARTS:
    Differentiable Architecture
    Search》一文中提出DARTS方法。一個要搜索最優結構的cell,可以看作是包含N個有序結點的有向無環圖。結點代表隱式表徵(例如特徵圖),連接結點的的有向邊代表算子操作。。 摘自這裏

說了這麼多, 似然並沒有實際的告訴我們應該怎麼進行NAS. 不要着急, 上面應該提到了一個很著名的例子, MnasNet, 顧名思義, 就是尋找在移動端最優的神經網絡架構搜索.

我們不如以這個作爲例子, 來搜索一個MnasNet. 先放出結論, 你搜索出來的MnasNet 將會比 MobileNetV2快 1.5x. 一刻賽聽!

動手實現MnasNet

MnasNet的論文連接來自於: https://arxiv.org/abs/1807.11626. MnasN et實際上設計的初衷就是, 設計一個自動優化步驟, 將網絡的latency(也就是計算時間) 與精確度之間進行一個tradeoff, 使得他們達到一個平衡 (二者不可能兼得, 但是一定存在一個最優點). 這篇文章首選確定在同一個平臺進行比對(不同平臺算力不同無法比較), 最重要的是提出了一種 分層分解搜索空間的方法 來進行網絡結構的搜索.

在這裏插入圖片描述

我們來分析一下MnasNet的結構塗:

  • 首先我們計算latency(來自於訓練時候的時間) 以及網絡準確度, 共同得到一個reward,
    這個reward就是我們要權衡的兩個東西, 運算時間和網絡準確度;
  • 然後將reward返回到一個controller中, 這個controller應該就是決定了如何對網絡進行重構和擇優;

計算時延和網絡精度沒啥可說的, 但是 這個搜索就得說一下了. 幾乎是本方法核心內容. 這是一種基於梯度的增強學習尋優辦法, 下圖展示的便是 層級搜索空間方法:

在這裏插入圖片描述

具體搜索方式, 在以前大家搜索都是以op爲單位, 就是每一層卷積當做是一個cell, 搜索cell的尺寸和形式, 比如你是3x3, 還是 5x5, 還是 7x7, 是帶pooling的卷積還是不帶的, 是帶maxpooling的還是帶averagepooling的. 這些都是需要去搜索的. 這個MnasNet的最大不同之處在於, 它還允許cell的形式, 比如同樣是執行3x3的計算, 可以先用乘在用1x1的卷積, 也可以直接用3x3的卷積這個操作雖然結果一樣, 但是 計算時間是不同的, 這個本質上也是mobilenetv2所改進的地方.

OK, 下面看看搜索出來的MnasNet 跟 Mobilenetv2 有啥差別.

在這裏插入圖片描述

在這裏插入圖片描述

mobilenetv2與mobilenetv1的不同之處在於在SeperableConv前面再增加了一個pw層來進行通道的擴充從而可以獲取更多的特徵. 這一點其實在MnasNet得到了保留, 似乎神經網絡也認爲這種操作是正確的.

在這裏插入圖片描述

Mobilenetv2和Resnet的insight的不同, mobilenetv2是擴張擴張提取特徵再還原, 而resnet是壓縮壓縮提取特徵再還原.

Mnasnet裏面似乎與MobileNetV2沒啥區別, 同樣也是inverted-residual, 先擴充通道提取特徵, 再還原. 最後我們看一下MnasNet的pytorch實現:

from torch.autograd import Variable
import torch.nn as nn
import torch
import math

def Conv_3x3(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)

def Conv_1x1(inp, oup):
return nn.Sequential(
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)

def SepConv_3x3(inp, oup): #input=32, output=16
return nn.Sequential(
# dw
nn.Conv2d(inp, inp , 3, 1, 1, groups=inp, bias=False),
nn.BatchNorm2d(inp),
nn.ReLU6(inplace=True),
# pw-linear
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)

class InvertedResidual(nn.Module):
def init(self, inp, oup, stride, expand_ratio, kernel):
super(InvertedResidual, self).init()
self.stride = stride
assert stride in [1, 2]

    self.use_res_connect = self.stride == 1 and inp == oup

    self.conv = nn.Sequential(
        # pw
        nn.Conv2d(inp, inp * expand_ratio, 1, 1, 0, bias=False),
        nn.BatchNorm2d(inp * expand_ratio),
        nn.ReLU6(inplace=True),
        # dw
        nn.Conv2d(inp * expand_ratio, inp * expand_ratio, kernel, stride, kernel // 2, groups=inp * expand_ratio, bias=False),
        nn.BatchNorm2d(inp * expand_ratio),
        nn.ReLU6(inplace=True),
        # pw-linear
        nn.Conv2d(inp * expand_ratio, oup, 1, 1, 0, bias=False),
        nn.BatchNorm2d(oup),
    )

def forward(self, x):
    if self.use_res_connect:
        return x + self.conv(x)
    else:
        return self.conv(x)

class MnasNet(nn.Module):
def init(self, n_class=1000, input_size=224, width_mult=1.):
super(MnasNet, self).init()

    # setting of inverted residual blocks
    self.interverted_residual_setting = [
        # t, c, n, s, k
        [3, 24,  3, 2, 3],  # -> 56x56
        [3, 40,  3, 2, 5],  # -> 28x28
        [6, 80,  3, 2, 5],  # -> 14x14
        [6, 96,  2, 1, 3],  # -> 14x14
        [6, 192, 4, 2, 5],  # -> 7x7
        [6, 320, 1, 1, 3],  # -> 7x7
    ]

    assert input_size % 32 == 0
    input_channel = int(32 * width_mult)
    self.last_channel = int(1280 * width_mult) if width_mult > 1.0 else 1280

    # building first two layer
    self.features = [Conv_3x3(3, input_channel, 2), SepConv_3x3(input_channel, 16)]
    input_channel = 16

    # building inverted residual blocks (MBConv)
    for t, c, n, s, k in self.interverted_residual_setting:
        output_channel = int(c * width_mult)
        for i in range(n):
            if i == 0:
                self.features.append(InvertedResidual(input_channel, output_channel, s, t, k))
            else:
                self.features.append(InvertedResidual(input_channel, output_channel, 1, t, k))
            input_channel = output_channel

    # building last several layers
    self.features.append(Conv_1x1(input_channel, self.last_channel))
    self.features.append(nn.AdaptiveAvgPool2d(1))

    # make it nn.Sequential
    self.features = nn.Sequential(*self.features)

    # building classifier
    self.classifier = nn.Sequential(
        nn.Dropout(),
        nn.Linear(self.last_channel, n_class),
    )

    self._initialize_weights()

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

def _initialize_weights(self):
    for m in self.modules():
        if isinstance(m, nn.Conv2d):
            n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
            m.weight.data.normal_(0, math.sqrt(2. / n))
            if m.bias is not None:
                m.bias.data.zero_()
        elif isinstance(m, nn.BatchNorm2d):
            m.weight.data.fill_(1)
            m.bias.data.zero_()
        elif isinstance(m, nn.Linear):
            n = m.weight.size(1)
            m.weight.data.normal_(0, 0.01)
            m.bias.data.zero_()

if name == ‘main’:
net = MnasNet()
x_image = Variable(torch.randn(1, 3, 224, 224))
y = net(x_image)
# print(y)

總的來說Mnasnet比較多得采用了 5x5 的卷積. 這是與mobilenet的不太一樣的地方…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章