图像分割这个方向,我从2019年开始陆续在工业质检、遥感解译和医疗影像三个场景里落地过项目,踩过的坑比看过的论文还多。很多人刚入门的时候会被各种网络结构名字绕晕——FCN、U-Net、SegNet、Deeplab、SegFormer,每个都号称自己是SOTA,但真正上手做项目的时候才发现,选错架构比调错超参的代价大得多。这篇内容我打算把这五个经典分割网络从设计动机、核心机制、实操细节到避坑经验完整拆一遍,不管你是刚接触计算机视觉的学生,还是需要快速选型落地的工程师,都能从中找到可以直接用的东西。全文会围绕语义分割这条主线展开,穿插大量我在实际项目中的参数配置和调试记录,尽量做到看完就能动手。
1. 图像分割到底在解决什么问题
1.1 从分类到分割:任务定义的演进逻辑
图像分割本质上是一个像素级分类问题。图像分类是给整张图打一个标签,目标检测是给每个物体画一个框,而语义分割要求对图像中每一个像素都判断它属于哪个类别。这个区别听起来简单,但带来的计算量和工程复杂度是指数级上升的。一张512×512的图片,分类任务只需要输出一个N维向量,分割任务则需要输出512×512×N的张量。
我第一次做分割项目的时候,最大的认知冲击就是显存消耗。同样backbone下,分类任务的batch size可以开到64,换成分割直接掉到4甚至2。原因很简单:分类网络最后经过全局池化,空间维度被压缩成1×1;分割网络必须保留完整的空间分辨率,中间特征图的尺寸远大于分类网络。
从技术演进的角度看,分割任务经历了几个阶段。早期是传统方法,靠区域生长、图割、超像素聚类这些手段,依赖手工特征,泛化能力很差。2015年FCN的出现标志着深度学习正式进入分割领域,之后U-Net、SegNet在结构上做了针对性改进,Deeplab系列引入了空洞卷积和多尺度处理,到了SegFormer则把Transformer架构引入分割,开启了新的范式。
1.2 语义分割、实例分割与全景分割的边界
这三个概念经常被混淆,我在面试候选人的时候也喜欢问这个问题。语义分割只关心像素的类别,不区分同类物体的不同个体——比如图中有三只猫,语义分割会把它们都标成"猫"这一类,不会区分猫A和猫B。实例分割则要求区分同类物体的不同实例,三只猫要分别标成猫1、猫2、猫3。全景分割是两者的结合,既要做语义分割(处理背景类如天空、道路),又要做实例分割(处理前景类如人、车)。
实际项目中选哪种,取决于业务需求。工业质检里的缺陷分割通常只需要语义分割,因为不需要区分"第几个缺陷"。自动驾驶的街景理解则需要全景分割,既要识别道路、天空这些背景,又要区分每一辆车、每一个行人。
1.3 主流应用场景与对应的技术选型
不同场景对分割网络的要求差异很大,我整理了一个选型参考表:
| 应用场景 | 核心需求 | 推荐架构 | 关键考量 |
|---|---|---|---|
| 医疗影像 | 小样本、高精度 | U-Net及其变体 | 跳跃连接保留细节 |
| 自动驾驶 | 实时性、多尺度 | Deeplabv3+ / SegFormer | 推理速度与精度平衡 |
| 工业质检 | 高分辨率、小缺陷 | FCN + 高分辨率分支 | 感受野与分辨率兼顾 |
| 遥感解译 | 大尺度、多类别 | SegFormer / Deeplab | 全局上下文建模 |
| 广告牌/地毯等特定分割 | 快速部署、边缘设备 | SegNet / 轻量U-Net | 模型体积与速度 |
这个表不是绝对的,但能帮你快速缩小选型范围。我见过太多人一上来就用SegFormer做医疗影像分割,结果数据量不够,Transformer根本训不起来,反而不如老老实实用U-Net。
2. 五大经典分割网络的核心机制拆解
2.1 FCN:全卷积网络的奠基性设计
FCN(Fully Convolutional Network)的核心贡献在于把分类网络的全连接层替换成卷积层,使得网络可以接受任意尺寸的输入并输出对应尺寸的分割图。这个想法在2015年是非常超前的,因为当时大家还习惯用全连接层做分类。
FCN的具体做法是:以VGG16为backbone,把最后三个全连接层换成卷积层,然后通过上采样(反卷积)把特征图恢复到原图尺寸。FCN有三个版本——FCN-32s、FCN-16s、FCN-8s,区别在于融合了不同层级的特征。FCN-32s只用了最后一层特征上采样32倍,结果很粗糙;FCN-16s融合了pool4的特征,上采样16倍后再融合;FCN-8s进一步融合了pool3的特征,效果最好。
我在实际使用FCN的时候发现一个关键问题:上采样32倍带来的信息损失是不可逆的。即使融合了浅层特征,边缘依然很模糊。这也是为什么后续的U-Net和SegNet都在跳跃连接上做文章。
FCN的另一个重要贡献是引入了逐像素的softmax损失,配合类别平衡权重来处理类别不均衡问题。这个思路至今仍在广泛使用。
# FCN核心结构的简化实现 import torch import torch.nn as nn class FCN8s(nn.Module): def __init__(self, num_classes): super().__init__() # 以VGG16为backbone self.features = vgg16_features() # 替换全连接层为卷积层 self.fc6 = nn.Conv2d(512, 4096, 7, padding=3) self.fc7 = nn.Conv2d(4096, 4096, 1) # 分类头 self.score_fr = nn.Conv2d(4096, num_classes, 1) self.score_pool4 = nn.Conv2d(512, num_classes, 1) self.score_pool3 = nn.Conv2d(256, num_classes, 1) # 上采样层 self.upscore2 = nn.ConvTranspose2d(num_classes, num_classes, 4, stride=2, padding=1) self.upscore8 = nn.ConvTranspose2d(num_classes, num_classes, 16, stride=8, padding=4)2.2 U-Net:编码器-解码器与跳跃连接的经典范式
U-Net最初是为医学图像分割设计的,但它的影响力远远超出了医学领域。它的结构像一个"U"形——左边是编码器(下采样),右边是解码器(上采样),中间通过跳跃连接把编码器每一层的特征直接拼接到解码器对应层。
这个设计的精妙之处在于:编码器在逐层下采样的过程中,感受野越来越大,语义信息越来越强,但空间细节越来越模糊;解码器在逐层上采样的过程中,空间分辨率逐渐恢复,但语义信息不够精确。跳跃连接把编码器的高分辨率细节直接送到解码器,让解码器在恢复分辨率的同时能利用这些细节,从而得到既精确又语义明确的分割结果。
我在做视网膜血管分割的时候,U-Net的表现远超FCN。血管的宽度只有几个像素,FCN的上采样根本恢复不出这种细粒度结构,而U-Net通过跳跃连接保留了浅层的高分辨率特征,血管的连续性明显更好。
U-Net的另一个实用设计是overlap-tile策略,用于处理大尺寸图像。把大图切成有重叠的小块分别预测,再拼接起来,可以有效解决显存不足的问题。这个策略在遥感图像分割中特别常用。
# U-Net核心模块 class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch=3, num_classes=2): super().__init__() # 编码器 self.enc1 = DoubleConv(in_ch, 64) self.enc2 = DoubleConv(64, 128) self.enc3 = DoubleConv(128, 256) self.enc4 = DoubleConv(256, 512) self.pool = nn.MaxPool2d(2) # 瓶颈层 self.bottleneck = DoubleConv(512, 1024) # 解码器(注意输入通道翻倍,因为要拼接跳跃连接) self.up4 = nn.ConvTranspose2d(1024, 512, 2, stride=2) self.dec4 = DoubleConv(1024, 512) self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2) self.dec3 = DoubleConv(512, 256) self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2) self.dec2 = DoubleConv(256, 128) self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.dec1 = DoubleConv(128, 64) self.out = nn.Conv2d(64, num_classes, 1)2.3 SegNet:编码器索引传递的高效解码方案
SegNet来自剑桥大学,它的核心创新在解码器的上采样方式上。传统上采样要么用反卷积(需要学习参数),要么用双线性插值(不需要学习但效果一般)。SegNet提出了一种池化索引传递的机制:在编码器做最大池化的时候,记录下每个池化窗口最大值的位置索引;在解码器上采样的时候,直接把值放回这些记录的位置,其余位置填零。
这样做的好处是解码器不需要学习上采样参数,模型参数量大幅减少,推理速度更快。在道路分割、广告牌分割这类对实时性要求较高的场景中,SegNet的优势很明显。我实测过,在同等backbone下,SegNet的推理速度比U-Net快约20%,而精度只差1-2个百分点。
但SegNet也有明显的短板:池化索引传递会丢失一部分信息,尤其是当池化窗口较大时,上采样的结果会比较稀疏。我在做地毯图像分割系统的时候,SegNet在纹理复杂区域的边缘处理明显不如U-Net,后来换成了U-Net的变体。
2.4 Deeplab系列:空洞卷积与多尺度上下文
Deeplab系列是Google出品,经历了v1、v2、v3、v3+四个主要版本。它的核心贡献是空洞卷积(Atrous Convolution)和ASPP(Atrous Spatial Pyramid Pooling)。
空洞卷积的思路是在卷积核中插入"空洞",使得在不增加参数量的情况下扩大感受野。比如一个3×3的卷积核,设置dilation rate=2,实际覆盖的区域就是5×5,但参数量还是9个。这对于分割任务非常重要,因为我们需要大感受野来捕获全局上下文,但又不能通过下采样来扩大感受野(会丢失空间信息)。
ASPP则是在多个不同的dilation rate上并行做空洞卷积,然后把结果拼接起来。这样可以在多个尺度上捕获上下文信息,对于多尺度目标的分割效果很好。Deeplabv3+进一步引入了编码器-解码器结构,把ASPP放在编码器末端,解码器则融合浅层特征。
我在遥感图像分割项目中大量使用Deeplabv3+,建筑物、道路、植被这些不同尺度的目标都能较好地分割出来。但要注意,空洞卷积对小目标不友好,因为dilation rate太大时,卷积核会"跳过"小目标。所以实际使用中需要根据目标尺度调整dilation rate。
# ASPP模块实现 class ASPP(nn.Module): def __init__(self, in_ch, out_ch=256): super().__init__() self.conv1 = nn.Conv2d(in_ch, out_ch, 1) self.conv2 = nn.Conv2d(in_ch, out_ch, 3, padding=6, dilation=6) self.conv3 = nn.Conv2d(in_ch, out_ch, 3, padding=12, dilation=12) self.conv4 = nn.Conv2d(in_ch, out_ch, 3, padding=18, dilation=18) self.global_pool = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_ch, out_ch, 1) ) self.project = nn.Sequential( nn.Conv2d(out_ch * 5, out_ch, 1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): size = x.shape[2:] feat1 = self.conv1(x) feat2 = self.conv2(x) feat3 = self.conv3(x) feat4 = self.conv4(x) feat5 = self.global_pool(x) feat5 = nn.functional.interpolate(feat5, size=size, mode='bilinear', align_corners=True) out = torch.cat([feat1, feat2, feat3, feat4, feat5], dim=1) return self.project(out)2.5 SegFormer:Transformer架构下的轻量高效分割
SegFormer是2021年NVIDIA提出的,它把Transformer引入分割任务,但做了大量针对性的简化。核心设计有两点:一是分层Transformer编码器,输出多尺度特征图;二是轻量级MLP解码器,不需要复杂的上采样和融合操作。
SegFormer的编码器借鉴了Swin Transformer的分层思想,但做了简化——它使用无位置编码的注意力机制,通过卷积来隐式编码位置信息。解码器则非常简单,把四个尺度的特征图统一到同一维度后直接拼接,再用MLP融合。
我在实际项目中的体验是:SegFormer在数据量充足时表现非常好,尤其是多类别、多尺度的场景。但数据量少的时候容易过拟合,因为Transformer的自注意力机制需要大量数据来学习有效的注意力模式。如果你的数据集只有几百张图,建议还是用U-Net或Deeplab。
SegFormer的另一个优势是模型系列丰富,从MiT-B0到MiT-B5,参数量从3.7M到84.7M,可以根据部署环境灵活选择。我在边缘设备上部署过MiT-B0,推理速度完全能满足实时要求。
3. 实操落地:从数据准备到模型部署的完整流程
3.1 数据集构建与标注策略
分割任务的数据标注成本远高于分类和检测。一张512×512的图,分类标注只需要几秒钟,分割标注可能需要几分钟甚至更久。我在实际项目中总结了几条降低标注成本的策略:
第一,先用弱标注启动。可以用图像级标签训练一个分类器,然后用CAM(类激活图)生成粗略的分割伪标签,再人工修正。这样比从零开始标注快很多。
第二,主动学习策略。先标注一小部分数据训练模型,然后用模型预测未标注数据,挑选置信度低或不确定性高的样本优先标注。我做过对比,主动学习能减少约40%的标注量。
第三,数据增强要针对分割任务设计。分类任务常用的随机裁剪、颜色抖动在分割中同样适用,但要注意几何变换必须同步应用到图像和标签上。另外,分割任务特有的增强包括:弹性形变(对医学图像特别有效)、超像素替换、CutMix等。
# 分割任务的数据增强示例 import albumentations as A train_transform = A.Compose([ A.RandomResizedCrop(512, 512, scale=(0.5, 1.0)), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.3), A.RandomRotate90(p=0.5), A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, p=0.5), A.ElasticTransform(alpha=120, sigma=120 * 0.05, p=0.3), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # 注意:几何变换必须同步应用到mask # albumentations会自动处理image和mask的同步变换3.2 损失函数选择与类别不均衡处理
分割任务中最常见的坑就是类别不均衡。比如在道路分割中,道路像素可能只占全图的5%,如果直接用交叉熵损失,模型会倾向于把所有像素都预测成背景,因为这样也能达到95%的准确率。
解决方案有几种:
加权交叉熵:给少数类更高的权重。权重的计算方式通常是类别频率的倒数,或者取对数后的倒数。我一般用中位数频率平衡(Median Frequency Balancing),效果比较稳定。
Dice Loss:直接优化预测区域和真实区域的交并比,对类别不均衡不敏感。但Dice Loss在训练初期梯度不稳定,通常和交叉熵联合使用。
Focal Loss:降低易分类样本的权重,让模型聚焦于难分类样本。在分割任务中,Focal Loss对边界像素的处理效果很好。
Tversky Loss:Dice Loss的推广形式,可以调节FP和FN的权重。在医学图像分割中,如果漏检的代价高于误检,可以增大FN的权重。
我在实际项目中的经验是:交叉熵 + Dice Loss的组合最稳定,权重比一般设为1:1或1:2。如果类别极度不均衡(比如1:100以上),再加Focal Loss或Tversky Loss。
# 组合损失函数 class CombinedLoss(nn.Module): def __init__(self, ce_weight=1.0, dice_weight=1.0): super().__init__() self.ce_weight = ce_weight self.dice_weight = dice_weight self.ce = nn.CrossEntropyLoss(weight=class_weights) def dice_loss(self, pred, target, smooth=1e-6): pred = torch.softmax(pred, dim=1) target_onehot = nn.functional.one_hot(target, num_classes).permute(0, 3, 1, 2).float() intersection = (pred * target_onehot).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) dice = (2 * intersection + smooth) / (union + smooth) return 1 - dice.mean() def forward(self, pred, target): ce_loss = self.ce(pred, target) dice_loss = self.dice_loss(pred, target) return self.ce_weight * ce_loss + self.dice_weight * dice_loss3.3 训练策略与学习率调度
分割网络的训练有几个关键点:
学习率设置:backbone通常用预训练权重,学习率要设小一些(比如1e-4),解码器是随机初始化的,学习率可以大一些(比如1e-3)。这种分层学习率的策略能加速收敛。
学习率调度:我常用的是Poly策略,即lr = base_lr * (1 - iter/max_iter)^0.9。这个策略在分割任务中表现稳定,比Step策略更平滑。Cosine退火也不错,但需要配合warmup使用。
Batch Size与BN:分割任务的batch size通常较小,如果小于8,BatchNorm的统计量会不准确。这时候可以考虑用GroupNorm或SyncBN。我在单卡训练时一般用GroupNorm,多卡时用SyncBN。
训练轮数:分割任务通常需要比分类更长的训练时间。小数据集(几千张)一般训练100-200个epoch,大数据集(几万张以上)训练50-100个epoch就够了。要注意监控验证集的mIoU,如果连续10个epoch不提升就可以考虑早停。
3.4 推理优化与部署注意事项
训练好的模型要部署到实际系统中,还需要做一系列优化:
模型剪枝与量化:把不重要的通道剪掉,或者把FP32量化成INT8,可以大幅减少模型体积和推理时间。我实测过,INT8量化后SegFormer-B0的推理速度提升约2倍,精度只掉0.5个百分点。
TensorRT加速:如果部署在NVIDIA GPU上,用TensorRT可以把推理速度再提升2-3倍。但要注意TensorRT对某些操作的支持有限,比如空洞卷积在某些版本中需要特殊处理。
滑动窗口推理:对于大尺寸图像,直接resize到网络输入尺寸会丢失细节。滑动窗口推理把大图切成有重叠的小块分别预测,再拼接起来。重叠区域取平均或取最大值。这个方法在遥感图像分割中几乎是标配。
后处理:分割结果通常需要后处理来去除小噪点、平滑边缘。常用的方法有条件随机场(CRF)、形态学操作、连通域分析等。CRF效果最好但速度慢,形态学操作速度快但效果一般。我在实际项目中一般用导向滤波做边缘平滑,速度和效果的平衡比较好。
4. 常见问题与排查技巧实录
4.1 训练不收敛或Loss震荡
这是最常见的问题,原因可能有很多。我整理了一个排查清单:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Loss完全不降 | 学习率过大 | 打印梯度范数 | 降低学习率10倍 |
| Loss震荡剧烈 | Batch size太小 | 检查BN统计量 | 增大batch或换GroupNorm |
| Loss降但mIoU不升 | 过拟合 | 对比训练/验证loss | 加数据增强或正则化 |
| 某些类别IoU为0 | 类别不均衡 | 统计类别分布 | 加类别权重或Focal Loss |
| 训练后期Loss突增 | 学习率调度问题 | 检查lr曲线 | 调整调度策略 |
我踩过最坑的一次是:数据增强中的Normalize用了ImageNet的均值和方差,但我的数据是医学图像,分布差异很大,导致模型前几个epoch完全不收敛。后来改成用自己的数据统计均值和方差,问题就解决了。数据预处理的均值和方差一定要用自己数据集的统计量,这是很多人容易忽略的细节。
4.2 边缘分割粗糙的改善方法
边缘粗糙是分割任务的通病,尤其是FCN和SegNet这类上采样倍数较大的网络。改善方法有几种:
提高输出分辨率:把网络的输出stride从32降到8或4。Deeplabv3+的输出stride是4,边缘质量明显好于FCN-32s。
增加边界损失:在损失函数中加入边界感知项,比如Boundary Loss或Hausdorff Distance Loss。这些损失直接优化预测边界和真实边界的距离。
后处理细化:用CRF或导向滤波对预测结果做后处理。CRF利用原图的颜色信息来细化边界,效果很好但速度慢。导向滤波速度快,效果也不错。
多尺度融合:把不同尺度的预测结果融合起来,边缘通常比单尺度更好。
4.3 小目标分割效果差的解决思路
小目标分割是分割任务中的难点。我在工业质检中遇到过只有十几个像素的缺陷,用常规方法根本分不出来。后来总结了几个有效策略:
提高输入分辨率:把输入从512×512提高到1024×1024,小目标的像素数增加4倍,分割效果明显改善。但显存消耗也增加4倍,需要权衡。
使用空洞卷积:空洞卷积可以在不降低分辨率的情况下扩大感受野,对小目标友好。但dilation rate不能太大,否则会跳过小目标。
多尺度训练:训练时随机缩放输入图像,让模型适应不同尺度的目标。测试时用多尺度推理,把不同尺度的结果融合。
针对性数据增强:对小目标做过采样,或者用Copy-Paste增强把小目标复制到不同位置。
4.4 模型部署时的显存与速度优化
部署阶段最常见的问题是显存不够或速度太慢。我的优化顺序一般是:
- 降低推理分辨率:这是最直接的方法,但会影响小目标的分割效果。
- 模型量化:FP32转INT8,显存减少4倍,速度提升2倍左右。
- 模型剪枝:去掉冗余通道,参数量减少30-50%。
- TensorRT/ONNX Runtime:推理框架优化,速度提升2-3倍。
- 滑动窗口+批处理:把多个窗口拼成一个batch推理,提高GPU利用率。
我在边缘设备上部署SegFormer-B0的时候,经过INT8量化和TensorRT优化,单帧推理时间从120ms降到了25ms,完全满足实时要求。
4.5 跨域泛化能力不足的应对
训练集和测试集分布不一致是实际项目中的常见问题。比如用晴天数据训练的模型,在雨天数据上表现很差。应对方法有:
域适应训练:在训练时加入目标域的未标注数据,用对抗训练或自训练的方式让模型适应目标域。
强数据增强:在训练时模拟目标域的分布变化,比如加雨、加雾、调亮度等。
测试时适应:在测试时用少量目标域数据微调模型,或者用测试时的统计量来调整BN层。
我在遥感项目中遇到过季节变化导致的域偏移问题,用强数据增强(模拟不同季节的颜色分布)加上测试时BN调整,mIoU从62%提升到了71%。
5. 架构选型的决策框架与实战建议
5.1 不同数据量下的架构选择
数据量是选型的第一约束。我根据实际项目经验总结了一个参考:
| 数据量 | 推荐架构 | 理由 |
|---|---|---|
| <500张 | U-Net + 强增强 | 跳跃连接对小数据友好 |
| 500-5000张 | Deeplabv3+ / U-Net++ | 多尺度能力强,不易过拟合 |
| 5000-50000张 | SegFormer / Deeplabv3+ | Transformer开始发挥优势 |
| >50000张 | SegFormer-B4/B5 | 大数据下Transformer优势明显 |
这个表是经验值,实际还要看任务的复杂度和类别数。类别越多、场景越复杂,需要的数据量越大。
5.2 实时性与精度的权衡策略
如果项目对实时性有要求,选型逻辑完全不同。我的建议是:
极致速度:SegNet或轻量U-Net(通道数减半),配合INT8量化,可以在边缘设备上跑到30FPS以上。
速度与精度平衡:SegFormer-B0/B1或Deeplabv3+ with MobileNetV2 backbone,精度比轻量U-Net高3-5个点,速度也能满足实时。
精度优先:SegFormer-B5或Deeplabv3+ with ResNet101,精度最高但速度慢,适合离线处理。
我在广告牌图像分割系统中用的是SegFormer-B1,在 Jetson Xavier NX 上跑到了18FPS,精度mIoU 78%,客户很满意。
5.3 从零搭建分割项目的checklist
最后分享一个我每次启动新分割项目都会过一遍的清单:
- [ ] 明确任务类型:语义分割/实例分割/全景分割
- [ ] 确定类别定义和标注规范
- [ ] 评估数据量和标注成本
- [ ] 选择backbone和分割头
- [ ] 设计数据增强策略
- [ ] 选择损失函数和类别权重
- [ ] 设置学习率和调度策略
- [ ] 确定评估指标(mIoU、Dice、Boundary F1等)
- [ ] 规划训练资源和时间预算
- [ ] 设计推理优化和部署方案
- [ ] 准备后处理和可视化工具
这个清单看起来简单,但每一条都对应着实际项目中的关键决策点。我见过太多项目因为标注规范不清晰导致返工,或者因为评估指标选错导致模型方向跑偏。
5.4 我个人的架构选型心得
做了这么多分割项目,我最大的体会是:没有最好的架构,只有最合适的架构。FCN虽然老,但在某些简单场景下依然够用;SegFormer虽然新,但数据量不够时反而不如U-Net。
我的选型逻辑通常是:先看数据量,再看实时性要求,最后看精度需求。数据量少就选U-Net系,实时性高就选SegNet或轻量模型,精度要求高且数据充足就上SegFormer或Deeplabv3+。
另外,不要忽视工程细节。同样的架构,数据增强做得好不好、损失函数选得对不对、学习率调得合不合适,最终效果可能差10个点以上。我见过太多人花大量时间尝试新架构,却忽略了最基础的数据清洗和增强,结果模型效果一直上不去。
还有一点:预训练权重非常重要。在ImageNet上预训练的backbone比随机初始化的收敛快得多,最终精度也更高。如果实在找不到合适的预训练权重,至少要用自监督方法(如MoCo、SimCLR)在无标注数据上预训练一下。
最后再分享一个小技巧:用验证集的可视化结果来指导调优。不要只看mIoU这个数字,把预测结果叠加到原图上,看看哪些区域分错了、为什么分错。很多时候,可视化能发现数字指标发现不了的问题,比如某个类别的边界总是偏移、某个光照条件下模型完全失效等。这些洞察比盲目调参有用得多。