想象一下,你正在教一个小朋友看图说话。传统的方法可能是让他先看整张图,然后说出图中有什么。但全卷积网络(FCN)更像是一个细心的老师,它会指着图中的每一个小部分,告诉小朋友这是什么——这就是像素级语义分割的核心思想。
FCN是深度学习在图像分割领域的开山之作,它的最大特点就是"全卷积"。传统神经网络通常会在最后使用全连接层进行分类,但FCN把所有层都换成了卷积层。这就好比把原来只能看整张图的"近视眼",变成了可以看清每个像素的"显微镜"。
我刚开始接触FCN时,最让我惊讶的是它的输入尺寸可以任意变化。传统网络因为全连接层的存在,输入图片必须固定尺寸,就像必须把照片剪成统一大小才能放进相册。而FCN就像智能相册,无论照片多大都能处理。这个特性在实际应用中特别实用,比如处理医疗影像时,不同病人的扫描图尺寸往往各不相同。
传统CNN用于分类时,最后通常会接几个全连接层。这就像让网络看完整个画面后做一个总体判断。但要做像素级分割,我们需要网络对每个小区域都做出判断。
FCN的聪明之处在于,它把全连接层也改成了卷积层。举个例子,假设原来有个全连接层需要7×7×512的输入,输出4096维。改成卷积层后,就变成了用7×7的卷积核,输入通道512,输出通道4096。这样改造后,网络就变成了真正的"全卷积"结构。
我在第一次实现这个转换时,发现有个很妙的地方:这样的改造不仅保持了网络的表达能力,还让它具备了处理任意尺寸输入的能力。就像把固定大小的筛子换成了可伸缩的渔网,不管鱼多大都能捞。
FCN处理图像时,会先通过卷积和下采样让特征图越来越小,最后又要把它恢复到原图大小进行预测。这个过程的关键就是转置卷积(也叫反卷积)。
转置卷积的工作原理有点像拼图。想象你把一张大图切成小块后,现在要把它重新拼回去。转置卷积就是那个拼图高手,它知道如何把小特征图"放大"回原尺寸。不过要注意的是,这种放大不是简单的插值,而是通过学习得到的上采样方式。
我做过一个对比实验:用双线性插值上采样和转置卷积上采样,结果后者在边缘细节上明显更胜一筹。这是因为转置卷积的参数是可学习的,网络会自动调整到最适合当前任务的上采样方式。
FCN-32s是最简单的版本,直接从32倍下采样的特征图通过一次转置卷积恢复到原图尺寸。这就像把一张照片缩小32倍后再放大回原尺寸——虽然整体轮廓还在,但细节损失严重。
在实际应用中,我发现FCN-32s适合对细节要求不高的场景。比如只需要大致区分前景背景的任务,它的速度最快,计算量最小。但要做精细分割,比如区分不同器官的医疗影像,就显得力不从心了。
FCN-16s聪明地结合了深层和较浅层的特征。它先把特征图上采样2倍,再与pool4层的特征相加,最后再上采样16倍。这就好比画家先画轮廓,再添加一些细节,最后完成整幅画。
我在一个街景分割项目中对比过两种结构,FCN-16s在保持较快速度的同时,对小型物体(如交通标志)的分割效果明显提升。特别是处理远处的小物体时,准确率比FCN-32s提高了约15%。
FCN-8s进一步融合了更多层的特征:在FCN-16s的基础上,再融合pool3层的特征。这相当于在绘画时不仅考虑整体构图,还关注更细微的笔触。
测试FCN-8s时有个有趣的发现:它对边缘的处理特别细腻。比如分割树叶时,FCN-32s可能会把整棵树当成一个色块,而FCN-8s能区分出单个叶片的轮廓。不过这种精细是有代价的——计算量增加了近30%,推理速度明显下降。
深层特征就像站在山顶看风景——视野开阔,能把握全局,但看不清细节;浅层特征则像用放大镜观察——细节丰富,但容易"只见树木不见森林"。FCN的特征融合就是要把这两种视角结合起来。
我在一个卫星图像分割项目中深刻体会到这一点。单独使用深层特征时,能准确区分城市和森林区域,但道路网络模糊不清;而只用浅层特征时,能看清道路线条,却经常把小型林地误认为建筑群。特征融合后,这两个问题都得到了明显改善。
FCN论文中采用的是简单的相加(skip-add)方式。具体来说,就是把深层特征上采样后,与浅层特征逐元素相加。这个过程需要注意两点:一是要调整通道数一致,二是要调整空间尺寸匹配。
实现时我踩过一个坑:直接相加会导致浅层特征被"淹没"。后来发现应该先用1×1卷积调整浅层特征的通道数,同时对其做适当的裁剪或padding。这个小技巧让模型性能提升了约8%。
python复制import torch
import torch.nn as nn
import torchvision.models as models
class FCN32s(nn.Module):
def __init__(self, num_classes):
super().__init__()
# 使用预训练的VGG16作为特征提取器
vgg = models.vgg16(pretrained=True)
features = list(vgg.features.children())
# 前4个卷积块(到pool4)
self.features1 = nn.Sequential(*features[:24])
# 第5个卷积块(pool5)
self.features2 = nn.Sequential(*features[24:])
# 分类器改为全卷积
self.classifier = nn.Sequential(
nn.Conv2d(512, 4096, kernel_size=7, padding=3),
nn.ReLU(inplace=True),
nn.Dropout2d(),
nn.Conv2d(4096, 4096, kernel_size=1),
nn.ReLU(inplace=True),
nn.Dropout2d(),
nn.Conv2d(4096, num_classes, kernel_size=1)
)
# 转置卷积上采样32倍
self.upsample = nn.ConvTranspose2d(
num_classes, num_classes,
kernel_size=64, stride=32,
padding=16, bias=False
)
def forward(self, x):
x = self.features1(x)
x = self.features2(x)
x = self.classifier(x)
x = self.upsample(x)
return x
这个实现有几个关键点:首先利用预训练的VGG16作为特征提取器,然后把最后的全连接层替换为卷积层,最后添加转置卷积进行上采样。我建议初学者可以先用这个基础版本理解FCN的核心思想。
训练FCN时,学习率的设置特别重要。因为使用了预训练模型,我通常会把特征提取部分的学习率设小些(如1e-5),而新添加的层学习率大些(如1e-3)。这样可以既保留预训练模型学到的通用特征,又让新层快速适应新任务。
另一个实用技巧是在损失函数中加入类别权重。语义分割数据经常存在类别不平衡问题,比如街景中天空和道路的像素远多于交通标志。通过给少数类别分配更高权重,可以显著提升模型在小物体上的表现。
虽然FCN开创了深度学习语义分割的先河,但它也有明显不足。最大的问题是感受野固定,难以适应不同大小的物体。比如在同一个场景中,远处的汽车和近处的汽车在图像上尺寸差异很大,但FCN对它们使用的感受野是相同的。
我在实际项目中发现,对于非常小的物体(小于32×32像素),FCN的准确率会明显下降。一个改进方法是加入注意力机制,让网络可以动态调整对不同区域的关注程度。另一个方向是引入多尺度特征融合,就像FCN-8s那样,但可以设计更复杂的融合方式。