深度学习入门笔记之GoogLeNet网络


GoogLeNet, 2014年ILSVRC挑战赛冠军,将Top5 的错误率降低到6.67%,一个22层的深度网络。VGG继承了LeNet以及AlexNet的一些框架结构,而GoogLeNet则做了更加大胆的网络结构尝试,与VGGNet模型相比较,GoogleNet模型的网络深度已经达到了22层( 如果只计算有参数的层,GoogleNet网络有22层深 ,算上池化层有27层),而且在网络架构中引入了Inception单元,从而进一步提升模型整体的性能。虽然深度达到了22层,但大小却比AlexNet和VGG小很多,GoogleNet参数为500万个( 5M ),VGG16参数是138M,是GoogleNet的27倍多,而VGG16参数量则是AlexNet的两倍多。

一、简介

那么,GoogLeNet是如何进一步提升性能的呢?
一般来说,提升网络性能最直接的办法就是增加网络深度和宽度,深度指网络层次数量、宽度指神经元数量。但这种方式存在以下问题:
(1)参数太多,如果训练数据集有限,很容易产生过拟合;
(2)网络越大、参数越多,计算复杂度越大,难以应用;
(3)网络越深,容易出现梯度弥散问题(梯度越往后穿越容易消失),难以优化模型。
所以,有人调侃“深度学习”其实是“深度调参”。
解决这些问题的方法当然就是在增加网络深度和宽度的同时减少参数,为了减少参数,自然就想到将全连接变成稀疏连接。但是在实现上,全连接变成稀疏连接后实际计算量并不会有质的提升,因为大部分硬件是针对密集矩阵计算优化的,稀疏矩阵虽然数据量少,但是计算所消耗的时间却很难减少。
那么,有没有一种方法既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。大量的文献表明可以将稀疏矩阵聚类为较为密集的子矩阵来提高计算性能,就如人类的大脑是可以看做是神经元的重复堆积,因此,GoogLeNet团队提出了Inception网络结构,就是构造一种“基础神经元”结构,来搭建一个稀疏性、高计算性能的网络结构。

二、Inception网络结构各版本的演化

GoogleNet提出了一个全新的深度CNN架构——Inception。

1 Inception单元结构

Inception 最初提出的版本主要思想是利用不同大小的卷积核实现不同尺度的感知,网络结构图如下:
在这里插入图片描述
Inception Module基本组成结构有四个成分:**11卷积,33卷积,55卷积,33最大池化。**最后对四个成分运算结果进行通道上组合,这就是Naive Inception的核心思想:利用不同大小的卷积核实现不同尺度的感知,最后进行融合,可以得到图像更好的表征。

下面通过一个具体的实例来看看整个Naive Inception单元的详细工作过程,假设在上图中Naive Inception单元的**前一层输入的数据是一个32×32×256的特征图,**该特征图先被复制成4份并分别被传至接下来的4个部分。我们假设这4个部分对应的滑动窗口的步长均为1,其中,1×1卷积层的Padding为0,滑动窗口维度为1×1×256,要求输出的特征图深度为128;3×3卷积层的Padding为1,滑动窗口维度为3×3×256,要求输出的特征图深度为192;5×5卷积层的Padding为2,滑动窗口维度为5×5×256,要求输出的特征图深度为96;3×3最大池化层的 Padding为1,滑动窗口维度为3×3×256。这里对每个卷积层要求输出的特征图深度没有特殊意义,仅仅举例用,之后通过计算,**分别得到这4部分输出的特征图为32×32×128、32×32×192、32×32×96 和 32×32×256,最后在合并层进行合并,得到32×32×672的特征图,**合并的方法是将各个部分输出的特征图相加,最后这个Naive Inception单元输出的特征图维度是32×32×672,总的参数量就是11256128+33256192+55256*96=1089536。

2 Inception V1

Naive Inception有两个非常严重的问题:首先,所有卷积层直接和前一层输入的数据对接,所以卷积层中的计算量会很大;其次,在这个单元中使用的最大池化层保留了输入数据的特征图的深度,所以在最后进行合并时,总的输出的特征图的深度只会增加,这样增加了该单元之后的网络结构的计算量。于是人们就要想办法减少参数量来减少计算量,在受到了模型 “Network in Network”的启发,开发出了在GoogleNet模型中使用的Inception单元(Inception V1),这种方法可以看做是一个额外的11卷积层再加上一个ReLU层。如下所示:
在这里插入图片描述
这里使用1x1 卷积核主要目的是进行压缩降维,减少参数量,从而让网络更深、更宽,更好的提取特征,这种思想也称为Pointwise Conv,简称PW。
下面我们看看1x1的卷积核是如何减少训练模型参数的。同样是对一个深度为512的特征矩阵使用64个大小为5x5的卷积核进行卷积,不使用1x1卷积核进行降维话一共需要819200个参数,如果使用1x1卷积核进行降维一共需要50688个参数,明显少了很多。
在这里插入图片描述
**举个例子来论证下吧。**假设新增加的 1×1 的卷积的输出深度为64,步长为1,Padding为0,其他卷积和池化的输出深度、步长都和之前在Naive Inception单元中定义的一样(即上面例子中定义的一样),前一层输入的数据仍然使用同之前一样的维度为32×32×256的特征图,通过计算,分别得到这 4 部分输出的特征图维度为32×32×128、32×32×192、32×32×96 和32×32×64,将其合并后得到维度为32×32×480的特征图,将这4部分输出的特征图进行相加,最后Inception单元输出的特征图维度是32×32×480。新增加的3个 1×1 的卷积参数量是3
1125664=49152,原来的卷积核参数量是11256128+3364192+556496=296960,总的参数量就是49152+296960=346112。
在输出的结果中,32×32×128、32×32×192、32×32×96 和之前的Naive Inception 单元是一样的,但其实这三部分因为1×1卷积层的加入,总的卷积参数数量已经大大低于之前的Naive Inception单元,而且因为在最大池化层之前也加入了1×1的卷积层,所以最终输出的特征图的深度也降低了,这样也降低了该单元之后的网络结构的计算量。

基于Inception V1构建的GoogLeNet的网络结构如下:
在这里插入图片描述
其中LocalRespNorm在VGG也有使用,作用不大,所以在搭建GoogLeNet的时候,可以直接把这个层去掉。
GoogLeNet的网络结构图细节如下:
在这里插入图片描述
下面这幅图是我将Inception模块所使用到的参数信息标注在每个分支上,其中#1x1对应着分支1上1x1的卷积核个数,#3x3reduce对应着分支2上1x1的卷积核个数,#3x3对应着分支2上3x3的卷积核个数,#5x5reduce对应着分支3上1x1的卷积核个数,#5x5对应着分支3上5x5的卷积核个数,poolproj对应着分支4上1x1的卷积核个数。
在这里插入图片描述
GoogLeNet网络结构明细表解析如下:
输入
原始输入图像为224x224x3,且都进行了零均值化的预处理操作(图像每个像素减去均值)。
第一模块
第一模块采用的是一个单纯的卷积层紧跟一个最大池化层。
**卷积层:**卷积核大小77,步长为2,padding为3,输出通道数64,输出特征图尺寸为(224-7+32)/2+1=112.5(向下取整)=112,输出特征图维度为112x112x64,卷积后进行ReLU操作。
**池化层:**窗口大小33,步长为2,输出特征图尺寸为((112 -3)/2)+1=55.5(向上取整)=56,输出特征图维度为56x56x64。
第二模块
第二模块采用2个卷积层,后面跟一个最大池化层。
在这里插入图片描述
卷积层:(1)先用64个1x1的卷积核(3x3卷积核之前的降维)将输入的特征图(56x56x64)变为56x56x64,然后进行ReLU操作。参数量是1
16464=4096。(2)再用卷积核大小33,步长为1,padding为1,输出通道数192,进行卷积运算,输出特征图尺寸为(56-3+12)/1+1=56,输出特征图维度为56x56x192,然后进行ReLU操作。参数量是3364192=110592。第二模块卷积运算总的参数量是110592+4096=114688,即114688/1024=112K。
池化层: 窗口大小3
3,步长为2,输出通道数192,输出为((56 - 3)/2)+1=27.5(向上取整)=28,输出特征图维度为28x28x192。
第三模块(Inception 3a层)
Inception 3a层,分为四个分支,采用不同尺度,图示如下:
在这里插入图片描述
再看下表格结构,来分析和计算吧:
在这里插入图片描述
(1)使用64个1x1的卷积核,运算后特征图输出为28x28x64,然后RuLU操作。参数量1119264=12288。
(2)96个1x1的卷积核(3x3卷积核之前的降维)运算后特征图输出为28x28x96,进行ReLU计算,再进行128个3x3的卷积,输出28x28x128。参数量1
119296+3396128=129024。
(3)16个1x1的卷积核(5x5卷积核之前的降维)将特征图变成28x28x16,进行ReLU计算,再进行32个5x5的卷积,输出28x28x32。参数量1
119216+551632=15872。
(4)pool层,使用3x3的核,输出28x28x192,然后进行32个1x1的卷积,输出28x28x32.。总参数量1
119232=6144。
将四个结果进行连接,对这四部分输出结果的第三维并联,即64+128+32+32=256,最终输出28x28x256。总的参数量是12288+129024+15872+6144=163328,即163328/1024=159.5K,约等于159K。
第三模块(Inception 3b层)
Inception 3b层,分为四个分支,采用不同尺度。
(1)128个1x1的卷积核,然后RuLU,输出28x28x128。
(2)128个1x1的卷积核(3x3卷积核之前的降维)变成28x28x128,进行ReLU,再进行192个3x3的卷积,输出28x28x192。
(3)32个1x1的卷积核(5x5卷积核之前的降维)变成28x28x32,进行ReLU,再进行96个5x5的卷积,输出28x28x96。
(4)pool层,使用3x3的核,输出28x28x256,然后进行64个1x1的卷积,输出28x28x64。
将四个结果进行连接,对这四部分输出结果的第三维并联,即128+192+96+64=480,最终输出输出为28x28x480。
Inception 3b和Inception 4a之间有一个最大池化下采样层 窗口大小33,步长为2,输出特征图维度为14x14x480。
在这里插入图片描述
第四模块(Inception 4a、4b、4c、4e)
与Inception3a,3b类似
在这里插入图片描述
Inception 4e和Inception 5a之间有一个最大池化下采样层 窗口大小3
3,步长为2,输出特征图维度为7x7x832。
在这里插入图片描述
第五模块(Inception 5a、5b)
与Inception3a,3b类似
在这里插入图片描述
输出层
在输出层GoogLeNet与AlexNet、VGG采用3个连续的全连接层不同,GoogLeNet采用的是全局平均池化层,得到的是高和宽均为1的卷积层,然后添加丢弃概率为40%的Dropout,输出层激活函数采用的是softmax。
在这里插入图片描述
激活函数
GoogLeNet每层使用的激活函数为ReLU激活函数。
辅助分类器(分别来自于Inception 4a和Inception 4d的输出)
根据实验数据,发现神经网络的中间层也具有很强的识别能力,为了利用中间层抽象的特征,在某些中间层中添加含有多层的分类器。如下图所示,红色边框内部代表添加的辅助分类器。GoogLeNet中共增加了两个辅助的softmax分支,作用有两点,一是为了避免梯度消失,用于向前传导梯度。反向传播时如果有一层求导为0,链式求导结果则为0。二是将中间某一层输出用作分类,起到模型融合作用。最后的loss=loss_2 + 0.3 * loss_1 + 0.3 * loss_0。实际测试时,这两个辅助softmax分支会被去掉。
(1)辅助分类器的第一层是一个平均池化下采样层,池化核大小为5x5,stride=3。使得(4a)阶段的输出为4×4×512,(4d)的输出为4×4×528。
(2)第二层是卷积层,卷积核大小为1x1,stride=1,卷积核个数是128。
(3)第三层是全连接层,节点个数是1024。
(4)丢弃70%输出的丢弃层。
(5)第四层是全连接层,节点个数是1000(对应分类的类别个数)。
在这里插入图片描述

3 Inception V2——卷积核替换

**Inception V2最主要的贡献就是提出了batch normalization,**目的主要在于加快训练速度。网络训练过程中参数不断改变导致后续每一层输入的分布也发生变化,而学习的过程又要使每一层适应输入的分布,因此我们不得不降低学习率、小心地初始化。作者将分布发生变化称之为internal covariate shift。 网络结构也发生了变化,就算有了Pointwise Conv,由于 5x5 卷积核直接计算参数量还是非常大,训练时间还是比较长,于是Google学习VGGNet的特点,**提出了使用多个小卷积核替代大卷积核的方法,**这就是 Inception V2:
在这里插入图片描述
在Inception V2中,使用两个 3x3 卷积核来代替 5x5 卷积,不仅使参数量少了,深度也变深了,提升了神经网络的效果,可谓一举多得。
让我们计算下参数量感受下吧!
假设输入 256 维,输出 512 维,计算参数量:
使用 5x5 卷积核,参数量为55256512=3276800
使用两个 3x3 卷积核,参数量为3
3256256+33256*512=1769472
从结果1769472/3276800=0.54可以看到,第二种方式的参数量是第一种方式0.54倍,大大的减少了参数量,加快训练速度。

4 Inception V3——卷积核拆分

在使用多个小卷积核替代大卷积核的方法后,参数量还是比较大,于是Google学习Factorization into small convolutions的思想,在Inception V2的基础上,**将一个二维卷积拆分成两个较小卷积,例如将77卷积拆成17卷积和71卷积,**这样做的好处是降低参数量。该paper中指出,通过这种非对称的卷积拆分比对称的拆分为几个相同的小卷积效果更好,可以处理更多,更丰富的空间特征。这就是Inception V3网络结构:
在这里插入图片描述
让我们计算下参数量感受下吧!
假设输入 256 维,输出 512 维,计算参数量:
使用 5x5 卷积核,参数量为5
5256512=3276800
先使用两个 1x5和5x1 卷积核,参数15256256+51256512=983040
从结果983040/3276800=0.3可以看到,第二种方式的参数量是第一种方式0.3倍,比使用多个小卷积核替代大卷积核的方法减少还多。
Inception V4考虑到借鉴了微软的ResNet网络结构思想,等以后再做详细介绍。

5 Inception V4

借鉴了微软的ResNet网络结构思想。
Inception V4主要利用残差连接(Residual Connection)来改进V3结构,得到Inception-ResNet-v1,Inception-ResNet-v2,Inception-v4网络。
ResNet的残差结构如下:
在这里插入图片描述
将该结构与Inception相结合,变成下图:
在这里插入图片描述
通过20个类似的模块组合,Inception-ResNet构建如下:
在这里插入图片描述

6 Bottleneck卷积结构

我们发现使用上面的结构和方法,参数量还是较大,于是人们提出了 Bottleneck 的结构降低参数量。
Bottleneck结构分三步走,首先用Pointwise Conv进行降维,再用常规卷积核进行卷积,最后使用Pointwise Conv进行进行升维,如下所示:
在这里插入图片描述
来吧,又到了计算参数量的时刻!
假设输入 256 维,输出 512 维,计算参数量:
使用 3x3 卷积核,参数量为33256256=589824
使用 Bottleneck 的方式,先使用1x1卷积核将输入的256维讲到64维,再使用3x3卷积核进行卷积,最后用1x1卷积核将64升到256维,参数量为1
125664+336464+1164256=69632
从结果69632/3276800=0.12可以看到,第二种方式的参数量是第一种方式0.12倍,参数量降得令人惊叹!

7 Xception的Depthwise Separable Conv深度可分离卷积(这个方法需要的话后面再详细了解)

人们发现上面的方法参数量还是不少啊,于是又提出了Depthwise Separable Conv(深度可分离卷积),这就是大名鼎鼎的Xception的网络结构。
Depthwise Separable Conv的核心思想是首先经过1*1卷积,即Pointwise Convolution(逐点卷积),然后对每一个通道分别进行卷积,即Depthwise Conv(深度卷积),这就是Xception,即Extreme Inception。
现在我们来对比一下,计算参数量吧!
一般的卷积如下:
在这里插入图片描述
上图,输入通道2,输出通道3,卷积核大小3x3,参数量为3323=54
Depthwise Separable Conv如下:
在这里插入图片描述
输入通道2,先进行经过1
1卷积,输出通道为3,参数量:1123=6,再对这三个通道分别进行卷积,即进行Depthwise Conv(深度卷积),参数量:33*3=27,总的参数量为6+27=33
从结果33/54=0.61可以看到,第二种方式的参数量是第一种方式0.61倍,如果有更多卷积核对不同通道进行卷积,则参数量降低的效果更明显。
需要注意的是,Xception里面的Depthwise Separable Convolution是先PW,后DW。而MobileNet里面的Depthwise Separable Convolution是先DW,后PW,这个在后面的MobileNet里面会有详细介绍,并计算这两种方式的参数量和性能。

**进化历史总结:**
Inception v1的多尺度卷积利用不同大小的卷积核实现不同尺度的感知,可以得到图像更好的表征。
Inception v1的Pointwise Conv利用1x1卷积核进行压缩降维,减少参数量,使模型更加精简。
Inception v2使用多个小卷积核替代大卷积核的方法,不仅使参数量少了,深度也变深了,提升了神经网络的效果。
Inception v3的卷积核非对称拆分不仅可以降低参数量,而且可以处理更多,更丰富的空间特征。
Bottleneck卷积结构分三步走,参数量降得令人惊叹!
Xception的Depthwise Separable Conv首先经过PW,然后DW,再度减少参数量,使分组卷积这样的思想被广泛用于设计性能高效的网络。

三、GoogLeNet的创新之处

1 使用1x1的卷积核进行降维以及映射处理 (虽然VGG网络中也有,但这里介绍的更详细)。
借鉴NiN(Network in Network)中提出的思想,采用1×1 conv 来保持空间维度的同时,降低深度,也就是降低通道数量,同时1×1 conv还可以为你的网络增强非线性。
2 引入了Inception结构(融合不同尺度的特征信息)。
横向的卷积核排列设计,使得多个不同size的卷积核能够得到图像当中不同cluster的信息 ,我们称之为“多尺度”。这样融合了不同尺度的卷积以及池化,一个模块一层就可以得到多个尺度的信息,下一阶段也可以同时从不同尺度中提取的特征,可以进行多维度特征的融合,所以效果更好。把计算力拓宽,也避免了太深训练梯度弥散的问题。
3 添加两个辅助分类器帮助训练。
为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度(辅助分类器)。辅助分类器是将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个网络的训练很有裨益。而在实际测试的时候,这两个额外的softmax会被去掉。
对深度相对较大的网络来说,梯度反向传播能够通过所有层的能力就会降低。文中指出:“在这个任务上,更浅网络的强大性能表明网络中部层产生的特征应该是非常有识别力的”。通过将辅助分类器添加到这些中间层,可以提高较低阶段分类器的判别力,这是在提供正则化的同时克服梯度消失问题。后面的实验表明辅助网络的影响相对较小(约0.5),只需要其中一个辅助分类器就能取得同样的效果。
4 丢弃全连接层,使用平均池化层(大大减少模型参数,除去两个辅助分类器,网络大小只有vgg的1/20)。虽然移除了全连接,但是网络中依然使用了Dropout。
较高层会捕获较高的抽象特征,其空间集中度会减少。这表明随着网络转移到更高层,Inception架构中3×3和5×5卷积的比例应该会增加。而到最后一个卷积层出来的全连接,由全局平均池化替代了,减少了参数,节约了计算量。
5 GoogLeNet测试样本处理(严格来说这一条算不得创新)
(1)对于一个测试样本,将图像的短边缩放成4种尺寸,分别为256,288,320,352。
(2)从每种尺寸的图像的左边,中间,右边(或者上面,中间,下面)分别截取一个方形区域,共三块方形区域。
(3)对于每一个方形区域,我们取其四角和中心,裁切出5个区域,再将方形区域缩小到224×224,共6快区域,加上它们的镜像版本(将图像水平翻转),一共得到4×3×6×2=144张图像。这样的方法在实际应用中是不必要的,可能存在更合理的修剪方法。

四、代码(Pytorch实现)

注释model.py

import torch.nn as nn
import torch
import torch.nn.functional as F

class GoogLeNet(nn.Module):
    #参数如下:num_classes分类类别个数,aux_logits是否使用辅助分类器,init_weights是否初始化权重
    def __init__(self, num_classes=1000, aux_logits=True, init_weights=False):
        super(GoogLeNet, self).__init__()
        self.aux_logits = aux_logits

        self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
        #ceil_mode=True,表示当计算出来的数是小数,就会向上取整;ceil_mode=False会向下取整
        self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
        #这里的LocalRespNorm的层都没有搭建

        self.conv2 = BasicConv2d(64, 64, kernel_size=1)
        self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)
        self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)
        self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64)
        self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)
        self.inception4c = Inception(512, 128, 128, 256, 24, 64, 64)
        self.inception4d = Inception(512, 112, 144, 288, 32, 64, 64)
        self.inception4e = Inception(528, 256, 160, 320, 32, 128, 128)
        self.maxpool4 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)
        self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)

        if self.aux_logits:
            self.aux1 = InceptionAux(512, num_classes)
            self.aux2 = InceptionAux(528, num_classes)
        #网络结构图中的AveragePool7*7+1(V)平均池化下采样层是针对输入图像是224*224大小的标准图像
        #这里采用AdaptiveAvgPool2d((1, 1)),无论输入的特征矩阵的高和宽是什么样的大小,都能得到所指定的一个特征矩阵的高和宽,这样可以不用限定输入图像的尺寸
        #AdaptiveAvgPool2d((1, 1)),无论输入是多大的图像,通过这个函数都能得到一个高为1宽为1的特征矩阵
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.dropout = nn.Dropout(0.4)
        self.fc = nn.Linear(1024, num_classes)
        if init_weights:
            self._initialize_weights()

    def forward(self, x):
        # N x 3 x 224 x 224
        x = self.conv1(x)
        # N x 64 x 112 x 112
        x = self.maxpool1(x)
        # N x 64 x 56 x 56
        x = self.conv2(x)
        # N x 64 x 56 x 56
        x = self.conv3(x)
        # N x 192 x 56 x 56
        x = self.maxpool2(x)

        # N x 192 x 28 x 28
        x = self.inception3a(x)
        # N x 256 x 28 x 28
        x = self.inception3b(x)
        # N x 480 x 28 x 28
        x = self.maxpool3(x)
        # N x 480 x 14 x 14
        x = self.inception4a(x)
        # N x 512 x 14 x 14
        if self.training and self.aux_logits:    # eval model lose this layer
            aux1 = self.aux1(x)

        x = self.inception4b(x)
        # N x 512 x 14 x 14
        x = self.inception4c(x)
        # N x 512 x 14 x 14
        x = self.inception4d(x)
        # N x 528 x 14 x 14
        if self.training and self.aux_logits:    # eval model lose this layer
            aux2 = self.aux2(x)

        x = self.inception4e(x)
        # N x 832 x 14 x 14
        x = self.maxpool4(x)
        # N x 832 x 7 x 7
        x = self.inception5a(x)
        # N x 832 x 7 x 7
        x = self.inception5b(x)
        # N x 1024 x 7 x 7

        x = self.avgpool(x)
        # N x 1024 x 1 x 1
        x = torch.flatten(x, 1)
        # N x 1024
        x = self.dropout(x)
        x = self.fc(x)
        # N x 1000 (num_classes)
        if self.training and self.aux_logits:   # eval model lose this layer
            return x, aux2, aux1
        return x

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0)

#ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj表示不同分支的卷积核的个数
#注意 Inception结构需要四个分支合并,所以四个分支输出的特征矩阵的高和宽必须相同
class Inception(nn.Module):
    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
        super(Inception, self).__init__()

        self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)

        self.branch2 = nn.Sequential(
            BasicConv2d(in_channels, ch3x3red, kernel_size=1),
            #output_zise=(input_size-3+2*1)/1+1=input_size
            BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1)   # 保证输出大小等于输入大小
        )

        self.branch3 = nn.Sequential(
            BasicConv2d(in_channels, ch5x5red, kernel_size=1),
            #output_zise=(input_size-5+2*2)/1+1=input_size
            BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2)   # 保证输出大小等于输入大小
        )

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            BasicConv2d(in_channels, pool_proj, kernel_size=1)
        )

    def forward(self, x):
        branch1 = self.branch1(x)
        branch2 = self.branch2(x)
        branch3 = self.branch3(x)
        branch4 = self.branch4(x)

        outputs = [branch1, branch2, branch3, branch4]
        #将四个分支进行合并,第一个参数outputs是输出特征矩阵的列表,第二个参数1是我们需要合并的一个维度,因为我们需要在channel这个维度进行合并
        return torch.cat(outputs, 1)


class InceptionAux(nn.Module):
    #输入特征矩阵的深度,num_classes所对应的分类的类别个数
    def __init__(self, in_channels, num_classes):
        super(InceptionAux, self).__init__()
        self.averagePool = nn.AvgPool2d(kernel_size=5, stride=3)
        #kernel_size=1不会改变特征矩阵的高和宽
        self.conv = BasicConv2d(in_channels, 128, kernel_size=1)  # output[batch, 128, 4, 4]
        #2048=128*4*4
        self.fc1 = nn.Linear(2048, 1024)
        self.fc2 = nn.Linear(1024, num_classes)

    def forward(self, x):
        # aux1: N x 512 x 14 x 14, aux2: N x 528 x 14 x 14
        x = self.averagePool(x)
        # aux1: N x 512 x 4 x 4, aux2: N x 528 x 4 x 4
        x = self.conv(x)
        # N x 128 x 4 x 4
        #x = torch.flatten(x, 1)中的1是表示从channel这个维度进行展平的
        x = torch.flatten(x, 1)
        #x是特征矩阵,这里用的0.5是以50%的比例随机失火神经元
        #self.training参数:当我们实例化一个模型model后,可以通过model.train()和model.eval()控制模型的状态,
        #model.train()模式下self.training=True;model.eval()模式下,self.training=False
        x = F.dropout(x, 0.5, training=self.training)
        # N x 2048
        #将dropout函数得到的输出输入到全连接层1中,然后再经过一个Relu激活函数得到我们的输出
        x = F.relu(self.fc1(x), inplace=True)
        x = F.dropout(x, 0.5, training=self.training)
        # N x 1024
        x = self.fc2(x)
        # N x num_classes
        return x

#因为卷积通常是和Relu激活函数共同使用的
class BasicConv2d(nn.Module):
    #传入两个参数in_channels输入特征矩阵的深度,out_channels输出特征矩阵的深度
    def __init__(self, in_channels, out_channels, **kwargs):
        super(BasicConv2d, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.conv(x)
        x = self.relu(x)
        return x

注释train.py(这部分与前面的VGG网路基本相同)

import os
import sys
import json

import torch
import torch.nn as nn
from torchvision import transforms, datasets
import torch.optim as optim
from tqdm import tqdm

from model import GoogLeNet


def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print("using {} device.".format(device))

    data_transform = {
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
        "val": transforms.Compose([transforms.Resize((224, 224)),
                                   transforms.ToTensor(),
                                   transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}

    data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # get data root path
    image_path = os.path.join(data_root, "data_set", "flower_data")  # flower data set path
    assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
    train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
                                         transform=data_transform["train"])
    train_num = len(train_dataset)

    # {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
    flower_list = train_dataset.class_to_idx
    cla_dict = dict((val, key) for key, val in flower_list.items())
    # write dict into json file
    json_str = json.dumps(cla_dict, indent=4)
    with open('class_indices.json', 'w') as json_file:
        json_file.write(json_str)

    batch_size = 32
    nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
    print('Using {} dataloader workers every process'.format(nw))

    train_loader = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size, shuffle=True,
                                               num_workers=nw)

    validate_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val"),
                                            transform=data_transform["val"])
    val_num = len(validate_dataset)
    validate_loader = torch.utils.data.DataLoader(validate_dataset,
                                                  batch_size=batch_size, shuffle=False,
                                                  num_workers=nw)

    print("using {} images for training, {} images for validation.".format(train_num,
                                                                           val_num))

    # test_data_iter = iter(validate_loader)
    # test_image, test_label = test_data_iter.next()

    # net = torchvision.models.googlenet(num_classes=5)
    # model_dict = net.state_dict()
    # pretrain_model = torch.load("googlenet.pth")
    # del_list = ["aux1.fc2.weight", "aux1.fc2.bias",
    #             "aux2.fc2.weight", "aux2.fc2.bias",
    #             "fc.weight", "fc.bias"]
    # pretrain_dict = {k: v for k, v in pretrain_model.items() if k not in del_list}
    # model_dict.update(pretrain_dict)
    # net.load_state_dict(model_dict)
    net = GoogLeNet(num_classes=5, aux_logits=True, init_weights=True)
    net.to(device)
    loss_function = nn.CrossEntropyLoss()
    optimizer = optim.Adam(net.parameters(), lr=0.0003)

    epochs = 30
    best_acc = 0.0
    save_path = './googleNet.pth'
    train_steps = len(train_loader)
    for epoch in range(epochs):
        # train
        net.train()
        running_loss = 0.0
        train_bar = tqdm(train_loader, file=sys.stdout)
        for step, data in enumerate(train_bar):
            images, labels = data
            optimizer.zero_grad()
            logits, aux_logits2, aux_logits1 = net(images.to(device))
            loss0 = loss_function(logits, labels.to(device))
            loss1 = loss_function(aux_logits1, labels.to(device))
            loss2 = loss_function(aux_logits2, labels.to(device))
            loss = loss0 + loss1 * 0.3 + loss2 * 0.3
            loss.backward()
            optimizer.step()

            # print statistics
            running_loss += loss.item()

            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                     epochs,
                                                                     loss)

        # validate
        net.eval()
        acc = 0.0  # accumulate accurate number / epoch
        with torch.no_grad():
            val_bar = tqdm(validate_loader, file=sys.stdout)
            for val_data in val_bar:
                val_images, val_labels = val_data
                outputs = net(val_images.to(device))  # eval model only have last output layer
                predict_y = torch.max(outputs, dim=1)[1]
                acc += torch.eq(predict_y, val_labels.to(device)).sum().item()

        val_accurate = acc / val_num
        print('[epoch %d] train_loss: %.3f  val_accuracy: %.3f' %
              (epoch + 1, running_loss / train_steps, val_accurate))

        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(net.state_dict(), save_path)
    print('Finished Training')

if __name__ == '__main__':
    main()

注释predict.py

import os
import json

import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt

from model import GoogLeNet


def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    data_transform = transforms.Compose(
        [transforms.Resize((224, 224)),
         transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    # load image
    img_path = "../tulip.jpg"
    assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
    img = Image.open(img_path)
    plt.imshow(img)
    # [N, C, H, W]
    img = data_transform(img)
    # expand batch dimension
    img = torch.unsqueeze(img, dim=0)

    # read class_indict
    json_path = './class_indices.json'
    assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)

    json_file = open(json_path, "r")
    class_indict = json.load(json_file)

    # create model
    #预测不需要使用两个辅助分类器,所以aux_logits=False,从而不会构建辅助分类器
    model = GoogLeNet(num_classes=5, aux_logits=False).to(device)

    # load model weights
    weights_path = "./googleNet.pth"
    #载入保存的模型的时候,已经将辅助分类器参数保存在权重当中,所以需要设定strict=False
    #默认strict = True会精准的匹配当前模型和我们所需要载入的权重模型。
    #我们现在搭建的GoogLeNet没有辅助分类器,和所保存的模型权重相比会缺一些层结构
    assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
    #如果进行调试,会发现unexpected_keys是一系列层,这些层都是属于那两个辅助分类器
    missing_keys, unexpected_keys = model.load_state_dict(torch.load(weights_path, map_location=device),
                                                          strict=False)
    model.eval()
    with torch.no_grad():
        # predict class
        output = torch.squeeze(model(img.to(device))).cpu()
        predict = torch.softmax(output, dim=0)
        predict_cla = torch.argmax(predict).numpy()

    print_res = "class: {}   prob: {:.3}".format(class_indict[str(predict_cla)],
                                                 predict[predict_cla].numpy())
    plt.title(print_res)
    for i in range(len(predict)):
        print("class: {:10}   prob: {:.3}".format(class_indict[str(i)],
                                                  predict[i].numpy()))
    plt.show()

if __name__ == '__main__':
    main()
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值