
这两年做目标检测做得比较多真正上手分割任务还是从YOLOv8开始。先说明白一个事儿很多人一提分割就发怵觉得比检测高一大截其实YOLOv8的实例分割入口比你想的简单得多难点全在数据准备和训练细节上。这篇文章就把我自己跑通的一条完整链路写出来用Labelme标注自己的数据集转成YOLO格式配置训练环境跑通YOLOv8-seg训练最后做推理验证。整个过程附带我踩过的坑和调参心得代码部分可以直接抄目录结构和参数都是实测能跑的。可能有人会问现在标注工具那么多X-AnyLabeling、LabelStudio、CVAT哪个不比Labelme香我承认但Labelme有一个不可替代的优势轻量、零服务端依赖、单机离线可用。对个人开发者或者小团队来说拿起来就能干不需要部署数据库和Web服务。还有一个更重要的理由Labelme生成的JSON格式非常透明你完全清楚每一条标注数据长什么样后续写转换脚本也好、做数据清洗也好心里有底。这篇文章适合谁看刚接触分割任务、需要用自定义数据集训练的同学以及已经在用YOLOv8做检测、想进一步切换到分割的老手。内容不涉及底层原理推演重心放在怎么把数据弄好、怎么把模型训起来这件事上。1. 动手之前先理清YOLOv8分割到底是干什么的很多人会把语义分割和实例分割搞混YOLOv8官方原生的分割能力是实例分割。这俩有什么区别语义分割是把图像里每个像素归类比如把所有人像素标成一类多个人黏在一起也没关系实例分割则要求把每个独立物体分开图像里有三个人就要输出三个独立的掩码。YOLOv8-seg的输出结构也印证了这一点每个检测框除了坐标和类别还附带一组掩码系数配合原型掩码prototype masks还原出像素级掩码。这个设计有个很实际的好处如果你之前做过YOLOv8检测迁移到分割几乎零成本。检测头那一套anchor-free的Decoupled Head还在只是多了一个并行的分割分支。这意味着训练速度、显存占用、数据增强策略大部分经验和检测模型是通用的。具体到网络结构YOLOv8-seg在C2f模块提取特征之后会额外输出一个掩码分支。损失函数也变成三部分叠加分类损失用BCE框回归损失用CIoU掩码损失用BCE逐像素二分类。所以你在训练日志里会看到三个loss的求和而不是像检测任务那样只有两个。理解了这一点后面看loss曲线就不会懵。还有一个容易忽略的点YOLOv8的实例分割默认输出的是框掩码的组合结果不是纯粹的掩码。推理阶段用NMS把重复框去掉再按掩码阈值默认0.5把prototype mask还原成最终分割结果。当你后面做部署时比如转TensorRT要把这个还原逻辑一并带上否则不会得到正确的掩码。我自己实际用下来YOLOv8-seg在小物体分割上比Mask R-CNN要省心很多不需要单独调ROI Align那套参数而且推理速度肉眼可见地快。在GTX 1660 Ti这种6GB显存的卡上COCO预训练模型跑640分辨率批量1的情况下能稳定跑到30ms左右。这是个非常现实的硬件门槛后面训练参数设置也会围绕这个级别来聊。2. 环境配置从零搭起YOLOv8训练环境含GTX 1660 Ti实测显存数据2.1 环境版本搭配先说结论这是我实测稳定的版本组合组件版本说明Python3.8 ~ 3.113.10最稳3.11也没问题PyTorch2.0.1cu118CUDA 11.8对应的版本CUDA11.8驱动版本需≥520cuDNN8.6以上默认随PyTorch安装即可ultralytics8.0.0以上越新功能越多但8.0.x够用labelme5.3.1新版界面改动大但功能没问题CUDA不一定要装系统级的那一套PyTorch的pip包会自带CUDA runtime。你只要保证显卡驱动足够新就行。Linux下用nvidia-smi看右上角的CUDA Version那个是驱动支持的最高版本不是系统实际装的。只要这个数字≥11.8直接pip装PyTorch就行不用折腾。创建虚拟环境这一步别省我见过太多人把环境搞成一锅粥最后重装系统。用conda的话conda create -n yolov8 python3.10 -y conda activate yolov8 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics labelme这里有两个细节。第一先装PyTorch再装ultralytics否则ultralytics的依赖解析可能把你PyTorch版本换了。第二CUDA 11.8版本对老卡支持很好实测GTX 1660 Ti、20系、30系都没问题。如果显卡比较新比如40系可以考虑cu121对应的PyTorch 2.1。2.2 验证环境是否可用装完先跑个最小验证确认CUDA可用python -c import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))输出True和你的显卡型号就说明OK。然后下载YOLOv8官方预训练权重yolo predict modelyolov8n-seg.pt sourcehttps://ultralytics.com/images/bus.jpg第一次运行会自动下载权重文件到当前目录同时能验证整个推理链路是否正常。如果这一步跑通了你的环境基本就稳了。说回显存问题。很多人用6GB显存的卡训练分割模型心里没底。我实测下来yolov8n-seg输入640batch size8显存占用大约5.2GByolov8s-seg同样640batch size4占用接近5.8GB。如果图像分辨率提高到1280同样模型batch直接减半都不一定够。所以小显存卡训练分割模型的核心策略就两条用小模型、小batch、低分辨率起步先验证流程再追求精度。3. Labelme标注全流程从安装到产出高质量分割数据3.1 安装与界面操作Labelme的安装没有任何门槛pip install labelme labelme启动后就是经典的三栏界面。左侧是文件列表中间是图像显示区右侧是标注操作区。标注分割数据关键在于用**多边形Create Polygons**工具沿着目标边缘打点。这里有一个新手特别容易犯的错误标注之前没设置标签列表。正确的做法是在启动命令里直接指定标签文件labelme --labels labels.txt --nodata --autosavelabels.txt内容很简单一行一个类别名比如person car dog--nodata表示不保存图片数据到JSON里JSON体积大幅减小--autosave表示每次标注完一个对象自动保存防止手滑关闭窗口丢数据。强烈建议把这两个参数用上尤其是标了几百张图后突然丢标注文件的心情我不想你再体验一次。标注操作本身不复杂但也有技巧沿着物体边缘打点点越多轮廓越精细但也不要无必要地堆点。分割掩码精度会受标注点密度影响点太少边缘锯齿严重点太多文件大、标注效率低。对普通物体30~80个点足够。对遮挡边界要果断标成可见部分的轮廓即可不要脑补被遮挡的部分。实例分割训练出来本来就会自己学习遮挡关系。同一个物体实例被分散成多个连通区域比如人被车挡住分成上下两截用同一个标签标注两块区域Labelme会认为这是两个多边形但属于同一个实例吗答案是不一定。这里我的建议是如果一个实例被完全隔断确保这两个多边形在同一个组里或者干脆分别标为两个实例。YOLOv8训练时会把每个多边形当作一个独立实例如果同一物体被标成两个实例预测时也会输出两个掩码这在一些场景下能接受但在计数任务里会有误差。缩放图像滚轮之后按住右键拖动可以平移画布这个操作很多人不知道标注效率差一大截。3.2 JSON格式解析Labelme保存的JSON文件核心结构是这样的{ version: 5.3.1, flags: {}, shapes: [ { label: dog, points: [[x1,y1], [x2,y2], ...], group_id: null, shape_type: polygon, flags: {} } ], imagePath: img_001.jpg, imageData: null }每个shapes元素就是一个标注实例points是多边形顶点坐标shape_type必须是polygon也有可能是rectangle但分割任务里我们统一用polygon。理解了这个结构你就知道为什么需要写转换脚本了——YOLO分割格式不要JSON它要的是归一化的多边形坐标文本。顺带说一个新手容易忽略的问题图像路径。Labelme记录的是相对路径imagePath字段存的是打开图片时的文件名。如果图片和JSON不在同一级目录转换脚本里要用JSON所在目录拼出完整图片路径。如果你的图片、JSON分散在不同文件夹建议标注前先统一整理成同一个目录能少写不少判断逻辑。3.3 数据质量控制标注质量直接决定模型上限这一点在分割任务里体现得尤其明显。检测任务里框歪一点点影响不大分割任务的掩码边缘如果标得随意模型学出来的掩码边界就会毛毛躁躁。一个亲测有用的办法标注完一批图片后用Labelme的可视化功能快速检查——在工具菜单栏选File Save Automatically旁边有个View Show Label选项或者用labelme_export_json把标注叠加到图上。我习惯标完20张图就整体检查一遍重点看三点有没有漏标、有没有标错类别、有没有轮廓明显不对的。漏标和错类别比轮廓粗糙的危害大得多一个漏标的实例会在训练时被当成背景直接干扰模型对这类物体的判断。4. 核心转换脚本Labelme JSON转YOLO分割格式附完整代码4.1 YOLO分割格式是什么YOLO分割数据格式和检测格式同源同样是一个txt文件对应一张图片每一行代表一个实例但内容从class x_center y_center width height变成了class x1 y1 x2 y2 x3 y3 ...后面的坐标序列就是多边形顶点的归一化坐标。举个例子一张宽1000、高800的图片某个实例的多边形顶点是(100, 50), (200, 150), (150, 300)归一化后就是(0.1, 0.0625), (0.2, 0.1875), (0.15, 0.375)。归一化方式是直接除以图片宽高不做任何偏移。注意YOLO格式要求坐标值在0~1之间如果你的标注框超出了图像边界Labelme允许你把点打到画布外转换时需要做clip否则训练会崩。4.2 完整转换代码下面是完整可用的转换脚本。支持多类别、自动生成训练集和验证集划分也支持图片子目录。可以直接复制使用import json import os import random import shutil from pathlib import Path def convert_labelme_json_to_yolo(json_path: Path, image_dir: Path, label_map: dict, output_dir: Path): 将单个Labelme JSON文件转换为YOLO分割格式。 label_map: 类别名到id的映射如 {dog: 0, cat: 1} with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path image_dir / data[imagePath] if not img_path.exists(): # 有些JSON中imagePath可能包含子路径尝试直接用文件名查找 img_path image_dir / Path(data[imagePath]).name if not img_path.exists(): print(f[跳过] 图片不存在: {img_path}) return img_width data.get(imageWidth, 0) img_height data.get(imageHeight, 0) if not img_width or not img_height: # 如果JSON里没记录宽高直接用PIL读取 from PIL import Image with Image.open(img_path) as im: img_width, img_height im.size lines [] for shape in data[shapes]: if shape[shape_type] ! polygon: print(f[警告] {json_path.name} 中有非polygon形状: {shape[shape_type]}) continue label shape[label] if label not in label_map: print(f[警告] 未定义的标签: {label}) continue class_id label_map[label] points shape[points] if len(points) 3: print(f[警告] {json_path.name} 中有少于3个点的多边形) continue normalized_points [] for x, y in points: nx min(max(x / img_width, 0.0), 1.0) ny min(max(y / img_height, 0.0), 1.0) normalized_points.extend([f{nx:.6f}, f{ny:.6f}]) line f{class_id} .join(normalized_points) lines.append(line) if not lines: print(f[跳过] {json_path.name} 没有有效标注) return # 复制图片到输出目录 out_img_dir output_dir / images out_label_dir output_dir / labels out_img_dir.mkdir(parentsTrue, exist_okTrue) out_label_dir.mkdir(parentsTrue, exist_okTrue) img_dst out_img_dir / img_path.name shutil.copy2(img_path, img_dst) # 写入标签txt label_dst out_label_dir / (json_path.stem .txt) with open(label_dst, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f[完成] {json_path.stem}) def convert_dataset(json_root: Path, image_dir: Path, output_root: Path, label_map: dict, val_ratio: float 0.2): 批量转换整个数据集。 json_root: 存放所有Labelme JSON文件的目录 image_dir: 存放原始图片的目录 output_root: 输出目录会创建images/和labels/两个子目录 json_files list(json_root.glob(*.json)) if not json_files: print(没有找到JSON文件) return # 打乱并划分数据集 random.seed(42) random.shuffle(json_files) val_count int(len(json_files) * val_ratio) val_files json_files[:val_count] train_files json_files[val_count:] print(f共 {len(json_files)} 张图训练集 {len(train_files)} 张验证集 {len(val_files)} 张) split_dirs {train: train_files, val: val_files} for split_name, split_files in split_dirs.items(): split_root output_root / split_name (split_root / images).mkdir(parentsTrue, exist_okTrue) (split_root / labels).mkdir(parentsTrue, exist_okTrue) for json_file in split_files: convert_labelme_json_to_yolo(json_file, image_dir, label_map, split_root) if __name__ __main__: # 修改这里的配置 label_map { dog: 0, cat: 1, } json_root Path(data/annotations) # JSON文件所在目录 image_dir Path(data/images) # 原始图片所在目录 output_root Path(data/yolo_dataset) # 转换输出目录 val_ratio 0.2 # convert_dataset(json_root, image_dir, output_root, label_map, val_ratio)脚本里我做了几个关键设计自动补读图片宽高有些Labelme老版本生成的JSON不包含imageWidth和imageHeight字段脚本会退回用PIL读取避免转换中断。坐标clip到[0,1]把越界的点拉回有效范围避免训练报错。文件复制而非移动保留原始标注文件转换出错了随时能重新来不破坏原始数据。固定随机种子确保每次划分训练集/验证集的结果一致方便复现实验。4.3 转换后的目录结构YOLOv8训练需要的目录结构非常固定转换脚本输出后应该长这样data/yolo_dataset/ ├── train/ │ ├── images/ │ │ ├── img_001.jpg │ │ └── ... │ └── labels/ │ ├── img_001.txt │ └── ... └── val/ ├── images/ │ ├── img_002.jpg │ └── ... └── labels/ ├── img_002.txt └── ...注意一点同一张图片的.jpg和.txt文件名必须完全一致不区分扩展名。YOLOv8通过文件前缀匹配图片和标签对不上就报warning并跳过。所以原始图片文件不要重名转换脚本如果发现同名覆盖会在终端打印大量警告。5. 训练配置data.yaml编写与关键参数详解5.1 写data.yamlYOLOv8用yaml文件描述数据集路径和类别名都在里面。最简写法# data.yaml path: data/yolo_dataset # 数据集根目录相对于你执行训练命令的位置 train: train/images val: val/images names: 0: dog 1: cat这里有几个细节容易踩坑path可以是相对路径也可以是绝对路径相对路径相对于当前工作目录不是相对于yaml文件位置。所以训练命令在哪个目录下执行写相对路径时要想清楚。train和val填的是images目录路径还是labels目录路径填images目录路径YOLOv8会自动找同级labels目录。你填labels目录反而会报错。names的顺序和数量必须和转换脚本里的label_map完全一致。类别名只是可读字符串模型训练只用id但如果你排序错了等于整个标注全部错位。5.2 训练命令与关键参数环境就绪、数据集就绪后训练命令其实一行就能跑yolo segment train datadata.yaml modelyolov8n-seg.pt epochs100 imgsz640 batch8 device0但为了拿到更好的效果和可控的训练过程我一般写成这样yolo segment train \ datadata.yaml \ modelyolov8n-seg.pt \ epochs200 \ imgsz640 \ batch8 \ device0 \ patience30 \ optimizerSGD \ lr00.01 \ lrf0.01 \ augmentTrue \ seed42 \ projectruns/segment \ namedogcat_seg_exp1逐个说明关键参数背后的逻辑modelyolov8n-seg.pt既是模型结构定义又是预训练权重来源。从头训练用yolov8n-seg.yaml但默认情况下用预训练权重迁移学习效果远好于从头训练。imgsz640训练分辨率。如果你标注的物体较小建议用768或896小物体检测/分割提升明显。但显存也要跟着涨6GB显存用896会有点紧张可以先试640跑通后续再往上加。batch8单卡batch size。小显存卡建议4~8之间。batch太小比如2会导致BN统计量不稳定训练震荡。我实测GTX 1660 Ti上yolov8n-seg640batch 8是能跑的但再加到12就会OOM。patience30早停耐心值验证集mAP连续30轮不提升就停止。这个参数能省大量时间。小数据集尤其管用我经常100轮的活40轮就停了。optimizerSGD官方默认是auto会自动选择AdamW。我的经验是分割任务用SGDmomentum0.937weight_decay0.0005收敛效果更平滑最终mAP往往比AdamW略高。当然auto也完全能用这个见仁见智。augmentTrue开启Mosaic、MixUp、Flip等增强。YOLOv8默认在训练最后10轮会自动关闭Mosaic不需要你操心。小数据集开增强是必需品不开增强基本会过拟合到妈都不认识。project和name把每次实验的权重、结果曲线放到独立目录方便对比。养成好习惯不同实验用不同name不然实验结果会互相覆盖。5.3 训练过程可视化训练过程中终端会实时输出每个epoch的loss、mAP50、mAP50-95等指标。训练结束后runs/segment/dogcat_seg_exp1目录下会生成大量的结果文件。我最常看的是这两个results.png包含训练/验证的box loss、seg loss、mAP变化曲线。这是判断训练是否正常的第一手资料。confusion_matrix.png混淆矩阵能看到哪些类别互相混淆。还有一个被很多人忽略的文件args.yaml。训练时用到的所有超参数都会记录在里面方便复现。下次训练想调整参数时直接对照这个文件改就行不用翻聊天记录。如果你在服务器上训练想远程看曲线直接在本地运行tensorboard --logdir runs/segment会自动收集训练日志。YOLOv8官方集成了TensorBoard不需要额外装插件。6. 训练监控与loss曲线解读我踩过的训练异常这部分算是全篇最有价值的地方因为训练跑不起来/训不好的时候真能急死人。我把自己实测踩过的几种异常情况列出来对照排查能省很多时间。6.1 loss曲线不下降或直接发散loss不下降先检查数据不要怀疑模型。最常见的原因标签和图片不匹配图片显示的是狗但txt里标的是猫的id。这类错误不会报错模型默默学错loss表现为降到一个平台就再也不动。tags文件为空有些图片的txt文件因为转换脚本跳过等原因不存在或为空YOLOv8会直接跳过这些图片导致实际训练数据远少于你的预期。学习率过大小数据集如果lr00.01发散降到0.001重试。数据集越小lr越要保守。排查方法很直接写个小脚本随机抽几张训练图片把对应的txt坐标画回去看看。如果掩码位置对不上说明转换或映射有问题。6.2 mAP50不错但mAP50-95特别低这个现象在分割任务里非常典型。mAP50只看IoU0.5的正确率mAP50-95要看IoU从0.5到0.95的区间平均表现。如果你的mAP50很高但mAP50-95低通常说明掩码边界不精细得分高的那部分预测框其实掩码轮廓很粗糙。原因基本是这几类标注多边形顶点太少掩码本来就是粗糙的训练分辨率太低640对于小物体来说输出掩码分辨率不够小物体占比高但模型对小物体分割能力天然偏弱我遇到过最典型的案例标注一个数据集物体占比小且在图片边缘用640训练mAP500.9但mAP50-95只有0.45。后来把imgsz提到896mAP50-95直接涨到0.62。分辨率对分割任务的影响比对检测任务大得多因为掩码预测需要有足够的feature map分辨率来还原细节。6.3 验证集loss持续低于训练集loss正常情况下训练loss应该低于验证loss。如果反过来且差距很大恭喜你你遇到了验证集泄漏或者数据划分异常。最常见原因训练集和验证集存在同源图片比如连续帧截图模型等于提前见过验证集数据预处理不一致验证集做了一些额外增强类别不均衡导致验证集恰好都是简单样本第二个原因在YOLOv8里基本不会出现因为验证流程是固定的。最可能的就是数据划分问题。我的建议是如果你的数据来自视频帧务必按时间顺序分帧而不是随机分帧否则相邻帧几乎一样验证结果虚高上线就翻车。6.4 显存OOM的应对OOM的排查优先级降低batch一次减半降低imgsz从640降到512换更小的模型从yolov8s-seg换到yolov8n-seg开启梯度累积batch4 accumulate2等效于batch8的梯度更新步长但显存占用只有batch4的量梯度累积这个技巧很多新手不知道在train()参数里加accumulate2即可表示每2个batch累加一次梯度再反向传播。这是小显存卡训练大模型最实用的技巧没有之一。6.5 早停后如何恢复训练早停不是坏事但如果最优epoch还没收敛就停了就有点亏了。YOLOv8在早停时会把最优权重保存为best.pt你可以直接基于它继续训练yolo segment train datadata.yaml modelruns/segment/dogcat_seg_exp1/weights/best.pt epochs300 patience50这里model参数传上次训练的最优权重就能接着训。注意epochs300是新的总epoch数不是要增加的epoch数。想新增50轮要设成epochs旧的最好epoch数50。具体的计算方式是看上次最优权重对应的epoch然后设新epoch总和。7. 推理与评估验证模型效果并导出部署模型7.1 单图和批量推理训练完成后可以用命令行直接跑推理yolo segment predict modelruns/segment/dogcat_seg_exp1/weights/best.pt sourcetest_images/ imgsz640 conf0.25 iou0.45 saveTrueconf0.25表示置信度阈值低于0.25的结果直接过滤iou0.45是NMS的IoU阈值值越大保留的重叠框越多。我刚训练完模型时习惯设conf0.1看全部预测用来检查有没有漏检调优时用conf0.25来看实际效果。推理结果中除了带掩码的可视化图还会保存一个predictions.json如果你加了save_jsonTrue里面包含每个检测框的坐标、类别、置信度以及RLE编码的掩码。后续想画自定义可视化、计算IoU等都可以基于这个JSON处理。7.2 评估指标怎么看训练结束后val命令能给出详细评估yolo segment val modelruns/segment/dogcat_seg_exp1/weights/best.pt datadata.yaml imgsz640输出里最有用的两个指标是mAP50IoU阈值0.5下的平均精度衡量框大致位置对不对mAP50-95IoU阈值从0.5到0.95求平均衡量掩码精细程度和定位精度对于大多数业务场景mAP50达标就能用如果要做精细测量比如面积统计、轮廓分析就得抠mAP50-95。另外注意验证时imgsz最好和训练时保持一致否则指标会有波动。7.3 导出ONNX与TensorRT如果你要把模型部署到服务端导出ONNXyolo export modelbest.pt formatonnx imgsz640 opset12导出后可以用onnxruntime做推理也可以用trtexec转TensorRT引擎。从搜索结果的热词来看很多人关心TensorRT 8.6的部署这里提醒一个关键点YOLOv8-seg导出的ONNX里包含多个输出其中一个output是掩码系数另一个是原型掩码部署时要同时处理。用onnxruntime推理时要手动做矩阵乘法和sigmoid才能得到最终掩码。简单说后处理流程为拿到检测框输出(1, 300, 4nc32)其中32是掩码系数维度通过NMS筛选框把掩码系数和原型掩码(1, 32, 160, 160)做矩阵乘法sigmoid后按阈值二值化再缩放到原图尺寸如果你不熟悉这套流程最简单的做法是用ultralytics自带的predict做推理它在内部已经处理好了全部后处理。只有在需要极致性能时才手动搞TensorRT和后处理。8. 从检测模型迁移到分割模型的技巧如果你之前训练过YOLOv8检测模型转分割时有个投机取巧的路子先把检测模型训好再用它的权重做分割模型的起始点。原理也很简单YOLOv8的backbone和neck在检测和分割之间完全共享只有head部分不同。所以你可以把检测任务训练好的best.pt传给分割训练yolo segment train datadata.yaml modelpath/to/detection_best.pt epochs100实际测试下来迁移起始的mAP能比直接用COCO预训练权重高出2~3个点尤其是你的数据集和COCO分布差异大的时候。这个做法的坑在于那个检测权重文件的结构里没有分割head训练时会随机初始化分割分支但backbone权重被完整保留所以整体收敛速度会快不少。另一个窍门如果你已经有检测模型的数据集只有框没有掩码不想重新标注可以考虑先用检测框生成初始伪掩码——把检测框中心区域填成前景周边填成背景生成粗糙分割训练集然后训练一个初版分割模型再人工修正、迭代。这个方法能大幅降低从零标注的工作量但只适合对掩码精度要求不高的场景。9. 总结一下我的实操心得整条链路走下来我的体会是YOLOv8分割的上手难度被严重高估了。环境配置、数据转换、训练推理每一环都有清晰的工具和文档最大的工作量其实花在数据标注和清洗上。而恰恰是这部分最值得用心做。标注质量、类别均衡、划分合理性这些数据层面的问题决定模型上限调参和网络结构层面的优化只是逼近这个上限而已。最后分享两个小技巧收尾。第一训练前用脚本统计一下每个类别的实例数量和掩码面积分布。如果某个类别的标注数量特别少预测效果基本不会好。这时候可以给这类数据多标一些或者用weight参数给类别加权重。YOLOv8的class weight配置方式在data.yaml里给names对应位置写入权重列表比如names: {0: dog, 1: cat}旁边加weight: [0.7, 0.3]。这个参数不是官方文档里的常用参数但实测有效。第二数据标注阶段就顺手把标签一致性检查做掉。我写过一个简单脚本遍历所有JSON统计每个标签名称的出现次数。如果发现dog和Dog混在一起这种低级错误趁早统一别等训练跑了一半才想起来。分割模型训练是一场持久战但每一步的反馈都很明确数据标注得好不好loss曲线会告诉你模型设计得对不对mAP会告诉你。剩下的交给时间去迭代就行。