一、 ResNet(残差网络)和 DenseNet(密集连接网络)
ResNet(残差网络)和 DenseNet(密集连接网络)都是深度学习中非常经典的卷积神经网络架构,它们在图像分类、目标检测等诸多视觉任务中表现出色。下面从多个方面对二者进行具体比较:
1. 网络结构
连接方式
- ResNet:采用残差连接(shortcut connection),即通过跳跃连接将前一层的输出直接加到后面某一层的输出上。例如,在一个残差块中,输入 x 经过一系列卷积操作得到 \(F(x)\),最终输出为 \(y = F(x)+x\)。这种连接方式有效缓解了深度网络训练时的梯度消失和梯度爆炸问题,使得网络可以更深。
- DenseNet:采用密集连接方式,网络中的每一层都与前面所有层相连。在一个密集块中,第 i 层的输入是前面 \(i - 1\) 层输出特征图在通道维度上的拼接。假设第 i 层之前的所有层输出特征图数量分别为 \(x_1,x_2,\cdots,x_{i - 1}\),那么第 i 层的输入为 \([x_1,x_2,\cdots,x_{i - 1}]\),这种连接方式能促进特征的重复利用和传播。
模块设计
- ResNet:主要有两种基本模块,即用于较浅网络(如 ResNet18、ResNet34)的
BasicBlock
和用于更深网络(如 ResNet50、ResNet101、ResNet152)的Bottleneck
。BasicBlock
由两个 3x3 卷积层组成,Bottleneck
则通过 1x1 卷积进行降维和升维,减少了参数数量和计算量。 - DenseNet:基本模块是
DenseBlock
和Transition
层。DenseBlock
由多个DenseLayer
组成,每个DenseLayer
通常包含批归一化(BN)、ReLU 激活函数和卷积层;Transition
层用于在不同的DenseBlock
之间进行下采样和通道数的缩减。
2. 性能表现
特征利用效率
- ResNet:通过残差连接使得信息可以在不同层之间快速传递,但在特征的重复利用方面相对较弱。每一层主要学习当前层输入特征的残差信息,不同层之间的特征交互相对有限。
- DenseNet:由于其密集连接的特性,能够充分利用前面所有层的特征信息,大大提高了特征的利用率。每一层都可以获取到之前所有层的特征,使得模型能够学习到更丰富的特征表示,尤其在小数据集上表现出色。
计算量与内存需求
- ResNet:计算量和内存需求相对较为可控。特别是使用
Bottleneck
模块时,通过 1x1 卷积减少了中间层的通道数,从而降低了计算复杂度。因此,ResNet 在大规模数据集上训练时,能够在保证性能的同时,减少对计算资源的需求。 - DenseNet:由于每一层的输入都包含了前面所有层的特征,导致特征图的通道数会随着网络深度的增加而快速增长,从而增加了计算量和内存需求。尤其是在深层网络中,这种问题更为明显。
泛化能力
- ResNet:通过残差连接使得网络更容易训练,能够学习到更复杂的特征表示,从而在不同的数据集和任务上都具有较好的泛化能力。
- DenseNet:由于其密集连接的方式,使得模型能够学习到更丰富的特征信息,增强了模型的泛化能力。在一些数据集上,DenseNet 能够取得比 ResNet 更好的分类准确率。
3. 训练与收敛
训练速度
- ResNet:残差连接使得梯度能够更顺畅地传播,网络更容易收敛,训练速度相对较快。在大规模数据集上进行训练时,ResNet 能够更快地达到较好的性能。
- DenseNet:由于其密集连接的特性,训练时需要处理更多的特征信息,计算量较大,因此训练速度相对较慢。尤其是在网络较深、数据集较大的情况下,训练时间会明显增加。
收敛稳定性
- ResNet:残差连接有效缓解了梯度消失和梯度爆炸问题,使得网络在训练过程中更加稳定,收敛速度更快。即使在网络深度很大的情况下,也能保持较好的训练效果。
- DenseNet:虽然密集连接也有助于梯度的传播,但由于特征图通道数的快速增长,可能会导致训练过程中的数值不稳定问题。在训练时,需要更加小心地调整学习率等超参数,以保证网络的收敛。
4. 实际应用场景
资源受限场景
- ResNet:由于其计算量和内存需求相对较低,更适合在资源受限的设备上部署,如移动设备、嵌入式系统等。
- DenseNet:由于其较高的计算量和内存需求,在资源受限的场景下应用可能会受到一定的限制。
小数据集场景
- ResNet:在小数据集上也能取得较好的性能,但由于其特征利用效率相对较低,可能需要更多的训练数据来达到最佳效果。
- DenseNet:由于其密集连接的方式能够充分利用特征信息,在小数据集上表现更优,能够通过较少的训练数据学习到更丰富的特征表示。
大数据集场景
- ResNet:凭借其良好的训练速度和收敛稳定性,在大规模数据集上训练时具有明显优势,能够快速收敛并取得较好的性能。
- DenseNet:虽然在大数据集上也能取得不错的效果,但由于其训练速度较慢,可能需要更多的时间和计算资源。
5.代码演示
ResNet(残差网络):
from torch import nn
from torchsummary import summary
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = 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:
residual = self.downsample(x)
out += residual
return out
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(residual)
out += residual
out = self.relu(out)
return out
class MyResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
super(MyResNet, self).__init__()
self.inplanes = 8
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 8, layers[0], stride=1)
self.layer2 = self._make_layer(block, 16, layers[1], stride=2)
self.layer3 = self._make_layer(block, 32, layers[2], stride=2)
self.layer4 = self._make_layer(block, 64, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.2)
self.fc = nn.Linear(64 * block.expansion, num_classes)
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion # 在创建第一个残差块后更新 inplanes
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
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 = x.view(x.size(0), -1)
x = self.dropout(x)
x = self.fc(x)
return x
def myresnet18(num_classes=1000):
model = MyResNet(BasicBlock, [2, 2, 2, 2], num_classes=num_classes)
return model
def myresnet34(num_classes=1000):
model = MyResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes)
return model
def myresnet50(num_classes=1000):
model = MyResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes)
return model
def myresnet101(num_classes=1000):
model = MyResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes)
return model
def myresnet152(num_classes=1000):
model = MyResNet(Bottleneck, [3, 8, 36, 3], num_classes=num_classes)
return model
if __name__ == '__main__':
mynet = myresnet50(num_classes=2)
print(summary(mynet, (3, 224, 224)))
print(sum(p.numel() for p in mynet.parameters() if p.requires_grad))
DenseNet(密集连接网络):
import torch
from torch import nn
class Bottleneck(nn.Module):
def __init__(self, in_channels, out_channels):
super(Bottleneck, self).__init__()
self.conv1 = nn.Sequential(
nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels=in_channels, out_channels=4 * out_channels, kernel_size=1, stride=1, padding=0, bias=False)
)
self.conv2 = nn.Sequential(
nn.BatchNorm2d(4 * out_channels),
nn.ReLU(),
nn.Conv2d(in_channels=4 * out_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=False)
)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
return x
class DenseBlock(nn.Module):
def __init__(self, in_channels, out_channels, num_bottleneck):
super(DenseBlock, self).__init__()
layers = []
for i in range(num_bottleneck):
layers.append(Bottleneck(in_channels + i * out_channels, out_channels)) # 拼接(理解困难点)
self.layers = nn.ModuleList(layers)
def forward(self, x):
for layer in self.layers:
x = torch.cat((x, layer(x)), dim=1)
return x
class Transition(nn.Module):
def __init__(self, in_channels, out_channels):
super(Transition, self).__init__()
self.conv = nn.Sequential(
nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.AvgPool2d(kernel_size=2, stride=2)
)
def forward(self, x):
x = self.conv(x)
return x
class MyDenseNet(nn.Module):
def __init__(self, block_config, num_classes=10, growth_rate=32, reduction=0.5):
super(MyDenseNet, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, stride=2, padding=3, bias=False),
nn.BatchNorm2d(64),
nn.ReLU()
)
self.pool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
in_channels = 64
self.dense_blocks = nn.ModuleList() # 存放DenseBlock
self.transitions = nn.ModuleList() # 存放Transition
for i, block in enumerate(block_config):
self.dense_blocks.append(DenseBlock(in_channels, growth_rate, block))
in_channels += growth_rate * block
if i != len(block_config) - 1:
self.transitions.append(Transition(in_channels, int(in_channels * reduction)))
in_channels = int(in_channels * reduction)
self.bn2 = nn.BatchNorm2d(in_channels)
self.pool2 = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(in_channels, num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.pool1(x)
for i in range(len(self.dense_blocks)):
x = self.dense_blocks[i](x)
if i != len(self.dense_blocks) - 1:
x = self.transitions[i](x)
x = self.bn2(x)
x = self.pool2(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def densenet121(num_classes=10):
return MyDenseNet([6, 12, 24, 16], num_classes=num_classes, growth_rate=32, reduction=0.5)
def densenet169(num_classes=10):
return MyDenseNet([6, 12, 32, 32], num_classes=num_classes, growth_rate=32, reduction=0.5)
def densenet201(num_classes=10):
return MyDenseNet([6, 12, 48, 32], num_classes=num_classes, growth_rate=32, reduction=0.5)
def densenet264(num_classes=10):
return MyDenseNet([6, 12, 64, 48], num_classes=num_classes, growth_rate=32, reduction=0.5)
if __name__ == '__main__':
model = densenet121()
# 测试数据
x = torch.randn(2, 3, 224, 224)
y = model(x)
print(y.shape)
print(sum(p.numel() for p in model.parameters()))
print(sum(p.numel() for p in model.parameters() if p.requires_grad))
二、补充(BN位置不同的差异)
Batch Normalization(BN)是深度学习中常用的一种技术,用于加速网络训练和提高模型的稳定性。在池化层前后使用 BN 会产生不同的效果,下面从原理、对特征的影响以及实际应用等方面详细分析它们的区别。
1. 原理上的区别
- 池化前使用 BN
- 在池化操作之前应用 BN,BN 会对卷积层输出的特征图进行归一化处理。具体来说,对于输入的特征图
(其中 i 表示通道索引,j 表示特征图上的位置索引),BN 会计算每个通道的均值
和方差
,然后进行归一化:
) 其中
是一个很小的常数,用于保证分母不为零。接着,通过可学习的参数
和
对归一化后的特征进行缩放和平移:
- 这样做的目的是使得特征图在进入池化层之前具有稳定的分布,有助于梯度的传播,从而加速网络的训练。
- 在池化操作之前应用 BN,BN 会对卷积层输出的特征图进行归一化处理。具体来说,对于输入的特征图
- 池化后使用 BN
- 当在池化操作之后使用 BN 时,BN 处理的是经过池化后的特征图。池化操作(如最大池化或平均池化)会改变特征图的尺寸和数值分布。BN 同样会对池化后的特征图计算均值和方差,并进行归一化和缩放平移操作。由于池化后的特征图已经经过了下采样,其统计信息(均值和方差)与池化前不同,因此归一化的效果也会有所差异。
2. 对特征的影响
- 池化前使用 BN
- 特征分布调整:可以使卷积层输出的特征图在进入池化层之前具有更稳定的分布。这有助于缓解梯度消失或梯度爆炸问题,使得网络能够更有效地学习特征。例如,在卷积层中,不同通道的特征图可能具有不同的均值和方差,通过 BN 可以将它们拉到相似的分布上,避免某些通道的特征对后续层的影响过大。
- 增强特征多样性:由于归一化操作会调整特征的数值范围,使得每个通道的特征都能在相对公平的环境下被池化层处理,从而增强了特征的多样性。这有助于模型学习到更丰富的特征表示。
- 池化后使用 BN
- 适应池化后的特征分布:池化操作会改变特征图的数值分布,在池化后使用 BN 可以对这种变化进行调整,使得后续层能够更好地处理池化后的特征。例如,最大池化会保留特征图中的最大值,而平均池化会计算平均值,这些操作都会导致特征图的分布发生变化,BN 可以对其进行归一化,使其更适合后续的处理。
- 减少信息损失:池化操作会导致一定程度的信息损失,在池化后使用 BN 可以在一定程度上减少这种损失。通过调整特征的分布,BN 可以使得池化后的特征更具有代表性,从而提高模型的性能。
3. 实际应用中的区别
- 计算效率
- 池化前使用 BN:通常在池化前使用 BN 会增加一些计算量,因为需要对卷积层输出的较大尺寸的特征图进行归一化操作。但是,由于归一化后的特征图更易于后续处理,可能会减少整体的训练时间。
- 池化后使用 BN:池化后的特征图尺寸通常较小,因此在池化后使用 BN 的计算量相对较小。这在处理大规模数据或计算资源有限的情况下可能更具优势。
- 模型性能
- 池化前使用 BN:在一些情况下,池化前使用 BN 可以提高模型的性能,特别是当卷积层输出的特征图分布差异较大时。通过归一化操作,可以使模型更好地学习到不同通道的特征,从而提高分类或回归的准确性。
- 池化后使用 BN:在另一些情况下,池化后使用 BN 可能更合适。例如,当池化操作对特征图的分布影响较大时,在池化后进行归一化可以更好地适应这种变化,从而提高模型的性能。