资讯动态

ResNet18做图像分割:编码器改造与训练实战

发布时间:2026/10/17 2:30:30 来源:尧图企业网站定制
ResNet18 做图像分割第一反应可能都是“这不是分类网络吗”。没错ResNet18 原生的设计确实是为图像分类服务的最后接的是全局池化和全连接层。但在实际项目里尤其是分割任务中ResNet18 的价值恰恰不在那个分类头而在它前面的卷积骨干。把 ResNet18 当作编码器来提取特征再接上解码器恢复空间分辨率这是中小型分割项目里非常实用的一套组合拳。本篇文章就围绕“ResNet18 应用于图像分割”这条主线展开讲清楚网络结构怎么改、跳跃连接怎么接、损失函数怎么配、训练时有哪些坑照着做就能跑通一个基础的分割流程。适合刚入门分割方向、或者想把分类模型改造成分割模型来试水的同学参考。1. 为什么选 ResNet18 作为分割骨干1.1 分割任务对骨干网络的核心诉求图像分割本质上要做两件事一是“看懂”图像里有什么二是“说清楚”每个像素属于哪个类别。前者依赖语义特征后者依赖空间细节。骨干网络负责前者解码器负责后者。在选择骨干时主要看三个指标特征表达能力强不强、输出特征图的尺寸变化是否可控、计算量是否在可接受范围内。ResNet18 在这三方面的表现都很均衡。18 层深度在 ImageNet 分类任务上不算深但它通过残差结构解决了深层网络难以优化的问题提取到的特征已经足够丰富。相比 VGG 系列ResNet18 的参数更少、计算量更低相比 ResNet50ResNet18 的推理速度更快对显存的要求也更友好。在医学影像分割、工业缺陷检测、遥感图像分析这类数据量不大、类别数不多、单张图像尺寸受限的场景里ResNet18 作为编码器是非常务实的方案。1.2 与常见替代方案的横向对比拿几个常用骨干放在一起对比能更清楚看到 ResNet18 的定位。我把实际使用中的体感整理成一张表方便参考。骨干网络参数量推理速度特征丰富度适用场景VGG16较大慢中传统分割方法、迁移学习基线ResNet18小快中高小样本、实时性要求高的分割任务ResNet34中中高数据量充足、精度优先的分割任务ResNet50较大较慢很高大模型、多类别、复杂语义分割MobileNetV3极小极快中移动端、嵌入式设备上的分割从表里能看出ResNet18 是一个典型的“均衡型选手”。我在实际项目中用 ResNet18 做编码器配合轻量级解码器能在 GTX 1080Ti 上把 batch size 开到 16 左右训练速度比 ResNet50 快接近一倍而精度差距通常只在 1 到 3 个百分点之间。对于很多业务场景来说这点精度差距换来的训练效率和显存余量非常划算。1.3 什么情况下不建议用 ResNet18也得说清楚边界。如果任务是 Cityscapes 这种 19 类、高分辨率、大样本的场景分割或者需要分割出非常精细的边缘结构ResNet18 的容量可能会不够。这时候要么换 ResNet34 甚至更深要么在解码器侧加注意力模块。另外如果目标物体在图像中占比极小比如一张 512x512 的图像里目标只有几十个像素ResNet18 的深层特征图只有 16x16 分辨率细节信息已经丢得差不多了单靠编码器很难找回这种情况需要配合多尺度特征融合或者超分辨率预处理。2. ResNet18 的结构拆解分割场景下的关键细节2.1 残差块与下采样机制ResNet18 的核心单元是 BasicBlock由两个 3x3 卷积组成中间接 BN 和 ReLU最后通过恒等映射把输入加到输出上。这个“跳线”设计解决了一个关键问题网络加深时梯度消失导致训练不收敛。在分割任务中残差连接还有一个额外好处——它让编码器每一层的输出都保留了一部分原始位置的响应这对后续恢复空间位置信息是有帮助的。再来看下采样。ResNet18 在 conv2_x 到 conv5_x 四个 stage 的入口处通过 stride2 的卷积或池化把特征图尺寸减半。完整流程下来输入 224x224 的图像会依次输出 56x56、28x28、14x14、7x7 四个尺度的特征图。对于分割任务来说这个多尺度特征恰好是解码器需要的“原材料”。我在设计分割头的时候会把这四个尺度的特征都引出来而不是只取最后一层这一点在后面讲跳跃连接时会细说。2.2 各 stage 输出尺寸与通道数速查搞分割的人必须对自己骨干网络每一层的输出尺寸和通道数烂熟于心因为解码器的结构设计完全依赖这些数字。以输入 256x256 单通道灰度图为例ResNet18 前向传播后各 stage 的输出如下表所示。Stage模块组成输出尺寸输出通道数Stem7x7 卷积 BN ReLU 最大池化64x6464Layer1BasicBlock x 264x6464Layer2BasicBlock x 2首个 Block 下采样32x32128Layer3BasicBlock x 2首个 Block 下采样16x16256Layer4BasicBlock x 2首个 Block 下采样8x8512表中最关键的信息是当前 stage 在整条特征金字塔中的位置。解码器恢复分辨率时通常要先利用 Layer4 的 8x8 特征图生成初步预测再逐级与 Layer3、Layer2、Layer1 的特征图融合。这里有个容易出问题的地方torchvision 官方 ResNet18 实现里Stem 部分自带一个最大池化所以 Layer1 的输出尺寸是输入的 1/4。如果你用别的实现或者自己改了下采样策略尺寸会对不上后面解码器拼接时就容易报维度错误。2.3 去掉分类头骨干网络的“外科手术”把 ResNet18 用作编码器首先要做的改造是去掉最后的全局平均池化和全连接层只保留从输入到 Layer4 输出的部分。这个操作听着简单但有一个细节值得注意torchvision 的实现里ResNet18 前向传播只返回最后一层的特征图。为了拿到多尺度特征我通常会把 layer1 到 layer4 的输出都显式保存下来。具体改法也不复杂下面这段代码是常见的处理方式把一个标准的 ResNet18 改造成返回多尺度特征的编码器。import torch import torch.nn as nn from torchvision.models import resnet18 class ResNet18Encoder(nn.Module): def __init__(self, in_channels1): super().__init__() backbone resnet18(pretrainedTrue) # 替换 Stem 的输入通道适配单通道输入 first_conv nn.Conv2d(in_channels, 64, kernel_size7, stride2, padding3, biasFalse) # 简单起见这里直接复制预训练权重前 in_channels 个通道的均值 with torch.no_grad(): if in_channels 1: first_conv.weight[:] backbone.conv1.weight.mean(dim1, keepdimTrue) else: first_conv.weight[:] backbone.conv1.weight[:, :in_channels] self.stem nn.Sequential( first_conv, backbone.bn1, backbone.relu, backbone.maxpool ) self.layer1 backbone.layer1 self.layer2 backbone.layer2 self.layer3 backbone.layer3 self.layer4 backbone.layer4 def forward(self, x): x self.stem(x) c1 self.layer1(x) c2 self.layer2(c1) c3 self.layer3(c2) c4 self.layer4(c3) return [c1, c2, c3, c4]这段代码有两点值得说明。第一替换第一个卷积时直接把预训练权重的多通道均值赋给单通道比随机初始化收敛快很多。第二返回的 c1 到 c4 就是后续解码器做跳跃连接的输入。如果输入是三通道 RGB 图像in_channels 直接设为 3 即可保留原权重即可不需要额外处理。3. 解码器设计与跳跃连接实操3.1 最小可行解码器从 8x8 恢复到原尺寸编码器把图像从 256x256 压缩到 8x8解码器要做的就是把 8x8 逐步放大回 256x256。如果直接用一个转置卷积一步到位恢复出来的边缘会很粗糙。更稳妥的做法是分级上采样每级把分辨率翻倍同时通过跳跃连接引入对应尺度的编码器特征。轻量解码器的主流做法是用双线性插值上采样 3x3 卷积的组合而不是堆转置卷积。原因是转置卷积虽然可学习但容易产生棋盘伪影在分割任务的像素级输出上非常明显。双线性插值没有可学习参数但计算稳定配合卷积层学习语义映射效果更可控。下面是实际可跑的轻量解码器结构用了一个简单的 UNet 风格跳跃连接。class DecoderBlock(nn.Module): def __init__(self, in_ch, skip_ch, out_ch): super().__init__() self.up nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) self.conv nn.Sequential( nn.Conv2d(in_ch skip_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x, skip): x self.up(x) # 若尺寸不一致做中心裁剪或填充对齐 if x.size(2) ! skip.size(2): diff skip.size(2) - x.size(2) x nn.functional.pad(x, [0, 0, diff // 2, diff - diff // 2]) x torch.cat([x, skip], dim1) return self.conv(x)这个解码器块的核心操作是“先上采样、再拼接、后卷积”。为什么先拼接再卷积因为卷积能学习如何融合两个来源的特征。如果先独立处理再相加融合的灵活性就差一些。我把这种块串联起来从 c4 一路恢复到 c1最后再接一个 1x1 卷积输出类别数。整个模型参数规模不大一张 256x256 图像前向传播一次显存占用大约 1.5GB非常友好。3.2 跳跃连接有三条路拼接、相加、注意力跳跃连接怎么把编码器特征送给解码器有很多种做法各自适用场景不同。直接拼接是最常见的方式特征通道数翻倍信息无损解码器卷积层能够充分利用两侧的特征。相加的方式更省显存和计算量但要求两路特征语义相近否则会互相干扰。注意力融合是近几年效果最好的方式比如在拼接前对编码器特征做一个通道注意力或空间注意力加权强调有用的区域、抑制背景。实际项目里如果刚开始做建议先上拼接简单高效。如果精度遇到了瓶颈再考虑在特定层级引入注意力模块。我个人比较推荐的一个小技巧是只在 Layer1 和 Layer2 这两个高分辨率层级做跳跃连接Layer3 和 Layer4 直接顺序上采样。这样做的理由是深层特征已经高度语义化空间细节很少拼接进来反而可能引入噪声而且省去两次拼接操作能让训练速度提升 10% 左右。当然不同数据集情况不同这个取舍需要在验证集上对比确认。3.3 完整模型如何组装与输出把编码器和解码器拼起来就是一个能跑的 ResNet18 分割网络。输出层用 1x1 卷积把最后一个解码块的通道压缩成类别数然后直接输出未经过 Softmax 的 logits。训练时和交叉熵损失搭配推理时再对 logits 做 argmax 得到每个像素的类别标签。class ResNet18Seg(nn.Module): def __init__(self, in_channels1, num_classes2): super().__init__() self.encoder ResNet18Encoder(in_channels) self.dec4 DecoderBlock(512, 256, 128) self.dec3 DecoderBlock(128, 128, 64) self.dec2 DecoderBlock(64, 64, 32) self.dec1 DecoderBlock(32, 64, 32) self.out nn.Conv2d(32, num_classes, 1) def forward(self, x): c1, c2, c3, c4 self.encoder(x) x self.dec4(c4, c3) x self.dec3(x, c2) x self.dec2(x, c1) x self.dec1(x, None) # 最后一个 Block 不拼接也可以再传一次 c1 return self.out(x)注意 dec1 的写法实际使用时我会传 c1 进去再融合一次避免过早丢失浅层信息。整个网络的输出尺寸和输入尺寸保持一致这一点对分割任务来说是最基本的保证。模型定义完之后下一步就是损失函数和训练策略了。4. 损失函数、训练策略与关键参数配置4.1 损失函数怎么选交叉熵还是 Dice分割任务最常用的损失函数是像素级交叉熵它收敛稳定、梯度平滑适合大多数场景。但交叉熵在类别极度不平衡时会出问题。比如工业缺陷检测里正常像素占比可能达到 99%缺陷像素只占 1%交叉熵会让模型倾向于把一切都预测为背景因为这样也能把损失压得很低。解决不平衡问题有两条思路。一条是给交叉熵加类别权重权重可以设置为类别频率的倒数但权重怎么定需要反复实验。另一条是用 Dice Loss 或 Focal Loss。Dice Loss 直接优化预测和真实标注之间的重叠度对小目标更友好。Focal Loss 通过调制因子降低易分类样本的损失权重让模型把注意力放在难样本上。我的实践经验是两者组合效果最好。简单做法是loss 0.5 * CrossEntropyLoss 0.5 * DiceLoss。但要注意Dice Loss 在训练初期梯度不稳定建议先用纯交叉熵训练几个 epoch再切换为组合损失。4.2 学习率、Batch Size 和优化器设置用 ResNet18 做分割骨干时我通常会保留 ImageNet 预训练权重的初始化然后采用分层差异化学习率。具体来说编码器的学习率设为解码器的 0.1 倍因为预训练好的特征只需要微调而解码器是从头训练的需要更大的学习步长。用 PyTorch 实现时给编码器和解码器参数分组即可。param_groups [ {params: model.encoder.parameters(), lr: 1e-4}, {params: model.decoder.parameters(), lr: 1e-3}, ] optimizer torch.optim.AdamW(param_groups, weight_decay1e-4)学习率调度上CosineAnnealingLR 是我的首选。它在前半段保持较快的下降、后半段逐渐收敛比 StepLR 那种阶梯式下降更稳不容易在边界处反复震荡。初始学习率从 1e-3 起步最低降到 1e-5。Batch Size 方面BN 层要求 batch size 不能太小否则统计量不准。如果显存只允许 batch size 等于 2 或 4建议改用 GroupNorm 替代 BN或者在训练时累积梯度来模拟更大的 batch。4.3 数据增强组合拳分割任务的数据增强要特别注意“图像和标注必须同步变换”。随机翻转、随机旋转、随机缩放、随机裁剪这些操作都要用同一套参数作用在图像和 mask 上。我常用的增强组合是随机水平翻转、随机旋转 10 度、随机缩放 0.8 到 1.2、随机亮度对比度调整以及最后统一裁剪到 256x256。这个组合能在不破坏空间对应关系的前提下显著增加样本多样性。有一点值得提醒像 CutMix 或 MixUp 这类强增强在分割任务里要谨慎使用因为它会破坏边界处的语义对应关系导致模型对边缘的预测变得模糊。分割任务更倾向保留像素级对应的增强方案。4.4 推理时的后处理细节训练完模型推理时输出的是每个像素的类别概率通常直接取 argmax 得到分割结果。如果发现边缘锯齿明显可以加一个条件随机场或简单的形态学操作来平滑但这类操作会增加推理耗时在实时系统里要权衡。我的习惯是先在验证集上计算 mIoU 和 Dice 系数这两个指标能全面反映分割质量。mIoU 对类别不平衡有一定敏感性Dice 系数更偏向小目标的匹配度。两者同时看如果 mIoU 高但 Dice 低说明大类别分割得很好但小类别被忽略了这时候要回头检查损失函数里权重分配。5. 训练过程中常见问题与排查技巧5.1 损失值不下降或震荡明显如果训练了十几个 epoch损失值还停滞在初始水平不下降先别急着调模型结构。第一步检查数据预处理确认 mask 和图像是否对齐像素值范围是否正确。第二步检查学习率如果初始学习率设置过高损失可能在某个值附近震荡可以尝试降低一个数量级看看。第三步检查标签类别数看模型输出通道数是否和标注类别数一致。我之前遇到过一个很隐蔽的情况数据集里有一张 mask 的类别标号和其余所有样本不同导致交叉熵损失偶尔异常跳变。排查方法是在训练循环里打印每个 batch 的损失值如果发现每隔固定步数出现尖峰优先怀疑数据问题。5.2 类别不平衡导致预测全为背景如果用交叉熵训练模型输出全为背景几乎可以断定是类别不平衡引起的。解决路径有三条一是给损失函数加权重二是换成 Dice Loss 或 Focal Loss三是用 Hard Negative Mining 策略在训练中只选择难分类的负样本参与梯度回传。第三条实现起来最麻烦但效果最直接。我在医学影像分割中遇到过同样的问题最终是通过 Dice Loss 加类别权重、加上对小目标的形态学增强解决的。具体做法是将小目标区域的图像块复制粘贴到其他图像的背景区域同时更新对应的 mask这相当于对小目标做了过采样。5.3 预测结果存在明显棋盘伪影棋盘伪影通常和转置卷积有关。解决办法有一劳永逸的把转置卷积全部替换成双线性插值上采样加普通卷积。如果已经训练出了一个带转置卷积的模型可以用后处理方法做轻微的高斯模糊或中值滤波来缓解伪影但这只能治标。5.4 显存溢出怎么办显存溢出是分割任务绕不开的坎。有几个实用技巧减小 Batch Size、降低输入分辨率、使用混合精度训练、用梯度累积替代大 Batch、将模型切分到多张卡上。这些措施通常按顺序尝试先减 Batch再开混合精度基本能解决大多数情况。值得注意的一点是ResNet18 之所以在显存受限时表现好就是因为它参数少、计算量低。如果换了 ResNet50 做骨干导致显存溢出优先考虑把输入分辨率从 512 降到 384 或 256这比强行加各种注意力模块更有效。5.5 验证集指标高但可视化效果差这一类问题最迷惑人。指标不错但可视化发现边缘不清、小物体漏检。原因可能有三个一是验证集和训练集分布相似导致模型过拟合二是 mIoU 是按类别分别计算的如果小类别的 IoU 很低但占比小对整体 mIoU 影响有限三是后处理过度。排查方法是按类别拆分指标重点看每个类别的 IoU而不是只看平均结果。6. 从 ResNet18 出发的扩展思路6.1 把编码器换成 ResNet34 或 ResNet50如果数据量足够、精度还有提升空间最简单粗暴的升级路径就是换更深的骨干。ResNet34 相比 ResNet18 增加了两个 BasicBlock参数量多了一些但特征表达更强。ResNet50 用了 Bottleneck 结构虽然层数更多但计算效率并不差。换骨干的核心工作不是替换模型类而是重新对齐解码器的输入通道数和特征尺度。6.2 引入注意力机制与多尺度模块在 ResNet18 基础上加注意力常见做法是在解码器融合前加一个轻量的 SE Block 或 CBAM。这类模块不会显著增加参数量但能引导模型关注更重要的通道和空间区域。另一个思路是加入 ASPP 空洞空间金字塔池化在最后一层特征图上并行使用多个不同空洞率的卷积捕捉多尺度上下文信息。这一招对分割效果提升非常明显DeepLabv3 系列就是靠它出名的。6.3 实际项目中的工程化建议最后说点工程上的体会。ResNet18 做分割骨干非常契合“快速迭代、快速验证”的开发节奏。我在做一个表面缺陷检测项目时第一天用预训练 ResNet18 搭建基线第二天就已经能跑通完整训练流程并得到初步结果。后续通过数据增强、损失函数优化、后处理调参把 mIoU 从 0.61 提升到 0.74。这个项目案例说明用成熟骨干加上合理的分割头远比从头设计一个复杂网络来得高效。另外建议所有做分割的同学把实验记录做得细一点每个参数组合都记下来。分割任务的调参空间比分类任务大得多同样的模型、不同的损失权重或增强策略结果差异可能非常显著。有了记录后续复现和调优会轻松很多。如果你正打算做分割项目拿 ResNet18 打底是一个试错成本极低的好起点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑