UNet多分类分割数据集制作全流程:从标注规范到训练避坑指南 📅 发布时间:2026/9/17 15:40:55 👁 浏览次数: 用了UNet也调了好几次loss分割效果看起来还行但真到了项目验收才发现问题出在数据集上而不是模型上。这是很多人绕了一大圈才明白的事做多分类分割最耗精力的不是搭网络而是从零构建一份能用的数据集。标注不规范、类别定义模糊、格式转换出bug任何一环都能让你在训练阶段折腾好几个通宵。这篇文章就按我实际做过项目的顺序把UNet多分类分割数据集从标注到训练的全流程拆开讲。适合两类人一是刚接了自己的场景比如占道经营识别、遥感地物分类、桥梁病害检测发现网上没有现成数据集可用的二是已经用labelme之类工具标了几百张图但卡在格式转换和训练效果不理想这一步的。核心是帮你避坑不只给步骤更讲清楚每一步的缘由。1. 标注之前先想清楚的三件事类别、粒度与范式很多人一上来就打开标注工具开始画画到一半发现类别定义对不上或者同一张图不同人的标法差异很大这时候再返工成本极高。我在做占道经营数据集时第一版就吃了这个亏五十张图全作废。所以标注前这三件事必须定死。1.1 类别设计多分类不是越多越好UNet做多分类分割首先落地的文件就是类别清单。但类别不是拍脑袋写几个词就完事它决定后面所有环节。一个关键原则是每个类别必须在视觉上可区分并且边界清晰。比如你要识别占道经营那么摊位和行人在图像里常常挨在一起如果类别定义只写占道物体标注员会把伞棚、推车、堆物、行人全部混在一个类里。结果训练出来的模型不知道到底该分割什么loss曲线也乱成一团。更合理的做法是先用粗粒度定义主类别再决定是否需要细粒度。例如第一版只分违规摊位和背景跑通流程后再细分固定摊位、流动推车、堆物。还有一个容易忽略的点类别之间的层级关系。如果你定义了车辆和机动车标注员就会纠结电瓶车算哪一类。解决方法是避免定义语义重叠的类别每条类别的描述要写清楚包含什么、不包含什么。另外统计类别频率是标注前就该做的预判。如果一张遥感图里建筑占了60%像素道路占30%植被占1%那模型对植被的召回通常会很差。应对方案后面会讲但你心里得有数。1.2 掩码语义实例与类别的纠缠多分类分割里面有一个容易混淆的点同一类别的多个目标到底怎么处理。语义分割要求整张图每个像素属于一个类别。比如图上有三辆占道的共享单车它们都属于共享单车这一个类最终生成的mask里三个目标连成一片也是合法的。但如果你希望模型能区分单车A和单车B那就变成实例分割了UNet原生结构并不擅长这个方向。我的建议是不要指望UNet做实例级区分实在需要实例数量统计就在后处理里用连通域分析。标注时画多边形同一类的不同实例共用同一个类别值不用担心交叉重叠。只要两个实例之间有一条缝隙后处理就能把它们分开。还有一个细节是“重叠遮挡怎么标”。比如一张堆物图后面有个行人被遮住一半大多数标注工具都支持分层标注你可以把被遮挡物体完整轮廓画出来也可以只画可见部分。UNet训练建议只画可见部分因为你也没有被遮挡区域的真实标签。如果强行凭想象补全轮廓反而会让模型学到错误的形状分布。1.3 标注范式多边形、画笔和交互式分割标注工具的操作方式会直接影响效率和边界质量。目前主流就三类多边形polygon点出轮廓适合边缘清晰的目标比如建筑物、桥墩、车辆。缺点是慢细节多的物体要几十个点。画笔brush直接涂抹适合不规则纹理区域比如植被、地面但边界噪声大。交互式分割如SAM辅助点一下目标自动生成掩码再人工修正是最近两年效率提升最明显的方案。我在实际项目中第一遍标注常用多边形把大类轮廓勾出来再用画笔的橡皮擦功能修边缘细节。如果你的场景纹理复杂比如遥感图像里的植被区域用交互式分割会快很多。x-anylabeling这类工具已经集成了Segment Anything系列模型CPU上也能跑分割推理实测下来在win11上安装后导入模型权重就能用不需要GPU对于小数据集标注来说非常实用。2. 标注工具部署与规范落地工具本身都是免费的真正拉开差距的是标注规范和管理流程。2.1 工具选型CVAT、labelme还是x-anylabeling我自己项目里用过的三款工具各有适用场景。这里给一个对比工具部署方式AI辅助适合场景实际感受LabelMe本地Python安装弱单人小项目、快速原型简单直接JSON格式好处理但多人协作费劲CVAT服务器/Docker强支持自动标注团队协作、视频序列、复杂任务功能全管理成本高普通小项目有点杀鸡用牛刀X-anylabeling本地安装强集成SAM等模型个人效率优先、需要交互式分割省时间CPU可跑但有时模型会“自作聪明”如果你只是标注几百张图自己一个人就能搞定选labelme或者x-anylabeling都行。如果是团队标注而且有大量相似角度、相似场景的图像CVAT的配额和任务分配机制会省很多管理时间。我从实际项目里感受到的排序是效率优先选x-anylabeling协作优先选CVAT轻量优先选labelme。有些朋友用x-anylabeling时发现CPU推理速度偏慢可以退而求其次用它的交互式分割功能只做粗分割再手动调整多边形顶点。我实测下来对1200张遥感图像用SAM辅助标注大约能节省40%的时间但前提是目标边界比较清晰。2.2 一份可落地的标注规范长什么样标注规范不是给工具用的是给“人”用的。规范写得越细标注一致率越高。我一般在项目开始时写一页纸内容包括类别清单类别名称、对应的RGB掩码颜色、一句话定义、两到三个包含示例、两到三个不包含示例。边界规则目标边缘与背景之间允许的最大空隙像素级边界是否必须贴合物体边缘。遮挡规则被遮挡物体只画可见部分如果多个物体互相遮挡按前景层级标注。小目标规则小于某尺寸的目标是忽略还是标注必须明确。例如占道经营数据集里小于16×16像素的堆物忽略。争议处理如果标到一半不确定截图记录并创建issue不要自作主张。举个例子植被类的不包含示例可以写“草地上的枯枝落叶如果枯黄面积超过50%算裸土”这样标起来就有依据。2.3 质检与返工机制很多新手标完图直接进训练这是大忌。图像分割属于像素级标注错误很容易在训练后被放大。我常用的质检流程第一轮全检所有mask与原始图像的贴合度重点看边缘是否“画穿了”类别是否串类。之后按标注批次抽查20%如果错误率超过阈值就整体返工。可视化各类别面积占比如果某个类占比明显低于训练预期检查是不是漏标了。工具上可以用CVAT的人工审核功能分配审阅任务也可以用程序直接检查mask里每个类别的连通域数量。一条简单的Python脚本加载mask统计每个类别的像素数和连通域个数如果“摊位”类像素占了全图40%而连通域只有1基本可判定标注有问题。3. 从标注导出到训练数据格式转换与预处理这一步是绝大多数人“卡壳”的地方。标注完的JSON文件不能直接喂给UNet你需要把它转成模型需要的mask图像。3.1 搞清楚UNet需要什么格式的标签UNet在PyTorch中训练时对分割标签有两种常见输入方式单通道mask每个像素的值是类别ID0表示背景1表示类别12表示类别2……形状是[H, W]。One-hot编码形状是[C, H, W]的张量C是类别数每个通道是该类别的二值mask。最省事的是第一种。注意mask必须是无损格式PNG不要用JPG保存mask因为JPG压缩会破坏像素值导致类别ID出现连续变化的异常值。另外mask图的尺寸必须和原图完全一致否则训练时dataloader会报错或错位。3.2 LabelMe JSON到Mask的转换脚本以LabelMe为例JSON里保存的是每个目标的多边形坐标点和类别名称。转换逻辑不复杂但细节很多。我写一个最核心的转换函数import json import numpy as np import cv2 from glob import glob def labelme_json_to_mask(json_path, height, width, class_dict): with open(json_path, encodingutf-8) as f: data json.load(f) # 初始化mask背景为0 mask np.zeros((height, width), dtypenp.uint8) for shape in data[shapes]: label shape[label] if label not in class_dict: continue class_id class_dict[label] points np.array(shape[points], dtypenp.int32) # 关键必须是多边形填充 cv2.fillPoly(mask, [points], colorclass_id) return mask # 示例类别映射 class_dict { background: 0, 摊位: 1, 行人: 2, 堆物: 3, }这个函数有几个容易踩的坑fillPoly前必须把坐标转为np.int32float坐标会报错。class_dict必须保证和训练时的类别顺序一致否则会让mask和训练配置错位。如果同一个区域被两个多边形覆盖例如不同的标注图层后画的会覆盖先画的。所以标注规范里要约定好遮挡顺序否则转换后mask可能出现类别的意外替换。边界像素值保留问题cv2.fillPoly在边缘会有锯齿如果标注规范里没有做平滑要求最终mask的边界也会是锯齿状。我建议转换后额外做一步可视化检查把mask叠加到原图上生成透明叠加预览图用肉眼看几张这样能发现很多肉眼标注时忽略的问题。3.3 数据集划分的讲究与推荐目录结构划分数据集时最常见的错误是随机划分。对于连续拍摄的帧例如同一视频中的连续图像如果一些帧在训练集、另一些帧在验证集模型相当于“偷看”了高度相似的场景验证指标会虚高。尤其是遥感图像的条带、监控视频的同一镜头附近帧必须按场景或时间段分组。我推荐的一种目录结构dataset/ ├── train/ │ ├── images/ │ ├── masks/ ├── val/ │ ├── images/ │ ├── masks/ ├── class_dict.csv写一个简单脚本把原图与mask成对划分建议按照“场景维度”分组而不是按文件名散列。对于普通静态图像也至少要保证同一张图不能同时出现在训练集和验证集。对于占道经营数据集如果同一摄像头在不同时间拍的照片按时间窗口划分例如前10分钟作为训练、后2分钟作为验证比随机划分更有说服力。3.4 类别不平衡的处理与Patch切图策略多分类分割任务中模型可能被背景主导。我用过的最佳实践是顺序做这几件事统计各类别像素占比。发现某一类占比低于5%就要考虑。过采样小目标的样本。如果整张大图里只有一个小目标那就用滑窗裁patch把包含该目标的patch作为训练样本放大它在数据集中的比重。在loss中加类别权重。如果目标类别占比低直接用torch.nn.CrossEntropyLoss(weight...)权重的计算方式我放在第4节。如果图像过大做Patch切图。遥感图像经常是上万像素宽直接缩放会让小目标消失。合理做法是切成512×512或1024×1024的patch并且保留一些overlap。Patch切图时要注意mask和image必须同步切用完全相同的坐标切图后不需要额外拼接处理因为训练和推理时可以保持一致。对于桥墩病害这类目标极小的场景我建议以病害目标为中心做裁剪而不是固定网格能有效增强模型对细小裂缝的感受。4. 训练UNet时的关键配置与故障排查数据集就绪后就进入训练环节。UNet本身网络结构成熟训练时的坑主要在损失函数、指标分析和报错排查上。4.1 损失函数与类别权重的设定多分类分割默认用交叉熵但类别不平衡时一定要用带权重的交叉熵。一个简单的权重设计import torch import torch.nn as nn def compute_class_weight(class_counts, total_pixels): # 中位数频率平衡 median_freq torch.median(class_counts / total_pixels) weights median_freq / (class_counts / total_pixels) weights torch.clip(weights, min0.1, max10.0) return weights原理是让出现频率更低的类别获得更大的梯度权重。但要注意权重不能给太大否则模型会倾向于把大片背景预测成罕见类别导致高召回低精度。我实测中clip到[0.1, 10]区间比较稳妥。对于小目标单独交叉熵的优化信号太弱可以另外叠加一个Dice Loss。公式不复杂目的是让预测mask和真值mask的区域重叠度更高。但我提醒一点Dice Loss对边缘极其敏感如果标注边缘噪声很大Dice Loss反而会让loss曲线抖动非常明显。所以标注规范的质量会直接影响你能不能顺利加上Dice Loss。4.2 评估指标mIoU之外还要看什么只看mIoU一个数值会掩盖问题。比如背景占95%像素模型把背景完全预测对其它类别预测一塌糊涂mIoU可能仍然超过80。但实际业务中你关心的恰恰是那些占比低的小类别。所以我在每个epoch后不只看mIoU还输出每个类别自己的IoU并关注以下两个指标边界F1Boundary F1对mask边缘做形态学膨胀计算预测边缘与真值边缘的重合度能反映分割边界质量。连通域数量差异统计真值mask和预测mask中每个类别的连通域数量。如果真值有30个目标预测只有8个说明漏检严重如果预测有45个说明过分割严重。还需要把预测mask与原图叠加可视化目视检查几张。很多问题通过数值分析发现不了但眼睛一眼就能看出“这个边缘歪了”或“那里多了一块”。4.3 三个常见故障的排查链路我在训练UNet的过程中几乎每次都会遇到下面几个问题这里把排查链路列出来故障一loss不下降或先降后升检查顺序先确认mask像素值是否有超出num_classes-1的异常值比如标注文件里误把背景填充为255会导致CrossEntropyLoss报错或梯度异常。再确认输入图像是否归一化到[0,1]区间。如果图像不是归一化而是0-255学习率设置不当的话梯度会剧烈震荡。最后检查学习率是否过大UNet训练常用学习率在1e-4到3e-4之间我用过5e-4导致loss直接发散。故障二acc很高但预测全是背景这种基本可以断定类别ID映射错位。比如mask是用fillPoly填充类别值为1但训练代码里的CrossEntropyLoss要求的target是从0开始的而你的背景又是1导致所有真实目标都被认为是背景。排查方法是打印训练集里label的类别直方图看各类别有大量样本没有。故障三GPU内存溢出OOMUNet是encoder-decoder结构中间特征图占显存很大。如果图像分辨率过高batch size调低是首选。另外一个思路是使用更轻量的backbone比如把UNet的encoder换成MobileNet结构。但如果没有改动网络的能力就把输入尺寸调整为32的倍数因为UNet下采样5次是2的5次方非32倍数的尺寸会在skip connection处出现尺寸不匹配。5. 数据集质量对最终模型的影响两个迭代案例只有做过一次数据集迭代才会真正理解数据集质量的影响有多大。这里分享两个我做过的案例很能说明问题。5.1 占道经营数据集边缘规范带来的变化第一版标注时团队为了赶进度对堆物这类非刚性目标只画粗略的矩形框边界和真实物体边缘相差最多有二十个像素。训练出来的模型预测mask边缘锯齿感非常强一眼就能看出“不像一个专业分割模型”。后来我修改了标注规范要求所有堆物必须用多边形贴合边缘并规定标注点间距不超过5像素边缘平滑度作为质检项。重新标注并训练后同一个模型结构、同一个loss边界F1从0.72升到0.88。整个过程中模型网络一个参数没改纯粹是数据集质量带来的提升。这说明一个判断对UNet来说数据的边界噪声会实实在在转化为预测噪声。标注阶段省下的时间会在训练和调参阶段加倍赔回去。5.2 桥墩病害数据集极端类别不平衡下的补救另一个项目是桥墩表面病害分割类别包括“裂缝”、“渗水”、“背景”其中裂缝往往只有几十个像素占比远低于1%。第一版我在全图上进行训练模型很快就收敛到把所有像素预测为背景mIoU指标也有0.95左右但显然毫无价值。补救分三步第一步按病害目标中心裁512×512的patch每张图最多保留50%纯背景patch其余patch必须包含至少一个裂缝或渗水目标。 第二步给“裂缝”类别设置较高的loss权重并额外加入Dice Loss。 第三步推理阶段用滑窗预测把大图切成重叠patch最后按位置拼回完整mask。三步做完裂缝类的IoU从0.05提升到0.49虽然数字仍然不算高但至少可以用于初步筛选疑似病害大幅减少了人工巡检范围。这个案例也说明面对极端不平衡数据侧的处理优先级远高于模型侧。另外如果你发现标注速度实在太慢可以先用第一版模型对未标注的图像做伪标签生成再用x-anylabeling或CVAT打开自动生成的mask进行人工修正。这个半监督流程能明显降低重复劳动唯一要注意的是伪标签里的错误如果没被修正会以“错误共识”的形式固化到新模型里所以人工修正环节不能省。我在实际项目里最大的体会就是模型结构可以换超参数可以调但数据集质量才是真正决定项目成败的底层因素。与其羡慕别人用UNet刷了多高的分数不如沉下心把自己的标注规范、转换脚本和质检流程打磨好。后面的训练过程就是水到渠成的事了。