GoogLeNet(附Pytorch源码)
GoogleNet(Inception v1)是由Google团队在2014年提出的深度卷积神经网络架构。它是为解决图像分类任务而设计的,并在ImageNet图像分类挑战赛中取得了很好的成绩。与VGGNet、LeNet、AlexNet有较大不同。在之前我们介绍的架构中VGG大量使用了3×3卷积,AlexNet使用了5×5,而NiN使用了1×1。因此,在构建卷积层时,我们要决定过滤器的大小究竟是1×1,3×3还是5×5,或者要不要添加池化层。
而GoogleNet网络的想法就是**我全都要!**GoogleNet最显著的特点是采用了一系列并行连接的Inception模块。
原文作者提到,“Inception”这个名字的想法来自于电影《盗梦空间》一个著名的网络梗:WE NEED TO GO DEEPER。
虽然网络架构因此变得更加复杂,但网络表现却非常好,下面我们来看一下GoogleNet具体组成架构:
1×1卷积
1×1 卷积由 NiN引入。1×1卷积与ReLU一起使用。因此,NiN最初使用它来引入更多的非线性,以提高网络的表示能力,因为NiN的作者认为数据是非线性形式的。在GoogLeNet中,1×1卷积被用作降维模块,以减少计算量。通过减少计算瓶颈,可以增加深度和宽度。
我举一个简单的例子来说明这一点。假设我们需要执行 5×5 卷积而不使用 1×1 卷积,如下所示:
运算次数 = (14×14×48)×(5×5×480) = 112.9M
使用 1×1 卷积:
- 1×1的运算次数 = (14×14×16)×(1×1×480) = 1.5M
- 5×5的运算次数 = (14×14×48)×(5×5×16) = 3.8M
- 总操作次数 = 1.5M + 3.8M = 5.3M,这比 112.9M 小得多!!!
事实上,上面的例子是在inception时 5×5 卷积的计算。(我们可以认为,当降维时,实际上我们正在以非线性的方式进行从高维到低维的映射。相反,对于PCA,它执行线性降维。)因此,与没有 1×1 卷积的情况相比,我们可以在不增加操作数量的情况下构建 inception 模块。1×1卷积可以帮助减小模型大小,这在某种程度上也有助于减少过拟合问题
2. Inception模块
与之前介绍的lexNet
和 VGGNet
,每层的 conv 大小都是固定的相比。Inception模块将输入特征图分别进行多个不同尺寸的卷积和池化操作,并将它们的结果进行拼接。这样可以同时捕捉到不同尺度上的特征,从而提高了特征的表达能力。具体而言,Inception模块包含了1×1、3×3和5×5的卷积层,以及1×1卷积和3×3最大池化层。通过使用不同尺寸的卷积和池化操作,网络能够有效地捕捉到局部和全局的特征。
3. 全局平均池化层:
在网络的最后,GoogleNet使用全局平均池化层将特征图的大小降为1×1。这种操作可以将特征图中每个通道的特征合并为一个标量值,从而减少参数数量,并且有助于提取更加全局的特征。
之前的网络以全连接(FC)层用于网络末端,所有输入都连接到每个输出。
上面的权重(连接)数量 = 7×7×1024×1024 = 51.3M
在GoogLeNet中,全局平均池化在网络末端使用,通过对每个特征图从7×7到1×1进行平均
权重数量 = 0
4.训练辅助分类器
在模型中间的一部分有softmax分支,它们仅用于训练。这些分支是辅助分类器,包括:
- 5×5 平均池化(步幅 3)
- 1×1 卷积(128 个过滤器)
- 1024 FC
- 1000FC
- Softmax
5.基本架构
了解了上面介绍的基本单元之后,我们就可以谈谈整体的网络架构了。
总共有22层!与之前的AlexNet、LeNet和VGGNet相比,它已经是一个非常深的模型了。 而且我们可以看到有很多inception模块连接在一起,甚至可以更深入。
以下是各层参数的详细信息。我们其实可以扩展1×1卷积的例子来自己计算运算次数。
6.Pytorch代码实现
import warnings
warnings.filterwarnings('ignore')
# 导入相关库
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn import functional as F
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# inception块
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)
# 构建google-Net
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.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(),
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),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
# Xavier初始化:防止梯度爆炸,这是CNN常用的操作,特别对于GoogleNet这种已经算比较深的网络而言,特别有效,之前我们也介绍过他的具体公式。
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d: #对全连接层和卷积层初始化
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
# 检查是否有可用的GPU
device = torch.device('cuda'if torch.cuda.is_available() else 'cpu')
model = net.to(device)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 加载Fashion-MNIST数据集
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize((96,96)),
transforms.Normalize((0.5,), (0.5,))
])
trainset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
# 训练模型
num_epochs = 10
train_losses = []
test_losses = []
train_accs = []
test_accs = []
for epoch in range(num_epochs):
train_loss = 0.0
train_total = 0
train_correct = 0
model.train()
for images, labels in trainloader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
train_total += labels.size(0)
train_correct += (predicted == labels).sum().item()
train_loss /= len(trainloader)
train_accuracy = 100*train_correct / train_total
train_losses.append(train_loss)
train_accs.append(train_accuracy)
test_loss = 0.0
test_total = 0
test_correct = 0
model.eval()
with torch.no_grad():
for images, labels in testloader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
test_total += labels.size(0)
test_correct += (predicted == labels).sum().item()
test_loss /= len(testloader)
test_accuracy = 100*test_correct / test_total
test_losses.append(test_loss)
test_accs.append(test_accuracy)
print(f"Epoch {epoch+1}/{num_epochs}: Train Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.2f}%, Test Loss: {test_loss:.4f}, Test Acc: {test_accuracy:.2f}%")
# 绘制训练误差和测试误差曲线
plt.figure(figsize=(10, 5))
plt.plot(range(1, num_epochs+1), train_losses, label='Train Loss')
plt.plot(range(1, num_epochs+1), test_losses, label='Test Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training and Testing Loss')
plt.legend()
plt.show()
# 绘制训练准确率和测试准确率曲线
plt.figure(figsize=(10, 5))
plt.plot(range(1, num_epochs+1), train_accs, label='Train Acc')
plt.plot(range(1, num_epochs+1), test_accs, label='Test Acc')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training and Testing Accuracy')
plt.legend()
plt.show()
Epoch 1/10: Train Loss: 2.0245, Train Acc: 0.24, Test Loss: 1.2393, Test Acc: 0.50
Epoch 2/10: Train Loss: 0.8974, Train Acc: 0.67, Test Loss: 1.9615, Test Acc: 0.29
Epoch 3/10: Train Loss: 0.6790, Train Acc: 0.75, Test Loss: 0.4919, Test Acc: 0.81
Epoch 4/10: Train Loss: 0.4275, Train Acc: 0.84, Test Loss: 0.4063, Test Acc: 0.85
Epoch 5/10: Train Loss: 0.3615, Train Acc: 0.86, Test Loss: 0.3978, Test Acc: 0.84
Epoch 6/10: Train Loss: 0.3253, Train Acc: 0.88, Test Loss: 0.3284, Test Acc: 0.88
Epoch 7/10: Train Loss: 0.2964, Train Acc: 0.89, Test Loss: 0.3079, Test Acc: 0.89
Epoch 8/10: Train Loss: 0.2726, Train Acc: 0.90, Test Loss: 0.2953, Test Acc: 0.89
Epoch 9/10: Train Loss: 0.2540, Train Acc: 0.90, Test Loss: 0.3061, Test Acc: 0.88
Epoch 10/10: Train Loss: 0.2380, Train Acc: 0.91, Test Loss: 0.2799, Test Acc: 0.90
从结果可以看出,GoogLeNet的精确度超过了0.9,比之前的AlexNet更好。
总结
GoogleNet的是通过Inception模块的并行连接和多尺度的卷积和池化操作来提高特征的表示能力。它相对于传统的深度网络具有更高的计算效率和更强大的特征学习能力。
基本思想是Inception网络不需要人为决定使用哪个过滤器或者是否需要池化,而是由网络自行确定这些参数,你可以给网络添加这些参数的所有可能值,然后把这些输出连接起来,让网络自己学习它需要什么样的参数,采用哪些过滤器组合。简单来说,这些参数都是试出来的,Google太有钱了。
GoogleNet的成功开创了一系列基于Inception架构的后续版本(如Inception v2、v3等),为深度学习在计算机视觉任务中的广泛应用奠定了基础。