主页
avatar

Kared

图像视觉的脑神经经:卷积神经网络 & 经典网络结构

卷积神经网络

一个典型的卷积神经网络(convolutional neural network,CNN)通常是由多个卷积层、池化层和全连接层交错堆叠而成的。在这种网络中,各层以特定顺序叠加在一起,每一层执行特定任务以提取特征并减少输入数据的维度。

1、卷积层

卷积(Convolution)是一种用于对输入图像进行特征提取的技术,它可以避免输入图像转化成一维向量时丢失相关的空间信息,同时避免对每个像素点进行全连接而导致模型参数过多的问题。卷积层(Convolutional Layer)是卷积神经网络中的一种基本层结构,卷积层中通常包含多个卷积核,它通过利用卷积核进行卷积操作来对输入的图像进行特征提取。卷积核(Convolutional Kernel),也被滤波器(Filter),是一个可学习的矩阵,卷积操作正是通过将卷积核应用于输入图像的一个部分,并使其在输入数据上滑动并与其进行按元素乘法和加法操作来实现的,这样可以生成表示图像局部空间信息的输出数据。

如图 2-2 所示,输入的图像数据是一个维度为 3×33\times 3的二维数组;中间维度为 2×22\times 2的二维数组正是通常所说的卷积核。先将卷积核与输入图像数据的左上角部分进行相乘再加和操作,从而获得左上角部分空间信息的特征数据,然后依次向右、向下滑动,进行同样的操作即可获得对应的输出特征图。

💡 卷积神经网络通过卷积操作解决了以下问题:

  • 局部关联:实现了在图像某个小区域内对特征进行提取。
  • 参数共享:减少了参数数量,并且降低了过拟合的风险。

填充 & 步长

特征图(Feature Map)的大小除了与输入矩阵有关,还与卷积核的大小、填充和步长有关。假设输入矩阵大小是 V×VV\times V,卷积核大小是 F×FF\times F,步长是 ss,填充值是 pp,卷积层输出特征图大小为 T×TT\times T,则 TT大小计算方法定义如下:

T=(V+2timespF)/s+1T = (V + 2\\times p − F)/s + 1

在卷积神经网络中,步长(Stride)是指卷积核在输入数据上移动的步长,填充(Padding)则是指在输入数据周围添加一定数量的虚拟数据(通常为零),以便在进行卷积操作时保持输出数据的大小与输入数据相同。

感受野

卷积所得的特征图中,每个像素点取值依赖于输入图像中的某个局部空间区域,该区域被称为感受野(Receptive Field)。如图 2-3 所示,经过一次卷积操作后得到的输出特征图1中,它的每个像素点对应着输入图像中某块 3×33\times 3的区域,即它的感受野大小为 3×33\times 3;而经过两次卷积操作后得到的输出特征图2中,它的每个像素点对应着输出特征图1中某块 3×33\times 3的区域,同时对应着输入图像中某块 5×55\times 5的区域,所以它的感受野大小为 5×55\times 5

1×1 卷积层

因为使用了最小窗口,1×11\times 1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。 因此,1×11\times 1卷积的唯一计算发生在通道上。

  • 调整网络层的通道数量和控制模型复杂性:它可以将输入特征图的通道数进行调整,通过调整通道数,可以控制模型的复杂度和参数量,同时也可以改变特征图的维度。
  • 跨通道信息融合:它可以通过在通道维度上进行卷积操作,将不同通道的特征进行融合,从而提取更丰富的特征表示。这种特征融合可以帮助网络更好地捕捉到不同通道之间的相关性,提升模型性能。

2、 池化层

在卷积神经网络中,池化(Pooling)是一种常用的操作,用于缩小特征图的大小并减少计算量。池化层(Pooling layer)通常在卷积层之后进行,通过对卷积层输出的特征图进行约减,实现了下采样,生成新的特征图。池化操作可以实现对感受域内的特征进行筛选,提取区域内最具代表性的特征,保留特征图中最主要的信息,常见的池化操作有平均池化和最大池化。

平均池化(Average Pooling)是指在输入数据的局部区域中计算平均值作为输出。如图 2-4 (a)所示,特征图的大小为 4×44\times 4,池化窗口的大小为 2×22\times 2,每次窗口滑动的步长为2。通过计算每次池化窗口中所有像素点的平均值,得到大小为 2×22\times 2的输出特征图。平均池化可以平滑输入数据的空间分布,从而减少噪声和冗余信息,并提高模型的泛化能力。

最大池化(Max Pooling)是指在输入数据的局部区域中取最大值作为输出。如图 2-4 (b)所示,特征图的大小为 4×44\times 4,池化窗口的大小为 2×22\times 2,每次窗口滑动的步长为2。通过取每次池化窗口中所有像素点的最大值,得到大小为 2×22\times 2的输出特征图。最大池化可以提取输入数据的最显著特征,从而增强模型的鲁棒性。

3、全连接层

全连接层(Fully Connected Layer)通常位于卷积神经网络的最后一部分,是卷积神经网络的重要组成部分。它的输入通常是一个展平的特征向量,其输出通常是一个向量,表示不同类别的概率分布或回归值。

如图 2-5 所示,在全连接层中,每个神经元都与前一层中的所有神经元相连,因此全连接层的参数量很大,也称为密集连接层。为了防止过拟合,常常采用正则化、Dropout等技术来减少参数数量。此外,还可以使用批标准化(Batch Normalization)等技术来加速网络的训练和提高模型的泛化能力。

最后,通常利用 Softmax\text{Softmax}函数对输出样本类别进行概率归一,将神经网络提取特征信息映射到输出类别或回归值。具体来说,给定一个模型的输出向量 z=[z1,z2,,zn]]z = [z_1, z_2, \dots , z_n]],其中 nn为类别数,Softmax\text{Softmax}函数将其转化为一个概率分布 p=[p1,p2,,pn]p = [p_1, p_2, \dots , p_n],其中 pip_i表示输入属于第 ii个类别的概率,计算公式为:

p_i = \frac{e^{z_i}}{\sum_{j=1}^n e^{z_j}}

其中 ee为自然对数的底数。根据上述公式可知,Softmax\text{Softmax}函数对每个输出值进行指数化,然后再进行归一化,使得所有概率值之和为1。因此,在进行多分类任务时,可以根据概率大小来判断输入属于哪个类别,同时也可以评估模型对每个类别的置信度。

4、激活函数

激活函数在神经网络中的主要作用是引入非线性因素,对输入信息进行非线性变换,从而使得神经网络可以逼近任何非线性函数。这种非线性变换可以让神经网络更加灵活地适应不同类型的数据分布和特征,从而提高神经网络的性能和准确率。如图 2-6 所示,常见的激活函数有 Sigmoid\text{Sigmoid}Tanh\text{Tanh}ReLU\text{ReLU}

经典卷积神经网络

1、LeNet-5

✨ LeNet-5模型诞生于1998年,它是第一个成功应用于数字识别问题的卷积神经网络,麻雀虽小五脏俱全,它包含了深度学习的基本模块:卷积层,池化层,全连接层。

LeNet 是一个具有 7 层的深度学习网络(不包括输入层),其中包含两个卷积层、两个池化层和三个全连接层。

在卷积层中,通过使用一组可学习的使用大小为 5×55 \times 5、步距为 11的卷积核(也称为滤波器)对输入进行卷积操作,从而将输入映射到多个二维特征输出。每个卷积核都会生成一个输出特征图,而多个卷积核的数量决定了输出特征图的通道数。其中,第一卷积层有 66个输出通道,而第二个卷积层有 1616个输出通道。卷积操作后的特征图还会经过 Sigmoid\text{Sigmoid}激活函数进行非线性变换,然后作为输出特征图。

池化层通常紧随卷积层之后,在 LeNet 中,池化层会将卷积层的输出特征图通过大小为 2×22 \times 2、步距为 22的池化窗口进行平均池化操作,实现空间下采样,用于减小特征图的空间尺寸并保留主要的特征信息。通过对每个特征图的局部区域进行池化操作,可以得到池化后的特征图。

在全连接层中,卷积层和池化层提取的输入数据特征信息会被展平为一维向量,然后输入到全连接层进行处理。在LeNet中,最后的三个全连接层分别有120、84和10个输出神经元,每个神经元都与前一层的所有神经元相连,每个连接都有一个可学习的权重。

最后一层全连接层也称为输出层,其10个神经元对应于最终的分类结果的数量。通常情况下,输出层的每个神经元都代表了一个类别,而神经元的输出值则表示模型对该类别的预测概率。

使用深度学习框架实现 LeNet 这样的模型非常简单,只需要实例化一个Sequential块就可以将需要的层连接在一起,下面是一个示例代码:

[collapse status=“false” title=“LeNet 代码实现”]

import torch
from torch import nn

LeNet = nn.Sequential(
        nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))

[/collapse]

2、AlexNet

💡 AlexNet 是 2012 **年 ISLVRC *2012*(ImageNet Large Scale Visual Recognition Challenge)竞赛的冠军网络,分类准确率由传统的70%+提升到80%+,在大规模视觉竞赛中击败传统计算机视觉模型的大型神经网络,首次实现了令人惊讶的准确度。

ImageNet Classification with Deep Convolutional Neural Networks

AlexNet 网络结构由五个卷积池化层、两个全连接隐藏层和一个全连接输出层组成。每个卷积层的卷积核大小也存在差异,从 11×1111\times 11逐渐减小到 3×33\times 3,这样可以捕捉不同尺度的特征。

在卷积层之间,使用最大池化层来减小特征图的尺寸,从而降低网络的复杂度。在全连接层中,采用 $\text{ReLU}$ 激活函数,它能够有效地缓解梯度消失问题,并提供非线性变换能力。最后的 $\text{Softmax}$ 层用于将网络输出转化为概率分布,用于分类任务。

[collapse status=“false” title=“AlexNet 和 LeNet 网络对比”]

AlexNet的架构与LeNet相似,但使用了更多的卷积层和更多的参数来拟合大规模的数据集。

[/collapse]

通过实例化一个 Sequential 块就可以实现 AlexNet,下面是一个示例代码:

[collapse status=“false” title=“AlexNet 代码实现”]

import torch
from torch import nn

AlexNet = nn.Sequential(
    # 这里使用一个11*11的更大窗口来捕捉对象。
    # 同时,步幅为4,以减少输出的高度和宽度。
    # 另外,输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Flatten(),
    # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
    nn.Linear(6400, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    # 最后是输出层。
    nn.Linear(4096, 1000))

[/collapse]

AlexNet 的创新点:

  • 使用 ReLU\text{ReLU}作为激活函数,加速学习过程,解决了 Sigmoid\text{Sigmoid}函数的梯度消失问题。
  • 随机失活(Dropout):减少过拟合现象并提高模型的泛化能力。
  • 最大池化(Max Pooling):增强的模型特征提取能力。
  • 局部响应值归一化(Local Response Normalization):增强模型的泛化能力并减少过拟合。
  • 数据增强技术(Data Augmentation):进一步扩充数据,更大的样本量有效地减少了过拟合。
  • GPU加速计算:在GPU硬件上运行的深度卷积神经网络,实现了并行运算。

激活函数(ReLU VS. Sigmoid)

ReLU\text{ReLU}(rectified linear unit)激活函数和 Sigmoid\text{Sigmoid}激活函数是深度学习中常见的非线性激活函数。ReLU\text{ReLU}相对于 Sigmoid\text{Sigmoid}的优点:

  1. 解决梯度消失问题Sigmoid\text{Sigmoid}函数在输入值非常大或非常小的情况下,梯度接近于0,这可能会导致反向传播时梯度消失,从而难以更新深层网络的参数。而 ReLU\text{ReLU}函数在输入大于0时,梯度始终为1,可以有效地缓解梯度消失问题。
  2. 计算效率高ReLU\text{ReLU}函数的计算复杂度比 Sigmoid\text{Sigmoid}函数低。ReLU\text{ReLU}只需要判断输入是否大于0,而 Sigmoid\text{Sigmoid}函数需要进行指数运算,计算开销更大。
  3. 稀疏激活ReLU\text{ReLU}在输入小于0时,输出为0,这意味着在任何时候,只有部分神经元被激活,造成了稀疏激活。这种稀疏性可能会带来一些优点,如减少神经元之间的依赖性,提高模型的泛化能力。

随机失活(Dropout)

Dropout是一种用于防止过拟合的技术,在训练时随机关闭一些神经元,以防止神经网络与训练数据过于匹配,导致泛化能力降低。在测试时,所有神经元将继续工作,以最大化预测准确性。

💡 随机失活(Dropout)和 剪枝(Pruning)有什么区别?

✨ 随机失活(Dropout)和剪枝(Pruning)是用于减少过拟合的两种不同技术。

随机失活(Dropout)是在训练过程中随机关闭一部分神经元的技术,它通过减少模型的复杂度来防止过拟合。在测试阶段,所有的神经元都是激活的,因此Dropout并不影响模型的最终预测。

剪枝(Pruning)则是指对神经网络中的某些参数进行减少或删除,以便减小模型的复杂度和内存消耗。剪枝通常在训练结束后进行,并且可以在训练前或训练后进行。它通过移除那些对模型结果影响不大的参数,从而提高模型的效率和减小过拟合的风险。

综上所述,Dropout主要用于在训练过程中降低过拟合,而剪枝则是在训练结束后通过移除不必要的参数来提高模型效率。

最大池化(Max Pooling)

最大池化(Max Pooling)和平均池化(Average Pooling)是卷积神经网络(CNN)中常用的池化策略,它们的主要区别在于如何从输入的一小块区域中选取值。最大池化相对于平均池化的优点:

  1. 保留主要特征:最大池化操作是取一块区域内的最大值作为输出,这样可以保留这块区域内的主要特征,而忽略一些不重要的信息。而平均池化是取平均值,可能会混淆主要信息和次要信息。
  2. 更强的空间不变性:最大池化由于只关注最大值,所以比平均池化具有更好的空间不变性。这意味着,即使图像发生一些小的平移,最大池化的输出也不会改变太多。
  3. 提高模型的非线性:最大池化本身是一个非线性操作,可以增加模型的非线性,有助于模型学习更复杂的特征。

局部响应值归一化(Local Response Normalization)

局部响应值归一化(Local Response Normalization,LRN)的计算方式是在每个神经元的输出上应用一个归一化函数,该函数将神经元的输出除以一组相邻神经元的输出的平方和。这样做的目的是使得神经元的响应对于相邻神经元的响应有竞争性的抑制效果,从而增加模型的鲁棒性。

bx,yi=ax,yi(k+αi=max(0,in/2)min(N1,i+n/2)ax,yi2)βb_{x,y}^i = \frac{{a_{x,y}^i}}{{(k + \alpha \sum_{{i=max(0, i-n/2)}}^{{min(N-1, i+n/2)}} {a_{x,y}^i}^2)^\beta}}

其中,ax,yia_{x,y}^i表示神经元的输出,kkα\alphaβ\betann是超参数,用于控制归一化的程度和范围。

数据增强(Data Augmentation)

  1. 平移、翻转和对称
    • 随机裁剪(crop),例如,在训练时将 256×256256 \times 256的图像随机裁剪为 224×224224 \times 224
    • 水平翻转,可以将样本数量翻倍。
  2. 改变RGB通道强度
    • 对RGB空间进行高斯扰动,例如,每个RGB图像的像素:

      Ixy=[IxyR,IxyG,IxyB]TI_{xy} = [I_{xy}^R,\quad I_{xy}^G,\quad I_{xy}^B]^T

      经过变换后变为:

      Ixy=[IxyR,IxyG,IxyB]T+[p1,p2,p3][α1λ1,α2λ2,α3λ3]TI_{xy} = [I_{xy}^R, I_{xy}^G, I_{xy}^B]^T + [p_1, p_2, p_3][\alpha_1\lambda_1, \alpha_2\lambda_2, \alpha_3\lambda_3]^T

GPU加速计算

卷积神经网络中的计算瓶颈:卷积和矩阵乘法,都是可以实现在GPU硬件上并行化的操作。 AlexNet 使用两个显存为 3GB 的 NVIDIA GTX580 GPU 实现了快速卷积运算。其创新 cuda-convnet 几年来它一直是行业标准,并推动了深度学习热潮。

3、VGG

💡 VGG 是由牛津大学的 **视觉几何组(visual geometry group)**在 2014 年提出的。VGG网络的主要特点是使用多个重复的卷积块,这些卷积块由卷积层和池化层组成。

Very Deep Convolutional Networks for Large-Scale Image Recognition

神经网络架构的设计逐渐变得更加抽象,研究人员开始从单个神经元的角度思考问题,发展到整个层,现在又转向块,重复层的模式。在 VGG 中首先使用了块的概念,通过使用循环和子程序,可以很容易地在任何现代深度学习框架的代码中实现这些重复的架构。

💡 在 VGG中,使用了一系列堆叠的卷积层来构建深度神经网络。作者尝试了各种架构,特别是他们发现深层且窄的卷积(即3×3)比较浅层且宽的卷积更有效。

两个堆叠的卷积层(卷积核为 3×33\times 3),有限感受野是 5×55 \times 5,而三个堆叠的卷积层(卷积核为 3×33 \times 3)的感受野为 7×77 \times 7。相比于单个 5×55\times 57×77\times 7的卷积核,使用多个 3×33\times 3的卷积核可以实现相同的感受野大小(即输出的特征图覆盖输入图像的区域大小),但使用更少的参数。

[collapse status=“false” title=“VGG 和 AlexNet 网络对比”]

[/collapse]

VGG 块是由一系列卷积层组成,后面再加上用于空间下采样的最大池化层。在模型中使用了带有 3×33\times 3卷积核、填充为1(保持高度和宽度)的卷积层,和带有 2×22\times 2池化窗口、步幅为2(每个块后的分辨率减半)的最大池化层。在下面的代码中,我们定义了一个名为 vgg_block 的函数来实现一个 VGG 块。

下面设计的 VGG-11 模型使用可复用的卷积块构造网络,不同的 VGG 模型可通过每个块中卷积层数量和输出通道数量的差异来定义。块的使用导致网络定义的非常简洁,使用块可以有效地设计复杂的网络。

[collapse status=“false” title=“VGG 代码实现”]

import torch
from torch import nn

def vgg_block(num_convs, in_channels, out_channels):
    layers = []
    for _ in range(num_convs):
        layers.append(nn.Conv2d(in_channels, out_channels,
                                kernel_size=3, padding=1))
        layers.append(nn.ReLU())
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

def vgg(conv_arch):
    in_channels, conv_blks = 1, []
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
        in_channels = out_channels

    return nn.Sequential(
        *conv_blks, nn.Flatten(),
        # 全连接层部分
        nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 10))

conv_arch = {
    "vgg11": ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)),
    "vgg13": ((2, 64), (2, 128), (2, 256), (2, 512), (2, 512)),
    "vgg16": ((2, 64), (2, 128), (3, 256), (3, 512), (3, 512)),
    "vgg19": ((2, 64), (2, 128), (4, 256), (4, 512), (4, 512)),
}

VGG_11 = vgg(conv_arch["vgg11"])

[/collapse]

4、GoogLeNet

💡 GoogLeNet 是由 Google 团队在 2014 年提出的深度神经网络模型,也被称为 Inception v1。其主要创新是引入了Inception模块,使用多个不同大小的卷积核进行卷积和池化操作,并将它们在同一层级上进行拼接,从而同时捕获不同尺度的特征,提高网络的表达能力。

Google Inception Net是一个大家族,Inception 历经了V1、V2、V3、V4等多个版本的发展,不断趋于完善。

Inception块

💡 设计思路:采用多种样式卷积核进行卷积来增加特征的多样性,考虑怎样用容易获得密集组件近似覆盖卷积视觉网络的最优稀疏结构。

在 GoogLeNet 中,基本的卷积块被称为 Inception 块(Inception block),该模块能够在不增加网络参数数量的情况下提高网络的深度和宽度。Inception 块相当于一个有 4 条路径的子网络。它通过不同窗口形状的卷积层和最大汇聚层来并行抽取信息,并使用 1×11\times 1卷积层减少每像素级别上的通道维数从而降低模型复杂度。

✨ Inception V2 在 V1 的基础上增加了一些 1 * 1 的卷积核,从而减少了网络的参数量和计算量,并且使用了 Batch Normalization 和移动平均技术来加速网络训练过程,提高了网络的效率和泛化能力。同时,Inception V2 还改进了池化操作,并引入了 Factorization 方法,使用小卷积核来减少维度,从而解决了Inception V1 的冗余维度问题。

如上图所示,Inception 块由四条并行路径组成。 前三条路径使用窗口大小为 1×11\times 13×33\times 35×55\times 5的卷积层,从不同空间大小中提取信息。 中间的两条路径在输入上执行 1×11\times 1卷积,以减少通道数,从而降低模型的复杂性。 第四条路径使用 3×33\times 3最大池化层,然后使用 1×11\times 1卷积层来改变通道数。 这四条路径都使用合适的填充来使输入与输出的高和宽一致,最后我们将每条线路的输出在通道维度上连结,并构成 Inception 块的输出。在 Inception 块中,通常调整的超参数是每层输出通道数。

✨ Inception V3 在 V2 的基础上,将大卷积核替换成小卷积核,从而减少了参数的数量,同时,增加非线性激活函数,使网络产生更多独立特(disentangled feature),表征能力更强,训练更快。V3 版本还使用了 RMSprop 优化器来替代了 SGD,进一步提高了模型的训练速度和效率。

💡 GoogLeNet 将多个设计精细的Inception块与其他层(卷积层、全连接层)串联起来。其中Inception块的通道数分配之比是在ImageNet数据集上通过大量的实验得来的。

GoogLeNet一共使用9个 Inception 块和全局平均池化层的堆叠来生成其估计值。Inception 块之间的最大池化层可降低维度。 第一个模块类似于 AlexNet 和 LeNet,Inception 块的组合从 VGG 继承,全局平均池化层避免了在最后使用全连接层。

  • 辅助分类器:解决由于模型深度过深导致的梯度消失的问题。

[collapse status=“false” title=“GoogLeNet 代码实现”]

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

class Inception(nn.Module):
    # c1--c4是每条路径的输出通道数
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.ReLU(),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   nn.AdaptiveAvgPool2d((1,1)),
                   nn.Flatten())

GoogLeNet = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

[/collapse]

5、ResNet

💡 ResNet(Residual Neural Network)由微软研究院的 Kaiming He 等人于 2015 年提出。它通过引入残差连接(residual connection)来解决深层网络训练过程中的梯度消失问题,使得网络可以更深更容易训练。

📎 何凯明现场演讲【中英字幕】:https://zhuanlan.zhihu.com/p/54072011

Deep Residual Learning for Image Recognition

存在的问题

✨ 当训练较浅的神经网络时,使用正则初始化权值和 BN 可以解决梯度问题。然而,当网络层数增加时,就会出现退化问题(degradation problem),这不是由过拟合造成的,而是随着层数的加深,训练集的准确率会饱和,甚至可能下降。

  • 梯度消失或梯度爆炸
    • 梯度消失:误差梯度小于 1,反向传播时,随着网络加深,梯度逐渐趋近于 00的现象。
    • 梯度爆炸:误差梯度大于 1,反向传播时,随着网络加深,梯度逐渐趋近于 \infty的现象。
  • 退化问题(degradation problem)
    • 在解决了梯度消失、爆炸问题后,仍然存在深层网络的效果可能比浅层网络差的现象

Residual 模块

✨ 通过引入 Identity mapping,BasicBlock 在输入和输出之间建立了一个直接的连接通道。这样,强大的有参层就可以集中力量学习输入和输出之间的残差。简而言之,如果残差更小,学习更容易,也更快收敛。

💫 Shortcut 是残差结构的一种分支,是将输入特征矩阵直接加到主分支的输出特征矩阵上,以捷径的方式将信息直接传递到网络的深层。Shortcut 的目的是为了避免在深层网络中造成信息丢失或者退化,从而提高残差网络的效果。

  • 残差的思想:去掉相同的主体部分,从而突出微小的变化。
  • 可以被用来训练非常深的网络。

✨ 残差结构的巧妙在于它利用了残差映射的思想,而不是让每个堆叠的层直接拟合所需的映射。具体来说,假设所需的映射为H(x)H(x),则让堆叠的非线性层拟合另一个映射F(x):=H(x)xF(x):= H(x)−x,将原始映射重新归纳为F(x)+xF(x)+x

  • 残差结构的思想就是通过去掉相同的主要部分,来突出微小的变化残差结构中,FF表示将网络参数映射到求和前的网络映射,HH表示将输入映射到求和后的网络映射。举个例子,如果将 5 映射到 5.1,那么在没有残差结构的情况下,F(5)=5.1F’(5)=5.1。而在引入残差结构后,H(5)=5.1H(5)=5.1, H(5)=F(5)+5H(5)=F(5)+5, F(5)=0.1F(5)=0.1。比较 FF’FF可以发现,在引入残差后的映射更敏感,变化更大。比如,当输出从 5.1 变为 5.2 时,FF’的输出仅仅增加了 2%,但是 FF的输出增加了 100%。这说明残差结构的输出变化对网络参数的调整作用更大,从而提高了模型的效果。

✨ 在更深层的网络中,作者提出了一种 bottleneck 结构块来代替残差结构,这种结构在像 Inception 网络一样,通过使用 1x1 卷积巧妙地缩减或扩张特征图维度,使得 3x3 卷积核数目不受上一层输入的影响,而它的输出也不会影响到下一层,同时大量减少了参数数量,解决在实际计算中的问题。

ResNet 的网络设计规律

✨ 图表显示,不同层数的 ResNet 具有相似的结构,其包括输入部分、输出部分和中间卷积部分, 中间卷积部分平均池化层,全连接输出层和 softmax 分类处理。

[collapse status=“false” title=“34 层 ResNet 示意图”]

[/collapse]

在 conv3_1, conv4_1, conv5_1 中(即图中虚线部分),因为主分支和 shortcut 分支的输出特征矩阵形状必须相同,但是由于输入和输出的特征矩阵形状不同,因此需要在 shortcut 分支上插入一个 1×11 \times 1的卷积层来调整特征矩阵的深度,同时通过调整主分支和 shortcut 分支上的卷积层的步长来调整特征矩阵的高和宽。

[collapse status=“false” title=“ResNet 代码实现”]

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

class Residual(nn.Module):
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)

def resnet_block(input_channels, num_channels, num_residuals,
                 first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.BatchNorm2d(64), nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

ResNet = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))

[/collapse]

批量归一化(Batch Normalization)

✨ Batch Normalization 的目的是通过归一化技术使得一批(batch)的特征图的均值为0,方差为1,从而提高神经网络的训练效率,加快模型的收敛,避免过拟合等问题。ResNet 正是通过数据的预处理以及在网络中使用 BN 层来解决梯度消失或梯度爆炸问题。

Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift

✨ 通常在预处理阶段和卷积层之间添加 BN 层,从而使得输入 Conv1 和 Conv2 的特征图满足同一分布规律(理论上是指整个训练样本集所对应特征图的数据要满足分布规律),而 BN 正是可以使其满足均值为 0,方差为 1 的规律。

μ\muσ2\sigma^2在正向传播过程中统计得到;

γ\gammaβ\beta在反向传播过程中训练得到。

相关博客内容参考:

Batch Normalization详解以及pytorch实验太阳花的小绿豆的博客-CSDN博客pytorch batchnormal

拓展:ResNeXt

Aggregated Residual Transformations for Deep Neural Networks

Deep Learning CNN CV 深度学习 卷积神经网络 计算机视觉