GoogLeNet:图像分类神经网络的深度剖析与实践
文章目录
- 前言
- GoogLeNet
- 一、网络基本介绍
- 1.1 论文信息
- 1.2 网络结构的表
- 1.3 网络结构图
- 二、网络的创新
- 2.1 Inception结构
- 2.2 1x1卷积核进行降维
- 2.3 辅助分类器
- 三、GoogLeNet的缺点
- 四、代码示例
- 总结
前言
在当今的人工智能领域,图像分类是一个至关重要的研究方向,其在安防监控、医学影像分析、自动驾驶等众多领域都有着广泛的应用。为了实现更精准、高效的图像分类,各种神经网络架构不断涌现。其中,GoogLeNet作为一款具有里程碑意义的网络模型,于2014年由Google团队在ImageNet挑战赛中首次提出,并一举斩获第一名。它独特的设计理念和卓越的性能,为后续的神经网络发展奠定了坚实的基础。本文将深入剖析GoogLeNet的网络结构、创新点、不足之处,并给出相应的代码示例,希望能帮助读者全面了解这一经典的图像分类网络。
GoogLeNet
一、网络基本介绍
GoogLeNet由Google团队于2014年在ImageNet挑战赛中首次提出,在该次图像分类比赛中斩获第一名。它是一种常用于图像分类任务的流行神经网络架构。
其主要特点如下:
- Inception模块:采用了名为“Inception”的模块结构,该模块将网络结构划分为多个并行分支,每个分支用于捕捉不同尺度的特征。这种设计使得GoogLeNet能更有效地学习图像特征,同时还能减小网络体积。
- 平均池化技术:运用“平均池化”技术,可在不改变图像尺寸的情况下对图像进行降采样。这有助于GoogLeNet更快地处理图像,还能提高网络的鲁棒性。
1.1 论文信息
- 论文名:Going deeper with convolutions
- 论文下载:google_net_paper.pdf
1.2 网络结构的表
在论文中给出了网络结构的表,表中包含以下信息:
- type:每一层的结构。
- patch size/stride:卷积或者池化的核大小/步长。
- output size:本层的输出尺寸。
- depth:这一层的卷积深度,即卷积层数。
- param:本层的参数量。
- ops:运算量。
特殊说明:
- “#1×1” 表示 1×1 卷积核的数量。
- “#3×3” 表示 3×3 卷积核的数量。
- “#5×5” 表示 5×5 卷积核的数量。
- “#3×3 reduce” 和 “#5×5 reduce” 表示在 3×3 和 5×5 卷积之前的降维层中 1×1 滤波器(卷积核)的数量。
Inception 结构中的对应卷积的参数可与 Inception 结构对照查看。
1.3 网络结构图
给出了 GoogLeNet 网络的网络结构图。
二、网络的创新
2.1 Inception结构
Inception结构的思想与之前的卷积思想不同。之前的模型是将不同的卷积层通过串联方式连接,而Inception结构采用串联 + 并联的方式连接卷积层。
Inception结构会对输入图像并行地执行多个卷积运算或池化操作,然后将所有输出结果拼接为一个深度较大的特征图。不同大小卷积核的卷积运算能够获取图像中的不同信息,对这些信息进行处理可以得到更好的图像特征。其结构示例图如下
Inception结构常用于图像分类和识别任务,因为它能有效捕捉图像中的细节信息。其主要优势在于可以高效处理大量数据,且模型参数量相对较少,适合在不同设备上运行。
2.2 1x1卷积核进行降维
Inception结构的四个分支中,每个分支得到的特征矩阵宽和高必须相同,之后将四个分支的特征矩阵沿着深度方向进行拼接。
其中三个分支采用1x1卷积的原因是其具有降维作用。例如,有一个输入特征矩阵为 n × n × 512 n \times n \times 512 n×n×512:
- 直接卷积情况:若直接经过64个5x5的卷积核进行卷积,所需参数量为 5 × 5 × 512 × 64 = 819200 5\times5\times512\times64 = 819200 5×5×512×64=819200。
- 降维卷积情况:若先经过1x1的卷积(假设1x1卷积核个数为24个)再经过64个5x5的卷积,所需参数量为 1 × 1 × 512 × 24 + 5 × 5 × 24 × 64 = 50688 1\times1\times512\times24+5\times5\times24\times64 = 50688 1×1×512×24+5×5×24×64=50688。
对比可知,经过1x1的卷积降维后,参数量明显减少。
2.3 辅助分类器
辅助分类器通常与主要的分类器结合使用,有助于模型更好地理解图像中的细节和复杂模式,提高模型的泛化能力,使其更准确地预测未知图像。
在GoogLeNet中有两个辅助分类器,分别位于Inception4a和Inception4d。这两个辅助分类器仅用于训练,目的是解决梯度消失问题。分类器的损失值将乘以0.3的权重后加到最终损失中。
两个辅助分类器的结构完全一致,具体如下:
- 平均池化层:池化核大小为5x5,步长为3。
- 卷积层:128个卷积核,大小为1x1,步长为1。
- 全连接层:节点个数为1024个。
- Dropout层:在全连接之后接一个dropout,比例为70%,即随机失活70%的神经元。
- 全连接输出层:由于数据集是ImageNet,所以输出层有1000个节点,然后经过softmax函数。
需要注意的是,辅助分类器仅在训练时使用,预测时不使用。
三、GoogLeNet的缺点
- 计算复杂度高:采用Inception模块致使网络计算复杂度大幅提升,训练速度慢,不适用于对实时性要求较高的应用场景。
- 网络结构复杂:网络结构本身较难理解,且辅助分类器的存在增加了调试和优化网络的难度。
综上所述,计算复杂度高和网络结构复杂是GoogLeNet的主要短板。
四、代码示例
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim# 定义一个名为Inception的类,该类继承自nn.Module,用于构建Inception模块
class Inception(nn.Module):# 定义类的构造函数,初始化Inception模块的各个分支# in_channels:输入通道数# ch1x1:1x1卷积的输出通道数# ch3x3red:3x3卷积前的压缩通道数# ch3x3:3x3卷积的输出通道数# ch5x5red:5x5卷积前的压缩通道数# ch5x5:5x5卷积的输出通道数# pool_proj:池化投影后的输出通道数def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):# 调用父类的构造函数super(Inception, self).__init__()# 定义第一个分支,使用1x1卷积self.branch1 = BasicConv2d(in_channels=in_channels, out_channels=ch1x1, kernel_size=1)# 定义第二个分支,先使用1x1卷积进行通道压缩,再使用3x3卷积self.branch2 = nn.Sequential(BasicConv2d(in_channels=in_channels, out_channels=ch3x3red, kernel_size=1),# 保证输出大小等于输入大小BasicConv2d(in_channels=ch3x3red, out_channels=ch3x3, kernel_size=3, padding=1))# 定义第三个分支,先使用1x1卷积进行通道压缩,再使用5x5卷积self.branch3 = nn.Sequential(BasicConv2d(in_channels=in_channels, out_channels=ch5x5red, kernel_size=1),# 在官方的实现中,其实是3x3的kernel并不是5x5BasicConv2d(in_channels=ch5x5red, out_channels=ch5x5, kernel_size=5, padding=2) # 保证输出大小等于输入大小)# 定义第四个分支,先使用3x3最大池化,再使用1x1卷积self.branch4 = nn.Sequential(nn.MaxPool2d(kernel_size=3, stride=1, padding=1),BasicConv2d(in_channels=in_channels, out_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]# 使用torch.cat函数将outputs列表中的张量在通道维度(维度1)拼接在一起return torch.cat(outputs, 1)# 定义辅助分类器 InceptionAux类,用于在训练过程中提供额外的监督信息
class InceptionAux(nn.Module):# 定义构造函数,初始化辅助分类器的各个层# in_channels:输入通道数# num_classes:类别数def __init__(self, in_channels, num_classes):# 调用父类的构造函数super(InceptionAux, self).__init__()# 定义一个2D平均池化层,池化窗口大小为5x5,步长为3self.averagePool = nn.AvgPool2d(kernel_size=5, stride=3)# 定义一个基本的2D卷积层,输入通道数为in_channels,输出通道数为128,卷积窗口大小为1x1self.conv = BasicConv2d(in_channels, 128, kernel_size=1)# 定义一个全连接层,输入特征数为2048,输出特征数为1024self.fc1 = nn.Linear(2048, 1024)# 定义第二个全连接层,输入特征数为1024,输出特征数为num_classes(类别数)self.fc2 = nn.Linear(1024, num_classes)# 定义前向传播函数,将输入数据经过各个层得到分类结果def forward(self, x):# 对输入的x进行平均池化x = self.averagePool(x)# 对池化后的x进行卷积x = self.conv(x)# 将卷积后的x按照第1维进行展平x = torch.flatten(x, 1)# 进行dropout操作,丢弃概率为0.5,防止过拟合x = F.dropout(x, 0.5, training=self.training)# 使用ReLU进行激活x = F.relu(self.fc1(x))# 再次进行dropout操作,丢弃概率为0.5x = F.dropout(x, 0.5, training=self.training)# 通过全连接层得到最终的分类结果x = self.fc2(x)return x# 定义一个名为BasicConv2d的类,用于构建基本的卷积层,包含卷积和ReLU激活函数
class BasicConv2d(nn.Module):# 定义构造函数,初始化卷积层和ReLU激活函数# in_channels:输入通道数# out_channels:输出通道数# **kwargs:可变关键字参数,用于传递卷积层的其他参数,如卷积核大小、步长等def __init__(self, in_channels, out_channels, **kwargs):# 调用父类的构造函数super(BasicConv2d, self).__init__()# 定义一个2D卷积层,输入通道为in_channels,输出通道为out_channels,其他参数通过**kwargs传入self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)# 定义一个ReLU激活函数self.relu = nn.ReLU(inplace=True)# 定义前向传播函数,将输入数据经过卷积和ReLU激活def forward(self, x):# 数据通过卷积层x = self.conv(x)# 对卷积后的结果进行ReLU激活x = self.relu(x)return x# 定义GoogleNet类,构建完整的GoogleNet模型
class GoogleNet(nn.Module):# 定义构造函数,初始化GoogleNet模型的各个层# num_classes:类别数,默认为1000# aux_logits:是否使用辅助分类器,默认为Truedef __init__(self, num_classes=1000, aux_logits=True):# 调用父类的构造函数super(GoogleNet, self).__init__()# 是否使用辅助分类器的标志self.aux_logits = aux_logits# 第一层卷积层,使用BasicConv2d模块,输入通道数为3,输出通道数为64,卷积核大小为7,步长为2,填充为3self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)# 第一层最大池化层,池化窗口大小为3,步长为2,采用ceil方式计算输出大小self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)# 第二层卷积层,输入通道数为64,输出通道数为64,卷积核大小为1self.conv2 = BasicConv2d(64, 64, kernel_size=1)# 第三层卷积层,输入通道数为64,输出通道数为192,卷积核大小为3,填充为1self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)# 第二层最大池化层,池化窗口大小为3,步长为2,采用ceil方式计算输出大小self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)# 第四层卷积层,使用Inception模块,输入通道数为192,输出通道数分别为64、96、128、16、32、32self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)# 第五层卷积层,使用Inception模块,输入通道数为256,输出通道数分别为128、128、192、32、96、64self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)# 第三层最大池化层,池化窗口大小为3,步长为2,采用ceil方式计算输出大小self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)# 第六层卷积层,使用Inception模块,输入通道数为480,输出通道数分别为192、96、208、16、48、64self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64)# 第七层卷积层,使用Inception模块,输入通道数为512,输出通道数分别为160、112、224、24、64、64self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)# 第八层卷积层,使用Inception模块,输入通道数为512,输出通道数分别为128、128、256、24、64、64self.inception4c = Inception(512, 128, 128, 256, 24, 64, 64)# 第九层卷积层,使用Inception模块,输入通道数为512,输出通道数分别为112、144、288、32、64、64self.inception4d = Inception(512, 112, 144, 288, 32, 64, 64)# 第十层卷积层,使用Inception模块,输入通道数为528,输出通道数分别为256、160、320、32、128、128self.inception4e = Inception(528, 256, 160, 320, 32, 128, 128)# 第四层最大池化层,池化窗口大小为3,步长为2,采用ceil方式计算输出大小self.maxpool4 = nn.MaxPool2d(3, stride=2, ceil_mode=True)# 第十一层卷积层,使用Inception模块,输入通道数为832,输出通道数分别为256、160、320、32、128、128self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)# 第十二层卷积层,使用Inception模块,输入通道数为832,输出通道数分别为384、192、384、48、128、128self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)# 如果使用辅助分类器,则添加两个InceptionAux模块,分别输入512和528通道的张量,输出num_classes个结果if self.aux_logits:self.aux1 = InceptionAux(512, num_classes)self.aux2 = InceptionAux(528, num_classes)# 使用自适应平均池化层,将输入张量的大小调整为(1, 1)self.avgpool = nn.AdaptiveAvgPool2d((1, 1))# Dropout层,将输入张量的40%元素设为0,防止过拟合self.dropout = nn.Dropout(0.4)# 全连接层,输入大小为1024,输出大小为num_classesself.fc = nn.Linear(1024, num_classes)# 定义前向传播函数,将输入数据经过各个层得到最终的分类结果def forward(self, x):# 数据通过第一层卷积层x = self.conv1(x)# 对卷积后的x进行最大池化x = self.maxpool1(x)# 数据通过第二层卷积层x = self.conv2(x)# 数据通过第三层卷积层x = self.conv3(x)# 对卷积后的x进行最大池化x = self.maxpool2(x)# 数据通过第一个Inception模块x = self.inception3a(x)# 数据通过第二个Inception模块x = self.inception3b(x)# 对Inception后的x进行最大池化x = self.maxpool3(x)# 数据通过第三个Inception模块x = self.inception4a(x)# 如果在训练阶段并且使用辅助分类器,则通过辅助分类器得到辅助分类结果aux1if self.training and self.aux_logits:aux1 = self.aux1(x)# 数据通过第四个Inception模块x = self.inception4b(x)# 数据通过第五个Inception模块x = self.inception4c(x)# 数据通过第六个Inception模块x = self.inception4d(x)# 如果在训练阶段并且使用辅助分类器,则通过辅助分类器得到辅助分类结果aux2if self.training and self.aux_logits:aux2 = self.aux2(x)# 数据通过第七个Inception模块x = self.inception4e(x)# 对Inception后的x进行最大池化x = self.maxpool4(x)# 数据通过第八个Inception模块x = self.inception5a(x)# 数据通过第九个Inception模块x = self.inception5b(x)# 对特征图进行全局平均池化,将其变为一维向量x = self.avgpool(x)# 将二维特征图展平为一维向量x = torch.flatten(x, 1)# 在向量的维度上添加dropout操作,随机丢弃部分神经元以防止过拟合x = self.dropout(x)# 通过全连接层得到最终的输出结果,通常用于分类任务x = self.fc(x)# 如果在训练阶段并且使用了辅助分类器,则返回最终输出、辅助分类结果aux2和aux1if self.training and self.aux_logits:return x, aux2, aux1# 如果不是在训练阶段或者没有使用辅助分类器,则只返回最终输出return x# 实例化GoogleNet模型
model = GoogleNet()
# 创建一个随机输入张量,模拟输入数据
input_tensor = torch.rand(5, 3, 224, 224)
# 设置模型为训练模式
model.train()
# 进行前向传播,得到最终输出和辅助分类结果
output, aux_output2, aux_output1 = model(input_tensor)
# 打印最终输出、辅助分类结果
print(output, aux_output2, aux_output1)
# 创建标签张量,模拟真实标签
labels = torch.tensor([1, 0, 2, 0, 1])
# 定义交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 定义Adam优化器,用于更新模型的参数
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 计算损失,包括最终输出的损失和辅助分类结果的损失
loss = criterion(output, labels) + 0.3 * criterion(aux_output1, labels) + 0.3 * criterion(aux_output2, labels)
# 打印损失值
print(loss)
总结
本文围绕GoogLeNet展开了全面且深入的探讨。首先介绍了GoogLeNet的基本信息,包括其提出背景、主要特点,如Inception模块和平均池化技术,还展示了论文信息、网络结构表以及网络结构图。接着详细阐述了该网络的创新之处,如Inception结构采用串联 + 并联的方式连接卷积层,能有效捕捉图像细节;1x1卷积核可实现降维,大幅减少参数量;辅助分类器有助于解决梯度消失问题,提高模型泛化能力。然而,GoogLeNet也存在计算复杂度高和网络结构复杂的缺点,这限制了它在实时性要求高的场景中的应用。最后,文章给出了GoogLeNet的代码示例,帮助读者更好地理解其实现方式。通过本文的介绍,读者能够对GoogLeNet有一个系统、清晰的认识。