For the complete documentation index, see llms.txt. This page is also available as Markdown.

8.1 图像分类简述与 ResNet 源码分析

本章代码:https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson8/resnet_inference.py

这篇文章主要介绍了 图像分类的 inference,其中会着重介绍 ResNet。

模型概览

在torchvision.model中,有很多封装好的模型。

可以分类 3 类:

  • 经典网络

    • alexnet

    • vgg

    • resnet

    • inception

    • densenet

    • googlenet

  • 轻量化网络

    • squeezenet

    • mobilenet

    • shufflenetv2

  • 自动神经结构搜索方法的网络

    • mnasnet

ResNet18 使用

以 ResNet 18 为例。

首先加载训练好的模型参数:

然后比较重要的是把模型放到 GPU 上,并且转换到eval模式:

在inference 时,主要流程如下:

  • 代码要放在with torch.no_grad():下。torch.no_grad()会关闭反向传播,可以减少内存、加快速度。

  • 根据路径读取图片,把图片转换为 tensor,然后使用unsqueeze_(0)方法把形状扩大为$B \times C \times H \times W$,再把 tensor 放到 GPU 上 。

  • 模型的输出数据outputs的形状是$1 \times 2$,表示 batch_size 为 1,分类数量为 2。torch.max(outputs,0)是返回outputs中每一列最大的元素和索引,torch.max(outputs,1)是返回outputs中每一行最大的元素和索引。

    这里使用_, pred_int = torch.max(outputs.data, 1)返回最大元素的索引,然后根据索引获得 label:pred_str = classes[int(pred_int)]。

关键代码如下:

全部代码如下所示:

总结一下 inference 阶段需要注意的事项:

  • 确保 model 处于 eval 状态,而非 trainning 状态

  • 设置 torch.no_grad(),减少内存消耗,加快运算速度

  • 数据预处理需要保持一致,比如 RGB 或者 rBGR

残差连接

以 ResNet 为例:

一个残差块有2条路径$F(x)$和$x$,$F(x)$路径拟合残差,不妨称之为残差路径;$x$路径为identity mapping恒等映射,称之为shortcut。图中的⊕为element-wise addition,要求参与运算的$F(x)$和$x$的尺寸要相同。

shortcut 路径大致可以分成2种,取决于残差路径是否改变了feature map数量和尺寸。

  • 一种是将输入x原封不动地输出。

  • 另一种则需要经过$1×1$卷积来升维或者降采样,主要作用是将输出与$F(x)$路径的输出保持shape一致,对网络性能的提升并不明显。

两种结构如下图所示:

ResNet 中,使用了上面 2 种 shortcut。

网络结构

ResNet 有很多变种,包括 ResNet 18、ResNet 34、ResNet 50、ResNet 101、ResNet 152,网络结构对比如下:

ResNet 的各个变种,数据处理大致流程如下:

  • 输入的图片形状是$3 \times 224 \times 224$。

  • 图片经过 conv1 层,输出图片大小为 $ 64 \times 112 \times 112$。

  • 图片经过 max pool 层,输出图片大小为 $ 64 \times 56 \times 56 $。

  • 图片经过 conv2 层,输出图片大小为 $ 64 \times 56 \times 56$。(注意,图片经过这个 layer, 大小是不变的)

  • 图片经过 conv3 层,输出图片大小为 $ 128 \times 28 \times 28$。

  • 图片经过 conv4 层,输出图片大小为 $ 256 \times 14 \times 14$。

  • 图片经过 conv5 层,输出图片大小为 $ 512 \times 7 \times 7$。

  • 图片经过 avg pool 层,输出大小为 $ 512 \times 1 \times 1$。

  • 图片经过 fc 层,输出维度为 $ num_classes$,表示每个分类的 logits。

下面,我们称每个 conv 层为一个 layer(第一个 conv 层就是一个卷积层,因此第一个 conv 层除外)。

其中 ResNet 18、ResNet 34 的每个 layer 由多个 BasicBlock 组成,只是每个 layer 里堆叠的 BasicBlock 数量不一样。

而 ResNet 50、ResNet 101、ResNet 152 的每个 layer 由多个 Bottleneck 组成,只是每个 layer 里堆叠的 Bottleneck 数量不一样。

源码分析

我们来看看各个 ResNet 的源码,首先从构造函数开始。

构造函数

ResNet 18

resnet18 的构造函数如下。

[2, 2, 2, 2] 表示有 4 个 layer,每个 layer 中有 2 个 BasicBlock。

conv1为 1 层,conv2、conv3、conv4、conv5均为 4 层(每个 layer 有 2 个 BasicBlock,每个 BasicBlock 有 2 个卷积层),总共为 16 层,最后一层全连接层,$总层数 = 1+ 4 \times 4 + 1 = 18$,依此类推。

ResNet 34

resnet 34 的构造函数如下。

[3, 4, 6, 3] 表示有 4 个 layer,每个 layer 的 BasicBlock 数量分别为 3, 4, 6, 3。

ResNet 50

resnet 34 的构造函数如下。

[3, 4, 6, 3] 表示有 4 个 layer,每个 layer 的 Bottleneck 数量分别为 3, 4, 6, 3。

依此类推,ResNet 101 和 ResNet 152 也是由多个 layer 组成的。

_resnet()

上面所有的构造函数中,都调用了 _resnet() 方法来创建网络,下面来看看 _resnet() 方法。

可以看到,在 _resnet() 方法中,又调用了 ResNet() 方法创建模型,然后加载训练好的模型参数。

ResNet()

首先来看 ResNet() 方法的构造函数。

构造函数

构造函数的重要参数如下:

  • block:每个 layer 里面使用的 block,可以是 BasicBlock Bottleneck。

  • num_classes:分类数量,用于构建最后的全连接层。

  • layers:一个 list,表示每个 layer 中 block 的数量。

构造函数的主要流程如下:

  • 判断是否传入 norm_layer,没有传入,则使用 BatchNorm2d。

  • 判断是否传入孔洞卷积参数 replace_stride_with_dilation,如果不指定,则赋值为 [False, False, False],表示不使用孔洞卷积。

  • 读取分组卷积的参数 groups,width_per_group。

  • 然后真正开始构造网络。

  • conv1 层的结构是 Conv2d -> norm_layer -> ReLU。

  • conv2 层的代码如下,对应于 layer1,这个 layer 的参数没有指定 stride,默认 stride=1,因此这个 layer 不会改变图片大小:

  • conv3 层的代码如下,对应于 layer2(注意这个 layer 指定 stride=2,会降采样,详情看下面 _make_layer 的讲解):

  • conv4 层的代码如下,对应于 layer3(注意这个 layer 指定 stride=2,会降采样,详情看下面 _make_layer 的讲解):

  • conv5 层的代码如下,对应于 layer4(注意这个 layer 指定 stride=2,会降采样,详情看下面 _make_layer 的讲解):

  • 接着是 AdaptiveAvgPool2d 层和 fc 层。

  • 最后是网络参数的初始:

    • 卷积层采用 kaiming_normal_() 初始化方法。

    • bn 层和 GroupNorm 层初始化为 weight=1,bias=0。

    • 其中每个 BasicBlock 和 Bottleneck 的最后一层 bn 的 weight=0,可以提升准确率 0.2~0.3%。

完整的构造函数代码如下:

forward()

在 ResNet 中,网络经过层层封装,因此forward() 方法非常简洁。

数据变换大致流程如下:

  • 输入的图片形状是$3 \times 224 \times 224$。

  • 图片经过 conv1 层,输出图片大小为 $ 64 \times 112 \times 112$。

  • 图片经过 max pool 层,输出图片大小为 $ 64 \times 56 \times 56 $。

  • 对于 ResNet 18、ResNet 34 (使用 BasicBlock):

    • 图片经过 conv2 层,对应于 layer1,输出图片大小为 $ 64 \times 56 \times 56$。(注意,图片经过这个 layer, 大小是不变的)

    • 图片经过 conv3 层,对应于 layer2,输出图片大小为 $ 128 \times 28 \times 28$。

    • 图片经过 conv4 层,对应于 layer3,输出图片大小为 $ 256 \times 14 \times 14$。

    • 图片经过 conv5 层,对应于 layer4,输出图片大小为 $ 512 \times 7 \times 7$。

    • 图片经过 avg pool 层,输出大小为 $ 512 \times 1 \times 1$。

  • 对于 ResNet 50、ResNet 101、ResNet 152(使用 Bottleneck):

    • 图片经过 conv2 层,对应于 layer1,输出图片大小为 $ 256 \times 56 \times 56$。(注意,图片经过这个 layer, 大小是不变的)

    • 图片经过 conv3 层,对应于 layer2,输出图片大小为 $ 512 \times 28 \times 28$。

    • 图片经过 conv4 层,对应于 layer3,输出图片大小为 $ 1024 \times 14 \times 14$。

    • 图片经过 conv5 层,对应于 layer4,输出图片大小为 $ 2048 \times 7 \times 7$。

    • 图片经过 avg pool 层,输出大小为 $ 2048 \times 1 \times 1$。

  • 图片经过 fc 层,输出维度为 $ num_classes$,表示每个分类的 logits。

在构造函数中可以看到,上面每个 layer 都是使用 _make_layer() 方法来创建层的,下面来看下 _make_layer() 方法。

_make_layer()

_make_layer()方法的参数如下:

  • block:每个 layer 里面使用的 block,可以是 BasicBlock,Bottleneck。

  • planes:输出的通道数

  • blocks:一个整数,表示该层 layer 有多少个 block。

  • stride:第一个 block 的卷积层的 stride,默认为 1。注意,只有在每个 layer 的第一个 block 的第一个卷积层使用该参数。

  • dilate:是否使用孔洞卷积。

主要流程如下:

  • 判断孔洞卷积,计算 previous_dilation 参数。

  • 判断 stride 是否为 1,输入通道和输出通道是否相等。如果这两个条件都不成立,那么表明需要建立一个 1 X 1 的卷积层,来改变通道数和改变图片大小。具体是建立 downsample 层,包括 conv1x1 -> norm_layer。

  • 建立第一个 block,把 downsample 传给 block 作为降采样的层,并且 stride 也使用传入的 stride(stride=2)。后面我们会分析 downsample 层在 BasicBlock 和 Bottleneck 中,具体是怎么用的。

  • 改变通道数self.inplanes = planes * block.expansion。

    • 在 BasicBlock 里,expansion=1,因此这一步不会改变通道数。

    • 在 Bottleneck 里,expansion=4,因此这一步会改变通道数。

  • 图片经过第一个 block后,就会改变通道数和图片大小。接下来 for 循环添加剩下的 block。从第 2 个 block 起,输入和输出通道数是相等的,因此就不用传入 downsample 和 stride(那么 block 的 stride 默认使用 1,下面我们会分析 BasicBlock 和 Bottleneck 的源码)。

下面来看 BasicBlock 和 Bottleneck 的源码。

BasicBlock

构造函数

BasicBlock 构造函数的主要参数如下:

  • inplanes:输入通道数。

  • planes:输出通道数。

  • stride:第一个卷积层的 stride。

  • downsample:从 layer 中传入的 downsample 层。

  • groups:分组卷积的分组数,使用 1

  • base_width:每组卷积的通道数,使用 64

  • dilation:孔洞卷积,为1,表示不使用 孔洞卷积

主要流程如下:

  • 首先判断是否传入了 norm_layer 层,如果没有,则使用 BatchNorm2d。

  • 校验参数:groups == 1,base_width == 64,dilation == 1。也就是说,在 BasicBlock 中,不使用孔洞卷积和分组卷积。

  • 定义第 1 组 conv3x3 -> norm_layer -> relu,这里使用传入的 stride 和 inplanes。(如果是 layer2 ,layer3 ,layer4 里的第一个 BasicBlock,那么 stride=2,这里会降采样和改变通道数)。

  • 定义第 2 组 conv3x3 -> norm_layer -> relu,这里不使用传入的 stride (默认为 1),输入通道数和输出通道数使用planes,也就是不需要降采样和改变通道数。

forward()

forward() 方法的主要流程如下:

  • x 赋值给 identity,用于后面的 shortcut 连接。

  • x 经过第 1 组 conv3x3 -> norm_layer -> relu,如果是 layer2 ,layer3 ,layer4 里的第一个 BasicBlock,那么 stride=2,第一个卷积层会降采样。

  • x 经过第 1 组 conv3x3 -> norm_layer,得到 out。

  • 如果是 layer2 ,layer3 ,layer4 里的第一个 BasicBlock,那么 downsample 不为空,会经过 downsample 层,得到 identity。

  • 最后将 identity 和 out 相加,经过 relu ,得到输出。

注意,2 个卷积层都需要经过 relu 层,但它们使用的是同一个 relu 层。

Bottleneck

构造函数

参数如下:

  • inplanes:输入通道数。

  • planes:输出通道数。

  • stride:第一个卷积层的 stride。

  • downsample:从 layer 中传入的 downsample 层。

  • groups:分组卷积的分组数,使用 1

  • base_width:每组卷积的通道数,使用 64

  • dilation:孔洞卷积,为1,表示不使用 孔洞卷积

主要流程如下:

  • 首先判断是否传入了 norm_layer 层,如果没有,则使用 BatchNorm2d。

  • 计算 width,等于传入的 planes,用于中间的 $ 3 \times 3 $ 卷积。

  • 定义第 1 组 conv1x1 -> norm_layer,这里不使用传入的 stride,使用 width,作用是进行降维,减少通道数。

  • 定义第 2 组 conv3x3 -> norm_layer,这里使用传入的 stride,输入通道数和输出通道数使用width。(如果是 layer2 ,layer3 ,layer4 里的第一个 Bottleneck,那么 stride=2,这里会降采样)。

  • 定义第 3 组 conv1x1 -> norm_layer,这里不使用传入的 stride,使用 planes * self.expansion,作用是进行升维,增加通道数。

forward()

forward() 方法的主要流程如下:

  • x 赋值给 identity,用于后面的 shortcut 连接。

  • x 经过第 1 组 conv1x1 -> norm_layer -> relu,作用是进行降维,减少通道数。

  • x 经过第 2 组 conv3x3 -> norm_layer -> relu。如果是 layer2 ,layer3 ,layer4 里的第一个 Bottleneck,那么 stride=2,第一个卷积层会降采样。

  • x 经过第 1 组 conv1x1 -> norm_layer -> relu,作用是进行降维,减少通道数。

  • 如果是 layer2 ,layer3 ,layer4 里的第一个 Bottleneck,那么 downsample 不为空,会经过 downsample 层,得到 identity。

  • 最后将 identity 和 out 相加,经过 relu ,得到输出。

注意,3 个卷积层都需要经过 relu 层,但它们使用的是同一个 relu 层。

总结

最后,总结一下。

  • BasicBlock 中有 1 个 $3 \times 3 $卷积层,如果是 layer 的第一个 BasicBlock,那么第一个卷积层的 stride=2,作用是进行降采样。

  • Bottleneck 中有 2 个 $1 \times 1 $卷积层, 1 个 $3 \times 3 $ 卷积层。先经过第 1 个 $1 \times 1 $卷积层,进行降维,然后经过 $3 \times 3 $卷积层(如果是 layer 的第一个 Bottleneck,那么 $3 \times 3 $ 卷积层的 stride=2,作用是进行降采样),最后经过 $1 \times 1 $卷积层,进行升维 。

ResNet 18 图解

layer1

下面是 ResNet 18 ,使用的是 BasicBlock 的 layer1,特点是没有进行降采样,卷积层的 stride = 1,不会降采样。在进行 shortcut 连接时,也没有经过 downsample 层。

layer2,layer3,layer4

而 layer2,layer3,layer4 的结构图如下,每个 layer 包含 2 个 BasicBlock,但是第 1 个 BasicBlock 的第 1 个卷积层的 stride = 2,会进行降采样。在进行 shortcut 连接时,会经过 downsample 层,进行降采样和降维。

ResNet 50 图解

layer1

在 layer1 中,首先第一个 Bottleneck 只会进行升维,不会降采样。shortcut 连接前,会经过 downsample 层升维处理。第二个 Bottleneck 的 shortcut 连接不会经过 downsample 层。

layer2,layer3,layer4

而 layer2,layer3,layer4 的结构图如下,每个 layer 包含多个 Bottleneck,但是第 1 个 Bottleneck 的 $ 3 \times 3 $ 卷积层的 stride = 2,会进行降采样。在进行 shortcut 连接时,会经过 downsample 层,进行降采样和降维。

参考资料

如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。

我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学。

最后更新于

这有帮助吗?