深度学习:ResNet每一层的输出形状
其中
/**在输出通道数为64、步幅为2的7 × 7卷积层后,接步幅为2的3 × 3的最大汇聚层,与GoogLeNet区别是每个卷积层后增加了批量规范层**/
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))
/**ResNet在后面接了由4个残差块组成的模块,每个模块使用若干个同样输出通道数的残差块,本例每个模块使用2个残差块
**/
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))
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(), nn.Linear(512, 10))
##模块的构成
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
##残差块的构成(详细解释在下面)
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)
残差块里首先有2个有相同输出通道数的3 × 3卷积层。每个卷积层后接一个批量规范化层和ReLU激活函数。然后我们通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。如果想改变通道数,就需要引入一个额外的1 × 1卷积层来将输入变换成需要的形状后再做相加运算。
接下来通过一个实例来展示下ResNet的每一层的输出形状
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)
输出结果为
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 128, 28, 28])
Sequential output shape: torch.Size([1, 256, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
Flatten output shape: torch.Size([1, 512])
Linear output shape: torch.Size([1, 10])
解释每一层的输出形状
-
b1
输入形状:torch.Size([1, 1, 224, 224])输出形状:torch.Size([1, 64, 56, 56])
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3):将输入的 1 个通道扩展为 64 个通道,特征图的尺寸从 224x224 变为 112x112。
卷积层的输出特征图的尺寸可以通过以下公式计算: Output Size=(Input Size−Kernel
Size+2×Padding)/Stride+1 本例即Output Size=(224−7+2×3)/2+1 = 112nn.BatchNorm2d(64):对 64 个通道进行批量归一化。
nn.ReLU():应用 ReLU 激活函数。
nn.MaxPool2d(kernel_size=3, stride=2, padding=1):最大池化层将特征图的尺寸从 112x112 变为 56x56。最大池化的输出特征图的尺寸可以通过以下公式计算: Output Size=(Input Size−Kernel
Size+2×Padding)/2Stride+1 本例即Output Size=(112−3+2×1)/2+1=56 -
b2:
输入形状:torch.Size([1, 64, 56, 56])
输出形状:torch.Size([1, 64, 56, 56])
解释:resnet_block(64, 64, 2, first_block=True):包含两个残差块,输入和输出通道数均为 64,特征图的尺寸保持不变(56x56)。
-
b3:
输入形状:torch.Size([1, 64, 56, 56])
输出形状:torch.Size([1, 128, 28, 28])
解释:resnet_block(64, 128, 2):包含两个残差块,输入通道数为 64,输出通道数为 128,特征图的尺寸减半(56x56 -> 28x28)。
是通过在第一个残差块中使用步幅为 2 的卷积操作来实现的。特征图尺寸减半的原因是为了在增加网络深度的同时,减小特征图的尺寸,从而减少计算量和参数数量,同时增加感受野,提高网络的性能和效率。
-
b4:
输入形状:torch.Size([1, 128, 28, 28])
输出形状:torch.Size([1, 256, 14, 14])
解释:resnet_block(128, 256, 2):包含两个残差块,输入通道数为 128,输出通道数为 256,特征图的尺寸减半(28x28 -> 14x14)。
-
b5:
输入形状:torch.Size([1, 256, 14, 14])
输出形状:torch.Size([1, 512, 7, 7])
解释:resnet_block(256, 512, 2):包含两个残差块,输入通道数为 256,输出通道数为 512,特征图的尺寸减半(14x14 -> 7x7)。
-
AdaptiveAvgPool2d:
输入形状:torch.Size([1, 512, 7, 7])
输出形状:torch.Size([1, 512, 1, 1])
解释:自适应平均池化层将特征图的尺寸调整为 1x1,通道数保持不变。
-
Flatten:
输入形状:torch.Size([1, 512, 1, 1])
输出形状:torch.Size([1, 512])
解释:展平层将多维特征图展平成一维向量,总元素数为 512。
-
Linear:
输入形状:torch.Size([1, 512])
输出形状:torch.Size([1, 10])
解释:全连接层将输入的 512 个元素映射到 10 个元素,输出 10 个类别的概率分布。