【卷积神经网络系列】四、NIN-Net


参考资料:

论文地址:

Network In Network

参考博客:

[论文解读]深度学习基础模型NIN(Network in Network)+Pytorch

NIN论文翻译及解读

NIN来源论文《Network In Network》读后总结

泛读+复现---- NiN(Network In Network)

NIN一个即使放到现在也不会过时的网络

一文读懂卷积神经网络中的1x1卷积核

一文带你深入全局平均池化


一、简介

 CNN低层只学习了局部像素,深层产生语义信息,只有加深网络深度或者加大卷积核也就是加大感受野,才能学习到更加深层的特征。但是感受野大小不能随便动,感受野大于图片大小,则整个图片都变成背景了,提取不到特征,若太小,则局部信息过多导致丢失全局信息。

 作者说CNN的广义线性模型GLM对局部的抽象能力不太行(传统的卷积+池化+激活的操作),这里的抽象能力我个人理解为将图片转换成编码的过程,或者是对图片局部像素特征的学习表达能力。该文章通过提出了一种新的特征提取技术(MLPConv)以及使用全局均值池化(Global Average Pooling)代替寻常网络最后的全连接层,在CIFAR-10、CIFAR-100和MNIST数据集上均获得了最优的效果。

该文章的贡献为:

  • 1.设计了MLPConv层来代替通常的“卷积+激活函数”层;
  • 2.用全局均值池化GAP代替寻常网络最后的全连接层;
  • 3.设计了NIN神经网络。

在这里插入图片描述

二、相关概念

几个相关的名词:

2.1 感受野

(1)感受野概念

 卷积神经网络每一层输出的特征图(feature map)上的像素点映射回输入图像上的区域大小。通俗点的解释是,特征图上一点,相对于原图的大小,也是卷积神经网络特征所能看到输入图像的区域,随着卷积核的增多(即网络的加深),感受野会越来越大。

 若输入图像的尺寸大小是 5 × 5 5 \times 5 5×5,经过两次 3 × 3 3 \times 3 3×3的卷积核(其中stride=1,padding=0)后,(由卷积计算公式:N=(W-F+2P)/S+1,得到第一次卷积后的图像大小为,第 3 × 3 3 \times 3 3×3二次卷积后的图像大小为 1 × 1 1 \times 1 1×1),其感受野大小为 5 × 5 5 \times 5 5×5,如下图所示:

在这里插入图片描述

 若输入图像的尺寸大小是 7 × 7 7 \times 7 7×7,经过三次 3 × 3 3 \times 3 3×3的卷积核(其中stride=1,padding=0)后,其感受野大小为 7 × 7 7 \times 7 7×7,如下图所示:

在这里插入图片描述

(2)感受野的计算

  • 感受野大小的计算方式是从最后一层feature map开始,往下往上的计算方法,即先计算最深层在前一层上的感受野,然后以此类推逐层传递到第一层。
  • 感受野大小的计算不考虑padding的大小;
  • 最后一层的特征图感受野的大小等于其卷积核的大小(见上面示例图);
  • 第i层特征图的感受野大小和第i层的卷积核大小和步长有关系,同时也与第(i+1)层特征图的感受野大小有关。

感受野的计算公式:

在这里插入图片描述

(3)感受野的作用

  • 1、小尺寸的卷积代替大尺寸的卷积,可减少网络参数、增加网络深度、扩大感受野(例如:3 个 3 x 3 的卷积层的叠加可以替代7*7的卷积),网络深度越深感受野越大性能越好;
  • 2、对于分类任务,最后一层特征图的感受野大小要大于等于输入图像大小,否则分类性能会不理想;
  • 3、对于目标检测任务,若感受野很小,目标尺寸很大,或者目标尺寸很小,感受野很大,模型收敛困难,会严重影响检测性能;所以一般检测网络anchor的大小的获取都要依赖不同层的特征图,因为不同层次的特征图,其感受野大小不同,这样检测网络才会适应不同尺寸的目标。

2.2 特征图(特征映射)Feature map

在每个卷积层,数据都是以三维形式存在,可以看作许多个二维图像叠在一起,每一层称为feature map。

Feature = 二维图像 ∗ N = W ∗ H ∗ C = 长 ∗ 宽 ∗ 通道 *N = W*H*C=长*宽*通道 N=WHC=通道

  • 灰度图像:1通道,一个feature map
  • 彩色图像:3通道RGB,三个feature map

层与层之间会有若干个卷积核(kernel),设依次A层B层C层:

  • A层的feature map 1依次进行卷积操作,都会产生feature map 2,即为B层feature map;
  • feature map:该层卷积核的个数=产生的feature map个数(同上述);
  • 假如一种滤波器,也就是一种卷积核就是提出图像的一种特征,例如某个方向的边缘。那么需要提取不同的特征时,只需要加多几种滤波器。
  • 随着网络的加深,feature map的长宽尺寸缩小,本卷积层的每个map提取的特征越具有代表性。

 卷积网络在学习过程中保持了图像的空间结构,也就是说最后一层的激活值(feature map)总和原始图像具有空间上的对应关系,具体对应的位置以及大小,可以用感受野来度量。

可视化具体参考:如何理解特征图Feature map

2.3 CNN中的低维和高维

 我觉得这里的低维和高维,可以从feature map的channel数量也就是CNN filter数量去理解,一般我们的CNN网络都是filter数量越来越多的,而feature map经过下采样,尺寸越来越小,感受野越来越大。

  • 1、靠近输入层,也就是CNN网络的浅层,一般CNN filter数量少,维度低,feature map的尺寸大,分辨率高,感受野小,所以主要包含的是局部的细节特征
  • 2、靠近输出层,也就是CNN网络的深层,一般CNN filter数量多,维度高,feature map的尺寸小,抽象程度高,感受野大,所以主要包含的是全局的信息

2.4 1 × 1 1\times1 1×1卷积核

一文读懂卷积神经网络中的1x1卷积核

(1)降维/升维

 从卷积层流程图中可以清楚的看到卷积后的特征图通道数与卷积核的个数是相同的。所以,如果想要升维或降维,只需要通过修改卷积核的个数即可。

在这里插入图片描述

(2)增加非线性

 每使用 1 * 1的卷积核,及增加一层卷积层,所以网络深度得以增加。 而使用 1 * 1的卷积核后,可以保持特征图大小与输入尺寸相同,卷积层卷积过程会包含一个激活函数,从而增加了非线性。在输入尺寸不发生改变的情况下而增加了非线性,所以会增加整个网络的表达能力。

(3)参数量减少,降低计算量

在这里插入图片描述

  • 只使用32个192 * 5 * 5的卷积核,输入为192 * 28 * 28,输出为32 * 28 * 28,参数量为(192 * 5 * 5 + 1)* 32 = 153632
  • 先使用16个192 * 1 * 1的卷积核,输入为192 * 28 * 28,输出为16 * 28 * 28,参数量为(192 * 1 * 1 + 1)16 = 3088,再使用32个16 * 5 * 5的卷积核,参数量为(16 * 5 * 5 + 1) 32 = 12832,所以总数量为15920,参数量少了一个数量级。

(4)跨通道信息交互(通道的变换)

 1 * 1的卷积核一般只改变输出通道数(C),而不改变输出的宽度(W)和高度(H)。实现降维和升维的操作其实就是 Channel 间信息的线性组合变化。比如:在尺寸 3 * 3,64通道个数的卷积核后面添加一个尺寸1 * 1,28通道个数的卷积核,就变成了尺寸3 * 3,28尺寸的卷积核。 原来的64个通道就可以理解为跨通道线性组合变成了28通道,这就是通道间的信息交互。

三、网络细节

3.1 MLP Convolution Layers

 普通卷积层(卷积核大小大于1)即文中提到的GLM,如图(a)所示,相当于单层网络,抽象能力有限。

 而MLPConv层包含微网络(该文章使用的是多层感知器),如图(b)所示,MLPConv其实就是在常规卷积(卷积核大小大于1)后面接若干层若干个1x1卷积,这些1x1卷积如果看成神经元的话,这样就像是MLP(多层感知机)了,也就是说这里有一个微型的神经网络,而且还是全连接,用这个微型的神经网络来进行特征提取。 **这里的全连接操作实质上就等价于卷积核窗口为[1×1]的卷积操作。**因此,我们在实现这个网络的时候也不需要做任何的全连接操作,只需要做多次窗口大小为[1×1]的卷积即可,这也就相当于在整个特征图的角度做了一次全连接操作。

 作者还从跨通道组合的角度来解释了[1×1]卷积的合理性。因为[1×1]窗口的卷积核在执行卷积的过程中,实际上就是对不同通道上同一位置处的特征值进行了一次线性组合。因此,这就可以根据最终训练得到的这个[1×1]的卷积核权重参数来确定每个特征通道的重要性占比(有点注意力机制的味道),并进行融合形成一个通道。作者认为,这种做法的好处就是能够使得模型具有跨特征图交互的能力。

 在当时作者应该是第一个使用1x1卷积的,具有划时代的意义,之后的Googlenet借鉴了1*1卷积,还专门致谢过这篇论文,现在很多优秀的网络结构都离不开1x1卷积,ResNet、ResNext、SqueezeNet、MobileNetv1-3、ShuffleNetv1-2等等。1x1卷积作为NIN函数逼近器基本单元,除了增强了网络局部模块的抽象表达能力外,在现在看来, 还可以实现跨通道特征融合和通道升维降维。

在这里插入图片描述
在这里插入图片描述

 本文实际的NIN块(即MLPConv层)等价于普通卷积后跟两个1×1Conv(即MLP层),其中每个卷积操作后面都有ReLU进行非线性激活。

1、MLPConv的作用总结:

  • ①相当于在通道间做特征融合。
  • ②相当于每一层卷积之后加一个激活函数,增加了结构的非线性表达能力。
  • 可以将与MLP等价的1×1Conv+ReLU灵活运用到其他网络中(如GoogLeNet中的InceptionV1模块),后面的网络运用MLPConv时直接将之转化为1×1Conv+ReLU。

2、1×1卷积思想:

  • ①可以将1×1卷积看作是全连接层(将通道看成特征维,将宽和高维度上的样本当作是数据样本);
  • ②1×1卷积既可以增加、减少通道数,也可以使通道数量不变化。

3.2 Global Average Pooling(GAP)

 在最后一个MLPConv层中为分类任务的每个对应类别生成一个特征图,通过GAP层取每个特征图的空间平均值,将结果向量直接传入到softmax层,但是传统的做法都是在最后一层卷积层得到的特征图后面跟上FC层。

GAP思想:

① 前面的MLPConv层已经能提取出高维的有分类能力的特征了,不需要再多加几层FC层。可以把通道维看做特征维,宽度和长度的维度看做样本。那么GAP的本质就是把样本求均值映射到特征,因为最后一层的Feature Map数和类别数相同,那么也就是把样本映射到要分类的类别。

② FC层会综合前面卷积所提取所有的特征,可以把特征理解到更高的维度,来进行判别。也正是因为加上FC层,网络才更容易过拟合,因为FC层的理解力过于强大,它实际上是把所有可能考虑的情况都进行了考虑。怎么进行改进?因为我们前面用 MLPConv层已经理解到了更高维能够分类的特征,那么最后就可以不用FC层,直接用 GAP 来对每个 Feature Map 求均值就行,因为模型已经把提取能够分类的特征的任务重心放到了整个模型的前面(MLPConv),后面只用综合前面的结果做出最后的判断就行。

在这里插入图片描述

GAP较FC的优点:

  • 强制Feature Map和类别之间的对应关系,更适合卷积结构。在使用 GAP 时Feature Map数要等于要分类的类别数,然后再对每一个Feature Map求均值,送到softmax中。
  • ②GAP中没有要优化的参数,减少了参数量,避免了过拟合
  • GAP汇总了空间信息,对输入的空间转化更加稳定。
  • ④FC输入的大小必须固定,而GAP对网络输入的图像尺寸没有固定要求

GAP也有个缺点:训练的收敛速度会变慢

3.3 NIN网络结构

 这篇文章中整个NIN网络包括:三个堆叠的MLPConv层和一个全局平均池化(GAP)层,如下图所示:

在这里插入图片描述
在这里插入图片描述

四、论文复现

  NIN论文翻译及解读

  • ①使用Mnist数据集,并将图像resize到227x227的size,修改in_features=1,out_features=10;

  • ②使用猫狗大战数据集,并将图像resize到227x227的size,修改in_features=3,out_features=2;

问题:训练猫狗大战数据集时,模型精度上不去,可以调整最后几层block的out_features,比如把block4的out_features=384调成50,然后再去调整学习率;

4.1 MLP Convolution Layers实现

class NIN_BLOCK(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(NIN_BLOCK, self).__init__()
        # 常规卷积核
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
        
        # 两个1x1的卷积核
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=1)
        self.conv3 = nn.Conv2d(out_channels, out_channels, kernel_size=1)
    
    def forward(self, x):
        # 每个卷积核后面跟ReLU激活函数
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = self.conv3(x)
        return F.relu(x)

4.2 GAP层实现

class GlobalAvgPool2d(nn.Module):
	def __init__(self):
		super(GlobalAvgPool2d, self).__init__()
		## 如:
		# nn.AdaptiveAvgPool2d((6, 6))
		## 那么此函数会根据当前来到此处的 feature map 自动求 padding 和 stride,
		# 并输出(6,6)大小的 feature map,其实就是把wxh变成了6x6;
		## 所以,此函数也能用到全卷积中,用来自动调整步长以适应图像不同的输入

		# nn.AdaptiveAvgPool2d(要转化成的大小)
		self.GAP = nn.AdaptiveAvgPool2d((1, 1))	# 全局平均池化,就是这种写法;
	def forward(self, x):
		# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
		# return F.avg_pool2d(x, kernel_size=x.size()[2:])  # 第二种写法
		return self.GAP(x)

4.3 NIN网络总体实现

class NIN(nn.Module):
    def __init__(self):
        super(NIN, self).__init__()
        # 输入为227x227x1
        self.block1 = NIN_BLOCK(in_channels=1, out_channels=96, 
                                kernel_size=11, stride=4, padding=0)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2)  # 227*227*1 → 55*55*96 由以上两步共同完成
        
        self.block2 = NIN_BLOCK(in_channels=96, out_channels=256, 
                                kernel_size=5, stride=1, padding=2)
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2)  # 55*55*96 → 27*27*256
        
        self.block3 = NIN_BLOCK(in_channels=256, out_channels=384, 
                                kernel_size=3, stride=1, padding=1)
        self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2)  # 27*27*256 → 13*13*384
        
        self.block4 = NIN_BLOCK(in_channels=384, out_channels=10, 
                                kernel_size=3, stride=1, padding=1)
        self.GAP = GlobalAvgPool2d()
    
    def forward(self, x):
        x = self.block1(x)
        x = self.maxpool1(x)
        
        x = self.block2(x)
        x = self.maxpool2(x)
        
        x = self.block3(x)
        x = self.maxpool3(x)
        
        x = F.dropout(x, 0.5, training=self.training)
        x = self.block4(x)
        x = self.GAP(x)
        x = x.view(x.shape[0], -1)  # 将四维输出转换为二维输出,即形状为(批量大小,1000)
        
        return x

# 设置GPU训练
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 实例化模型对象
model = NIN()

# 把模型放到GPU上
model.to(device)

五、在GPU服务器上训练模型

 使用九天深度学习平台,数据集使用的是public里面的Fashion-Mnist数据集,pytorch自带的数据读取函数无法使用,于是参考了博客pytorch读取本地的mnist数据集,并且修改了部分代码,数据读取部分如下:

class DealDataset(Dataset):
    """
        读取数据、初始化数据
    """

    def __init__(self, folder, data_name, label_name, transform=None):
        (train_set, train_labels) = load_data(folder, data_name,
                                              label_name)  # 其实也可以直接使用torch.load(),读取之后的结果为torch.Tensor形式
        self.train_set = train_set
        self.train_labels = train_labels
        self.transform = transform

    def __getitem__(self, index):
        img, target = self.train_set[index], int(self.train_labels[index])
        # 将numpy.ndarray转为PIL
        img = Image.fromarray(img)
        if self.transform is not None:
            img = self.transform(img)
            
        return img, target

    def __len__(self):
        return len(self.train_set)


def load_data(data_folder, data_name, label_name):
    with gzip.open(os.path.join(data_folder, label_name), 'rb') as lbpath:  # rb表示的是读取二进制数据
        y_train = np.frombuffer(lbpath.read(), np.uint8, offset=8)

    with gzip.open(os.path.join(data_folder, data_name), 'rb') as imgpath:
        x_train = np.frombuffer(
            imgpath.read(), np.uint8, offset=16).reshape(len(y_train), 28, 28)
    return (x_train, y_train)
"""
标准化(Normalization)是神经网络对数据的一种经常性操作。
标准化处理指的是:样本减去它的均值,再除以它的标准差,最终样本将呈现均值为0,方差为1的数据分布。
神经网络模型偏爱标准化数据,原因是均值为0方差为1的数据在sigmoid、tanh经过激活函数后求导得到的导数很大,
反之原始数据不仅分布不均(噪声大)而且数值通常都很大(本例中数值范围是 0~255),激活函数后求导得到的导数则接近与0,这也被称为梯度消失。
所以说,数据的标准化有利于加快神经网络的训练。 
"""
pipline_train = transforms.Compose([
    #随机旋转图片
    transforms.RandomHorizontalFlip(),
    #将图片尺寸resize到227x227
    transforms.Resize((227,227)),
    #将图片转化为Tensor格式
    transforms.ToTensor(),
    #正则化(当模型出现过拟合的情况时,用来降低模型的复杂度)
    transforms.Normalize((0.1307,),(0.3081,))    
])

pipline_test = transforms.Compose([
    #将图片尺寸resize到227x227
    transforms.Resize((227,227)),
    transforms.ToTensor(),
    transforms.Normalize((0.1307,),(0.3081,))
])

train_batchsize = 64    # 训练集batch大小
test_batchsize = 32     # 测试集batch大小

#下载数据集
# train_set = datasets.MNIST(root="./data/mnist/", train=True, 
#                            download=False, transform=pipline_train)
# test_set = datasets.MNIST(root="./data/mnist/", train=False, 
#                           download=False, transform=pipline_test)

train_set = DealDataset('./data/mnist/MNIST/raw',
                           "train-images-idx3-ubyte.gz",
                           "train-labels-idx1-ubyte.gz",
                           transform=pipline_train)

test_set = DealDataset('./data/mnist/MNIST/raw',
                           "t10k-images-idx3-ubyte.gz",
                           "t10k-labels-idx1-ubyte.gz",
                           transform=pipline_train)


#dataset中的数据个数可能不是batch_size的整数倍,drop_last为True会将多出来不足一个的batch的数据丢弃
#加载数据集
trainloader = torch.utils.data.DataLoader(train_set, batch_size=train_batchsize, 
                                          shuffle=True, drop_last=False)
testloader = torch.utils.data.DataLoader(test_set, batch_size=test_batchsize, 
                                         shuffle=False, drop_last=False)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值