☰
ResNet深度解析:从退化问题到残差块实现,掌握深度学习核心架构
2026/10/19 0:44:24 网站建设 项目流程

1. 从“堆层数”到“残差学习”:ResNet到底在解决什么问题

如果你在2015年之前尝试过训练一个超过20层的卷积神经网络,大概率会碰到一个非常反直觉的现象:训练误差不降反升。这不是过拟合——过拟合的表现是训练误差持续下降而验证误差上升,但当时的情况是训练集上的误差本身就比浅层网络更高。换句话说,网络更深了,连“死记硬背”训练数据的能力都变差了。这个问题在当时困扰了很多人,因为按照直觉,更深的网络表达能力应该更强才对。

ResNet(深度残差网络)就是冲着这个问题去的。它的核心贡献不是“发明了更深的网络”,而是提出了一种让深层网络变得可训练的结构设计——残差连接。这个设计后来几乎成了深度学习领域的标配,不管你是做图像分类、目标检测、语义分割,还是做自然语言处理、语音识别,残差连接无处不在。

这篇文章适合谁看?如果你正在学深度学习,已经了解卷积神经网络的基本结构(卷积层、池化层、全连接层),但对ResNet的理解还停留在“它很深、它很强”这个层面,那这篇内容就是为你准备的。我会从“为什么深层网络难训练”这个根问题讲起,把残差块的设计逻辑、两种残差块的区别、网络结构的演进、以及实际训练中的关键细节都拆开讲清楚。读完之后,你不仅能理解ResNet的设计精髓,还能自己动手搭一个残差网络,并且知道训练时该注意什么。

关键词方面,这篇文章会围绕深度残差网络、ResNet、残差块、跳跃连接、退化问题、Batch Normalization、瓶颈结构、迁移学习这些核心概念展开,每个概念都会结合实际操作和代码来讲解,不做纯理论堆砌。

2. 退化问题的本质:为什么加深网络反而更难训练

2.1 退化问题不是过拟合,也不是梯度消失那么简单

很多人第一次接触ResNet时,会把它解决的问题简单归结为“梯度消失”。这个理解不算错,但不够准确。梯度消失确实是深层网络的经典难题——反向传播时梯度逐层相乘,如果每层的梯度都小于1,经过几十层之后梯度就趋近于零,浅层的参数几乎不更新。但ResNet论文中揭示的“退化问题”比梯度消失更微妙。

退化问题的核心现象是:一个56层的网络,在训练集上的表现竟然不如一个20层的网络。注意,这里说的不是验证集,是训练集。这意味着深层网络连拟合训练数据的能力都退化了。如果仅仅是梯度消失导致的参数不更新,那至少浅层网络能学到的特征深层网络也应该能学到——毕竟深层网络可以看作是在浅层网络后面叠加了更多层,最差的情况是后面那些层学成恒等映射(输入等于输出),这样深层网络的性能至少不应该比浅层差。

但实际训练中,网络学不会恒等映射。这就是退化问题的本质:深层网络难以学习恒等映射。随机初始化的额外层会把原本学好的特征破坏掉,而让这些层学到“什么都不做”这件事,对于非线性层堆叠的结构来说并不容易。

2.2 恒等映射为什么这么难学

你可以这样理解:假设有一个20层的网络已经训练得不错了,现在你想把它加深到56层。理想情况下,新增的36层应该学会恒等映射——输入什么就输出什么,这样56层网络的性能至少和20层一样。但问题是,这36层里每一层都包含卷积、批归一化、激活函数,这些操作叠加起来,要让最终输出严格等于输入,需要权重和偏置精确配合。随机梯度下降在这么大的参数空间里搜索,很难恰好找到这组“恒等映射”的解。

打个比方:你让一个人闭着眼睛在一张纸上画一条直线,要求他画完后笔尖回到起点。如果他只画一小段(浅层网络),很容易控制;但如果让他画很长一段再回来(深层网络),偏差就会累积,很难精确回到原点。恒等映射对于多层非线性变换的堆叠来说,就是那个“精确回到原点”的要求。

ResNet的解决方案非常巧妙:既然让网络学会“恒等映射”很难,那我直接把输入加到输出上,让网络只需要学习“残差”——也就是输入和期望输出之间的差值。如果最优的输出就是输入本身,那网络只需要把残差学成零就行了,这比学一个恒等映射简单得多。

2.3 残差连接的数学表达与直觉理解

残差块的基本形式可以写成:

y = F(x, {Wi}) + x

其中x是输入,F(x, {Wi})是残差函数(也就是需要学习的部分),y是输出。当F(x) = 0时,y = x,这就是恒等映射。网络只需要把F(x)推向零,而不是从头学习一个恒等映射。

这个设计的精妙之处在于:残差连接提供了一条“高速公路”,让梯度可以直接从深层回传到浅层。反向传播时,梯度经过残差连接时相当于乘以1,不会像经过多层非线性变换那样逐层衰减。这就同时缓解了梯度消失和退化问题。

从另一个角度看,残差连接让网络变成了一个“集成模型”——你可以把ResNet看作是多条不同深度路径的集成,浅层路径负责捕捉低级特征,深层路径负责高级语义,残差连接让信息可以在不同深度之间自由流动。

3. 残差块的两副面孔:基础块与瓶颈块的设计取舍

3.1 基础残差块:两层3x3卷积的经典组合

ResNet-18和ResNet-34使用的是基础残差块(Basic Block),结构很简单:两个3x3卷积层,每个卷积后面接批归一化和ReLU激活,最后把输入加到输出上再过一次ReLU。

具体流程是这样的:

  1. 输入x经过第一个3x3卷积,输出通道数不变
  2. 经过Batch Normalization和ReLU
  3. 再经过第二个3x3卷积,输出通道数不变
  4. 经过Batch Normalization
  5. 将原始输入x与步骤4的输出相加
  6. 最后经过ReLU激活

这里有一个关键细节:第二个卷积之后、相加之前不能加ReLU。因为如果加了ReLU,输出就变成非负的了,而残差连接要求输出可以取任意值(包括负值),否则相加之后的信息会被截断。这个细节在实际写代码时很容易搞错,我见过不少人在第二个卷积后面顺手加了ReLU,结果训练效果大打折扣。

基础块适用于较浅的ResNet(18层、34层),因为它的参数量和计算量相对较小,在浅层网络中已经足够表达。

3.2 瓶颈块:用1x1卷积压缩再扩张

当网络加深到50层、101层、152层时,如果还用基础块,参数量和计算量会急剧膨胀。这时候就需要瓶颈块(Bottleneck Block)出场了。

瓶颈块的设计思路是:先用1x1卷积把通道数压缩,再用3x3卷积在低维空间做特征提取,最后再用1x1卷积把通道数扩张回来。典型的压缩比例是4倍——比如输入256通道,先压到64通道,3x3卷积在64通道上操作,再扩回256通道。

为什么这样设计?因为3x3卷积的计算量是跟通道数的平方成正比的。如果直接在256通道上做3x3卷积,计算量是256×256×9;而先压到64通道再扩回来,3x3卷积的计算量变成64×64×9,加上两个1x1卷积的计算量,总体算下来比直接做3x3卷积少了将近一半。这就是瓶颈结构的价值——在保持表达能力的同时大幅降低计算成本。

瓶颈块还有一个细节:当输入输出通道数不一致或者特征图尺寸需要下采样时,残差连接上需要加一个1x1卷积来调整维度和尺寸,这个叫做“投影捷径”(projection shortcut)。如果输入输出维度一致,就直接用恒等映射。

3.3 两种块的对比与选型建议

对比维度基础残差块瓶颈残差块
适用网络深度ResNet-18/34ResNet-50/101/152
核心结构两个3x3卷积1x1降维 + 3x3卷积 + 1x1升维
参数量较大(相同通道数下)较小(通过压缩通道)
计算量较高较低
残差连接处理维度一致时直接相加维度不一致时用1x1卷积投影
实际使用场景小数据集、轻量级任务大数据集、深层网络

选型建议很直接:如果你的任务比较简单、数据集不大,用ResNet-18或34就够了,基础块的结构更简单,训练也更容易收敛。如果你需要更强的表达能力、有足够的计算资源,那就上ResNet-50或更深,瓶颈块能帮你省下大量显存和计算时间。

4. 从18层到152层:ResNet家族的结构演进与设计逻辑

4.1 整体架构:阶段划分与下采样策略

ResNet的整体架构可以分成几个阶段:首先是7x7的大卷积核加最大池化做初步特征提取和下采样,然后进入四个阶段(stage),每个阶段由若干个残差块堆叠而成,阶段之间通过步长为2的卷积做下采样,特征图尺寸减半、通道数翻倍。

以ResNet-50为例,四个阶段的残差块数量分别是3、4、6、3,通道数分别是256、512、1024、2048。这个“3-4-6-3”的配置不是随便定的,它遵循了一个经验规律:随着特征图尺寸减小,通道数增加,每个阶段的残差块数量也相应增加。这是因为浅层阶段处理的是高分辨率、低语义的特征,计算量大但每个特征的信息量少;深层阶段处理的是低分辨率、高语义的特征,需要更多的通道来编码丰富的语义信息。

4.2 下采样时残差连接怎么处理

当特征图尺寸减半、通道数翻倍时,残差连接上的恒等映射就对不上了——输入是56x56x64,输出是28x28x128,直接相加会报错。ResNet的处理方式是在捷径路径上加一个1x1卷积,步长为2,输出通道数设为与主路径一致。这样既完成了下采样,又对齐了通道数。

这里有一个实操中容易忽略的点:这个1x1投影卷积后面通常不加Batch Normalization。虽然有些实现会加,但原始论文和主流实现都不加。原因是这个投影卷积的作用只是维度对齐,加BN反而会增加不必要的复杂度和计算量。

4.3 各版本ResNet的参数量与计算量对比

网络版本残差块类型残差块配置参数量(约)FLOPs(约)Top-1准确率(ImageNet)
ResNet-18基础块[2,2,2,2]11.7M1.8G69.8%
ResNet-34基础块[3,4,6,3]21.8M3.6G73.3%
ResNet-50瓶颈块[3,4,6,3]25.6M3.8G76.1%
ResNet-101瓶颈块[3,4,23,3]44.5M7.6G77.4%
ResNet-152瓶颈块[3,8,36,3]60.2M11.3G78.3%

从表中可以看出,ResNet-50的参数量只比ResNet-34多了不到4M,但准确率提升了近3个百分点,这就是瓶颈结构的威力。而ResNet-101和152虽然参数量和计算量大幅增加,但准确率提升的边际效益在递减。实际选型时,ResNet-50通常是性价比最高的选择。

5. 动手搭建一个ResNet:从零实现的完整路径

5.1 环境准备与依赖确认

在开始写代码之前,先确认你的环境。我推荐用PyTorch,因为它的动态图机制对调试残差连接这种结构非常友好。需要的依赖很简单:

pip install torch torchvision

如果你有GPU,确认CUDA可用:

import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

CPU也能跑,但训练ResNet-50这种规模的网络,CPU会慢到让你怀疑人生。建议至少有一块显存8G以上的GPU。

5.2 基础残差块的代码实现与关键注释

先实现基础残差块:

import torch import torch.nn as nn class BasicBlock(nn.Module): expansion = 1 # 输出通道数相对于中间通道数的倍数 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super(BasicBlock, self).__init__() # 第一个3x3卷积 self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) # 第二个3x3卷积 self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) # 下采样捷径(当维度不匹配时使用) self.downsample = downsample def forward(self, x): identity = x # 保存输入用于残差连接 out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) # 注意:这里不加ReLU,直接与identity相加 if self.downsample is not None: identity = self.downsample(x) out += identity # 残差连接 out = self.relu(out) # 相加后再激活 return out

这段代码里有几个关键点需要强调。第一,bias=False——因为后面接了Batch Normalization,卷积层的偏置会被BN的均值减法抵消掉,加上去纯属浪费参数。第二,第二个卷积后面不加ReLU,原因前面已经解释过了。第三,downsample是一个独立的模块,通常是一个1x1卷积加BN,用来在维度不匹配时调整identity的维度。

5.3 瓶颈块的实现与通道数变化追踪

瓶颈块的实现稍微复杂一点,因为涉及通道数的压缩和扩张:

class Bottleneck(nn.Module): expansion = 4 # 输出通道数是中间通道数的4倍 def __init__(self, in_channels, mid_channels, stride=1, downsample=None): super(Bottleneck, self).__init__() # 1x1卷积降维 self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, bias=False) self.bn1 = nn.BatchNorm2d(mid_channels) # 3x3卷积提取特征 self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(mid_channels) # 1x1卷积升维 self.conv3 = nn.Conv2d(mid_channels, mid_channels * self.expansion, kernel_size=1, bias=False) self.bn3 = nn.BatchNorm2d(mid_channels * self.expansion) self.relu = nn.ReLU(inplace=True) self.downsample = downsample def forward(self, x): identity = 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: identity = self.downsample(x) out += identity out = self.relu(out) return out

注意expansion=4这个类属性,它表示瓶颈块的输出通道数是中间通道数的4倍。在构建网络时,这个属性用来计算每个阶段的输出通道数。比如ResNet-50的第一个阶段,中间通道数是64,输出通道数就是64×4=256。

5.4 组装完整的ResNet网络

有了残差块之后,组装网络就相对直接了:

class ResNet(nn.Module): def __init__(self, block, layers, num_classes=1000): super(ResNet, self).__init__() self.in_channels = 64 # 初始卷积层 self.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], stride=1) 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, num_blocks, stride): downsample = None # 当stride不为1或通道数不匹配时,需要下采样捷径 if stride != 1 or self.in_channels != out_channels * block.expansion: downsample = nn.Sequential( nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels * block.expansion), ) layers = [] # 第一个块可能需要下采样 if block == Bottleneck: layers.append(block(self.in_channels, out_channels, stride, downsample)) else: layers.append(block(self.in_channels, out_channels, stride, downsample)) self.in_channels = out_channels * block.expansion # 后续块不需要下采样 for _ in range(1, num_blocks): if block == Bottleneck: layers.append(block(self.in_channels, out_channels)) else: 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 x

组装网络时最容易出错的地方是_make_layer里的通道数追踪。self.in_channels需要在每个阶段结束后更新为当前阶段的输出通道数,这样下一个阶段才能正确判断是否需要下采样捷径。我建议在写完之后打印一下每一层的输出形状,确认维度变化符合预期。

6. 训练ResNet时那些文档不会告诉你的实操细节

6.1 学习率预热与衰减策略

ResNet论文中使用的学习率策略是:初始学习率0.1,每30个epoch除以10。但这是针对ImageNet这种大规模数据集的。如果你在自己的数据集上训练,尤其是数据集比较小的时候,直接上0.1的学习率很可能导致训练初期loss爆炸。

我的经验是:先用一个较小的学习率(比如0.01)跑几个epoch,观察loss下降是否平稳,再决定是否调大。另外,学习率预热(warmup)在训练ResNet时非常有用——前5个epoch把学习率从0线性增加到初始值,可以让网络在初期稳定地找到下降方向,避免一开始就震荡。

# 学习率预热示例 def adjust_learning_rate(optimizer, epoch, base_lr=0.1, warmup_epochs=5): if epoch < warmup_epochs: lr = base_lr * (epoch + 1) / warmup_epochs else: lr = base_lr * (0.1 ** ((epoch - warmup_epochs) // 30)) for param_group in optimizer.param_groups: param_group['lr'] = lr

6.2 Batch Normalization与权重初始化的配合

ResNet大量使用了Batch Normalization,这带来一个好处:对权重初始化的敏感度降低了。但也不是随便初始化就行。PyTorch默认的Kaiming初始化对于ResNet来说已经够用,但如果你自己实现卷积层,记得用Kaiming正态初始化,fan_out模式,这样可以让每层输出的方差保持一致。

另外一个细节:BN层的weight初始化为1,bias初始化为0。这是PyTorch的默认行为,但如果你手动改了初始化,记得把BN层恢复成这个配置。BN的weight如果初始化太小,相当于把特征压缩了,会影响残差连接的信息流动。

6.3 数据增强与正则化的实际效果

在ImageNet上训练ResNet时,标准的数据增强是随机裁剪(从256x256裁剪到224x224)加随机水平翻转。如果你在自己的数据集上训练,数据量不够大的话,建议加上颜色抖动(Color Jitter)和随机旋转。但要注意,旋转角度不要太大,尤其是对于方向敏感的任务(比如数字识别),过度旋转会引入噪声。

权重衰减方面,ResNet论文用的是1e-4。这个值在大多数场景下都适用。如果你发现训练集准确率很高但验证集准确率上不去,可以适当加大权重衰减到5e-4或1e-3。Dropout在ResNet中通常不用,因为BN已经起到了正则化的作用,再加Dropout反而可能拖慢收敛。

6.4 迁移学习:加载预训练权重的正确姿势

实际项目中,很少有人从零训练ResNet,大多数情况是加载预训练权重做微调。这里有几个坑需要注意:

第一,替换全连接层时要注意输入维度。ResNet-50的全连接层输入是2048(512×4),ResNet-18是512。如果你把num_classes改了,只替换最后一层就行,前面的层不用动。

第二,冻结策略要分阶段。如果目标数据集和预训练数据集差异很大(比如从自然图像迁移到医学图像),建议先冻结前面的层,只训练后面的层和全连接层,跑几个epoch之后再解冻全部层做微调。如果差异不大,可以直接全部解冻。

第三,学习率要调小。微调时的学习率通常设为从头训练的十分之一甚至更小,比如1e-3或1e-4。因为预训练权重已经在一个很好的位置了,学习率太大会把学到的特征破坏掉。

# 加载预训练权重并替换分类头 model = torchvision.models.resnet50(pretrained=True) num_features = model.fc.in_features model.fc = nn.Linear(num_features, num_classes) # 替换为你的类别数 # 冻结前面的层 for name, param in model.named_parameters(): if 'fc' not in name: param.requires_grad = False # 只优化全连接层 optimizer = torch.optim.SGD(model.fc.parameters(), lr=1e-3, momentum=0.9)

7. 残差连接之后:它如何影响了后续的网络设计

7.1 残差思想在目标检测与分割中的延伸

ResNet本身是一个分类网络,但它的残差结构被广泛移植到了其他任务中。目标检测领域的Faster R-CNN、YOLO系列,语义分割领域的FCN、U-Net,都大量使用了ResNet作为骨干网络。原因很简单:残差连接让深层骨干网络变得可训练,而深层网络提取的特征语义更强,对小目标和复杂场景的识别能力更好。

在U-Net中,跳跃连接(skip connection)和ResNet的残差连接有异曲同工之妙,但目的不完全一样。U-Net的跳跃连接是把编码器的特征图直接拼接到解码器上,目的是保留空间细节;ResNet的残差连接是把输入加到输出上,目的是让梯度更好地流动。两者可以结合使用,比如在U-Net的每个卷积块里加入残差连接。

7.2 DenseNet与ResNeXt:残差连接的两个进化方向

DenseNet把残差连接推到了极致:每一层的输入都来自前面所有层的输出拼接。这样做的好处是特征复用更充分,参数量更少;缺点是显存占用大,因为每一层的输出都要保存下来用于后续层的拼接。

ResNeXt则是在残差块内部做文章:把3x3卷积拆分成多个并行的组卷积,每个组独立计算,最后拼接。这种“分组卷积+残差连接”的设计在参数量相近的情况下,准确率比ResNet更高。它的核心思想是增加“基数”(cardinality)——也就是并行路径的数量,而不是单纯增加深度或宽度。

7.3 实际项目中的选型建议

如果你现在要做一个图像分类项目,我的建议是:

  • 数据量小于1万张:用ResNet-18或34,配合强数据增强和迁移学习。不要上太深的网络,容易过拟合。
  • 数据量在1万到10万张:ResNet-50是首选,瓶颈结构在参数量和准确率之间取得了很好的平衡。
  • 数据量大于10万张:可以考虑ResNet-101或152,但要注意计算资源。如果资源有限,ResNeXt-101(32x4d)是比ResNet-152更好的选择。
  • 移动端或嵌入式部署:考虑ResNet-18配合通道剪枝或量化,或者直接用MobileNet系列。

还有一个经验:不要盲目追求准确率的小数点后几位。在实际业务中,推理速度、模型大小、部署难度往往比那0.5%的准确率提升更重要。ResNet-50在大多数场景下已经足够好,把精力花在数据质量和后处理上,收益往往比换更深的网络更大。

8. 我踩过的那些坑:ResNet实操中的常见问题与排查思路

8.1 训练loss不下降或震荡剧烈

这是最常见的问题。排查顺序应该是:先检查数据预处理有没有问题(比如归一化的均值和方差是否匹配),再检查学习率是不是太大,然后检查Batch Normalization的momentum参数。BN的默认momentum是0.1,如果batch size很小(比如小于16),BN的统计量会很不稳定,这时候可以考虑用Group Normalization替代,或者增大batch size。

还有一个容易被忽略的点:残差连接相加之后的值域。如果残差函数的输出方差很大,加上identity之后会导致数值不稳定。这时候可以在残差函数的最后一个BN层之前加一个缩放系数(比如0.1),但这属于比较进阶的技巧,一般用标准的BN就能解决。

8.2 验证集准确率远低于训练集

如果训练集准确率95%而验证集只有70%,大概率是过拟合。解决方案按优先级排序:加数据增强、加权重衰减、加Dropout(虽然ResNet不常用,但可以试试)、减小模型规模。如果训练集准确率本身就不高(比如80%),那可能是欠拟合,需要增大模型或延长训练时间。

还有一个特殊情况:验证集的预处理和训练集不一致。比如训练时用了随机裁剪,验证时用了中心裁剪,这本身没问题,但如果归一化参数不一致,就会导致验证集准确率异常低。检查一下transforms.Normalize的均值和方差是否在训练和验证时保持一致。

8.3 显存不够用的优化策略

ResNet-50在batch size为32、输入224x224的情况下,大约需要8G显存。如果显存不够,可以尝试以下方法:

  • 减小batch size(但不要小于8,否则BN不稳定)
  • 使用混合精度训练(AMP),可以节省约40%显存
  • 使用梯度累积,用小的batch size模拟大的batch size
  • 使用梯度检查点(gradient checkpointing),用计算时间换显存

混合精度训练现在在PyTorch里已经很简单了:

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for inputs, labels in dataloader: optimizer.zero_grad() with autocast(): outputs = model(inputs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

8.4 模型推理速度慢的排查方向

如果推理速度不达预期,先确认是不是用了GPU。然后检查输入尺寸——224x224和320x320的推理时间差很多。再检查是不是用了太大的batch size导致显存带宽瓶颈。最后,如果部署在边缘设备上,考虑用TensorRT或ONNX Runtime做推理优化,通常能提速2到3倍。

还有一个容易忽略的点:PyTorch的默认卷积算法在推理时不一定是最优的。可以设置torch.backends.cudnn.benchmark = True,让cuDNN自动搜索最快的卷积算法。但这个设置只适合输入尺寸固定的场景,如果输入尺寸变化频繁,反而会拖慢速度。

9. 关于ResNet,我个人的几点体会

ResNet从2015年提出到现在,已经快十年了,但它依然是深度学习领域最经典、最常用的网络架构之一。我自己的体会是,ResNet的价值不仅在于它解决了深层网络的训练问题,更在于它提供了一种通用的设计思路:当直接学习目标映射很难时,转而学习残差。这个思路后来被用在了很多地方,比如Transformer里的残差连接、扩散模型里的残差预测,本质上都是同一个思想。

另外,ResNet的代码实现虽然不复杂,但细节很多。我建议每个学深度学习的人都自己从零实现一遍ResNet,不要直接调torchvision.models.resnet50()。自己写一遍,你才会真正理解downsample的作用、expansion的含义、以及为什么第二个卷积后面不能加ReLU。这些细节在调参和排错时非常有用。

最后分享一个小技巧:如果你在训练ResNet时发现某个阶段的loss突然跳变,可以检查一下那个阶段的残差块里,下采样捷径的步长和主路径的步长是否一致。我遇到过好几次因为步长不匹配导致特征图尺寸对不上,最后相加时报错的情况。写代码时多打印形状,能省下大量调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询