从V1到V3+:手把手带你复现DeepLab系列的核心模块(含PyTorch代码)

语义分割作为计算机视觉领域的核心任务之一,其技术演进始终围绕着一个关键矛盾:如何在保持高分辨率特征图的同时扩大感受野。DeepLab系列作为该领域的标杆工作,通过四次迭代逐步解决了这一难题。本文将带您从代码层面拆解每个版本的核心创新,用PyTorch实现从V1的空洞卷积到V3+的编解码架构全过程。

1. DeepLabV1:空洞卷积的首次实践

2015年问世的DeepLabV1首次将空洞卷积(Atrous Convolution)引入语义分割领域。传统CNN通过池化层扩大感受野时会导致特征图分辨率下降,而空洞卷积通过在卷积核中插入零值元素,实现了"不降分辨率增感受野"的效果。

空洞卷积的数学本质 :标准3×3卷积核在输入特征图上滑动时,每个位置计算9个相邻像素的加权和。当空洞率(dilation rate)为2时,卷积核会扩展为5×5(实际参数仍为3×3),但只在间隔1像素的位置进行计算,等效感受野扩大为5×5。

import torch
import torch.nn as nn

class AtrousConvDemo(nn.Module):
    def __init__(self):
        super().__init__()
        # 标准卷积 vs 空洞卷积
        self.conv_std = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)
        self.conv_atrous = nn.Conv2d(3, 64, kernel_size=3, 
                                   stride=1, padding=2, 
                                   dilation=2)  # 空洞率=2
        
    def forward(self, x):
        std_out = self.conv_std(x)
        atrous_out = self.conv_atrous(x)
        print(f"标准卷积输出尺寸: {std_out.shape}")
        print(f"空洞卷积输出尺寸: {atrous_out.shape}")
        return atrous_out

# 测试代码
demo = AtrousConvDemo()
input_tensor = torch.randn(1, 3, 224, 224)  # 模拟224x224输入
output = demo(input_tensor)

注意:padding值需与dilation rate匹配,计算公式为padding = dilation * (kernel_size - 1) // 2

V1的另一个重要设计是修改VGG16网络结构:

  1. 将最后两个maxpool层的stride改为1(避免过度下采样)
  2. 在stage5的所有卷积层应用空洞卷积(rate=2)
  3. 最终输出上采样8倍得到预测结果

2. DeepLabV2:多尺度特征的金字塔策略

DeepLabV2的核心创新ASPP(Atrous Spatial Pyramid Pooling)模块,通过并行使用不同空洞率的卷积来捕获多尺度信息。这种设计灵感来自空间金字塔池化,但完全基于空洞卷积实现。

ASPP模块的四个关键分支

  • 1×1普通卷积(捕获局部特征)
  • 3×3空洞卷积(rate=6)
  • 3×3空洞卷积(rate=12)
  • 3×3空洞卷积(rate=18)
  • 全局平均池化分支(V3新增)
class ASPP(nn.Module):
    def __init__(self, in_channels, out_channels=256):
        super().__init__()
        # 1x1卷积分支
        self.conv1x1 = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU()
        )
        # 不同空洞率的卷积分支
        self.conv3x3_r6 = self._make_aspp_conv(in_channels, out_channels, 6)
        self.conv3x3_r12 = self._make_aspp_conv(in_channels, out_channels, 12)
        self.conv3x3_r18 = self._make_aspp_conv(in_channels, out_channels, 18)
        
        # 全局特征分支
        self.global_avg = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(in_channels, out_channels, 1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU()
        )
        
        self.project = nn.Sequential(
            nn.Conv2d(out_channels*5, out_channels, 1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(),
            nn.Dropout(0.5)
        )
    
    def _make_aspp_conv(self, in_c, out_c, rate):
        return nn.Sequential(
            nn.Conv2d(in_c, out_c, 3, padding=rate, dilation=rate),
            nn.BatchNorm2d(out_c),
            nn.ReLU()
        )
    
    def forward(self, x):
        h, w = x.shape[2:]
        # 各分支处理
        feat1x1 = self.conv1x1(x)
        feat3x3_r6 = self.conv3x3_r6(x)
        feat3x3_r12 = self.conv3x3_r12(x)
        feat3x3_r18 = self.conv3x3_r18(x)
        # 全局特征上采样
        global_feat = self.global_avg(x)
        global_feat = F.interpolate(global_feat, (h,w), mode='bilinear', align_corners=True)
        # 特征拼接
        combined = torch.cat([feat1x1, feat3x3_r6, feat3x3_r12, 
                             feat3x3_r18, global_feat], dim=1)
        return self.project(combined)

实际部署时需要注意:

  • 输入输出通道数需根据backbone调整
  • 当特征图尺寸较小时,过大空洞率会导致卷积退化为1×1卷积
  • 各分支输出需保持相同尺寸才能拼接

3. DeepLabV3:多网格策略与模块优化

DeepLabV3在V2基础上进行了三项重要改进:

3.1 多网格(Multi-Grid)策略 在ResNet的block4中,为每个残差块设置不同的空洞率。例如当基础rate=2且multi_grid=(1,2,4)时:

  • 第一个残差块:rate = 2×1 = 2
  • 第二个残差块:rate = 2×2 = 4
  • 第三个残差块:rate = 2×4 = 8
def make_resnet_layer(block, in_c, out_c, blocks, stride=1, dilation=1, multi_grid=None):
    layers = []
    # 第一个block处理下采样
    layers.append(block(in_c, out_c, stride, dilation))
    
    # 后续block处理多网格
    if multi_grid is None:
        multi_grid = [1]* (blocks-1)
    for i in range(1, blocks):
        layers.append(block(out_c, out_c, 
                          dilation=dilation*multi_grid[i-1]))
    return nn.Sequential(*layers)

3.2 ASPP增强

  • 增加BatchNorm层加速收敛
  • 引入全局平均池化分支捕获图像级语义
  • 移除CRF后处理(实验证明纯CNN结构已能达到更好效果)

3.3 输出步长(Output Stride) 定义输出特征图与输入图像的尺寸比:

  • OS=16:平衡精度与速度(默认)
  • OS=8:更高精度但更耗内存
class DeepLabV3(nn.Module):
    def __init__(self, backbone='resnet50', output_stride=16):
        super().__init__()
        # 根据output_stride设置不同层的dilation
        if output_stride == 16:
            aspp_dilations = [6, 12, 18]
            backbone_dilations = [1, 1, 2]
        elif output_stride == 8:
            aspp_dilations = [12, 24, 36] 
            backbone_dilations = [1, 2, 4]
        
        # 构建backbone(以ResNet为例)
        self.backbone = build_resnet_backbone(dilations=backbone_dilations)
        self.aspp = ASPP(2048, aspp_dilations)  # ResNet最终通道数为2048
        self.decoder = nn.Sequential(
            nn.Conv2d(256, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.Conv2d(256, num_classes, 1)
        )
    
    def forward(self, x):
        h, w = x.shape[2:]
        # backbone提取特征
        features = self.backbone(x)
        # ASPP处理
        aspp_features = self.aspp(features)
        # 分类头
        out = self.decoder(aspp_features)
        # 上采样到原图尺寸
        return F.interpolate(out, (h,w), mode='bilinear')

4. DeepLabV3+:编解码架构与深度可分离卷积

V3+在保持ASPP优势的基础上,引入编码器-解码器结构提升边缘分割精度,同时使用深度可分离卷积大幅减少计算量。

4.1 编解码架构实现 编码器使用DeepLabV3的输出(含ASPP模块),解码器则通过以下步骤逐步恢复空间信息:

  1. 对编码器输出进行4倍上采样
  2. 与backbone的中间特征(如ResNet的conv2层)拼接
  3. 通过3×3卷积融合特征
  4. 再次上采样至原图尺寸
class Decoder(nn.Module):
    def __init__(self, low_level_channels, num_classes):
        super().__init__()
        # 处理低级特征的1x1卷积
        self.conv_low = nn.Sequential(
            nn.Conv2d(low_level_channels, 48, 1),
            nn.BatchNorm2d(48),
            nn.ReLU()
        )
        # 特征融合部分
        self.feature_fusion = nn.Sequential(
            nn.Conv2d(304, 256, 3, padding=1),  # 256+48=304
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.Dropout(0.1)
        )
        self.classifier = nn.Conv2d(256, num_classes, 1)
    
    def forward(self, x, low_level_feat):
        # 处理ASPP输出(4倍上采样)
        x = F.interpolate(x, scale_factor=4, mode='bilinear')
        # 处理低级特征
        low_level_feat = self.conv_low(low_level_feat)
        # 特征拼接与融合
        x = torch.cat([x, low_level_feat], dim=1)
        x = self.feature_fusion(x)
        return self.classifier(x)

4.2 深度可分离卷积优化 将标准卷积分解为:

  1. 逐通道卷积(Depthwise) :每个输入通道单独卷积
  2. 逐点卷积(Pointwise) :1×1卷积整合通道信息
class SeparableConv2d(nn.Module):
    def __init__(self, in_c, out_c, kernel_size=3, stride=1, dilation=1):
        super().__init__()
        # 逐通道卷积
        self.depthwise = nn.Conv2d(
            in_c, in_c, kernel_size, 
            stride=stride, 
            padding=dilation,
            dilation=dilation,
            groups=in_c  # 关键参数:分组数=输入通道数
        )
        # 逐点卷积
        self.pointwise = nn.Conv2d(in_c, out_c, 1)
    
    def forward(self, x):
        x = self.depthwise(x)
        return self.pointwise(x)

参数计算对比:

  • 标准3×3卷积:Cin×Cout×3×3
  • 深度可分离卷积:Cin×3×3 + Cin×Cout 当Cout=256时,参数量减少约8-9倍

完整V3+实现要点

  1. 将ASPP中的普通卷积替换为深度可分离卷积
  2. 解码器中的3×3卷积也使用深度可分离版本
  3. 训练时采用渐进式策略:先训练编码器,再微调解码器
class DeepLabV3Plus(nn.Module):
    def __init__(self, num_classes, output_stride=16):
        super().__init__()
        # Backbone(获取低级和高级特征)
        self.backbone = ResNetBackbone(output_stride)
        # ASPP模块(使用深度可分离卷积)
        self.aspp = ASPP(2048, [6,12,18])
        # 解码器
        self.decoder = Decoder(256, num_classes)  # ResNet的conv2输出256通道
        
    def forward(self, x):
        h, w = x.shape[2:]
        # 获取低级和高级特征
        low_level_feat, features = self.backbone(x)
        # ASPP处理
        aspp_out = self.aspp(features)
        # 解码器恢复细节
        out = self.decoder(aspp_out, low_level_feat)
        # 上采样到原图尺寸
        return F.interpolate(out, (h,w), mode='bilinear')

5. 实战技巧与常见问题

5.1 训练策略优化

  • 学习率设置:初始lr=0.007,采用多项式衰减(power=0.9)
  • 数据增强:随机缩放(0.5-2.0)、左右翻转、颜色抖动
  • 损失函数:交叉熵损失 + 辅助损失(中间层监督)
# 多项式学习率衰减
def adjust_learning_rate(optimizer, epoch, max_epoch, init_lr, power=0.9):
    lr = init_lr * (1 - epoch/max_epoch)**power
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

5.2 常见问题排查

  1. 输出尺寸不匹配 :检查所有卷积层的padding设置,确保满足:

    out_size = (in_size + 2*padding - dilation*(kernel_size-1) -1)/stride + 1
    
  2. GPU内存不足

    • 减小batch size
    • 使用output_stride=16代替8
    • 尝试混合精度训练
  3. 边缘分割不准确

    • 检查解码器是否正确融合了低级特征
    • 增加边缘敏感的数据增强
    • 尝试添加边缘检测辅助任务

5.3 模型轻量化方向

  1. 替换backbone为MobileNetV3
  2. 减少ASPP分支数量
  3. 使用知识蒸馏训练小模型
# MobileNetV3作为backbone的示例
class MobileNetV3Backbone(nn.Module):
    def __init__(self, output_stride=16):
        super().__init__()
        from torchvision.models import mobilenet_v3_large
        original_model = mobilenet_v3_large(pretrained=True)
        # 提取特征层
        self.features = original_model.features[:-1]
        # 调整空洞卷积
        self._adjust_dilations(output_stride)
    
    def _adjust_dilations(self, output_stride):
        if output_stride == 16:
            for m in self.features[15:]:
                if isinstance(m, nn.Conv2d):
                    m.dilation = (2, 2)
                    m.padding = (2, 2)
        elif output_stride == 8:
            for m in self.features[10:]:
                if isinstance(m, nn.Conv2d):
                    if m.stride == (2,2):
                        m.stride = (1,1)
                        m.dilation = (2,2)
                        m.padding = (2,2)
                    elif m.kernel_size == (3,3):
                        m.dilation = (2,2)
                        m.padding = (2,2)
    
    def forward(self, x):
        # 获取低级特征(用于解码器)
        low_level = self.features[:4](x)
        # 获取高级特征
        x = self.features[4:](x)
        return low_level, x
Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐