GoogLeNet详解:特征还能这样提取?

author: abinng date: 2025-12-19 16:19 createDate:2025-12-19 15:26

背景

在2014年的ImageNet图像识别挑战赛中,一个名叫_GoogLeNet_ (Szegedy et al., 2015)的网络架构大放异彩。 GoogLeNet吸收了NiN中串联网络的思想,并在此基础上做了改进。 这篇论文的一个重点是解决了什么样大小的卷积核最合适的问题。 毕竟,以前流行的网络使用小到,大到的卷积核。 本文的一个观点是,有时使用不同大小的卷积核组合是有利的。本节将介绍一个稍微简化的GoogLeNet版本:我们省略了一些为稳定训练而添加的特殊特性,现在有了更好的训练方法,这些特性不是必要的。

结构

file-20260128093920808

上图是略微简化的GoogLeNet模型,省略了一些为稳定训练而添加的特殊特性

Inception块

上图结构中有一个很明显的、没见过的块,叫Inception块,也就是这个(看起来好恐怖?): file-20260128093920809 ==| 注意:输入在该图下方 |== 这很可能得名于电影《盗梦空间》(Inception),因为电影中的一句话“我们需要走得更深”(“We need to go deeper”)。

从输入开始看:Inception块由四条并行路径组成。前三条路径使用窗口大小为的卷积层,从不同空间大小中提取信息。 中间的两条路径在输入上执行卷积,以减少通道数,从而降低模型的复杂性。 第四条路径使用最大汇聚层,然后使用卷积层来改变通道数。这四条路径都使用合适的填充来使输入与输出的高和宽一致,最后我们将每条线路的输出在通道维度上连结,并构成Inception块的输出。在Inception块中,通常调整的超参数是每层输出通道数。最后输出的通道数是四个路径上面通道数之和(所以需要四条路径上输出的高和宽一样)。

卷积如何减少通道数?

上面提到了 卷积可以减少通道数 例如 的输入, 的卷积层,经过该卷积层之后就是的输出

Inception块的参数计算

假设输入为的图像

  1. 路径1: 输入为 卷积核数量为个;卷积核的尺寸为;步幅为, 填充 输出为的特征图。
  2. 路径2: 1.输入为 卷积核数量为个;卷积核的尺寸为;步幅, 填充 输出为的特征图。 2.输入为 卷积核数量为个;卷积核的尺寸为;步幅, 填充 输出为的特征图。
  3. 路径3: 1.输入为 卷积核数量为个;卷积核的尺寸为;步幅, 填充 输出为的特征图。 2.输入为 卷积核数量为个;卷积核的尺寸为;步幅, 填充 输出为的特征图。
  4. 路径4: 1.输入为 池化感受野为;步幅, 填充 输出为的特征图。 2.输入为 卷积核数量为个;卷积核的尺寸为;步幅, 填充 输出为的特征图。
  5. 通道合并: 1.路径1的输出为: 2.路径2的输出为: 3.路径3的输出为: 4.路径4的输出为: 最终通道合并为,最终的输出为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

class Inception(nn.Module):
# c1--c4是每条路径的输出通道数
def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
# 线路2,1x1卷积层后接3x3卷积层
self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1x1卷积层后接5x5卷积层
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3x3最大汇聚层后接1x1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, 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)

卷积核的作用

  • ==提前声明==,其实这些作用只要是卷积都可以达到,但是的卷积核更“专一”,计算时并不会把一些不想包含的邻域的值也包含进来计算
  • 实现跨通道的交互和信息整合
    • 其实其他大小的卷积核也有同样作用,但是卷积核首先计算量小,其次卷积核在进行卷积运算时,权重控制明确,且边长大于1的卷积核在进行卷积运算时,如果要控制图像大小不变,要进行填充,此时就混合了一些0的值进行运算,而且只要卷积核边长大于1,就会进行空间的融合+通道的融合,而边长为1的卷积和只进行纯粹通道的融合,更好控制权重,例子:的三通道输入,分别用三通道且填充为1的卷积核与三通道的卷积核进行卷积运算,通过这个计算过程,就可以理解到
  • 可以控制降维和升维,不改变
    • 这个不难理解,可以参考上面的卷积如何减少通道数,看懂了那么升维也同理
  • 通过降维可以减少计算量,升维也可以增加计算量
    • 实际上其他大小的卷积核也可以实现,但是还是看上面的解释,显然的更方便且更容易控制,作用在参数的值的方面便变化上的也不大不乱,毕竟我只想降维呢。升维同理
  • 增加非线性特性
    • 卷积核可以在保持特征图()尺寸不变的前提下,通过后接的非线性激活函数(如)大幅增加网络的非线性特性。这使得网络能够学习更复杂的特征,从而提高模型的表达能力。

全局平均池化层(GAP)

上面的模型结构图在最后连接FC层之前,有一个全局平均池化层,这是干嘛的? 一句话,把特征总结到一个均值上了,指望这个均值往后传递信息

这个全局平均池化的操作,实际上就是用一个和图像大小一样的池化感受野进行平均池化操作,最终得到的矩阵,是通道数。实际作用是用均值代替每一通道图像的信息特征,详细看下图: file-20260128093920811

  1. 优点:一句话,直接把参数变得很少,并且保留了一定特征
    • 抑制过拟合。因为参数变少了,单有全连接层的话,容易大量保留下来空间信息,导致过拟合。
    • 使特征图输入尺寸更加灵活。因为神经网络参数在此刻不再与输入图像的尺寸有关了,而是与通道有关,也就是输入图像的长宽可以不固定。
  2. 缺点:一句话,一些重要特征可能被忽略
    • 信息丢失:每个通道上的特征信息数值被压缩成一个数值,肯定会丢失信息,一些重要的特征表达可能会被平均掉,导致降低了网络的表达能力
    • 梯度信息:特征图被降维成一个向量,导致梯度的传播变得更加困难,可能会导致梯度消失/爆炸
    • 复杂任务:还是一样的,对于一些重视细节的任务,一些特征被忽略,表达能力不够

参数计算

不多说,还是用到了这个公式: 假设输入为的图像 第一个模块使用64通道、卷积层

1
2
3
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))

第二个模块使用两个卷积层:第一个卷积层是64个通道、卷积层;第二个卷积层使用将通道数量增加三倍的卷积层。这对应于Inception块中的第二条路径。

1
2
3
4
5
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

第三个模块串联两个完整的Inception块。第一个Inception块的输出通道数为,四个路径之间的输出通道数量比为。 第二个和第三个路径首先将输入通道的数量分别减少到,然后连接第二个卷积层。第二个Inception块的输出通道数增加到,四个路径之间的输出通道数量比为。 第二条和第三条路径首先将输入通道的数量分别减少到

1
2
3
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))

第四模块更加复杂, 它串联了5个Inception块,其输出通道数分别是。 这些路径的通道数分配和第三模块中的类似,首先是含卷积层的第二条路径输出最多通道,其次是仅含卷积层的第一条路径,之后是含卷积层的第三条路径和含最大汇聚层的第四条路径。 其中第二、第三条路径都会先按比例减小通道数。 这些比例在各个Inception块中都略有不同。

1
2
3
4
5
6
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))

第五模块包含输出通道数为的两个Inception块。 其中每条路径通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。 需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均汇聚层,将每个通道的高和宽变成1。 最后我们将输出变成二维数组,再接上一个输出个数为标签类别数的全连接层。

1
2
3
4
5
6
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())

net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

权重初始化:

初始化权重的原因:

  • 不良的初始化,极易造成梯度消失or梯度爆炸
  • 如果权重初始化得太远离最优解,模型需要更多时间来调整这些权重以达到最佳性能。而一个好的初始化策略可以使权重开始时就更接近最优解,从而加快训练过程。
  • 可能让模型陷入局部最小值出不去

从反向传播中理解,这里引用上面梯度下降法反向传播的一个公式

梯度下降法反向传播的基础知识

好了回忆完了,所以我们知道:

  • 很小时,为梯度消失
  • 很大时,为梯度爆炸 而从上面的公式可以知道,的大小就能影响梯度 当网络的层数比较深的情况下,如VGG,GoogLeNet
  • 当权重比1大一点,随着连乘,就会爆炸式增长
  • 当权重比1小一点,随着连乘,就会指数级减少
1
2
3
4
5
6
7
8
9
10
11
12
13
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(
m.weightm,
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)
if m.bias is not None:
nn.init.constant_(m.bias, 0)

GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。 为了使Fashion-MNIST上的训练短小精悍,我们可以将输入的高和宽从224降到96,这简化了计算。

代码

GitHub - abinng/Blog-code: 用于存放我博客中的代码

进入后右上角可以给仓库一个Star哦~(´▽`ʃ♡ƪ)