ResNet论文学习
- 引言
- Deep Residual Learning 深度残差学习
- Residual Learning 残差学习
- Identity Mapping by Shortcuts 通过捷径来恒等映射
- 网络结构
- Plain Network
- Residual Network
- 实现细节
- 实验
- 总结
- 代码复现
- Building block
- Bottleneck
- Resnet 18
- Resnet 34
- Resnet 50
引言
深度网络自然地以端到端的多层方式集成低/中/高级特征和分类器,特征的“级别”可通过堆叠层的数量来丰富
随着网络层数加深,提取的特征越强,但是
网络层数越深就一定性能越好吗?答案是否定的
深层网络存在的问题
-
梯度消失/梯度爆炸
这个问题已经通过 normalized initialization 和 intermediate normalization layers 得到了很大程度的解决,这使得具有数十层的网络能够开始收敛随机梯度下降(SGD)与反向传播 -
网络退化
随着网络层数的增加,准确率趋于饱和,然后迅速下降。这说明,并不是所有的网络结构都同样容易优化。
为了解释这一现象,作者考虑了一个较浅的网络结构及其对应的更深版本,后者在前者的基础上增加了更多的层。
理论上,对于这个更深的网络,可以构造一个解:新增加的层执行 identity mapping (即直接将输入传递到输出),而其他层则复制自较浅网络中已经学习到的层(就是把较浅的网络搬过来)。这样构造出的解应该能使得更深的网络不会比其较浅的对应网络有更高的训练误差。
然而,实验显示,目前使用的优化算法似乎无法找到与这个构造解相当好或更好的解,或者在合理的时间内无法找到这样的解。这意味着,尽管理论上更深的网络不应该有更差的性能,但在实际优化过程中却遇到了困难。这也是引入残差学习(ResNet)的初衷之一,即通过学习残差来缓解这种优化难题。
为了解决深度神经网络训练中的退化问题,作者提出了深度残差学习框架。
在这个框架中,不再期望网络的几层直接拟合一个 underlying mapping,而是让这些层去拟合一个残差映射(residual mapping)。
具体而言,
假设 underlying mapping 是 H ( x ) \mathcal{H}(x) H(x),那么让堆叠的非线性层去拟合另一个映射 F : = H ( x ) − x \mathcal{F}:=\mathcal{H}(x)-x F:=H(x)−x,那么原始的映射就变成了 F ( x ) + x \mathcal{F}(x)+x F(x)+x
假设优化残差映射 (residual mapping) 比优化原始的、未引用的映射更容易。
图中展示了残差学习的一个基本构建块。右侧的分支直接将输入 x x x 通过一个“快捷连接”传递到输出,这就是所谓的恒等映射(identity mapping)。
而 F ( x ) \mathcal{F}(x) F(x) 表示的是一个由 权重层和非线性激活函数(例如ReLU)组成的映射,
其目的是学习输入 x x x 和输出 H ( x ) \mathcal{H}(x) H(x) 之间的残差,即 F ( x ) : = H ( x ) − x \mathcal{F}(x) := \mathcal{H}(x) - x F(x):=H(x)−x。
图中所示的构建块的输出是通过将 F ( x ) \mathcal{F}(x) F(x) 和 x x x 相加得到的,即 F ( x ) + x \mathcal{F}(x) + x F(x)+x,这里的 F ( x ) \mathcal{F}(x) F(x) 是通过两个权重层和非线性激活函数ReLU构成的子网络来学习的。
这样设计的好处是,如果 F ( x ) \mathcal{F}(x) F(x) 的理想输出是0,即不存在残差,那么 shortcut connection 可以使得恒等映射成为可能,即直接输出 x x x,这对于网络训练的稳定性和加速有很大帮助。
Deep Residual Learning 深度残差学习
Residual Learning 残差学习
即使理论上更深的网络应该不会比较浅的网络性能更差,因为更深的网络至少可以通过学习 恒等映射 来达到和较浅网络一样的性能。
但在实际中,传统的优化算法(solvers)在训练时 很难通过多个非线性层来近似恒等映射。
如果恒等映射是最优的,solvers 可以简单地 将多个非线性层的权重推向零(就是 F ( x ) \mathcal{F}(x) F(x) 这个映射趋于 0 0 0) 来近似恒等映射,而不是去学习一个复杂的映射关系。
Identity Mapping by Shortcuts 通过捷径来恒等映射
作者对若干个堆叠层应用残差学习,
具体而言,一个 building block 被定义为:
y = F ( x , { w i } ) + x \textbf{y}=\mathcal{F}(\textbf{x},\{w_i\})+\textbf{x} y=F(x,{wi})+x 其中, x , y \textbf{x},\textbf{y} x,y 是输入和输出向量, F ( x , { w i } ) \mathcal{F}(\textbf{x},\{w_i\}) F(x,{wi}) 代表被学习的残差映射
就像图 2,有 2 个层,那么 F = W 2 σ ( W 1 x ) \mathcal{F}=W_2\sigma(W_1 \textbf{x}) F=W2σ(W1x), 其中 σ \sigma σ 代表 ReLU 函数,为了简化符号,省略了 bias
F + x \mathcal{F}+\textbf{x} F+x 操作 是通过一个 shortcut connections 和 element-wise 加法来执行
加法执行后采用第二个 ReLU 函数应用非线性,如图 2
其中 x \textbf{x} x 和 F \mathcal{F} F 的维度必须一致,如果不一致,
可以执行一个线性投影 W s W_s Ws 匹配维度然后 shortcut connections: y = F ( x , { w i } ) + W s x \textbf{y}=\mathcal{F}(\textbf{x},\{w_i\})+W_s \textbf{x} y=F(x,{wi})+Wsx
残差函数 F \mathcal{F} F 的形式是灵活的,本文的实验中,涉及有 2 层或 3 层的残差函数 F \mathcal{F} F ,但是只有 1 层的残差函数 F \mathcal{F} F 近似于线性层 y = W 1 x + x \textbf{y}=W_1\textbf{x}+\textbf{x} y=W1x+x,作者并未观察到其优越性。
尽管为了简化表述,这一部分的描述和公式是基于全连接层的情形,但是这些概念和方法也同样适用于卷积层。
在实际的深度残差网络(ResNet)中,作者主要是在讨论卷积层的应用,因为卷积层是现代卷积神经网络中的核心组件。
函数 F ( x , { w i } ) \mathcal{F}(\textbf{x},\{w_i\}) F(x,{wi}) 可以表示多个卷积层,逐个通道地在两个特征图上执行元素加法。
网络结构
Plain Network
plain baselines 主要受 VGG nets 启发,卷积层大多具有3×3滤波器,并遵循两个简单的设计规则:
(i) 对于相同的输出特征图大小,各层具有相同数量的滤波器;
(ii) 如果特征图大小减半,则滤波器的数量增加一倍,以保持每层的时间复杂度。
通过步长为 2 的卷积层直接执行下采样。网络以一个全局平均池化层和一个带有 softmax 的 1000 路全连接层结束。图3(中间部分)加权层总数为34层。
作者的模型比VGG网络具有更少的卷积滤波器和更低的复杂度。
Residual Network
在 Plain Network 的基础上,插入 shortcut connections 将网络转换为对应的残差版本。
当输入和输出维度相同时 (图3中的实线 shortcuts),可以直接使用 y = F ( x , { w i } ) + x \textbf{y}=\mathcal{F}(\textbf{x},\{w_i\})+\textbf{x} y=F(x,{wi})+x
当维度增加时 (图3中的虚线 shortcuts),作者考虑两种选择:
(A) shortcut 仍然执行恒等映射,增加维度时填充额外的零项。这个选项不引入额外的参数;
(B) y = F ( x , { w i } ) + W s x \textbf{y}=\mathcal{F}(\textbf{x},\{w_i\})+W_s \textbf{x} y=F(x,{wi})+Wsx 中的投影用于匹配维度 (通过1×1卷积完成)。
对于这两个选项,当 shortcuts 跨越不同大小的特征图时,卷积核的步长为 2。
实现细节
对 ImageNet 的实现,随机采样图像的短边,以进行尺度增强。
从图像或其水平翻转中随机采样224×224裁剪,并减去每像素平均值。
使用标准颜色增强。在每次卷积之后在激活函数之前采用批归一化(BN)。
初始化权值,并从头开始训练所有的 plain/residual 网络。
使用SGD的小批量大小为256。学习率从0.1开始,当误差趋于平稳时除以10,模型的训练次数可达60 × 104次。
使用0.0001的权重衰减和0.9的动量。 没有使用dropout
在测试中,采用标准的 10 种作物测试进行比较研究。
为了获得最佳结果,采用了全卷积形式,并在多个尺度上平均得分(图像被调整大小)。
实验
首先评估了18层和34层的普通网。34层平面网如图3(中)所示。
18层的平面网也是类似的形式。参见表1了解详细的体系结构。
上表是用于测试 ImageNet 的架构。方括号中显示了构建块(参见图5),并显示了堆叠块的数量。下采样由conv3_1、conv4_1和conv5_1进行,步长为2。
总结
残差连接最核心的地方是 Identity Mapping by Shortcuts
就是说 resnet 是结合了 identify mapping 和 shortcut connection
shortcut connection 是那些跳过一个或多个层的连接。在本文中, shortcut connection 只是执行 identify mapping,其输出被添加到堆叠层的输出中。
identify mapping 连接既不增加额外的参数,也不增加计算复杂度。整个网络仍然可以通过反向传播的SGD进行端到端训练。
下面这个残差块中,旁边的分支首先是一个 shortcut connection,而在这个 shortcut connection 之上,执行了 identify mapping 将输入恒等映射到输出端,并与 underlying mapping 进行相加
代码复现
Building block
import torch
import torch.nn as nn
import torch.nn.functional as Fclass BuildingBlock(nn.Module):def __init__(self,inchannel,outchannel,stride=1):super(BuildingBlock,self).__init__()self.left = nn.Sequential(nn.Conv2d(inchannel,outchannel,kernel_size=3,stride=stride,padding=1,bias=False),nn.BatchNorm2d(outchannel),nn.ReLU(inplace=True),nn.Conv2d(outchannel,outchannel,kernel_size=3,stride=1,padding=1,bias=False),nn.BatchNorm2d(outchannel))self.shortcut = nn.Sequential()# 卷积核步长输入和输出通道不同时需要进行下采样if stride != 1 or inchannel != outchannel:self.shortcut = nn.Sequential(nn.Conv2d(inchannel,outchannel,kernel_size=1,stride=stride,bias=False),nn.BatchNorm2d(outchannel)) def forward(self,x):out = self.left(x)out += self.shortcut(x)out = F.relu(out)return out
-
不带参数的
nn.Sequential()
会返回什么?不带参数的
nn.Sequential()
会返回一个空的顺序容器。这意味着如果你通过这个空的nn.Sequential()
容器传递一个张量,它将直接返回这个张量而不做任何处理。在上面代码中,self.shortcut
初始化为一个空的nn.Sequential()
,这意味着如果没有进行下采样(即stride == 1
并且inchannel == outchannel
),那么shortcut
分支将简单地返回输入张量x
。 -
nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False)
中等号右边的stride
来自哪里?在这段代码中,
stride
是BuildingBlock
类的构造函数的一个参数,默认值为1
。当你创建一个BuildingBlock
实例时,你可以指定stride
的值,这个值将用于卷积层中的步长(stride
参数)。nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False)
中,等号右边的 stride 来自于 BuildingBlock 类的构造函数def __init__(self, inchannel, outchannel, stride=1)
中的 stride 参数。 -
当
if stride != 1 or inchannel != outchannel:
时,是如何实现下采样的?当
stride != 1
或inchannel != outchannel
时,需要对输入进行下采样以匹配主分支 (left
) 的输出维度。这是通过self.shortcut
分支实现的,它包含一个步长为stride
的1x1
卷积层和一个批量归一化层。1x1
卷积用于改变通道数(如果需要),而步长stride
用于减小特征图的空间尺寸(宽度和高度)。这样,shortcut
分支的输出就可以与主分支的输出相加了。 -
上面的代码中,已经在
def __init__(self, inchannel, outchannel, stride=1)
中指定了stride=1
,为什么还需要对stride != 1
进行判断?当创建 BuildingBlock 实例时,可以指定 stride 参数的值,包括 stride != 1 的情况。因此,需要对 stride != 1 进行判断,以决定是否需要进行下采样以匹配主分支和残差分支的输出尺寸。
在ResNet的基本块(building block)中,对于输入和输出的维度不同的情况,会使用 padding=1 来使维度相同,这样可以方便进行残差连接。
这样做的目的是为了确保在每个基本块中,输入和输出的尺寸相同,以便能够将它们相加。
nn.Conv2d()
方法用于创建一个二维卷积层。其中,bias 是一个布尔值参数,用于指定是否在卷积操作中使用偏置项(bias)。如果设置为 True,则会添加一个可学习的偏置项到输出;如果设置为 False,则不会添加偏置项。默认值为 True。
偏置(bias)是一个可学习的参数,用于调整每个卷积核在卷积操作中的作用。偏置的作用类似于线性回归中的截距,它允许模型学习在没有输入时的输出值。具体地说,在卷积操作中,偏置被添加到每个卷积核的输出中,然后通过激活函数进行处理,以产生最终的特征图。
偏置的引入可以增加模型的灵活性,使其能够更好地拟合训练数据。通过学习适当的偏置值,模型可以更好地捕获数据中的偏移和非线性关系。
Bottleneck
class Bottleneck(nn.Module):def __init__(self, inchannel, outchannel, stride=1):super(Bottleneck, self).__init__()self.left = nn.Sequential(nn.Conv2d(inchannel, int(outchannel / 4), kernel_size=1, stride=stride, padding=0, bias=False),nn.BatchNorm2d(int(outchannel / 4)),nn.ReLU(inplace=True),nn.Conv2d(int(outchannel / 4), int(outchannel / 4), kernel_size=3, stride=1, padding=1, bias=False),nn.BatchNorm2d(int(outchannel / 4)),nn.ReLU(inplace=True),nn.Conv2d(int(outchannel / 4), outchannel, kernel_size=1, stride=1, padding=0, bias=False),nn.BatchNorm2d(outchannel),)self.shortcut = nn.Sequential()if stride != 1 or inchannel != outchannel:self.shortcut = nn.Sequential(nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),nn.BatchNorm2d(outchannel))def forward(self, x):out = self.left(x)y = self.shortcut(x)out += self.shortcut(x)out = F.relu(out)
- 为什么
self.left
结构中,三个卷积层除了第一层的输入通道数和第三层的输出通道数以外,全部是outchannel / 4
?
在 ResNet 的 Bottleneck 结构中,前两个卷积层的输出通道数被设为 int(outchannel / 4)
是为了减少计算量和参数数量,同时保持网络的表达能力。
Bottleneck 结构由三个卷积层组成:第一个和第三个卷积层使用 1x1
卷积核,主要用于改变通道数;第二个卷积层使用 3x3
卷积核,负责提取空间特征。在这种设计中,第一个 1x1
卷积层将输入通道数减少到 outchannel / 4
,这样第二个 3x3
卷积层就可以在较小的通道数上操作,从而减少计算量。最后,第三个 1x1
卷积层再次将通道数增加到 outchannel
,恢复到原来的维度。
通过这种方式,Bottleneck 结构能够在减少计算量的同时,保持足够的表达能力,这是 ResNet 在深层网络中有效减少参数数量和计算复杂度的关键设计之一。
- 为什么三个卷积层的padding分别是0,1,0?
在这个 Bottleneck 结构中,三个卷积层的 padding
设置是为了控制特征图的空间尺寸:
-
第一个卷积层使用
kernel_size=1
和padding=0
。因为1x1
卷积不会改变特征图的空间尺寸,所以不需要填充(padding=0
)。 -
第二个卷积层使用
kernel_size=3
和padding=1
。3x3
卷积会减小特征图的尺寸,除非使用填充。为了保持特征图的空间尺寸不变,需要使用padding=1
(当stride=1
时)。这样,卷积操作后,特征图的宽度和高度保持不变。 -
第三个卷积层再次使用
kernel_size=1
和padding=0
,原因同第一个卷积层一样,1x1
卷积不改变特征图的空间尺寸,所以不需要填充。
通过这种设置,Bottleneck 结构可以在不改变特征图空间尺寸的情况下,有效地进行特征提取和通道数的变换。
Resnet 18
import torch
import torch.nn as nn
import torch.nn.functional as Fdef conv3x3(in_channels, out_channels, stride=1):return nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)def conv1x1(in_channels, out_channels, stride=1):return nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False)class BasicBlock(nn.Module):expansion = 1def __init__(self, in_channels, out_channels, stride=1, downsample=None):super(BasicBlock, self).__init__()self.conv1 = conv3x3(in_channels, out_channels, stride)self.bn1 = nn.BatchNorm2d(out_channels)self.relu = nn.ReLU(inplace=True)self.conv2 = conv3x3(out_channels, out_channels)self.bn2 = nn.BatchNorm2d(out_channels)self.downsample = downsampledef forward(self, x):identity = xout = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)if self.downsample is not None:identity = self.downsample(x)out += identityout = self.relu(out)return outclass ResNet(nn.Module):def __init__(self, block, layers, num_classes=1000):super(ResNet, self).__init__()self.in_channels = 64self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)self.bn1 = nn.BatchNorm2d(64)self.relu = nn.ReLU(inplace=True)self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)self.layer1 = self._make_layer(block, 64, layers[0])self.layer2 = self._make_layer(block, 128, layers[1], stride=2)self.layer3 = self._make_layer(block, 256, layers[2], stride=2)self.layer4 = self._make_layer(block, 512, layers[3], stride=2)self.avgpool = nn.AdaptiveAvgPool2d((1, 1))self.fc = nn.Linear(512 * block.expansion, num_classes)def _make_layer(self, block, out_channels, blocks, stride=1):downsample = Noneif stride != 1 or self.in_channels != out_channels * block.expansion:downsample = nn.Sequential(conv1x1(self.in_channels, out_channels * block.expansion, stride),nn.BatchNorm2d(out_channels * block.expansion),)layers = []layers.append(block(self.in_channels, out_channels, stride, downsample))self.in_channels = out_channels * block.expansionfor _ in range(1, blocks):layers.append(block(self.in_channels, out_channels))return nn.Sequential(*layers)def forward(self, x):x = self.conv1(x)x = self.bn1(x)x = self.relu(x)x = self.maxpool(x)x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)x = self.avgpool(x)x = torch.flatten(x, 1)x = self.fc(x)return xdef resnet18(num_classes=1000):return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)# 示例化模型
model = resnet18(num_classes=1000)
下面这个代码实现之所以一些参数和论文网络结构原图不一样,是因为这是一个简化/修改版的 ResNet18 结构。例如,在处理较小的图像数据集时(比如 CIFAR-10,其图像尺寸为 32x32),可能不需要在一开始就迅速减小空间尺寸,这种情况下使用较小的卷积核和不使用步长为 2 的操作可能更为合适。
在原始的 ResNet 结构中,较大的卷积核和步长用于减小输出特征图的尺寸,同时增加感受野,使得网络可以捕捉到更大范围的输入信息。这在处理较大的图像(如 ImageNet 数据集中的图像,尺寸通常为 224x224)时是有优势的。因此,如果代码是针对不同尺寸的数据集设计的,那么这种调整是合理的。
'''----------ResNet18----------'''
class ResNet_18(nn.Module):def __init__(self, ResidualBlock, num_classes=10):super(ResNet_18, self).__init__()self.inchannel = 64self.conv1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),nn.BatchNorm2d(64),nn.ReLU(),)self.layer1 = self.make_layer(ResidualBlock, 64, 2, stride=1)self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)self.fc = nn.Linear(512, num_classes)def make_layer(self, block, channels, num_blocks, stride):strides = [stride] + [1] * (num_blocks - 1) # strides=[1,1]layers = []for stride in strides:layers.append(block(self.inchannel, channels, stride))self.inchannel = channelsreturn nn.Sequential(*layers)def forward(self, x): # 3*32*32 3是通道数,32*32是特征图尺寸out = self.conv1(x) # 64*32*32 卷积层conv1输出通道是64#layer1的输出通道是64,且strile=1,所以输出的通道数不改变out = self.layer1(out) # 64*32*32 #layer2 strile=2,特征图尺寸宽度和高度各减小一半,通道数翻倍out = self.layer2(out) # 128*16*16 out = self.layer3(out) # 256*8*8 layer3同理out = self.layer4(out) # 512*4*4 layer4同理out = F.avg_pool2d(out, 4) # 512*1*1out = out.view(out.size(0), -1) # 512out = self.fc(out)return out
-
为什么
self.inchannel = 64
?在 ResNet18 的初始层中,卷积层
conv1
将输入的 RGB 图像(3 通道)转换为 64 通道的特征图。因此,设置self.inchannel = 64
是为了记录当前特征图的通道数,以便在构建后续的残差层时使用。这个值会随着网络的深入而更新,以匹配不同残差层的输入通道数。 -
make_layer
中的参数channels
是输入通道还是输出通道?在
make_layer
方法中,参数channels
指的是输出通道数。每个残差块(ResidualBlock
)的输出通道数由这个参数确定,而输入通道数则由self.inchannel
维护和提供。 -
详细地解释
strides = [stride] + [1] * (num_blocks - 1)
和*layers
-
strides = [stride] + [1] * (num_blocks - 1)
:这行代码用于生成一个列表,其中第一个元素是stride
(可以是 1 或 2),其余元素都是 1。列表的长度等于num_blocks
。这是因为在每个残差层中,只有第一个残差块可能会改变特征图的尺寸(通过stride
控制),而后续的残差块则保持特征图尺寸不变(即stride=1
)。例如,对于self.layer2
,strides
将为[2, 1]
,意味着第一个残差块将特征图尺寸减半,而第二个残差块保持尺寸不变。 -
*layers
:这是 Python 中的解包操作符,用于将layers
列表中的元素作为独立的参数传递给nn.Sequential
。这样,nn.Sequential
可以接收一个由多个残差块组成的序列,从而构建出一个完整的残差层。在这个例子中,layers
列表包含了一个残差层中的所有残差块。
-
在PyTorch中,avg_pool2d
函数用于对二维信号(例如图像)进行平均池化操作。函数的调用形式一般是:
torch.nn.functional.avg_pool2d(input, kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True, divisor_override=None)
其中,input
是输入的四维张量,形状为(N, C, H, W)
,分别代表批大小、通道数、高度和宽度。
在你提到的avg_pool2d(out, 4)
中:
out
:输入的四维张量,即要进行池化的数据。4
:kernel_size
参数,表示池化窗口的大小。如果是一个单一的整数,那么池化窗口的高度和宽度都将是这个值。在这个例子中,池化窗口的大小是4x4。
其他参数在这个例子中使用的是默认值:
stride
:窗口移动的步长。默认情况下,stride
等于kernel_size
。padding
:在输入数据周围添加的零填充的数量。默认为0。ceil_mode
:是否使用向上取整来计算输出大小。默认为False。count_include_pad
:在计算平均值时是否包括填充的零。默认为True。divisor_override
:如果设置了,它将用作除数,否则使用池化区域的大小。默认为None。
因此,avg_pool2d(out, 4)
将对输入张量out
执行4x4的平均池化操作。
4x4的平均池化操作将会根据池化窗口的大小和步长来减小特征图(feature maps)的尺寸。在avg_pool2d(out, 4)
的情况下,池化窗口的大小是4x4,且如果没有另外指定步长(stride),步长默认也是4。
假设输入特征图的尺寸是(N, C, H, W)
,其中:
N
是批大小(batch size)C
是通道数(channel number)H
是特征图的高度W
是特征图的宽度
经过4x4的平均池化操作后,输出特征图的尺寸将变为(N, C, H/4, W/4)
。这是因为池化窗口沿着特征图的高度和宽度每次移动4个单位,因此,输出特征图的高度和宽度都将是输入特征图的高度和宽度除以4(假设H和W都能被4整除)。如果H或W不能被4整除,实际的输出尺寸还会受到填充(padding)和向上取整(ceil_mode)等参数的影响。