深度学习二:卷积神经网络

卷积神经网络简介

卷积神经网络(convnets或者cnns)属于神经网络范畴,已经在图像识别和分类的领域证明了高效的能力。卷积神经网络可以成功识别人脸、物体和信号,为机器人和自动驾驶汽车提供视力。

image-20240309160159479

从上图可以看到,卷积神经网络可以识别场景,可以提供相关的插图说明,除此之外,卷积神经网络可以做语义分割。图像分类,目标检测和语义分类,是CV的三大任务。

LeNet架构

LeNet是推进深度学习领域发展最早的卷积神经网络之一。经过多次迭代,到1988年,LeNet已经成功进化到了LeNet5。当时,LeNet架构主要用于字符识别任务,比如:读取邮政编码、数字等等。

image-20240309160710169

上图中的卷积神经网络和原始的LeNet结构比较相似,可以把输入的图像分为四类:狗、猫、船或者鸟(原始的LeNet主要用于字符识别任务)。正如上面,当输入一张船的图片,网络可以正确从四个类别中把最高的概率分配给船。在输出层所有概率的和应该为1。

上图中的ConvNet有四个主要操作。

  • 卷积
  • 非线性激活
  • 池化
  • 分类

这些操作对于卷积神经网络来说都是基本组件,因此理解它们的工作原理有助于充分理解卷积神经网络。

图像时像素值的矩阵

在我们看来。每张图像都可以看作像素值的矩阵。一个相机拍摄出来的图像都会有三个通道,红、绿、蓝;可以把它们看作时相互堆叠在一起的二维矩阵,每一个通道代表一个颜色,每个通道的像素值都在0~255之间。灰度图像只有一个通道,0表示黑色,255表示白色。

卷积直观理解

我们使用橙色的矩阵在原始图像上滑动,每次滑动移动一个像素,也叫做步长,在每个位置上,我们计算对应元素的乘积,两个矩阵之间,并把乘积的和作为一个最后的结果,得到输出矩阵(粉色)中的每一个元素的值。在CNN的描述中,3*3的矩阵叫做滤波器或者卷积核。通过滑动滤波器并计算点乘得到的矩阵叫做“卷积特征”或者“激活图”。

不同滤波器对图卷积的结果是不一样,如下图所示,不同滤波器可以从图中检测到不同的特征,比如边缘、曲线等。

image-20240309163520168

可以看到第一个卷积核是等价层,对图像没有改变;第二个卷积核的任务是边缘检测,绘制出了图像的边缘;第三个卷积核是锐化卷积核,可以看到棱角更加锐利。

在实战中:CNN会在训练过程中学习到这些滤波器的值,但是我们还是需要在训练前指定注入滤波器的个数,滤波器的大小,网络架构等参数。我们使用的滤波器越多,提取到的特征就越多,网络所能在位置图像上识别的模式也就越好。

特征图的大小由下面三个参数控制。需要事先确定它们。

  • 深度(Depth):深度对应的是卷积操作所需的滤波器的个数,在下面的网格中,我们使用三个不同的滤波器对原始图像进行卷积操作,这样就可以生成3个不同的特征图。可以把三个特征图看作堆叠的2维矩阵,那么特征图的深度就是3

image-20240309164015347

  • 步长:指的是输入矩阵上滑动滤波矩阵的像素数。当步长为1时,我们每次移动滤波器一个像素的位置,当步长为2时,我们每次移动滤波器2个像素,步长越大,我们的特征图就越小。
  • 零填充:又是,我们需要在输入矩阵的边缘进行零值填充,这样我们可以对输入矩阵的边缘进行滤波。零填充的好处就是可以让我们控制特征图的大小。使用零填充也叫做泛卷积,不适用零填充的叫做严格卷积。

激活函数

卷积操作之后需要使用RELU作为激活函数,RELU表示修正线性单元,是一个非线性的操作。将特征图的所有小于0的像素值设置为0,RELU的目的时在ConvNet中引入非线性,因为在大部分的我们希望ConvNet学习的实际数据是非线性的(卷积是一个线性操作——元素级别的矩阵相乘和相加)

RELU操作可以从下面的图中理解,它展示的RELU操作是应用到上面那张城市图像得到的特征图之一。这里的特征图是修正过的特征图。

image-20240309165230195

第二张图消除了黑色。相对于其它的非线性函数,比如tanh或者sigmoid也可以用来替换RELU,但是RELU在大部分情况下表现是更好的。

池化直观解释

空间池化也叫做降采样或者下采样,可以降低各个特征图的维度,但可以保持大部分重要的信息,空间池化由下面的几种方式:最大化、平均化和加和等

对于最大池化(max pooling)我们定义一个空间邻域(比如:2*2的窗口),并从窗口内的修正特征图中取出最大的元素;除了取出最大的元素,我们还可以取平均(Average Pooling)或者对窗口内的元素进行求和。在实际中,最大池化效果好一些。

下图展示了使用2*2的窗口在修正特征图(在卷积+RELU操作后得到)使用最大池化的例子。

image-20240309165804353

我们以两个元素(也叫做步长)来滑动我们的2*2的窗口,并在每个区域内取得最大值,如上图所示,这样操作可以降低我们特征图的维度。

池化操作是分开应用到各个特征图上的,正因如此,我们可以从三个输入图像中得到三个输出图像。

image-20240309170036644

下图展示了上面的图中我们RELU操作后得到的修正特征图的池化操作的例子。

image-20240309170130293

池化函数可以逐渐降低输入表示的空间尺度。特别地,池化:

  • 降采样:使特征维度变得更小,并且网络中的参数和计算的数量变得更加可控的减少,因此,可以控制过拟合。
  • 特征提取:池化操作通过对输入数据进行聚合,提取处输入数据的主要特征。
  • 平移不变性:无论物体在输入图像中的位置如何变化,池化操作都会选择该区域的最大或者平均值作为代表,从而使模型对物体位置的改变具有一定的鲁棒性。

全连接层直观理解

全连接层使用的传统的多层感知机,在输出层使用的使softmax激活函数,就是一个逻辑回归,做一个分类任务,也可以使用SVM、决策树等。

卷积层和池化层的输出表示了输入图像的高级特征,全连接层的目的是为了使用这些特征把输入图像基于训练数据集进行分类,比如,在下面的图中进行的图像分类有四个可能的输出结果。

image-20240309171128630

组合并用反向传播训练

完整的卷积网络的训练过程如下:

第一步:我们初始化所有的滤波器,使用随机值设置参数/权重

第二步:网络接收一张训练图像作为输入,通过前向传播过程(卷积、ReLU 和池化操作,以及全连接层的前向传播),找到各个类的输出概率。我们假设船这张图像的输出概率是 [0.2, 0.4, 0.1, 0.3]。因为对于第一张训练样本的权重是随机分配的,输出的概率也是随机的

第三步:在输出层计算总误差(计算4类的和)
$$
Total Error=frac{1}{2}sum(target probability -outputprobability)^2
$$
第四步:使用反向传播算法,根据网络的权重计算出误差的梯度,并使用梯度下降算法更新所有滤波器的值以及参数的值,使输出误差最小化。

第五步:对训练数据中所有的图像重复步骤1~4

那么池化层的梯度怎么解决呢?(chatgpt3.5生成)

在传统的池化层中,确实没有可训练的参数需要更新,因此在反向传播过程中,并没有像卷积层或全连接层那样明确计算和更新梯度。然而,在反向传播过程中,仍然需要将梯度传递回池化层,以确保梯度能够正确地传播到前一层。

在池化层中,通常使用一种称为 "反池化"(unpooling)的操作来传递梯度。反池化的目标是将池化层输入的梯度值重新分配到对应的最大值或平均值所在的位置,从而恢复原始输入的梯度分布。

最大池化的反池化通常使用掩码(mask)的方式,将梯度值放回相应的最大值所在的位置。这样,梯度只会被分配给最大值所在的位置,其他位置的梯度保持为零。而平均池化的反池化则将梯度均匀地分配到对应区域的每个位置。

反池化的具体实现方式因不同的池化方法而异。一种常见的方法是使用类似于最大池化时使用的掩码来分配梯度,将梯度放回最大值所在的位置,其他位置保持为零。另一种方法是使用插值技术,通过将梯度值分配到对应区域的每个位置,以实现反池化。

需要注意的是,池化层的反池化操作是非参数化的,不涉及可训练的参数更新。它们的主要目的是确保梯度能够正确传播回前一层,以便在整个网络中进行优化和更新。

总结起来,尽管池化层本身没有可训练的参数需要更新,但在反向传播过程中,可以使用反池化操作将梯度传递回池化层,以确保梯度能够正确地传播到前一层。

卷积神经网络的发展历程

image-20240309172403422

AlexNet之后卷积神经网络的演化过程有4个方向:

  • 网络加深
  • 增强卷积层的功能
  • 分类任务到检测任务
  • 增加新的功能模块

Lenet

模型代码如下:

import torch
import torch.nn as nn

class LeNet(nn.Module):
    def __init__(self, num_classes=10):
        super(LeNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(6, 16, kernel_size=5, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        self.classifier = nn.Sequential(
            nn.Linear(16 * 5 * 5, 120),
            nn.ReLU(inplace=True),
            nn.Linear(120, 84),
            nn.ReLU(inplace=True),
            nn.Linear(84, num_classes),
        )

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

AlexNet

alexnet在2012年的imagenet图像分类竞赛中,top5错误率为15.3%,遥遥领先第二名。

AlexNet 的结构如图所示。图中明确显示了两个GPU之间的职责划分:一个GPU运行图中顶部的层次部分,另一个GPU运行图中底部的层次部分。GPU之间仅在某些层互相通信(这是不是防止过拟合)。AlexNet模型的结构可以描述如下:

image-20240309174430159

防止过拟合:dropout和数据增强。

激活函数:RELU

大数据训练:百万级的imagenet图像数据

GPU和LRN(local responce normalization)规范化层的使用。

数据增强

增加训练数据使避免过拟合的好方法,并且可以提升算法的准确率,当训练数据有限的时候,可以通过一些变换从已有的训练数据集中生成一些新的数据,来扩大训练数据量,通常采用的变形方式有下面几种。

  • 水平翻转图像(发射变化,flip)
  • 从原始图像(大小256256)随机地平移变换(crop)出一些图像(如大小为224224)
  • 给图像增加一些随机的光照(又称光照、彩色变换、颜色抖动)

dropout

AlexNet做的是以0.5的概率将每个隐层神经元的输出设置为0。以这种方式被抑制的神经元既不参与前向传播,也不参与反向传播。因此,每次输入一个样本,就相当于该神经网络尝试了一个新结构,但是所有这些结构之间共享权重。因为神经元不能依赖于其他神经元而存在,所以这种技术降低了神经元复杂的互适应关系。因此,网络需要被迫学习更为健壮的特征,这些特征在结合其他神经元的一些不同随机子集时很有用。如果没有Dropout,我们的网络会表现出大量的过拟合。Dropout 使收敛所需的迭代次数大致增加了一倍。

Alex 用非线性激活函数ReLU代替了sigmoid,发现得到的SGD的收敛速度会比 sigmoid/tanh快很多。单个GTX 580 GPU只有3 GB 内存,因此在其上训练的数据规模有限。从AlexNet结构图可以看出,它将网络分布在两个GPU上,并且能够直接从另一个GPU的内存中读出和写入,不需要通过主机内存,极大地增加了训练的规模。

代码


class AlexNet(nn.Module):
    def __init__(self, num_classes=1000):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), 256 * 6 * 6)
        x = self.classifier(x)
        return x

VGGNet

VGGnet可以看作使加深版本的AlexNet,代码如下:

import torch
import torch.nn as nn

# 定义VGGNet模型
class VGGNet(nn.Module):
    def __init__(self, num_classes=1000):
        super(VGGNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, num_classes),
        )

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

GoogleNet

GoogleNet采用了多个Inception模块的堆叠,每个Inception模块由不同大小的卷积核和池化层组成,通过并行地计算多种尺寸的特征图,并将它们连接起来。这样做的好处是可以捕捉输入数据的不同尺度和抽象层次的特征。此外,为了减少参数数量和计算量,GoogleNet还使用了1×1的卷积核来进行降维和压缩。

与传统的卷积神经网络相比,GoogleNet具有更深的网络结构,但参数量却相对较少,这使得它在计算资源有限的情况下仍然能够进行高效的训练和推理。

代码

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

class InceptionModule(nn.Module):
    def __init__(self, in_channels, out_1x1, reduce_3x3, out_3x3, reduce_5x5, out_5x5, out_pool):
        super(InceptionModule, self).__init__()

        # 1x1 convolution branch
        self.branch1 = nn.Conv2d(in_channels, out_1x1, kernel_size=1)

        # 1x1 convolution followed by 3x3 convolution branch
        self.branch2 = nn.Sequential(
            nn.Conv2d(in_channels, reduce_3x3, kernel_size=1),
            nn.Conv2d(reduce_3x3, out_3x3, kernel_size=3, padding=1)
        )

        # 1x1 convolution followed by 5x5 convolution branch
        self.branch3 = nn.Sequential(
            nn.Conv2d(in_channels, reduce_5x5, kernel_size=1),
            nn.Conv2d(reduce_5x5, out_5x5, kernel_size=5, padding=2)
        )

        # 3x3 max pooling followed by 1x1 convolution branch
        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, out_pool, kernel_size=1)
        )

    def forward(self, x):
        branch1_output = self.branch1(x)
        branch2_output = self.branch2(x)
        branch3_output = self.branch3(x)
        branch4_output = self.branch4(x)

        # Concatenate the branch outputs along the channel dimension
        output = torch.cat([branch1_output, branch2_output, branch3_output, branch4_output], dim=1)
        return output

class GoogLeNet(nn.Module):
    def __init__(self, num_classes=1000):
        super(GoogLeNet, self).__init__()

        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)
        self.conv2 = nn.Conv2d(64, 192, kernel_size=3, stride=1, padding=1)
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

        self.inception3a = InceptionModule(192, 64, 96, 128, 16, 32, 32)
        self.inception3b = InceptionModule(256, 128, 128, 192, 32, 96, 64)

        self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

        self.inception4a = InceptionModule(480, 192, 96, 208, 16, 48, 64)
        self.inception4b = InceptionModule(512, 160, 112, 224, 24, 64, 64)
        self.inception4c = InceptionModule(512, 128, 128, 256, 24, 64, 64)
        self.inception4d = InceptionModule(512, 112, 144, 288, 32, 64, 64)
        self.inception4e = InceptionModule(528, 256, 160, 320, 32, 128, 128)

        self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

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

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.dropout = nn.Dropout(0.4)
        self.fc = nn.Linear(1024, num_classes)

    def forward(self, x):
            x = self.conv1(x)
            x = F.relu(x)
            x = self.maxpool1(x)
            x = self.conv2(x)
            x = F.relu(x)
            x = self.maxpool2(x)

            x = self.inception3a(x)
            x = self.inception3b(x)
            x = self.maxpool3(x)

            x = self.inception4a(x)
            x = self.inception4b(x)
            x = self.inception4c(x)
            x = self.inception4d(x)
            x = self.inception4e(x)
            x = self.maxpool4(x)

            x = self.inception5a(x)
            x = self.inception5b(x)

            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.dropout(x)
            x = self.fc(x)
            return x

ResNets

科研好难呀!我是不是下一个何凯明doge

DenseNet

CNN的各大流派

  • 暴力加深流:以AlexNetVGGNet为首的模型,这一派观点很直接,就是不断交替使用卷积层池化层,暴力增加网络层数,最后接一下全连接层分类。这类模型在CNN早期是主流,特点是参数量大,尤其是后面的全连接层,几乎占了一般参数量。而且相比于后续的模型,效果也较差。因此这类模型后续慢慢销声匿迹了。
  • nception流派谷歌流派,这一派最早起源于NIN,(将原来的线性卷积层变成了多层感知卷积层,将全连接层改进为全局平均池化)称之为网络中的网络,后被谷歌发展成Inception模型(这个单词真的不好翻译。。。)。这个模型的特点是增加模型的宽度,使得模型不仅仅越长越高,还越长越胖。也就是说每一层不再用单一的卷积核卷积,而是用多个尺度的卷积核试试。显然,如果你熟悉CNN,就不难发现,这样做会使每一层的feature map数量猛增,因为一种尺寸的卷积核就能卷出一系列的feature map,何况多个!这里google使用了1*1的卷积核专门用来降channel。谷歌的特点是一个模型不玩到烂绝不算完,所以又发展出了Inception v2、Inception v3、Inception v4等等。
  • 残差流派:2015年ResNet横空出世,开创了残差网络。使用残差直连边跨层连接,居然得到了意想不到的好效果。最重要的是,这一改进几乎彻底突破了层数的瓶颈,1000层的resnet不是梦!之后,最新的DenseNet丧心病狂地在各个层中间都引入了残差连接。目前大部分模型都在尝试引入残差连接。

问题

我不知道卷积层和池化层的优化模式是什么

Share

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Post comment

 ©2025 [忧郁小王子的乌托邦] - 稳定运行: