GoogLeNet,也被称为Inception网络,是一种深度卷积神经网络架构,由Google研究人员在2014年提出。它在当年的ImageNet挑战赛中取得了显著的成绩。GoogLeNet的核心创新是引入了Inception模块,这些模块能够以不同的尺度并行捕获信息,然后将结果合并,从而提高了网络的效率和性能。
GoogLeNet的主要特点包括:
-
Inception模块:这是GoogLeNet的核心,它由多个不同大小的卷积核(1x1, 3x3, 5x5)和一个3x3最大池化层组成。这些卷积层和池化层并行运行,然后将结果合并。这样可以在不同的尺度上捕捉图像的特征。
-
辅助分类器:GoogLeNet在网络的不同层级上添加了辅助分类器,这些分类器可以提供额外的梯度信号,有助于网络的早期收敛,并有助于防止过拟合。
-
全局平均池化:在网络的最后,GoogLeNet使用全局平均池化层代替了传统的全连接层,这减少了模型的参数数量,并且减少了过拟合的风险。
-
Dropout和批量归一化:GoogLeNet在训练过程中使用了Dropout和批量归一化(Batch Normalization)技术,以提高模型的泛化能力和训练速度。
-
深度架构:GoogLeNet是一个非常深的网络,有22层(如果包括辅助分类器的话是30层),这使得它能够学习非常复杂的特征表示。
GoogLeNet的结构:
- 输入层:接收原始图像。
- 卷积层 + 池化层:进行初步的特征提取。
- Inception模块:多个并行的卷积和池化操作。
- 辅助分类器:在某些Inception模块后添加,用于提供额外的梯度信号。
- 全局平均池化层:减少参数数量,防止过拟合。
- Dropout层:减少过拟合。
- Softmax层:输出最终的分类结果。
GoogLeNet的变种:
- Inception-v2:在Inception模块中引入了批量归一化。
- Inception-v3:对Inception模块进行了改进,引入了因子分解,减少了参数数量和计算量。
- Inception-v4:进一步优化了网络结构,提高了性能。
GoogLeNet及其变种在图像识别、分类和其他视觉任务中表现出色,是深度学习领域的一个重要里程碑。
代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
# 定义Inception模块
class InceptionModule(nn.Module):
def __init__(self, in_channels, filters):
super(InceptionModule, self).__init__() # 调用父类构造函数
self.conv_1x1 = nn.Conv2d(in_channels, filters[0], kernel_size=1, padding=0) # 1x1卷积层
self.conv_3x3_reduce = nn.Conv2d(in_channels, filters[1], kernel_size=1, padding=0) # 3x3卷积层的降维1x1卷积
self.conv_3x3 = nn.Conv2d(filters[1], filters[2], kernel_size=3, padding=1) # 3x3卷积层
self.conv_5x5_reduce = nn.Conv2d(in_channels, filters[3], kernel_size=1, padding=0) # 5x5卷积层的降维1x1卷积
self.conv_5x5 = nn.Conv2d(filters[3], filters[4], kernel_size=5, padding=2) # 5x5卷积层
self.pool_proj = nn.Conv2d(in_channels, filters[5], kernel_size=1, padding=0) # 池化层后的1x1卷积
def forward(self, x):
conv_1x1 = self.conv_1x1(x) # 应用1x1卷积
conv_3x3_reduce = self.conv_3x3_reduce(x) # 应用3x3卷积的降维1x1卷积
conv_3x3 = self.conv_3x3(conv_3x3_reduce) # 应用3x3卷积
conv_5x5_reduce = self.conv_5x5_reduce(x) # 应用5x5卷积的降维1x1卷积
conv_5x5 = self.conv_5x5(conv_5x5_reduce) # 应用5x5卷积
pool = F.max_pool2d(x, kernel_size=3, stride=1, padding=1) # 应用最大池化
pool_proj = self.pool_proj(pool) # 应用池化后的1x1卷积
outputs = torch.cat([conv_1x1, conv_3x3, conv_5x5, pool_proj], 1) # 将所有卷积层的输出在通道维度上拼接
return outputs
# 构建GoogLeNet模型
class GoogLeNet(nn.Module):
def __init__(self, num_classes=1000):
super(GoogLeNet, self).__init__() # 调用父类构造函数
self.pre_layers = nn.Sequential( # 初始卷积层
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False), # 7x7卷积层
nn.MaxPool2d(kernel_size=3, stride=2, padding=1), # 最大池化层
)
self.inception_blocks = nn.Sequential( # Inception模块序列
InceptionModule(64, [64, 96, 128, 16, 32, 32]), # 第一个Inception模块
InceptionModule(256, [128, 128, 192, 32, 96, 64]), # 第二个Inception模块
) # 后面6个参数 分别是卷积核的数量 对应 InceptionModule
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1)) # 自适应平均池化层
self.fc = nn.Linear(1024, num_classes) # 全连接层
def forward(self, x):
x = self.pre_layers(x) # 应用初始卷积层
x = self.inception_blocks(x) # 应用Inception模块
x = self.avg_pool(x) # 应用自适应平均池化
x = torch.flatten(x, 1) # 扁平化
x = self.fc(x) # 应用全连接层
return x
# 示例用法
model = GoogLeNet(num_classes=1000)
print(model)
# 打印模型概要(PyTorch没有内置的模型概要打印功能,但可以使用torchsummary库)
from torchsummary import summary
summary(model, (3, 224, 224))
批量归一化(Batch Normalization,简称BN)是一种在深度学习中广泛应用的技术,它通过标准化每一层的激活输出,从而加速训练过程并提高模型的稳定性。批量归一化在2015年由Sergey Ioffe和Christian Szegedy提出,并迅速成为深度学习模型的标准组件之一。
批量归一化的原理
批量归一化的基本思想是在每一层的激活之前对数据进行归一化处理,使得每一层的输入具有稳定的均值和方差。这一过程有助于缓解梯度消失或爆炸的问题,并且使得每一层的激活值更加稳定,从而加速训练过程。
批量归一化的步骤
批量归一化通常包含以下几个步骤:
-
计算均值和方差: 对于一个给定的小批量(batch)数据,首先计算该批次数据的均值(mean)和方差(variance)。
-
归一化: 使用计算出的均值和方差对数据进行归一化处理,使得数据具有零均值和单位方差。
-
缩放和平移: 为了保持模型的灵活性,归一化后的数据会通过可学习参数γ(scale)和β(shift)进行缩放和平移。这两个参数通过反向传播学习得到。
批量归一化的优势
-
加速训练: 通过归一化激活值,可以加速训练过程,因为归一化后的数据更接近于标准正态分布,有助于梯度传播。
-
提高稳定性: 归一化可以减少内部协变量偏移(internal covariate shift),使得训练过程更加稳定。
-
减少正则化需求: 批量归一化本身具有一些正则化效果,因此在某些情况下可以减少对其他正则化技术(如Dropout)的需求。
-
增强模型表现: 在很多情况下,批量归一化可以提高模型的泛化能力和最终的测试精度。
批量归一化要在哪个位置?
批量归一化层通常放置在卷积层之后,激活函数之前。这是因为批量归一化的主要目的是使每一层的输入具有零均值和单位方差,从而加速训练过程并提高模型的稳定性。具体来说:
-
卷积层之后:
- 卷积层的输出通常是非线性的,其分布可能不符合标准正态分布。通过批量归一化,可以使得这些输出具有稳定的统计特性。
-
激活函数之前:
- 在激活函数之前应用批量归一化,可以确保激活函数的输入具有稳定的分布,从而有助于梯度传播和模型训练。
代码:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 定义一个简单的卷积神经网络模型
class SimpleConvNet(nn.Module):
def __init__(self):
super(SimpleConvNet, self).__init__()
# 卷积层:输入通道为3(RGB图像),输出通道为64,卷积核大小为7x7,步长为2,填充为3
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
# 批量归一化层:应用于卷积层的输出,输出通道为64
self.bn1 = nn.BatchNorm2d(64)
# ReLU激活函数
self.relu = nn.ReLU(inplace=True)
# 最大池化层:核大小为3x3,步长为2,填充为1
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
def forward(self, x):
# 输入通过卷积层
x = self.conv1(x)
# 应用批量归一化
x = self.bn1(x)
# 应用ReLU激活函数
x = self.relu(x)
# 应用最大池化层
x = self.maxpool(x)
# 返回最终的输出
return x
# 实例化模型
model = SimpleConvNet()
因此,在上面的代码中,批量归一化层 self.bn1
放置在卷积层 self.conv1
之后,并且在激活函数 self.relu
之前。这种安排有助于确保每一层的输入具有良好的统计特性,从而加速训练过程并提高模型的稳定性。
ResNet(残差网络)是另一种在深度学习领域具有里程碑意义的卷积神经网络架构,由何凯明等人在2015年提出,并在当年的ImageNet Large Scale Visual Recognition Challenge (ILSVRC) 中取得了优异的成绩。ResNet 解决了深层神经网络中的梯度消失和梯度爆炸问题,使得训练更深的网络成为可能。
ResNet 的主要特点
-
残差块(Residual Block):
- ResNet 的核心在于引入了残差块,这是一种特殊的网络结构,通过在主干网络中加入跳跃连接(skip connection)来解决梯度消失和梯度爆炸问题。
- 残差块允许信息和梯度更容易地向前和向后传递,从而使得网络能够训练得更深而不失去有效性。
-
身份映射(Identity Mapping):
- 残差块中的跳跃连接实际上是在执行一种身份映射(identity mapping),即直接将输入传递到下一个层,而不是仅仅通过网络的权重矩阵。
- 这种设计使得即使不考虑残差块中的卷积层,网络仍然可以学习到恒等映射(即输入等于输出),从而保证网络不会比浅层网络更差。
ResNet的优势
- 解决梯度消失问题:通过残差块的跳跃连接,梯度可以直接通过这些连接进行反向传播,避免了梯度在深层网络中消失的问题。
- 提高训练效率:残差块使得网络可以更快地收敛,并且可以训练更深的网络结构。
- 提高泛化能力:ResNet在多个数据集上都表现出了良好的泛化能力
残差块详解
标准残差块
一个典型的残差块结构如下所示:
输入 -> [F(x; W)] -> 加法 -> 激活函数 -> 输出
其中:
F(x; W)
表示一个或多个卷积层组成的子网络,W
是这些层的权重。- 加法操作是指将残差块的输入直接加到
F(x; W)
的输出上。 - 激活函数通常是ReLU。
示例结构
一个基本的残差块可以用以下形式表示:
输入 -> [Conv -> BN -> ReLU -> Conv -> BN] -> 加法 -> ReLU -> 输出
其中:
Conv
表示卷积层。BN
表示批量归一化层。ReLU
表示激活函数(通常为ReLU)。- 加法操作将输入与卷积层的输出相加
ResNet 的变体
ResNet 有很多变体,最常见的包括:
-
ResNet-18 和 ResNet-34:
- 使用基本的残差块(不包含瓶颈设计)。
- 分别有18层和34层。
-
ResNet-50、ResNet-101 和 ResNet-152:
- 使用带有瓶颈设计的残差块。
- 分别有50层、101层和152层。
瓶颈残差块
瓶颈残差块(Bottleneck Residual Block)通常用于更深的网络,如 ResNet-50 及其以上的版本。它包含三个卷积层,其中第一个和最后一个卷积层的通道数较少,中间的卷积层通道数较多,形成一个“瓶颈”结构。
一个典型的瓶颈残差块结构如下:
输入 -> [1x1 Conv -> BN -> ReLU -> 3x3 Conv -> BN -> ReLU -> 1x1 Conv -> BN] -> 加法 -> ReLU -> 输出
代码如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
# 定义基本的残差块
class BasicBlock(nn.Module):
# 扩展因子,用于处理不同类型的残差块
expansion = 1
def __init__(self, in_channels, out_channels, stride=1):
super(BasicBlock, self).__init__()
# 第一个卷积层,用于提取特征
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
# 批量归一化层
self.bn1 = nn.BatchNorm2d(out_channels)
# 第二个卷积层,用于进一步提取特征
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
# 批量归一化层
self.bn2 = nn.BatchNorm2d(out_channels)
# 跳跃连接(shortcut)用于处理输入和输出通道数不一致的情况!!
self.shortcut = nn.Sequential()
# 如果步长不是1或者输入和输出的通道数不一致,则需要通过一个额外的卷积层来进行维度匹配
if stride != 1 or in_channels != self.expansion * out_channels:
self.shortcut = nn.Sequential(
# 1x1卷积层,用于调整通道数
nn.Conv2d(in_channels, self.expansion * out_channels, kernel_size=1, stride=stride, bias=False),
# 批量归一化层
nn.BatchNorm2d(self.expansion * out_channels)
)
def forward(self, x):
# 输入通过第一个卷积层
out = F.relu(self.bn1(self.conv1(x)))
# 输入通过第二个卷积层
out = self.bn2(self.conv2(out))
# 将跳跃连接的输出与卷积层的输出相加
out += self.shortcut(x)
# 通过ReLU激活函数
out = F.relu(out)
# 返回最终的输出
return out
# 定义ResNet模型
class ResNet(nn.Module):
def __init__(self, block, num_blocks, num_classes=10):
super(ResNet, self).__init__()
# 初始化输入通道数
self.in_channels = 64
# 第一个卷积层,用于提取初始特征
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
# 批量归一化层
self.bn1 = nn.BatchNorm2d(64)
# 构建多个残差块层
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
# 最后的全连接层,用于分类
self.linear = nn.Linear(512 * block.expansion, num_classes)
#⬇⬇⬇用于构建多个连续的残差块层⬇⬇⬇
def _make_layer(self, block, out_channels, num_blocks, stride):
# 计算每个残差块的步长
strides = [stride] + [1] * (num_blocks - 1)
layers = []
# 构建多个连续的残差块
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
# 更新输入通道数
self.in_channels = out_channels * block.expansion
# 返回一个顺序容器,包含多个残差块
return nn.Sequential(*layers)
def forward(self, x):
# 输入通过第一个卷积层
out = F.relu(self.bn1(self.conv1(x)))
# 依次通过每个残差块层
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
# 应用全局平均池化
out = F.avg_pool2d(out, 4)
# 展平输出
out = out.view(out.size(0), -1)
# 通过全连接层
out = self.linear(out)
# 返回最终的输出
return out
# 实例化模型
def ResNet18():
# 创建一个ResNet-18模型,包含四个残差块层,每个层包含2个基本残差块
return ResNet(BasicBlock, [2, 2, 2, 2])
# 创建模型实例
model = ResNet18()
print(model)