当前位置: 代码迷 >> 综合 >> CNN基础论文 精读+复现----GoolgeNet InceptionV1 (二)
  详细解决方案

CNN基础论文 精读+复现----GoolgeNet InceptionV1 (二)

热度:83   发布时间:2023-12-26 11:01:22.0

文章目录

  • 代码复现
    • 网络搭建
      • 卷积+Relu组合类
      • 辅助分类器类
      • inception结构类
      • 初始化权重与偏置
      • GoogLeNet模型类
    • 训练集
  • 程序结果
  • 完整代码
  • 总结

昨天看完了Googlenet论文,没看的可以看一下: CNN基础论文 精读+复现----GoogleNet & InceptionV1 (一)
今天复现一下把。

inception系列:

  • inceptionV1 & GoogleNet 精读
  • inceptionV2 & BN 精读
  • inceptionV3 精读
  • inceptionV4 精读
  • xception 精读

代码复现

代码部分还是较之前的VGG和ZF有一些变化的,分为几个块,我分开来写吧。

完整代码GitHub连接:https://github.com/shitbro6/paper

网络搭建

卷积+Relu组合类

论文在介绍Googlenet的那一节中说到了卷积后跟激活函数的结构,所以这里创建一个类将卷积和激活函数放在一起。

class BasicConv2d(nn.Module):     def __init__(self, in_channels, out_channels, **kwargs):super(BasicConv2d, self).__init__()self.BC1 = nn.Sequential(       nn.Conv2d(in_channels, out_channels, **kwargs),nn.ReLU(inplace=True))def forward(self, x):x = self.BC1(x)return x

这里再定义函数的时候用了 **kwargs,因为后面还要定义inception,inception里的卷积层有不同尺寸的卷积核和padding,所以这里定义了 **kwargs用于接收这些参数,
在调用的时候直接就可以 (输入通道,输出通道,**kwargs),函数会自动解析 **kwargs。

其余代码应该没什么好解释的,都是基础代码,定义了一个卷积层和一个Relu激活函数层放到了Sequential组合里,然后再forward向前传播一下。

辅助分类器类

根据论文给的图,辅助分类器长这样:

从下往上走, 平均池化 -> 卷积(含Relu) -> 全连接 ->全连接。

将细节都加进去就是 平均池化 -> 卷积(含Relu) -> 展平成向量 -> dropout -> 线性层1 -> Relu -> dropout ->线性层2。

同样用Sequential进行组合:

class InceptionAux(nn.Module):def __init__(self, in_channels, num_classes):super(InceptionAux, self).__init__()self.IA1 = nn.Sequential(nn.AvgPool2d(kernel_size=5, stride=3),BasicConv2d(in_channels, 128, kernel_size=1),nn.Flatten(),nn.Dropout2d(p=0.5),nn.Linear(2048, 1024),nn.ReLU(True),nn.Dropout2d(0.5),nn.Linear(1024, num_classes),)def forward(self, x):x = self.IA1(x)return x

inception结构类

论文中给出的inception结构图,输入在下,且整个googlenet的inception都是一样的结构,所以只需要定义一个类,分块操作然后合并就行了。

这里需要注意的是每一个卷积后面都跟着一个池化。

在这里插入图片描述
所有卷积操作和池化操作的步长都定义为1,卷积核为3 * 3 的padding = 1,卷积核为 5 * 5的 padding为2。

这里注意要定义好每一个卷积之后的输出通道数,因为后面还要继续卷积。

class Inception(nn.Module):    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):super(Inception, self).__init__()self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)  #第一个分支。self.branch2 = nn.Sequential(      #第二个分支BasicConv2d(in_channels, ch3x3red, kernel_size=1),BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1)   )self.branch3 = nn.Sequential(     # 第三个分支BasicConv2d(in_channels, ch5x5red, kernel_size=1),BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2)   )self.branch4 = nn.Sequential(        nn.MaxPool2d(kernel_size=3, stride=1, padding=1),BasicConv2d(in_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]return torch.cat(outputs, 1)  

def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
这里定义了在inception中每一个块所输出的通道数,表示为: ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj。

torch.cat
张量拼接。

然后按照上面inception的结构图罗列代码就行了。

最后的outputs列表组合了四个分支的结果,用torch.cat拼接四个分支得到的结果返回。

初始化权重与偏置

文章并没有重点说初始化权重等这些参数,仅在最后参加比赛的时候采用了七模型融合的激进做法时提了一下,所以我觉得这里可加可不加把,加的话就是下面这段代码。

def _initialize_weights(self):for m in self.modules():    if isinstance(m, nn.Conv2d): # 采用凯明初始化nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')if m.bias is not None:# 偏置初始化为0nn.init.constant_(m.bias, 0)elif isinstance(m, nn.Linear):    # 如果是全连接层# 权重均值为0, 标准差为0.01nn.init.normal_(m.weight, 0, 0.01)# 偏置为0nn.init.constant_(m.bias, 0)

这段代码的 逐句详解 在我的 VGG精读 里已经说过了,这里只是把xavier初始化方法换成了凯明初始化方法,其余基本没变。

GoogLeNet模型类

最后一块就是总的模型类了。
网络结构图片很长我就不直接放上来了,点我看高清大图

按照这个图一点一点写就行了,这块比较恶心建议直接抄一份。
记得定义两个分类辅助器。

# 模型类
class GoogLeNet(nn.Module):def __init__(self, num_classes=1000):super(GoogLeNet, self).__init__()self.Main = nn.Sequential(BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3),nn.MaxPool2d(3, stride=2, ceil_mode=True),  # ceil_mode=true代表向上取整BasicConv2d(64, 64, kernel_size=1),BasicConv2d(64, 192, kernel_size=3, padding=1),nn.MaxPool2d(3, stride=2, ceil_mode=True),Inception(192, 64, 96, 128, 16, 32, 32),Inception(256, 128, 128, 192, 32, 96, 64),nn.MaxPool2d(3, stride=2, ceil_mode=True),Inception(480, 192, 96, 208, 16, 48, 64))self.Main2 = nn.Sequential(Inception(512, 160, 112, 224, 24, 64, 64),Inception(512, 128, 128, 256, 24, 64, 64),Inception(512, 112, 144, 288, 32, 64, 64),)self.Main3 = nn.Sequential(Inception(528, 256, 160, 320, 32, 128, 128),nn.MaxPool2d(3, stride=2, ceil_mode=True),Inception(832, 256, 160, 320, 32, 128, 128),Inception(832, 384, 192, 384, 48, 128, 128),# 辅助分类器nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten(),nn.Dropout(0.4),nn.Linear(1024, num_classes),)self.aux1 = InceptionAux(512, num_classes)self.aux2 = InceptionAux(528, num_classes)# 初始化权重self._initialize_weights()# 前向传播def forward(self, x):x = self.Main(x)aux1 = self.aux1(x)x = self.Main2(x)aux2 = self.aux2(x)x = self.Main3(x)# N x 1000 (num_classes)# 在训练模式并且采用辅助分类器的情况下,返回主分类结果和两个辅助分类器的结果return x, aux2, aux

在最后向前传播的时候,分类辅助器和主结果一起返回。

训练集

训练集没啥好说的都是之前那一套,这里唯一的变动就是之前都是返回一个主训练结果,而这里由于多了两个分类器,所以是三个分类结果,按照论文要求,三个分类结果加权求和。
最终的LOSS = L主+0.3 * L辅 + 0.3 * L辅

所以在之前的训练集上稍作改动。

之前的:

y_pre = model(x)
loss = criterion(y_pre, y)

改动之后:

log,aux1,aux2 = model(x)
# 分别计算损失
loss0 = criterion(log, y)
loss1 = criterion(aux1, y)	
loss2 = criterion(aux2, y)
# 加权求和
loss = loss0 + loss1 * 0.3 + loss2 * 0.3

程序结果

使用CIFAR10数据集,由于实在跑的慢,懒得看结果了(想去追剧了。。。),直接测试运行了一下 没啥问题。

完整代码

import torch.nn as nn
import torch
import torch.nn.functional as F
import torchvision
from torch import optim
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn as nn
import matplotlib.pyplot as plt
from torch.utils.tensorboard import SummaryWriter
import cv2batch_size = 4
transform = transforms.Compose([transforms.Resize(224),transforms.ToTensor (),transforms.Normalize((0.4915, 0.4823, 0.4468,), (1.0, 1.0, 1.0)),
])train_dataset = datasets.CIFAR10(root='../data/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = datasets.CIFAR10(root='../data/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)print("训练集长度",len(train_dataset))
print("测试集长度",len(test_dataset))
# 模型类
from torch import optim# 模型类
class GoogLeNet(nn.Module):def __init__(self, num_classes=1000):super(GoogLeNet, self).__init__()self.Main = nn.Sequential(BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3),nn.MaxPool2d(3, stride=2, ceil_mode=True),  # ceil_mode=true代表向上取整BasicConv2d(64, 64, kernel_size=1),BasicConv2d(64, 192, kernel_size=3, padding=1),nn.MaxPool2d(3, stride=2, ceil_mode=True),Inception(192, 64, 96, 128, 16, 32, 32),Inception(256, 128, 128, 192, 32, 96, 64),nn.MaxPool2d(3, stride=2, ceil_mode=True),Inception(480, 192, 96, 208, 16, 48, 64))self.Main2 = nn.Sequential(Inception(512, 160, 112, 224, 24, 64, 64),Inception(512, 128, 128, 256, 24, 64, 64),Inception(512, 112, 144, 288, 32, 64, 64),)self.Main3 = nn.Sequential(Inception(528, 256, 160, 320, 32, 128, 128),nn.MaxPool2d(3, stride=2, ceil_mode=True),Inception(832, 256, 160, 320, 32, 128, 128),Inception(832, 384, 192, 384, 48, 128, 128),# 辅助分类器nn.AdaptiveAvgPool2d((1, 1)),nn.Flatten(),nn.Dropout(0.4),nn.Linear(1024, num_classes),)self.aux1 = InceptionAux(512, num_classes)self.aux2 = InceptionAux(528, num_classes)# 初始化权重self._initialize_weights()# 前向传播def forward(self, x):x = self.Main(x)aux1 = self.aux1(x)x = self.Main2(x)aux2 = self.aux2(x)x = self.Main3(x)return x, aux2, aux1def _initialize_weights(self):for m in self.modules():if isinstance(m, nn.Conv2d):# 采用凯明初始化nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')if m.bias is not None:# 将偏置初始化为0nn.init.constant_(m.bias, 0)elif isinstance(m, nn.Linear):  # 如果是全连接层# 权重均值是0, 标准差是0.01nn.init.normal_(m.weight, 0, 0.01)# 偏置是0nn.init.constant_(m.bias, 0)class Inception(nn.Module):def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):super(Inception, self).__init__()self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)  # 第一个分支self.branch2 = nn.Sequential(  # 第二个分支BasicConv2d(in_channels, ch3x3red, kernel_size=1),BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1))self.branch3 = nn.Sequential(  # 第三个分支BasicConv2d(in_channels, ch5x5red, kernel_size=1),BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2))self.branch4 = nn.Sequential(nn.MaxPool2d(kernel_size=3, stride=1, padding=1),BasicConv2d(in_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]return torch.cat(outputs, 1)class InceptionAux(nn.Module):def __init__(self, in_channels, num_classes):super(InceptionAux, self).__init__()self.IA1 = nn.Sequential(nn.AvgPool2d(kernel_size=5, stride=3),BasicConv2d(in_channels, 128, kernel_size=1),nn.Flatten(),nn.Dropout2d(p=0.5),nn.Linear(2048, 1024),nn.ReLU(True),nn.Dropout2d(0.5),nn.Linear(1024, num_classes),)def forward(self, x):x = self.IA1(x)return xclass BasicConv2d(nn.Module):def __init__(self, in_channels, out_channels, **kwargs):super(BasicConv2d, self).__init__()self.BC1 = nn.Sequential(  # 第四个分支nn.Conv2d(in_channels, out_channels, **kwargs),nn.ReLU(inplace=True))def forward(self, x):x = self.BC1(x)return xmodel = GoogLeNet().cuda()
# 损失函数
criterion = torch.nn.CrossEntropyLoss().cuda()
# 优化器
optimizer = optim.Adam(model.parameters(),lr=0.01)def train(epoch):runing_loss = 0.0i = 1for i, data in enumerate(train_loader):x, y = datax, y = x.cuda(), y.cuda()i +=1if i % 10 == 0:print("运行中,当前运行次数:",i)# 清零 正向传播 损失函数 反向传播 更新optimizer.zero_grad()# y_pre = model(x)# loss = criterion(y_pre, y)log,aux1,aux2 = model(x)loss0 = criterion(log, y)loss1 = criterion(aux1, y)loss2 = criterion(aux2, y)loss = loss0 + loss1 * 0.3 + loss2 * 0.3loss.backward()optimizer.step()runing_loss += loss.item()# 每轮训练一共训练1W个样本,这里的runing_loss是1W个样本的总损失值,要看每一个样本的平均损失值, 记得除10000print("这是第 %d轮训练,当前损失值 %.5f" % (epoch + 1, runing_loss / 782))return runing_loss / 782def test(epoch):correct = 0total = 0with torch.no_grad():for data in test_loader:x, y = datax, y = x.cuda(), y.cuda()pre_y = model(x)# 这里拿到的预测值 每一行都对应10个分类,这10个分类都有对应的概率,# 我们要拿到最大的那个概率和其对应的下标。j, pre_y = torch.max(pre_y.data, dim=1)  # dim = 1 列是第0个维度,行是第1个维度total += y.size(0)  # 统计方向0上的元素个数 即样本个数correct += (pre_y == y).sum().item()  # 张量之间的比较运算print("第%d轮测试结束,当前正确率:%d %%" % (epoch + 1, correct / total * 100))return correct / total * 100
if __name__ == '__main__':plt_epoch = []loss_ll = []corr = []for epoch in range(1):plt_epoch.append(epoch+1) # 方便绘图loss_ll.append(train(epoch)) # 记录每一次的训练损失值 方便绘图corr.append(test(epoch)) # 记录每一次的正确率plt.rcParams['font.sans-serif'] = ['KaiTi']plt.figure(figsize=(12,6))plt.subplot(1,2,1)plt.title("训练模型")plt.plot(plt_epoch,loss_ll)plt.xlabel("循环次数")plt.ylabel("损失值loss")plt.subplot(1,2,2)plt.title("测试模型")plt.plot(plt_epoch,corr)plt.xlabel("循环次数")plt.ylabel("正确率")plt.show()

总结

GoogleNet是之后学习各种网络变种的基础,他提出了很多新颖的点的。

  • 使用了赫布理论和多尺度处理方法,提出了inception结构,既占了稀疏的优势,又利用了硬件计算密集数据的优势,改善了VGG数据量巨大,嫉妒耗费资源的问题。
  • 将1 * 1 卷积核应用到里面了,再次提升了对 1 * 1的认识。
  • 提出使用辅助分类器在浅层或者中层提前出一波分类结果,然后和整体结果加权作为最后结果。
  • 最后提出了七模型融合 + 1图变 144图的激进方法用于比赛。

最后在复现代码的时候,一定要注意辅助分类层放的位置,就这个辅助分类层和上一个inception层前后顺序错了 导致调试了一个多小时(我太菜了,pytorch不熟.开摆!),整个程序从VGG改成Googlenet都没用一个小时,就离谱,明后天可能弄一下resnet吧。