第一次接触语义分割任务时,我对着传统CNN的输出结果陷入了沉思——为什么这个能准确识别猫咪的模型,却无法告诉我猫咪的轮廓在哪里?这就像请了一位美食家品尝菜肴,他能说出食材却画不出菜谱。2015年Jonathan Long团队提出的FCN(全卷积神经网络)彻底改变了这个局面,让计算机真正学会了"描边"这项技能。
传统CNN在图像分类任务中表现出色,但它的设计存在两个致命缺陷:一是最后的全连接层要求固定尺寸输入,就像强迫所有照片必须裁剪成相同大小;二是池化层不断压缩特征图尺寸,导致空间信息严重丢失。FCN用三个巧妙设计解决了这些问题:首先用卷积层替代全连接层,使网络可以处理任意尺寸输入;其次引入转置卷积进行上采样,逐步恢复特征图分辨率;最后通过跳级结构融合深浅层特征,就像画家先勾勒轮廓再补充细节。
实际测试VGG16作为backbone的FCN-32s时,我发现分割边缘像打了马赛克。这是因为32倍下采样的特征图太小,就像用8x8像素的贴图还原4K照片。这时跳级结构的作用就凸显出来了——当我把pool4层的特征融合进来(FCN-16s),分割结果立即精细了许多,特别是对小型物体的识别明显改善。这验证了浅层特征保留的空间信息对分割精度至关重要。
在VGG16的改造过程中,我把最后三个全连接层全部替换为卷积层:fc6改为卷积核7x7的卷积层,fc7改为1x1卷积,fc8改为1x1卷积且输出通道数等于类别数。这个改动带来两个惊喜:一是输入图像不再受224x224限制,测试时甚至可以输入800x600的图片;二是输出变成了热力图而非单一标签,每个像素都有了发言权。
这里有个容易踩的坑:1x1卷积看似简单,实则承担着通道维度和语义维度的转换重任。在Pascal VOC数据集上,当我将fc8的21维输出(20类物体+背景)直接作为分割结果时,发现某些类别总是混淆。后来在1x1卷积后加入批归一化层,准确率提升了3.2%。这说明全卷积化不是简单替换,需要重新设计各层配合。
第一次实现上采样模块时,我天真地用了双线性插值,结果分割边缘总是模糊。转置卷积才是正解——它让网络自己学习如何"放大"特征图。具体实现时,stride为2的转置卷积配合2x2的卷积核,相当于在特征点间插入可学习的权重。
在PyTorch中这个操作非常简洁:
python复制self.upsample = nn.ConvTranspose2d(
in_channels, out_channels,
kernel_size=2, stride=2
)
但要注意输出可能产生棋盘效应(checkerboard artifacts)。我的解决方案是添加跳层连接,同时使用多尺度损失监督。实验证明,配合L2正则化的转置卷积核,能使上采样过程更稳定。
FCN-8s的成功关键在于它实现了特征金字塔的完美融合。具体操作分三步:首先将pool5层的特征上采样2倍,与pool4特征相加;然后将结果上采样2倍,再与pool3特征相加;最后上采样8倍得到最终输出。这个过程就像考古学家拼接陶器——先用大碎片确定轮廓,再用小碎片补充细节。
我在Cityscapes数据集上做过对比实验:单独使用pool3特征时,交通标志等小物体识别率很高但整体支离破碎;仅用pool5特征则相反,道路区域连贯但丢失细节。跳级结构使mIOU指标从62.1%提升到68.9%,证明多尺度融合确实有效。不过要注意各层特征的归一化处理,否则会出现特征幅值不匹配的问题。
直接端到端训练FCN-8s往往效果不佳,我采用四阶段训练法:先用分类网络预训练权重初始化;然后固定浅层参数,仅训练新增的卷积层;接着解冻部分中间层;最后微调全部参数。这种渐进式解冻策略使验证集loss下降了18%。
在TensorFlow中实现分阶段训练特别方便:
python复制# 阶段1:仅训练新增层
trainable_vars = tf.trainable_variables()
new_vars = [var for var in trainable_vars if 'upsample' in var.name]
train_op = optimizer.minimize(loss, var_list=new_vars)
# 阶段2:添加部分中间层
mid_vars = [var for var in trainable_vars if 'block4' in var.name]
train_op = optimizer.minimize(loss, var_list=new_vars+mid_vars)
交叉熵损失是基础,但有三点优化经验:一是对难样本(如道路边缘像素)施加5倍权重;二是在损失计算前对logits进行温度系数调节;三是添加辅助损失监督中间层输出。在CamVid数据集上,这种改进使人行道分割的F1-score提升了7.5%。
我的损失函数通常长这样:
python复制def weighted_loss(y_true, y_pred):
class_weights = tf.constant([0.1, 1.0, 5.0, ...])
weights = tf.gather(class_weights, tf.cast(y_true, tf.int32))
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=y_true, logits=y_pred/temperature)
return tf.reduce_mean(loss * weights)
FCN对数据量需求很大,但语义分割标注成本高昂。我的解决方案是:首先使用经典几何变换(旋转、翻转),然后应用颜色抖动(亮度±30%,对比度±20%),最后添加随机裁剪。特别重要的是保持图像与标注的同步变换,这个在PyTorch中可以用torchvision.transforms的functional模块精准控制。
测试FCN-8s在医疗图像分割时,发现小病灶经常漏检。这是因为最大下采样32倍时,3mm结节在特征图上不足1个像素。我的改进方案是:修改backbone的stride设置,使用空洞卷积(dilated convolution)保持感受野的同时减少下采样次数。在肺结节数据集上,这使召回率从76%提升到89%。
FCN的逐像素预测缺乏全局考虑,导致分割结果不够连贯。后来我在跳级连接处添加了注意力机制,让网络自动学习区域关联性。具体是在pool4和pool5特征融合前,先通过SE模块(Squeeze-and-Excitation)进行通道重加权,再通过空间注意力图调整像素关系。这个改动让道路场景下的车辆分割连贯性显著改善。
原始FCN在1080p图像上推理需要300ms,达不到实时要求。通过这三步优化:将VGG替换为MobileNetV3、使用深度可分离卷积替代转置卷积、采用TensorRT加速,最终在Titan XP上实现45fps的实时分割。代价是mIOU下降4.2%,但对自动驾驶等场景完全可接受。