1. 图7.4.1 中的 Inception 块与残差块之间的主要区别是什么?在删除了 Inception 块中的一些路径之后,它们是如何相互关联的?
图中的 Inception 块与残差块之间的主要区别在于它们的结构和功能目的。
-
结构区别:
- Inception 块:
- Inception 块由多个并行的路径组成,每个路径执行不同的卷积操作(例如,1x1 卷积、3x3 卷积、5x5 卷积)或池化操作(例如,3x3 最大池化)。
- 最终,这些并行路径的输出会在通道维度上合并(concatenate),以形成最终的输出。
- 残差块:
- 残差块的主要结构是通过跳跃连接(skip connection)将输入直接与输出相加。这种结构通常包含两个卷积层,每层后接批归一化(Batch Normalization)和激活函数(如 ReLU)。
- 通过这种直接相加的方式,残差块可以帮助缓解深层网络中的梯度消失问题,并促进更快的网络收敛。
- Inception 块:
-
功能目的:
- Inception 块:
- 目的是通过多尺度特征提取来捕捉不同大小的特征,并且通过并行计算提高计算效率。
- 每个路径负责提取不同大小的感受野的信息,从而提升特征表示的丰富性和多样性。
- 残差块:
- 目的是通过添加残差连接来简化优化过程,并减轻深度增加所带来的退化问题。
- 残差连接使得网络可以更容易地学习恒等映射,这在实际应用中证明可以训练出更深的网络。
- Inception 块:
-
删除路径后的关系:
- 如果从 Inception 块中删除一些路径(例如,只保留 1x1 卷积路径和 3x3 卷积路径),它们将变得更加接近于简单的卷积块,但仍然保持并行路径的特性。
- 残差块则没有并行路径的设计,主要依赖于跳跃连接。因此,即使从 Inception 块中删除一些路径,它们仍然不会完全变成残差块,因为残差块的核心在于输入与输出的直接相加。
总结来说,Inception 块和残差块在设计理念和结构上有显著区别。Inception 块侧重于并行多尺度特征提取,而残差块则侧重于通过残差连接来简化深度网络的训练。如果删除 Inception 块中的一些路径,它仍然保持多路径特性,但其复杂度和多样性会降低,接近于单一卷积路径块,但不会变成残差块。
2. 参考 ResNet 论文 (He et al. , 2016) 中的表 1,以实现不同的变体。
ResNet-34
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, 3, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 4))
b4 = nn.Sequential(*resnet_block(128, 256, 6))
b5 = nn.Sequential(*resnet_block(256, 512, 3))
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(512, 10))
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])
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
# 2m 34.1s
# loss 0.043, train acc 0.985, test acc 0.822
# 4397.7 examples/sec on cuda:0
3. 对于更深层次的网络,ResNet 引入了 “bottleneck” 架构来降低模型复杂性。请试着去实现它。
ResNet-50
class Residual(nn.Module): #@save
def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(num_channels, num_channels * 4, kernel_size=1)
if use_1x1conv:
self.conva = nn.Conv2d(input_channels, num_channels * 4, kernel_size=1, stride=strides)
else:
self.conva = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
self.bn3 = nn.BatchNorm2d(num_channels * 4)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = F.relu(self.bn2(self.conv2(Y)))
Y = self.bn3(self.conv3(Y))
if self.conva:
X = self.conva(X)
Y += X
return F.relu(Y)
def resnet_block(input_channels, num_channels, num_residuals, stride=1):
blk = []
blk.append(Residual(input_channels, num_channels, use_1x1conv=True, strides=stride))
for _ in range(1, num_residuals):
blk.append(Residual(num_channels * 4, 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, 3))
b3 = nn.Sequential(*resnet_block(256, 128, 4, stride=2))
b4 = nn.Sequential(*resnet_block(512, 256, 6, stride=2))
b5 = nn.Sequential(*resnet_block(1024, 512, 3, stride=2))
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(2048, 10))
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, 256, 56, 56])
Sequential output shape: torch.Size([1, 512, 28, 28])
Sequential output shape: torch.Size([1, 1024, 14, 14])
Sequential output shape: torch.Size([1, 2048, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 2048, 1, 1])
Flatten output shape: torch.Size([1, 2048])
Linear output shape: torch.Size([1, 10])
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
执行时间:4m 32.5s
loss 0.122, train acc 0.955, test acc 0.866
2425.8 examples/sec on cuda:0
4. 在 ResNet 的后续版本中,作者将 “卷积层、批量规范化层和激活层” 架构更改为 “批量规范化层、激活层和卷积层” 架构。请尝试做这个改进。详见 (He et al. , 2016) 中的图 1。
# conv -> bn -> relu => bn -> relu -> conv
5. 为什么即使函数类是嵌套的,我们仍然要限制增加函数的复杂性呢?
即使函数类是嵌套的,我们仍然要限制增加函数的复杂性,主要出于以下几个原因:
1. 防止过拟合
函数的复杂性与模型的容量密切相关。高复杂性的模型虽然在训练数据上可能表现得很好,但也容易过拟合,即模型记住了训练数据的细节和噪声,导致其在新数据上的泛化能力变差。限制函数的复杂性可以帮助模型更好地捕捉数据的整体模式,而不是记住细节和噪声。
2. 提高泛化能力
简单的模型往往具有更好的泛化能力,即能够更准确地预测未见数据的表现。通过限制复杂性,我们希望找到一个平衡,使模型既足够复杂以捕捉数据中的重要模式,又不过于复杂以至于无法泛化到新的数据。
3. 计算资源和效率
复杂的模型需要更多的计算资源进行训练和推理,包括更多的内存和处理时间。限制复杂性可以降低计算成本,提高模型的效率和可扩展性。
4. 可解释性
复杂的模型往往难以解释,这在某些应用中可能是一个问题。限制模型复杂性可以使模型更容易解释和理解,从而增强人们对模型预测的信任,尤其是在需要明确因果关系或决策过程的领域。
5. 避免过度复杂的搜索空间
在训练过程中,过于复杂的模型会导致一个庞大的搜索空间,使得优化过程变得更加困难和缓慢。简化模型可以使优化过程更加高效和稳定,提升训练速度和效果。
6. 理论和实践的结合
根据奥卡姆剃刀原则,简单的解释往往比复杂的解释更有可能是正确的。在机器学习中,这一原则同样适用。理论上,简单的模型更符合统计学习理论中的泛化界限和理论分析,从而在实践中也更可靠。
示例:神经网络中的正则化技术
在神经网络中,为了限制模型复杂性,我们可以使用以下正则化技术:
- L1/L2正则化:通过增加权重的惩罚项,限制权重的大小,从而控制模型的复杂性。
- Dropout:在训练过程中随机丢弃一部分神经元,防止神经网络过度依赖某些路径,从而增强模型的泛化能力。
- 早停法:在训练过程中监控验证集的性能,一旦验证集的误差不再降低或开始上升,就停止训练,从而防止过拟合。
结论
即使函数类是嵌套的,我们仍然要限制增加函数的复杂性,这是为了在模型性能和泛化能力之间找到一个平衡,同时提高模型的计算效率和可解释性。这一原则在实践中被广泛应用于各类机器学习和统计模型的设计和训练中。