深度学习CNN基础之1x1卷积
深度学习CNN基础之1x1卷积
1x1卷积是深度学习中一种特殊的卷积操作,它在GoogLeNet和ResNet等经典网络架构中发挥着重要作用。本文将详细介绍1x1卷积的基本概念、主要作用,并通过具体案例展示其在实际应用中的效果。
1. 1x1 卷积
1x1卷积与标准卷积的主要区别在于卷积核的尺寸为1x1,这意味着它不会考虑输入数据局部信息之间的关系,而是将关注点放在不同通道间的信息交互上。当输入矩阵的尺寸为3x3,通道数也为3时,使用4个1x1卷积核进行卷积计算,最终会得到与输入矩阵尺寸相同,但通道数为4的输出矩阵。
图1 11 卷积结构示意图*
2. 1x1 卷积的作用
实现信息的跨通道交互与整合:考虑到卷积运算的输入输出都是三维的(宽、高、多通道),所以1x1卷积实际上是对每个像素点,在不同的通道上进行线性组合,从而整合不同通道的信息。
对卷积核通道数进行降维和升维,减少参数量:经过1x1卷积后的输出保留了输入数据的原有平面结构,通过调控通道数,从而完成升维或降维的作用。
增加非线性:利用1x1卷积后的非线性激活函数,在保持特征图尺寸不变的前提下,大幅增加非线性。
3. 应用示例
3.1. 1x1 卷积在GoogLeNet中的应用
GoogLeNet是2014年ImageNet比赛的冠军,它的主要特点是网络不仅有深度,还在横向上具有“宽度”。由于图像信息在空间尺寸上的巨大差异,如何选择合适的卷积核来提取特征就显得比较困难了。空间分布范围更广的图像信息适合用较大的卷积核来提取其特征;而空间分布范围较小的图像信息则适合用较小的卷积核来提取其特征。为了解决这个问题,GoogLeNet提出了一种被称为Inception模块的方案。
图2 Inception模块结构示意图
Inception模块的设计思想采用多通路(multi-path)的设计形式,每个支路使用不同大小的卷积核,最终输出特征图的通道数是每个支路输出通道数的总和。然而,这将会导致输出通道数变得很大,尤其是将多个Inception模块串联操作的时候,模型参数量会变得非常大。
为了减小参数量,Inception模块改进了设计方式。在3x3和5x5的卷积层之前均增加1x1的卷积层来控制输出通道数;在最大池化层后面增加1x1卷积层减小输出通道数。
下面这段程序是Inception块的具体实现方式,可以对照图2(b)和代码一起阅读。
我们这里可以简单计算一下Inception模块中使用1x1卷积前后参数量的变化,这里以图2(a)为例,输入通道数 $C_{in}=192$,1x1卷积的输出通道数$C_{out1}=64$,3x3卷积的输出通道数$C_{out2}=128$,5x5卷积的输出通道数$C_{out3}=32$,则图2(a)中的结构所需的参数量为:
$$ 1\times1\times192\times64+3\times3\times192\times128+5\times5\times192\times32=387072 $$
图2(b)中在3x3卷积前增加了通道数$C_{out4}=96$ 的 1x1卷积,在5x5卷积前增加了通道数$C_{out5}=16$ 的 1x1卷积,同时在maxpooling后增加了通道数$C_{out6}=32$ 的 1x1卷积,参数量变为:
$$ \begin{eqnarray}\small{ 1\times1\times192\times64+1\times1\times192\times96+1\times1\times192\times16+3\times3\times96\times128+5\times5\times16\times32 \ +1\times1\times192\times32 =163328} \end{eqnarray} $$
可见,1x1卷积可以在不改变模型表达能力的前提下,大大减少所使用的参数量。
Inception模块的具体实现如下代码所示:
import numpy as np
import paddle
from paddle.nn import Conv2D, MaxPool2D, AdaptiveAvgPool2D, Linear
import paddle.nn.functional as F
# 定义Inception块
class Inception(paddle.nn.Layer):
def __init__(self, c0, c1, c2, c3, c4, **kwargs):
'''
Inception模块的实现代码,
c1,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数
c2,图(b)中第二条支路卷积的输出通道数,数据类型是tuple或list,
其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3
c3,图(b)中第三条支路卷积的输出通道数,数据类型是tuple或list,
其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3
c4,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数
'''
super(Inception, self).__init__()
# 依次创建Inception块每条支路上使用到的操作
self.p1_1 = Conv2D(in_channels=c0,out_channels=c1, kernel_size=1)
self.p2_1 = Conv2D(in_channels=c0,out_channels=c2[0], kernel_size=1)
self.p2_2 = Conv2D(in_channels=c2[0],out_channels=c2[1], kernel_size=3, padding=1)
self.p3_1 = Conv2D(in_channels=c0,out_channels=c3[0], kernel_size=1)
self.p3_2 = Conv2D(in_channels=c3[0],out_channels=c3[1], kernel_size=5, padding=2)
self.p4_1 = MaxPool2D(kernel_size=3, stride=1, padding=1)
self.p4_2 = Conv2D(in_channels=c0,out_channels=c4, kernel_size=1)
def forward(self, x):
# 支路1只包含一个1x1卷积
p1 = F.relu(self.p1_1(x))
# 支路2包含 1x1卷积 + 3x3卷积
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
# 支路3包含 1x1卷积 + 5x5卷积
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
# 支路4包含 最大池化和1x1卷积
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 将每个支路的输出特征图拼接在一起作为最终的输出结果
return paddle.concat([p1, p2, p3, p4], axis=1)
3.2. 1x1 卷积在ResNet中的应用
随着深度学习的不断发展,模型的层数越来越多,网络结构也越来越复杂。但是增加网络的层数之后,训练误差往往不降反升。由此,Kaiming He等人提出了残差网络ResNet来解决上述问题。ResNet是2015年ImageNet比赛的冠军,将识别错误率降低到了3.6%,这个结果甚至超出了正常人眼识别的精度。在ResNet中,提出了一个非常经典的结构—残差块(Residual block)。
残差块是ResNet的基础,具体设计方案如图3所示。不同规模的残差网络中使用的残差块也并不相同,对于小规模的网络,残差块如图3(a)所示。但是对于稍大的模型,使用图3(a)的结构会导致参数量非常大,因此从ResNet50以后,都是使用图3(b)的结构。图3(b)中的这种设计方案也常称作瓶颈结构(BottleNeck)。11的卷积核可以非常方便的调整中间层的通道数,在进入33的卷积层之前减少通道数(256->64),经过该卷积层后再恢复通道数(64->256),可以显著减少网络的参数量。这个结构(256->64->256)像一个中间细,两头粗的瓶颈,所以被称为“BottleNeck”。
我们这里可以简单计算一下残差块中使用1x1卷积前后参数量的变化,为了保持统一,我们令图3中的两个结构的输入输出通道数均为256,则图3(a)中的结构所需的参数量为:
$$ 3\times3\times256\times256\times2=1179648 $$
而图3(b)中采用了1x1卷积后,参数量为:
$$ 1\times1\times256\times64+3\times3\times64\times64+1\times1\times64\times256=69632 $$
同样,1x1卷积可以在不改变模型表达能力的前提下,大大减少所使用的参数量。
图3 残差块结构示意图
残差块的具体实现如下代码所示:
import numpy as np
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
# ResNet中使用了BatchNorm层,在卷积层的后面加上BatchNorm以提升数值稳定性
# 定义卷积批归一化块
class ConvBNLayer(paddle.nn.Layer):
def __init__(self,
num_channels,
num_filters,
filter_size,
stride=1,
groups=1,
act=None):
"""
num_channels, 卷积层的输入通道数
num_filters, 卷积层的输出通道数
stride, 卷积层的步幅
groups, 分组卷积的组数,默认groups=1不使用分组卷积
"""
super(ConvBNLayer, self).__init__()
# 创建卷积层
self._conv = nn.Conv2D(
in_channels=num_channels,
out_channels=num_filters,
kernel_size=filter_size,
stride=stride,
padding=(filter_size - 1) // 2,
groups=groups,
bias_attr=False)
# 创建BatchNorm层
self._batch_norm = paddle.nn.BatchNorm2D(num_filters)
self.act = act
def forward(self, inputs):
y = self._conv(inputs)
y = self._batch_norm(y)
if self.act == 'leaky':
y = F.leaky_relu(x=y, negative_slope=0.1)
elif self.act == 'relu':
y = F.relu(x=y)
return y
# 定义残差块
# 每个残差块会对输入图片做三次卷积,然后跟输入图片进行短接
# 如果残差块中第三次卷积输出特征图的形状与输入不一致,则对输入图片做1x1卷积,将其输出形状调整成一致
class BottleneckBlock(paddle.nn.Layer):
def __init__(self,
num_channels,
num_filters,
stride,
shortcut=True):
super(BottleneckBlock, self).__init__()
# 创建第一个卷积层 1x1
self.conv0 = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters,
filter_size=1,
act='relu')
# 创建第二个卷积层 3x3
self.conv1 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters,
filter_size=3,
stride=stride,
act='relu')
# 创建第三个卷积 1x1,但输出通道数乘以4
self.conv2 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters * 4,
filter_size=1,
act=None)
# 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
# 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
if not shortcut:
self.short = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters * 4,
filter_size=1,
stride=stride)
self.shortcut = shortcut
self._num_channels_out = num_filters * 4
def forward(self, inputs):
y = self.conv0(inputs)
conv1 = self.conv1(y)
conv2 = self.conv2(conv1)
# 如果shortcut=True,直接将inputs跟conv2的输出相加
# 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
if self.shortcut:
short = inputs
else:
short = self.short(inputs)
y = paddle.add(x=short, y=conv2)
y = F.relu(y)
return y
参考文献
[1] -Going deeper with convolutions
[2] -Deep Residual Learning for Image Recognition