ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

FCN-8S语义分割实战:ResNet双主干与辅助分支解析

FCN-8S语义分割实战:ResNet双主干与辅助分支解析 简介语义分割是计算机视觉的基础任务需要为图像中的每个像素预测语义类别。全卷积网络FCN通过将分类网络全连接层替换为卷积层并利用转置卷积实现端到端的像素级预测奠定现代分割模型的基础。ResNet作为主干网络凭借残差结构和预训练权重有效解决深层网络收敛慢的问题成为平衡精度与计算量的经典选择。辅助分支机制则通过中间层监督信号加速训练收敛并提升模型泛化能力。在工程实践中合理配置主干网络类型和辅助损失权重能够显著影响模型在复杂场景下的分割质量与推理效率。基于FCN-8S架构的系统讲解涵盖结构原理、参数配置与调优策略为语义分割任务提供了一个可复用的工程起点。 做语义分割的同行看到这个项目名应该会心一笑FCN-8S、ResNet50/101双主干、AuxiliaryBranch辅助分支这几个词凑在一起基本就是一个从入门到进阶的完整闭环。这个项目最打动我的地方不在于它用了多新的tricks而在于它把经典架构的工程细节做得很扎实——既保留了FCN作为语义分割开山之作的结构精髓又通过ResNet预训练权重解决了深层网络收敛慢的痛点还额外留了一个辅助分支的开关让训练过程更可控。无论你是刚接触分割任务的学生还是在工业场景里被精度和效率反复折磨的工程师这份配置都能给你提供一个相当顺手的起点。下面我结合自己的复现和调优经验把这个项目的设计逻辑、关键实现和踩坑记录完整拆开来讲。1. 项目整体设计与思路拆解1.1 为什么是FCN-8S经典结构的现代价值FCNFully Convolutional Network是语义分割领域真正意义上的开山之作它第一次证明了纯卷积结构可以在像素级别做密集预测。FCN的核心思路是把分类网络最后的全连接层替换成卷积层让网络接受任意尺寸输入再通过转置卷积把特征图逐步上采样回原图分辨率。FCN-32S、FCN-16S、FCN-8S这三个变体的区别在于上采样过程中融合了不同层级的特征32S直接从最后一层特征图上采样16S融合了pool4的输出8S则进一步融合了pool3的输出。为什么大家最终普遍选择FCN-8S因为单从最后一层特征图上采样丢失的空间细节太多分割边缘会非常粗糙。融合浅层的pool3和pool4之后网络能同时利用高层的语义信息和低层的纹理边缘信息在分割精度和细节保留之间取了一个最实用的平衡点。我自己实际用下来的体会是FCN-8S在建筑分割、道路分割这类边缘相对规整的任务上效果依然很能打而且结构直观、计算量可控非常适合做baseline或者作为理解语义分割原理的教学样例。相比于后续的U-Net、DeepLab系列FCN-8S的代码逻辑更直白你完全可以把每一行张量的shape变化在纸上画出来这对理解整个分割任务的本质特别有帮助。1.2 ResNet50与ResNet101双主干深度与效率的取舍项目提供双主干支持本质上是给使用者一个灵活的精度-速度旋钮。ResNet50由5个阶段组成每个阶段包含多个BasicBlock或Bottleneck模块整体层数较浅前向推理速度更快显存占用更低适合在实时性要求较高的场景下使用或者作为初步实验的快速迭代版本。ResNet101在ResNet50的基础上增加了更多的Bottleneck模块主要在layer3和layer4阶段加深网络能够捕获更丰富的语义特征在复杂场景下的分割精度通常比ResNet50高出1到3个百分点但代价是训练时间更长显存消耗更大。选择哪个主干取决于你的实际资源情况。如果你只有一张消费级显卡比如RTX 3060 12G或者3080 10G我建议先跑ResNet50把整个pipeline走通确认数据加载、损失计算、评估逻辑都没有问题再切换到ResNet101去刷精度。如果你的数据集比较小比如只有几千张图像直接上ResNet101反而容易过拟合不如先用ResNet50配合数据增强来得稳。1.3 AuxiliaryBranch辅助分支的设计意图AuxiliaryBranch辅助分支这个设计在图像分类任务里叫auxiliary loss或deep supervision在分割任务里的作用其实更关键。默认情况下语义分割网络只在最后的主输出处计算损失误差信号需要从最深层一路反向传播到浅层梯度在传递过程中会不断衰减浅层网络学习得不够充分。引入辅助分支后我们从网络中间某个特征层引出一个额外的输出头单独计算一次损失让中间层也能直接接收到监督信号。这样做有两个直接好处一是梯度传播路径变短主干浅层更容易被优化二是相当于一种正则化约束能有效抑制过拟合。这个项目的辅助分支是可以通过配置开关启用的。我的经验是在训练初期或者使用深层主干ResNet101时把auxiliary_branch设置为True能明显看到loss下降更快、收敛曲线更平滑训练中后期可以把这个分支关掉或者给辅助损失加一个较小的权重避免它干扰主分支的最终表现。具体怎么配下一节详细讲。2. 核心实现细节与配置解析2.1 FCN_8S网络结构的逐层拆解整个FCN_8S网络可以拆成三大部分特征提取主干、特征融合模块、上采样输出头。特征提取主干就是去掉全连接层的ResNet50或ResNet101输入图像经过conv1到conv5的逐级下采样特征图分辨率依次缩小为原图的1/2、1/4、1/8、1/16、1/32通道数从64逐步增加到2048。后面的特征融合模块和上采样输出头是这个项目的关键。从尾部往前来捋。conv5输出的特征图记作feat5分辨率是输入的1/32。首先对feat5做一次1×1卷积将通道数降到需要的数量通常是21对应类别数记为score5。然后对score5做2倍上采样得到与conv4特征图相同分辨率的特征图。conv4的输出feat4经过1×1卷积降通道后记为score4将上采样后的score5与score4按元素相加得到融合特征Fusion1。接下来对Fusion1进行2倍上采样使其与conv3的输出分辨率一致再与conv3经过1×1卷积处理后的score3相加得到Fusion2。最后对Fusion2进行8倍上采样恢复到输入图像分辨率经过softmax或argmax得到最终的分割预测。整个结构里最值得注意的操作细节是融合方式选择的是逐元素相加而非通道拼接。相加操作不增加通道数后续卷积的计算量更小而且FCN原文实验也证明了相加和拼接的效果差距不大。不过这里有一个容易被忽视的点不同层的特征尺度差异可能很大直接相加会把浅层的强响应和深层的弱响应混合在一起所以每个1×1卷积后面最好接一个BN层做归一化虽然FCN论文年代还没有BN但现代复现里加了BN之后效果会稳定很多。2.2 ResNet预训练权重加载与冻结策略使用ResNet50/101做主干最大的隐形福利是可以加载ImageNet预训练权重这比从头训练省下太多时间。实际加载时要特别注意state_dict的键名匹配问题。如果主干部分是从torchvision或者timm库加载的键名通常形如layer1.0.conv1.weight如果你的FCN模型代码把整个主干封装成了一个子模块可能键名会多出一层前缀。我建议在项目代码里写一个专门的权重加载函数把strictFalse作为参数传入加载后打印出missing_keys和unexpected_keys人工确认一下主干部分是否全部对上。关于主干是否冻结这件事我的建议是分阶段处理。第一阶段冻结ResNet的前两个stage只训练后面的层和分割头这个阶段显存占用小训练速度快适合先把分割头训快。第二阶段解冻全部层用一个较小的学习率微调所有参数这个阶段精度会有一个明显提升。如果一开始就全部解冻浅层预训练特征会在训练初期被分割损失的大梯度破坏反而得不偿失。2.3 配置文件里的关键参数怎么定完整配置的骨架大概是这样的model: name: FCN8S backbone: resnet50 # 可选 resnet50 / resnet101 num_classes: 21 pretrained: true aux_branch: enabled: true weight: 0.4 position: layer3 train: input_size: [512, 512] batch_size: 8 epochs: 100 optimizer: sgd lr: 0.01 lr_scheduler: poly momentum: 0.9 weight_decay: 1e-4input_size的选取直接影响整个网络的参数量。拿512×512输入来说输入的张量在主干最深层是16×16融合上采样过程中要保证每一步的尺寸计算都能被2整除否则会报形状不匹配的错误。这里强烈建议输入尺寸选择2的幂次比如256、512、1024。aux_branch里的weight参数值得多说几句。辅助损失和主损失最终会加起来作为整体的训练目标即total_loss main_loss weight * aux_loss。weight设得太大会让网络过度关注中间层特征而忽视最终输出质量太小又起不到辅助训练的作用。我测试下来0.3到0.5之间是比较稳的区间跑完100个epoch之后可以逐步把weight降到0让主分支在最后几个epoch里精调。2.4 双主干切换的实现思路双主干的实现本质上是在网络初始化时根据backbone参数动态选择不同的卷积堆叠方式。ResNet50和ResNet101的区别集中在layer3和layer4的Bottleneck数量上ResNet50是6和3ResNet101是23和3。代码层面只需要把构建layer的函数参数化传入blocks数量对应的列表即可。需要注意的一点是ResNet101的layer3更深中间特征图的通道数更大因此辅助分支如果放在layer3参数量和显存占用都会比ResNet50版本高一些。切换主干之前最好把batch_size适当调小避免显存溢出。3. 实操过程与核心环节实现3.1 环境配置与数据准备这个项目需要的基础环境很常规Python 3.8以上、PyTorch 1.10以上、CUDA 11.x。如果你用的是Ubuntu 22.04装PyTorch的时候要注意CUDA版本和显卡驱动的匹配问题。装上之后可以用这句话验证环境是否就绪import torch print(torch.__version__) print(torch.cuda.is_available())数据准备阶段推荐使用VOC2012数据集作为起点它包含20个物体类别加1个背景类别是语义分割最经典的数据集。先把数据组织成这样的目录结构VOCdevkit/ VOC2012/ JPEGImages/ SegmentationClass/ ImageSets/ Segmentation/ train.txt val.txt数据加载器要做三件事读取图像和对应的掩膜、做同步的数据增强图像和掩膜要执行相同的随机裁剪、翻转操作、把类别索引转成one-hot编码或者直接作为长整型标签。值得注意的是分割任务的掩膜图像中类别的颜色值不是从0开始连续编号的而是Pascal VOC调色板索引读取掩膜时一定要用PIL的getpalette方法还原成索引图不要直接当RGB图像读否则类和索引会错乱。这一步我见过太多人踩坑了。3.2 完整训练流程与损失函数实现训练流程主循环的伪代码如下for epoch in range(start_epoch, epochs): model.train() for images, masks in train_loader: images images.to(device) masks masks.to(device) outputs model(images) if isinstance(outputs, tuple): main_out, aux_out outputs else: main_out outputs aux_out None main_loss criterion(main_out, masks) total_loss main_loss if aux_out is not None and aux_cfg.enabled: aux_loss criterion(aux_out, masks) total_loss main_loss aux_cfg.weight * aux_loss optimizer.zero_grad() total_loss.backward() optimizer.step()损失函数这里有几个细节。最常用的是交叉熵损失但直接使用会遇到类别不平衡问题背景像素占了绝大多数前景目标的学习会被淹没。解决办法有两个方向一是给不同类别加权计算每个类别的像素频率频率高的类别给较低的权重二是使用Focal Loss或者复合损失。我的建议是在数据集类别分布严重不均匀时至少给交叉熵加上类别权重参数或者在评估时注意观察mIoU而非像素准确率因为即使什么都不训练把所有像素预测为背景像素准确率也能高达80%以上但这显然不是我们要的模型。优化器方面FCN这类全卷积结构用SGD加动量的效果往往优于Adam这也是迁移学习的常见经验。学习率策略推荐使用poly衰减策略初始学习率乘以(1 - iter/total_iter)的power次方power通常取0.9。这种策略让网络到训练后期逐步降低学习率收敛得更稳定。3.3 评估指标与可视化验证评估语义分割模型的标准指标是mIoUmean Intersection over Union计算方式是每个类别分别计算预测与标签之间的IoU然后取平均。IoU的定义是交集区域像素数除以并集区域像素数公式为IoU TP / (TP FP FN)。这一指标同时惩罚错误检测和漏检能比较好地反映分割质量。实现评估循环时建议将每个类别的TP、FP、FN累加最后统一计算IoU这样比逐张图计算后取平均更准确。可视化验证同样重要每训练几个epoch保存一批预测结果与标签的对比图不需要多么复杂的可视化代码只是把模型输出经argmax后的结果映射为颜色图和原图以及GT并排保存就行。这一步的价值在于能第一时间发现网络是否在正常学习比如如果预测结果长时间全黑或全空白大概率是标签读取或者输出通道数没有对应上如果边缘出现了棋盘格效应说明转置卷积的kernel_size和stride参数设置不合理导致重叠区域权重异常。3.4 辅助分支的两种启用姿势辅助分支的位置选择影响很大。项目里默认可能放在layer3输出之后这个位置的特征图分辨率是原图1/8信息量充足梯度回传路径不长是比较稳妥的选择。如果你想让辅助分支发挥更大作用可以把它放在更浅的layer2但注意layer2的特征图分辨率更高辅助头需要额外做更精细的上采样显存开销会变大。辅助分支的输出头设计也不必太复杂一个1×1卷积将通道数调整为num_classes再直接上采样到输入尺寸即可。我建议不要为辅助分支单独设计复杂的ASPP结构这会让训练显存和计算时间成倍增加而带来的收益非常有限。4. 常见问题与排查技巧实录4.1 形状不匹配与维度冲突问题几乎所有教程的评论区都会出现这种报错size mismatch for backbone.layer4.2.conv1.weight: copying a param with shape torch.Size([512, 1024, 1, 1]) from checkpoint, the shape in current model is torch.Size([2048, 2048, 1, 1])。这种问题基本就是主干网络版本不一致导致的。比如想加载ResNet50的预训练权重但代码里实际构建的是ResNet101或者构建ResNet时参数用了replace_stride_with_dilation修改了输出步长导致conv1的权重形状完全对不上。解决办法是确认权重文件的来源与代码里的主干定义一致如果不一致则去掉strict模式按需加载能对应的层。还有一个常见问题是辅助分支的输出与GT尺寸不一致。比如辅助分支的特征图是1/8分辨率直接上采样8倍后如果输入尺寸是512则输出是64×64而上采样后是512×512GT也是512×512按理说没问题。但如果输入尺寸不是8的整数倍比如500×500上采样后的尺寸就变成了504×504和GT差了4个像素loss就会报错。所以再次强调输入尺寸一定要选2的幂次。4.2 显存不足与训练速度过慢显存不足是分割训练最常见的拦路虎。FCN-8S虽然结构不算深但输入分辨率拉高后显存消耗依然很可观。排查优化思路按顺序来先把batch_size降到1用输入分辨率512×512试跑看显存是否够用如果还不行把输入分辨率降到384或320再不行把主干换回ResNet50且辅助分支关掉。在最终评估时再切回完整配置即可。另外开启梯度累积可以等效模拟更大的batch_sizeaccumulation_steps 4 loss loss / accumulation_steps loss.backward() if (iter 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()训练速度慢的问题则优先检查数据加载环节。如果把num_workers设成0会让GPU大部分时间都在等CPU喂数据。建议设置为CPU核心数的一半以上并在数据加载器的pin_memory参数设为True。还有一个容易被忽略的点如果训练集图片特别大最好先用脚本统一做resize预处理并保存为LEB格式避免训练时临时做解码和缩放。4.3 精度达不到预期的排查方向模型训练完发现mIoU不理想别急着换模型结构先按这三个维度排查第一损失曲线是否正常下降如果训练loss没降下来大概率是学习率设置过大或者梯度爆炸可以在backward之前做梯度裁剪第二评估指标是否合理如果mIoU低但像素准确率高往往是类别不平衡问题考虑给loss加权重第三预测可视化是否粗糙如果分割结果的边缘出现大量锯齿状噪声说明特征融合策略还不够强可以尝试在上采样输出头后面接一个3×3卷积做平滑。我亲测过的一个非常有效的优化是在FCN-8S的最终上采样前加入一个ASPP-like的多尺度空洞卷积模块。改动量不大只需要把不同rate的3×3空洞卷积输出拼接起来再做1×1卷积mIoU通常能提升3到5个点。这个改动不改变整体架构逻辑但让网络对不同尺度的目标响应更准确。4.4 数据集类别不平衡的正确处理方式街景、遥感这类数据集中类别不平衡问题非常突出比如道路、天空、树木等背景类别占据了绝大部分像素而行人、车辆等目标类别只有极少像素。如果有类别权重配置直接加权即可如果没有最实用的是采用OHEM或者hard example mining思路在计算loss时只取loss值最大的K个像素参与梯度回传。这个思路实现起来非常简单在PyTorch中只需对展开后的loss做top-k筛选即可但效果显著。需要注意的一个小细节是在计算mIoU时一定要剔除未出现的类别即在GT和预测中都没有该类别的像素否则会得到一个为0的IoU把整体均值拉低。处理方式是为每个类别维护一个hit计数器只有当GT中出现该类别时才累计该类的IoU。5. 从FCN到更强架构的扩展思路5.1 在FCN-8S基础上插入注意力模块如果单纯想把这个项目做成一个毕业设计或者竞赛baseline给主干插入轻量级注意力机制是一个非常讨巧的改进方向。比如在ResNet每个stage的输出后接一个SE模块或CBAM模块只增加极小的参数量却往往能带来1到2个点的mIoU提升。注意力机制的核心作用是对特征通道进行重新标定让网络更关注对分割任务有用的特征通道抑制无关通道的响应。实现上也很简单以SE模块为例对特征图先做全局平均池化得到每个通道的全局描述然后经过两个全连接层输出一组通道权重最后将权重与原始特征图相乘。整个模块只有几行代码不需要改变FCN原有结构直接插在feature map进入fusion之前的每个分支上即可。5.2 模型部署与推理加速的实际经验训练好模型之后如果要落地到实际场景还需要做几步转换和加速。第一步是把PyTorch模型转换成TorchScript格式或者ONNX格式。TorchScript适合PyTorch生态内部的部署ONNX则方便转到TensorRT、OpenVINO等推理引擎。转换过程中最容易掉链子的操作是上采样部分因为F.interpolate的坐标对齐方式在不同框架里语义不完全一致如果想保证转换后输出精度不损失最好把双线性插值的align_corners参数固定为False并确保所有上采样操作都一致。第二步是推理时降低输入分辨率。对于实时性要求高的场景输入分辨率可以从512降到384或320模型推理耗时会大幅缩短精度损失往往在可接受范围内。如果还想进一步压缩计算量可以考虑把浮点模型量化到INT8不过量化对分割这种密集预测任务的影响比较大容易出现大面积掉精度建议先量化后做校准数据集上的mIoU测试确认损失可控再上线。5.3 项目代码管理与复现建议这种带配置文件的工程代码组织建议按模块拆分为五个目录models网络结构、configs配置文件、datasets数据加载器、utils工具函数如损失计算、评估指标、train.py训练脚本。配置解析推荐用YAML文件比argparse和硬编码好维护太多。使用yaml.safe_load读取配置后动态地把参数传给模型构建函数即可。另外务必配置好随机种子否则每次跑的结果都会不一致影响后续对比实验。最后再提一个很多新手不太注意的细节辅助分支开关的维护。当aux_branch.enabled为True时模型forward返回的是主输出和辅助输出两个张量在训练和评估时的处理方式完全不同训练时两者都要参与损失计算但评估和后续推理时只取主输出。建议在网络forward方法里加一个return_aux参数推理时默认False避免推理时多算了辅助分支的开销。写在最后的一些体会在我自己复现这个项目并做了大量调优实验之后最大的感受是FCN-8S虽然已经是多年前的架构但作为理解语义分割全流程的载体它的结构化程度和可扩展空间依然无可替代。很多现代分割网络的组件比如多尺度特征融合、辅助监督、空洞卷积都可以从FCN这个骨架上自然生长出来。这个仓库的价值不仅在于给你一个能跑通的基线更在于它把主干网络选择、辅助训练机制、配置化驱动的工程思想全部串在了一条线路上你完全可以把它当做一个语义分割工具箱往里面随意插入自己的实验想法。如果你现在正处在想入坑语义分割但不知道从哪下手的阶段把这份代码从网络结构到训练脚本逐行吃透会比跟风上手最新的SAM大模型更能帮你建立扎实的基础。本文还有配套的精品资源点击获取
返回列表