Press "Enter" to skip to content

轻量的神经网络

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

很久之前我觉得移动端应用几百兆的模型不切实际,在不考虑蒸馏、量化等压缩方法下,发现了 MobileNet 设计的很神奇,大小只有几 MB,可以说是一股清流了。就整理发布了一下,然后今天发现找不到了,神奇。(于是顺手和 ShuffleNet 一并整理到轻量化的神经网络中)

 

MobileNet-V1

 

基本上可以说这个版本是后面几个版本的出发点。先来看一下创新点:提出 depthwise separable conv 和 pointwise conv 来降低网络的计算次数。还是直接画图吧:

 

 

对于传统卷积而言,输入一个三通道的图片,如果想要输出五通道,那幺就需要 5 个 $3\times 3 \times 3$ 的卷积核。一般一些,假设传统卷积处理图像的大小是 $D_F\times D_F$,有 $M$ 个通道,卷积核的大小是 $D_K$,输出的通道数数 $N$,那幺计算量就是 $D_K \cdot D_K \cdot M \cdot N \cdot D_F \cdot D_F$。

 

在得到相同大小输出的情况下,使用 DW 卷积和 PW 卷积来简化一下这个计算过程:

 

 

如果换成深度可分离卷积和逐点卷积,可以看到达到同样的输出,参数量从 $27\times 5$ 减少到了 $27+15$,而且计算量为 $D_K \cdot D_K \cdot M \cdot D_F \cdot D_F + M \cdot N \cdot D_F \cdot D_F$。两者的比值是 $1/N+1/D_K^2$。

 

class MobileNetV1(nn.Module):
    def __init__(self, ch_in, n_classes):
        super(MobileNetV1, self).__init__()
        def conv_bn(inp, oup, stride):
            return nn.Sequential(
                nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
                nn.BatchNorm2d(oup),
                nn.ReLU(inplace=True)
                )
        def conv_dw(inp, oup, stride):
            return nn.Sequential(
                # dw
                # 输入通道和输出通道相等,groups 表示每个卷积核只处理一个通道
                nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),
                nn.BatchNorm2d(inp),
                nn.ReLU(inplace=True),
                # pw
                # 卷积核大小为 1X1
                nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
                nn.ReLU(inplace=True),
                )
        self.model = nn.Sequential(
            conv_bn(ch_in, 32, 2),
            conv_dw(32, 64, 1),
            conv_dw(64, 128, 2),
            conv_dw(128, 128, 1),
            conv_dw(128, 256, 2),
            conv_dw(256, 256, 1),
            conv_dw(256, 512, 2),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 1024, 2),
            conv_dw(1024, 1024, 1),
            nn.AdaptiveAvgPool2d(1)
        )
        self.fc = nn.Linear(1024, n_classes)
    def forward(self, x):
        x = self.model(x)
        x = x.view(-1, 1024)
        x = self.fc(x)
        return x

 

MobileNet-V2

 

V1 的思想可以概括为:首先利用 3×3 的深度可分离卷积提取特征,然后利用 1×1 的卷积来扩张通道。但是有人在实际使用的时候,发现训完之后发现 dw 卷积核有不少是空的。

 

作者认为这是 ReLU 激活函数导致的。于是做了一个实验,就是对一个 n 维空间中的一个东西乘以矩阵 $T$,而后做 ReLU 运算,然后利用 $T$ 的逆矩阵恢复,对比 ReLU 之后的结果与 Input 的结果相差有多大。作者发现:低维度做 ReLU 运算,很容易造成信息的丢失。而在高维度进行 ReLU 运算的话,信息的丢失则会很少。

 

由于卷积本身没有改变通道的能力,来的是多少通道输出就是多少通道。上面又得出低维通道不好的结论,因此使用 PW 卷积升维再降维,这也就形成了 Inverted Residuals 这种结构,因为传统的残差结构和本文相反,传统的是先降维在升维。

 

这样高维的仍然使用 ReLU 激活函数,低维的换成线性激活函数。因为有先升维在降维的结构,因此使用了残差连接来提升性能。

 

class InvertedResidual(nn.Module):
    def __init__(self, inp, oup, stride, expand_ratio):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        assert stride in [1, 2]
        hidden_dim = int(inp * expand_ratio)
        self.use_res_connect = self.stride == 1 and inp == oup
        if expand_ratio == 1:
            self.conv = nn.Sequential(
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # pw-linear
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )
        else:
            self.conv = nn.Sequential(
                # pw 升维
                nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # dw 深度可分离卷积
                nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # pw-linear 激活
                nn.Conv2d(hidden_dim, 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)

 

MobileNet-V3

 

主要做了两点创新,一个是在 MobileNet V2 残差分支加入了 SE(Squeeze-and-Excitation) 注意力机制的模块,一个是更新了激活函数。SE 注意力就是通过池化得到每个通道的值,并输入到全连接层学习到每个通道的权重,对每个通道的数值进行更新。

 

class SELayer(nn.Module):
    def __init__(self, channel, reduction=4):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        # 输入维度和输出维度相同
        self.fc = nn.Sequential(
            nn.Linear(channel, _make_divisible(channel // reduction, 8)),
            nn.ReLU(inplace=True),
            nn.Linear(_make_divisible(channel // reduction, 8), channel),
            h_sigmoid()
        )
    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        # y 是每个通道的权重
        return x * y

 

在重新设计激活函数方面,使用 h-swish 激活函数代替了 swish 激活函数,因为更容易计算。对于 swish 激活函数:

 

\begin{equation}

 

\begin{aligned}

 

\text{swish} x &= x \cdot \sigma(x) \\

 

\sigma(x) &= \frac{1}{1+e^{-x}}

 

\end{aligned}

 

\end{equation}

 

这个反向传播和激活的计算过程略显复杂,对量化不够友好。于是使用较为接近的 h-swish 激活函数代替:

 

\begin{equation}

 

\begin{aligned}

 

\text{h-sigmoid} &= \frac{\text{ReLU6}(x+3)}{6} \\

 

\text{h-swish} &= x \cdot \text{h-sigmoid}

 

\end{aligned}

 

\end{equation}

 

class h_sigmoid(nn.Module):
    def __init__(self, inplace=True):
        super(h_sigmoid, self).__init__()
        self.relu = nn.ReLU6(inplace=inplace)
    def forward(self, x):
        return self.relu(x + 3) / 6
class h_swish(nn.Module):
    def __init__(self, inplace=True):
        super(h_swish, self).__init__()
        self.sigmoid = h_sigmoid(inplace=inplace)
    def forward(self, x):
        return x * self.sigmoid(x)

 

class InvertedResidual(nn.Module):
    def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se, use_hs):
        super(InvertedResidual, self).__init__()
        assert stride in [1, 2]
        self.identity = stride == 1 and inp == oup
        if inp == hidden_dim:
            self.conv = nn.Sequential(
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, (kernel_size - 1) // 2, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                h_swish() if use_hs else nn.ReLU(inplace=True),
                # Squeeze-and-Excite
                SELayer(hidden_dim) if use_se else nn.Identity(),
                # pw-linear
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )
        else:
            self.conv = nn.Sequential(
                # pw 升维
                nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
                nn.BatchNorm2d(hidden_dim),
                h_swish() if use_hs else nn.ReLU(inplace=True),
                # dw 提取特征
                nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, (kernel_size - 1) // 2, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                # Squeeze-and-Excite
                SELayer(hidden_dim) if use_se else nn.Identity(),
                h_swish() if use_hs else nn.ReLU(inplace=True),
                # pw-linear 先行激活,降维
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )
    def forward(self, x):
        if self.identity:
            return x + self.conv(x)
        else:
            return self.conv(x)

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。