最近有了一篇最新的论坛, MobileNetV3, 想必对于MobileNetV2大家有所耳闻, 但是这个mobilenetv3是什么鬼. 简单来说, 就是一个自动搜索出来的升级版本MobileNet, 速度更快, 精度更高, 模型体积更小. 这无疑是非常吸引人的, 看来神经网络架构搜索是一个十分有前景又是十分有用的东西, 这就好比, 以前只有大佬才能通过网络稍微修改, 参数稍微修改来达到更高的精度 ( 当然我觉得也是瞎JB一顿乱改) 而现在, 你可以通过一个优化算法, 自动搜索出最优化的架构, 并且很有可能是最优解!

神经网络架构搜索的基本思路

既然NAS这么牛逼, 那么这个玩意怎么做呢? 这让我们想起了当初最早做极限学习机的时候, 极限学习机通常是跟权重无关,而与结构有关, 因此还写了一篇论文来研究如果通过优化极限学习机的网络结构来达到更高的准确率. 现在想起来, 这和NAS简直如出一辙. 但是, 既然涉及到优化问题, 你就需要你的损失函数.

其他的问题损失函数是那么的显而易见, 比如分类问题, 那就是你的分类损失. 可是NAS的损失函数啥呢? 当然是整个网络的准确率.

那么问题来了, 我们通常评估一个网络的准确率, 是通过训练模型, 经过几千次上万次迭代, 才能判断这个模型的准确率的, 而如果我们要这羊在海量的搜索空间去搜索一个结构 ( 试想一下, 我一个stride步长不同都可以组成很多结构, 不同的连接方式组成的网络更多 ) 这么大的搜索空间, 你如何去优化? 顺便说一句, 这和我之前做的极限学习机优化有着本质的不同, 极限学习机每次计算速度非常快, 而且不同迭代, 因此它具备搜索的可行性.

那么我们如何来进行神经网络的架构搜索呢?

image.png

事实上, 在比较早期的NAS工作, 正式我们上面所思考的那样, 以至于一个想法可能需要在几百个G的TPU上运行上百天才能得出结果, 这个几乎是除了谷歌这样的公司以外都不切实际的做法. 但至少给我们探索出了NAS可能的三个重要步骤:

  • 首先确定搜索空间, 而这个空间可以以一个人工设计的网络为起点;
  • 然后,我们需要确定所采用的优化算法, 比如用强化学习, 进化算法, 或者贝叶斯优化等;
  • 最后我们需要设计我们的评估方案, 如果评估搜索出来的算法是有卵用的算法.

image.png

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 有啥差别.

image.png

image.png

mobilenetv2与mobilenetv1的不同之处在于在SeperableConv前面再增加了一个pw层来进行通道的扩充从而可以获取更多的特征. 这一点其实在MnasNet得到了保留, 似乎神经网络也认为这种操作是正确的.

image.png

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的不太一样的地方..