ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

水下垃圾检测数据集全解析:7类5328张三种标签格式与YOLO训练实战

水下垃圾检测数据集全解析:7类5328张三种标签格式与YOLO训练实战

简介:这是基于水下机器人实拍影像构建的7类水下垃圾检测数据集,共5328张标注图片,覆盖水下生物、塑料、金属、木材、橡胶、布料及捕鱼工具等类别,适合用于水域环境监测、水下智能巡检及目标检测算法训练。包内文件总数21313个,包含5328张JPG原图及对应的XML、TXT、JSON三种格式标签,可无缝适配VOC、YOLO及主流检测框架,省去格式转换环节。整体压缩包127.54MB,图片与标签按编号一一对应,数据分布较均衡。目前已有325人学习使用,适合课程设计、毕业设计、竞赛训练或真实水下项目预研。对于需要快速搭建水下目标检测任务的开发者,可直接划分训练集投入模型训练,并利用多格式标签灵活切换工具链。

1. 水下垃圾检测数据集:5328张图的7类标注,三种标签格式怎么选、怎么用

做水下垃圾检测,模型选谁大家心里都有底,真正卡脖子的永远是数据集。你搜到的开源目标检测数据,要么是COCO这种通用场景,对水下的瓶子、渔网、塑料碎片覆盖极少,要么就是一个压缩包下来只有图片没有标签,整个周末耗在标注上。这个数据集标题里最大的价值就在这:水下垃圾检测数据集7类-5328张-含voc(xml)+yolo(txt)+json三种格式标签.zip,图片量不算大,但7类目标的标签一次给齐了三种格式,覆盖了VOC生态、YOLO生态和COCO风格json生态的对接需求。

对刚入门yolo学习的工程师,它是最合适的练手底料:有标注、有多类、有现成格式,直接按步骤整理就能训练;对已经在做水下水产、河道垃圾清理项目的人,它可以作为迁移学习的起点模型数据,不必再从零标几百张图。下面按「标签格式解剖 → 目录整理与训练 → 踩坑排查 → 验证与进阶」展开,每一段都能照着做。

2. 三种标签格式的解剖:VOC的xml、YOLO的txt、JSON的结构差异

拿到压缩包第一件事,先别急着解压后丢给训练脚本。把三种格式各自的用途边界搞清楚,才知道在什么环节该用哪一份,也才能在训练中途报错时快速判断是数据问题还是模型问题。

2.1 VOC的xml怎么读:annotation节点、object层级与xml解析

VOC格式是目标检测里最老派也最稳定的框表达方式。它的特点是“人眼友好”:一个xml文件对应一张图片,根节点是annotation,里面有size(宽高、通道数)、segmented(是否有更细粒度的分割标注)以及若干个object节点,每个object里的bndbox给出左上角和右下角两个点的绝对像素坐标。

xml文件用什么打开和编辑都很随意,VS Code、Notepad++、浏览器都能看,但要批量改几千个xml里的类别名或坐标,脚本才兜得住。我这里用python的xml.etree.ElementTree做xml解析,不依赖第三方库:

import xml.etree.ElementTree as ET from pathlib import Path xml_path = Path("xml/000001.xml") # VOC标签目录里的单个文件 tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) # 原始图像宽 img_h = int(root.find("size/height").text) # 原始图像高 boxes = [] for obj in root.iter("object"): name = obj.find("name").text # 类别名,如 bottle bndbox = obj.find("bndbox") xmin = int(bndbox.find("xmin").text) # 左上角 x(绝对像素) ymin = int(bndbox.find("ymin").text) xmax = int(bndbox.find("xmax").text) # 右下角 x(绝对像素) ymax = int(bndbox.find("ymax").text) boxes.append({"name": name, "xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax}) print(f"图像尺寸: {img_w}x{img_h}, 目标数量: {len(boxes)}") print(boxes)

这段代码的核心逻辑是把一个“人读的标签”转成“机器好处理的列表”。用root.find("size/width")而不是root.findall(),是因为每个xml里只有一个size节点,find直接取第一个匹配项就够了;root.iter("object")遍历所有嵌套的object,即使节点层级里多包了一层也能拿到,比逐层findall更稳。

参数说明:VOC的bndbox里存的是坐标边界,不是中心点加宽高;如果你在用LabelImg标注,导出的就是这个格式。数值必须int(),xml里读出来是字符串,直接参与计算会出类型错误。xml解析对编码敏感,建议统一用utf-8,Windows上如果看到中文乱码,多半是文件本身存成了gbk,先用文本编辑器另存为utf-8再接ET.parse。

2.2 YOLO的txt:归一化坐标为什么是非整数

YOLO的txt标签每一行对应一个目标,格式是“类别id 中心点x 中心点y 宽 高”,五个值全部按图像宽高归一化,范围0到1。这也解释了为什么你在YOLO标注文件里看到一堆0.几的小数而不是“320,241”这种整数。归一化的好处是模型在不同输入分辨率下都能复用同一份标签,不用每换一次输入尺寸就改一遍标注。

with open("yolo/000001.txt", "r", encoding="utf-8") as f: for idx, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: print(f"第{idx+1}行格式异常: {line}") continue cls_id, cx, cy, bw, bh = map(float, parts) # 中心点坐标换算回绝对像素,便于和XML交叉检查 center_x_abs = int(cx * img_w) center_y_abs = int(cy * img_h) w_abs = int(bw * img_w) h_abs = int(bh * img_h) print(f"类别{cls_id} 中心({center_x_abs},{center_y_abs}) 宽{w_abs} 高{h_abs}")

注释里反复强调“和XML交叉检查”:同一个目标在txt里是归一化数值,在xml里是绝对像素,把txt反算回绝对像素再和xml比对,能快速验证两个标注文件是不是对应同一目标。实际操作里,我一般会抽3到5张图做这种交叉检查,如果同一目标在两个文件里算出的像素坐标差超过3个像素,就要怀疑数据集在生成时用的原图和当前目录里的图分辨率不一致。

参数说明:map(float, parts)是整行解析的关键,类别id虽然应该是整数,但统一用float解析再处理,比直接int(parts[0])更不容易因为“1.0”这种写法崩掉。空行跳过比报错更稳妥,很多标注工具会在文件末尾留一个空行,这不影响YOLO读取,但会影响你按行检查时的索引。

2.3 JSON格式标签:COCO风格的annotations数组与json查询函数

JSON标签常见有两种形态:一种是每张图片一个json文件,另一种是COCO风格的总json,内含images和annotations两个大数组。这类数据集给出json格式,绝大多数是为了方便对接COCO API、Detectron2这类需要结构化注释的工具。json的层级和数组可以用json查询函数直接定位,比如键索引、数组遍历,不需要像xml那样一层一层find。

import json with open("annotations/instances.json", "r", encoding="utf-8") as f: anns = json.load(f) img_id_to_path = {img["id"]: img["file_name"] for img in anns["images"]} cat_id_to_name = {cat["id"]: cat["name"] for cat in anns["categories"]} records = [] for ann in anns["annotations"]: x, y, w, h = ann["bbox"] # coco bbox是左上角 + 宽高 records.append({ "image": img_id_to_path[ann["image_id"]], "category": cat_id_to_name[ann["category_id"]], "bbox": [int(x), int(y), int(w), int(h)], "area": ann.get("area"), "segmentation": ann.get("segmentation") # 多边形/掩码信息 }) print(records[:5])

注意COCO的bbox定义和VOC完全不一样:COCO是“左上角x,左上角y,宽,高”,VOC是“左上角x,左上角y,右下角x,右下角y”,互转时要么减要么加,别拿错。json数组里的元素可以用下标取,也可以按key过滤,这种json查询函数只要记住“先取字典再取key”的写法,不要在长的链式调用上翻车就行。

我特意提一下segmentation字段:如果它是polygon格式的列表,说明这份json可以做实例分割的底料;如果segmentation是空数组,那它只是检测框。这个字段决定了你后期能不能升级成yolo实例分割任务,后面第5章还会用到这个判断。

2.4 三种格式互转:VOC的xml到YOLO的txt,公式与脚本

理解了三种格式之后,互转脚本几乎必写一遍。即使数据本身三种都给齐,你也可能在三个目录里发现某个xml和txt对不上目标数。自己写一次转换脚本,能在过程中发现这类不一致,比事后怀疑模型可靠得多。

def voc_to_yolo(xmin, ymin, xmax, ymax, img_w, img_h): """VOC绝对坐标转YOLO归一化坐标的完整公式""" x_center = ((xmin + xmax) / 2.0) / img_w y_center = ((ymin + ymax) / 2.0) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 防止浮点误差导致越界 x_center = max(0.0, min(1.0, x_center)) y_center = max(0.0, min(1.0, y_center)) return x_center, y_center, box_w, box_h def yolo_to_voc(cx, cy, bw, bh, img_w, img_h): """YOLO归一化坐标转VOC绝对坐标的完整公式""" xmin = int((cx - bw / 2.0) * img_w) ymin = int((cy - bh / 2.0) * img_h) xmax = int((cx + bw / 2.0) * img_w) ymax = int((cy + bh / 2.0) * img_h) return xmin, ymin, xmax, ymax

逻辑说明:VOC转YOLO的核心是求中心点再除以宽高;YOLO转VOC是反过来,用归一化中心点减去一半宽高得到左上角。最后那行max/min裁剪很重要,四舍五入可能把值推到1.0000001,YOLO训练时一旦出现超出[0,1]的坐标,边界框回归的损失函数会算出无效值,loss显示成nan。

顺便说一个常见困惑:VOC的xmax是右下角的像素索引,还是边界框的宽度?主流标注工具里它表示右下角坐标,所以框宽是xmax-xmin,而不是xmax-xmin+1。这个+1对最终mAP影响很小,但批量转换时要全量统一,否则同一张图在不同格式间来回转,边界会越差越多。如果你以前只接触过COCO 80类的预训练权重,这里尤其要注意:这个数据集是7类,不能直接套coco80的类别名去读txt,id从0到6,和COCO的id顺序完全无关。

3. 把数据集整理成YOLOv8能直接训练的目录结构:划分、yaml配置、标签自检

标签格式看得懂,不等于train.py能跑起来。很多新手把下载的数据直接丢进yolo的data文件夹,报错说“no labels found”时一脸懵。YOLO系列对目录结构有强约定:图片和标签要分两个根目录,且图片名和标签名(不含扩展名)必须完全一致。这一章按YOLOv8(ultralytics)训练自己的数据集的常见做法,把目录、配置文件、自检脚本一次讲清。

3.1 目录结构设计:image与label分开放,train/val/test按比例划分

训练前先把数据固定成一个稳定结构,推荐这样排:

datasets/underwater/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/

对应的,每个train/images/xxx.jpg的标签必须在train/labels/xxx.txt里。划分比例我用最常见的8:1:1,5328张图大约是4262/533/533。下面的脚本按文件stem(不含扩展名的文件名)把图片和标签一起搬进对应子目录:

import random, shutil from pathlib import Path src_img = Path("src_data/images") src_lab = Path("src_data/labels") img_files = sorted(src_img.glob("*.jpg")) random.seed(42) random.shuffle(img_files) total = len(img_files) train_imgs = img_files[:int(total * 0.8)] val_imgs = img_files[int(total * 0.8):int(total * 0.9)] test_imgs = img_files[int(total * 0.9):] def copy_group(img_list, split_name): img_dst = Path(f"datasets/underwater/images/{split_name}") lab_dst = Path(f"datasets/underwater/labels/{split_name}") img_dst.mkdir(parents=True, exist_ok=True) lab_dst.mkdir(parents=True, exist_ok=True) for img in img_list: shutil.copy(img, img_dst / img.name) label = src_lab / f"{img.stem}.txt" if label.exists(): shutil.copy(label, lab_dst / label.name) else: print(f"警告:{img.name} 没有对应标签txt") copy_group(train_imgs, "train") copy_group(val_imgs, "val") copy_group(test_imgs, "test")

复制之前先做一次shuffle,而且固定随机种子。原因很实在:水下垃圾图片里连续帧相似度高,如果直接按文件名排序切分,很可能前50张全是同一片水域的同一堆垃圾,训练集和验证集互相泄漏,val mAP虚高,换数据立刻打回原形。固定random.seed(42),别人复现你的实验时,划分逻辑也能对得上。

参数说明:这里用Path.glob(".jpg"),如果压缩包里是png或jpeg,把后缀换成对应扩展名,或者干脆glob(".j*")。找标签时不建议用字符串replace("images", "labels"),因为一旦目录命名不规范就出错;统一改成src_lab / f"{img.stem}.txt",通过stem精确对接,最稳。

3.2 data.yaml配置:nc、names、path三个参数一个都不能错

YOLOv8训练时读取的data.yaml是数据集的契约文件。路径写错、类别数写错、类别名写错,训练要么直接报错,要么mAP全是0还不报错。我给这套水下垃圾数据集写的yaml长这样:

# datasets/underwater/data.yaml path: /absolute/path/to/datasets/underwater # 数据集的绝对根目录 train: images/train # 相对path的文件夹 val: images/val test: images/test # 测试集,训练时用不到但可以留着 # 7个类别,以下名称只是示例,务必以你解压后的xml/txt里实际name为准 nc: 7 names: 0: plastic_bag 1: bottle 2: fishing_net 3: metal_can 4: glass_bottle 5: rubber 6: other

唯一要特别小心的是path这一行。写成绝对路径,可以避免从不同目录启动训练时找不到图片;如果你有多台机器同步训练数据,用相对路径配合软链接也可以,但新手阶段直接写绝对路径最省事。nc和names的数量必须对应txt第一列出现过的最大id再加1,比如txt里最大类别id是6,nc就是7,写成6的结果是越界报错,错误信息还经常藏在一堆日志里,不好定位。

提示:data.yaml里的names顺序一旦确定,就不要在训练中途改。YOLO的类别id是按names列表下标取的,顺序变了等于所有txt的类别都变了。

3.3 标签自检:写一个批量检查脚本,跑训练前先排除明显错误

数据整理完、yaml写好后,我习惯先跑一个标签自检脚本,而不是直接开大训练。这个脚本能在几十秒内把“缺图、缺标签、格式行数不对、坐标越界、类别id越界”一次查完:

from pathlib import Path img_dir = Path("datasets/underwater/images/train") lab_dir = Path("datasets/underwater/labels/train") nc_expected = 7 img_suffixes = {".jpg", ".jpeg", ".png"} images = {p.stem: p for p in img_dir.iterdir() if p.suffix.lower() in img_suffixes} for txt in lab_dir.glob("*.txt"): if txt.stem not in images: print(f"[缺图] 标签 {txt.name} 没有对应图片") continue lines = txt.read_text(encoding="utf-8").strip().splitlines() if not lines: print(f"[空标签] {txt.name}") continue for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: print(f"[列数错误] {txt.name} 第{i+1}行: {line}") continue cls_id, cx, cy, bw, bh = map(float, parts) if cls_id < 0 or cls_id >= nc_expected: print(f"[类别越界] {txt.name} 第{i+1}行 cls={cls_id}") if cx < 0 or cx > 1 or cy < 0 or cy > 1 or bw <= 0 or bh <= 0: print(f"[坐标异常] {txt.name} 第{i+1}行 : {cx},{cy},{bw},{bh}") # 反向查找缺标签的图片 for stem, img in images.items(): if not (lab_dir / f"{stem}.txt").exists(): print(f"[缺标签] 图片 {img.name} 没有对应txt")

逻辑说明:脚本做了四件事——标签与图片是否存在配对、每一行是不是5个字段、类别id是否在0到6之间、归一化坐标是否在合法区间。“空标签”那种情况很常见且隐蔽:标注时漏了一张,txt是0字节文件,YOLO不会报错,但训练时会产生大量背景误检,val mAP被拖低。

参数说明:nc_expected要与data.yaml里的nc对齐。如果你想查得更细,可以在脚本里用PIL读图片真实尺寸,再验证每个目标的宽高是否明显大于0且不超过图像尺寸。实际跑一遍,大部分错误都集中在“列数不对”和“缺标签”,纠完再训练,基本不闹脾气。

3.4 启动训练:epochs、batch、imgsz怎么设

目录和yaml就绪后,训练命令很短。我一般用ultralytics提供的CLI直接起训练:

yolo detect train data=datasets/underwater/data.yaml \ model=yolov8s.pt \ epochs=100 \ batch=16 \ imgsz=640 \ seed=42 \ project=runs_train \ name=underwater_baseline

参数说明:model=yolov8s.pt表示用yolov8s的预训练权重做起点,水下垃圾和COCO的通用特征有部分重叠,迁移学习比从零训练收敛快得多。epochs=100是起步值,5328张图对单卡来说100轮大概2到4小时,看显卡而定。imgsz=640是精度和速度的平衡点,水下小目标多,如果显存够,试试imgsz=1280会明显提升小目标召回,但训练时间接近翻倍。

训练日志里重点关注三个值:box_loss、cls_loss、dfl_loss。box_loss稳定下降说明定位在学,cls_loss下降说明分类在学,dfl_loss是分布损失,波动大没关系,但持续上升就要检查标签质量。这三项对应着yolo损失函数的三个组成部分,之前自检脚本没跑干净的话,这里就会出幺蛾子。

4. 水下垃圾数据的5个常见坑:训练不收敛、损失nan、mAP虚高的排查清单

水下垃圾数据集相比常规目标检测数据,有几个特别容易踩的坑。下面按“现象→原因→解决”记录,都是实际跑yolo系列时反复踩过的。

4.1 训练第一轮就报IndexError:类别id和nc对不上

现象:数据加载到一半就断了,错误信息是IndexError: index X is out of range for axis 0 with size 7。 原因:txt里某一行的类别id是7或者更大,但data.yaml里nc配的是7;或者raw数据里有8个类别,生成标签时漏了一层过滤,多余类别混了进来。 解决:先跑3.3的自检脚本,再用一条命令统计全量标签的类别分布:

cat datasets/underwater/labels/train/*.txt | awk '{print $1}' | sort | uniq -c

这条awk命令会输出每个类别id出现的次数。看到最大值是6就说明类别id没问题;如果出现7甚至更大,用sed做全量id修正,把id=7并到id=6:

sed -i 's/^7 /6 /' datasets/underwater/labels/train/*.txt datasets/underwater/labels/val/*.txt

修正完重新跑一遍自检。原因是全量数据里出现了原始类标的噪声,常见是标注员把同一个目标标成了两个相近类别名,合类比重新标注省力得多。

4.2 训练两个epoch后loss变成nan:框宽为0或中心坐标越界

现象:loss曲线第一个epoch正常下降,第二个epoch突然变成nan,之后无法恢复。 原因:某个txt里bw或bh是0,或者中心坐标cx写成1.0以上,YOLO损失函数在做边界框回归时算出了无效值,反向传播直接把权重冲坏了。 解决:自检脚本里保留bw <= 0和bh <= 0的判断,就是为抓这种问题。命中后直接删掉那一行,或者把该目标过滤出训练集。如果越界值是1.0000001这种浮点误差,用awk裁剪:

awk '{if($2>1)$2=1; if($3>1)$3=1; print}' file.txt > tmp && mv tmp file.txt

这个命令把第二列第三列超过1的值拉回1,再原地替换文件。注意awk会把原文件格式重排一遍,操作前先备份。

4.3 水下图像整体偏蓝绿色,训练loss不下降

现象:loss一直在0.5上下抖动,训练了上百个epoch也不见明显下降,val mAP一直上不去。 原因:水下垃圾图片的颜色分布和常规图片差异太大,整张图普遍偏蓝绿色,RGB三个通道信息量不如常规数据丰富。如果直接套用YOLOv8默认的马赛克和色彩增强,部分增强反而把原本就低对比度的目标变得更难识别。 解决:训练参数里把hsv_h、hsv_s、hsv_v的增强幅度调小,把mosaic概率降到0.3左右,让模型多看原始水下画面,少看人工拼接后的画面。具体做法是在训练命令里追加:

yolo detect train ... \ hsv_h=0.02 hsv_s=0.4 hsv_v=0.3 \ mosaic=0.3

这类参数改动对水下数据集经常有奇效,代价是鲁棒性略降,但mAP能上去。

4.4 7个类别样本量差异巨大:少数类mAP为0

现象:训练结束,前几个大类mAP在0.6以上,但像鱼线、渔网这种细长目标mAP是0。 原因:数据不平衡,少数类可能只有几十个标注框;细长目标本身在yolo的方形锚框体系里不占优势,正样本太少,模型直接忽略它们。 解决:要么用类权重,要么做复制粘贴增广。我一般把少数类在训练集中重复几遍,设cls权重偏置到少数类:

# 统计类别频率后,把低频类权重调高,例如2.0 weights = [1.0, 1.0, 2.5, 1.5, 2.0, 2.0, 3.0]

更直接的做法是把少数类目标的小图裁出来,贴到其他背景图上扩充样本数。对鱼线这种完全线形的目标,认真考虑转实例分割可能比调anchor更有效,但不要指望纯角框yolo有很好的表现。

4.5 val集划分不固定:同一次训练两次验证结果差3个点

现象:没改任何参数,重启一次训练,val mAP从0.72变成0.69。 原因:训练时没固定随机种子,val集在dataloader里被shuffle了两次;或者val集里漏标严重,随机掉几张小目标图,mAP就往下跳。 解决:固定seed,在训练命令里加seed=42;然后把val集固定下来,用自检脚本检查val里有没有空标签txt。如果val只有几十张图,改成95:5或者增加val集到800张,指标才稳定。val集越小,统计噪声越大,这个波动不是模型退化,是评测本身不稳定。

注意:固定随机种子的方法不只用于切分数据集,训练阶段也要传seed=42,否则同样的yaml跑两次,指标仍然不一样。

4.6 文件名带空格或中文:图片加载成功,标签对不上

现象:训练时提示image 1/xxx corrupt JPEG,或者有些图片直接跳过,实际训练量比预期少。 原因:文件名里带空格、括号或中文时,路径拼接的偶然性变大;更麻烦的是标签名和图片名带着同样的特殊字符,一个转义一个不转义,就会出现“图片存在但label找不到”。 解决:统一把所有图片和标签改成纯数字ID,顺便把命名不规范的问题一次清理干净:

from pathlib import Path def rename_clean(split_dir, start_id=1): img_dir = Path(split_dir) / "images" / "train" lab_dir = Path(split_dir) / "labels" / "train" for img in sorted(img_dir.glob("*.jpg")): new_name = f"{start_id:05d}{img.suffix}" lab = lab_dir / f"{img.stem}.txt" if lab.exists(): new_lab = f"{start_id:05d}{lab.suffix}" img.rename(img.parent / new_name) lab.rename(lab.parent / new_lab) start_id += 1

参数说明:start_id=1,格式化用05d,到99999之前都保持固定位数。数据集更大时改成06d。重命名是数据清洗最后一道保险,花几分钟,但后续所有脚本都不用在编码问题上绕来绕去。很多标注工具导出时就带UUID文件名,名字里混进中文或括号是常态。

5. 训练后的验证技巧:批量推理出图,再决定要不要升级实例分割

训练结束不要只看val mAP,一晚上的成果最终要落在test集上。这里给两个实用手法:一是脚本批量推理并出图,人工核对盲区;二是用json标签里的segmentation字段判断能不能升级成yolo实例分割。

对test集批量预测,ultralytics框架下直接用predict接口最省事:

from ultralytics import YOLO model = YOLO("runs/train/underwater_baseline/weights/best.pt") results = model.predict(source="datasets/underwater/images/test", conf=0.25, save=True, save_txt=True, project="runs/predict_underwater", name="test_output") # 打印每张图的检测概况 for res in results: boxes = res.boxes print(res.path, len(boxes.cls), boxes.cls.tolist(), [round(float(c), 3) for c in boxes.conf])

参数说明:save=True把带框结果图保存下来,save_txt=True把推理坐标落成txt,方便后续统计。conf=0.25是置信度门限,水下垃圾目标偏小,我会拉到0.1到0.15多看候选框,再由业务需求决定误检容忍度。

人工看完几组图,你会发现模型盲区集中在两类:远距离小目标和半透明塑料。这是水下数据的自然瓶颈,不是模型毛病。想突破的话,回到第2章的json标签,检查segmentation字段:如果多数目标带polygon,就把它转成COCO分割标签,用yolo实例分割重训一版,比单纯调anchor有效得多。

数据集边界也要心里有数。5328张对7类来说是能到入门泛化的底料,但对密集遮挡、浑浊水体场景还是会overfit。我的习惯是做完baseline后,再补1/3相近场景数据,用小模型先验证增量有效,再上大模型。这条路线走下来,返工次数最少。

最后说个血泪经验:无论数据格式多规范,训练前一定要跑一遍标签自检,固定随机种子;不然模型指标再好,换台机器复现出来对不上,排查起来极其痛苦。希望这份笔记能帮你在水下垃圾检测方向少走一段路,把更多时间留给调模型,而不是清理数据。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进