
大家做深度学习这么久肯定绕不过一个坎语义分割。不管你是做自动驾驶、医学影像分析、遥感图像处理还是抠图、人像分割最终都得跟“给每个像素分类”这件事打交道。而一聊到语义分割有两座绕不开的大山一个是全卷积神经网络FCN另一个就是U-Net。很多时候大家看论文、看博客网络结构图能看懂但真正到自己搭模型、调参数、做训练的时候就会冒出各种奇怪的问题为什么我的分割边界这么糊为什么小目标全丢了为什么loss降不下去为什么显存一下就爆了这些问题的根源往往就藏在FCN和U-Net的设计细节里。这篇文章我结合自己实际训练分割模型的经验把这两个经典网络从原理到实操彻底拆一遍把自己踩过的坑也一并交代清楚希望能帮你真正弄懂它们而不是只会调库。先说清楚这篇内容适合谁。如果你刚接触图像分割想知道FCN到底“全卷积”在哪、U-Net为什么长这样这篇文章完全跟着走一遍就能建立起整体认知如果你已经跑通了分割模型但在调优阶段卡住了这篇文章里的损失函数选择、上采样细节、显存优化技巧也会有点用。我不打算把论文里的公式全抄一遍而是尽量用“这是干什么用的”“为什么这么做”“实际训练中怎么选”的角度来讲。1. 先从FCN说起它是怎么把分类网络变成分割网络的1.1 分类网络和分割网络之间隔着一道“全连接层”的墙想理解FCN得先理解一个非常朴素的问题在FCN出现之前为什么大家做不了像素级的分类当时的图像分类网络比如VGG、GoogLeNet结构上可以分两段前面是一堆卷积和池化把一张大图逐步压成很小的feature map这一步叫特征提取后面接几个全连接层把特征图拉成一长条向量最后做分类。这套结构在ImageNet分类任务上表现很好但完全没法直接用于分割因为全连接层把空间位置信息整个打平了输出是一个类别概率而不是一张跟输入大小一致的像素级标签图。你可以这样理解分类网络只告诉我们“这张图里有一只猫”但完全不说猫在哪里、轮廓长什么样。分割任务需要的是“这张图里每个像素分别属于猫、背景、还是别的东西”。想要达到这个目标网络结构必须保留空间分辨率或者说必须设计一种方式把特征图恢复到像素级别。FCN的突破性想法非常直接把分类网络尾部的全连接层全部扔掉换成卷积层。所以它叫“全卷积神经网络”意思就是整个网络从头到尾只有卷积、池化、上采样这类操作没有任何全连接层。这样一来网络输出就不再是一个固定长度的类别向量而是一张有空间维度的分数图每个位置对应原图上一个区域逐像素做分类。这一步替换看起来简单但它带来一个连锁问题网络中间经过多次池化特征图已经缩得很小。以VGG16为例输入224x224的图到最后一个卷积层特征图只有7x7如果直接用这个分辨率做分割输出就是一张7x7的粗糙标签图根本无法还原到原图尺度。所以FCN的核心难点变成了怎么把低分辨率的高层语义特征恢复成高分辨率的像素级预测。1.2 上采样方式的选择转置卷积和插值到底应该怎么选恢复分辨率这个操作学名叫做“上采样”。FCN里用到了一个非常核心的层叫转置卷积Transposed Convolution也叫反卷积。很多新手第一次看到转置卷积会被绕晕其实它理解起来不复杂。普通卷积是把输入的一个区域“压缩”成输出上的一个点而转置卷积做的事情正好反过来把输入上的一个点“扩展”成输出上的一个区域。给它一个输入值它乘以卷积核的权重再把结果分散写到输出的多个位置上所以它能增加特征图的空间尺寸。这个操作里面有一个很重要的细节——转置卷积是有学习参数的卷积核里的权重可以通过反向传播更新也就是说网络可以自己学习用什么样的模式把特征图放大。但实际使用中转置卷积有个知名的副作用会产生棋盘格伪影就是输出图上出现那种重复的方块状纹理尤其是在放大倍数比较高的场景里特别明显。我自己第一次用FCN训练分割模型结果图放大了细看边缘区域全是隐隐的格子状条纹找了好久才定位到原因——转置卷积的权重更新过程中相邻位置之间会出现不一致的响应频率越高的区域越明显。针对这个问题现在工程上已经形成了一些更稳的替代方案。最常用的做法是“双线性插值上采样 普通卷积”也就是先用双线性插值把特征图放大再送进一个普通卷积层做精细的特征变换。这个组合既能有效放大分辨率又不会带来棋盘格伪影代价是需要多一个卷积层的参数量。还有一个方案是用子像素卷积PixelShuffle它在超分辨率任务里用得比较多分割里相对少一些。如果让我给建议我会说如果做快速原型验证直接用转置卷积也够用如果要做最终效果比较好的模型优先考虑双线性插值加卷积的方案。这里不光是伪影问题还有一个好处是双线性插值没有学习参数减少了训练时的变量稳定性和收敛速度都更好。1.3 FCN的三层结构浅层细节和深层语义如何融合FCN论文里提出了三种融合模式分别叫FCN-32s、FCN-16s和FCN-8s。这个命名中后面的数字表示最终预测相对原图的下采样倍数也可以理解成“缩放比例是1/多少”。这里的关键是特征融合策略。FCN-32s最简单直接把最后一层特征图做32倍上采样回原图大小完成预测。它的优点是结构简单缺点是丢失了太多细节信息。因为高层特征图经过多次池化小物体的位置信息早就没了直接放大之后就只剩一团糊的边缘。FCN-16s做了一次融合把最后一层特征图先上采样2倍然后和之前第4次池化得到的特征图相加再一起上采样回原图。这样做的好处是较浅层的特征带着比较清晰的边缘和位置信号能补充一些细节。FCN-8s更进一步在第16s融合的基础上再把第3次池化层的特征图也加入融合这样浅层信息占比更大输出分辨率从32倍变成了8倍下采样。实际效果上FCN-8s是三种方案里分割质量最好的因为它保留了更准确的边界位置同时还有足够的高层语义。我在实际实验中也明显感受到了这个趋势。用同一份训练数据、同一个后端特征提取网络三种结构的mIoU几乎总是FCN-8s FCN-16s FCN-32s。这个结论很重要它告诉我们一个设计原则高层语义负责“知道是什么”浅层特征负责“知道在哪里”两个都想要就必须在结构上做跨层融合。不过有一点必须提醒FCN的融合方式比较粗放就是直接相加element-wise sum。具体使用中浅层特征和深层特征的数值量级往往不一致直接相加可能导致数值波动失衡。我自己实验时发现如果在相加之前分别对两个分支做一个BatchNorm归一化训练的稳定性会提高不少。这一点在原始论文里没细说但在实际调优时很值得一试。2. U-Net一拍即合的编解码器对称结构2.1 为什么说U-Net是FCN的“进化版”如果说FCN开启了语义分割的先河那么U-Net就是把语义分割推向成熟的关键模型。它最早是为医学图像分割设计的但后来几乎所有领域的分割任务都验证了它的有效性。U-Net的结构看起来像一个U形字母左边是逐渐下采样的编码器Contracting Path右边是逐渐上采样的解码器Expanding Path中间底部是bottleneck层左右两边用跳跃连接Skip Connection衔接。编码器的每一层downsample之后特征图分辨率减半通道数翻倍解码器的每一层upsample之后特征图分辨率翻倍通道数减半。这个结构与FCN最大的不同在于跳跃连接的处理方式。FCN的跨层融合是直接相加而U-Net用的是拼接concatenation把编码器的浅层特征图和解码器的深层特征图沿着通道维度拼在一起。拼接比相加保留的信息量更大解码器可以同时看到“不同抽象级别”的信息既知道全局语义又知道边界细节。我举个直观的类比编码器相当于一个经验丰富的医生能一眼看出画面里是一个器官边界解码器相当于一个还在学习的实习生需要看着医生说“这块区域的轮廓比较模糊注意仔细描边”。如果只有解码器自己干活很多边缘会画错但有了编码器提供的轮廓草稿解码器的工作难度大大降低。拼接操作就是把医生的建议原封不动递给实习生而不是只给一个抽象结论。2.2 编码器下采样的每一步都做了什么U-Net的编码器早期版本是基于卷积层加最大池化构建的。每一层基本由两个3x3卷积加ReLU激活组成然后通过2x2最大池化把特征图尺寸减半。这里两个连续卷积的设计是有讲究的一个卷积能看到的感受野太小两个串联之后第二层每个输出位置已经可以看到输入层5x5范围的信息感受野更大特征表达能力更强。下采样过程中的通道数变化也有规律。以原始U-Net论文的配置为例第一层64个通道第二层128第三层256第四层512到bottleneck层是1024。通道数增长是有原因的分辨率减半意味着每个像素对应的“原始区域”变大所以用更多通道来承载更丰富、更抽象的语义信息。这个设计思想在后面几乎所有分割模型中都被保留下来。这里有一个实际训练中的细节通道数越多显存占用和计算量就越大。很多人在自己的数据集上复现U-Net时发现显存根本不够用于是动辄把通道数缩减一半或者四分之一。这个方法可行但要注意缩减之后模型的表达能力确实下降了。我的建议是先在原始通道数配置下跑一个小尺寸输入确定模型能收敛、效果逻辑正常再根据显存限制逐步缩减不要一上来就大刀阔斧地砍。2.3 解码器和跳跃连接的配合哲学解码器的工作是把低分辨率语义图逐步还原为高分辨率预测。每一步先对来自上一层的特征图做上采样原始U-Net用转置卷积也可以换成双线性插值然后把上采样结果与编码器对应层的特征图拼接再通过两个卷积层做融合与精修。这里有一个关键点拼接操作要求两个特征图的空间尺寸完全一致所以上采样的倍数必须算准确。常见做法是直接以编码器某层输出的大小为目标让上采样后的特征图和它一样大。如果你用的是双线性插值注意align_corners这个参数会影响结果不同框架之间的默认设置不一样处理不好会导致特征图尺寸偏差一个像素训练时报维度不匹配的错。这类错误在调试时非常浪费时间建议看到维度报错第一反应就去检查这个位置。跳跃连接数量也有讲究。原始U-Net是每一层都做跳跃连接但实际工程中可以每次都连也可以隔层连。如果你的任务边界简单、语义明确隔层连可以减少计算量如果任务复杂、目标尺度变化大建议保留完整跳跃连接。我自己在实验中发现完整跳跃连接对小目标的保留能力明显更强因为浅层位置信息仍然通过连接路径直接传到了解码器。2.4 U-Net家族里那些好用的变体经典的U-Net只是起点后来围绕它生出了一整个家族。先提几类我认为在实际项目中很常用的ResUnet就是把编码器的卷积块换成残差结构。残差连接能有效缓解网络加深时的梯度消失问题所以ResUnet通常可以堆得更深训练也更稳定。Attention U-Net在跳跃连接处增加了注意力门控模块让网络自动学习哪些浅层特征值得被重点利用。它特别适合目标区域占比小、背景占比大的场景比如病灶分割、小目标检测。unet改名叫做嵌套U-Net核心变化是重新设计了跳跃连接的路径让不同层的特征在解码前就做了多次融合。它在医学分割比赛里拿了很好的成绩不过模型体积和训练时间也相应变大了。还有一个很常见的工程操作是换backbone比如把U-Net的编码器换成ResNet34、ResNet50等预训练模型。这样能借助大数据集上预训练的特征迁移能力在小数据集上往往能获得明显提升。这是我在大多数真实项目中都会采用的做法因为从头训练一个U-Net编码器在数据量小的时候很容易过拟合。3. 从FCN到U-Net两者的关键差异和选型思路3.1 结构设计上的四大差异点FCN和U-Net虽然都是编解码思想下的产物但设计取向有非常明显的分歧。我用表格把这几个关键差异整理出来方便对照对比维度FCNU-Net网络形态非对称只有编码器下沉后直接上采样完全对称的U形编解码跨层连接FCN-16s/FCN-8s做浅层特征相加融合每一层都用拼接方式跳跃连接上采样方式转置卷积后续可替换转置卷积或插值卷积特征融合策略元素级相加通道维度拼接信息保留能力较浅层细节有限浅层细节信息完整边界准确初始应用场景通用图像分割医学图像分割因数据少强语义结构参数量和计算量相对较小相对较大这张表的最核心启示是U-Net用更复杂的结构换来了更强的细节还原能力。FCN更适合结构相对简单、边界不是主要瓶颈的场景U-Net在复杂边界、小目标、细节丰富的场景里几乎是默认选项。3.2 实际选型时我会怎么做判断那么问题来了项目里到底用哪个这没有绝对答案但有一些经验可以分享。如果你处理的是遥感图像里的建筑物提取、农业地块分割、或者工业质检里的区域分割这些场景往往需要比较精细的边缘我的第一选择基本都是U-Net或者它的变体。因为这类任务对你的评分指标往往基于交并比边界画得准不准直接决定IoU高低而U-Net的浅层特征拼接对边界精度的提升非常明显。如果你的场景是图像级的大区域分类比如一个大路面图像只需要分辨“哪里是车道哪里是人行道”不要求细致边缘FCN是更轻量的选择。它结构简单、参数量少、训练速度快还容易部署到算力不足的设备上。还有一个很实用的参考维度是训练数据量。U-Net的跳跃连接和拼接结构在数据量小的时候也能稳定收敛这在医学图像场景里早就被验证了FCN的融合方式相对简单数据量不足时容易出现过拟合或者特征利用率不高的问题。所以小数据集任务闭眼选U-Net基本不会错。3.3 输出层的设计每类一个通道还是每像素一个类别无论用FCN还是U-Net网络末尾都需要一个输出层把特征变成分割结果。通常的做法是最后一层特征图跟着一个1x1卷积把特征图的通道数变成类别数。然后有两种常见预测方式。第一种方式是每个类别使用独立的通道也就是输出一个C通道的热力图每个通道对应一类每个空间位置在哪个通道上的值最大就预测为哪个类别。这种方式在物体分类思维里非常自然。第二种方式是直接把每个空间位置预测成对应类别标签这在某些轻量部署中会用到但训练时一般不会直接输出类别整数因为整数标签没法做梯度回传。常见的做法还是第一种然后在最后做argmax取类别。关于输出层的激活函数有一个经常被问到的点为什么分割模型最后一层不做sigmoid或者softmax实际上训练时我们会在损失函数里内置这个操作比如交叉熵损失自带softmax而模型输出层只是返回线性分数。如果你在模型末尾手写一个softmax后再喂给损失函数可能会遇到数值稳定性的问题反而把梯度弄数值化了。保持输出层为线性分数是最稳妥的工程习惯。4. 实操全过程从数据准备到训练一个能用的分割模型4.1 数据标注和预处理比想象中更影响效果分割模型吃的训练数据是成对的一张原图一张同尺寸的标签图标签图的每个像素值对应一个类别索引。做数据准备时我遇到过几个特别容易踩的坑。第一个坑是类别不平衡。如果目标区域只占整张图像的5%背景占95%模型很容易学会“全部预测为背景”这种偷懒策略因为这样也能拿到很高的准确率。解决办法可以分几个方向用带权重的交叉熵损失对前景类别赋更高的权重或者用Dice损失它直接针对区域重叠程度优化天然对前景占比不敏感还有一个更进阶的方案是组合损失比如Dice损失加交叉熵损失一起使用我实验下来效果最好的是这种组合方式。第二个坑是标签图和原图的空间对齐。有些标注工具导出的标签图会因为缩放、裁剪操作和原图产生半像素偏移尤其是训练数据集较大的时候这种问题不容易被发现但训练到后期loss曲线就降不下去呈现一个平缓的高原。排查方法很简单写一个可视化脚本把原图和标签图叠加显示看看地物边缘是否完全贴合。我每次拿到新的数据集都会先做这一步。第三个坑是数据增强的参数需要谨慎。很多图像分类里常用的增强方式比如随机旋转、反转、颜色抖动在分割任务里不是都能直接用。必须注意对图像做几何变换时标签图必须同步做完全相同的变换否则标签和图像错位模型永远学不对边所以代码里要复用同一套随机参数来变换两个图。颜色抖动是可以只用于原图的因为标签不受颜色影响。4.2 损失函数的选择逻辑和常见组合损失函数对分割结果的影响很多时候比网络结构更大。这里把最常用的几种说清楚。交叉熵损失逐像素独立计算分类误差对每个像素一视同仁。类别不平衡时效果差但梯度计算稳定收敛速度尚可。Dice损失定义是两倍预测和真实区域的重叠面积除以两个区域面积之和它直接优化区域的重叠程度对小目标和类别不平衡场景特别友好。但它有个问题当目标区域非常小时损失曲线可能抖动剧烈因为分母很小、梯度变化被放大需要较小的学习率。带边界惩罚的损失函数这类设计通常是在交叉熵基础上对边界像素额外加权让模型更关注边界。比如可以用图像膨胀获取边界区域对边界像素权重乘以一个大于1的系数。我个人最常使用的组合是“交叉熵 Dice损失”两者按比例加和系数比如设为1比1或者0.5比0.5。这个组合的思路是交叉熵提供稳定的逐像素梯度Dice损失提供更明确的区域重叠信号互相补充。踩过一次深坑只用Dice损失训练医学图像分割前期loss剧烈抖动后期又过拟合换成组合损失后效果稳定很多。4.3 训练策略学习率、batch size和显存间的平衡分割模型的训练目标比普通分类更复杂需要更大的显存很多参数选择也因此受限。这里有几个重要的工程经验。Batch size在分割任务中应尽量取大但显存往往不让。常用策略是使用梯度累积相当于用多个小批次反向传播累积梯度后再更新参数可以在显存有限的条件下等效增大batch size。我实际操作时通常把batch size设为2到4梯度累积步数设为4到8等效batch size就能达到16左右效果比直接小batch训练明显稳定。学习率方面分割模型比分类模型更容易出现后期震荡。推荐使用学习率衰减策略比如阶梯衰减或者余弦退火。个人经验是余弦退火在分割模型上效果比较稳后期loss曲线能更平滑地收敛到较低水平。还有一个必须警惕的问题是过拟合。分割模型的参数量大在小数据集上非常容易过拟合。常用的防护策略包括数据增强、Dropout、Early Stopping、以及使用预训练编码器。其中预训练编码器是最有效的一招相当于让网络一开始就携带了从大数据集学来的通用特征。4.4 评估指标为什么accuracy在分割任务上不够用很多初学者上来就用accuracy评估分割效果然后发现分数高得吓人却看不出模型好坏。考虑一个分割任务里目标占5%的情况就算模型把整张图都预测成背景accuracy也是95%看上去很“好”。显然这个指标完全无法反映模型真实能力。语义分割领域最核心的指标是IoUIntersection over Union计算预测区域和真实区域交集除以并集。每个类别单独计算IoU再求平均就是mIoU这是各种分割论文和比赛里的标准指标。还有一个常用指标叫Dice系数和IoU正相关计算方式是两倍交集除以两个集合面积之和很多医学分割论文学者习惯用它。我自己在项目汇报中一般同时报mIoU和Dice因为对于类别类别严重不平衡的任务mIoU更能反映多类别的综合表现而Dice能突出对小目标的敏感度。训练过程中的验证集监控也是重点关注mIoU它比loss更直观。评估的另一个细节是推理时输出的是每个类别的概率图要根据阈值或argmax得到最终标签图再与真实标签图逐像素比对。常见错误是用了不同尺寸的标签评估或者忘记处理边缘像素导致指标偏低这类问题需要提前用可视化脚本排除。5. 实际项目里那些折腾人的问题与排查记录5.1 边缘为什么总是模糊、丢失细结构边缘模糊是分割模型最常见的问题之一原因可能出在多个环节。如果是FCN可能是浅层特征融合不足处理办法是尝试更深的融合策略或换U-Net类结构。如果是U-Net但依然模糊优先检查跳跃连接是否完整保留浅层高分辨率特征以及在解码部分是否还有卷积层在精修边缘。另外一个容易被忽视的原因是把数据增强里的旋转角度设得过大。过大的旋转会导致标签图里的小结构因为插值被抹平模型学的目标本身就不清晰输出自然模糊。我的经验是几何增强的幅度保持适中不要为了强行增大多样性而让标签失真。5.2 小目标分割效果差、容易丢失怎么办在遥感图像和医学图像里小目标多而小经常出现整个目标被模型忽略的烦恼。排查思路大致如下第一检查输入分辨率。如果原图很大被压得很小一个小目标可能只占几个像素几乎不可能被正确分割。方案是做大图切片训练——把大图裁成512x512或1024x1024的切片保持目标原始像素大小再训练模型。这个方法我在遥感分割项目里用了无数次效果提升非常明显。第二检查损失函数。如果用的是纯交叉熵小目标很容易被淹没在背景里建议换成Dice损失或组合损失。第三使用多尺度输入训练。推理时可以对原图做几个尺度的缩放分别推理再把预测结果融合。这个策略能显著提升小目标的检测率代价是推理时间变大。5.3 显存不够用从模型结构到推理阶段的全面优化显存不足是分割任务里反复出现的恶梦尤其当你尝试大图输入或者大通道数模型的时候。我提供几条实际可落地的措施按性价比从高到低排列。输入图片尺寸切分是立竿见影的办法。不要试图一次性把大图塞进网络而是做一个滑窗切片推理最后拼回大图。1536x1536以上的图显存不够基本就只能走这条路。训练时也可以切片训练模型会适应固定尺寸输入推理时再根据情况做重叠切片拼接解决边缘效应。更换backbone同样有效。把VGG16换成ResNet34或更轻量的MobileNet变体能显著减少参数量和显存占用代价是精度可能小幅下降。训练时可以用大模型蒸馏出的伪标签来提升轻量模型这是后话。开启混合精度训练是很多人忽略的做法。现在主流框架都支持自动混合精度能把显存占用降低一半左右训练速度往往还会加快。只要数据类型支持强烈建议开。最后是推理阶段用小尺寸切片。推理时不需要保存中间梯度但显存占用依旧来自激活值切片推理是最直接的手段。如果整图推理显存不够就采用重叠切片加边缘融合拼回去效果基本一致。5.4 类别不平衡严重只靠调整权重还不够当某些类别占比极小的时候我的处理顺序是这样的先换损失函数Dice或组合损失再考虑类别权重然后是数据层面做重采样或者合成少数类别样本最后才是修改网络结构。千万不要一开始就试图加复杂模块结构改动成本高收益往往不明确。重采样的具体操作包括两种一种是对整张图的类别比例做采样让每batch内各类别比例尽量均衡另一种是做前景裁剪对包含小目标样本的区域做局部放大。后者在医学图像和遥感图像中效果极好因为它让模型看到了更丰富的小目标细节。5.5 一个完整的训练配置示例作为参考把我最近一次做道路分割实验的配置放在这里不算最优但足够稳。框架选PyTorch模型是ResNet34编码器的U-Net输入尺寸是512x512batch size是4梯度累积4步优化器是AdamW初始学习率1e-4学习率调度用余弦退火训练语料是4000张标注图。损失函数取“0.5倍交叉熵 0.5倍Dice”训练60个epoch。评估指标在验证集上mIoU能到0.72在测试集上0.68。前面试过纯Dice和纯交叉熵效果都没有这个组合好。这个配置有个需要注意的地方AdamW的权重衰减不能设太大我用的5e-4太大会导致深层特征学不动太小则有过拟合风险。学习率1e-4在分割任务里属于适中范围如果batch size较大可以适当提升到2e-4如果数据很少则要降低到5e-5附近。6. 关于FCN和U-Net我一直想说的几个经验总结从FCN到U-Net核心就是理解两个概念上采样和跨层连接。上采样告诉你低分辨率特征怎么变回高分辨率跨层连接告诉你如何在不丢失位置细节的前提下获得高层语义。把这两个概念吃透后面再看DeepLab、PSPNet、SegFormer这些后续模型时会发现它们都只是在解决不同侧面的问题空洞卷积是为了增大感受野但不降低分辨率金字塔池化是为了捕捉多尺度上下文Transformer结构则是换了一种全局建模方式。但底层的“编码-解码”和“多尺度融合”思想都是相通的。我个人在实际操作中最大的体会是不要把网络结构看得太重。同样的数据、同样的训练策略微调损失函数和输入预处理方式带来的收益往往比换个更新奇的网络大得多。结构化创新是热点但工程落地时踏踏实实调好数据、损失函数、训练策略才是稳定出效果的关键路径。最后再分享一个小技巧每次训练结束后除了保存模型的权重文件我还会保存一份训练集的浅层特征可视化图以及几组验证集图像的前后对比图。一旦后续发现模型效果下降这些材料能帮你快速定位是新数据的问题还是模型本身的问题。这个小习惯让我避开了很多“效果突然变差又找不到原因”的局面建议大家也试一试。