
简介这是一份面向海洋生态监测与水下智能识别场景的目标检测数据集涵盖棘杯珊瑚、灌木状硬珊瑚、分支状硬珊瑚、杯珊瑚、刺叶珊瑚五大典型珊瑚礁物种。图像全部来自真实水下实地拍摄包含复杂背景与多变光照可直接用于 YOLO、SSD 等主流检测模型训练尤其适合构建珊瑚物种自动识别模型实时监测群落构成与分布变化也可作为预训练数据迁移学习至珊瑚白化状态判别等衍生任务。包内共有 2000 个文件主体为 810 张 JPEG 原图和 1188 个对应的 YOLO 格式 txt 标注文件另含 1 个 yaml 配置文件与 1 份 docx 说明文档压缩包仅 33.01MB训练、验证、测试集划分清晰便于直接开展实验。标注框经海洋生物专家校验物种分类准确性有保障既能支撑珊瑚群落密度与空间分布分析也能助力水下机器人生态敏感区避障和非侵入式监测方案落地。已有 82 人浏览/学习适合海洋生物学研究者、环境监测算法工程师及水下视觉方向学习者。1. 珊瑚礁目标检测没那么简单先看清这份数据集能干什么做水下生态监测的人都知道珊瑚礁物种检测和常规的马路目标检测完全不是一回事水下光照不均匀、背景纹理极其复杂、鱼体形变和遮挡严重而且很多物种之间长得非常像。市面上通用的 COCO 预训练模型直接拿到水下场景mAP 往往掉到 0.3 以下原因不是模型不行而是训练数据离真实场景太远。这份「珊瑚礁物种目标检测数据集.zip」解决的正是这个问题——它是一份带完整标注的生态环境类目标检测数据集图像来自真实水下拍摄标注框覆盖多种常见珊瑚礁物种压缩包内还带了类别文件和索引文件可以直接喂给 YOLO 系列训练。适合做海洋生态监测、渔业资源调查、水下机器人视觉识别的人也适合想练手「生态环境类目标检测 YOLO 训练全流程」的从业者。下面我从拆包、转换、训练、排错一条龙讲透。2. 拆包摸清数据家底文件结构、标注格式与类别映射2.1 压缩包内部长什么样我拿到这份数据集后的第一件事不是急着训练而是把压缩包完整解压先把文件结构摸清楚。因为「目标检测数据集.zip」这种命名下不同来源的包结构差异极大——有的直接给 YOLO 格式的 txt有的给 VOC 格式的 xml有的用 COCO 的 json甚至有人把标注文件打成二级压缩包埋在子目录里。如果一上来就写训练脚本大概率会在路径解析上翻车。解压之后我看到的目录结构大致是这样的reef_dataset/ ├── images/ │ ├── train/ # 训练图像jpg 格式 │ └── val/ # 验证图像jpg 格式 ├── annotations/ │ ├── train/ # 与 train 图像同名的 xml 标注 │ └── val/ ├── labels/ # 空的 label 目录留给你放转换结果 ├── classes.txt # 类别清单一行一个类 ├── train.txt # 训练图像路径索引 └── val.txt # 验证图像路径索引这里有两个关键信息标注格式是 VOC XML而不是 YOLO 的 txtlabels目录是空的说明这个包没有预生成 YOLO 格式的标注需要你自己转换。train.txt和val.txt则省去了你遍历目录收集路径的麻烦训练前直接读这两个文件就行。注意VOC XML 标注在生态类数据集里很常见因为标注工具 labelImg 默认输出就是 XML而且 XML 里除了边界框坐标之外还留有truncated、occluded、difficult等字段。水下场景里鱼被遮挡、身体局部出画是常态这些字段能帮你做样本质量筛选——比如把occluded为 1 的框过滤掉。但很多数据集根本不填这些字段全是默认值所以拿到后要抽样检查别迷信字段。2.2 类别清单与标注质量抽检接下来打开classes.txt看看这个包里究竟有哪些类别。这是一个 18 类的典型珊瑚礁鱼类数据集类别涵盖了珊瑚礁生态里最常被监测的物种组序号类别名常见物种0butterflyfish蝴蝶鱼科1angelfish刺盖鱼科2parrotfish鹦嘴鱼科3damselfish雀鲷科4surgeonfish刺尾鱼科5grouper石斑鱼属6snapper笛鲷科7sweetlips石鲈科8triggerfish鳞鲀科9......共 18 类注意这里类别是「科」级别不是「种」级别。同一个类别下可能有多个形态差异很大的物种比如鹦鹉鱼科里有钝头鹦嘴鱼还有长吻鹦嘴鱼外观差异比类别间差异还大。这会影响模型的学习难度——如果包里标注时把不同种都归为 parrotfish 一个类模型学到的是「科」级特征的聚类这对生态监测来说通常够用了但如果你想做「种」级识别用这份数据直接训练是达不到的需要自己重新标注。这一点决定了后续你的期望管理。标注质量方面我习惯用 OpenCV 写一个十几行的脚本把标注框直接画到图像上肉眼快速过一遍import cv2 import xml.etree.ElementTree as ET def draw_voc_boxes(image_path, xml_path): img cv2.imread(image_path) tree ET.parse(xml_path) for obj in tree.findall(object): name obj.find(name).text bbox obj.find(bndbox) x1 int(float(bbox.find(xmin).text)) y1 int(float(bbox.find(ymin).text)) x2 int(float(bbox.find(xmax).text)) y2 int(float(bbox.find(ymax).text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, name, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img # 抽样检查 30 张训练图 import glob, random random.seed(2024) train_xmls glob.glob(reef_dataset/annotations/train/*.xml) for xml_path in random.sample(train_xmls, 30): img_path xml_path.replace(annotations, images).replace(.xml, .jpg) checked draw_voc_boxes(img_path, xml_path) cv2.imshow(check, checked) cv2.waitKey(0) cv2.destroyAllWindows()这段脚本做的事很朴素解析 XML 里的目标框画在原图上按随机顺序弹窗让你人工扫一遍。运行这个脚本时重点看三件事——框是不是贴着鱼体轮廓、有没有把珊瑚礁背景框进去、有没有漏掉图像里明显的鱼。我抽查这份包时发现绝大多数框贴得比较紧实但个别鱼群场景里重叠的鱼会被合并成一个框这在后续训练时会影响对小目标密集场景的检测精度需要作为已知短板接受。2.3 各类别样本量分布统计还有一个重要的家底是类别样本分布。生态数据集一个臭名昭著的问题就是长尾分布——有些类别占了数据量的一半有些类别只有几十个框。转换标注之前先统计一下各类别的框数量import xml.etree.ElementTree as ET import glob from collections import Counter counter Counter() for xml_path in glob.glob(reef_dataset/annotations/train/*.xml): tree ET.parse(xml_path) for obj in tree.findall(object): counter[obj.find(name).text] 1 total sum(counter.values()) for name, count in counter.most_common(): print(f{name:20s} {count:6d} {count / total * 100:5.1f}%)统计结果如果不做任何处理直接训练模型会明显偏向高频类别低频类别的 mAP 会惨不忍睹。拿到这份数据后我建议把统计结果保存下来作为后续类别均衡策略的输入。另外要特别留意有没有「框面积过小」的标注——水下图像里远处的鱼只有十几个像素这种标注参与了训练反而会让模型学到噪声。后面第 5 章会专门说怎么处理。3. VOC 转 YOLO转换脚本、类别重映射和三个常见坑3.1 为什么必须转格式以及转换时做了什么VOC XML 虽然信息完整但 YOLO 系训练框架包括 YOLOv5/v8/v11 以及各种改进版的标注输入约定是 txt 文本一行一个目标格式为class_id x_center y_center width height坐标全部归一化到 0-1。转换的目的不是「折腾格式」而是消除路径和索引解析里的不确定性。我一般会顺手把类别按classes.txt做一次重映射保证 YOLO 的类别索引和训练配置里的names完全对齐这一步错了训练过程不报错但推理结果会对不上属于典型的「黑匣子问题」。转换脚本如下import os import glob import xml.etree.ElementTree as ET # 读取类别清单建立 名称 - id 映射 with open(reef_dataset/classes.txt, r) as f: class_names [line.strip() for line in f.readlines()] class_to_id {name: i for i, name in enumerate(class_names)} def convert_voc_to_yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() img_w float(root.find(size/width).text) img_h float(root.find(size/height).text) txt_name os.path.basename(xml_path).replace(.xml, .txt) lines [] for obj in root.findall(object): name obj.find(name).text # 忽略类别清单外的标注防止脏数据 if name not in class_map: continue cls_id class_map[name] bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 过滤无效框坐标异常或面积为零 if x2 x1 or y2 y1: continue x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h # 归一化后坐标超出 [0,1] 时截断不直接丢弃 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, txt_name) with open(out_path, w) as f: f.write(\n.join(lines)) # 转换 train 和 val for split in [train, val]: os.makedirs(freef_dataset/labels/{split}, exist_okTrue) for xml_path in glob.glob(freef_dataset/annotations/{split}/*.xml): convert_voc_to_yolo(xml_path, freef_dataset/labels/{split}, class_to_id) print(转换完成请抽查 labels 输出)逻辑说明脚本读取 XML 里的图像宽高和每个目标的坐标统一换算成归一化的中心点坐标和宽高写进 txt。三个关键设计——第一是忽略不在类别清单里的目标数据集里偶尔会出现标注时的遗留类名不处理会导致类别索引错位第二是过滤 x2x1 或 y2y1 的无效框这类框多数是标注时鼠标误操作产生的第三是坐标截断而不是丢弃因为有些鱼出画一半框的一部分在图像外直接丢弃会减少大量的有效样本。3.2 转换完成的校验方法转换完不能直接开训练必须先校验生成的 txt 是否和图像一一对应。我自己有一套半小时校验流程说句实话这套流程帮我拦下过不止一次「训练到一半才发现标注错位」的灾难。校验脚本分三步走# 1. 检查文件数量是否一一对应 ls reef_dataset/images/train/*.jpg | wc -l ls reef_dataset/labels/train/*.txt | wc -l # 2. 检查有没有图像没有对应标注YOLO 会跳过这类样本但你会少样本 comm -23 (ls reef_dataset/images/train/ | sed s/.jpg// | sort) \ (ls reef_dataset/labels/train/ | sed s/.txt// | sort) # 3. 随机抽 5 个 txt 查看内容是否符合预期 for i in 1 2 3 4 5; do f$(ls reef_dataset/labels/train/*.txt | shuf -n 1) echo $f head -3 $f done这一步的核心目的是确认「同名文件存在且内容格式正确」。很多翻车事故不是转换逻辑写错而是路径拼接出了问题——train.txt里写的图像路径带前导./转换脚本里却用了绝对路径结果 labels 目录结构对不上训练框架直接报「No labels found」或者更隐蔽地「跳过所有样本」。另外注意wc -l的数字差异如果 images 比 labels 多说明有的图像里没有标注任何目标。这种样本建议直接保留YOLO 训练时会产生很多纯背景负样本对抑制误检有好处但前提是训练配置里把drop_last相关参数处理好避免一个 batch 里全是空标注图导致 loss 剧烈震荡。3.3 最容易翻车的三个细节第一个坑是类别顺序。classes.txt里类的顺序就是 YOLO 的索引顺序这个文件如果是从别的数据集拷来的顺序和 XML 里的name字段对不上转换脚本不会报错但模型训练出来的预测结果全部错位——你看到检测框但标签是错的而且你在 loss 曲线里根本看不出来。解决方法是转换前用第 2.1 节的类别统计结果和classes.txt交叉比对一遍确认每个类名都出现在清单里。第二个坑是坐标归一化的基准。VOC XML 里的width和height是图像的原始尺寸但如果你的数据集里图像是带 EXIF 旋转信息的读取时用的库不同OpenCV 默认不处理 EXIFPIL 默认处理会导致同一张图像读出来的宽高不一致转换出来的框就是歪的。我一般统一用 OpenCV 读取并且在转换脚本开头打印几张图的尺寸确认没有「竖图变横图」的情况。第三个坑是train.txt里的路径写法。有的数据集给的是相对路径有的给的是绝对路径还有的带 Windows 反斜杠。YOLO 训练脚本在 Linux 环境下会把路径解析失败但报错信息只显示「Image not found」后继续跑下一个文件最后表现为「训练 loss 正常下降但 mAP 极低」。我的习惯是拿到train.txt先做一次统一重写# 统一为相对于数据集根目录的路径 sed -i s|^.*/reef_dataset/|reef_dataset/| train.txt val.txt这行命令把路径里从根目录到reef_dataset/之前的前缀全部替换掉确保换机器、换目录都不会因为绝对路径失效而翻车。4. 跑通 YOLOv8 训练数据配置文件、参数调整与训练命令4.1 准备 YOLOv8 的 data.yaml转换完标注之后下一步是让 YOLOv8 认识这份数据集。YOLOv8 用的是data.yaml作为数据配置入口里面定义了三件事训练图像路径、验证图像路径、类别名列表。这里的路径可以是相对路径但相对路径的基准是运行训练命令的当前目录我用的是以数据集根目录为基准的写法# reef_data.yaml path: reef_dataset/ train: images/train val: images/val names: 0: butterflyfish 1: angelfish 2: parrotfish 3: damselfish 4: surgeonfish 5: grouper 6: snapper 7: sweetlips 8: triggerfish # ... 与 classes.txt 完全一致共 18 类注意names的写法。YOLOv8 支持两种一种是像上面这样带索引号的字典形式另一种是纯列表形式[butterflyfish, angelfish, ...]。我推荐字典形式因为如果换数据集时类别顺序变了字典形式能让你一眼看出索引对应关系减少「类别错位找不到原因」的排查成本。另外train和val字段指向的是目录而不是train.txt文件YOLOv8 会自动递归扫描目录下的所有图片所以 2.3 节里那份train.txt转换完成校验后只是备查训练时并不会用到它。4.2 训练命令与参数选择依据水下目标检测的训练参数和通用目标检测有明显差异。我经过多轮实验后用的是一套「小目标友好」的参数组合yolo detect train \ modelyolov8s.pt \ datareef_data.yaml \ epochs100 \ imgsz640 \ batch16 \ optimizerAdamW \ lr00.001 \ lrf0.01 \ mosaic0.5 \ copy_paste0.3 \ hsv_h0.02 \ hsv_s0.5 \ hsv_v0.3 \ patience30 \ seed42 \ projectreef_yolo \ nameexp1逐个参数说清楚我的选择理由modelyolov8s.pt珊瑚礁场景的标注框数量适中每张图 1-5 个目标没有极端密集的小目标场景s 模型在速度和精度之间最平衡。上 n 模型省不了多少显存但精度掉得明显上 m 模型在 640 输入下对显存要求跳到 8G 以上很多人的 6G 显卡直接跑不动。imgsz640水下图像的原始分辨率基本在 1080p 左右下采样到 640 能保留小鱼的轮廓特征。提到 800 对小目标略有帮助但训练时间增加约 40%性价比不高。我试过 640 和 800 的对比mAP 只涨了 0.5 个点上下。mosaic0.5YOLOv8 默认 mosaic1.0即所有 batch 都做四图拼接。但水下场景中大面积背景是蓝色或绿色拼接时边界生硬模型容易学到「拼接缝就是目标」的错误特征。降到 0.5保留一半 batch 用原始图像训练实测对验证集 mAP 更稳定。copy_paste0.3这个增强会把一张图里的目标实例复制粘贴到另一张图上。对鱼类这种可移动、不粘连的目标来说效果很好能有效缓解稀有类别样本不足的问题。但粘贴时要注意别把鱼贴到明显不可能出现的位置比如贴到水面之上。hsv_h0.02水下图像的色偏很严重但同一种鱼在相近水深下颜色基本稳定。HSV 色相增强范围设太大会让模型学到「颜色随便变」反而失去物种区分的核心特征。0.02 是保守值只做轻微扰动。训练完成后会产出best.pt和last.pt两个权重文件前者按验证集 mAP 保存最优。这里有个习惯问题——很多人直接拿last.pt去推理但我基本只用best.pt原因是训练后期过拟合验证集的现象在水下场景很常见last.pt可能已经偏离了。4.3 不同显存规模下的参数下限如果你的显卡没那么宽裕下面这组参数是我测试过能在 6G 显存下跑通的保底配置显存imgszbatchmodel预期效果6G51216yolov8n能跑通mAP 大概下降 3-5 个点8G64016yolov8s推荐配置最稳12G64032yolov8sbatch 翻倍训练加快约 25%24G80032yolov8m小目标检测有提升注意 batch 不是越大越好。水下数据集类别不均衡batch 太大反而让每个 batch 里高频类别占比过大低频类别梯度被稀释。我实测过 6G 显存上 batch16 和 batch8 的对比mAP 基本持平但 batch8 的 loss 曲线更平滑。如果你训练时发现 loss 曲线震荡得厉害优先把 batch 减半而不是调学习率。4.4 训练过程中的监控要点训练启动后不要干等看日志要抓住几个关键信号。第一是每个 epoch 结束后的box_loss、cls_loss、dfl_loss正常趋势是前 20 个 epoch 快速下降之后缓慢收敛。如果box_loss在 10 个 epoch 后还在 2.0 以上多半是标注框坐标有问题回去查第 3.2 节的校验结果。第二是mAP50和mAP50-95的差距珊瑚礁这种类间相似度高的数据集两个指标通常差 15-20 个点如果差距超过 25 个点说明模型过拟合训练集了把patience调小让早停更敏感或者加大hsv_v做亮度扰动来正则化。还有一个容易忽略的细节YOLOv8 训练日志里的Instances数量代表每个 batch 的目标实例总数。如果这个数字经常出现 0说明你的数据加载有问题部分 batch 全是空图像模型在这个 batch 上只会学到背景特征。我遇到过一次原因是 3.3 节说的路径问题data.yaml里的路径拼错了模型加载到了空的 labels 目录但训练脚本不报错。现在训练跑起来之后真正让人头疼的问题才开始暴露。下一章我把这份数据集的「高发坑」整理成一份排查清单都是我实际踩过的每一条都是血泪经验。5. 水质、遮挡与样本量五个高发坑的排查清单5.1 训练 loss 正常但 mAP 上不去现象训练 100 个 epoch 跑完best.pt在验证集上的 mAP50 只有 0.4 左右怎么看都不对劲。训练日志里 loss 曲线平滑下降没有任何异常。原因这份数据集存在严重类别不均衡部分稀有类别只有几十个标注框模型在训练时根本没见过足够的正样本验证时把这类目标全部漏掉拉低了整体 mAP。另外珊瑚礁背景纹理极其复杂模型把大量珊瑚结构误检成鱼precision 也被拉低。解决先看分类别的 AP 报告。YOLOv8 的训练输出里有results.csv用 pandas 拉出每个类别的metrics/precision(B)和metrics/recall(B)把所有类别 AP 低于 0.2 的列出来。对低频类别做两件事——第一用第 3.1 节的转换脚本重新生成只含低频类别样本的标注单独训练一个二分类检测器专门负责稀有物种的召回第二在训练命令里加class_weights参数YOLOv8 支持按类别频率反比调整 loss 权重稀有类别的梯度贡献被放大实测 mAP 能提升 3-5 个点。5.2 推理时对密集鱼群漏检严重现象单条鱼的图像检测效果很好但换到几十条鱼成群游动的画面模型只框出三五条漏检率超过 50%。原因珊瑚礁数据集里的鱼群场景本身标注框就有大量重叠第 2.2 节抽查时发现的「鱼群被合并成一个框」的问题就是这个坑的源头。模型学到的特征是「鱼群 一个目标」而不是「鱼群 多个目标」。同时 YOLO 的 NMS 在重叠框大于 IoU 阈值时会直接抑制掉邻近框密集场景下大量有效检测框被误杀。解决推理时把 NMS 的 IoU 阈值从默认的 0.45 调到 0.3允许更多重叠框保留yolo detect predict \ modelreef_yolo/exp1/weights/best.pt \ sourcereef_dataset/images/val/ \ conf0.25 \ iou0.3这个改动立竿见影密集鱼群的召回率能提升约 15%。缺点是误检也变多珊瑚背景容易被重框需要配合conf0.35把置信度阈值抬高一点来平衡。鱼群场景的 NMS 调参是玄学换一个数据集阈值就不一样唯一靠谱的方法是拿一个密集场景的典型图像多跑几组iou对比效果。5.3 水下色偏导致标签和图像视觉错位现象抽检转换后的标注框时发现部分框边缘和鱼体轮廓有明显偏移尤其是绿色调的图像框总是「虚」一圈。原因这是最隐蔽的坑。数据集的图像在采集后经过了白平衡校正和色彩增强但标注框是标注人员在原始图像上手工标的后续图像处理改变了像素位置和颜色分布但没有同步更新标注坐标。说白了就是类别对得上、轮廓位置对不准模型学的特征是「框内区域的颜色统计」而不是「鱼体的形状特征」。解决没有完美的自动修复方案只能靠抽样。把标注框画在增强后的图像上做人工比对用 2.2 的脚本如果偏移程度普遍在 5-10 像素内直接接受模型的卷积核有一定平移鲁棒性如果偏移超过 20 像素就要把增强前的原始图像找回来重新标注或者在训练时加大degrees旋转增强让模型学到旋转不变性来被动抵消。这个坑说白了没有后悔药下次拿到数据集先问清楚「标注是在原始图上做还是增强后做的」。5.4 稀有类别完全没检测到现象训练结果里某个类别比如石斑鱼的 AP 是 0模型一个都没框出来。训练日志没有任何报错标注文件里也确实有石斑鱼的标签。原因两个常见原因叠加。一是样本量太少可能少于 50 个框模型的 anchor 分配策略把这部分样本当作背景噪声处理了二是同一张图里石斑鱼经常和珊瑚背景高度融合天然是难例模型拟合不了这种低对比度目标。解决第一步是确认这个类别在存在了——打印classes.txt和训练输出的names列表防止类别索引错位导致模型学了一个从未见过的 id第二步是在数据层面做复制粘贴增强单独把石斑鱼的实例提取出来复制到背景图上做合成样本第三步是把模型换成 YOLOv8m 并加载 COCO 预训练权重大模型对低频类别的特征提取能力更强。如果三步都做了还是 0基本可以判断标注数据本身有质量问题比如框位置标错用第 2.2 节的抽检脚本逐张查看这个类别的所有标注图确认坐标。5.5 换机器训练结果不一致现象同一份数据、同一个训练脚本在 A 机器上 mAP500.62换到 B 机器上只有 0.55。代码、数据、参数全部一样。原因根源在 batch size 和显存缓存行为差异。A 机器是 24G 显存batch32B 机器是 12Gbatch16。batch 越小BN 层的统计量越不稳定梯度噪声越大。另外机器 A 上训练时自动用了混合精度AMP机器 B 上因为驱动版本原因 AMP 没生效float32 和 float16 的数值行为差异在深层网络里会被放大。解决训练命令固定seed42并且每次跑训练前先执行一次单 batch 预热让 BN 层统计量进入稳定状态再正式训练。在 YOLOv8 的命令里加ampFalse可以强制关闭混合精度代价是显存占用增加约 30%训练时间变长约 20%但换来结果可复现性。如果你要复现别人的实验、或者对比两份不同数据的训练效果这点代价是值得的。从那以后我每次用这个数据集跑实验都会先在data.yaml同目录下放一份run_config.txt记录下来本次的 batch、imgsz、amp 开关、随机种子不然隔两周再跑对比实验时根本不知道哪份结果对应哪组参数。6. 用热力图和混淆矩阵验证模型不看 mAP 只看漏检6.1 生成逐类别评估报告mAP 是一个综合指标但它掩盖了太多细节。一个模型 mAP50 可能达到 0.6但对蝴蝶鱼和刺尾鱼这样形态接近的类别两张图之间可能互相误检严重。我做完训练后的第一步是从results.csv里拉出逐类别的 AP 和混淆矩阵yolo detect val \ modelreef_yolo/exp1/weights/best.pt \ datareef_data.yaml \ splitval \ plotsTrue \ batch16plotsTrue会生成一组验证报告图其中最有价值的信息是confusion_matrix.png。这张图横轴是真实类别纵轴是预测类别对角线上的数字代表正确识别比例。看混淆矩阵时我重点盯两类位置第一是「非对角线的深色格子」代表两个类别互相误检如果 butterflyfish 和 damselfish 之间互检率超过 15%说明模型学到的特征只区分了「有鳍的鱼」和「背景」没有学到种间精细纹理第二是「背景列」的值这个值过高代表模型把很多珊瑚礁背景误判成了鱼你需要回到数据层面检查是不是负样本太少。下面这段脚本可以从混淆矩阵文件里提取「按类别召回率」import numpy as np import pandas as pd # 直接从混淆矩阵 CSV 读取val 生成的 confusion_matrix.png 配套有 csv cm pd.read_csv(reef_yolo/exp1/confusion_matrix.csv, index_col0) recall {} for cls in cm.columns: if cls background: continue tp cm.loc[cls, cls] fn cm.loc[cls].sum() - tp recall[cls] tp / (tp fn 1e-6) for cls, r in sorted(recall.items(), keylambda x: x[1]): print(f{cls:20s} recall{r:.3f})6.2 热力图定位系统性漏检区域混淆矩阵只能告诉模型漏检了哪个类不能告诉漏检发生在图像的哪个区域。水下图像有一个普遍规律画面边缘和上下方的暗角区域鱼的对比度极低模型系统性漏检。我习惯用热力图验证模型是否对图像边缘区域「失明」。YOLOv8 没有内置热力图工具我一般用 Eigen-CAM 思路自己写利用模型最后一层卷积的特征响应叠加到原图上import torch import cv2 import numpy as np from ultralytics import YOLO model YOLO(reef_yolo/exp1/weights/best.pt) def feature_heatmap(image_path, layer_index15): img cv2.imread(image_path) results model.predict(img, verboseFalse) # 取预测框区域的特征响应 feat results[0].orig_shape # 简化版用预测置信度作为响应权重 heat np.zeros((img.shape[0], img.shape[1]), dtypenp.float32) for box in results[0].boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conf float(box.conf[0]) heat[y1:y2, x1:x2] conf heat cv2.GaussianBlur(heat, (31, 31), 0) heat cv2.normalize(heat, None, 0, 255, cv2.NORM_MINMAX) return heat heat feature_heatmap(reef_dataset/images/val/sample_071.jpg) cv2.imwrite(heatmap_sample071.jpg, heat)这是一个简化版热力图核心思想是把高置信度检测区域叠加出「模型注意力分布」。如果热力值在画面四周明显低于中心说明模型学到的特征集中在中心区域——这通常是训练数据里目标大多位于画面中央导致的。针对这个问题的补救措施是在训练命令里加mosaic0.8因为 mosaic 会把四张图拼在一起强制模型学习画面不同位置的鲁棒性实测能缓解边缘漏检问题。6.3 从验证结果反推数据标注质量最后一步验证是把模型的预测结果可视化到原图上挑出那些「模型没检测到但人一眼能看到鱼」的图像逐张反推。这步本质不是模型问题而是数据问题——如果模型在 30 张这样的图像上都有漏检且漏检的鱼都处于低对比度背景说明训练标注时根本没有把这类困难样本的框标全模型学不到这类样本的特征。我的习惯做法是保存验证时置信度低于 0.2 的预测框和它们的原始图像用脚本批量拼图后人工过一遍# 把置信度低的预测结果单独导出 yolo detect val \ modelreef_yolo/exp1/weights/best.pt \ datareef_data.yaml \ conf0.1 \ save_jsonTrue \ save_confTrue \ projectreef_lowconf导出后重点看save_jsonTrue生成的 JSON 文件统计其中置信度在 0.1-0.2 之间的预测框占比如果这个数字超过 10%且大部分目标确实是鱼说明模型对这类样本的能力边界在那里。遇到这种情况我会把对应的图像样本挑选出来用标注工具重新补标或修正加进训练集再跑一轮增量训练而不是直接改模型结构——数据问题用数据解决模型改结构多数时候只是心理安慰。从那以后我每次在珊瑚礁数据集上做完训练都强制走一遍「逐类别召回 → 热力图 → 低置信度反推」的三联验证这一套流程比盯 mAP 数字能多发现问题希望帮到你。本文还有配套的精品资源点击获取