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], dtype=np.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_ok=True) 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(f'dataset/{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而不是DeepLabV3+PaddleSeg内置的模型很多,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都会出问题。optimizer:SGD配合momentum是分割任务的标准选择,收敛稳定。Adam也能用,但有时候泛化稍差。lr_scheduler:多项式衰减是PaddleSeg的默认推荐,学习率从0.01平滑降到0,避免后期震荡。loss:二分类用交叉熵就够了。如果天空和背景比例严重失衡(比如天空只占10%),可以加DiceLoss,coef设为[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-LiteSegCE+Dice0.0140.928exp03BiSeNetV2CE+Dice0.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.py:python 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_size=8但显存只够4,就设batch_size=4、accumulate_steps=2,效果接近。另外,训练中断后想接着训,用--resume_path指定上次保存的模型路径即可,不用从头再来。这个在跑长任务时很实用,我一般每1000次迭代存一次,中断了最多损失1000次的计算。5.4 推理速度慢的优化方向如果部署时发现单张图推理要好几秒,先确认是不是在用CPU跑。GPU推理通常几十毫秒一张。如果必须用CPU,可以:换更轻的backbone(比如STDC0)、降低输入分辨率、开启MKLDNN加速。PaddleSeg的推理配置里可以设置enable_mkldnn为True,CPU上能快不少。还有,导出静态图模型比动态图推理快,部署一定要用导出的模型。注意:不要为了追求速度把分辨率降得太狠,256以下天空边缘会明显糊掉,得不偿失。512是个比较平衡的选择。6. 后续可扩展的方向天空分割做出来之后,能玩的花样其实不少。最直接的是天空替换——把分割出的天空区域抠掉,换上另一张天空图,做出来的效果比一键换天App还自然,因为你有精确的掩膜。再进一步,可以结合图像修复,把天空区域擦除后用生成模型补全,做创意合成。另一个方向是多类别扩展。现在只分天空和背景,可以加上云、太阳、晚霞等子类别,变成一个细粒度的天空解析模型。数据标注成本会上去,但应用价值也更高,比如自动判断天气、生成天气描述。工程上还可以做视频流实时分割,把单张推理改成批处理或者流式处理,用在直播背景替换、无人机视觉等场景。PP-LiteSeg的速度支撑实时视频没问题,关键是把前后处理 pipeline 优化好,避免成为瓶颈。我自己后续打算试试把天空分割和深度估计结合起来,用天空区域辅助判断场景的远近层次,这个思路在风景摄影的自动构图里有潜在价值。等跑出结果再另开一篇聊。
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], dtype=np.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_ok=True) 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(f'dataset/{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而不是DeepLabV3+PaddleSeg内置的模型很多,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都会出问题。optimizer:SGD配合momentum是分割任务的标准选择,收敛稳定。Adam也能用,但有时候泛化稍差。lr_scheduler:多项式衰减是PaddleSeg的默认推荐,学习率从0.01平滑降到0,避免后期震荡。loss:二分类用交叉熵就够了。如果天空和背景比例严重失衡(比如天空只占10%),可以加DiceLoss,coef设为[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-LiteSegCE+Dice0.0140.928exp03BiSeNetV2CE+Dice0.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.py:python 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_size=8但显存只够4,就设batch_size=4、accumulate_steps=2,效果接近。另外,训练中断后想接着训,用--resume_path指定上次保存的模型路径即可,不用从头再来。这个在跑长任务时很实用,我一般每1000次迭代存一次,中断了最多损失1000次的计算。5.4 推理速度慢的优化方向如果部署时发现单张图推理要好几秒,先确认是不是在用CPU跑。GPU推理通常几十毫秒一张。如果必须用CPU,可以:换更轻的backbone(比如STDC0)、降低输入分辨率、开启MKLDNN加速。PaddleSeg的推理配置里可以设置enable_mkldnn为True,CPU上能快不少。还有,导出静态图模型比动态图推理快,部署一定要用导出的模型。注意:不要为了追求速度把分辨率降得太狠,256以下天空边缘会明显糊掉,得不偿失。512是个比较平衡的选择。6. 后续可扩展的方向天空分割做出来之后,能玩的花样其实不少。最直接的是天空替换——把分割出的天空区域抠掉,换上另一张天空图,做出来的效果比一键换天App还自然,因为你有精确的掩膜。再进一步,可以结合图像修复,把天空区域擦除后用生成模型补全,做创意合成。另一个方向是多类别扩展。现在只分天空和背景,可以加上云、太阳、晚霞等子类别,变成一个细粒度的天空解析模型。数据标注成本会上去,但应用价值也更高,比如自动判断天气、生成天气描述。工程上还可以做视频流实时分割,把单张推理改成批处理或者流式处理,用在直播背景替换、无人机视觉等场景。PP-LiteSeg的速度支撑实时视频没问题,关键是把前后处理 pipeline 优化好,避免成为瓶颈。我自己后续打算试试把天空分割和深度估计结合起来,用天空区域辅助判断场景的远近层次,这个思路在风景摄影的自动构图里有潜在价值。等跑出结果再另开一篇聊。