经典深度卷积神经网络模型原理与实现
卷积神经网络(Convolutional Neural Network,CNN)最初是为解决图像识别等问题设计的,在早期的图像识别研究中,最大的挑战是如何组织特征,因为图像数据不像其他类型的数据那样可以通过人工理解来提取特征。卷积神经网络相比传统的机器学习算法,无须手工提取特征,也不需要使用诸如SIFT之类的特征提取算法,可以在训练中自动完成特征的提取和抽象,并同时进行模式分类,大大降低了应用图像识别的难度;相比一般的神经网络,CNN在结构上和图片的空间结构更为贴近,都是2D的有联系的结构,并且CNN的卷积连接方式和人的视觉神经处理光信号的方式类似。
卷积和池化的随机组合赋予了CNN很大的灵活性,因此也诞生了很多耳熟能详的经典网络:LeNet,AlexNet,VGGNet,NiN,Google Inception Net,ResNet,DenseNet这几种网络在深度和复杂度方面依次递增。下面将分别介绍这几种网络原理,架构以及实现(Pytorch实现)。
LeNet
LeNet诞生于1994年,是最早的深层卷积神经网络之一,并且推动了深度学习的发展。从1988年开始,在多次成功的迭代后,这项由Yann LeCun完成的开拓性成果被命名为LeNet5。它是第一个成功大规模应用在手写数字识别问题的卷积神经网络,在MNIST数据集中的正确率可以高达99.2%。
下面详细介绍一下LeNet-5模型工作的原理。
LeNet-5网络是针对灰度图进行训练的,输入图像大小为\(32*32*1\),不包含输入层的情况下共有7层,每层都包含可训练参数(连接权重),如下:
- C1层是一个卷积层(通过卷积运算,可以使原信号特征增强,并且降低噪音)。第一层使用\(5*5\)大小的过滤器6个,步长s = 1,padding = 0,输出得到的特征图大小为\(28*28*6\),一共有156个可训练参数(每个滤波器\(5*5=25\)个unit参数和1个bias参数,一共6个滤波器,共\((5*5+1)*6=156\)个参数),共\(156*(28*28)=122,304\)个连接。
- S2层是一个下采样层(平均池化层)(利用图像局部相关性的原理,对图像进行子抽样,可以1.减少数据处理量同时保留有用信息,2.降低网络训练参数及模型的过拟合程度)。第二层使用\(2*2\)大小的过滤器,步长s = 2,padding = 0,输出得到的特征图大小为14146。池化层只有一组超参数 f 和 s,没有需要学习的参数。
- C3层是一个卷积层。第三层使用\(5*5\)大小的过滤器16个,步长s = 1,padding = 0,输出得到的特征图大小为\(10*10*16\)。C3有\(416\)个可训练参数。
- S4层是一个下采样层(平均池化层)。第四层使用\(2*2\)大小的过滤器,步长s = 2,padding = 0,输出得到的特征图大小为\(5*5*16\)。
- F5层是一个全连接层,有120个单元。有120*(400+1)=48120个可训练参数。
- F6层是一个全连接层,有84个单元,有84*(120+1)=10164个可训练参数。
- Output输出层
随着网络越来越深,图像的宽度和高度都在缩小,信道数量一直在增加。目前,一个或多个卷积层后边跟一个池化层,再接上一个全连接层的排列方式很常用。
下面是就基于pytorch的网络实现:
# LeNet
import torch
import torch.nn as nn
import torch.optim as optim
import time
#net
class Flatten(torch.nn.Module): #展平操作
def forward(self, x):
return x.view(x.shape[0], -1)
class Reshape(torch.nn.Module): #将图像大小重定型
def forward(self, x):
return x.view(-1,1,32,32) #(B x C x H x W) 通道数在第二维度
net = torch.nn.Sequential(
Reshape(),
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1), #b*1*32*32 =>b*6*28*28
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*6*28*28 =>b*6*14*14
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), #b*6*14*14 =>b*16*10*10
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), #b*16*10*10 => b*16*5*5
Flatten(), #b*16*5*5 => b*400
nn.Linear(in_features=16*5*5, out_features=120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
X = torch.randn(size=(1,1,32,32), dtype = torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape: \t',X.shape)
AlexNet
AlexNet是现代深度CNN的奠基之作。AlexNet以显著的优势赢得了竞争激烈的ILSVRC 2012比赛,top-5的错误率降低至了16.4%,相比第二名的成绩26.2%错误率有了巨大的提升,确立了深度学习(深度卷积网络)在计算机视觉的统治地位,同时也推动了深度学习在语音识别、自然语言处理、强化学习等领域的拓展。
这幅图分为上下两个部分的网络,论文中提到这两部分网络是分别对应两个GPU,只有到了特定的网络层后才需要两块GPU进行交互,这种设置完全是利用两块GPU来提高运算的效率,其实在网络结构上差异不是很大。为了更方便的理解,我们假设现在只有一块GPU或者我们用CPU进行运算,我们从这个稍微简化点的方向区分析这个网络结构。网络总共的层数为8层,5层卷积,3层全连接层:
- 卷积层C1。,输入为\(224×224×3\)的图像,卷积核的数量为96,论文中两片GPU分别计算48个核; 卷积核的大小为\(11×11×3\);stride = 4, pad = 0;
- 卷积层C2, 输入为上一层卷积的feature map, 卷积的个数为256个,论文中的两个GPU分别有128个卷积核。卷积核的大小为:\(5×5×48 5\); pad = 2, stride = 1; 然后做\(LRN\), 最后 max_pooling, pool_size = (3, 3), stride = 2;
- 卷积层C3, 输入为第二层的输出,卷积核个数为384, kernel_size =\(3×3×256\), padding = 1, 第三层没有做LRN和Pool
- 卷积层C4,输入为第三层的输出,卷积核个数为384,kernel_size =\(3×3×256\), padding = 1,, 和第三层一样,没有LRN和Pool
- 卷积层C5, 输入为第四层的输出,卷积核个数为256, kernel_size =\(3×3\), padding = 1。然后直接进行max_pooling, pool_size = (3, 3), stride = 2;
- 第6,7,8层是全连接层,每一层的神经元的个数为4096,最终输出softmax为1000,全连接层中使用了RELU和Dropout。
AlexNet首次在CNN中成功应用了图像增强、ReLU、Dropout和LRN等Trick。同时AlexNet也使用了GPU进行运算加速。
由于AlexNet是使用两块显卡进行训练的,其网络结构的实际是分组进行的。并且,在C2,C4,C5上其卷积核只和上一层的同一个GPU上的卷积核相连。 对于单显卡来说,并不适用,这里基于pytorch的实现,忽略其关于双显卡的的结构,并且忽略局部归一化操作。其网络结构实现如下:
# AlexNet
import time
import torch
from torch import nn, optim
import torchvision
import numpy as np
import sys
import os
import torch.nn.functional as F
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 1000),
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
net = AlexNet()
print(net)
VGGNet
VGGNet是牛津大学计算机视觉研究组(Visual Geometry Group)和Google DeepMind公司的研究员们一起研发的深度卷积神经网络。VGGNet探索了卷积神经网络的深度与其性能之间的关系,通过反复的堆叠3×3的小型卷积核和2×2的最大池化层,构建了16~19层深度的卷积神经网络,整个网络结构简洁,都使用同样大小的卷积核尺寸(3×3)和最大池化尺寸(2×2)。VGGNet的扩展性很强,迁移到其他图片数据上的泛化性很好,因此,截止到目前为止,也常被用来抽取图像的特征。VGGNet训练后的模型参数在其官方网站开源了,可以用来做为其他类似图像分类任务的初始化参数,被广泛用于其他很多地方。
VGGNet论文中全部使用了\(3*3\)的卷积核和\(2*2\)的池化核,通过不断加深网络结构来提升性能。下图所示为VGGNet各级别的网络结构图,和每一级别的参数量,从11层的网络一直到19层的网络都有详尽的性能测试。
虽然从A到E每一级网络逐渐变深,但是网络的参数量并没有增长很多,这是因为参数量主要都消耗在最后3个全连接层。前面的卷积部分虽然很深,但是消耗的参数量不大,不过训练比较耗时的部分依然是卷积,因其计算量比较大。
VGGNet输入是大小为\(224*224\)的RGB图像,预处理(preprocession)后,图像经过一系列卷积层处理,在卷积层中使用了非常小的\(3*3\)卷积核,在有些卷积层里则使用了\(1*1\)的卷积核。卷积层步长(stride)设置为1个像素,\(3*3\)卷积层的填充(padding)设置为1个像素。池化层采用max pooling,共有5层,在一部分卷积层后,max-pooling的窗口是\(2*2\),步长设置为2。卷积层之后是三个全连接层(fully-connected layers,FC)。前两个全连接层均有4096个通道,第三个全连接层有1000个通道,用来分类。所有网络的全连接层配置相同。全连接层后是Softmax,用来分类。
VGGNet所有隐藏层(每个conv层中间)都使用ReLU作为激活函数。VGGNet不使用局部响应标准化(LRN),这种标准化并不能在ILSVRC数据集上提升性能,却导致更多的内存消耗和计算时间(LRN:Local Response Normalization,局部响应归一化,用于增强网络的泛化能力)。
其网络结构实现如下:
# VGG11
def vgg_block(num_convs, in_channels, out_channels): #卷积层个数,输入通道数,输出通道数
blk = []
for i in range(num_convs):
if i == 0:
blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
else:
blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
blk.append(nn.ReLU())
blk.append(nn.MaxPool2d(kernel_size=2, stride=2)) # 这里会使宽高减半
return nn.Sequential(*blk)
conv_arch = ((1, 1, 64), (1, 64, 128), (2, 128, 256), (2, 256, 512), (2, 512, 512))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 = 7
fc_features = 512 * 7 * 7 # c * w * h
fc_hidden_units = 4096 # 任意
def vgg(conv_arch, fc_features, fc_hidden_units=4096):
net = nn.Sequential()
# 卷积层部分
for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):
# 每经过一个vgg_block都会使宽高减半
net.add_module("vgg_block_" + str(i+1), vgg_block(num_convs, in_channels, out_channels))
# 全连接层部分
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(),
nn.Linear(fc_features, fc_hidden_units),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(fc_hidden_units, fc_hidden_units),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(fc_hidden_units, 10)
))
return net
net = vgg(conv_arch, fc_features, fc_hidden_units)
X = torch.rand(1, 1, 224, 224)
# named_children获取一级子模块及其名字(named_modules会返回所有子模块,包括子模块的子模块)
for name, blk in net.named_children():
X = blk(X)
print(name, 'output shape: ', X.shape)
NiN
NiN出自新加坡国立大学2014年的论文Network In Network。该设计后来为 ResNet 和 Inception 等网络模型所借鉴。NiN有两个很重要的观点:
- \(1×1\)卷积的使用
文中提出使用mlpconv网络层替代传统的convolution层。mlp层实际上是卷积加传统的mlp(多层感知器),因为convolution是线性的,而mlp是非线性的,后者能够得到更高的抽象,泛化能力更强。在跨通道(cross channel,cross feature map)情况下,mlpconv等价于卷积层+1×1卷积层,所以此时mlpconv层也叫cccp层(cascaded cross channel parametric pooling)。 - CNN网络中不使用FC层(全连接层)
提出使用Global Average Pooling取代最后的全连接层,因为全连接层参数多且易过拟合。做法即移除全连接层,在最后一层(文中使用mlpconv)层,后面加一层Average Pooling层。
完整网络结构如下:
其网络结构实现如下:
# NiN
import time
import torch
from torch import nn, optim
import torchvision
import numpy as np
import sys
import d2lzh_pytorch as d2l
import os
import torch.nn.functional as F
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1),
nn.ReLU())
return blk
# 已保存在d2lzh_pytorch
class GlobalAvgPool2d(nn.Module):
# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
def __init__(self):
super(GlobalAvgPool2d, self).__init__()
def forward(self, x):
return F.avg_pool2d(x, kernel_size=x.size()[2:])
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, stride=4, padding=0),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(96, 256, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=3, stride=2),
nin_block(256, 384, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, stride=1, padding=1),
GlobalAvgPool2d(),
# 将四维的输出转成二维的输出,其形状为(批量大小, 10)
d2l.FlattenLayer())
Google Inception Net
Google Inception Net首次出现在ILSVRC 2014的比赛中(和VGGNet同年),就以较大优势取得了第一名。那届比赛中的Inception Net通常被称为Inception V1,它最大的特点是控制了计算量和参数量的同时,获得了非常好的分类性能——top-5错误率6.67%,只有AlexNet的一半不到。
Inception V1有22层深,比AlexNet的8层或者VGGNet的19层还要更深。但其计算量只有15亿次浮点运算,同时只有500万的参数量,仅为AlexNet参数量(6000万)的1/12,却可以达到远胜于AlexNet的准确率,可以说是非常优秀并且非常实用的模型。
Inception V1降低参数量的目的有两点:第一,参数越多模型越庞大,需要供模型学习的数据量就越大,而目前高质量的数据非常昂贵;第二,参数越多,耗费的计算资源也会更大。
Inception V1参数少但效果好的原因除了模型层数更深、表达能力更强外,还有两点:
一是去除了最后的全连接层,用全局平均池化层(即将图片尺寸变为1*1)来取代它。全连接层几乎占据了AlexNet或VGGNet中90%的参数量,而且会引起过拟合,去除全连接层后模型训练更快并且减轻了过拟合。
二是Inception V1中精心设计的Inception Module提高了参数的利用效率,其结构如下图所示。这一部分也借鉴了Network In Network的思想,形象的解释就是Inception Module本身如同大网络中的一个小网络,其结构可以反复堆叠在一起形成大网络。
Inception Module的基本结构有4个分支:第一个分支对输入进行\(1*1\)的卷积,这其实也是NIN中提出的一个重要结构。\(1*1\)的卷积是一个非常优秀的结构,它可以跨通道组织信息,提高网络的表达能力,同时可以对输出通道升维和降维。Inception Module的4个分支都用到了\(1*1\)卷积,来进行低成本的跨通道的特征变换。第二个分支先使用了\(1*1\)卷积,然后连接\(3*3\)卷积,相当于进行了两次特征变换。第三个分支类似,先是\(1*1\)的卷积,然后连接\(5*5\)卷积。最后一个分支则是\(3*3\)最大池化后直接使用\(1*1\)卷积。Inception Module的4个分支在最后通过一个聚合操作合并(在输出通道数这个维度上聚合)。
这里采用不同大小的卷积核意味着不同大小的感受野,最后拼接意味着不同尺度特征的融合; 之所以卷积核大小采用\(1*1\)、\(3*3\)和\(5*5\),主要是为了方便对齐。设定卷积步长stride=1之后,只要分别设定padding =0、1、2,采用same卷积可以得到相同维度的特征,然后这些特征直接拼接在一起;如下,为GoogLeNet模型整体结构:
# GoogLeNet V1
class Inception(nn.Module):
# c1 - c4为每条线路里的层的输出通道数
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
return torch.cat((p1, p2, p3, p4), dim=1) # 在通道维上连结输出
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
d2l.GlobalAvgPool2d())
net = nn.Sequential(b1, b2, b3, b4, b5, d2l.FlattenLayer(), nn.Linear(1024, 10))
X = torch.rand(1, 1, 96, 96)
for blk in net.children():
X = blk(X)
print('output shape: ', X.shape)
ResNet
ResNet(Residual Neural Network)由微软研究院的Kaiming He等4名华人提出,通过使用Residual Unit成功训练152层深的神经网络,在ILSVRC 2015比赛中获得了冠军,取得3.57%的top-5错误率,同时参数量却比VGGNet低,效果非常突出。
ResNet的结构可以极快地加速超深神经网络的训练,模型的准确率也有非常大的提升,其原理可参考之前的论文分析:ResNet。
下面,直接来实现整个结构:
# ResNet
class Residual(nn.Module): # 本类已保存在d2lzh_pytorch包中方便以后使用
#可以设定输出通道数、是否使用额外的1x1卷积层来修改通道数以及卷积层的步幅。
def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
super(Residual, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
return F.relu(Y + X)
net = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
def resnet_block(in_channels, out_channels, num_residuals, first_block=False):
if first_block:
assert in_channels == out_channels # 第一个模块的通道数同输入通道数一致
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(in_channels, out_channels, use_1x1conv=True, stride=2))
else:
blk.append(Residual(out_channels, out_channels))
return nn.Sequential(*blk)
net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10)))
print(net)
DenseNet
研究表明,如果卷积网络在接近输入和接近输出地层之间包含较短地连接,那么,该网络可以显著地加深,变得更精确并且能够更有效地训练。该论文基于这个观察提出了以前馈地方式将每个层与其它层连接地密集卷积网络(DenseNet)。DenseNet缓解了消失梯度问题,加强了特征传播,鼓励特征重用,并大大减少了参数的数量,但需要消耗更多的内存。
该架构与ResNet相比,在将特性传递到层之前,没有通过求和来组合特性,而是通过连接它们的方式来组合特性。因此第x层(输入层不算在内)将有x个输入,这些输入是之前所有层提取出的特征信息。
下面,直接来实现整个结构:
# DenseNet
def conv_block(in_channels, out_channels):
blk = nn.Sequential(nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
return blk
# 1×1 卷积层:来减小通道数
def transition_block(in_channels, out_channels):
blk = nn.Sequential(
nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2))
return blk
class DenseBlock(nn.Module):
def __init__(self, num_convs, in_channels, out_channels):
super(DenseBlock, self).__init__()
net = []
for i in range(num_convs):
in_c = in_channels + i * out_channels
net.append(conv_block(in_c, out_channels))
self.net = nn.ModuleList(net)
self.out_channels = in_channels + num_convs * out_channels # 计算输出通道数
def forward(self, X):
for blk in self.net:
Y = blk(X)
X = torch.cat((X, Y), dim=1) # 在通道维上将输入和输出连结
return X
net = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
num_channels, growth_rate = 64, 32 # num_channels为当前的通道数
num_convs_in_dense_blocks = [4, 4, 4, 4]
for i, num_convs in enumerate(num_convs_in_dense_blocks):
DB = DenseBlock(num_convs, num_channels, growth_rate)
net.add_module("DenseBlosk_%d" % i, DB)
# 上一个稠密块的输出通道数
num_channels = DB.out_channels
# 在稠密块之间加入通道数减半的过渡层
if i != len(num_convs_in_dense_blocks) - 1:
net.add_module("transition_block_%d" % i, transition_block(num_channels, num_channels // 2))
num_channels = num_channels // 2
net.add_module("BN", nn.BatchNorm2d(num_channels))
net.add_module("relu", nn.ReLU())
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, num_channels, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(num_channels, 10)))
X = torch.rand((1, 1, 96, 96))
for name, layer in net.named_children():
X = layer(X)
print(name, ' output shape:\t', X.shape)
总结
最后,对这几种经典的卷积神经网络做出以下总结:
网络名称 | 层数 | 参数量 |
---|---|---|
LeNet | 5 | |
AlexNet | 8(5+3) | 6000W |
vgg | 11/13/16/19 | ~13800W |
GoogleNet V1 | 22 | 500W |