1. 项目概述:FCN全卷积网络实战解析
在计算机视觉领域,语义分割一直是极具挑战性的任务。传统的CNN网络最后通常接全连接层进行分类,会丢失空间信息。而FCN(Fully Convolutional Network)通过将全连接层转换为卷积层,实现了端到端的像素级分类。我第一次接触FCN是在处理遥感图像分割项目时,需要精确识别卫星图像中的道路网络。当时尝试了多种传统方法效果都不理想,直到采用FCN架构才实现了突破性进展。
FCN的核心创新在于:
- 全卷积化:将网络末端的全连接层替换为卷积层,保留二维特征图
- 跳级连接:融合浅层高分辨率特征与深层语义特征
- 转置卷积:实现上采样恢复分辨率
这个项目将带您从零实现FCN-32s、FCN-16s和FCN-8s三个经典变种,使用PyTorch框架在PASCAL VOC数据集上完成训练和评估。不同于大多数教程只展示基础实现,我会重点分享工业级应用中的优化技巧,比如:
- 多尺度训练的数据增强策略
- 类别不平衡问题的解决方案
- 推理阶段的边缘细化技巧
2. 核心原理与网络架构
2.1 全卷积化设计原理
传统CNN如VGG16最后使用全连接层会丢失空间信息。假设输入为224x224的图像,经过5次下采样后特征图变为7x7,如果直接展平为49维向量,原有的二维结构关系就被破坏了。FCN的巧妙之处在于将全连接层视为特殊卷积层:
- 原全连接层4096个神经元 → 转换为7x7x4096的卷积核
- 原1000维分类输出 → 转换为1x1x1000的卷积核
这样处理后,网络输出仍然是二维特征图,只是通道数对应类别数。以PASCAL VOC的21类为例(20个物体类别+背景),最终输出就是HxWx21的特征图。
提示:实际实现时要注意预训练模型的适配。比如加载VGG16预训练权重时,需要将全连接层参数reshape为卷积核形式。
2.2 跳级连接结构解析
FCN通过跳级连接融合不同层级的特征:
- 浅层特征:高分辨率但语义信息弱(如pool3)
- 深层特征:低分辨率但语义信息强(如pool5)
具体实现时有三点关键:
- 对深层特征进行2倍上采样(使用转置卷积)
- 与浅层特征进行逐元素相加(需先1x1卷积调整通道数)
- 重复上述过程直到恢复原图尺寸
以FCN-8s为例:
python复制class FCN8s(nn.Module):
def __init__(self, n_class=21):
super().__init__()
# 骨干网络(使用VGG16前5个block)
self.features = make_layers(vgg16_cfg['E'])
# 分类器(替换全连接层)
self.classifier = nn.Sequential(
nn.Conv2d(512, 4096, 7, padding=3),
nn.ReLU(inplace=True),
nn.Dropout2d(),
nn.Conv2d(4096, 4096, 1),
nn.ReLU(inplace=True),
nn.Dropout2d(),
nn.Conv2d(4096, n_class, 1)
)
# 跳级连接相关层
self.score_pool3 = nn.Conv2d(256, n_class, 1)
self.score_pool4 = nn.Conv2d(512, n_class, 1)
# 上采样层
self.upscore2 = nn.ConvTranspose2d(n_class, n_class, 4, stride=2, bias=False)
self.upscore8 = nn.ConvTranspose2d(n_class, n_class, 16, stride=8, bias=False)
self.upscore_pool4 = nn.ConvTranspose2d(n_class, n_class, 4, stride=2, bias=False)
2.3 转置卷积实现细节
转置卷积(Transposed Convolution)是实现上采样的关键,但初学者常误解其工作原理。它并非真正的逆卷积,而是通过插入零值实现尺寸扩大。具体计算过程:
假设输入为2x2,卷积核3x3,步长2,填充1:
code复制输入: 卷积核:
[1, 2] [a, b, c]
[3, 4] [d, e, f]
[g, h, i]
实际计算步骤:
- 在输入元素间插入零值(步长-1个零)
- 添加外部填充(根据padding参数)
- 执行标准卷积
在PyTorch中实现时需注意:
python复制# 错误示范:忘记设置output_padding会导致尺寸不匹配
nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1)
# 正确写法:当输入尺寸为奇数时需要output_padding=1
nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2,
padding=1, output_padding=1)
3. 完整实现流程
3.1 数据准备与增强
PASCAL VOC数据集包含1464张训练图像和1449张验证图像。为提高模型鲁棒性,我推荐使用以下增强组合:
python复制from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomResizedCrop(512, scale=(0.5, 2.0)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 特别注意:标签图只需几何变换,不能做颜色变换
label_transform = transforms.Compose([
transforms.RandomResizedCrop(512, scale=(0.5, 2.0), interpolation=Image.NEAREST),
transforms.RandomHorizontalFlip(),
transforms.ToTensor()
])
处理类别不平衡的两种实用方法:
- 样本加权:根据类别频率计算权重
python复制def calculate_weights(dataset):
class_count = torch.zeros(21)
for _, label in dataset:
hist = torch.histc(label.float(), bins=21, min=0, max=20)
class_count += hist
return 1 / (class_count / class_count.sum())
- 损失函数加权:使用带权重的交叉熵
python复制criterion = nn.CrossEntropyLoss(weight=class_weights)
3.2 模型训练技巧
训练FCN时我总结出三个关键点:
- 分层学习率设置:
python复制optimizer = optim.SGD([
{'params': model.features.parameters(), 'lr': base_lr * 0.1},
{'params': model.classifier.parameters()},
{'params': model.score_pool3.parameters()},
{'params': model.score_pool4.parameters()},
{'params': model.upscore2.parameters()},
{'params': model.upscore8.parameters()},
], lr=base_lr, momentum=0.9, weight_decay=5e-4)
- 渐进式训练策略:
- 第一阶段:只训练分类器部分(冻结特征提取层)
- 第二阶段:微调整个网络
- 第三阶段:启用全部跳级连接
- 学习率预热与衰减:
python复制scheduler = optim.lr_scheduler.SequentialLR(optimizer, [
optim.lr_scheduler.LinearLR(optimizer, start_factor=0.1, total_iters=5),
optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs-5)
], milestones=[5])
3.3 评估指标实现
除常规的mIoU外,建议实现以下评估指标:
python复制def pixel_accuracy(output, target):
with torch.no_grad():
pred = torch.argmax(output, dim=1)
correct = (pred == target).sum().item()
total = target.numel()
return correct / total
def mean_iou(output, target, n_classes=21):
with torch.no_grad():
pred = torch.argmax(output, dim=1)
ious = []
for cls in range(n_classes):
pred_inds = (pred == cls)
target_inds = (target == cls)
intersection = (pred_inds & target_inds).sum().float()
union = (pred_inds | target_inds).sum().float()
ious.append((intersection / (union + 1e-6)).item())
return sum(ious) / n_classes
4. 实战问题与解决方案
4.1 常见报错排查
- 尺寸不匹配错误:
- 现象:RuntimeError: output with shape ... doesn't match the broadcast shape ...
- 原因:跳级连接时特征图尺寸未对齐
- 解决方案:在转置卷积中添加output_padding参数
- 显存不足问题:
- 现象:CUDA out of memory
- 优化方案:
- 使用更小的裁剪尺寸(如从512降到384)
- 启用梯度检查点
python复制from torch.utils.checkpoint import checkpoint def forward(self, x): x = checkpoint(self.features, x) # ...
4.2 预测结果后处理
原始输出常有边缘锯齿问题,可采用以下优化:
- CRF后处理:
python复制import pydensecrf.densecrf as dcrf
def apply_crf(image, output):
h, w = image.shape[:2]
probs = output.detach().softmax(dim=1)[0].numpy()
d = dcrf.DenseCRF2D(w, h, 21)
U = -np.log(probs + 1e-6)
d.setUnaryEnergy(U.reshape(21,-1))
# 添加颜色和位置相关项
d.addPairwiseGaussian(sxy=3, compat=3)
d.addPairwiseBilateral(sxy=80, srgb=13, rgbim=image, compat=10)
Q = d.inference(5)
return np.argmax(Q, axis=0).reshape(h,w)
- 测试时增强(TTA):
python复制def predict_with_tta(model, image, scales=[0.5, 1.0, 1.5]):
outputs = []
for scale in scales:
sized = F.resize(image, int(scale * image.size[0]))
output = model(sized)
output = F.resize(output, image.size)
outputs.append(output)
return torch.mean(torch.stack(outputs), dim=0)
4.3 模型轻量化技巧
当需要部署到移动设备时,可采用:
- 知识蒸馏:
- 使用大FCN模型作为教师模型
- 训练轻量化的学生模型(如MobileNetV3+FPN)
- 通道剪枝:
python复制from torch.nn.utils import prune
parameters_to_prune = [(module, 'weight') for module in model.modules()
if isinstance(module, nn.Conv2d)]
prune.global_unstructured(parameters_to_prune, pruning_method=prune.L1Unstructured, amount=0.3)
5. 扩展应用与优化方向
在实际项目中,我尝试过以下改进方案效果显著:
- 多任务学习:同时预测语义分割和边缘检测
python复制class MultiTaskFCN(nn.Module):
def __init__(self, n_class=21):
super().__init__()
self.backbone = FCN8s(n_class)
self.edge_head = nn.Sequential(
nn.Conv2d(256, 64, 3, padding=1),
nn.ReLU(),
nn.Conv2d(64, 1, 1)
)
def forward(self, x):
seg_out = self.backbone(x)
edge_out = self.edge_head(self.backbone.features[16]) # pool3特征
return seg_out, edge_out
- 自注意力增强:在跳级连接处添加CBAM模块
python复制class CBAM(nn.Module):
def __init__(self, channels, reduction=16):
super().__init__()
self.channel_attention = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(channels, channels//reduction, 1),
nn.ReLU(),
nn.Conv2d(channels//reduction, channels, 1),
nn.Sigmoid()
)
self.spatial_attention = nn.Sequential(
nn.Conv2d(2, 1, 7, padding=3),
nn.Sigmoid()
)
def forward(self, x):
# 通道注意力
ca = self.channel_attention(x) * x
# 空间注意力
sa = torch.cat([ca.max(dim=1)[0].unsqueeze(1), ca.mean(dim=1).unsqueeze(1)], dim=1)
sa = self.spatial_attention(sa)
return sa * ca
- 半监督学习:利用伪标签扩充训练数据
python复制def generate_pseudo_labels(model, unlabeled_loader, threshold=0.9):
model.eval()
pseudo_data = []
with torch.no_grad():
for img in unlabeled_loader:
output = model(img)
prob, pred = torch.max(F.softmax(output, dim=1), dim=1)
mask = (prob > threshold)
if mask.sum() > 0: # 只保留高置信度预测
pseudo_data.append((img, pred))
return pseudo_data
经过多个项目的实战检验,FCN虽然结构简单,但通过合理的调优和扩展,在工业级应用中仍能表现出色。特别是在计算资源受限的场景下,相比更复杂的DeepLab等模型,FCN提供了更好的性价比。