创新点:
- 引入了 Inception 结构(融合不同尺度的特征信息)
- 使用1x1的卷积核进行降维以及映射处理 (虽然VGG网络中也有,但该论文介绍的更详细)
- 添加两个辅助分类器帮助训练
- 丢弃全连接层,使用平均池化层(大大减少模型参数,除去两个辅助分类器,网络大小只有vgg的1/20)
把多个卷积或池化操作,放在一起组装成一个网络模块,设计神经网络时以模块为单位去组装整个网络结构。模块如下图所示
主要贡献:
- 使用1x1的卷积来进行升降维;
- 在多个尺寸上同时进行卷积再聚合
1x1卷积有什么用?
- 在相同尺寸的感受野中叠加更多的卷积,能提取到更丰富的特征(观点来源于NiN)(三个1x1卷积都起到了该作用)
- NIN结构中无论是第一个3x3卷积还是新增的1x1卷积,后面都紧跟着激活函数(比如relu)。
- 将两个卷积串联,就能组合出更多的非线性特征
- 进行降维,降低了计算复杂度(图中中间3x3卷积和5x5卷积前的1x1卷积都起到了这个作用)
- 当某个卷积层输入的特征数较多,对这个输入进行卷积运算将产生巨大的计算量;如果对输入先进行降维,减少特征数后再做卷积计算量就会显著减少。
- 只要最后输出的特征数不变(256组),中间的降维类似于压缩的效果,并不影响最终训练的结果
对输入做了4个分支,分别用不同尺寸的filter进行卷积或池化,最后再在特征维度上拼接到一起。这种全新的结构有什么好处呢?
- 在直观感觉上在多个尺度上同时进行卷积,能提取到不同尺度的特征。特征更为丰富也意味着最后分类判断时更加准确。
- 利用稀疏矩阵分解成密集矩阵计算的原理来加快收敛速度
- Hebbin赫布原理
代码实现
import torch
from torch import nn, optim
import torchvision
import sys
from time import time
import torch.nn.functional as F
device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')
class Inception(nn.Module):
def __init__(self, in_c, c1, c2, c3, c4): # c1-c4为每条路线里的层的输出通道数
super(Inception, self).__init__()
# 线路1,单1*1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1*1卷积层后接3*3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1*1卷积层后接5*5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3*3最大池化层后接1*1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(self.p2_1(x)))
p3 = F.relu(self.p3_2(self.p3_1(x)))
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 在通道维上连结输出
return torch.cat((p1, p2, p3, p4), dim=1)
class GlobalAvgPool2d(nn.Module):
# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
def __init__(self):
super(GlobalAvgPool2d, self).__init__()
def forward(self, x):
return F.avg_pool2d(x, kernel_size=x.size()[2:])
'''对 x 的形状转换 '''
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x):
return x.view(x.shape[0], -1)
class GoogLeNet(nn.Module):
def __init__(self, num_classes=1000):
super(GoogLeNet, self).__init__()
'''
Conv2d:输出特征图尺寸为(224-7+3*2)/2+1=112.5(向下取整)=112,输出特征图维度为112x112x64
MaxPool2d:输出特征图尺寸为((112 -3)/2)+1=55.5(向上取整)=56,输出特征图维度为56x56x64
'''
self.b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
# nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
'''
Conv2d:先用64个1x1的卷积核(3x3卷积核之前的降维)将输入的特征图(56x56x64)变为56x56x64
Conv2d:输出特征图尺寸为(56-3+1*2)/1+1=56,输出特征图维度为56x56x192
MaxPool2d:输出为((56 - 3)/2)+1=27.5(向上取整)=28,输出特征图维度为28x28x192
'''
self.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))
'''
3a
使用64个1x1的卷积核,运算后特征图输出为28x28x64,然后RuLU操作
96个1x1的卷积核(3x3卷积核之前的降维)运算后特征图输出为28x28x96,进行ReLU计算,再进行128个3x3的卷积,输出28x28x128
16个1x1的卷积核(5x5卷积核之前的降维)将特征图变成28x28x16,进行ReLU计算,再进行32个5x5的卷积,输出28x28x32。
pool层,使用3x3的核,输出28x28x192,然后进行32个1x1的卷积,输出28x28x32
3b
使用128个1x1的卷积核,运算后特征图输出为28x28x128,然后RuLU操作
128个1x1的卷积核(3x3卷积核之前的降维)运算后特征图输出为28x28x128,进行ReLU计算,再进行192个3x3的卷积,输出28x28x192
32个1x1的卷积核(5x5卷积核之前的降维)将特征图变成28x28x32,进行ReLU计算,再进行96个5x5的卷积,输出28x28x96。
pool层,使用3x3的核,输出28x28x256,然后进行64个1x1的卷积,输出28x28x64
将四个结果进行连接,对这四部分输出结果的第三维并联,即128+192+96+64=480,最终输出输出为28x28x480
'''
self.b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32), #3a
Inception(256, 128, (128, 192), (32, 96), 64), #3b
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
self.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))
self.b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
GlobalAvgPool2d())
self.output=nn.Sequential(FlattenLayer(),
nn.Dropout(p=0.4),
nn.Linear(1024, 10)) #原为1000
def forward(self, x):
x=self.b1(x)
x=self.b2(x)
x=self.b3(x)
x=self.b4(x)
x=self.b5(x)
output=self.output(x)
return output
'''测试输出'''
net = GoogLeNet()
# X = torch.rand(1, 3, 224, 224)
X = torch.rand(1, 1, 96, 96)
for blk in net.children():
X = blk(X)
print('output shape: ', X.shape)
'''读取数据集'''
def load_data_fashion_mnist(batch_size, resize=None):
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
'''定义数据预处理的转换函数列表'''
trans = []
if resize: #判断是否需要进行图像尺寸调整(resize)
trans.append(torchvision.transforms.Resize(size=resize))
#将torchvision.transforms.Resize转换函数添加到转换函数列表trans中,并指定目标尺寸为resize
trans.append(torchvision.transforms.ToTensor())
# 将torchvision.transforms.ToTensor转换函数添加到转换函数列表trans中。这个函数用于将图像数据转换为张量,并且按照通道顺序排列(RGB)
transform = torchvision.transforms.Compose(trans)
#通过torchvision.transforms.Compose函数将转换函数列表trans组合成一个转换操作
mnist_train = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=True,
download=True,
transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=False,
download=True,
transform=transform)
train_iter = torch.utils.data.DataLoader(mnist_train,
batch_size=batch_size,
shuffle=True,
num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_train,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers)
return train_iter, test_iter
'''设置网络'''
net = GoogLeNet()
'''用于GPU的准确率评估函数'''
def evaluate_accuracy(data_iter, net, device=None):
if device is None and isinstance(net, torch.nn.Module):
device = list(net.parameters())[0].device #如果没指定device就使用net的device
acc_sum, n = 0.0, 0
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train() # 改回训练模式
else:
if ('is_training' in net.__code__.co_varnames):
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
'''确保计算使用的数据和模型同在内存或显存上'''
def train_ch5(net, train_iter, test_iter, batch_size, optimizer , device, num_epochs):
# 将模型加载到指定运算器中
net = net.to(device)
print("training on ", device)
loss = torch.nn.CrossEntropyLoss()
for epoch in range(num_epochs):
# 在每一个迭代周期中,会使用训练集中所有样本一次(假设样本数能够被批量大小整除)
train_l_sum, train_acc_sum, n ,batch_count= 0.0, 0.0, 0,0
# 分别表示训练损失总和、训练准确度总和、样本数
start = time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
'''前向传播'''
y_hat = net(X)
#由于变量 l 并不是一个标量,所以我们可以调用 .sum() 将其求和得到一个标量
'''计算损失'''
l = loss(y_hat, y).sum()
'''梯度清零'''
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
'''反向传播'''
l.backward() # 运行 l.backward() 得到该变量有关模型参数的梯度
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
'''更新参数'''
optimizer.step()
'''计算精度'''
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f,\
time %.1f sec' %(epoch+1, train_l_sum/batch_count,
train_acc_sum/n, test_acc,
time()-start))
'''使用高和宽均为96像素的图像来训练GoogLeNet模型'''
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=96)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size, optimizer,device, num_epochs)