一位深度学习小萌新的学渣笔记(四)GoogLeNet网络介绍及代码详解

GoogLeNet网络结构详解与模型的搭建

简单介绍GoogLeNet网络

GoogLeNet网络在2014年被谷歌团队提出 LeNet大写是为了致敬LeNet

网络亮点:

  • 引入了Inception结构(融合不同尺度的特征信息)
  • 使用1 x 1的卷积核进行降维以及映射处理
  • 添加两个辅助分类器帮助训练

// AlexNet和VGG都只有一个输出层 GoogLeNet有三个输出层(其中有两个辅助分类层)

  • 丢弃全连接测层,使用平均池化层(大大减少模型参数)

模型结构


结构分析

input
卷积层
最大下采样层
LocalRespNorm层
卷积层
卷积层
LocalRespNorm层
最大下采样层

Inception层(3a)
在这里插入图片描述
lnception(3b)
在这里插入图片描述
最大池化层
Inception(4a)
在这里插入图片描述
黄色部分是辅助分类器1



softmax激活函数进行输出
在这里插入图片描述

Inception模型

初始的版本


将上一层的输出同时输入到四个分支中进行处理 处理之后将我们得到的这四个分支的特征矩阵按照深度进行拼接 得到我们的输出特征矩阵

注意:每个分支得到的特征矩阵的高和宽必须相同 否则无法沿深度方向进行拼接

优化后的版本

在这里插入图片描述
Inception结构加上一个降维的功能
用到了三个1 x 1的卷积层 进行降维

1x1的卷积核如何起到降维的作用呢?


这样子使用1x1的卷积核 目的是为了减少特征矩阵的深度,从而减少我们的参数个数 ,也就减少了我们的计算量

辅助分类器

两个辅助分类器的结构一样
在这里插入图片描述
**第一层是我们的平均下采样操作,池化核大小是5*5 ,步距是等于3 **
第一个辅助分类器是来自于我们的inception4a的输出
inception4a的输出矩阵的大小是14x14x512

第二个辅助分类器是来自于我们的inception4d的输出
inception4d的输出矩阵的大小是14x14x528

两个分类器的输出矩阵高度与宽度相同 但是深度不一样

计算一下AveragePool的输出
根据out(size)=in(size)- F(size)+2 P / S+1
可以计算得到out = (14 -5 )/ 3 + 1 = 4
而平均下采样层又不会改变输入矩阵的深度
所以第一个输出分类器AveragePool层的输出是4x4x512
所以第二个输出分类器AveragePool层的输出是4x4x528

采用128个卷积核大小为1*1的卷积层进行卷积处理,目的是为了降低维度,并且使用的是ReLU激活函数

采用节点个数为1024的全连接层,同样使用ReLU激活函数

全连接层与全连接层之间使用dropout函数,以70%的比例,随机失活神经元

采用节点个数为1000的全连接层,这里的节点个数是对应我们的类别个数!!!

通过softmax函数得到我们的概率分布

理解参数


表格数据从上到下 一一对应GoogLeNet的每一层结构的数据
LocalRespNorm层起到的作用不大 可以舍弃掉
辅助分类器的参数前面有详细讲解
池化层的参数表示是类似5x5+2(V) 表示的是5x5的大小 2的步长
而卷积层参数表示是5x5+2(S) 表示的是5x5的大小 2的步长
注意最后avg pool与全连接层直接还有一个dropout函数 是以40%的概率随机失活神经元

inception结构的对应 如下图所示

model.py代码解读

创建GoogLeNet网络之前先创建几个模板文件

首先是我们的BasicConv2d
因为我们在构建我们的卷积层的时候通常是将我们的卷积和我们的ReLU激活函数绑定在一起
所以创建一个常用的class BasicConv2d

定义BasicConv2d

class BasicConv2d(nn.Module):#继承于nn.Module
    def __init__(self, in_channels, out_channels, **kwargs):#参数有输出特征层的深度 输出矩阵特征层的深度 
        super(BasicConv2d, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)#定义卷积层 
        #输入特征矩阵深度就是我in_channels,卷积核的个数就是我们输出特征矩阵的深度
        self.relu = nn.ReLU(inplace=True)#定义ReLU激活函数

    def forward(self, x):#定义一个正向传播过程
        x = self.conv(x)
        x = self.relu(x)
        return x

定义Inception结构

class Inception(nn.Module):#继承自nn.Module
    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
    #参数分析:输入特征矩阵的深度,ch1x1代表#1x1 ch3x3red代表#3x3 reduce   
    #ch5x5red代表#5x5 reduce   ch5x5代表#5x5  pool_pro代表pool proj
        super(Inception, self).__init__()

        self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)
		#分支1   使用BasicConv2d模板(我们输入特征矩阵的深度,采用卷积核的个数,卷积核大小,步距是1是默认的)
        self.branch2 = nn.Sequential(
            BasicConv2d(in_channels, ch3x3red, kernel_size=1),
            BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1)   # 保证输出大小等于输入大小
        )
        #分支2使用nn.Sequential函数
        #使用BasicConv2d(输入特征矩阵的深度,采用卷积核的个数,卷积核大小是1x1,步距是1是默认的)
        #使用BasicConv2d模板(输入特征矩阵的深度这时候就是ch3x3red是上一层输出矩阵的深度,采用卷积核的个数,卷积核大小为3x3,步距是1是默认的)
        #为了保证每个分支所输出的高度和宽度相同,所以必须设置padding为1
        #output_size=(input_size-3+2*1)/1+1=input_size
        

        self.branch3 = nn.Sequential(
            BasicConv2d(in_channels, ch5x5red, kernel_size=1),
            BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2)   # 保证输出大小等于输入大小
        )
        #分支3 使用nn.Sequential函数
        #使用BasicConv2d(输入特征矩阵的深度,采用卷积核的个数,卷积核大小是1x1,步距是1是默认的)
		#使用BasicConv2d模板(输入特征矩阵的深度这时候就是ch5x5red是上一层输出矩阵的深度,采用卷积核的个数,卷积核大小为5x5,步距是1是默认的)
        #为了保证每个分支所输出的高度和宽度相同,所以必须设置padding为2
        #output_size=(input_size-5+2*2)/1+1=input_size
        
        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            BasicConv2d(in_channels, pool_proj, kernel_size=1)
        )
	 	#分支4 使用nn.Sequential函数
	 	#使用nn.MaxPool2d(卷积核大小为3x3,步距是1,padding是1)
	 	#为了保证每个分支所输出的高度和宽度相同,stride设置为1,设置padding为1
		#使用BasicConv2d模板(输入矩阵的深度,采用卷积核的个数是pool_proj,卷积核大小为1x1)   
      
    def forward(self, x): #定义正向传播过程
    #将我们的特征矩阵分别输入到branch1、branch2.。。。 分别得到这四个分支所对应的输出
        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)#使用一个torch.cat函数对我们的四个分支的输出进行合并,第一个参数是输出特征的列表,需要合并的一个维度,我们序呀在需要在深度方向上进行合并 
        #tensor的参数是(batch,channel,高度,宽度)

定义辅助分类器

class InceptionAux(nn.Module):#定义自nn.Module这个父类
    def __init__(self, in_channels, num_classes): #输入特征矩阵的深度 需要进行分类的类别个数
        super(InceptionAux, self).__init__()
        self.averagePool = nn.AvgPool2d(kernel_size=5, stride=3)  # output[batch, 512, 4, 4]/output[batch, 528, 4, 4]/
        #定义一个平均池化下采样层nn.AvgPool2d 池化和大小为5x5 步长为3
        self.conv = BasicConv2d(in_channels, 128, kernel_size=1)  # output[batch, 128, 4, 4]
		#定义第二个卷积层BasicConv2d  输入特征矩阵深度就是我们输入的in_channels,卷积核个数是128,卷积核大小为1x1 output batch不会改变 128是卷积核个数也是输出矩阵的深度128 4x4是输出的大小 是不会改变的 因为使用的卷积核大小是1x1的
        self.fc1 = nn.Linear(2048, 1024) #全连接层1 是输入节点个数是上面输出特征矩阵展平之后的一维向量 128x4x4=2048  输出的节点个数是1024
        self.fc2 = nn.Linear(1024, num_classes)
        #输入节点个数是1024 输出节点个数是我们需要分类的类别个数

    def forward(self, x):#定义一个正向传播过程
        # aux1: N x 512 x 14 x 14, aux2: N x 528 x 14 x 14
        #辅助分类器1输入的特征矩阵的维度 辅助分类器2输入的特征矩阵的维度
        x = self.averagePool(x)
        # aux1: N x 512 x 4 x 4, aux2: N x 528 x 4 x 4
    	#通过辅助分类器1 和辅助分类器2
        x = self.conv(x)
        # N x 128 x 4 x 4
        x = torch.flatten(x, 1)
        #将我们的特征矩阵进行展平处理,从channel维度开始展平
        x = F.dropout(x, 0.5, training=self.training)
        #随即失活函数 以0.5的概率进行失活,原论文中0.7效果不好改成0。5,x是输入特征矩阵
        #当我们实例化一个模型model后,可以通过model.train()和model.eval()来操控模型的状态
        #在model.train()模式下self.training=True,在model.eval()模式下self.training=False
        # N x 2048
        x = F.relu(self.fc1(x), inplace=True)#将我们的全连接层通过relu激活函数
        x = F.dropout(x, 0.5, training=self.training)#通过一个dropout函数0.5比例随机失活
        # N x 1024
        x = self.fc2(x)#全连接层2
        # N x num_classes
        return x


定义我们的GoogLeNet网络

import torch.nn as nn
import torch
import torch.nn.functional as F


class GoogLeNet(nn.Module):
    def __init__(self, num_classes=1000, aux_logits=True, init_weights=False):
    #分类类别个数是1000 是否使用辅助分类器 是否对我们的权重进行初始化
        super(GoogLeNet, self).__init__()
        self.aux_logits = aux_logits
        #是否使用辅助分类器的布尔变量传入到我们的类变量中

        self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
        #卷积层 输入特征矩阵深度是3x3 是RGB图像 卷积核个数64 卷积核大小7x7,stride是2,padding是3
        # padding=3这样做是为了将特征矩阵缩减为原来的一半
        #(224 - 7 + 2*3)/ 2 + 1 =112.5 pytorch默认向下取整,就是112
       
        self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
		#池化核大小为3x3 ceil_mode=True是指向上取整
		
		#此次没有使用localResNorm层
		
        self.conv2 = BasicConv2d(64, 64, kernel_size=1)
        #输入特征矩阵深度就是上一层输出特侦矩阵深度 卷积核大小为64是表中数据给出的
        self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)
        #此次没有使用localResNorm层
        
        self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)
        #使用已经定义好的inception类
        #输入特征矩阵的深度,ch1x1代表#1x1 ch3x3red代表#3x3 reduce   
    	#ch5x5red代表#5x5 reduce   ch5x5代表#5x5  pool_pro代表pool proj 
    	
        self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

		#每一个inception层的输入特征矩阵深度可以在表格数据中观察出来
		#也可以通过上一层inception的四个分支的特征矩阵深度加起来得到
        self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64)
        self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)
        self.inception4c = Inception(512, 128, 128, 256, 24, 64, 64)
        self.inception4d = Inception(512, 112, 144, 288, 32, 64, 64)
        self.inception4e = Inception(528, 256, 160, 320, 32, 128, 128)
        self.maxpool4 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)
        self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)

        if self.aux_logits: #如果使用辅助分类器的话,这个是我们刚刚传入的布尔参数
            self.aux1 = InceptionAux(512, num_classes) #输入特侦矩阵的深度(inception4a的输出)
            self.aux2 = InceptionAux(528, num_classes) #输入特侦矩阵的深度(inception4d的输出)

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  #自适应的平均池化下采样操作 参数所需要输出特征矩阵的高和宽 
        #好处:无论输入的特征矩阵的高和宽是多少 最后的输出都可以固定为1x1
        self.dropout = nn.Dropout(0.4) 
        self.fc = nn.Linear(1024, num_classes)
        #输入的是展平后的向量节点个数是1024,输出节点个数是num_classes
        
        if init_weights:#如果有初始化权重参数就是用 这里的详解在AlexNet模型笔记中
            self._initialize_weights()

    def forward(self, x): #定义下采样层
        # N x 3 x 224 x 224
        x = self.conv1(x)
        # N x 64 x 112 x 112
        x = self.maxpool1(x)
        # N x 64 x 56 x 56
        x = self.conv2(x)
        # N x 64 x 56 x 56
        x = self.conv3(x)
        # N x 192 x 56 x 56
        x = self.maxpool2(x)

        # N x 192 x 28 x 28
        x = self.inception3a(x)
        # N x 256 x 28 x 28
        x = self.inception3b(x)
        # N x 480 x 28 x 28
        x = self.maxpool3(x)
        # N x 480 x 14 x 14
        x = self.inception4a(x)
        # N x 512 x 14 x 14
        if self.training and self.aux_logits:    # eval model lose this layer
        #第一个参数是判断我们的当前处于的训练状态是否在训练状态 然后是是否使用辅助分类器
            aux1 = self.aux1(x)

        x = self.inception4b(x)
        # N x 512 x 14 x 14
        x = self.inception4c(x)
        # N x 512 x 14 x 14
        x = self.inception4d(x)
        # N x 528 x 14 x 14
        if self.training and self.aux_logits:    # eval model lose this layer
         #第一个参数是判断我们的当前处于的训练状态是否在训练状态 然后是是否使用辅助分类器
            aux2 = self.aux2(x)

        x = self.inception4e(x)
        # N x 832 x 14 x 14
        x = self.maxpool4(x)
        # N x 832 x 7 x 7
        x = self.inception5a(x)
        # N x 832 x 7 x 7
        x = self.inception5b(x)
        # N x 1024 x 7 x 7

        x = self.avgpool(x)
        # N x 1024 x 1 x 1
        x = torch.flatten(x, 1)
        # N x 1024
        x = self.dropout(x)
        x = self.fc(x)
        # N x 1000 (num_classes)
        if self.training and self.aux_logits:   # eval model lose this layer
            return x, aux2, aux1
            #返回三个值 一个是我们的主输出值 辅助分类器2的值 辅助分类器1的值
        return x

    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:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0)


train.py

与AlexNet基本相同 只有两点不同

import torch
import torch.nn as nn
from torchvision import transforms, datasets
import torchvision
import json
import matplotlib.pyplot as plt
import os
import torch.optim as optim
from model import GoogLeNet

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)

data_transform = {
    "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                 transforms.RandomHorizontalFlip(),
                                 transforms.ToTensor(),
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
    "val": transforms.Compose([transforms.Resize((224, 224)),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}

data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # get data root path
image_path = data_root + "/data_set/flower_data/"  # flower data set path

train_dataset = datasets.ImageFolder(root=image_path + "train",
                                     transform=data_transform["train"])
train_num = len(train_dataset)

# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
flower_list = train_dataset.class_to_idx
cla_dict = dict((val, key) for key, val in flower_list.items())
# write dict into json file
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)

batch_size = 32
train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=batch_size, shuffle=True,
                                           num_workers=0)

validate_dataset = datasets.ImageFolder(root=image_path + "val",
                                        transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
                                              batch_size=batch_size, shuffle=False,
                                              num_workers=0)

# test_data_iter = iter(validate_loader)
# test_image, test_label = test_data_iter.next()

# net = torchvision.models.googlenet(num_classes=5)
# model_dict = net.state_dict()
# pretrain_model = torch.load("googlenet.pth")
# del_list = ["aux1.fc2.weight", "aux1.fc2.bias",
#             "aux2.fc2.weight", "aux2.fc2.bias",
#             "fc.weight", "fc.bias"]
# pretrain_dict = {k: v for k, v in pretrain_model.items() if k not in del_list}
# model_dict.update(pretrain_dict)
# net.load_state_dict(model_dict)
net = GoogLeNet(num_classes=5, aux_logits=True, init_weights=True)
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0003)

best_acc = 0.0
save_path = './googleNet.pth'
for epoch in range(30):
    # train
    net.train()
    running_loss = 0.0
    for step, data in enumerate(train_loader, start=0):
        images, labels = data
        optimizer.zero_grad()
        logits, aux_logits2, aux_logits1 = net(images.to(device))
        loss0 = loss_function(logits, labels.to(device))
        loss1 = loss_function(aux_logits1, labels.to(device))
        loss2 = loss_function(aux_logits2, labels.to(device))
        loss = loss0 + loss1 * 0.3 + loss2 * 0.3
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        # print train process
        rate = (step + 1) / len(train_loader)
        a = "*" * int(rate * 50)
        b = "." * int((1 - rate) * 50)
        print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
    print()

    # validate
    net.eval()
    acc = 0.0  # accumulate accurate number / epoch
    with torch.no_grad():
        for val_data in validate_loader:
            val_images, val_labels = val_data
            outputs = net(val_images.to(device))  # eval model only have last output layer
            predict_y = torch.max(outputs, dim=1)[1]
            acc += (predict_y == val_labels.to(device)).sum().item()
        val_accurate = acc / val_num
        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(net.state_dict(), save_path)
        print('[epoch %d] train_loss: %.3f  test_accuracy: %.3f' %
              (epoch + 1, running_loss / step, val_accurate))

print('Finished Training')

与AlexNet不同的两点

1.在定义我们的模型部分

net = GoogLeNet(num_classes=5, aux_logits=True, init_weights=True)
#采用辅助分类器

2.在我们的损失函数部分

  logits, aux_logits2, aux_logits1 = net(images.to(device)) 
  #将我们的一批数据 训练图像传入到我们的网络当中,会得到三个输出
  #一个主输出,两个辅助分类器的输出
        loss0 = loss_function(logits, labels.to(device))
        #loss_function函数将我们的主输出与真实标签的之间的一个损失
        loss1 = loss_function(aux_logits1, labels.to(device))
        #loss_function函数将我们的辅助分类器1与真实标签的之间的一个损失
        loss2 = loss_function(aux_logits2, labels.to(device))
        #loss_function函数将我们的辅助分类器2与真实标签的之间的一个损失
        loss = loss0 + loss1 * 0.3 + loss2 * 0.3
        #原论文中就是以0.3的权重配置到损失当中
        loss.backward() #将损失进行一个反向传播
        optimizer.step()#通过我们的优化器更新我们的模型参数
#之前网络当中只有一个输出 现在采用了两个辅助分类器 ,所以一共有三个输出

3.注意的点net.train()和net.eval()
当我们的网络在train的过程中 training参数值就是True 返回一个主输出和两个辅助输出
当我们的网络在eval的过程中 training参数值就是False 只返一个主输出 因为验证环节不需要辅助分类器的结果 所以可以使用这个参数屏蔽这两个辅助分类器的结果 可以减少参数运算量

predict.py

大致与AlexNet模型的predict.py部分类似

import torch
from model import GoogLeNet
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import json

data_transform = transforms.Compose(
    [transforms.Resize((224, 224)),
     transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# load image
img = Image.open("../tulip.jpg")
plt.imshow(img)
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)

# read class_indict
try:
    json_file = open('./class_indices.json', 'r')
    class_indict = json.load(json_file)
except Exception as e:
    print(e)
    exit(-1)

# create model
model = GoogLeNet(num_classes=5, aux_logits=False)
# load model weights
model_weight_path = "./googleNet.pth"
missing_keys, unexpected_keys = model.load_state_dict(torch.load(model_weight_path), strict=False)
model.eval()
with torch.no_grad():
    # predict class
    output = torch.squeeze(model(img))
    predict = torch.softmax(output, dim=0)
    predict_cla = torch.argmax(predict).numpy()
print(class_indict[str(predict_cla)])
plt.show()

注意的点
载入模型的部分不需要构建辅助分类器 aux_logits=False

# create model
model = GoogLeNet(num_classes=5, aux_logits=False)

但是我们保存模型的时候,已经将我们的参数保存在我们的模型当中,所以我们要将我们的strict参数设为False,默认为True

strict=True就意味着会精准匹配我们当前模型和我们所需载入的权重模型的一个结构

因为我们现在的所搭建的GoogLeNet是没有辅助分类器的,所以和我们保存的模型结构是缺一些层结构

missing_keys, unexpected_keys = model.load_state_dict(torch.load(model_weight_path), strict=False)
#在unexpe_keys会有一系列层 这些层都是属于那些辅助分类器的
粤 ICP 备 2020080455 号