简介:基于PyTorch在VOC与Cityscapes数据集上训练DeepLabv3+图像分割算法的完整项目,面向已有Python基础、希望快速上手语义分割实战的开发者,也适合作为课程设计或算法预研的参考。资源共包含43个文件,其中23个Python脚本分工清晰,覆盖数据加载与增强、模型搭建(含ASPP空洞空间金字塔池化模块与解码器)、优化器与学习率策略设定、损失函数计算以及IoU指标评估,并提供了训练、预测等入口供直接调用;17张PNG图片展示了不同图像的分割结果、原始图像与标签叠加对比,方便直观判断模型表现。另有README和说明文档概述目录结构与运行要点。整套资源仅2.13MB,体量精简而代码组织模块化,特别适合边阅读边动手调试。目前已有492人学习,借助该源码既能了解VOC、Cityscapes两大数据集的预处理和训练流程,又能深入理解DeepLabv3+如何利用空洞卷积扩大感受野、结合解码器细化边缘,最终可迁移到自动驾驶、遥感等领域完成自定义分割任务。
1. 图像分割实战:在 VOC 和 Cityscapes 上用 PyTorch 把 DeepLabv3+ 完整跑通
图像分割模型的代码网上遍地都是,但能直接对着 VOC 和 Cityscapes 两个数据集完整跑训练、出评估指标的 PyTorch 工程并不多。这套 DeepLabv3+ 项目源码把模型结构、数据加载、训练、评估、推理可视化全部按模块拆开,拿到手里可以直接开始复现。
网络部分基于 DeepLabv3+ 的典型结构,backbone 独立放在 modeling.py 里,方便切换主干和加载预训练权重;datasets 模块对两个数据集分别封装了 data 类,类别数不同、标注格式不同,但底层统一成模型能直接吃的张量。metrics 目录里手写了流式 IoU 指标类,不依赖外部评估库,想改评估策略也很方便。适合正在做语义分割课程设计和研究课题的学生,也适合需要在自定义数据上验证 DeepLabv3+ 效果、不想从零写代码的算法工程师。
2. DeepLabv3+ 网络结构与双数据集加载:从 ASPP 模块、类别映射到数据集代码走读
2.1 ASPP 编码器与解码器:空洞卷积的并行多尺度设计
DeepLabv3+ 的核心在网络部分,项目的 network 目录下拆了三个文件:_deeplab.py 定义整体模型,backbone/modeling.py 放 ResNet 系列的基础编码网络,network/utils.py 负责权重初始化和预训练加载。模型主体是编码器-解码器结构,编码器先用 ResNet 提特征,再把最后一个特征图送进 ASPP(Atrous Spatial Pyramid Pooling)模块。ASPP 的设计思路很简单:同一张特征图,用不同 dilation rate 的空洞卷积并行提取多尺度上下文信息,最后拼接到一起。
空洞卷积的原理是往卷积核的采样点之间插入间隔,dilation=6 的 3x3 卷积等效于普通 13x13 卷积的感受野,但参数量仍然是九个。这样做的好处不用多谈:scale 变化大的场景里,模型能同时看到路面的全局上下文和栏杆的局部纹理,而计算量只增加了少量空洞卷积的运算。DeepLabv3+ 相比 DeepLabv3 的改进在于加了 Decoder 部分,把 ASPP 的低分辨率输出上采样 4 倍,与 ResNet 中层的低层高分辨率特征做 concat,再经过 3x3 卷积和上采样得到最终掩码,这样边界细节能保留得更好。
下面是这份代码中 ASPP 模块的典型实现结构:
# network/_deeplab.py 中的 ASPP 模块 import torch import torch.nn as nn import torch.nn.functional as F class ASPP(nn.Module): def __init__(self, in_channels, out_channels=256, rates=(6, 12, 18)): super().__init__() # 1x1 卷积分支,等价于 dilation=1 的普通卷积 self.branch1 = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) # 三个并行的空洞卷积分支,padding 与 dilation 保持一致 self.branch2 = nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, padding=rates[0], dilation=rates[0], bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) self.branch3 = nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, padding=rates[1], dilation=rates[1], bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) self.branch4 = nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, padding=rates[2], dilation=rates[2], bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) # 全局图像池化分支,用于捕捉整图上下文 self.branch5 = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, out_channels, 1, bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) # 五路特征拼接后统一降维到 out_channels self.project = nn.Sequential( nn.Conv2d(out_channels * 5, out_channels, 1, bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), ) def forward(self, x): size = x.size()[2:] f1 = self.branch1(x) f2 = self.branch2(x) f3 = self.branch3(x) f4 = self.branch4(x) # 全局池化输出是 1x1,拼接前必须插值回当前特征图尺寸 f5 = F.interpolate(self.branch5(x), size=size, mode='bilinear', align_corners=True) out = torch.cat([f1, f2, f3, f4, f5], dim=1) return self.project(out)注意几个容易忽略的点:分支 5 的全局池化输出只有 1x1 分辨率,拼接前必须双线性上采样回当前特征图的尺寸,代码里用 F.interpolate 处理,align_corners=True 是和 backbone 内部对齐保持一致的关键。BN 层在空洞卷积后是默认配置,训练时和预测时的行为差异会直接影响结果精度,这是后面避坑部分会列出来的问题。rates 用固定的 (6, 12, 18),当输入分辨率变化较大时一般不需要调整,改动它反而会让预训练权重失去适配含义。
2.2 VOC 和 Cityscapes 的类别映射:从原始标注到训练标签
VOC 数据集的语义分割标签有 21 个类,背景加 20 个物体类别,包括飞机、自行车、鸟、船、瓶子、公交车、汽车、猫、椅子、牛、餐桌、狗、马、摩托车、人、盆栽、羊、沙发、火车和电视显示器。Cityscapes 数据集是针对城市街景的,原始标注类别有 30 个左右,但绝大多数实现只用其中 19 个有效类参与训练,剩下的类——比如天空、地形里不可辨识的小块——映射到 ignore_index 255,不参与 loss 计算。
这个类别映射的关键点在于两个数据集的 class id 并不是连续的。VOC 的 id 基本连续 1-20,背景是 0;而 Cityscapes 的原始 id 里有大量空洞,比如车辆类从 26 到 35 跨了十个 id,直接送进 CrossEntropyLoss 网络是学不出来的。datasets/data/cityscapes.py 里通常维护一张 30 到 19 的映射表,把可训练类映射到 0-18,void 类统一映射到 255。
# datasets/data/cityscapes.py 中的类别 id 映射片段 # 原始 Cityscapes label id 到训练类别的映射,255 表示忽略 cityscapes_id_to_train_id = { 0: 255, # unlabeled 1: 255, # ego vehicle # ... 部分类别略 7: 0, # road -> 训练 id 0 8: 1, # sidewalk -> 训练 id 1 # ... 中间类别略 24: 18, # motorcycle -> 训练 id 18 33: 255, # pole,部分实现不使用 } def convert_label(label, id_to_trainid): label_copy = label.copy() for orig_id, train_id in id_to_trainid.items(): label_copy[label == orig_id] = train_id return label_copy这里要提醒一句:不同实现里 19 类的选择略有不同,有的把 pole 和 traffic light 合并成一个类,有的把 terrain 单独保留,所以拿到新工程的第一个动作是数一下 cityscapes.py 里 train_id 最后的最大值,确认类别数和 loss 里 ignore_index 的配置一致。
2.3 datasets/data/voc.py 和 cityscapes.py 的读取与增强流程
VOC 的标注是 PNG 格式的 index 图,PIL 打开时要用 P 模式,直接转成 RGB 再当普通图像处理会在边界处产生混乱的过渡值。项目里 datasets/data/voc.py 的基本读取流程是把 image 和 mask 同时加载,经过 transform 后返回张量对。
# datasets/data/voc.py 的核心结构 import os from PIL import Image import torch.utils.data as data class VOCSegmentation(data.Dataset): def __init__(self, root, image_set='train', transform=None): self.root = root self.transform = transform # ImageSets/Segmentation 下的 txt 文件列出图片 id with open(os.path.join(root, 'ImageSets', 'Segmentation', f'{image_set}.txt')) as f: self.images = [line.strip() for line in f.readlines()] def __len__(self): return len(self.images) def __getitem__(self, index): img_id = self.images[index] # 注意 mask 必须以 P 模式打开,保留像素索引 image = Image.open(os.path.join(self.root, 'JPEGImages', f'{img_id}.jpg')).convert('RGB') target = Image.open(os.path.join(self.root, 'SegmentationClass', f'{img_id}.png')) if self.transform is not None: image, target = self.transform(image, target) return image, target在这种 Transform 体系下,数据增强要同步作用于原图和 mask,不能对二者做不同的随机操作。utils/ext_transforms.py 里封装的 ExtRandomScale、ExtRandomCrop、ExtRandomHorizontalFlip 就是按双输入同步方式实现的,这里简单列一个常见的训练增强组合:
# utils/ext_transforms.py 中的训练增强组合 train_transform = ext_transforms.ExtCompose([ # 原图随机缩放 0.5~2.0,mask 同步插值 ext_transforms.ExtRandomScale((0.5, 2.0)), # 随机裁剪到 512x512,不足补边 ext_transforms.ExtRandomCrop(size=(512, 512), pad_if_needed=True), # 随机水平翻转 ext_transforms.ExtRandomHorizontalFlip(), # 转成 tensor,并做 ImageNet 均值方差归一化 ext_transforms.ExtToTensor(), ext_transforms.ExtNormalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])对 Cityscapes 的训练通常用 crop size 768,需要把 RandomCrop 的 size 相应调大,否则大图上的小目标容易裁不到。验证时的增强要简单得多,只做 resize 到固定尺寸加归一化,不做随机翻转。
3. main.py 训练主流程:参数配置、损失函数、poly 学习率与训练循环
3.1 训练入口与参数面板
main.py 是整个训练的入口,所有超参数通过 argparse 传入。实际部署时,经常用到的是这几个参数:
# VOC 数据集训练示例 python main.py \ --dataset voc \ --data-root /data/VOCdevkit \ --batch-size 16 \ --crop-size 512 \ --base-lr 0.007 \ --epochs 50 # Cityscapes 数据集训练示例,图更大所以 crop 更大 python main.py \ --dataset cityscapes \ --data-root /data/cityscapes \ --batch-size 8 \ --crop-size 768 \ --base-lr 0.01 \ --epochs 100以表格列出几个关键参数的经验值:
| 参数 | VOC 常用值 | Cityscapes 常用值 | 说明 |
|---|---|---|---|
| batch size | 16 | 8 | 实际以显存为准,V100 16G 可再调大 |
| crop size | 512 | 768 | 配合 ResNet 输出 stride 8 使用,需能被 16 整除 |
| base_lr | 0.007 | 0.01 | SGD + poly 时常用,Adam 要降到 1e-4 |
| epochs | 50 | 100 | Cityscapes 数据量大,迭代次数要更多 |
| weight_decay | 1e-4 | 1e-4 | 过大会让 BN 层的 scale 不稳定 |
| momentum | 0.9 | 0.9 | SGD 标配 |
注意 crop size 和 backbone 的 downsampling stride 有连带关系。DeepLabv3+ 的 ResNet 输出 stride 一般设成 8,输入 512x512 对应最后特征图 64x64,这个尺寸既能保留足够空间信息,又不至于让 ASPP 的感受野互相重叠太多。把 crop size 改成非 16 倍数的值,会直接触发断言报错。
3.2 损失函数:CrossEntropy 与 ignore_index
项目里 utils/loss.py 主要是对 CrossEntropyLoss 的封装。语义分割最常用的损失还是逐像素交叉熵,但对带忽略区域的标签要处理到位。
# utils/loss.py 的核心封装 import torch.nn as nn class SegmentationLoss(nn.Module): def __init__(self, ignore_index=255, reduction='mean'): super().__init__() self.criterion = nn.CrossEntropyLoss( ignore_index=ignore_index, reduction=reduction ) def forward(self, pred, target): # pred 是 (N, C, H, W) 的 logits,target 是 (N, H, W) 的类别 id return self.criterion(pred, target)ignore_index 的作用是让标签里等于 255 的位置完全不参与梯度计算。VOC 的标注里人和物体边界上有少量 255 像素,Cityscapes 的 void 区域更是大面量出现,如果忽略这个参数,loss 会被这些无意义像素拉高,模型收敛后边缘区域也会出现奇怪的伪影。另一个点是 reduction 默认用 mean,但有的实现用 sum,对小目标类别比较敏感的场景可以实验性切换。
3.3 poly 学习率调度与训练循环
DeepLab 系列论文里惯用的是 poly 衰减策略,学习率随着迭代步数线性指数下降到 0。这个策略对分割任务很有效,越到后期学习率越低,能稳定收敛到比较好的极值点。
# utils/scheduler.py 中的 PolyLR 调度器 class PolyLR: def __init__(self, optimizer, base_lr, max_iters, power=0.9): self.optimizer = optimizer self.base_lr = base_lr self.max_iters = max_iters self.power = power def get_lr(self, iteration): return self.base_lr * (1 - iteration / self.max_iters) ** self.power def step(self, iteration): lr = self.get_lr(iteration) for param_group in self.optimizer.param_groups: param_group['lr'] = lr注意 max_iters 应该是 total_iterations,等于 epochs 乘每 epoch 的 batch 数量,不是 epoch 数。很多入门实现在这里把 max_iters 填成 epochs,结果是学习率在第 1 个 epoch 内就快速衰减到零,后续 step 全成了小学习率的空转。
训练主循环的骨架不复杂,重点在验证时机的选择:
# main.py 中的训练主循环(伪代码) model.train() for epoch in range(epochs): for i, (images, targets) in enumerate(train_loader): images, targets = images.cuda(), targets.cuda() outputs = model(images) # 前向 loss = criterion(outputs, targets) # 交叉熵损失 optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step(global_iter) # poly 调度 global_iter += 1 # 每 500 步在验证集上统计一次 mIoU if global_iter % 500 == 0: model.eval() val_metrics = run_validation(model, val_loader, metrics) model.train()验证时一定要切回 eval 模式,把 BN 层和 Dropout 关掉,否则拿到的是不稳定的随机结果。这个验证掉点的问题很多新手会碰上,后面避坑部分会单独解释。
4. 评估与推理:stream_metrics 流式 IoU 计算与 predict.py 可视化
4.1 stream_metrics.py 的流式指标实现
语义分割评估最常用的指标是 mIoU(mean Intersection over Union),项目里 metrics/stream_metrics.py 自己实现了流式版本,不依赖外部评估包。流式的意思是维护一个全局混淆矩阵,每个 batch 往里面累加统计,最后统一计算各类别的 IoU。这种做法在验证集很大的时候很有用,不用把整个预测结果存在内存里。
# metrics/stream_metrics.py 的核心逻辑 import numpy as np class StreamSegMetrics: def __init__(self, n_classes): self.n_classes = n_classes self.confusion_matrix = np.zeros((n_classes, n_classes), dtype=np.int64) def reset(self): self.confusion_matrix = np.zeros( (self.n_classes, self.n_classes), dtype=np.int64 ) def update(self, pred, target): pred = pred.flatten() target = target.flatten() # 只统计有效类别区域,忽略 255 mask = (target >= 0) & (target < self.n_classes) # 一次 bincount 统计出所有组合计数,满秩 reshape 成混淆矩阵 cm = np.bincount( self.n_classes * target[mask] + pred[mask], minlength=self.n_classes ** 2 ) self.confusion_matrix += cm.reshape(self.n_classes, self.n_classes) def get_results(self): # 逐类 IoU = TP / (TP + FP + FN),加 1e-10 防除零 diag = np.diag(self.confusion_matrix).astype(np.float64) row_sum = self.confusion_matrix.sum(axis=1) # 真实类像素总数 col_sum = self.confusion_matrix.sum(axis=0) # 预测类像素总数 iou = diag / (row_sum + col_sum - diag + 1e-10) return { 'mIoU': round(np.nanmean(iou), 4), 'class_iou': iou, }bincount 那一段是混淆矩阵更新的关键,用 target 的行索引乘上类别数再加上 pred 的列索引,一次性把所有像素统计进一个一维数组,再 reshape 成混淆矩阵。这比两层 for 循环快得多,在 Cityscapes 这种单张图一百多万像素的验证集上,性能差别肉眼可见。pixAcc 像素准确率的计算同理,直接用对角线之和除以总有效像素即可。
4.2 predict.py 单图推理与结果文件
predict.py 是推理脚本,加载训练好的 checkpoint 对单张图片做分割。典型调用方式是:
python predict.py \ --checkpoint checkpoints/best_model_voc.pth \ --input samples/23_image.png \ --output samples/23_pred.png \ --dataset voc \ --save-overlay输出的 pred 图是每个像素按类别 id 着色的 index 图,overlay 则是把半透明的预测结果叠加到原图上,便于直接看分割边界是否对齐物体边缘。如果是 VOC 的 checkpoint,预测时要注意类别 id 0 是背景;如果是 Cityscapes 模型,还要注意 train_id 和原始标签 id 的转换,否则画出来的图颜色会完全错乱。
4.3 samples 目录结果文件对照与目检流程
项目 samples 目录里能看到若干组结果样例,包括 1、23、114 这几个 VOC 样本和 city_1、city_6 两个 Cityscapes 样本。每组都是几类文件:
| 文件后缀 | 含义 | 建议检查点 |
|---|---|---|
| _image.png | 原始输入图像 | 确认分辨率与训练时一致 |
| _target.png | 人工标注的真值 mask | 看边界是否有 255 忽略像素 |
| _pred.png | 模型预测的 mask | 主要关注小物体和边缘 |
| _overlay.png | 预测叠加到原图的效果 | 看轮廓贴合度,检查误判区域 |
建议的验证流程是用一个验证集跑完整评估,拿到整体 mIoU 后,再挑几个典型样本看 overlay 图。比如 114_image.png 这类样本通常包含小物体和遮挡关系,光看 mIoU 是 0.78 还是 0.80,不如直接看图更直观。samples 里的 visdom-screenshoot.png 就是训练过程中用 Visdom 截下来的训练曲线和分割效果,后面的进阶部分会讲怎么自己复现这个监控。
5. 避坑记录:从 loss 发散、显存溢出到类别对齐失败的经验
5.1 训练 loss 不降反升甚至出现 NaN
现象:训练前几个 epoch 的 loss 非但不降,反而从 0.5 慢慢涨到 2 以上,再过几轮直接变成 NaN。
原因:base_lr 设置过大是头号元凶。SGD 在 poly 策略下初始 lr 0.007 是常见值,但换到 batch size 较小的数据时,梯度更新幅度会相对偏大。另一个常见原因是 backbone 的预训练权重没加载成功,BN 层的 running_mean 还在默认值,前向输出不稳定,反向时梯度爆炸。还有一个容易忽略的情况是数据里有异常标签,比如 Cityscapes 的 target 最大值超出类别数,CrossEntropy 里出现了 out-of-range 的索引。
解决:先把初始 lr 降到 0.001 跑 500 步观察 loss 是否平稳;打印 load_state_dict 时的 missing key 和 unexpected key,确认 backbone 每个卷积层都加载到了权重;在 dataset 的__getitem__里加一行assert target.max() < n_classes,提前暴露标签问题。
5.2 CUDA out of memory 在训练刚启动就出现
现象:训练脚本刚跑几个 iteration,立刻报CUDA out of memory,退出训练。
原因:crop size 太大、batch size 太高、backbone 输出 stride 设成 8 导致中间特征图占用显存翻倍。DeepLabv3+ 的 ResNet 在 stride 8 时最后一层特征图是输入图的 1/8,对 512x512 输入就是 64x64,ASPP 三个空洞卷积分支加上 concat,显存消耗确实不低。
解决:先以 batch size 2、crop size 320 跑通一个迭代,再用 nvidia-smi 观察显存使用量,按可用显存估算合理 batch。常见做法是把 batch size 降半,或者用梯度累积,每两个 batch 累加一次梯度再更新,效果等价于跪了一截显存的大 batch。Cityscapes 的 768 裁剪可以暂时降到 640,先验证流程再逐步调回去。
5.3 切到 Cityscapes 后 mIoU 掉到个位数
现象:模型在 VOC 上训练正常,mIoU 能到 0.7 左右;换到 Cityscapes 之后,验证 mIoU 只有 1 到 2,几乎等于随机猜测。
原因:class id 映射错了。Cityscapes 原始标签里有三十多个值,void 类占掉很大比例,如果直接按原始 id 做 loss,等于把 void 当成一个正常类别参与训练,模型没有能力学习这样混乱的目标。同时 eval 阶段的指标统计也要用同样的映射,否则混淆矩阵里的行和列对不上。
解决:检查 cityscapes.py 里的 id_to_train_id 映射表,把 void 统一映射到 255,训练类映射到 0-18。同时确认 ignore_index 设置为 255,并且验证阶段跑 StreamSegMetrics 时 n_classes 传 19,不能传 30。这是切换数据集时最容易被忽视的一环,我见过不少人在这一步翻车后重新排查了整整两天。
5.4 预训练权重加载报 missing key 与 unexpected key
现象:加载.pth权重时,控制台刷出大段 missing key 和 unexpected key。
原因:官方发布的 ResNet 权重是 ImageNet 分类格式,最后一层是全连接分类头,而分割模型的 backbone 截到最后卷积层,分类头自然就成了 unexpected key。反过来,加载到自己定义的 ResNet 时,如果层名里带了 module. 前缀或自定义命名空间,就会大量出现 missing key。
解决:在 network/utils.py 里做一次映射处理,加载时用 strict=False,把不是分类头的 key 过滤掉。常见做法是在 load 前先打印两个 key 集合,手动对齐名称差异,不要直接忽略所有报错。另外 BN 层的 num_batches_tracked 键有时会出现在 missing 列表里,这是正常现象,不影响加载。
5.5 预测可视化全灰,看不到分割色块
现象:predict.py 跑完,输出的 pred 图整张灰蒙蒙,只有零星看点,与人眼看到的物体边界完全对应不上。
原因:可视化时用了错误的着色表。预测输出是 train_id 索引,直接用城市标注原始颜色的 colormap 上色,很多索引对应的颜色都是灰色。另一种情况是 overlay 叠加时透明度过高,把预测色块全盖没了。
解决:预测前做 train_id 到原始标签 id 的逆映射,或者直接用项目里给定的调色板 dict。叠加时 alpha 取 0.5 左右,先看 pred 图再看 overlay 图,两个图结合判断。如果用的是别人项目的可视化脚本,第一时间检查 colormap 的键是原始 id 还是 train id,这是很经典的坑。
6. 进阶:用 Visdom 实时监控训练,把 DeepLabv3+ 迁移到自己的数据集
6.1 Visdom 服务启动与指标监控
utils/visualizer.py 封装了 Visdom 可视化,启动方式很简单:
python -m visdom.server -port 8097训练脚本运行后,浏览器打开http://localhost:8097,在一个 env 里就能看到 loss 曲线、学习率曲线和验证集 mIoU 曲线。我一般会每隔 200 步把当前 batch 的原图、真值 mask 和预测图推到同一个 env,这样不用等一个 epoch 结束,就能看出模型是否在朝着正确的方向收敛。
6.2 自定义数据集迁移的四步
把这份代码迁移到自己的数据集,大致按四步走:先把标注转成 index 图,类别从 0 开始连续编号,void 用 255;再仿照 datasets/data/voc.py 写一个 Dataset 子类,替换图片读取路径;在 main.py 里注册新的 dataset 名,改 n_classes 参数;最后灵活决定冻结策略,如果数据量小,可以冻结 backbone 前几层,只让 ASPP 和 decoder 部分更新,能明显减少过拟合。
6.3 验证与坏例分析
模型最后一步的验证,我习惯按整体指标加典型样本目检加坏例分析三步走。先跑一遍完整验证集拿 mIoU,然后挑最好的和最差的若干样本,看 overlay 图和 target 图的差异,总结模型对哪一类出错率高,再决定是否对困难类别做加权采样。
图像分割最忌讳只看一个 mIoU 数字就结束,因为它掩盖了类别不均衡的问题。从那以后,我每次跑完分割实验都强制做一遍坏例分析,尤其是 Cityscapes 里的小目标类别,这些类经常被大目标整体吞掉。希望你后续做实验时也能保持这个习惯,希望帮到你。
本文还有配套的精品资源,点击获取