• 机器学习从入门到放弃:卷积神经网络CNN(二)


    一、前言

      通过上一篇文章,我们大概了解了卷积是什么,并且分析了为什么卷积能在图像识别上起到巨大的作用。接下来,废话不多话,我们自己尝试动手搭建一个简易的CNN网络。

     

    二、准备工作

      在开始的时候,我们首先概括一下卷积所需要进行的工作:

    1. 定义一个卷积核:卷积核是一个小的矩阵(例如3x3或5x5),包含一些数字。这个卷积核的作用是在图像中识别特定类型的特征,例如边缘、线条等,也可能是难以描述的抽象特征。
    2. 卷积核滑过图像:卷积操作开始时,卷积核会被放置在图像的左上角。然后,它会按照一定的步长(stride)在图像上滑动,可以是从左到右,也可以是从上到下。步长定义了卷积核每次移动的距离。
    3. 计算点积:在卷积核每个位置,都会计算卷积核和图像对应部分的点积。这就是将卷积核中的每个元素与图像中对应位置的像素值相乘,然后将所有乘积相加。
    4. 生成新的特征图:每次计算的点积结果被用来构建一个新的图像,也称为特征图或卷积图。
    5. 重复以上过程:通常在一个 CNN 中,我们会有多个不同的卷积核同时进行卷积操作。这意味着我们会得到多个特征图,每个特征图捕捉了原始图像中的不同特征。

      上图中间的矩阵就是所谓的卷积核,又称为滤波器。这个滤波器可以帮助我们观测到一定区域大小的像素信息,并且通过卷积计算,变成”低频“的信息特征,比如上面我们提到的一些图像的边缘,纹理等等。当权重系数(卷积核)的参数改变时,它可以提取的特征类型也会改变。所以训练卷积神经网络时,实质上训练的是卷积核的参数。如下图所示,是一次卷积计算的过程:

      一个内核从图像的左上角开始滑动,将核所覆盖的像素值与相应的核值相乘,并对乘积求和。结果被放置在新图像中与核的中心相对应的点处。上面的235其实就是计算所得出的灰度值,当一个内核走完整个图片之后,得出的结果大概是这样子:

     

    三、CNN架构

      当只有一层CNN结构时,一般都会有如下几个层级,来帮助我们进行一次卷积,训练对应的特征。

    • 输入层(Input Layer)

        输入层负责接收原始数据,例如图像。每个节点对应输入数据的一个特征。

    • 卷积层(Convolutional Layer)

        卷积层是CNN的核心。它通过应用卷积操作来提取图像中的特征。每个卷积层包含多个卷积核(也称为滤波器),每个卷积核负责检测输入中的不同特征。卷积操作通过滑动卷积核在输入上进行计算,并生成特征图。

    • 激活函数层(Activation Layer)

        在卷积层之后,一般会添加激活函数,例如ReLU(Rectified Linear Unit),用于引入非线性性。这有助于模型学习更复杂的模式和特征。

    • 池化层(Pooling Layer)

        池化层用于减小特征图的空间维度,降低计算复杂度,并减少过拟合风险。常见的池化操作包括最大池化和平均池化。

    • 全连接层(Fully Connected Layer)

        全连接层将前面层的所有节点与当前层的所有节点连接。这一层通常用于整合前面层提取的特征,并生成最终的输出。在分类问题中,全连接层通常输出类别的概率分布。

    • 输出层(Output Layer)

        输出层给出网络的最终输出,例如分类的概率分布。通常使用softmax函数来生成概率分布。

     

      输入层就不用解释了,毕竟在全连接网络中我们已经对它有了一定的了解,我们首先看看在卷积层中,具体是怎么实现的。在我们接下来要做的 MNIST 手写数字识别的数据集中,我们用其中的图片来举例,例如 8 这个图片:

      在卷积层中左边黑色为原图,中间为卷积层,右边为卷积后的输出。我们可以注意到,当我们是不用的卷积核时,所对应的结果是不同的。

      

      上面第一种卷积核所有元素值相同,所以它可以计算输入图像在卷积核覆盖区域内的平均灰度值。这种卷积核可以平滑图像,消除噪声,但会使图像变得模糊。第二种卷积核可以检测图像中的边缘,可以看到输入的8的边缘部分颜色更深一些,在更大的图片中这种边缘检测的效果会更明显。 

      需要注意的是,虽然上边说道不同的卷积核有着不同的作用,但是在卷积神经网络中,卷积核并不是手动设计出来的,而是通过数据驱动的方式学习得到的。这就是说,我们并不需要人工设计出特定的卷积核来检测边缘、纹理等特定的特征,而是让模型自己从训练数据中学习这些特征,即模型可以自动从复杂数据中学习到抽象和复杂的特征,这些特征可能人工设计难以达到。

      在卷积过程中需要注意的两个参数是:步长和零填充。如果两者优化得当,可以让CNN的效果更加好。

      1. 步长 - Strade 

      在卷积神经网络(CNN)中,"步长"(stride)是一个重要的概念。步长描述的是在进行卷积操作时,卷积核在输入数据上移动的距离。在两维图像中,步长通常是一个二元组,分别代表卷积核在垂直方向(高度)和水平方向(宽度)移动的单元格数。例如,步长为1意味着卷积核在每次移动时,都只移动一个单元格,这就意味着卷积核会遍历输入数据的每一个位置;同理,如果步长为2,那么卷积核每次会移动两个单元格。如下图,就是 strade=2 时,卷积后所得的结果:

       

      步长的选择会影响卷积操作的输出尺寸。更大的步长会产生更小的输出尺寸,反之同理。

      之所以设置步长,主要考虑以下几点:

    • 降低计算复杂性:当步长大于1时,卷积核在滑动过程中会"跳过"一些位置,这将减少输出的尺寸并降低后续层的计算负担。
    • 模型的可扩展性:增大步长可以有效地降低网络层次的尺寸,使得模型能处理更大尺寸的输入图片。
    • 控制过拟合:过拟合是指模型过于复杂,以至于开始"记住"训练数据,而不是"理解"数据中的模式。通过减少模型的复杂性,我们可以降低过拟合的风险。
    • 减少存储需求:更大的步长将产生更小的特征映射,因此需要更少的存储空间。

      

      2. 零填充 - Zero Padding

       注意上面的图,我们发现底部输入图片的中间十字部分,被输入了两次。并且输入图像与卷积核进行卷积后的结果中损失了部分值,输入图像的边缘被“修剪”掉了(边缘处只检测了部分像素点,丢失了图片边界处的众多信息)。这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外。这个结果我们是不能接受的,有时我们还希望输入和输出的大小应该保持一致。为解决这个问题,可以在进行卷积操作前,对原矩阵进行边界填充(Padding),也就是在矩阵的边界上填充一些值,以增加矩阵的大小,通常都用”空“来进行填充。

      如果定义输入层的边长是M,卷积核的边长是K,填充的圈数为P,以及步长的长度为S,我们可以推导出卷积之后输出的矩阵大小为:

      激活层就不解释了,一般来说引入激活层只是引入非线性,官方指导中也说明,一般使用 Relu 即可。而池化层则其实是为了让模型具有泛化的能力,其实说白了就是为了避免模型的 overfit,使用池化层让模型忘记一些之前学过的特征,这里的做法其实在后面的很多模型中都能看到。

      池化层主要采用最大池化(Max Pooling)、平均池化(Average Pooling)等方式,对特征图进行操作。以最常见的最大池化为例,我们选择一个窗口(比如 2x2)在特征图上滑动,每次选取窗口中的最大值作为输出,如下图这就是最大池化的工作方式:

      大致可以看出,经过池化计算后的图像,基本就是左侧特征图的“低像素版”结果。也就是说池化运算能够保留最强烈的特征,并大大降低数据体量。

     

    四、手写数字CNN设计

      接下来,我们可以使用上面提到的 CNN 的各个层,给我们自己的手写数字识别设计网络架构。其实就是让 卷积层 -> Relu -> 池化层 叠加 N 层,然后最后最后再加入全连接层,进行分类。

       下图就是具体的网络架构图

     

       用代码实现如下:

    复制代码
    import torch
    from torch import nn
    from torch.nn import functional as F
    from torch import optim
    import torchvision
    from matplotlib import pyplot as plt
    from utils import plot_curve, plot_image, one_hot, predict_plot_image
    
    # step 1 : load dataset
    batch_size = 512
    # https://blog.csdn.net/weixin_44211968/article/details/123739994
    # DataLoader 和 dataset 数据集的应用
    train_loader = torch.utils.data.DataLoader(
        torchvision.datasets.MNIST('./data', train=True, download=True,
                                   transform=torchvision.transforms.Compose([
                                       torchvision.transforms.ToTensor(),
                                       torchvision.transforms.Normalize(
                                           (0.1307,), (0.3081,)
                                       )
                                   ])),
        batch_size=batch_size, shuffle=True
    )
    
    test_loader = torch.utils.data.DataLoader(
        torchvision.datasets.MNIST('./data', train=True, download=True,
                                   transform=torchvision.transforms.Compose([
                                       torchvision.transforms.ToTensor(),
                                       torchvision.transforms.Normalize(
                                           (0.1307,), (0.3081,)
                                       )
                                   ])),
        batch_size=batch_size, shuffle=False
    )
    
    
    # step 2 : CNN网络
    class CNN(nn.Module):
        def __init__(self):
            super(CNN, self).__init__()
            # 图片是灰度图片,只有一个通道
            # 第一层
            self.conv1 = nn.Conv2d(in_channels=1, out_channels=16,
                                   kernel_size=5, stride=1, padding=2)
            self.relu1 = nn.ReLU()
            self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
    
            # 第二层
            self.conv2 = nn.Conv2d(in_channels=16, out_channels=32,
                                   kernel_size=5, stride=1, padding=2)
            self.relu2 = nn.ReLU()
            self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
    
            # 全连接层
            self.fc1 = nn.Linear(in_features=7 * 7 * 32, out_features=256)
            self.relufc = nn.ReLU()
            self.fc2 = nn.Linear(in_features=256, out_features=10)
    
        # 定义前向传播过程的计算函数
        def forward(self, x):
            # 第一层卷积、激活函数和池化
            x = self.conv1(x)
            x = self.relu1(x)
            x = self.pool1(x)
            # 第二层卷积、激活函数和池化
            x = self.conv2(x)
            x = self.relu2(x)
            x = self.pool2(x)
            # 将数据平展成一维
            x = x.view(-1, 7 * 7 * 32)
            # 第一层全连接层
            x = self.fc1(x)
            x = self.relufc(x)
            # 第二层全连接层
            x = self.fc2(x)
            return x
    
    
    # step3: 定义损失函数和优化函数
    # 学习率
    learning_rate = 0.005
    # 定义损失函数,计算模型的输出与目标标签之间的交叉熵损失
    criterion = nn.CrossEntropyLoss()
    model = CNN()
    optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
    
    # step4: 模型训练
    train_loss = []
    # 定义迭代次数
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    # 将神经网络模型 net 移动到指定的设备上。
    model = model.to(device)
    # 为了和全连接网路的训练次数一致,我们采用相同的迭代次数
    total_step = len(train_loader)
    num_epochs = 3
    for epoch in range(num_epochs):
        for i, (images, labels) in enumerate(train_loader):
            images = images.to(device)
            labels = labels.to(device)
            optimizer.zero_grad()  # 清空上一个batch的梯度信息
            # 将输入数据 inputs 喂入神经网络模型 net 中进行前向计算,得到模型的输出结果 outputs。
            outputs = model(images)
            # 使用交叉熵损失函数
            loss = criterion(outputs, labels)
            # 使用反向传播算法计算模型参数的梯度信息
            loss.backward()
            # 更新梯度
            optimizer.step()
    
            train_loss.append(loss.item())
            # 输出训练结果
            if (i + 1) % 100 == 0:
                print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch + 1, num_epochs, i + 1, total_step,
                                                                         loss.item()))
    
    print('Finished Training')
    # 打印 loss 损失图
    plot_curve(train_loss)
    
    
    # step 5 : 准确度测试
    # 测试CNN模型
    with torch.no_grad(): # 进行评测的时候网络不更新梯度
        correct = 0
        total = 0
        for images, labels in test_loader:
            outputs = model(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: {} %'.format(100 * correct / total))
    复制代码

      

      损失可以看到经过两层的卷积层之后,虽然没有全连接层的Loss那么低,但是下降的还是非常快的,最后趋于平稳。

     

      经过三个epoch的训练,结果如下:

    复制代码
    Epoch [1/3], Step [100/118], Loss: 0.4683
    Epoch [2/3], Step [100/118], Loss: 0.2781
    Epoch [3/3], Step [100/118], Loss: 0.1155
    Finished Training
    Accuracy of the network on the 10000 test images: 96.47 %
    复制代码

      我们可以惊奇的发现,这里test数据中预测的准确率竟然高达 96.47%。要知道我们使用好几层的全连接网络,最后虽然 Loss 值非常低,但是在 test 数据中的准确度都只是徘徊在 90% 左右。而我们在使用相同层数,同等训练 epoch 的情况下,我们就把识别准确率提高了7%左右,这真是令人兴奋的进步!!!

      以上就是CNN的全部实践过程了,这个系列的一小节就到此结束了,随着学习深入,我发现机器学习中比较能做出成果的是深度学习,所以这几篇都是关于深度学习的内容。后面会更多的更新机器学习相关知识,我只是知识的搬运工,下期再见~

     

    Reference

    [1] https://zhuanlan.zhihu.com/p/635438713

    [2] https://mlnotebook.github.io/post/CNN1/

    [3] https://poloclub.github.io/cnn-explainer/#article-convolution

    [4] https://blog.csdn.net/weixin_41258131/article/details/133013757

    [5] https://alexlenail.me/NN-SVG/LeNet.html


    __EOF__

  • 本文作者: Blackbinbin
  • 本文链接: https://www.cnblogs.com/blackbinbin/p/18042312
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    asp.net core mvc 控制器使用配置
    cartographer中的扫描匹配
    C++语言基础Day3-内联函数
    Python---继承与复写
    贪心算法-翻硬币
    SQL 注入笔记
    【译】defer-panic-and-recover
    ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法
    openlayers 地图组件封装
    代码随想录算法训练营第23期day52|300.最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组
  • 原文地址:https://www.cnblogs.com/blackbinbin/p/18042312