手搓GoogLeNet来对CIFAR100数据集进行分类

1.GoogLeNet介绍

GoogLeNet的名字不是GoogleNet,而是GoogLeNet,这是为了致敬LeNet。GoogLeNet和AlexNet/VGGNet这类依靠加深网络结构的深度的思想不完全一样。GoogLeNet在加深度的同时做了结构上的创新,引入了一个叫做Inception的结构来代替之前的卷积加激活的经典组件。GoogLeNet在ImageNet分类比赛上的Top-5错误率降低到了6.7%。

2.Inception Module

GoogLeNet中的基础卷积块叫作Inception块,Inception块在结构上比较复杂,如下图所示:

 

Inception块里有4条并行的线路。前3条线路使用窗口大小分别是1×1、3×3和5×5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1×1卷积来减少输入通道数,以降低模型复杂度。第4条线路则使用3×3最大池化层,后接1×1卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并向后进行传输。

Inception Module的代码实现如下,这段代码定义了一个 PyTorch 模块,用于实现 Inception 神经网络架构。Inception 是一种用于图像分类的流行卷积神经网络架构:

class Inception(nn.Module):
    def __init__(self, in_planes, kernel_1_x, kernel_3_in, kernel_3_x, kernel_5_in, kernel_5_x, pool_planes):
        super(Inception, self).__init__()
        # 1x1 conv branch
        self.b1 = nn.Sequential(
            nn.Conv2d(in_planes, kernel_1_x, kernel_size=1),
            nn.BatchNorm2d(kernel_1_x),
            nn.ReLU(True),
        )

        # 1x1 conv -> 3x3 conv branch
        self.b2 = nn.Sequential(
            nn.Conv2d(in_planes, kernel_3_in, kernel_size=1),
            nn.BatchNorm2d(kernel_3_in),
            nn.ReLU(True),
            nn.Conv2d(kernel_3_in, kernel_3_x, kernel_size=3, padding=1),
            nn.BatchNorm2d(kernel_3_x),
            nn.ReLU(True),
        )

        # 1x1 conv -> 5x5 conv branch
        self.b3 = nn.Sequential(
            nn.Conv2d(in_planes, kernel_5_in, kernel_size=1),
            nn.BatchNorm2d(kernel_5_in),
            nn.ReLU(True),
            nn.Conv2d(kernel_5_in, kernel_5_x, kernel_size=3, padding=1),
            nn.BatchNorm2d(kernel_5_x),
            nn.ReLU(True),
            nn.Conv2d(kernel_5_x, kernel_5_x, kernel_size=3, padding=1),#2 3*3=>1 5*5
            nn.BatchNorm2d(kernel_5_x),
            nn.ReLU(True),
        )

        # 3x3 pool -> 1x1 conv branch
        self.b4 = nn.Sequential(
            nn.MaxPool2d(3, stride=1, padding=1),
            nn.Conv2d(in_planes, pool_planes, kernel_size=1),
            nn.BatchNorm2d(pool_planes),
            nn.ReLU(True),
        )

    def forward(self, x):
        y1 = self.b1(x)
        y2 = self.b2(x)
        y3 = self.b3(x)
        y4 = self.b4(x)
        return torch.cat([y1,y2,y3,y4], 1)

该模块由四个并行分支组成,对输入张量以不同的方式进行处理,然后沿着通道维度将输出进行拼接。

第一个分支对输入张量应用了 1x1 卷积,然后进行批归一化和 ReLU 激活。

第二个分支对输入张量应用了 1x1 卷积,然后进行批归一化和 ReLU 激活,接着对第一个卷积的输出应用了带有 padding 1 的 3x3 卷积,然后进行批归一化和 ReLU 激活。

第三个分支对输入张量应用了 1x1 卷积,然后进行批归一化和 ReLU 激活,接着对第一个卷积的输出应用了带有 padding 1 的 3x3 卷积,然后进行批归一化和 ReLU 激活,然后对第二个卷积的输出应用了带有 padding 1 的 3x3 卷积,然后进行批归一化和 ReLU 激活。

第四个分支对输入张量应用了带有 stride 1 和 padding 1 的 3x3 最大池化,然后对池化后的张量应用了 1x1 卷积,进行批归一化和 ReLU 激活。

四个分支的输出沿着通道维度进行拼接,并作为模块的输出返回。该模块的输入是一个形状为 (batch_size, in_planes, height, width) 的张量,其中 in_planes 是输入通道数,输出是一个形状为 (batch_size, kernel_1_x + kernel_3_x + kernel_5_x + pool_planes, height, width) 的张量,其中 kernel_1_x、kernel_3_x、kernel_5_x 和 pool_planes 是四个分支的输出通道数。

3.GoogLeNet模型

GoogLeNet主要由Inception模块构成,如下图所示:

 

整个网络架构我们分为五个模块,每个模块之间使用步幅为2的3×3最大池化层来减小输出高宽。

GoogLeNet的网络设计如下表:

Pre-layers1模块

 

这段代码定义了一个 Pre-layers1 模块,包含了三个层,用于实现输入张量的预处理。这个模块包含以下三个层:

  1. 一个 2D 卷积层:这个卷积层使用了 7x7 的卷积核,处理输入张量的每一个空间位置。其中,参数 in_channels 表示输入张量的通道数,参数 64 表示输出通道数,参数 padding=2 表示在输入张量四周各添加两列/行的零填充,这样可以保持卷积后输出的张量大小不变。
  2. 一个批归一化层:该层对卷积层的输出进行批归一化处理,即将每个通道的输出值减去均值并除以标准差,以加速网络收敛并提高泛化性能。
  3. 一个 ReLU 激活函数:该层对批归一化后的输出进行 ReLU 操作,即将所有负数输出值设为零,保留所有非负数输出值。

 

self.pre_layers2这个模块是在 self.pre_layers1 模块的基础上继续对输出张量进行处理。这个模块包含以下三个层:

  1. 一个 2D 卷积层:这个卷积层使用了 3x3 的卷积核,处理输入张量的每一个空间位置。其中,参数 64 表示输入通道数,参数 192 表示输出通道数,参数 padding=1 表示在输入张量四周各添加一列/行的零填充,这样可以保持卷积后输出的张量大小不变。
  2. 一个批归一化层:该层对卷积层的输出进行批归一化处理,即将每个通道的输出值减去均值并除以标准差,以加速网络收敛并提高泛化性能。
  3. 一个 ReLU 激活函数:该层对批归一化后的输出进行 ReLU 操作,即将所有负数输出值设为零,保留所有非负数输出值。

模块3

 


 

 

模块3定义了三个模块:a3、b3和max_pool。

a3和b3都是基于Inception模块的,分别输入192和256个特征图,使用不同的卷积核大小和个数来进行特征提取。具体地,a3使用了一个1x1卷积核、一个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层,它们的输出通过在通道维度上进行拼接而合并在一起;b3使用了两个1x1卷积核、两个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层,同样通过拼接来合并它们的输出。

max_pool是一个最大池化层,输入特征图的大小为3x3,步幅为2,填充为1。

模块4

模块4定义了5个模块,每个模块都是基于Inception模块的,分别命名为a4、b4、c4、d4和e4。

这些模块的输入特征图的通道数逐步增加,分别为480、512、512、512和528。它们都是由不同的卷积核大小和数量组成的,用于提取不同层次的特征:

  1. a4使用了一个1x1卷积核、一个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层。
  2. b4使用了一个1x1卷积核、一个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层。
  3. c4使用了一个1x1卷积核、两个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层。
  4. d4使用了一个1x1卷积核、两个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层。
  5. e4使用了一个1x1卷积核、一个3x3卷积核和一个5x5卷积核以及一个3x3最大池化层。

 模块5

 

 

模块5定义了 GoogLeNet 模型的最后几层。模型的输入特征图的深度为 832。模型将输入特征图通过 Inception 模块 a5 和 b5 进行特征提取。最后,模型通过一个平均池化层对特征图进行降维,然后通过一个 dropout 层减少过拟合,最终通过一个全连接层将特征向量映射到 100 维的输出向量上。

具体来说,这里定义了 Inception 模块 a5 和 b5,它们的参数配置和之前定义的 Inception 模块类似。然后,通过一个平均池化层对特征图进行降维,将特征图的高和宽降至 1,然后通过一个 dropout 层减少过拟合。最后,通过一个线性全连接层将特征向量映射到 100 维的输出向量上。

 

关键函数

在模型评估部分,新增加了分类结果的可视化;在模型训练部分中,新增了对验证集的数据验证。

def test_model(net,testloader,criterion,device):
    ......
    # 随机选择 5*5 张图像进行可视化
    images, labels = iter(testloader).__next__()
    outputs = net(images.to(device))
    _, predicted = torch.max(outputs, 1)

    fig, axes = plt.subplots(nrows=5, ncols=5, figsize=(10, 10))

    for i, ax in enumerate(axes.flat):
        # 显示图像
        ax.imshow(np.transpose(images[i].cpu().numpy(), (1, 2, 0)))

        # 判断预测结果是否与真实标签相同
        predicted_label = class_names[predicted[i]]
        true_label = class_names[labels[i]]
        if predicted_label == true_label:
            label_color = 'green'
            label_text = 'Correct'
        else:
            label_color = 'red'
            label_text = 'Wrong'

        # 设置图像标题为分类名称和结果标签
        ax.set_title(f"Predicted: {predicted_label} \n results: {label_text} \n True: {true_label}", color=label_color)

    plt.tight_layout()
    plt.show()

def train_model(net, criterion, optimizer, trainloader, valloader, device,epochs=30):
    # Train the model
    .......
    for epoch in range(epochs):  # loop over the dataset multiple times
        print("Epoch[{}/{}]:".format(epoch + 1, epochs))
        ......
        for i, data in enumerate(trainloader, 0):
            # get the inputs
            inputs, labels = data[0].to(device), data[1].to(device)

            # zero the parameter gradients
            optimizer.zero_grad()

            # forward + backward + optimize
            outputs = net(inputs)
            loss = criterion(outputs, labels)
            acc = accuracy(outputs, labels)
            loss.backward()
            optimizer.step()

            # print statistics
            running_train_iter_loss += loss.item()
            running_train_iter_acc += acc.item()
            running_train_loss += loss.item()
            running_train_acc += acc.item()

            if i % 100 == 99:  # print every 100 mini-batches
                print('[%d, %5d] train_loss: %.3f, train_accuracy: %.3f' %
                      (epoch + 1, i + 1, running_train_iter_loss / 100, running_train_iter_acc / 100))
                train_loss_iter_list.append(running_train_iter_loss / 100)
                train_acc_iter_list.append(running_train_iter_acc / 100)
                running_train_iter_loss = 0.0
                running_train_iter_acc = 0.0

        train_loss_list.append(running_train_loss / len(trainloader))
        train_acc_list.append(running_train_acc / len(trainloader))
        print('Epoch[ %d / %d ] : train_loss: %.3f, train_accuracy: %.3f' % (epoch + 1, epochs, running_train_loss / len(trainloader), running_train_acc / len(trainloader)))


        # Evaluate the model on the validation set
        running_val_loss = 0.0
        running_val_acc = 0.0
        with torch.no_grad():
            for data in valloader:
                inputs, labels = data[0].to(device), data[1].to(device)
                outputs = net(inputs)
                val_loss = criterion(outputs, labels)
                val_acc = accuracy(outputs, labels)
                running_val_loss += val_loss.item()
                running_val_acc += val_acc.item()
        val_loss_list.append(running_val_loss / len(valloader))
        val_acc_list.append(running_val_acc / len(valloader))
        print('Epoch[ %d / %d ] : val_loss: %.3f, val_accuracy: %.3f' %
              (epoch+1, epochs, running_val_loss / len(valloader), running_val_acc / len(valloader)))

    print('Finished Training')
#plot
......

 

实验结果

Epochs=30,learning rate=1e-1

训练准确率(Train accuracy)和训练损失值(Train loss)随模型迭代次数的变化趋势图:

训练准确率(Train accuracy)和验证准确率(Validation accuracy)随训练轮数(epoch)的变化趋势图:

 

训练损失值(Train loss)和验证损失值(Validation loss)随训练轮数(epoch)的变化趋势图:

 

在10k测试集上测试得到模型的最终准确率为38%,随机抽取5*5的可视化结果图,结果显示如图:

 

可以看出训练的结果效果不理想,在给定参数条件下,测试集的准确率仅仅达到38%,这是非常不理想的,因此后续采取了调整模型结构和参数的措施,进一步优化模型。 

 

Epochs=20,learning rate=1e-3

训练准确率(Train accuracy)和训练损失值(Train loss)随模型迭代次数的变化趋势图:

训练准确率(Train accuracy)和验证准确率(Validation accuracy)随训练轮数(epoch)的变化趋势图: 

 

训练损失值(Train loss)和验证损失值(Validation loss)随训练轮数(epoch)的变化趋势图:

 在10k测试集上测试得到模型的最终准确率为60%,随机抽取5*5的可视化结果图,结果显示如图:

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值