从V1到V3+:手把手带你复现DeepLab系列的核心模块(含PyTorch代码)
从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网络结构:
- 将最后两个maxpool层的stride改为1(避免过度下采样)
- 在stage5的所有卷积层应用空洞卷积(rate=2)
- 最终输出上采样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模块),解码器则通过以下步骤逐步恢复空间信息:
- 对编码器输出进行4倍上采样
- 与backbone的中间特征(如ResNet的conv2层)拼接
- 通过3×3卷积融合特征
- 再次上采样至原图尺寸
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 深度可分离卷积优化 将标准卷积分解为:
- 逐通道卷积(Depthwise) :每个输入通道单独卷积
- 逐点卷积(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+实现要点 :
- 将ASPP中的普通卷积替换为深度可分离卷积
- 解码器中的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 常见问题排查
-
输出尺寸不匹配 :检查所有卷积层的padding设置,确保满足:
out_size = (in_size + 2*padding - dilation*(kernel_size-1) -1)/stride + 1 -
GPU内存不足 :
- 减小batch size
- 使用output_stride=16代替8
- 尝试混合精度训练
-
边缘分割不准确 :
- 检查解码器是否正确融合了低级特征
- 增加边缘敏感的数据增强
- 尝试添加边缘检测辅助任务
5.3 模型轻量化方向
- 替换backbone为MobileNetV3
- 减少ASPP分支数量
- 使用知识蒸馏训练小模型
# 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
更多推荐


所有评论(0)