1. 从零构建自定义语义分割数据集:为什么标注格式决定了后续所有环节
语义分割这个方向,很多人卡住的地方其实不是模型结构,也不是训练调参,而是第一步——把自己的数据“喂”进框架。我见过太多人拿着几百张标注好的图,在PaddleSeg和Detectron2之间反复横跳,最后发现连数据格式都没对齐。这篇内容就是把我自己踩过的坑、验证过的流程完整梳理一遍,围绕PaddleSeg自定义语义分割数据集和Detectron2自定义语义分割数据集两条技术路线,讲清楚从数据准备到训练跑通的全部细节。
先说清楚适用人群:如果你手头有一批自己采集的图像,想训练一个语义分割模型,但不确定该选PaddleSeg还是Detectron2,也不清楚标注该怎么做、格式该怎么转、配置文件该怎么写,那这篇内容基本能覆盖你的需求。如果你已经有训练经验,但每次换数据集都要重新查文档,那也可以把这篇文章当作一份可复现的操作手册。
语义分割的本质是给每个像素分类。和检测任务不同,检测输出的是边界框,分割输出的是逐像素的类别标签。这就决定了数据标注的形式必须是像素级的掩膜(mask),而不是框。很多人第一次做分割时,习惯性地用标注工具画框,结果导出后发现根本没法用。所以第一步必须明确:语义分割的数据标注单位是像素,不是区域框。
那标注工具怎么选?常见的有LabelMe、CVAT、LabelmePlus等。LabelMe的好处是轻量、支持多边形标注、导出JSON格式,后续可以转成各种框架需要的mask格式。CVAT功能更强,支持视频标注和自动标注,但部署成本高一些。我个人的建议是:如果数据量在几千张以内,LabelMe足够用;如果超过一万张,或者需要多人协作,考虑CVAT。
标注时有一个关键决策:类别定义要一次性定死。语义分割的类别是全局的,不像检测可以后期加类。如果你一开始定义了“道路、建筑、植被”三类,后面想加“天空”,那所有已标注的数据都要重新过一遍。所以标注之前,先把类别体系想清楚,最好写一个类别说明文档,标注人员人手一份。
标注完成后,导出的数据通常是JSON加原图。JSON里记录的是多边形点的坐标和对应的类别标签。接下来要做的就是把这种“多边形点集”转换成模型能读的掩膜图。掩膜图是一张单通道的PNG,每个像素的值就是类别ID。比如背景是0,道路是1,建筑是2,植被是3。这一步转换是后续所有工作的基础,转换脚本写错了,后面训练全白搭。
我见过一个典型的错误:有人用LabelMe标注时,多边形的点顺序是顺时针,但转换脚本按逆时针解析,结果生成的掩膜图全是乱的。所以转换完成后,一定要可视化检查几张,确认掩膜和原图是对齐的。检查方法很简单,把掩膜图叠加到原图上,用半透明颜色显示,肉眼一看就知道对不对。
还有一个容易被忽略的点:图像尺寸和掩膜尺寸必须严格一致。有些标注工具在导出时会自动缩放图像,但掩膜没有同步缩放,导致训练时输入和标签对不上。解决办法是在转换脚本里强制校验尺寸,不一致的直接报错,不要静默处理。
2. PaddleSeg的数据组织逻辑:从文件目录到配置文件的全链路拆解
PaddleSeg是百度飞桨生态下的语义分割工具库,它的数据组织方式比较直观,但有几个细节如果不注意,训练时就会报各种奇怪的错误。这一章我把PaddleSeg自定义数据集的全流程拆开讲,包括目录结构、文件列表、配置文件三个核心部分。
2.1 目录结构:为什么PaddleSeg要求固定的文件夹命名
PaddleSeg默认的数据集目录结构是这样的:
dataset/ ├── images/ │ ├── 001.png │ ├── 002.png │ └── ... ├── annotations/ │ ├── 001.png │ ├── 002.png │ └── ... ├── train.txt ├── val.txt └── test.txtimages放原图,annotations放掩膜图,两个文件夹里的文件名必须一一对应。train.txt、val.txt、test.txt是划分文件,每行写一个文件名(不带路径,不带扩展名)。比如train.txt里写001,PaddleSeg就会去images/001.png和annotations/001.png找对应的文件。
这里有一个坑:文件名不要用中文,也不要用空格。PaddleSeg底层用的是OpenCV读图,OpenCV对中文路径的支持一直有问题。我试过用中文文件名,训练时直接报“image not found”,排查了半天才发现是编码问题。所以从源头就规范命名,用数字或英文加下划线。
另一个坑是掩膜图的格式。PaddleSeg要求掩膜图是单通道的PNG,像素值就是类别ID。如果你用LabelMe导出的JSON转掩膜时,不小心转成了RGB三通道,PaddleSeg会把它当成三通道图像处理,导致类别数对不上。检查方法很简单,用Python读一下:
import cv2 mask = cv2.imread('annotations/001.png', cv2.IMREAD_UNCHANGED) print(mask.shape) # 应该是 (H, W),不是 (H, W, 3)如果输出是三维的,说明掩膜图是三通道,需要转成单通道。
2.2 划分文件:训练集、验证集、测试集的比例怎么定
划分文件决定了哪些数据用于训练、哪些用于验证、哪些用于测试。常见的比例是7:2:1或8:1:1。但如果你的数据量很小(比如只有几百张),验证集和测试集可以合并,用7:3的比例,训练时从训练集里再切一部分做验证。
划分时要注意类别平衡。如果某些类别只出现在少数几张图里,随机划分可能导致验证集里完全没有这个类别,验证指标就会失真。解决办法是分层抽样:先统计每张图包含哪些类别,然后按类别分布来划分。PaddleSeg本身不提供分层抽样功能,需要自己写脚本。
我一般会写一个简单的分层抽样脚本,核心逻辑是:对每个类别,统计包含该类的图像列表,然后按比例分配到训练集和验证集。最后合并去重,确保每张图只出现在一个集合里。
2.3 配置文件:PaddleSeg的yml文件怎么写才不出错
PaddleSeg的配置文件是yml格式,核心字段包括数据集路径、类别数、图像尺寸、数据增强方式等。下面是一个最小可用的配置示例:
batch_size: 4 iters: 10000 train_dataset: type: Dataset dataset_root: dataset train_path: dataset/train.txt num_classes: 4 transforms: - type: Resize target_size: [512, 512] - type: RandomHorizontalFlip - type: Normalize mode: train val_dataset: type: Dataset dataset_root: dataset val_path: dataset/val.txt num_classes: 4 transforms: - type: Resize target_size: [512, 512] - type: Normalize mode: val optimizer: type: sgd momentum: 0.9 weight_decay: 4.0e-5 lr_scheduler: type: PolynomialDecay learning_rate: 0.01 end_lr: 0 power: 0.9 loss: types: - type: CrossEntropyLoss coef: [1] model: type: FCN backbone: type: HRNet_W18 pretrained: https://paddleseg.bj.bcebos.com/dygraph/backbone/hrnet_w18_ssld.tar.gz几个关键点:
num_classes必须和掩膜图里的最大类别ID加1一致。比如你的类别ID是0、1、2、3,那num_classes就是4。如果写错了,训练时loss会直接nan。transforms里的Resize目标尺寸要和模型输入尺寸匹配。HRNet通常用512x512或1024x1024,FCN可以用任意尺寸,但建议用32的倍数。pretrained是预训练权重的路径。PaddleSeg提供了很多预训练backbone,直接用就行。如果网络环境不稳定,可以提前下载到本地,然后把路径改成绝对路径。
还有一个容易忽略的点:dataset_root和train_path的关系。dataset_root是数据集根目录,train_path是划分文件的路径。PaddleSeg会拼接dataset_root + images + 文件名 + .png来读图。所以train_path里写的文件名不要带路径,也不带扩展名。
3. Detectron2的注册机制:为什么它的数据集配置和PaddleSeg完全不同
Detectron2是Facebook开源的目标检测和分割框架,它的设计哲学和PaddleSeg差别很大。PaddleSeg是“配置文件驱动”,Detectron2是“代码注册驱动”。这意味着用Detectron2训练自定义分割数据集时,你需要写代码来注册数据集,而不是改配置文件。这一章我把Detectron2的注册流程、标注格式、训练配置完整讲一遍。
3.1 注册数据集:Detectron2的DatasetCatalog和MetadataCatalog
Detectron2读数据的方式是:你先写一个函数,返回一个列表,列表里每个元素是一个字典,字典包含file_name、image_id、height、width、annotations等字段。然后把这个函数注册到DatasetCatalog里,再把元数据(类别名、类别ID映射等)注册到MetadataCatalog里。
下面是一个注册语义分割数据集的示例:
import os import numpy as np import cv2 from detectron2.data import DatasetCatalog, MetadataCatalog def load_sem_seg_data(image_dir, mask_dir, split_file): data = [] with open(split_file, 'r') as f: names = [line.strip() for line in f if line.strip()] for name in names: img_path = os.path.join(image_dir, name + '.png') mask_path = os.path.join(mask_dir, name + '.png') img = cv2.imread(img_path) mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) data.append({ 'file_name': img_path, 'sem_seg_file_name': mask_path, 'image_id': name, 'height': img.shape[0], 'width': img.shape[1], }) return data DatasetCatalog.register('my_dataset_train', lambda: load_sem_seg_data('dataset/images', 'dataset/annotations', 'dataset/train.txt')) MetadataCatalog.get('my_dataset_train').set(stuff_classes=['background', 'road', 'building', 'vegetation']) MetadataCatalog.get('my_dataset_train').set(stuff_colors=[[0,0,0], [255,0,0], [0,255,0], [0,0,255]])几个关键点:
sem_seg_file_name是语义分割任务专用的字段,指向掩膜图。如果是实例分割,用的是annotations字段,里面是多边形点集。stuff_classes是类别名列表,顺序必须和掩膜图里的类别ID一致。比如掩膜图里0是背景,1是道路,那stuff_classes的第一个元素就是背景,第二个是道路。stuff_colors是可视化时用的颜色,不影响训练,但影响你调试时看到的图。
注册完成后,可以通过MetadataCatalog.get('my_dataset_train').stuff_classes来验证是否注册成功。
3.2 掩膜图的读取:Detectron2对掩膜格式的要求
Detectron2读语义分割掩膜时,默认用的是cv2.imread(mask_path, cv2.IMREAD_UNCHANGED),所以掩膜图必须是单通道的PNG,像素值是类别ID。如果掩膜图是RGB三通道,Detectron2会把它当成三通道图像,导致类别数对不上。
还有一个坑:掩膜图的像素值必须是整数。有些标注工具导出的掩膜图是浮点型的,像素值是0.0、1.0、2.0这种。Detectron2读进来后,会把它当成浮点型处理,训练时loss会出问题。解决办法是在转换脚本里强制转成uint8:
mask = mask.astype(np.uint8) cv2.imwrite(mask_path, mask)3.3 训练配置:Detectron2的cfg文件怎么改
Detectron2的训练配置是通过cfg对象来管理的。下面是一个语义分割训练的最小配置:
from detectron2.config import get_cfg from detectron2 import model_zoo cfg = get_cfg() cfg.merge_from_file(model_zoo.get_config_file('COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml')) cfg.DATASETS.TRAIN = ('my_dataset_train',) cfg.DATASETS.TEST = ('my_dataset_val',) cfg.DATALOADER.NUM_WORKERS = 2 cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url('COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml') cfg.SOLVER.IMS_PER_BATCH = 2 cfg.SOLVER.BASE_LR = 0.00025 cfg.SOLVER.MAX_ITER = 10000 cfg.MODEL.ROI_HEADS.NUM_CLASSES = 4 cfg.MODEL.SEM_SEG_HEAD.NUM_CLASSES = 4 cfg.INPUT.MASK_FORMAT = 'bitmask'几个关键点:
MODEL.SEM_SEG_HEAD.NUM_CLASSES必须和你的类别数一致。如果写错了,训练时直接报维度不匹配。INPUT.MASK_FORMAT有两个选项:polygon和bitmask。语义分割用bitmask,实例分割用polygon。SOLVER.IMS_PER_BATCH是总batch size,如果显存不够,可以调小。但要注意,Detectron2的batch size是跨GPU的总和,如果你有4张卡,IMS_PER_BATCH=8意味着每张卡2张图。
还有一个容易忽略的点:Detectron2的评估指标。语义分割默认用的是mIoU,但Detectron2的SemSegEvaluator需要你注册验证集时也设置stuff_classes和stuff_colors。如果验证集没注册,训练时不会报错,但评估结果全是0。
4. 两条路线的选型对比:什么场景下该用PaddleSeg,什么场景下该用Detectron2
PaddleSeg和Detectron2都能做语义分割,但它们的定位和适用场景差别很大。这一章我从数据量、硬件环境、模型库、部署需求四个维度做一个对比,帮你决定该选哪条路线。
4.1 数据量和硬件环境:小数据量和大数据量的分水岭
PaddleSeg的优势在于轻量、易用、中文文档完善。如果你的数据量在几千张以内,硬件只有一张消费级显卡,PaddleSeg是更合适的选择。它的配置文件驱动方式让调参变得很直观,不需要写太多代码。
Detectron2的优势在于灵活、可扩展、社区生态丰富。如果你的数据量超过一万张,或者需要多卡训练,Detectron2的分布式训练和混合精度支持更成熟。但它的学习曲线更陡,需要你对PyTorch和检测框架有一定了解。
我个人的经验是:如果只是做一个demo或者小规模实验,PaddleSeg能让你在半天内跑通全流程;如果是生产环境或者需要长期迭代,Detectron2的可维护性更好。
4.2 模型库:PaddleSeg的HRNet和Detectron2的Mask R-CNN
PaddleSeg内置了大量语义分割模型,包括FCN、DeepLabV3+、HRNet、OCRNet、BiSeNetV2等。其中HRNet和OCRNet在多个公开数据集上表现很好,适合对精度要求高的场景。BiSeNetV2是轻量级模型,适合移动端部署。
Detectron2的语义分割模型主要是Mask R-CNN的扩展,以及Panoptic FPN。它的强项是实例分割和全景分割,语义分割只是其中一个功能。如果你只需要语义分割,Detectron2的模型选择不如PaddleSeg丰富。
但Detectron2的backbone库更全,包括ResNet、ResNeXt、RegNet等,而且和PyTorch生态无缝衔接。如果你需要自定义backbone,Detectron2的扩展性更好。
4.3 部署需求:PaddleSeg的Paddle Inference和Detectron2的TorchScript
PaddleSeg的部署工具链很完善,支持Paddle Inference、Paddle Lite、ONNX等多种格式。如果你需要部署到移动端或嵌入式设备,PaddleSeg的Paddle Lite是很好的选择。
Detectron2的部署主要靠TorchScript和ONNX。TorchScript的导出比较麻烦,尤其是自定义算子多的模型。ONNX的兼容性更好,但有些后处理逻辑需要自己实现。
我实测下来,如果部署环境是服务器,两者差别不大;如果是移动端,PaddleSeg的Paddle Lite更省心。
4.4 选型决策表
| 维度 | PaddleSeg | Detectron2 |
|---|---|---|
| 数据量 | 几千张以内 | 一万张以上 |
| 硬件 | 单卡 | 多卡 |
| 模型库 | 丰富,专为分割设计 | 以检测为主,分割为辅 |
| 部署 | Paddle Lite,移动端友好 | TorchScript/ONNX,服务器友好 |
| 学习曲线 | 低,配置文件驱动 | 高,代码注册驱动 |
| 中文文档 | 完善 | 较少 |
这张表不是绝对的,只是一个参考。实际选型时,还要考虑团队的技术栈和长期维护成本。
5. 从标注到训练跑通:我踩过的五个坑和对应的解决方案
这一章我把实际操作中遇到的五个典型问题整理出来,每个问题都给出排查过程和解决方案。这些坑在官方文档里通常不会写,但实际做项目时几乎一定会遇到。
5.1 坑一:掩膜图类别ID不连续导致loss为nan
问题现象:训练时loss直接变成nan,没有任何报错信息。
排查过程:先检查了学习率,调小后还是nan。然后检查了数据,发现掩膜图里的类别ID是0、1、3、4,跳过了2。PaddleSeg和Detectron2都假设类别ID是连续的,从0到num_classes-1。如果中间有跳过的ID,计算loss时会出现索引越界或除零。
解决方案:写一个脚本,把掩膜图里的类别ID重新映射成连续的。比如原来的0、1、3、4映射成0、1、2、3。映射关系要保存下来,推理时再映射回去。
import numpy as np import cv2 def remap_mask(mask_path, mapping): mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) new_mask = np.zeros_like(mask) for old_id, new_id in mapping.items(): new_mask[mask == old_id] = new_id cv2.imwrite(mask_path, new_mask)5.2 坑二:图像尺寸不一致导致训练中断
问题现象:训练到一半突然报错,提示图像尺寸不匹配。
排查过程:检查了数据集,发现大部分图像是1024x1024,但有几张是1920x1080。PaddleSeg的Resize变换会把所有图像缩放到统一尺寸,但如果原图的长宽比差异太大,缩放后会出现黑边或拉伸。Detectron2的ResizeShortestEdge会保持长宽比,但最大尺寸有限制。
解决方案:在数据预处理阶段,统一把所有图像缩放到相同尺寸。如果长宽比差异大,用padding补齐,而不是直接拉伸。PaddleSeg的Resize支持interp参数,可以选择插值方式。Detectron2的ResizeShortestEdge可以设置max_size,超过的会等比缩放。
5.3 坑三:验证集mIoU为0但训练loss正常
问题现象:训练loss一直在下降,但验证集的mIoU始终是0。
排查过程:检查了验证集的注册,发现stuff_classes设置成了训练集的类别,但验证集的掩膜图里类别ID和训练集不一致。Detectron2的SemSegEvaluator会用stuff_classes的长度来推断类别数,如果对不上,评估结果全是0。
解决方案:确保训练集和验证集的stuff_classes完全一致,包括顺序和数量。如果验证集有训练集没有的类别,要么合并到训练集,要么在评估时忽略。
5.4 坑四:数据增强导致掩膜和原图不对齐
问题现象:训练时可视化发现掩膜和原图错位了。
排查过程:检查了数据增强的配置,发现用了RandomRotation,但旋转角度是随机的,原图和掩膜分别做了旋转,导致错位。PaddleSeg的transforms里,有些变换是同时作用于原图和掩膜的,有些只作用于原图。如果配置错了,就会错位。
解决方案:仔细看PaddleSeg的文档,确认每个变换是否支持掩膜同步。RandomHorizontalFlip、RandomVerticalFlip、Resize是同步的,RandomRotation需要设置im_padding_value和label_padding_value。Detectron2的AugmentationList里,RandomRotation也是同步的,但需要确保aug_input同时包含图像和掩膜。
5.5 坑五:多卡训练时batch size设置错误
问题现象:单卡训练正常,多卡训练时loss异常。
排查过程:检查了IMS_PER_BATCH的设置,发现单卡时设的是4,多卡时还是4。Detectron2的IMS_PER_BATCH是总batch size,多卡时会自动分配到每张卡。如果总batch size太小,每张卡的batch size就是1,BatchNorm会出问题。
解决方案:多卡训练时,IMS_PER_BATCH要设成单卡的倍数。比如单卡batch size是4,4张卡就设16。同时要调整学习率,通常按线性缩放规则,学习率乘以卡数。
6. 训练完成之后:模型导出、推理和效果验证的完整闭环
训练跑通只是第一步,真正要用起来,还需要把模型导出、做推理、验证效果。这一章我把PaddleSeg和Detectron2的导出和推理流程分别讲一遍。
6.1 PaddleSeg的模型导出和推理
PaddleSeg训练完成后,会在output目录下生成best_model文件夹,里面是动态图权重。要导出成静态图,用export.py:
python export.py \ --config config.yml \ --model_path output/best_model/model.pdparams \ --save_dir output/static_model导出后,用predict.py做推理:
python predict.py \ --config config.yml \ --model_path output/static_model/model.pdmodel \ --image_path test.jpg \ --save_dir output/result推理结果是一张伪彩色图,每个类别用不同颜色表示。如果要叠加到原图上,可以用--overlay参数。
6.2 Detectron2的模型导出和推理
Detectron2的模型导出用torchscript或onnx。下面是一个导出TorchScript的示例:
import torch from detectron2.modeling import build_model from detectron2.checkpoint import DetectionCheckpointer cfg = get_cfg() cfg.merge_from_file('config.yml') model = build_model(cfg) DetectionCheckpointer(model).load('output/model_final.pth') model.eval() # 导出TorchScript scripted_model = torch.jit.script(model) torch.jit.save(scripted_model, 'output/model.ts')推理时,用DefaultPredictor:
from detectron2.engine import DefaultPredictor predictor = DefaultPredictor(cfg) outputs = predictor(image)outputs里包含sem_seg字段,是每个像素的类别概率。取argmax就得到分割结果。
6.3 效果验证:怎么判断模型是真的学到了
训练loss下降不代表模型学到了有用的东西。我一般会做三个验证:
- 可视化验证:随机抽几张验证集的图,把预测结果和真实标签叠加显示。如果预测结果和真实标签高度重合,说明模型学到了。
- 混淆矩阵:统计每个类别的预测准确率和召回率。如果某个类别的召回率特别低,说明模型对这个类别不敏感,可能需要增加该类别的样本。
- 边界验证:专门看类别边界处的预测结果。语义分割最难的地方就是边界,如果边界处预测得很乱,说明模型的分辨率不够,或者感受野不合适。
我踩过的一个坑是:验证集mIoU很高,但实际推理时效果很差。后来发现是验证集和训练集的数据分布太接近,模型过拟合了。解决办法是重新划分验证集,确保验证集和训练集的采集场景不同。
6.4 推理速度优化:从秒级到毫秒级
如果推理速度太慢,可以从三个方向优化:
- 模型轻量化:换用BiSeNetV2、FastSCNN等轻量级模型。
- 输入分辨率:降低输入图像的尺寸,比如从1024x1024降到512x512。
- 推理引擎:PaddleSeg用Paddle Inference,Detectron2用TensorRT。TensorRT的加速效果很明显,但配置比较复杂。
我实测下来,BiSeNetV2在512x512输入下,单张图推理时间可以做到10ms以内,基本满足实时需求。
7. 一些零散但重要的经验:标注质量、类别平衡和迭代策略
最后这一章我分享一些零散但很重要的经验,这些经验不是某个具体步骤,而是贯穿整个项目的原则。
7.1 标注质量比数据量更重要
很多人觉得数据量越大越好,但如果标注质量差,再多的数据也没用。我见过一个项目,标注人员为了赶进度,多边形画得很粗糙,边界处偏差十几个像素。结果模型训练出来后,边界处全是锯齿。后来重新标注了一遍,数据量没变,但mIoU提升了5个点。
标注质量的控制方法:定期抽检,每个标注人员每天抽10张图检查;制定标注规范,明确边界处的标注标准;用交叉验证,两个人标同一张图,对比差异。
7.2 类别不平衡的处理策略
语义分割里,背景类通常占大部分像素,小类别(比如行人、交通标志)只占很少像素。如果不处理,模型会倾向于预测背景,小类别的召回率很低。
处理方法有几种:
- 加权loss:给小类别更高的权重。PaddleSeg的
CrossEntropyLoss支持weight参数,Detectron2的SemSegLoss也支持。 - 重采样:在数据加载时,对小类别多的图像更高的采样概率。
- 数据增强:对小类别做copy-paste增强,把小类别的像素复制到其他图像上。
我一般先用加权loss,如果效果不够,再加重采样。copy-paste增强效果最好,但实现起来复杂一些。
7.3 迭代策略:从粗到细的训练流程
我的习惯是分两阶段训练:
第一阶段:用低分辨率(比如256x256)快速训练一个粗模型,确认数据管道没问题,类别定义合理。这个阶段通常几个小时就能跑完。
第二阶段:用高分辨率(比如512x512或1024x1024)精调,用第一阶段的结果做初始化。这个阶段需要更长时间,但效果更好。
这样做的好处是:如果数据有问题,第一阶段就能发现,不用等到第二阶段浪费大量时间。而且第一阶段的粗模型可以用来做难例挖掘,找出模型预测差的样本,重点标注。
7.4 模型版本管理:别让实验结果变成一团乱麻
做分割项目时,会有很多实验:不同的backbone、不同的学习率、不同的数据增强。如果不做版本管理,过几天就忘了哪个配置对应哪个结果。
我的做法是:每次实验建一个文件夹,命名格式是日期_模型_关键参数,比如20240501_hrnet_lr0.01_bs4。文件夹里放配置文件、训练日志、验证结果。然后用一个表格记录每次实验的mIoU和备注。这样回溯起来很方便。
7.5 一个容易被忽略的细节:图像归一化参数
PaddleSeg和Detectron2的默认归一化参数都是ImageNet的均值和方差。但如果你的数据集和ImageNet差异很大(比如医学图像、遥感图像),用默认参数效果可能不好。解决办法是统计你自己数据集的均值和方差,然后替换掉默认值。
统计方法很简单:
import numpy as np from PIL import Image import os def compute_mean_std(image_dir): means = [] stds = [] for name in os.listdir(image_dir): img = np.array(Image.open(os.path.join(image_dir, name))) / 255.0 means.append(img.mean(axis=(0, 1))) stds.append(img.std(axis=(0, 1))) return np.mean(means, axis=0), np.mean(stds, axis=0)算出来的均值和方差填到配置文件的Normalize里就行。这个细节看起来小,但对精度的影响可能有1到2个点。
7.6 关于PaddleSeg和Detectron2的混合使用
有人问能不能两个框架混用,比如用PaddleSeg训练,用Detectron2推理。技术上可行,但没必要。两个框架的模型结构、预处理、后处理都不一样,混用容易出问题。如果非要混用,建议用ONNX作为中间格式,把PaddleSeg的模型导出成ONNX,再用Detectron2的ONNX运行时加载。但这样会损失一些性能,而且调试起来很麻烦。
我的建议是:选一个框架,从头用到尾。如果团队里有人熟悉PaddlePaddle,就用PaddleSeg;如果熟悉PyTorch,就用Detectron2。不要为了用某个功能而混用框架。
7.7 最后分享一个调试技巧:用单张图过拟合
如果训练效果不好,先别急着调参。拿一张图,用模型过拟合它。如果模型连一张图都过拟合不了,说明代码有问题,不是参数问题。过拟合一张图的配置:batch size设1,学习率调大,训练几百个iteration。如果loss降不下去,检查数据管道、loss函数、模型结构。这个技巧帮我省了很多排查时间。
7.8 关于数据增强的取舍
数据增强不是越多越好。有些增强(比如颜色抖动)对语义分割有帮助,有些增强(比如随机裁剪)可能会破坏类别边界。我的经验是:水平翻转、垂直翻转、随机缩放、颜色抖动是安全的;随机旋转、随机裁剪要谨慎使用,尤其是当类别边界很重要时。
PaddleSeg的transforms里,RandomHorizontalFlip和RandomVerticalFlip是默认开启的,RandomRotation需要手动配置。Detectron2的AugmentationList里,RandomFlip和ResizeShortestEdge是常用的,RandomRotation需要自己实现。
7.9 关于评估指标的选择
语义分割的评估指标主要是mIoU和mAcc。mIoU对类别不平衡更敏感,mAcc对整体准确率更敏感。如果小类别很重要,看mIoU;如果整体效果更重要,看mAcc。我一般两个都看,但以mIoU为主。
还有一个指标是边界F1(Boundary F1),专门评估边界处的分割质量。如果应用场景对边界要求高(比如自动驾驶、医学图像),可以加上这个指标。PaddleSeg和Detectron2都不直接支持Boundary F1,需要自己实现。
7.10 关于模型部署的注意事项
部署时要注意输入输出的格式。PaddleSeg的静态图模型输入是[N, C, H, W]的float32张量,输出是[N, num_classes, H, W]的概率图。Detectron2的TorchScript模型输入是[C, H, W]的uint8张量,输出是包含sem_seg字段的字典。
部署到移动端时,要注意模型大小和推理速度。PaddleSeg的Paddle Lite支持量化,可以把模型压缩到原来的1/4。Detectron2的TorchScript也支持量化,但需要自己写量化脚本。
7.11 关于数据集的版本管理
数据集也会迭代,比如增加了新类别、修正了标注错误。每次迭代都要记录版本号,并且保留旧版本。因为模型训练依赖数据集,如果数据集变了,模型效果也会变。我一般用dataset_v1、dataset_v2这样的命名,每次迭代写一个changelog,记录改了什么。
7.12 关于训练日志的分析
训练日志里有很多信息,不只是loss。我一般会关注这几个指标:学习率的变化、梯度范数、每个类别的loss。如果某个类别的loss一直很高,说明模型对这个类别学不好,可能需要增加样本或调整权重。如果梯度范数突然变大,说明训练不稳定,可能需要调小学习率或加梯度裁剪。
PaddleSeg的日志里会打印每个iteration的loss和lr,Detectron2的日志里会打印每个iteration的total_loss和各个子loss。分析日志时,可以用TensorBoard或WandB做可视化,更直观。
7.13 关于多任务学习
有时候一个模型需要同时做分割和检测。PaddleSeg和Detectron2都支持多任务,但配置起来比较复杂。我的建议是:如果两个任务的相关性很强(比如分割和检测都是识别同一批物体),可以尝试多任务;如果相关性不强,分开训练更简单。
多任务训练时,要注意loss的权重。如果分割loss和检测loss量级差很多,需要手动调权重。PaddleSeg的loss配置里可以设置coef,Detectron2的MODEL.MASK_ON和MODEL.KEYPOINT_ON可以控制是否开启多任务。
7.14 关于预训练模型的选择
PaddleSeg和Detectron2都提供了大量预训练模型。选择预训练模型时,要考虑两个因素:backbone的容量和预训练数据集的相似度。如果目标数据集和ImageNet相似,用ImageNet预训练的backbone就行;如果差异很大,可以考虑用目标数据集做自监督预训练。
我实测下来,HRNet_W18在大多数场景下够用,如果精度不够,换HRNet_W48。Detectron2的ResNet-50是默认选择,如果显存够,换ResNet-101。
7.15 关于推理结果的保存
推理结果一般保存为PNG,每个像素的值是类别ID。如果要可视化,可以映射成伪彩色。PaddleSeg的predict.py支持--save_dir参数,保存伪彩色图。Detectron2的Visualizer可以画分割结果,但需要自己写保存逻辑。
保存推理结果时,要注意文件名和原图对应。我一般用原图的文件名加_pred后缀,比如001_pred.png。这样后续做对比分析时很方便。
7.16 关于模型的可解释性
语义分割模型的可解释性不如分类模型直观。如果想知道模型为什么预测某个像素是某个类别,可以用Grad-CAM或Score-CAM做可视化。PaddleSeg和Detectron2都不直接支持这些方法,需要自己实现。我一般用pytorch-grad-cam库,对Detectron2的模型做可视化。
可解释性分析可以帮助发现模型的偏见。比如模型可能过度依赖纹理信息,而不是形状信息。如果发现这种问题,可以通过数据增强或模型结构来纠正。
7.17 关于训练时间的估算
训练时间取决于数据量、模型大小、硬件配置。我一般用这个公式估算:总时间 = (数据量 / batch_size) * iteration时间 * epoch数。比如10000张图,batch_size=4,iteration时间0.5秒,训练100个epoch,总时间大约是10000/40.5100 = 125000秒,约35小时。
实际训练时,可以用time命令或框架自带的计时器来测量iteration时间。如果太慢,可以优化数据加载(增加num_workers)、用混合精度训练、换更快的GPU。
7.18 关于模型的保存和加载
PaddleSeg保存的是动态图权重,加载时用model.set_state_dict()。Detectron2保存的是model_final.pth,加载时用DetectionCheckpointer。保存时要注意保存优化器的状态,方便断点续训。
PaddleSeg的train.py支持--resume_model参数,可以从断点恢复。Detectron2的DefaultTrainer支持resume=True,但需要手动设置cfg.OUTPUT_DIR。
7.19 关于分布式训练的配置
PaddleSeg的分布式训练用launch工具,Detectron2用torch.distributed.launch。配置分布式训练时,要注意batch_size和learning_rate的缩放。PaddleSeg的launch会自动处理,Detectron2需要手动设置。
分布式训练时,数据加载器要用DistributedSampler,确保每个GPU读到不同的数据。PaddleSeg的Dataset默认支持分布式,Detectron2的build_detection_train_loader也支持。
7.20 关于模型压缩和加速
如果模型太大,推理太慢,可以尝试模型压缩。PaddleSeg支持剪枝和量化,Detectron2支持量化。剪枝可以去掉不重要的通道,量化可以把float32转成int8。我实测下来,量化可以把模型大小压缩到1/4,推理速度提升2到3倍,精度损失在1个点以内。
量化时要注意校准集的选择。校准集要能代表真实数据的分布,否则量化后的模型精度会下降很多。PaddleSeg的quant.py支持自动校准,Detectron2的量化需要自己写校准脚本。
7.21 关于模型的跨平台部署
PaddleSeg的模型可以导出成ONNX,然后在TensorRT、OpenVINO等平台上运行。Detectron2的模型也可以导出成ONNX,但有些自定义算子不支持。跨平台部署时,要注意算子兼容性和精度对齐。
我一般先在服务器上验证ONNX模型的精度,确认和原模型一致后,再部署到目标平台。如果精度不一致,检查算子的实现差异,或者用ONNX Runtime的调试工具排查。
7.22 关于数据集的隐私和安全
如果数据集包含敏感信息(比如人脸、车牌),训练前要做脱敏处理。PaddleSeg和Detectron2都不提供脱敏功能,需要自己实现。我一般用模糊或马赛克处理敏感区域,然后再标注。
脱敏处理时,要注意不要破坏分割目标的完整性。比如如果分割目标是行人,模糊人脸不影响分割,但如果分割目标是车牌,模糊车牌就会影响。所以脱敏策略要根据任务来定。
7.23 关于模型的持续迭代
模型上线后,会收到新的数据。这些数据可以用来持续迭代模型。迭代时,要注意新旧数据的分布差异。如果新数据和旧数据差异很大,直接混合训练可能导致模型遗忘旧数据。解决办法是用增量学习或持续学习的方法,或者定期用全量数据重新训练。
我一般每季度做一次全量重新训练,中间用增量学习做小版本更新。增量学习时,会保留一部分旧数据,防止遗忘。
7.24 关于团队协作的规范
如果团队多人协作,要制定统一的规范:数据标注规范、代码规范、实验记录规范。数据标注规范前面讲过了,代码规范包括命名、注释、提交信息。实验记录规范包括每次实验的配置、结果、结论。
我一般用Git做代码版本管理,用DVC做数据版本管理,用WandB做实验记录。这样任何人接手项目时,都能快速了解历史实验和当前状态。
7.25 关于模型的可复现性
可复现性是机器学习项目的基本要求。要保证可复现,需要固定随机种子、记录所有配置、保存训练日志。PaddleSeg和Detectron2都支持设置随机种子,但有些操作(比如数据加载的多线程)可能引入不确定性。
我一般会在训练脚本开头设置random.seed()、np.random.seed()、paddle.seed()或torch.manual_seed()。如果用了CUDA,还要设置torch.cuda.manual_seed_all()。这样基本能保证每次训练的结果一致。
7.26 关于模型的鲁棒性测试
模型训练好后,要做鲁棒性测试。比如对图像加噪声、改变亮度、旋转角度,看模型的预测是否稳定。如果模型对噪声很敏感,说明泛化能力不够,需要增加数据增强或正则化。
鲁棒性测试的方法:生成一批扰动后的图像,用模型推理,统计预测结果的变化。如果变化很大,说明模型不稳定。PaddleSeg和Detectron2都不直接支持鲁棒性测试,需要自己写脚本。
7.27 关于模型的公平性评估
如果模型用于决策场景(比如医疗诊断、自动驾驶),要做公平性评估。比如检查模型在不同性别、年龄、肤色的人群上的表现是否一致。如果某个群体的表现明显差,说明模型有偏见,需要调整数据或模型。
公平性评估需要标注群体的属性,然后分组统计指标。PaddleSeg和Detectron2都不直接支持公平性评估,需要自己实现。
7.28 关于模型的能耗优化
如果模型部署在边缘设备上,能耗是一个重要指标。优化能耗的方法包括:降低推理频率、用低功耗硬件、优化模型结构。PaddleSeg的Paddle Lite支持功耗优化,Detectron2的TorchScript也支持。
我实测下来,BiSeNetV2在移动端CPU上的功耗比HRNet低很多,适合电池供电的设备。如果对精度要求不高,可以优先考虑轻量级模型。
7.29 关于模型的更新策略
模型上线后,什么时候更新?我的经验是:如果新数据量超过原数据量的10%,或者模型在新数据上的表现下降超过5%,就考虑更新。更新时,先用新数据做增量学习,如果效果不好,再用全量数据重新训练。
更新策略要写在文档里,让团队所有人都知道。更新前要做A/B测试,确认新模型比旧模型好。更新后要监控一段时间,确保没有异常。
7.30 关于模型的退役
模型也有生命周期。当模型的表现下降到不可接受,或者有更好的模型替代时,就要退役。退役时,要保留旧模型的权重和配置,方便回溯。同时要通知所有依赖该模型的服务,做好迁移。
退役策略也要写在文档里。我一般会保留最近三个版本的模型,更早的归档到冷存储。这样既节省空间,又能快速回滚。
这些经验看起来零散,但每一条都是实际项目中踩过的坑。语义分割是一个工程性很强的任务,模型结构只是其中一部分,数据、训练、部署、维护每个环节都有很多细节。希望这些内容能帮你少走一些弯路。