【第2周】卷积神经网络

1 MNIST 数据集分类

初步学习到卷积神经网络(CNN)并且用于手写数字识别。

PyTorch里包含了 MNIST, CIFAR10 等常用数据集,调用 torchvision.datasets 即可把这些数据由远程下载到本地,下面给出MNIST的使用方法:

torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False)

  • root 为数据集下载到本地后的根目录,包括 training.pt 和 test.pt 文件
  • train,如果设置为True,从training.pt创建数据集,否则从test.pt创建。
  • download,如果设置为True, 从互联网下载数据并放到root文件夹下
  • transform, 一种函数或变换,输入PIL图片,返回变换之后的数据。
  • target_transform 一种函数或变换,输入目标,进行变换。

另外值得注意的是,DataLoader是一个比较重要的类,提供的常用操作有:batch_size(每个batch的大小), shuffle(是否进行随机打乱顺序的操作), num_workers(加载数据的时候使用几个子进程)

Module 是一个非常重要的类,它是用来构建神经网络模型的基础。Module 类是所有自定义神经网络模型的父类,通过继承该类,可以按需实现自己的网络结构。

Module 类提供了一些重要的方法,包括:

  • __init__: 初始化方法,用于定义网络结构和初始化参数。
  • forward: 前向传播方法,定义了模型的前向计算逻辑。
  • parameters: 获取模型参数,返回一个迭代器,可以用来访问所有模型参数。
  • zero_grad: 将模型参数的梯度清零。
  • to: 将模型移动到指定的设备(如 CPU 或 GPU)。

如果在自定义的 Module 类中定义了 forward 方法,PyTorch 将自动为模型实现反向传播算法。所以只需要关注前向传播的逻辑,PyTorch 负责计算梯度并执行反向传播步骤。

在计算图中,forward 方法定义了从输入到输出的计算过程,PyTorch 会自动跟踪这些计算过程来构建动态计算图。然后,在调用 loss.backward() 方法时,PyTorch 使用计算图和定义的损失函数来计算模型参数的梯度。最后,可以通过优化器(如 torch.optim)来使用这些梯度更新模型的参数。

需要注意的是,如果在自定义的 Module 类中定义了 forward 方法,一定要确保在该方法中使用了可微的操作(如 PyTorch 提供的函数或模块),以便 PyTorch 能够正确跟踪和计算梯度。否则,在计算反向传播时可能会出现梯度丢失或错误的情况。

class FC2Layer(nn.Module):
    def __init__(self, input_size, n_hidden, output_size):
        # nn.Module子类的函数必须在构造函数中执行父类的构造函数
        # 下式等价于nn.Module.__init__(self)
        super(FC2Layer, self).__init__()
        self.input_size = input_size
        # 这里直接用 Sequential 就定义了网络,注意要和下面 CNN 的代码区分开
        self.network = nn.Sequential(
            nn.Linear(input_size, n_hidden),
            nn.ReLU(),
            nn.Linear(n_hidden, n_hidden),
            nn.ReLU(),
            nn.Linear(n_hidden, output_size),
            nn.LogSoftmax(dim=1)
        )
    def forward(self, x):
        # view一般出现在model类的forward函数中,用于改变输入或输出的形状
        # x.view(-1, self.input_size) 的意思是多维的数据展成二维
        # 代码指定二维数据的列数为 input_size=784,行数 -1 表示我们不想算,电脑会自己计算对应的数字
        # 在 DataLoader 部分,我们可以看到 batch_size 是64,所以得到 x 的行数是64
        # 大家可以加一行代码:print(x.cpu().numpy().shape)
        # 训练过程中,就会看到 (64, 784) 的输出,和我们的预期是一致的

        # forward 函数的作用是,指定网络的运行过程,这个全连接网络可能看不啥意义,
        # 下面的CNN网络可以看出 forward 的作用。
        x = x.view(-1, self.input_size)
        return self.network(x)

class CNN(nn.Module):
  def __init__(self,input_size,n_feature,output_size):
    super(CNN,self).__init__()
    self.n_feature = n_feature
    self.conv1 = nn.Conv2d(in_channels=1,out_channels = n_feature,kernel_size = 5)
    self.conv2 = nn.Conv2d(n_feature, n_feature, kernel_size=5)
    self.fc1 = nn.Linear(n_feature*4*4, 50)
    self.fc2 = nn.Linear(50, 10)

# 下面的 forward 函数,定义了网络的结构,按照一定顺序,把上面构建的一些结构组织起来
# 意思就是,conv1, conv2 等等的,可以多次重用
  def forward(self, x, verbose=False):
      x = self.conv1(x)
      x = F.relu(x)
      x = F.max_pool2d(x, kernel_size=2)
      x = self.conv2(x)
      x = F.relu(x)
      x = F.max_pool2d(x, kernel_size=2)
      x = x.view(-1, self.n_feature*4*4)
      x = self.fc1(x)
      x = F.relu(x)
      x = self.fc2(x)
      x = F.log_softmax(x, dim=1)
      return x

定义完训练函数和测试函数之后,就可以分别在小型全连接网络上和卷积神经网络上面训练,并且两个网络具有相同数量的模型参数。对比结果如下图所示 

小型全连接网络训练结果
卷积神经网络训练结果

然后我们可以发现由于卷积和池化,CNN能够更好的挖掘图像中的信息,与全连接网络相比,CNN 具有更强的特征提取能力和参数共享机制,能够更好地捕捉到输入数据中的本地特征。

考虑到CNN在卷积与池化上的优良特性,如果我们把图像中的像素打乱顺序,这样卷积和池化就难以发挥作用了,为了验证这个想法,我们把图像中的像素打乱顺序再试试。然后分别对比两种网络打乱顺序前后的准确度。

打乱顺序前小型全连接网络训练结果
打乱顺序后小型全连接网络训练结果

打乱顺序前卷积神经网络训练结果
打乱顺序后卷积神经网络训练结果

从打乱像素顺序的实验结果来看,全连接网络的性能基本上没有发生变化,但是卷积神经网络的性能明显下降。CNN 在处理图像时利用了卷积层中的局部连接和权值共享的特性,通过卷积运算捕捉图像中的局部特征。这些局部特征的位置信息在卷积运算中是被保留下来的,因为卷积运算是在整个输入上进行的。因此,输入图像的顺序对于 CNN 来说通常是有意义的。

2 CIFAR10 数据集分类

对于视觉数据,PyTorch 创建了一个叫做 totchvision 的包,该包含有支持加载类似Imagenet,CIFAR10,MNIST 等公共数据集的数据加载模块 torchvision.datasets 和支持加载图像数据数据转换模块 torch.utils.data.DataLoader。

下面将使用CIFAR10数据集,它包含十个类别:‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。CIFAR-10 中的图像尺寸为3x32x32,也就是RGB的3层颜色通道,每层通道内的尺寸为32*32。

首先,加载并归一化 CIFAR10 使用 torchvision 。torchvision 数据集的输出是范围在[0,1]之间的 PILImage,我们将他们转换成归一化范围为[-1,1]之间的张量 Tensors。

代码中说的是 0.5,怎么就变化到[-1,1]之间了?PyTorch源码中是这么写的:

input[channel] = (input[channel] - mean[channel]) / std[channel]

这样就是:((0,1)-0.5)/0.5=(-1,1)。

import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 注意下面代码中:训练的 shuffle 是 True,测试的 shuffle 是 false
# 训练时可以打乱顺序增加多样性,测试是没有必要
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
                                          shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=8,
                                         shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

接下来同样需要定义网络、损失函数、和优化器,然后就开始训练网络了。当把训练成功的网络放在整个数据集上的时候,表现就不太理想了。

correct = 0
total = 0

for data in testloader:
    images, labels = data
    images, labels = images.to(device), labels.to(device)
    outputs = net(images)
    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' % (
    100 * correct / total))

Accuracy of the network on the 10000 test images:63%

3 使用 VGG16 对 CIFAR10 分类

VGG16的网络结构如下图所示:

VGG16示意图

16层网络的结节信息如下:

  • 01:Convolution using 64 filters
  • 02: Convolution using 64 filters + Max pooling
  • 03: Convolution using 128 filters
  • 04: Convolution using 128 filters + Max pooling
  • 05: Convolution using 256 filters
  • 06: Convolution using 256 filters
  • 07: Convolution using 256 filters + Max pooling
  • 08: Convolution using 512 filters
  • 09: Convolution using 512 filters
  • 10: Convolution using 512 filters + Max pooling
  • 11: Convolution using 512 filters
  • 12: Convolution using 512 filters
  • 13: Convolution using 512 filters + Max pooling
  • 14: Fully connected with 4096 nodes
  • 15: Fully connected with 4096 nodes
  • 16: Softmax

当使用该模型的时候,这里的 transform,dataloader 和之前定义的有所不同,提供了更丰富的数据预处理操作(随机裁剪、水平翻转)。同时,数据加载器中的参数(如批次大小、是否打乱数据)也有所不同,更适合实际的训练和测试需求。

简易VCG网络定义

class VGG(nn.Module):
    def __init__(self):
        super(VGG, self).__init__()
        self.cfg = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']
        self.features = self._make_layers(cfg)
        self.classifier = nn.Linear(2048, 10)

    def forward(self, x):
        out = self.features(x)
        out = out.view(out.size(0), -1)
        out = self.classifier(out)
        return out

    def _make_layers(self, cfg):
        layers = []
        in_channels = 3
        for x in cfg:
            if x == 'M':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
            else:
                layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
                           nn.BatchNorm2d(x),
                           nn.ReLU(inplace=True)]
                in_channels = x
        layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
        return nn.Sequential(*layers)

再次训练之后就可以测试验证准确率了,代码和之前是一样的。

  可以看到,使用一个简化版的 VGG 网络,就能够显著地将准确率由 64%,提升到 84.13%。

4 问题总结

4.1  dataloader 里面 shuffle 取不同值有什么区别?

        shuffle=True

  • 在每个 epoch 开始时,数据加载器会对数据进行随机打乱,即将数据集中的样本顺序打乱。
  • 打乱样本顺序可以帮助模型避免对数据顺序的依赖,提高模型的泛化能力。

        shuffle=False

  • 数据在每个 epoch 中按照原始顺序进行加载,默认的加载顺序是数据集中样本的顺序。
  • 适用于验证或测试阶段,可以保持 samples 的原始顺序。

4.2 transform 里,取了不同值,这个有什么区别?

取值为 (0.5, 0.5, 0.5) 和 (0.5, 0.5, 0.5)

  • 这表示对每个像素通道(红、绿、蓝)进行归一化处理。其中,(0.5, 0.5, 0.5) 是用于均值的归一化参数,(0.5, 0.5, 0.5) 是用于标准差的归一化参数。
  • 归一化操作将像素值缩放到 -1 到 1 之间的范围,有助于提高模型的训练稳定性和收敛性。

取值为 (0.4914, 0.4822, 0.4465) 和 (0.2023, 0.1994, 0.2010)

  • 这表示对每个像素通道进行均值和标准差的归一化处理。这些参数应该是根据训练数据集的统计特征进行计算得到的。
  • 通过使用训练数据集的均值和标准差进行归一化处理,有助于使输入数据在各个通道上有更一致的尺度,从而提高模型的训练效果。

4.3 epoch 和 batch 的区别?

数据量:

  • Batch:每个批次中包含的样本数量,例如一个批次中有 32 个样本。
  • Epoch:表示模型接触整个数据集的次数,所有批次的样本都被遍历了一次。

训练过程:

  • Batch:一个批次被传递给模型进行前向传播、计算损失、反向传播和参数更新。
  • Epoch:一个 epoch 表示将整个数据集的所有批次都传递给模型进行一次完整的训练。

4.4 1x1的卷积和 FC 有什么区别?主要起什么作用?

  • 尺寸和计算量:1x1卷积在空间维度上只有1x1的卷积核,计算量相对较小;而全连接层需要连接所有的输入和输出节点,计算量较大。
  • 参数共享:1x1卷积在卷积计算时可利用参数共享,减少参数量;全连接层每个输入和输出都有不同的参数。
  • 特征提取:1x1卷积在通道维度上进行计算,可以通过特定的权值计算对通道进行特征的加权组合;全连接层可以通过节点之间的权重来进行特征加权组合。

4.5 residual leanring 为什么能够提升准确率?

残差学习通过引入残差连接,解决了深层网络中的梯度消失和优化困难问题,从而提升了模型的准确率。它改善了梯度传播,使得网络能够更深层地学习和建模,同时增强了网络的表示能力。这种方法可以在计算机视觉任务中取得很好的效果,例如图像分类、目标检测和语义分割等。

4.6 代码练习二里,网络和1989年 Lecun 提出的 LeNet 有什么区别?

代码练习二中的网络使用Adam优化器进行参数更新,而LeNet最初使用的是梯度下降等传统的优化算法,且代码练习二中的网络参数量更多。代码练习二中的网络的第一个全连接层(fc1)有120个输出特征,而LeNet的第一个全连接层只有84个输出特征。因此,代码练习二中的网络的参数量更大,模型容量更高,可以学习到更复杂的特征表示。

4.7 代码练习二里,卷积以后feature map 尺寸会变小,如何应用 Residual Learning?

  • 如果特征图的尺寸变小导致损失了空间信息,可以通过在卷积操作中使用适当的填充或调整步幅来保持特征图尺寸不变。
  • 填充可以在卷积操作中添加边缘像素,使特征图尺寸保持不变。调整步幅可以控制卷积窗口在特征图上滑动的距离,从而控制特征图尺寸的变化。

4.8 有什么方法可以进一步提升准确率?

  • 通过在训练过程中对输入数据进行随机变换和扩充,如旋转、平移、缩放、翻转等,来增加数据的多样性。这样可以增加模型的泛化能力。
  • 增加模型的深度、宽度或参数量,这样可以增加模型的表达能力和学习能力,充分挖掘更多的特征和语义信息。
  • 尝试不同的优化器、学习率、正则化方法等来优化模型的训练过程。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值