​DeepLabv3+语义分割代码解析

1.Pascal VOC 2012

Pascal VOC (Visual Object Classes) 2012 数据集是计算机视觉领域具有里程碑意义的公开基准数据集,以其全面性、高质量标注和在众多任务上的广泛应用而著称,被广泛用于模型训练、评估与比较研究,尤其作为图像分类、目标检测和语义分割等核心任务的经典基准。

1. 核心特性:多任务基准

Pascal VOC 2012 的核心价值在于其多任务性。它并非针对单一任务设计,而是为多种计算机视觉任务提供了丰富且一致的标注:

  • 图像分类 (Image Classification):​​ 判断图像中存在的物体类别(20类)。
  • 目标检测 (Object Detection):​​ 定位并识别图像中的目标物体,提供物体的边界框 (Bounding Boxes)​​ 和类别标签
  • 语义分割 (Semantic Segmentation):​​ 为图像中的每一个像素分配类别标签(包含20类物体和背景)。这也是本文聚焦的核心任务。​​
  • 其他任务:如动作识别 (Action Recognition)​​ 等。
    丰富的标注(类别标签、边界框、像素级分割掩码)使其成为评估模型在多种视觉理解能力上的通用性强基准。

2. 数据组成与目录结构​

数据集解压后几个文件夹分别作用:

  • JPEGImages/:存储数据集所有原始 .jpg 格式图像文件,内容涵盖包含20类目标的多样化真实世界场景。
  • Annotations/​:​ 存储 ​XML格式​ 的目标检测标注文件。每个图像对应一个 .xml 文件,详细记录其包含物体的类别边界框坐标 (如 xmin, ymin, xmax, ymax)​​ 以及其他属性(如是否截断、是否困难样本等)。
  • SegmentationClass/​:​ 存储标准的像素级语义分割标注图(伪彩色图,每个像素值对应预设类别)。
  • SegmentationClassAug/ (关键): 存储语义分割任务的增强版标签​(由第三方基于原始数据扩展)。本文将使用此目录下的标注进行训练,因为它提供了比原始SegmentationClass更多的标注图像。
  • ImageSets/:​ 包含划分不同任务的图像列表:
    • ImageSets/Main/​:​图像分类任务的训练/验证集列表(按类别划分)。
    • ImageSets/Segmentation/:​ (本文使用) 语义分割任务的 train.txtval.txt 和 trainval.txt 文件,列出用于训练和验证的图像文件名(不含扩展名)。
    • 其他任务对应目录:如 Layout/Action/ 等。
  • 其他目录:包含其他任务(非本文重点)的特定数据或标注。

3. 数据集划分与使用**​

  • 标准划分:​​ 数据集一般会明确划分为训练集 (train)​验证集 (val)​​ 和测试集 (test)​
  • 本地训练与验证:​​ 在本地进行模型训练和调优时,仅可使用训练集 (train) 和验证集 (val) 或其合并集 (trainval) 以及对应标注 (AnnotationsSegmentationClassAug)​
  • 测试集评估 (重要限制):​​ Pascal VOC 2012 官方未公开测试集的真实标签 (Ground Truth)​。对最终模型在测试集上的性能评估,​必须将模型的预测结果提交至官方评测服务器,由其使用私有的测试标注进行计算并反馈评估指标(如语义分割的 mIoU)。

4.语义分割任务目标与示例

本文的核心任务是语义分割。目标是训练一个模型,使其能为输入图像的每一个像素预测正确的类别标签(如“飞机”、“鸟”、“背景”等)。好的语义分割模型能够精确识别图像中不同物体的轮廓,并将属于同一语义类别的区域(无论实例数量)分割出来。如下图所示,模型输出应清晰划分不同对象区域(如飞机、天空、地面),并生成高质量的分割掩码。

2.参数解析与数据处理

1.参数说明

main.py/get_argparser中的各个参数主要用于配置 DeepLabV3+ 模型的训练和测试过程,也顺便可以看看整个工程支持哪些功能:

1.数据集相关参数 (Dataset Options)

1
2
3
4
5
6
7
parser.add_argument("--data_root", type=str, default='./datasets/data',
help="数据集根目录路径")
parser.add_argument("--dataset", type=str, default='voc',
choices=['voc', 'cityscapes'],
help="选择数据集类型:VOC或Cityscapes")
parser.add_argument("--num_classes", type=int, default=None,
help="类别数量(默认:None,会根据数据集自动设置)")

2.DeepLab模型相关参数 (Deeplab Options)

1
2
3
4
5
6
7
8
9
parser.add_argument("--model", type=str, default='deeplabv3plus_mobilenet',
choices=['deeplabv3_resnet50', 'deeplabv3plus_resnet50',
'deeplabv3_resnet101', 'deeplabv3plus_resnet101',
'deeplabv3_mobilenet', 'deeplabv3plus_mobilenet'],
help="选择模型架构")
parser.add_argument("--separable_conv", action='store_true', default=False,
help="是否在解码器和ASPP中使用可分离卷积")
parser.add_argument("--output_stride", type=int, default=16, choices=[8, 16],
help="输出步长,控制特征图大小")

3.训练相关参数 (Train Options)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
parser.add_argument("--test_only", action='store_true', default=False,
help="仅进行测试模式")
parser.add_argument("--save_val_results", action='store_true', default=False,
help="是否保存验证结果到'./results'目录")
parser.add_argument("--total_itrs", type=int, default=30e3,
help="总迭代次数(默认:30k)")
parser.add_argument("--lr", type=float, default=0.01,
help="学习率(默认:0.01)")
parser.add_argument("--lr_policy", type=str, default='poly', choices=['poly', 'step'],
help="学习率调度策略:多项式衰减或步进式衰减")
parser.add_argument("--step_size", type=int, default=10000,
help="学习率步进式衰减的步长")
parser.add_argument("--crop_val", action='store_true', default=False,
help="是否对验证集进行裁剪")
parser.add_argument("--batch_size", type=int, default=16,
help="训练批次大小")
parser.add_argument("--val_batch_size", type=int, default=4,
help="验证批次大小")
parser.add_argument("--crop_size", type=int, default=513,
help="输入图像裁剪大小")

4. 检查点相关参数

1
2
3
4
parser.add_argument("--ckpt", default=None, type=str,
help="从检查点恢复模型")
parser.add_argument("--continue_training", action='store_true', default=False,
help="是否继续训练")

5.损失函数和优化器参数

1
2
3
4
5
parser.add_argument("--loss_type", type=str, default='cross_entropy',
choices=['cross_entropy', 'focal_loss'],
help="损失函数类型")
parser.add_argument("--weight_decay", type=float, default=1e-4,
help="权重衰减系数")

6. 硬件和随机种子参数

1
2
3
4
parser.add_argument("--gpu_id", type=str, default='0',
help="使用的GPU ID")
parser.add_argument("--random_seed", type=int, default=1,
help="随机种子")

7. 日志和验证相关参数

1
2
3
4
parser.add_argument("--print_interval", type=int, default=10,
help="损失打印间隔")
parser.add_argument("--val_interval", type=int, default=100,
help="验证间隔")

8. PASCAL VOC特定参数

1
2
3
parser.add_argument("--year", type=str, default='2012',
choices=['2012_aug', '2012', '2011', '2009', '2008', '2007'],
help="VOC数据集年份")

9.可视化相关参数

1
2
3
4
5
6
7
8
parser.add_argument("--enable_vis", action='store_true', default=False,
help="是否使用visdom进行可视化")
parser.add_argument("--vis_port", type=str, default='13570',
help="visdom服务器端口")
parser.add_argument("--vis_env", type=str, default='main',
help="visdom环境名称")
parser.add_argument("--vis_num_samples", type=int, default=8,
help="可视化样本数量")

2.数据处理

以 VOC 数据集为例,我们主要关注以下几个方法:

1.数据读取

1.PASCAL VOC数据集读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class VOCSegmentation(data.Dataset):
def __init__(self, root, year='2012', image_set='train',
download=False, transform=None):
# 初始化数据集路径和参数
self.root = os.path.expanduser(root)
self.year = year
self.transform = transform

# 设置图像和掩码目录
image_dir = os.path.join(voc_root, 'JPEGImages')
mask_dir = os.path.join(voc_root, 'SegmentationClass')

# 读取数据集分割文件
split_f = os.path.join(splits_dir, image_set + '.txt')
with open(split_f, "r") as f:
file_names = [x.strip() for x in f.readlines()]

# 构建图像和掩码路径列表
self.images = [os.path.join(image_dir, x + ".jpg") for x in file_names]
self.masks = [os.path.join(mask_dir, x + ".png") for x in file_names]
2.数据读取的核心方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def __getitem__(self, index):
"""获取单个样本"""
# 读取图像和标签
img = Image.open(self.images[index]).convert('RGB')
target = Image.open(self.masks[index])

# 应用数据增强
if self.transform is not None:
img, target = self.transform(img, target)

return img, target

def __len__(self):
"""返回数据集大小"""
return len(self.images)

2.数据增强和预处理

数据增强通过ExtCompose组合多个转换操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
train_transform = et.ExtCompose([
# 随机缩放
et.ExtRandomScale((0.5, 2.0)),
# 随机裁剪
et.ExtRandomCrop(size=(opts.crop_size, opts.crop_size), pad_if_needed=True),
# 随机水平翻转
et.ExtRandomHorizontalFlip(),
# 转换为张量
et.ExtToTensor(),
# 标准化
et.ExtNormalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])

3.构建 DataLoader

在main.py中,使用DataLoader来批量加载数据:

1
2
3
4
5
6
7
8
9
10
11
12
train_loader = data.DataLoader(
train_dst,
batch_size=opts.batch_size,
shuffle=True,
num_workers=0
)
val_loader = data.DataLoader(
val_dst,
batch_size=opts.val_batch_size,
shuffle=True,
num_workers=0
)

3.核心架构设计

1.模型构建

1
2
3
4
5
6
7
8
9
10
11
12
13
# Set up model  
# 模型映射表
model_map = {
'deeplabv3_resnet50': network.deeplabv3_resnet50,
'deeplabv3plus_resnet50': network.deeplabv3plus_resnet50,
'deeplabv3_resnet101': network.deeplabv3_resnet101,
'deeplabv3plus_resnet101': network.deeplabv3plus_resnet101,
'deeplabv3_mobilenet': network.deeplabv3_mobilenet,
'deeplabv3plus_mobilenet': network.deeplabv3plus_mobilenet
}

# 创建模型实例
model = model_map[opts.model](num_classes=opts.num_classes,

2.模型定义

1.基础模型

1
2
3
4
5
6
7
8
9
10
11
12
class _SimpleSegmentationModel(nn.Module):  
def __init__(self, backbone, classifier):
super(_SimpleSegmentationModel, self).__init__()
self.backbone = backbone # 主干网络
self.classifier = classifier # 分类头

def forward(self, x):
input_shape = x.shape[-2:] # 获取输入图像的高和宽(H, W)
features = self.backbone(x) # 提取特征(可能是字典或张量)
x = self.classifier(features) # 生成分割逻辑图(未上采样)
x = F.interpolate(x, size=input_shape, mode='bilinear', align_corners=False)
return x

初始化模型的两个核心组件:

  • backbone:负责提取图像特征的主干网络(如 ResNet、MobileNet)
  • classifier:负责将特征图转换为分割结果的分类头(如 DeepLab 的 ASPP 模块)

前向传播 (forward)​步骤解析**​:

  1. 保存输入尺寸​:
    • x.shape[-2:] 获取输入张量的最后两个维度(即图像的高度 H 和宽度 W)。
    • 例如,输入 x 的形状为 (B, C, H, W),则 input_shape = (H, W)
  2. 特征提取​:
    • features = self.backbone(x):通过主干网络提取多级特征。
      • 对于 DeepLabv3+,backbone 可能返回一个字典,包含深层特征(out)和浅层特征(low_level)。
      • 对于 DeepLabv3,可能直接返回最后一层的特征图。
  3. 分类头处理​:
    • x = self.classifier(features):将特征转换为分割逻辑图(logits)。
      • 输出形状通常为 (B, num_classes, h, w),其中 h 和 w 是特征图的尺寸(小于输入尺寸)。
  4. 上采样恢复分辨率​:
    • F.interpolate:使用双线性插值将分割图上采样到输入图像尺寸
      • size=input_shape:目标尺寸(原始图像的 H 和 W)
      • mode='bilinear':插值方式(双线性插值适合分割任务)
      • align_corners=False:保持与其他框架(如 TensorFlow)的兼容性

2.DeepLabV3+模型类

1
2
3
class DeepLabV3(_SimpleSegmentationModel):
"""DeepLabV3模型实现"""
pass

DeepLabV3类继承自_SimpleSegmentationModel类,它通过继承获得了所有必要的功能,不需要额外添加新的方法或属性。

3.模型组件

1.骨干网络

支持两种骨干网络:

1.ResNet系列

创建一个基于ResNet的语义分割模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def _segm_resnet(name, backbone_name, num_classes, output_stride, pretrained_backbone):
# name: 模型类型,可以是 'deeplabv3plus' 或 'deeplabv3'
# backbone_name: ResNet主干网络名称(如 'resnet50', 'resnet101')
# num_classes: 分割输出的类别数量
# output_stride: 输出特征图的步幅(8或16),控制特征图分辨率
# pretrained_backbone: 是否加载ImageNet预训练权重(布尔值)

if output_stride == 8:
# 当output_stride=8时,配置高级特征提取参数
replace_stride_with_dilation = [False, True, True]
# 指示在ResNet的哪个阶段使用空洞卷积代替步幅卷积:
# [layer2, layer3, layer4] -> [False, True, True]
# 表示在layer3和layer4中使用空洞卷积

aspp_dilate = [12, 24, 36]
# 为ASPP(空洞空间金字塔池化)模块设置扩张率(dilation rates)
# 这些较大的扩张率适用于高分辨率输出(output_stride=8)
else:
# 当output_stride=16时(默认配置)
replace_stride_with_dilation = [False, False, True]
# 仅在最后一个阶段(layer4)使用空洞卷积
# 这会使layer4的特征图尺寸减半

aspp_dilate = [6, 12, 18]
# 使用较小的ASPP扩张率适用于标准分辨率输出

backbone = resnet.__dict__[backbone_name](
pretrained=pretrained_backbone,
replace_stride_with_dilation=replace_stride_with_dilation)
# 创建ResNet主干网络:
# resnet.__dict__[backbone_name]:通过名称从resnet模块获取对应的模型类
# pretrained:是否加载预训练权重
# replace_stride_with_dilation:传递空洞卷积配置

inplanes = 2048
# ResNet最后一层(layer4)的输出通道数
# 对于标准ResNet50/101/152,该值为2048

low_level_planes = 256
# 浅层特征(layer1输出)的通道数
# ResNet中layer1的输出通道固定为256

if name == 'deeplabv3plus':
# DeepLabv3+ 模型配置
return_layers = {'layer4': 'out', 'layer1': 'low_level'}
# 指定要提取的中间层及其别名:
# 'layer4': 高级语义特征 -> 别名为'out'
# 'layer1': 低级空间细节 -> 别名为'low_level'

classifier = DeepLabHeadV3Plus(inplanes, low_level_planes, num_classes, aspp_dilate)
# 创建DeepLabv3+解码器头部:
# 输入:深层特征通道数(inplanes), 浅层特征通道数(low_level_planes)
# 输出:分割类别数(num_classes)
# 使用配置的ASPP扩张率(aspp_dilate)

elif name == 'deeplabv3':
# DeepLabv3 模型配置
return_layers = {'layer4': 'out'}
# 仅提取最后一层(layer4)的特征

classifier = DeepLabHead(inplanes, num_classes, aspp_dilate)
# 创建DeepLabv3解码器头部:
# 仅使用深层特征,不包含浅层细节

backbone = IntermediateLayerGetter(backbone, return_layers=return_layers)
# 包装ResNet主干网络:
# IntermediateLayerGetter修改网络使其返回指定中间层的输出
# 例如对于DeepLabv3+:返回包含'out'和'low_level'两个特征图的字典

model = DeepLabV3(backbone, classifier)
# 组合主干网络和分类器:
# DeepLabV3是一个将特征提取和分类解耦的容器类
# backbone输出特征图,classifier生成最终分割图

return model
2.MobileNetV2

基于MobileNetV2构建DeepLabv3或DeepLabv3+模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def _segm_mobilenet(name, backbone_name, num_classes, output_stride, pretrained_backbone):
# name: 模型类型,'deeplabv3plus' 或 'deeplabv3'
# backbone_name: 主干网络名称(本函数固定使用mobilenetv2)
# num_classes: 分割类别数
# output_stride: 输出步幅(8或16),控制特征图下采样率
# pretrained_backbone: 是否加载预训练权重

# 根据输出步幅设置ASPP模块的空洞率(dilation rates)
if output_stride == 8:
aspp_dilate = [12, 24, 36] # 输出步幅8需要更大的空洞率保持感受野
else:
aspp_dilate = [6, 12, 18] # 默认输出步幅16的空洞率配置

# 初始化MobileNetV2主干网络
backbone = mobilenetv2.mobilenet_v2(
pretrained=pretrained_backbone, # 是否加载预训练权重
output_stride=output_stride # 控制网络内部的下采样行为
)

# 重构MobileNetV2的特征提取层
# 将原features模块拆分为低层特征和高层特征
backbone.low_level_features = backbone.features[0:4] # 前4层作为低层特征(保留更多空间细节)
backbone.high_level_features = backbone.features[4:-1] # 第5层到最后倒数第二层作为高层特征(包含更多语义信息)

# 释放不再需要的组件引用
backbone.features = None # 移除原特征提取器
backbone.classifier = None # 移除原始分类头(因为我们要做分割任务)

# 设置特征通道数
inplanes = 320 # 高层特征的输出通道数(MobileNetV2最后一层的通道数)
low_level_planes = 24 # 低层特征的输出通道数(第4层的输出通道数)

# 根据模型类型配置返回层和分类头
if name == 'deeplabv3plus':
# DeepLabv3+需要同时返回高层和低层特征
return_layers = {
'high_level_features': 'out', # 高层特征别名为'out'
'low_level_features': 'low_level' # 低层特征别名为'low_level'
}
# 使用DeepLabv3+特有的分类头(包含特征融合模块)
classifier = DeepLabHeadV3Plus(
inplanes, # 高层特征通道数
low_level_planes, # 低层特征通道数
num_classes, # 分类数
aspp_dilate # ASPP模块的空洞率配置
)
elif name == 'deeplabv3':
# DeepLabv3只需要返回高层特征
return_layers = {'high_level_features': 'out'}
# 使用标准DeepLab分类头
classifier = DeepLabHead(
inplanes, # 高层特征通道数
num_classes, # 分类数
aspp_dilate # ASPP模块的空洞率配置
)

# 使用IntermediateLayerGetter包装主干网络
# 使其能够返回指定的中间层结果
backbone = IntermediateLayerGetter(
backbone, # 主干网络
return_layers=return_layers # 指定需要返回的层及其别名
)

# 组合主干网络和分类头构建完整模型
model = DeepLabV3(
backbone, # 特征提取器
classifier # 分类解码器
)
return model

2.解码器头(Decoder Head)

DeepLabV3+的解码头模块,包含ASPP模块和多级特征融合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class DeepLabHeadV3Plus(nn.Module):

# in_channels: 高层特征输入通道数(来自backbone的layer4)
# low_level_channels: 低层特征输入通道数(来自backbone的layer1)
# num_classes: 分类类别数
# aspp_dilate: ASPP模块的空洞率配置(default=[12,24,36])
def __init__(self, in_channels, low_level_channels, num_classes, aspp_dilate=[12, 24, 36]):
super(DeepLabHeadV3Plus, self).__init__()

# 低层特征投影层:将低层特征通道数降维到48
self.project = nn.Sequential(
nn.Conv2d(low_level_channels, 48, 1, bias=False), # 1x1卷积降维
nn.BatchNorm2d(48), # 批归一化
nn.ReLU(inplace=True), # ReLU激活
)

# ASPP模块:提取多尺度上下文信息
self.aspp = ASPP(in_channels, aspp_dilate) # 输入通道为高层特征维度

# 分类器:融合高低层特征后输出最终预测
self.classifier = nn.Sequential(
nn.Conv2d(304, 256, 3, padding=1, bias=False), # 304=256(ASPP输出)+48(低层)
nn.BatchNorm2d(256), # 批归一化
nn.ReLU(inplace=True), # ReLU激活
nn.Conv2d(256, num_classes, 1) # 1x1卷积输出分类结果
)

# 初始化权重
self._init_weight()

def forward(self, feature):
# 前向传播流程: 1.处理低层特征 2.通过ASPP处理高层特征 3.特征融合后分类

# 低层特征处理 (shape: [B, 256, H/4, W/4] -> [B, 48, H/4, W/4])
low_level_feature = self.project(feature['low_level'])

# 高层特征通过ASPP (shape: [B, 2048, H/16, W/16] -> [B, 256, H/16, W/16])
output_feature = self.aspp(feature['out'])

# 上采样高层特征到低层特征尺寸 (H/16->H/4)
output_feature = F.interpolate(
output_feature,
size=low_level_feature.shape[2:], # 匹配低层特征的空间尺寸
mode='bilinear', # 双线性插值
align_corners=False
)

# 沿通道维度拼接特征 (256+48=304)
fused_feature = torch.cat([low_level_feature, output_feature], dim=1)

# 通过分类器得到最终输出 (shape: [B, num_classes, H/4, W/4])
return self.classifier(fused_feature)

def _init_weight(self):
# 权重初始化方法: 卷积层使用Kaiming初始化 归一化层权重初始化为1,偏置初始化为0
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight) # 卷积层He初始化
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1) # 归一化层权重设为1
nn.init.constant_(m.bias, 0) # 归一化层偏置设为0

3.ASPP模块

ASPP模块通过并行多分支卷积捕获多尺度上下文信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class ASPP(nn.Module):
# in_channels: 输入特征图的通道数
# atrous_rates: 三个空洞卷积的空洞率列表(如[6,12,18])
def __init__(self, in_channels, atrous_rates):
super(ASPP, self).__init__()
out_channels = 256 # 所有分支的输出通道数

# 分支模块列表
modules = []

# 分支1: 1x1标准卷积 (空洞率=1)
modules.append(nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, bias=False), # 1x1卷积
nn.BatchNorm2d(out_channels), # 批归一化
nn.ReLU(inplace=True))) # ReLU激活

# 分支2-4: 3x3空洞卷积 (不同空洞率)
rate1, rate2, rate3 = tuple(atrous_rates) # 解包空洞率参数
modules.append(ASPPConv(in_channels, out_channels, rate1)) # 空洞率1
modules.append(ASPPConv(in_channels, out_channels, rate2)) # 空洞率2
modules.append(ASPPConv(in_channels, out_channels, rate3)) # 空洞率3

# 分支5: 全局平均池化 + 1x1卷积
modules.append(ASPPPooling(in_channels, out_channels))

# 将5个分支注册为ModuleList
self.convs = nn.ModuleList(modules)

# 特征融合层: 拼接后降维
self.project = nn.Sequential(
nn.Conv2d(5 * out_channels, out_channels, 1, bias=False), # 5 * 256->256
nn.BatchNorm2d(out_channels), # 批归一化
nn.ReLU(inplace=True), # ReLU激活
nn.Dropout(0.1),) # 正则化

def forward(self, x):
# 前向传播流程: 各分支并行处理输入特征 -> 沿通道维度拼接结果 -> 融合降维后输出
res = []
for conv in self.convs:
res.append(conv(x)) # 各分支分别处理

# 沿通道维度拼接 (5个256通道 -> 1280通道)
res = torch.cat(res, dim=1)

# 通过融合层降维回256通道
return self.project(res)

4.训练策略

1.学习率调整策略

在utils.py中实现了两种学习率调整策略:

1.多项式衰减(Poly)

特点:

  • 控制平滑,学习率下降自然
  • 对于复杂任务(如语义分割)训练更稳定
  • 参数少(只需 max_iters 和 power),易于使用
1
2
3
4
5
6
7
8
9
class PolyLR(_LRScheduler):
def __init__(self, optimizer, max_iters, power=0.9, last_epoch=-1):
self.max_iters = max_iters
self.power = power
super(PolyLR, self).__init__(optimizer, last_epoch)

def get_lr(self):
return [base_lr * (1 - self.last_epoch / self.max_iters) ** self.power
for base_lr in self.base_lrs]

2.步进式衰减(Step)

StepLR 是一种“阶梯状”地调整学习率的方法,每隔固定的步数将学习率缩小(乘以 gamma),是一种简洁且有效的学习率下降策略。

1
2
3
4
5
6
7
8
9
class StepLR(_LRScheduler):
def __init__(self, optimizer, step_size, gamma=0.1, last_epoch=-1):
self.step_size = step_size
self.gamma = gamma
super(StepLR, self).__init__(optimizer, last_epoch)

def get_lr(self):
return [base_lr * self.gamma ** (self.last_epoch // self.step_size)
for base_lr in self.base_lrs]

2.优化器配置

在main.py中配置优化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建优化器:按模块设置不同学习率(用于迁移学习/微调)
optimizer = torch.optim.SGD(
params=[
{'params': model.backbone.parameters(), 'lr': 0.1 * opts.lr},
{'params': model.classifier.parameters(), 'lr': opts.lr},
],
momentum=opts.momentum,
weight_decay=opts.weight_decay
)

# 创建学习率调度器 # 根据配置选择 PolyLR 或 StepLR 学习率衰减策略
if opts.lr_policy == 'poly':
scheduler = utils.PolyLR(optimizer, max_iters=opts.epochs * len(train_loader), power=0.9)
else:
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=opts.step_size, gamma=0.1)

3.训练循环实现

在main.py中的训练循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def main():
# 训练循环
for epoch in range(opts.start_epoch, opts.epochs):
# 训练一个epoch
train_epoch(model, train_loader, criterion, optimizer, device, epoch, opts)

# 验证
if opts.val_interval > 0 and (epoch + 1) % opts.val_interval == 0:
val_epoch(model, val_loader, criterion, device, opts)

# 更新学习率
scheduler.step()

# 保存检查点
if (epoch + 1) % opts.save_interval == 0:
save_checkpoint(model, optimizer, epoch, opts)

4. 损失函数选择

支持的损失函数:

1
2
3
4
5
# 损失函数选择
if opts.loss_type == 'focal':
criterion = utils.FocalLoss(ignore_index=255, size_average=True)
elif opts.loss_type == 'cross_entropy':
criterion = nn.CrossEntropyLoss(ignore_index=255, reduction='mean')

5.评估与可视化

1.评估函数

评估函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def val_epoch(model, loader, criterion, device, opts):
model.eval()
ret_samples = []
total_loss = 0
total_correct = 0
total_pixels = 0

with torch.no_grad():
for i, (images, labels) in enumerate(loader):
images = images.to(device)
labels = labels.to(device)

outputs = model(images)
loss = criterion(outputs, labels)

# 计算准确率
pred = outputs.argmax(dim=1)
correct = (pred == labels).sum().item()
total_correct += correct
total_pixels += labels.numel()

# 计算损失
total_loss += loss.item()

# 保存样本用于可视化
if i < opts.vis_num_samples:
ret_samples.append((images[0].detach().cpu().numpy(),
labels[0].detach().cpu().numpy(),
pred[0].detach().cpu().numpy()))

# 计算平均指标
avg_loss = total_loss / len(loader)
avg_acc = total_correct / total_pixels

return avg_loss, avg_acc, ret_samples

2.评估指标计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class AverageMeter:
"""计算并存储平均值和当前值"""
def __init__(self):
self.reset()

def reset(self):
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0

def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count


def compute_metrics(pred, target, num_classes):
"""计算各种评估指标"""
# 混淆矩阵
confusion_matrix = np.zeros((num_classes, num_classes))
for i in range(num_classes):
for j in range(num_classes):
confusion_matrix[i, j] = np.sum((pred == i) & (target == j))

# 计算IoU
intersection = np.diag(confusion_matrix)
union = np.sum(confusion_matrix, axis=1) + np.sum(confusion_matrix, axis=0) - intersection
iou = intersection / (union + 1e-10)

# 计算准确率
accuracy = np.sum(intersection) / np.sum(confusion_matrix)

return {
'confusion_matrix': confusion_matrix,
'iou': iou,
'mean_iou': np.mean(iou),
'accuracy': accuracy
}

3.可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Visualizer:
def __init__(self, opts):
self.opts = opts
self.vis_dir = os.path.join(opts.vis_dir, opts.model)
os.makedirs(self.vis_dir, exist_ok=True)

def vis_segmentation(self, image, label, pred, epoch):
"""可视化分割结果"""
# 创建图像网格
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# 显示原始图像
axes[0].imshow(image.transpose(1, 2, 0))
axes[0].set_title('Input Image')
axes[0].axis('off')

# 显示真实标签
axes[1].imshow(label, cmap='tab20')
axes[1].set_title('Ground Truth')
axes[1].axis('off')

# 显示预测结果
axes[2].imshow(pred, cmap='tab20')
axes[2].set_title('Prediction')
axes[2].axis('off')

# 保存图像
plt.savefig(os.path.join(self.vis_dir, f'epoch_{epoch}.png'))
plt.close()

def vis_metrics(self, metrics, epoch):
"""可视化评估指标"""
# 创建图像网格
fig, axes = plt.subplots(2, 2, figsize=(15, 15))

# 绘制混淆矩阵
sns.heatmap(metrics['confusion_matrix'],
annot=True,
fmt='d',
ax=axes[0, 0])
axes[0, 0].set_title('Confusion Matrix')

# 绘制IoU条形图
axes[0, 1].bar(range(len(metrics['iou'])), metrics['iou'])
axes[0, 1].set_title('IoU per Class')

# 绘制损失曲线
axes[1, 0].plot(metrics['loss_history'])
axes[1, 0].set_title('Loss History')

# 绘制准确率曲线
axes[1, 1].plot(metrics['acc_history'])
axes[1, 1].set_title('Accuracy History')

# 保存图像
plt.savefig(os.path.join(self.vis_dir, f'metrics_{epoch}.png'))
plt.close()

4.训练过程可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def train_epoch(model, loader, criterion, optimizer, device, epoch, opts):
model.train()
vis = Visualizer(opts)
metrics = {
'loss_history': [],
'acc_history': [],
'confusion_matrix': None,
'iou': None
}

for i, (images, labels) in enumerate(loader):
# 前向传播和损失计算
outputs = model(images)
loss = criterion(outputs, labels)

# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()

# 计算指标
pred = outputs.argmax(dim=1)
metrics['loss_history'].append(loss.item())
metrics['acc_history'].append((pred == labels).float().mean().item())

# 定期可视化
if i % opts.vis_interval == 0:
vis.vis_segmentation(
images[0].detach().cpu().numpy(),
labels[0].detach().cpu().numpy(),
pred[0].detach().cpu().numpy(),
epoch
)

# 每个epoch结束可视化指标
vis.vis_metrics(metrics, epoch)

6.总结

1.Pascal VOC 2012 数据集​

  • 多任务基准​:支持分类、检测、分割(像素级标注)
  • 数据组成​:JPEGImages(原始图像)、Annotations(边界框)、SegmentationClass(分割标签)
  • 评估限制​:测试集标签不公开,需提交官方服务器计算指标(如 mIoU)

2.DeepLabV3+ 模型设计

  • 骨干网络​:支持 ResNet 和 MobileNetV2,适应不同计算需求
  • ASPP 模块​:多尺度空洞卷积,捕获上下文信息
  • 特征融合​:结合深层语义特征(高层)与浅层细节(低层)​

3.训练与优化

  • 学习率策略​:多项式衰减(PolyLR)或步进衰减(StepLR)
  • 损失函数​:交叉熵或 Focal Loss(处理类别不平衡)
  • 优化器​:SGD,分层设置学习率(骨干网络更低)

4.评估与可视化​

  • 指标计算​:mIoU、像素准确率、混淆矩阵
  • 可视化工具​:展示输入图像、真实标签、预测结果及训练曲线

7.备注

工程地址:https://github.com/VainF/DeepLabV3Plus-Pytorch
Pascal VOC (Visual Object Classes) 2012数据集:http://host.robots.ox.ac.uk/pascal/VOC/voc2012/