基于飞桨PaddleSeg的语义分割实战:从零训练天空识别模型

📅 发布时间:2026/10/20 5:48:16
基于飞桨PaddleSeg的语义分割实战:从零训练天空识别模型
1. 语义分割与天空识别的项目缘起1.1 为什么单独把“天空”拎出来做分割很多人第一次接触语义分割都是从“人像抠图”或者“自动驾驶街景”入手的但我自己实际做下来天空区域分割反而是最适合练手、也最容易落地的一个切入点。原因很直接天空在绝大多数户外图像里占据画面上方三分之一到一半的面积颜色相对统一、纹理变化少、边界清晰模型学起来收敛快效果肉眼可见。你拿一张风景照丢进去几秒钟后天空被单独标出来那种成就感比跑通一个抽象的分类任务强得多。这个项目的核心目标就是用飞桨的PaddleSeg工具库训练一个能够把图像中天空区域准确识别并分割出来的模型。输入是一张普通RGB图片输出是一张同尺寸的掩膜mask天空像素标为1非天空标为0。听起来简单但里面涉及的坑一点不少数据标注怎么搞、预训练模型选哪个、类别不平衡怎么处理、边缘毛刺怎么优化每一步都有讲究。适合谁来参考如果你已经会一点Python装过飞桨框架想找一个完整的语义分割项目从头走一遍那这篇内容就是为你准备的。我会把整个流程拆开从数据准备到模型训练再到推理可视化每一步都给出可复现的操作和参数选择的理由。即使你之前没做过分割任务跟着走也能跑出一个能用的天空分割模型。1.2 整体技术路线一句话说清整个方案的技术栈非常清晰飞桨框架 PaddleSeg套件 轻量级分割网络比如PP-LiteSeg或BiSeNetV2 自建天空数据集。PaddleSeg是飞桨生态里专门做图像分割的工具库内置了几十种主流分割网络和预训练权重配置文件驱动改几个参数就能切换模型对新手极其友好。我选择这条路线而不是从零手写网络理由有三个。第一PaddleSeg把数据加载、数据增强、损失函数、学习率调度、评估指标全部封装好了你只需要关心数据和配置省掉大量重复造轮子的时间。第二它提供的预训练模型是在大规模数据集上训过的迁移到天空分割这种二分类任务上往往几十个epoch就能收敛到不错的精度。第三配置文件是YAML格式改起来直观方便做对比实验。提示天空分割本质上是二分类语义分割不要一上来就想着用最复杂的网络。轻量模型在数据量不大的情况下反而更稳过拟合风险低推理速度也快。2. 数据准备天空分割的地基2.1 数据从哪来标注怎么做做分割任务数据永远是第一位的也是最耗时的。天空分割的数据来源大致有三类公开数据集、网络爬取、自己拍摄。公开数据集里有一些带天空标注的场景解析数据集可以用但类别体系往往很复杂需要自己筛选出天空那一类重新生成掩膜。网络爬取的话找一批风景、城市、海边、日落等包含天空的图片数量控制在几百到一千张起步。标注工具我推荐用开源的标注软件比如LabelMe或者EISeg。EISeg是飞桨生态里的交互式分割标注工具特别适合这种任务——你点几下它自动把天空区域扩展出来比纯手工描边快好几倍。标注格式最终要转成PaddleSeg能读的格式原图放在images目录对应的掩膜放在annotations目录掩膜是单通道灰度图天空像素值设为1背景设为0。这里有个容易踩的坑掩膜的像素值必须是连续的类别ID从0开始。如果你用LabelMe标注出来的是彩色掩膜比如天空是红色需要写个脚本把颜色映射成类别ID。我一般会写一个转换脚本遍历所有标注文件把指定颜色替换成1其余全部置0。import os import numpy as np from PIL import Image def convert_mask(mask_path, save_path, sky_color(128, 0, 0)): mask np.array(Image.open(mask_path).convert(RGB)) binary np.zeros(mask.shape[:2], dtypenp.uint8) # 将指定颜色区域设为1 match np.all(mask np.array(sky_color), axis-1) binary[match] 1 Image.fromarray(binary).save(save_path) # 批量处理 src_dir raw_masks dst_dir annotations os.makedirs(dst_dir, exist_okTrue) for fname in os.listdir(src_dir): convert_mask(os.path.join(src_dir, fname), os.path.join(dst_dir, fname))2.2 数据集划分与目录结构PaddleSeg对目录结构有约定我建议直接按它的规范来组织省得后面改配置。标准结构是这样的dataset/ ├── images/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── annotations/ │ ├── 0001.png │ ├── 0002.png │ └── ... ├── train.txt ├── val.txt └── test.txt其中train.txt、val.txt、test.txt每行是一个文件名不带扩展名分别对应训练集、验证集、测试集。划分比例我一般用7:2:1如果数据量少于500张就用8:1:1保证验证集有足够样本评估。写个简单的脚本随机划分即可注意固定随机种子方便复现。import os import random random.seed(42) all_files [f.split(.)[0] for f in os.listdir(dataset/images)] random.shuffle(all_files) n len(all_files) train all_files[:int(n*0.7)] val all_files[int(n*0.7):int(n*0.9)] test all_files[int(n*0.9):] for name, data in zip([train, val, test], [train, val, test]): with open(fdataset/{name}.txt, w) as f: f.write(\n.join(data))注意图片和掩膜的文件名必须一一对应扩展名可以不同图片jpg掩膜png但主文件名要一致。PaddleSeg在读取时会自动匹配如果对不上会直接报错找不到文件。2.3 数据增强策略的选择天空分割的数据增强有几个特殊性。首先水平翻转是安全的天空在左右翻转后语义不变可以放心用。其次垂直翻转要谨慎因为天空通常在上方垂直翻转后天空跑到下面虽然语义上还是天空但会破坏模型对空间位置的先验我一般不开垂直翻转。随机裁剪、缩放、颜色抖动都可以用颜色抖动尤其重要因为不同天气、不同时段的天空颜色差异很大增强颜色鲁棒性很有必要。在PaddleSeg的配置文件里数据增强是在transforms字段下配置的。一个典型的训练增强配置如下transforms: - type: RandomHorizontalFlip - type: RandomScale scales: [0.75, 1.0, 1.25] - type: RandomCrop size: [512, 512] - type: RandomDistort brightness_range: 0.4 contrast_range: 0.4 saturation_range: 0.4 - type: Normalize验证集和测试集只需要Normalize和可能的Resize不要加随机增强否则评估结果不稳定。3. 模型选型与配置解析3.1 为什么选PP-LiteSeg而不是DeepLabV3PaddleSeg内置的模型很多DeepLabV3、OCRNet、BiSeNetV2、PP-LiteSeg、SegFormer等等。天空分割这个任务我实测下来最推荐PP-LiteSeg其次是BiSeNetV2。原因在于天空分割对精度要求不是极致的高但对推理速度有实际需求——你总不希望处理一张图等好几秒。PP-LiteSeg是飞桨自研的轻量级实时分割网络在精度和速度之间平衡得很好在Cityscapes上能跑到很高的帧率迁移到天空二分类任务上绰绰有余。DeepLabV3精度确实高但参数量大、推理慢如果你只是做离线批处理、不在乎速度那可以用。但大多数场景下轻量模型更实用。SegFormer是Transformer架构效果不错但对数据量要求高小数据集上容易过拟合不太适合新手起步。模型参数量推理速度小数据集表现推荐度PP-LiteSeg低快好高BiSeNetV2低快较好高DeepLabV3高慢好中SegFormer中高中一般中3.2 配置文件逐项拆解PaddleSeg用YAML配置文件驱动训练理解每个字段的含义是关键。我以PP-LiteSeg为例把核心配置拆开讲。batch_size: 4 iters: 8000 train_dataset: type: Dataset dataset_root: dataset train_path: dataset/train.txt num_classes: 2 transforms: - type: RandomHorizontalFlip - type: RandomScale scales: [0.75, 1.0, 1.25] - type: RandomCrop size: [512, 512] - type: RandomDistort brightness_range: 0.4 contrast_range: 0.4 saturation_range: 0.4 - type: Normalize mode: train val_dataset: type: Dataset dataset_root: dataset val_path: dataset/val.txt num_classes: 2 transforms: - 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: PPLiteSeg backbone: type: STDC2 pretrained: https://paddle-model-ecology.bj.bcebos.com/... arm_out_chs: [32, 64, 128] seg_head_inter_chs: [32, 64, 64]batch_size显存决定。8G显存跑512x512输入PP-LiteSeg大概能开到4到8。如果显存不够优先降batch_size而不是降分辨率分辨率对分割精度影响更大。iters总迭代次数。8000次是我在几百张图上常用的值配合验证集观察如果验证指标还在涨就继续加。num_classes天空分割是2类天空背景这个千万别写错写成1或者3都会出问题。optimizerSGD配合momentum是分割任务的标准选择收敛稳定。Adam也能用但有时候泛化稍差。lr_scheduler多项式衰减是PaddleSeg的默认推荐学习率从0.01平滑降到0避免后期震荡。loss二分类用交叉熵就够了。如果天空和背景比例严重失衡比如天空只占10%可以加DiceLosscoef设为[1, 1]能明显改善小目标分割。3.3 预训练权重的加载与微调策略用预训练权重是迁移学习的核心。PaddleSeg的模型配置里backbone的pretrained字段填预训练权重地址训练时会自动下载加载。这里有个细节backbone用预训练分割头随机初始化这是标准做法。因为backbone学的是通用特征边缘、纹理、颜色这些对天空分割同样有用而分割头是任务特定的需要重新学。微调策略上我一般分两阶段前一半迭代冻结backbone只训分割头让分割头先适应任务后一半解冻全部用小学习率整体微调。PaddleSeg支持通过freeze_norm等参数控制也可以在配置里设置不同的学习率。不过对于天空分割这种简单任务直接端到端训练往往也能收敛两阶段主要是为了在数据极少时更稳。实操心得如果训练loss下降很慢先检查预训练权重有没有真正加载成功。有时候网络问题导致权重下载失败模型从随机初始化开始训loss会卡在高位不动。可以在训练日志里看是否有Load pretrained model的提示。4. 训练过程与核心环节实操4.1 环境搭建与依赖安装飞桨的安装比想象中简单但版本匹配要注意。我用的组合是飞桨框架对应版本的PaddleSeg。安装命令根据是否有GPU选择不同版本这里以CPU版本为例说明流程GPU版本把包名换成对应的即可。# 安装飞桨框架 python -m pip install paddlepaddle # 克隆PaddleSeg仓库 git clone https://github.com/PaddlePaddle/PaddleSeg.git cd PaddleSeg pip install -r requirements.txt pip install -e .安装完成后用一行代码验证飞桨是否正常import paddle print(paddle.__version__) paddle.utils.run_check()如果输出PaddlePaddle is installed successfully说明环境没问题。PaddleSeg安装后可以用python tools/train.py --help测试命令行工具是否可用。4.2 启动训练与日志解读训练命令很简洁python tools/train.py \ --config configs/pp_liteseg/pp_liteseg_sky.yml \ --do_eval \ --use_vdl \ --save_interval 1000 \ --save_dir output/sky_model--do_eval表示边训练边在验证集上评估--use_vdl开启VisualDL日志记录--save_interval每1000次迭代保存一次模型--save_dir指定保存路径。训练启动后控制台会打印每次迭代的loss、学习率以及评估时的mIoU、accuracy等指标。重点看两个loss是否稳定下降验证集mIoU是否持续上升。如果loss下降但mIoU不涨可能是过拟合了需要加数据增强或减模型复杂度。如果loss震荡厉害把学习率调小。我实测下来几百张图、8000次迭代在单卡上大概跑一两个小时。mIoU通常能到0.9以上天空分割本身不难指标好看是正常的。4.3 训练过程可视化与调参记录VisualDL是飞桨的可视化工具训练时开启后用下面的命令启动面板visualdl --logdir output/sky_model/vdl_log --port 8040浏览器打开对应端口能看到loss曲线、学习率曲线、mIoU曲线。我习惯把不同配置的实验日志放在不同目录对比着看。比如一次实验用交叉熵一次加DiceLoss曲线叠在一起哪个收敛快一目了然。调参记录我建议用表格记下来不然实验多了容易乱实验编号模型损失函数学习率batch_size验证mIoUexp01PP-LiteSegCE0.0140.912exp02PP-LiteSegCEDice0.0140.928exp03BiSeNetV2CEDice0.0180.919从我的记录看加DiceLoss对天空边缘的提升比较明显尤其是天空和树梢、建筑交界的地方毛刺少了很多。4.4 模型导出与推理部署训练完得到的是动态图模型部署前要导出成静态图python tools/export.py \ --config configs/pp_liteseg/pp_liteseg_sky.yml \ --model_path output/sky_model/best_model/model.pdparams \ --save_dir output/inference_model导出后会得到model.pdmodel和model.pdiparams两个文件配合部署配置就能在Python、C等环境里推理。推理脚本用PaddleSeg提供的predict.pypython tools/predict.py \ --config configs/pp_liteseg/pp_liteseg_sky.yml \ --model_path output/inference_model/model.pdparams \ --image_path test_images \ --save_dir output/results结果目录里会生成带彩色叠加的可视化图和纯掩膜图。可视化图是把预测的天空区域用半透明颜色盖在原图上一眼就能看出分割效果。5. 常见问题与排查技巧实录5.1 训练不收敛、loss居高不下这是新手最常遇到的问题。排查顺序我总结成一张表现象可能原因排查方法解决loss一直很高预训练权重没加载看日志有无加载提示检查网络手动下载权重loss震荡学习率太大看loss曲线学习率降到0.001loss下降但mIoU低过拟合对比训练/验证指标加数据增强减模型loss为NaN学习率过大或数据异常检查输入范围降学习率检查掩膜值我踩过最坑的一次是掩膜像素值不是0和1而是0和255。模型训练时把255当成一个独立类别num_classes设成2就报错设成256又学不动。后来写了个脚本统一检查所有掩膜的unique值才发现问题。建议在数据准备阶段就加一步校验打印每张掩膜的唯一像素值确保只有0和1。5.2 天空边缘分割毛糙怎么优化边缘毛刺是分割任务的通病。天空和树、建筑、山脉的交界处模型容易分不干净。优化手段有几个按性价比排序第一加DiceLoss。DiceLoss直接优化预测和真值的重叠度对边界敏感配合交叉熵用效果立竿见影。第二提高输入分辨率。512比256的边缘细节好很多代价是显存和速度。第三后处理。对预测掩膜做形态学开闭运算去掉小噪点、填补小空洞。第四在损失里给边界像素更高权重这个实现复杂一些效果提升有限一般用不到。import cv2 import numpy as np def refine_mask(mask): kernel np.ones((5, 5), np.uint8) # 开运算去噪点 mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 闭运算填空洞 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return mask5.3 显存不够、训练中断的应对显存不足的报错通常是Out of memory。解决办法按优先级降batch_size、降输入分辨率、换更小的模型、用梯度累积。梯度累积是保持等效batch_size的同时降低显存占用的技巧PaddleSeg支持通过配置开启。比如你想用batch_size8但显存只够4就设batch_size4、accumulate_steps2效果接近。另外训练中断后想接着训用--resume_path指定上次保存的模型路径即可不用从头再来。这个在跑长任务时很实用我一般每1000次迭代存一次中断了最多损失1000次的计算。5.4 推理速度慢的优化方向如果部署时发现单张图推理要好几秒先确认是不是在用CPU跑。GPU推理通常几十毫秒一张。如果必须用CPU可以换更轻的backbone比如STDC0、降低输入分辨率、开启MKLDNN加速。PaddleSeg的推理配置里可以设置enable_mkldnn为TrueCPU上能快不少。还有导出静态图模型比动态图推理快部署一定要用导出的模型。注意不要为了追求速度把分辨率降得太狠256以下天空边缘会明显糊掉得不偿失。512是个比较平衡的选择。6. 后续可扩展的方向天空分割做出来之后能玩的花样其实不少。最直接的是天空替换——把分割出的天空区域抠掉换上另一张天空图做出来的效果比一键换天App还自然因为你有精确的掩膜。再进一步可以结合图像修复把天空区域擦除后用生成模型补全做创意合成。另一个方向是多类别扩展。现在只分天空和背景可以加上云、太阳、晚霞等子类别变成一个细粒度的天空解析模型。数据标注成本会上去但应用价值也更高比如自动判断天气、生成天气描述。工程上还可以做视频流实时分割把单张推理改成批处理或者流式处理用在直播背景替换、无人机视觉等场景。PP-LiteSeg的速度支撑实时视频没问题关键是把前后处理 pipeline 优化好避免成为瓶颈。我自己后续打算试试把天空分割和深度估计结合起来用天空区域辅助判断场景的远近层次这个思路在风景摄影的自动构图里有潜在价值。等跑出结果再另开一篇聊。