
1. 为什么我们需要残差网络我第一次训练深度神经网络时遇到了一个奇怪的现象明明增加了网络层数模型的准确率却不升反降。这就像你给汽车加了更多引擎结果速度反而变慢了。后来才知道这就是著名的网络退化问题。传统神经网络随着深度增加会遇到三个主要障碍梯度消失、梯度爆炸和网络退化。梯度消失就像水流经过太长的管道到末端几乎没水了梯度爆炸则像滚雪球参数更新变得失控。而网络退化更诡异——深层网络在训练集上的表现都不如浅层网络这完全违背了越深越强的直觉。2015年何恺明团队提出的残差网络(ResNet)完美解决了这些问题。最让我印象深刻的是他们用如此简单的结构就实现了突破在ImageNet比赛中152层的ResNet错误率比之前最好的模型降低了近一半。这就像在百米赛跑中突然把世界纪录提高了3秒。2. 残差块化腐朽为神奇的设计2.1 残差连接的秘密残差网络的核心创新在于短路连接(shortcut connection)。想象你在学习弹钢琴与其直接从零开始学一首高难度曲子不如先学会简单版本再逐步修正差异。这就是残差学习的思想——让网络学习输出与输入之间的差值(残差)而非直接学习完整映射。数学表达很简单输出 F(x) x。其中F(x)是要学习的残差函数。这个加法操作带来了三个关键优势梯度可以直接回传到浅层缓解梯度消失网络可以轻松实现恒等映射当F(x)0时深层网络至少不会比浅层网络表现差我在实现第一个残差块时差点犯了个典型错误忘记处理维度不匹配的情况。当残差路径需要下采样时必须用1x1卷积调整x的维度否则加法会报错。这个细节在论文里其实讲得很清楚但新手很容易忽略。2.2 瓶颈结构更聪明的参数利用随着网络加深计算量会爆炸式增长。ResNet采用的瓶颈设计非常巧妙先用1x1卷积压缩通道数再用3x3卷积处理特征最后用1x1卷积恢复通道数。这就像先压缩文件再处理能大幅减少计算量。实测表明带瓶颈结构的ResNet-152比普通结构的参数量少了40%但准确率更高。我在Kaggle比赛中最喜欢用这种结构在有限GPU内存下能训练更深的模型。具体实现时要注意第一个1x1卷积通常用stride2来下采样同时shortcut路径也需要同步下采样。3. 手把手实现ResNet3.1 PyTorch实现详解让我们用PyTorch实现一个完整的ResNet-18。先定义基础残差块class BasicBlock(nn.Module): expansion 1 def __init__(self, in_planes, planes, stride1): super(BasicBlock, self).__init__() self.conv1 nn.Conv2d(in_planes, planes, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(planes) self.conv2 nn.Conv2d(planes, planes, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(planes) # 处理shortcut路径 self.shortcut nn.Sequential() if stride ! 1 or in_planes ! self.expansion*planes: self.shortcut nn.Sequential( nn.Conv2d(in_planes, self.expansion*planes, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(self.expansion*planes) ) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) out F.relu(out) return out这里有几个关键点expansion参数控制输出通道的扩展倍数当需要改变特征图尺寸时(stride1)shortcut路径需要同步调整所有卷积后都紧跟批归一化(BatchNorm)这是稳定训练的关键3.2 构建完整网络现在我们可以组装完整的ResNetclass ResNet(nn.Module): def __init__(self, block, num_blocks, num_classes10): super(ResNet, self).__init__() self.in_planes 64 self.conv1 nn.Conv2d(3, 64, kernel_size3, stride1, padding1, biasFalse) self.bn1 nn.BatchNorm2d(64) self.layer1 self._make_layer(block, 64, num_blocks[0], stride1) self.layer2 self._make_layer(block, 128, num_blocks[1], stride2) self.layer3 self._make_layer(block, 256, num_blocks[2], stride2) self.layer4 self._make_layer(block, 512, num_blocks[3], stride2) self.linear nn.Linear(512*block.expansion, num_classes) def _make_layer(self, block, planes, num_blocks, stride): strides [stride] [1]*(num_blocks-1) layers [] for stride in strides: layers.append(block(self.in_planes, planes, stride)) self.in_planes planes * block.expansion return nn.Sequential(*layers) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.layer1(out) out self.layer2(out) out self.layer3(out) out self.layer4(out) out F.avg_pool2d(out, 4) out out.view(out.size(0), -1) out self.linear(out) return out使用时只需指定块类型和每层块数def ResNet18(): return ResNet(BasicBlock, [2,2,2,2])4. 实战中的技巧与陷阱4.1 初始化与学习率设置残差网络对初始化非常敏感。我的经验是所有卷积层使用He初始化BatchNorm层的γ初始化为1β初始化为0最后一层全连接层学习率设为其他层的10倍学习率调度也很关键。我通常用余弦退火optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max200)4.2 数据增强策略在图像任务中合理的数据增强能显著提升ResNet表现。除了常规的随机裁剪、水平翻转外我还推荐Cutout随机遮挡部分区域Mixup线性混合两张图像AutoAugment自动学习最优增强策略但要注意增强太强反而会损害性能。有次我在小数据集上用了过强的增强模型完全无法收敛。后来发现对于小于10万样本的数据集简单的随机裁剪翻转效果最好。4.3 常见问题排查当ResNet表现不佳时可以检查shortcut路径是否正确实现了恒等映射BatchNorm是否在训练模式model.train()梯度是否正常流动可以用hook检查学习率是否合适初始可以尝试0.1有次我的模型准确率卡在10%相当于随机猜最后发现是数据标签没有从1开始编码PyTorch需要从0开始。这种低级错误往往最难发现。