在CNN网络结构的演化上,出现过许多优秀的CNN网络,CNN的经典结构始于1998年的LeNet,成于2012年历史性的AlexNet,从此大盛于图像相关领域,主要包括:
发展历史:Lenet --> Alexnet --> ZFnet --> VGG --> NIN --> GoogLeNet --> ResNet --> DenseNet -->ResNeXt ---> EfficientNet
- LeNet,1998年
- AlexNet,2012年
- ZF-net,2013年
- GoogleNet,2014年
- VGG,2014年
- ResNet,2015年
目录
Lenet(1988):
广为流传的LeNet诞生于1998年,网络结构比较完整,包括卷积层、pooling层、全连接层,这些都是现代CNN网络的基本组件,其被认为是CNN的开端。主要用于识别10个手写邮政编码数字,5*5卷积核,stride=1,应用最大池化。
Lenet-5网络结构详解
闪光点:LeCun在1998年提出,定义了CNN的基本组件,是CNN的鼻祖。
自那时起,CNN的最基本的架构就定下来了:卷积层、池化层、全连接层。
LetNet-5 是一种入门级的神经网络模型,是一个简单的卷积神经网络,可以用来做手写体识别
含输入层总共8层网络,分别为:输入层(INPUT)、卷积层(Convolutions,C1)、池化层(Subsampling,S2)、C3、 S4、 C5、全连接层(F6)、输出层
LeNet-5跟现有的conv->pool->ReLU的套路不同,它使用的方式是conv1->pool->conv2->pool2再接全连接层,但是不变的是,卷积层后紧接池化层的模式依旧不变经过卷积后输出维度大小的公式:
N: 输入的维度、F:卷积核大小、stride: 步长、pad: 扩充边缘
output = (N + 2*pad -F)/stride+1
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchsummary import summary
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(x.size()[0], -1) #展开成一维的,x.size()[0] --> batch_size
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def show_lenet_strcucture():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#net = NiN(10).to(device) # 10 分类
net = LeNet().to(device) # 10 分类
print("net struct \n", net)
summary(net, (1, 32, 32)) #输入是灰度图
show_lenet_strcucture()
AlexNet(2012):
2012年Geoffrey和他学生Alex在ImageNet的竞赛中,刷新了image classification的记录,一举奠定了deep learning 在计算机视觉中的地位。这次竞赛中Alex所用的结构就被称为作为AlexNet。
Alexnet网络结构详解
上图 Alexnet的整个网络结构是由5个卷积层和3个全连接层组成的,深度总共8层。AlexNet针对的是1000类的分类问题,输入图片规定是256×256的三通道彩色图片,为了增强模型的泛化能力,避免过拟合,作者使用了随机裁剪的思路对原来256×256的图像进行随机裁剪,得到尺寸为3×224×224的图像,输入到网络训练。
注意:输入Input的图像规格: 224X224X3(RGB图像),实际上会经过预处理变为227X227X3。因为 (224-11)/4+1 ≠ 55,所以这里是做了padding再做卷积的,即先padiing图像至227×227,再做卷积(227-11)/4+1 = 55AlexNet将CNN用到了更深更宽的网络中,其效果分类的精度更高相比于以前的LeNet 用到的trick:
- 数据增广技巧来增加模型泛化能力 256×256×3 -->随机裁剪224×224×3 -->进入网络。
- 非线性激活函数ReLU:AlexNet使用ReLU代替了Sigmoid,其能更快的训练,同时解决sigmoid在训练较深的网络中出现的梯度消失。
- 防止过拟合的方法:Dropout,Data augmentation等。Dropout随机失活,随机忽略一些神经元,以避免过拟合。
- 以前的CNN中普遍使用平均池化层,AlexNet全部使用最大池化层,避免了平均池化层的模糊化的效果,并且步长比池化的核的尺寸小,这样池化层的输出之间有重叠,提升了特征的丰富性。
- 提出了LRN层,局部响应归一化增加了泛化能力,做了平滑处理,提高了1%~2%的识别率, LRN 对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元。
- 使用多个GPU,LRN归一化层。其主要的优势有:网络扩大(5个卷积层+3个全连接层+1个softmax层);解决过拟合问题(dropout,data augmentation,LRN);多GPU加速计算。
ZF-Net
ZFNet是2013ImageNet分类任务的冠军,其网络结构没什么改进,只是调了调参,性能较Alex提升了不少。
ZF-Net 网络解析
ZF-Net只是将AlexNet第一层卷积核由11变成7,步长由4变为2,第3,4,5卷积层转变为384,384,256。这一年的ImageNet还是比较平静的一届,其冠军ZF-Net的名堂也没其他届的经典网络架构响亮。
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchsummary import summary
class ZFNet(nn.Module):
def __init__(self):
super(ZFNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=96,
kernel_size=7, stride=2, padding=1) # [-1, 96, 110, 110]
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # [-1, 96, 55, 55]
self.conv2 = nn.Conv2d(in_channels=96, out_channels=256,
kernel_size=5, stride=2) # [-1, 256, 26, 26]
self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) #[-1, 256, 13, 13]
self.conv3 = nn.Conv2d(in_channels=256, out_channels=384,
kernel_size=3, stride=1, padding=1)#[-1, 384, 13, 13]
self.conv4 = nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1)#[-1, 384, 13, 13]
self.conv5 = nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1)#[-1, 256, 13, 13]
self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2)#[-1, 256, 6, 6]
self.fc1 = nn.Linear(in_features=6 * 6 * 256, out_features=4096)#[-1, 4096]
self.fc2 = nn.Linear(in_features=4096, out_features=4096)#[-1, 4096]
self.fc3 = nn.Linear(in_features=4096, out_features=1000)#[-1, 1000]
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.maxpool1(x)
x = F.relu(self.conv2(x))
x = self.maxpool2(x)
x = F.relu(self.conv3(x))
x = F.relu(self.conv4(x))
x = self.maxpool3(F.relu(self.conv5(x)))
x = x.view(-1, 6 * 6 * 256)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def show_zfnet_strcucture():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
net = ZFNet().to(device) # 1000 分类
print("net struct \n", net)
summary(net, (3, 224, 224)) #输入为224 X 224
show_zfnet_strcucture()
VGG-Net(2014):
VGG-Net来自 Andrew Zisserman 教授的组 (Oxford),在2014年的 ILSVRC localization and classification 两个问题上分别取得了第一名和第二名,其不同于AlexNet的地方是:VGG-Net使用更多的层,通常有16/19层,而AlexNet只有8层。同时,VGG-Net的所有 convolutional layer 使用同样大小的 convolutional filter,大小为 3 x 3。总体来说,其由5层卷积层、3层全连接层、softmax输出层构成。用了3*3的卷积核(c模型用了1*1的卷积核,但一般都只用d、e模型,1*1不必管它),步长stride=1,padding=1,max池化,pooling窗口为2*2,pooling步长为2
VGG16网络结构详解
VGG系列网络使用的是3x3的小卷积核,这是有中心和上下左右的最小单元(中心+八邻域);两个3x3的卷积层连在一起可视为5x5的filter,三个连在一起可视为一个7x7的。和大卷积核相比:(1)可以减少参数(2)进行了更多的非线性映射,增加网络的拟合、表达能力。
VGG16模型(2+2+3+3+3个卷积层,3个全连接层):
VGG19模型:
![](https://i-blog.csdnimg.cn/blog_migrate/cafd5c729c1582795f33fd2a37fee9b6.png)
VGG16 是在 AlexNet 网络上每一层进行了改造,5个 stage 对应 AlexNet 中的5层卷积,3层全连接仍然不变,图片输入的大小还是沿用了 224x224x3。结构上的变化:
- 共16层(不包括Max pooling层和softmax层)
- 所有的卷积核都使用3*3的大小,max pooling池化核都使用大小为2*2
- 采用步长stride=2,padding=0的Max pooling
- 卷积层深度依次为64 -> 128 -> 256 -> 512 ->512
VGGNet改进点总结:
- 使用了更小的3*3卷积核,和更深的网络。两个3*3卷积核的堆叠相对于5*5卷积核的视野,三个3*3卷积核的堆叠相当于7*7卷积核的视野。 (好处:有更少的参数 3个堆叠的3*3结构只有7*7结构参数
数量的(3*3*3)/(7*7)=55% ) 另一方面拥有更多的非线性变换,增加了CNN对特征的学习能力。- VGG采用的是一种Pre-training的方式,先训练级别简单(层数较浅)的VGGNet的A级网络,然后使用A网络的权重来初始化后面的复杂模型,加快训练的收敛速度。
- 采用了Multi-Scale的方法来训练和预测。可以增加训练的数据量,防止模型过拟合,提升预测准确率
# -*- coding:utf-8 -*-
import torch
from torchsummary import summary
#pip install torchsummary
from torchvision import models
def show_nets_strcucture():
#[resnet, alexnet, vgg, squeezenet, densenet]标准输入为 224*224*3, inception_v3 标准输入为 299*299*3
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# pretrained=True 会下载在 C:\Users\F7687778/.cache\torch\checkpoints\alexnet-owt-4df8aa71.pth
vgg16 = models.vgg16(pretrained=False).to(device)# 查看网络结构其实设置 pretrained=False 就可以了
print("net struct \n", vgg16)
summary(vgg16, (3, 224, 224))
show_nets_strcucture()
'''
net struct
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
#ps:不管之前的特征图尺寸为多少,只要设置为(7, 7),那么最终特征图大小都为(7, 7)
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 64, 224, 224] 1,792
ReLU-2 [-1, 64, 224, 224] 0
Conv2d-3 [-1, 64, 224, 224] 36,928
ReLU-4 [-1, 64, 224, 224] 0
MaxPool2d-5 [-1, 64, 112, 112] 0
Conv2d-6 [-1, 128, 112, 112] 73,856
ReLU-7 [-1, 128, 112, 112] 0
Conv2d-8 [-1, 128, 112, 112] 147,584
ReLU-9 [-1, 128, 112, 112] 0
MaxPool2d-10 [-1, 128, 56, 56] 0
Conv2d-11 [-1, 256, 56, 56] 295,168
ReLU-12 [-1, 256, 56, 56] 0
Conv2d-13 [-1, 256, 56, 56] 590,080
ReLU-14 [-1, 256, 56, 56] 0
Conv2d-15 [-1, 256, 56, 56] 590,080
ReLU-16 [-1, 256, 56, 56] 0
MaxPool2d-17 [-1, 256, 28, 28] 0
Conv2d-18 [-1, 512, 28, 28] 1,180,160
ReLU-19 [-1, 512, 28, 28] 0
Conv2d-20 [-1, 512, 28, 28] 2,359,808
ReLU-21 [-1, 512, 28, 28] 0
Conv2d-22 [-1, 512, 28, 28] 2,359,808
ReLU-23 [-1, 512, 28, 28] 0
MaxPool2d-24 [-1, 512, 14, 14] 0
Conv2d-25 [-1, 512, 14, 14] 2,359,808
ReLU-26 [-1, 512, 14, 14] 0
Conv2d-27 [-1, 512, 14, 14] 2,359,808
ReLU-28 [-1, 512, 14, 14] 0
Conv2d-29 [-1, 512, 14, 14] 2,359,808
ReLU-30 [-1, 512, 14, 14] 0
MaxPool2d-31 [-1, 512, 7, 7] 0
AdaptiveAvgPool2d-32 [-1, 512, 7, 7] 0
Linear-33 [-1, 4096] 102,764,544
ReLU-34 [-1, 4096] 0
Dropout-35 [-1, 4096] 0
Linear-36 [-1, 4096] 16,781,312
ReLU-37 [-1, 4096] 0
Dropout-38 [-1, 4096] 0
Linear-39 [-1, 1000] 4,097,000
================================================================
'''
NIN网络-Network In Network
Network In Network 是发表于 2014 年 ICLR 的一篇 paper。当前被引了 3298 次。这篇文章采用较少参数就取得了 Alexnet 的效果,Alexnet 参数大小为 230M,而 Network In Network 仅为 29M,这篇 paper 主要两大亮点:mlpconv (multilayer perceptron,MLP,多层感知机) 和Global Average Pooling(全局平均池化)。
NIN网络结构详解
第一个卷积核是11x11x3x96,因此在一个patch块上卷积的输出是1x1x96的feature map(一个96维的向量)。在其后又接了一个MLP层,输出仍然是96。因此这个MLP层就等价于一个1 x 1 的卷积层
mlp conv另一种理解方式:在原来每一层输出后加一个 与通道数量相同1 x 1 的卷积层。mlp conv层相当于先进行一次普通的卷积(比如3x3),紧跟再进行一次1x1的卷积。见上图MLPCONV与CNN对比。作用:
1. 其实相当于在通道之间做了特征融合。
2. 每一层卷积之后加一个激活函数,比原结构多了一层激活函数,增加了结构的非线性表达能力。
Global Average Pooling
整个featuremap平均池化结果作为softmax 输入,相较于Alexnet全连接的优点:
1. 减少参数量(1000x1000+1000x6x6),全局平均池化没有参数,从而减轻过拟合。
2. 求和平均综合了整个featuremap的所有信息,全局平均池可以对空间信息进行汇总,因此对输入的空间转换具有更强的鲁棒性。
3. 不限输入图片的大小。
#NiN 网络由三个 mlpconv 块组成,最后一个 mlpconv 后使用全局平均池化。
#用 Pytorch 代码实现如下:假设输入是 32×32×3 (即 3 通道,32 高宽大小的图片)
import torch
import torch.nn as nn
from torchsummary import summary
class NiN(nn.Module):
def __init__(self, num_classes):
super(NiN, self).__init__()
self.num_classes = num_classes
self.classifier = nn.Sequential(
nn.Conv2d(3, 192, kernel_size=5, stride=1, padding=2),
nn.ReLU(inplace=True),
nn.Conv2d(192, 160, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Conv2d(160, 96, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
nn.Dropout(0.5),
nn.Conv2d(96, 192, kernel_size=5, stride=1, padding=2),
nn.ReLU(inplace=True),
nn.Conv2d(192, 192, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Conv2d(192, 192, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.AvgPool2d(kernel_size=3, stride=2, padding=1),
nn.Dropout(0.5),
nn.Conv2d(192, 192, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(192, 192, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
nn.Conv2d(192, 10, kernel_size=1, stride=1, padding=0),
nn.ReLU(inplace=True),
#nn.AvgPool2d(kernel_size=8, stride=1, padding=0),
#相比 nn.AvgPool2d() 多了个自适应,自适应就代表了使用更简单方便
nn.AdaptiveAvgPool2d(output_size=(1,1)),
)
def forward(self, x):
x = self.classifier(x)
logits = x.view(x.size(0), self.num_classes)
probas = torch.softmax(logits, dim=1)
return logits, probas
def show_nin_strcucture():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
net = NiN(10).to(device) # 10 分类
print("net struct \n", net)
summary(net, (3, 32, 32)) #最后 AdaptiveAvgPool2d-23 --> [-1, 10, 1, 1]
show_nin_strcucture()
GoogLeNet:
提出的Inception结构是主要的创新点,这是(Network In Network)的结构,即原来的结点也是一个网络。其使用使得之后整个网络结构的宽度和深度都可扩大,能够带来2-3倍的性能提升。
GoogLeNet 网络结构解析
神经网络除了纵向的扩展,是否能进行横向的拓展? 提升网络性能最直接的办法就是增加网络深度和宽度,深度指网络层次数量、宽度指神经元数量。
GoogLeNet在2014的ImageNet分类任务上击败了VGG-Nets夺得冠军,GoogLeNet跟AlexNet,VGG-Nets这种单纯依靠加深网络结构进而改进网络性能的思路不一样,它另辟幽径,在加深网络的同时(22层),也在网络结构上做了创新,引入Inception结构代替了单纯的卷积+激活的传统操作(这思路最早由Network in Network提出)
闪光点:
- 引入Inception结构
- 中间层的辅助LOSS单元
- 后面的全连接层全部替换为简单的全局平均pooling
GoogLeNet中的基础卷积块叫作Inception块,得名于同名电影《盗梦空间》 (Inception)
通过设计一个稀疏网络结构,但是能够产生稠密的数据,既能增加神经网络表现,又能保证计算资源的使用效率。一方面增加了网络的宽度,另一方面也增加了网络对尺度的适应性。
基于Inception构建了GoogLeNet的网络结构如下(共22层)
inception的结构,一分四,然后做一些不同大小的卷积,之后再堆叠feature map。残差网络做了相加的操作,而inception做了串联的操作。
![](https://i-blog.csdnimg.cn/blog_migrate/98a199c9473c3d3b56614dd1d43c10b4.png)
对上图做以下说明:
- 采用不同大小的卷积核意味着不同大小的感受野,最后拼接意味着不同尺度特征的融合;
- 之所以卷积核大小采用1、3和5,主要是为了方便对齐。设定卷积步长stride=1之后,只要分别设定pad=0、1、2,那么卷积之后便可以得到相同维度的特征,方便最后拼接在一起 out =((N + 2p -L)/s) +1
- 文章说很多地方都表明pooling挺有效,所以Inception里面也嵌入了。
- 网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3和5x5卷积的比例也要增加,以便能尽量保留信息。
但是,使用5x5的卷积核仍然会带来巨大的计算量。 文章借鉴NIN2,采用1x1卷积核来进行降维。
1*1卷积核的主要作用:(1*1卷积核并不改变特征图的二维维度,但是可以重新定义特征图的深度)
- 降维
- 使网络更深
- 增加RELU等非线性(图中每一个a*a卷积都是conv+BN+relu的操作)。
例如:上一层的输出为100x100x128,经过具有256个输出的5x5卷积层之后(stride=1,pad=2),输出数据为100x100x256。其中,卷积层的参数为128x5x5x256。假如上一层输出先经过具有32个输出的
1x1卷积层,再经过具有256个输出的5x5卷积层,那么最终的输出数据仍为为100x100x256,但卷积参数量已经减少为128x1x1x32 + 32x5x5x256,大约减少了4倍。
Googlenet的核心思想是inception,通过不垂直堆砌层的方法得到更深的网络(我的理解是变宽且视野范围种类多,vgg及resnet让网络变深,inception让网络变宽,在同一层整合不同感受野的信息,并让模型自己选择卷积核的大小)
对上图说明如下:
- GoogLeNet采用了模块化的结构(Inception结构),方便增添和修改;
- 网络最后采用了average pooling(平均池化)来代替全连接层,该想法来自NIN(Network in Network)事实证明这样可以将准确率提高0.6%。但是,实际在最后还是加了一个全连接层,主要是为了方便对输出进行灵活调整;
- 虽然移除了全连接,但是网络中依然使用了Dropout ;
- 为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度(辅助分类器)。辅助分类器是将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个网络的训练很有裨益。而在实际测试的时候,这两个额外的softmax会被去掉。
GoogLeNet Inception V2
Inception V2版本的解决方案就是修改Inception的内部计算逻辑,提出了比较特殊的“卷积”计算结构
先池化再作Inception卷积,或者先作Inception卷积再作池化。但是方法一(左图)先作pooling(池化)会导致特征表示遇到瓶颈(特征缺失),方法二(右图)是正常的缩小,但计算量很大。为了同时保持特征表示且降低计算量,将网络结构改为下图,使用两个并行化的模块来降低计算量(卷积、池化并行执行,再进行合并)
使用Inception V2作改进版的GoogLeNet,网络结构图如下:
注:上表中的Figure 5指没有进化的Inception,Figure 6是指小卷积版的Inception(用3x3卷积核代替5x5卷积核),Figure 7是指不对称版的Inception(用1xn、nx1卷积核代替nxn卷积核)。
V2优点:
1. 学习VGGNet的特点,用两个3*3卷积代替5*5卷积,降低参数量。
2. 提出了著名的Batch Normalization(BN)方法,BN算法是一个正则化方法,可以提高大网络的收敛速度。BN算法:就是对输入层信息分布标准化处理,使得规范化为N(0,1)的高斯分布,收敛速度大大提高。
GoogLeNet Inception V3
RMSProp优化器
Inception V3一个最重要的改进是分解(Factorization),将7x7分解成两个一维的卷积(1x7,7x1),3x3也是一样(1x3,3x1),这样的好处,既可以加速计算,又可以将1个卷积拆成2个卷积,使得网络深度进一步增加,增加了网络的非线性(每增加一层都要进行ReLU)。另外,网络输入从224x224变为了299x299。
GoogLeNet Inception V4
Inception V4研究了Inception模块与残差连接的结合。ResNet结构大大地加深了网络深度,还极大地提升了训练速度,同时性能也有提升。Inception V4主要利用残差连接(Residual Connection)来改进V3结构,得到Inception-ResNet-v1,Inception-ResNet-v2,Inception-v4网络。
V1提出inception结构,到v2添加了BN层,到V3使用分解卷积,到V4和残差网络结合。
ResNet(2015)(里程碑式创新):
2015年何恺明推出的ResNet在ISLVRC和COCO上横扫所有选手,获得冠军。ResNet在网络结构上做了大创新,而不再是简单的堆积层数,ResNet在卷积神经网络的新思路,绝对是深度学习发展历程上里程碑式的事件。
ResNet提出了一种减轻网络训练负担的残差学习框架,这种网络比以前使用过的网络本质上层次更深。其明确地将这层作为输入层相关的学习残差函数,而不是学习未知的函数。在ImageNet数据集用152 层(据说层数已经超过1000==)——比VGG网络深8倍的深度来评估残差网络,但它仍具有较低的复杂度。在2015年大规模视觉识别挑战赛分类任务中赢得了第一。
设计了“bottleneck”形式的block(有跨越几层的直连)用全局平均池化GAP代替全连层FC,解决全连接层参数冗余的问题,但FC的优势在于在迁移学习中可改善微调的效果。
闪光点:
- 层数非常深,已经超过百层(梯度消失/梯度爆炸)
- 引入残差单元来解决退化问题(Degradation):即准确率会先上升然后达到饱和,再持续加深网络则会导致准确率下降
随着网络深度增加,网络的准确度应该同步增加,当然要注意过拟合问题。但是网络深度增加的一个问题在于这些增加的层是参数更新的信号,因为梯度是从后向前传播的,增加网络深度后,比较靠前的层梯度会很小。这意味着这些层基本上学习停滞了,这就是梯度消失问题。
深度网络的第二个问题在于训练,当网络更深时意味着参数空间更大,优化问题变得更难,因此简单地去增加网络深度反而出现更高的训练误差,深层网络虽然收敛了,但网络却开始退化了,即增加网络层数却导致更大的误差,比如下图,一个56层的网络的性能却不如20层的性能好,这不是因为过拟合(训练集训练误差依然很高),这就是烦人的退化问题。残差网络ResNet设计一种残差模块让我们可以训练更深的网络。
分析一下残差单元来理解ResNet的精髓:增加一个identity mapping(恒等映射),将原始所需学的函数H(x)转换成F(x)+x,然而优化F(x)会比H(x) 简单很多
从上图可以看出,数据经过了两条路线,一条是常规路线,另一条则是捷径(shortcut),直接实现单位映射的直接连接的路线,这有点类似与电路中的“短路”。通过实验,这种带有shortcut的结构确实可以很好地应对退化问题。我们把网络中的一个模块的输入和输出关系看作是y=H(x),那么直接通过梯度方法求H(x)就会遇到上面提到的退化问题,如果使用了这种带shortcut的结构,那么可变参数部分的优化目标就不再是H(x),若用F(x)来代表需要优化的部分的话,则H(x)=F(x)+x,也就是F(x)=H(x)-x 因为在单位映射的假设中y=x就相当于观测值,所以F(x)就对应着残差,因而叫残差网络。
为啥要这样做,因为作者认为学习残差F(X)比直接学习H(X)简单!设想下,现在我们只需要去学习输出和输入的差值(H(x)-x 即残差), 不再是学习一个完整的输出H(x),优化起来简单很多。
考虑到x的维度与F(X)维度可能不匹配情况,需进行维度匹配。这里论文中采用两种方法解决这一问题
- zero_padding:对恒等层进行0填充的方式将维度补充完整。这种方法不会增加额外的参数
- projection:在恒等层采用1x1的卷积核来增加维度。这种方法会增加额外的参数
import torch
import torch.nn as nn
import torchvision
import numpy as np
from torchsummary import summary
print("PyTorch Version: ",torch.__version__)
print("Torchvision Version: ",torchvision.__version__)
#https://www.cnblogs.com/wzyuan/p/9880342.html
__all__ = ['ResNet50', 'ResNet101','ResNet152']
def Conv1(in_planes, places, stride=2):
return nn.Sequential(
nn.Conv2d(in_channels=in_planes,out_channels=places,kernel_size=7,
stride=stride,padding=3, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
class Bottleneck(nn.Module):
def __init__(self,in_places,places, stride=1,downsampling=False, expansion = 4):
super(Bottleneck,self).__init__()
self.expansion = expansion
self.downsampling = downsampling
self.bottleneck = nn.Sequential(
nn.Conv2d(in_channels=in_places,out_channels=places,kernel_size=1,stride=1, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=places, out_channels=places, kernel_size=3,
stride=stride, padding=1, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=places, out_channels=places*self.expansion,
kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(places*self.expansion),
)
if self.downsampling: #输出与输入通道数都不一样 就要走这一步
self.downsample = nn.Sequential(
nn.Conv2d(in_channels=in_places, out_channels=places*self.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(places*self.expansion)
)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
residual = x
out = self.bottleneck(x)
if self.downsampling:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self,blocks, num_classes=1000, expansion = 4):
super(ResNet,self).__init__()
self.expansion = expansion
self.conv1 = Conv1(in_planes = 3, places= 64) # [-1, 64, 112, 112]
self.layer1 = self.make_layer(in_places = 64, places= 64, block=blocks[0], stride=1)
self.layer2 = self.make_layer(in_places = 256,places=128, block=blocks[1], stride=2)
self.layer3 = self.make_layer(in_places=512,places=256, block=blocks[2], stride=2)
self.layer4 = self.make_layer(in_places=1024,places=512, block=blocks[3], stride=2)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(2048,num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def make_layer(self, in_places, places, block, stride):
layers = []
#每一层都有downsample,因为输出与输入通道数都不一样(每一层中的第一层 downsampling =True)
layers.append(Bottleneck(in_places, places,stride, downsampling =True))
for i in range(1, block):
layers.append(Bottleneck(places*self.expansion, places))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def ResNet50():
return ResNet([3, 4, 6, 3])
def ResNet101():
return ResNet([3, 4, 23, 3])
def ResNet152():
return ResNet([3, 8, 36, 3])
def show_resnet_strcucture():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
net = ResNet50().to(device) # 1000 分类
print("net struct \n", net)
summary(net, (3, 224, 224))
show_resnet_strcucture()
#downsampling resnet50,每一层都有downsample,因为输出与输入通道数都不一样
DenseNet
2017最佳论文DenseNet,主要还是和ResNet及Inception网络做对比,思想上有借鉴,但却是全新的结构,网络结构并不复杂,却非常有效,在CIFAR指标上全面超越ResNet。可以说DenseNet吸收了ResNet最精华的部分,并在此上做了更加创新的工作,使得网络性能进一步提升。
DenseNet是借鉴了ResNet,是ResNet的升级版,从上述ResNet可以看到,一般每个Block会有一个skip connect,而DenseNet会在每层conv间有一个skip connect
闪光点:
- 密集连接:缓解梯度消失问题,加强特征传播,极大的减少了参数量
需要明确一点,dense connectivity 仅仅是在一个dense block里的,不同dense block 之间是没有dense connectivity的
在同层深度下获得更好的收敛率,自然是有额外代价的。其代价之一,就是训练需要更多内存
densenet就同时做了两件事情,一是将网络中的每一层都直接与其前面层相连,提高特征的利用率;二是把网络的每一层设计得很窄,也就是卷积的输出通道数通常很小,只有几十,该层学习非常少的特征图并与输入concat使用。
import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import OrderedDict
class _DenseLayer(nn.Sequential):
def __init__(self, num_input_features, growth_rate, bn_size, drop_rate):
super(_DenseLayer, self).__init__()
self.add_module('norm1', nn.BatchNorm2d(num_input_features)),
self.add_module('relu1', nn.ReLU(inplace=True)),
self.add_module('conv1', nn.Conv2d(num_input_features, bn_size *
growth_rate, kernel_size=1, stride=1, bias=False)),
self.add_module('norm2', nn.BatchNorm2d(bn_size * growth_rate)),
self.add_module('relu2', nn.ReLU(inplace=True)),
self.add_module('conv2', nn.Conv2d(bn_size * growth_rate, growth_rate,
kernel_size=3, stride=1, padding=1, bias=False)),
self.drop_rate = drop_rate
def forward(self, x):
new_features = super(_DenseLayer, self).forward(x)
if self.drop_rate > 0:
new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
return torch.cat([x, new_features], 1)
class _DenseBlock(nn.Sequential):
def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate):
super(_DenseBlock, self).__init__()
for i in range(num_layers):
layer = _DenseLayer(num_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)
self.add_module('denselayer%d' % (i + 1), layer)
class _Transition(nn.Sequential):
def __init__(self, num_input_features, num_output_features):
super(_Transition, self).__init__()
self.add_module('norm', nn.BatchNorm2d(num_input_features))
self.add_module('relu', nn.ReLU(inplace=True))
self.add_module('conv', nn.Conv2d(num_input_features, num_output_features,
kernel_size=1, stride=1, bias=False))
self.add_module('pool', nn.AvgPool2d(kernel_size=2, stride=2))
class DenseNet(nn.Module):
def __init__(self, growth_rate=32, block_config=(6, 12, 24, 16),
num_init_features=64, bn_size=4, drop_rate=0, num_classes=1000):
super(DenseNet, self).__init__()
# First convolution
self.features = nn.Sequential(OrderedDict([
('conv0', nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False)),
('norm0', nn.BatchNorm2d(num_init_features)),
('relu0', nn.ReLU(inplace=True)),
('pool0', nn.MaxPool2d(kernel_size=3, stride=2, padding=1)),
]))
# Each denseblock
num_features = num_init_features
for i, num_layers in enumerate(block_config):
block = _DenseBlock(num_layers=num_layers, num_input_features=num_features,
bn_size=bn_size, growth_rate=growth_rate, drop_rate=drop_rate)
self.features.add_module('denseblock%d' % (i + 1), block)
num_features = num_features + num_layers * growth_rate
if i != len(block_config) - 1:
trans = _Transition(num_input_features=num_features, num_output_features=num_features // 2)
self.features.add_module('transition%d' % (i + 1), trans)
num_features = num_features // 2
# Final batch norm
self.features.add_module('norm5', nn.BatchNorm2d(num_features))
# Linear layer
self.classifier = nn.Linear(num_features, num_classes)
# Official init from torch repo.
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal(m.weight.data)
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
m.bias.data.zero_()
def forward(self, x):
features = self.features(x)
out = F.relu(features, inplace=True)
out = F.avg_pool2d(out, kernel_size=7, stride=1).view(features.size(0), -1)
out = self.classifier(out)
return out
def densenet121(**kwargs):
model = DenseNet(num_init_features=64, growth_rate=32, block_config=(6, 12, 24, 16), **kwargs)
return model
def densenet169(**kwargs):
model = DenseNet(num_init_features=64, growth_rate=32, block_config=(6, 12, 32, 32), **kwargs)
return model
def densenet201(**kwargs):
model = DenseNet(num_init_features=64, growth_rate=32, block_config=(6, 12, 48, 32), **kwargs)
return model
def densenet161(**kwargs):
model = DenseNet(num_init_features=96, growth_rate=48, block_config=(6, 12, 36, 24), **kwargs)
return model
if __name__ == '__main__':
# 'DenseNet', 'densenet121', 'densenet169', 'densenet201', 'densenet161'
# Example
net = DenseNet()
print(net)
总结
![](https://i-blog.csdnimg.cn/blog_migrate/d71294b657b5fd200a3408222597ec4b.png)
2016年ILSVRC的Top5-error已降到3%以下,不过主要采用的ensemble方法,相比前几年,出现较大革新方法的脚步有所放缓了。
REFERENCE
https://zhuanlan.zhihu.com/p/192835137
http://ziyubiti.github.io/2016/11/27/cnnnet/
https://blog.csdn.net/weixin_41819299/article/details/81115365