☰
鱼数据集VOC与YOLO格式解读及YOLOv8训练实践
2026/10/1 3:07:09 网站建设 项目流程

简介:面向目标检测与鱼类识别任务,该数据集包含31个鱼种、共2798张图像的多类别标注,适合算法学习者、渔业智能化项目开发者以及需要现成训练数据的入门者使用。压缩包约109.1MB,共2000个文件,主体为1999个XML标注文件,可对应VOC格式的目标框与类别信息,另含说明txt,便于了解目录结构与标注规则;文件以xyxr_fish_编号命名,方便按图片索引与批量处理。作为VOC风格标注,XML会记录图像尺寸、目标类别及边界框坐标,可直接解析或转换为YOLO等训练格式。标注类别涵盖Bangus、Big Head Carp、鲶鱼、鲤鱼、金鱼、丝足鲈等31类,覆盖常见养殖、观赏与野生鱼种,类别粒度较细,可以用于检测、分类两类任务的模型训练与误差分析。目前已有417人浏览学习,适合需要标准标注数据、希望节省手工注释时间的目标检测研究者与工程开发人员。

1. 鱼数据集2798张31个种类分类检测VOC+YOLO格式:这个数据集到底能做什么

做目标检测的都知道,公开数据集不缺,缺的是能直接拿去训练、标注干净、类别还够细的业务数据。鱼数据集2798张31个种类分类检测VOC+YOLO格式这个包,解决的正是"我要做鱼类识别/鱼种分类但不想从零标数据"的痛点。2798张图覆盖31个鱼种,每张图都带VOC和YOLO两套标注,拿过来可以直接跑训练,也能用来做检测模型的迁移学习基座。适合三类人:刚入门目标检测、拿现成数据练手的初学者;做渔业、水产养殖监测的开发者;还有需要细粒度分类场景做模型选型对比的工程师。我拿到这个包的第一件事不是看图片,而是先把标注文件从头到尾查了一遍——数据集能不能用,标注决定一大半。

2. 读懂鱼数据集的VOC与YOLO双格式:文件结构、标注字段与坐标换算

2.1 目录结构与文件清单:先摸清楚包里有什么

压缩包解开之后,典型的目标检测数据集目录长这样:VOC格式和YOLO格式各自独立成目录。VOC那边是JPEGImages、Annotations、ImageSets三个子目录,JPEGImages放原始图片,Annotations放XML标注文件,ImageSets/Main里是train.txt、val.txt这些划分文件。YOLO那边是images和labels两个目录,图片与同名txt一一对应。这套结构与Pascal VOC官方数据集、YOLO官方仓库的预期输入保持一致,后续接任何训练框架都不用改目录结构。

# 解压后建议先跑一遍目录核对,确认文件数量对得上 tar -xzf 鱼数据集2798张31个种类分类检测VOC+YOLO格式.zip find . -type f | wc -l find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l find . -name "*.txt" | wc -l

这里有个血泪经验:下载来的数据集解压后,第一件事永远是数文件,不要直接开训练。图片数量、XML数量、txt数量三条线对不齐,说明有漏标或者漏图。我见过一个数据集,图片有3000张,XML少了200份,训练时YOLO直接报错说找不到标注文件,排查了半天。另外注意VOC的ImageSets/Main下面可能同时有train.txt、val.txt、trainval.txt,如果包里只给了全量划分,训练前要自己重新切。VOC格式的train.txt里面存的是不带扩展名的图片文件名,YOLOv8训练时用不了这个,需要转成图片路径或者重新划分。

2.2 VOC的XML标注到底存了什么:解析bndbox与object字段

VOC格式的标注核心是XML文件里嵌套的object节点。每一个object代表图中一个鱼实例,里面最关键的是name标签(鱼种名称)和bndbox子节点(xmin、ymin、xmax、ymax四个坐标)。这四个值是像素绝对坐标,左上角为原点,x向右增大,y向下增大。做检测训练前需要把这四个值读出来,换算成YOLO需要的归一化中心坐标与宽高。

import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) objects = [] for obj in root.iter("object"): name = obj.find("name").text box = obj.find("bndbox") xmin = int(box.find("xmin").text) ymin = int(box.find("ymin").text) xmax = int(box.find("xmax").text) ymax = int(box.find("ymax").text) objects.append({"name": name, "bbox": [xmin, ymin, xmax, ymax]}) return img_w, img_h, objects

这段代码把XML里的图片宽高和每个鱼的目标框读进内存。注意size字段必须读,后续YOLO坐标归一化要用。有些数据集标注不规范,size字段可能缺失或者写错,这时候要自己用PIL或者OpenCV读一遍真实图片尺寸来兜底。我一般会加一道校验:bndbox的xmax不能超过img_w,ymax不能超过img_h,一旦有越界,要么标注坐标有问题,要么图片被resize过但XML没同步更新。

2.3 把VOC坐标转成YOLO格式:归一化换算与txt写入

YOLO格式的核心是把绝对像素坐标转换成相对坐标。每个目标占一行,格式是"class_id x_center y_center width height",四个坐标值都归一化到0到1之间。x_center和y_center是目标框中心点坐标,width和height是框的宽高,全部除以图片宽高。class_id是从0开始的整数,对应names列表里的索引,VOC里存的是鱼种名称字符串,需要先映射成数字。

def voc_to_yolo(img_w, img_h, objects, class_map, out_txt): lines = [] for obj in objects: name = obj["name"] if name not in class_map: print(f"[WARN] 未知类别 {name},跳过") continue class_id = class_map[name] xmin, ymin, xmax, ymax = obj["bbox"] x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) box_w = min(max(box_w, 0.0), 1.0) box_h = min(max(box_h, 0.0), 1.0) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_txt, "w") as f: f.write("\n".join(lines))

参数说明:class_map是鱼种名称到数字ID的映射字典,必须和后面训练用的data.yaml里names顺序完全一致,否则类别就错位了。归一化后的坐标micro数值很小,保留六位小数足够。min/max的clip操作是为了防止标注越界导致训练时报错——如果某个框的xmax超过图片宽度,归一化后width可能大于1,YOLO训练时这种目标会被直接过滤掉,造成有效样本白白流失。这里有个易错点:VOC的坐标是整数,但归一化后是浮点,转换脚本不要做四舍五入取整,直接保留浮点精度。

3. 31个鱼种分类数据体检:类别分布、目标尺度与训练集划分

3.1 统计每个鱼种的图片数量:发现类别不平衡

2798张图分31个类,平均每类90张左右,但真实分布几乎不可能这么均匀。有些鱼种可能有三四百张,有些可能只有二三十张。训练前必须把这个分布摸清楚,否则模型会严重偏向样本量大的鱼种,小样本类别的AP(平均精度)可能趋近于零。

import os from collections import Counter voc_ann_dir = "Annotations" name_counter = Counter() # 统计方式一:直接数XML里每个object的name for xml_file in os.listdir(voc_ann_dir): xml_path = os.path.join(voc_ann_dir, xml_file) img_w, img_h, objects = parse_voc_xml(xml_path) for obj in objects: name_counter[obj["name"]] += 1 for name, cnt in name_counter.most_common(): print(f"{name}: {cnt} 个实例")

统计维度要区分"图片数"和"实例数"。一图多鱼很常见,如果一张图里有三条同种鱼,实例数加了3,但图片数只加了1。YOLO训练时实际关心的是实例数,因为每个标注框都会参与损失计算。实例数少于30的鱼种要标记为高风险类,后面训练参数需要单独调整。统计完分布后,需要把结果保存成CSV格式,方便后续对照训练日志。这个操作也顺便能发现标注错误——比如某类突然出现一个离谱的实例数峰值,或者出现了31类以外的未知类别名,都是标注文件有问题的信号。

3.2 分析目标框尺寸分布:为imgsz参数提供依据

鱼在图片里往往不是均匀分布的。近景的养殖池图片,鱼可能占画面三分之一;水下监控拍的鱼,可能只有几十个像素。目标框的宽高分布直接决定训练时的输入分辨率设置。目标框普遍偏小,imgsz用640会导致小目标特征在downsample后丢失,这时候就要考虑把imgsz拉到960甚至1280。

import numpy as np def compute_box_stats(voc_ann_dir): all_boxes = [] for xml_file in os.listdir(voc_ann_dir): xml_path = os.path.join(voc_ann_dir, xml_file) img_w, img_h, objects = parse_voc_xml(xml_path) for obj in objects: xmin, ymin, xmax, ymax = obj["bbox"] box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h all_boxes.append((box_w, box_h)) arr = np.array(all_boxes) print(f"目标框归一化宽度:均值{arr[:,0].mean():.3f},中位数{np.median(arr[:,0]):.3f}") print(f"目标框归一化高度:均值{arr[:,1].mean():.3f},中位数{np.median(arr[:,1]):.3f}") print(f"小于0.05宽度的框占比:{(arr[:,0] < 0.05).mean()*100:.1f}%")

参数说明:归一化宽高中位数如果小于0.1,说明超过一半的鱼目标在图中占比不到10%,属于中小目标场景。这类数据用YOLOv8的话,建议把imgsz设到960起步,或者使用YOLOv8的P2输出层(ultralytics版本支持通过scale参数调整)。输出了这个统计之后,还要关注极端情况——有些框的宽度占比可能是0.8,说明是特写镜头,这种超大目标和小目标混在一起,模型训练时要靠Mosaic增强和Multi-scale训练来平衡。

3.3 划分训练集与验证集:先划分再训练,不要交给框架

目标检测训练集划分有一个关键原则:先划分,再训练。不要偷懒直接把全量数据丢给YOLO,让它自己切验证集。尤其是这个数据集是"2798张31个种类"这种偏业务场景的包,鱼种分布天然不均,随机划分容易把某个鱼种全部分到训练集,验证集里缺了这一类,指标会虚高但实际泛化不行。

import random from collections import defaultdict def split_dataset_by_class(ann_dir, train_ratio=0.8, seed=42): random.seed(seed) class_to_files = defaultdict(list) for xml_file in os.listdir(ann_dir): xml_path = os.path.join(ann_dir, xml_file) img_w, img_h, objects = parse_voc_xml(xml_path) class_names = set(obj["name"] for obj in objects) for cls in class_names: class_to_files[cls].append(xml_file) # 注意:这里不是直接划分,而是记录每个类别的文件归属,后续按图片级去重 train_files = set() val_files = set() for cls, files in class_to_files.items(): random.shuffle(files) split_point = max(1, int(len(files) * train_ratio)) for f in files[:split_point]: train_files.add(f) for f in files[split_point:]: val_files.add(f) return train_files, val_files

这里有一个容易被忽略的坑:一张图片可能同时包含多个鱼种,如果它含有A和B两类,A类划分时把这张图放进训练集,B类划分时又可能把它放进验证集。所以要按"图片级"去重——先统计每张图片涉及哪些类别,最后判断归属时,只要这张图被任一类别划到训练集,就整体归训练集;或者更严格的做法是,优先保证类别覆盖面。对于这个鱼数据集,我建议的做法是先把31个类别按实例数排序,小样本类(少于50实例)全部优先放入训练集,再从大样本类里按比例补齐验证集。验证集至少要覆盖全部31个鱼种,每个鱼种至少留5个实例来评估。

4. 用YOLOv8跑通鱼数据集训练:data.yaml配置、命令与参数调整

4.1 准备data.yaml与目录文件:命名空间、路径与类别顺序

拿到YOLO格式的鱼数据集后,训练前的准备工作集中在data.yaml和目录整理两件事。data.yaml是YOLO系列的灵魂文件,它告诉训练器去哪里读图片、去哪里读标注、一共有多少类、各类别叫什么名字。这个文件写错一个字段,训练能启动但结果会莫名其妙,最常见的就是names顺序和txt里的class_id对不上。

# fish.yaml path: /data/fish_dataset # 数据集根目录,用绝对路径最省心 train: images/train # 训练集图片目录,相对于path val: images/val # 验证集图片目录 nc: 31 # 类别总数 names: [ "class_00", "class_01", "class_02", "class_03", "class_04", "class_05", "class_06", "class_07", "class_08", "class_09", "class_10", "class_11", "class_12", "class_13", "class_14", "class_15", "class_16", "class_17", "class_18", "class_19", "class_20", "class_21", "class_22", "class_23", "class_24", "class_25", "class_26", "class_27", "class_28", "class_29", "class_30" ]

这里names列表我用的是占位符,实际使用时要替换成压缩包内标注的真实鱼种名称。注意一个细节:names列表的顺序必须和txt标注里的class_id完全一致,class_id是0到30,对应names列表里的第1到第31个元素。很多新手在这里翻车——解压数据集后自己把images重新整理过,labels也跟着移了但名称没同步,结果训练时图片和标注文件配对不上。另外,path字段建议写绝对路径,因为YOLOv8在训练时如果当前工作目录不同,相对路径会解析错。data.yaml文件本身可以放在任意位置,训练命令里引用它的路径即可。

4.2 YOLOv8训练命令:从预训练权重开始还是从头训练

鱼数据集有2798张图,说多不多说少不少。用COCO预训练权重做迁移学习是性价比最高的路线——YOLOv8默认会自动下载yolov8n.pt、yolov8s.pt这些预训练模型,特征提取层已经学到了通用的边缘、纹理、形状特征,鱼的鳞片纹理、鱼鳍轮廓这些底层特征不需要从零学。只有在数据量极大(十万张级别)或者目标域极其特殊时,才考虑从头训练。

# 使用yolov8s模型,输入尺寸960,训练150轮 yolo detect train \ data=fish.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=960 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ seed=42 \ workers=8 \ project=fish_experiment \ name=fish_yolov8s_960

参数说明:imgsz=960是基于前面目标框尺寸统计的结论——如果鱼目标偏小,640的输入会让小目标在特征图中只剩几个像素。batch=16要看显卡显存,12GB显存跑yolov8s加960输入,batch再往上加容易OOM。lr0是初始学习率,迁移学习场景0.01是安全起点,如果训练日志里loss剧烈震荡就降到0.005。epochs=150对2798张图的量级足够,配合Mosaic增强和cosine学习率衰减,一般到100轮左右mAP50就会收敛。workers是数据加载线程数,Windows下建议改成0或2,Linux下可以开大一些。

4.3 训练过程的监控与中断恢复:怎么判断模型在往好的方向走

训练一旦启动,不要干等。YOLOv8会在project/name目录下实时写训练日志和权重文件。看train_batch*.jpg可以直观检查mosaic增强后的图片是否正常;看results.csv里每一轮的metrics,重点盯val的mAP50和mAP50-95两条曲线的走势。如果mAP50在前20轮快速爬升然后平缓,这是正常形态;如果30轮后还在大起大落,说明学习率设置有问题——初始学习率过高会在损失面上震荡。

# 查看训练日志的关键指标,每5轮输出一次均值 tail -f fish_experiment/fish_yolov8s_960/results.csv # 中断后从断点恢复训练 yolo detect train \ data=fish.yaml \ model=fish_experiment/fish_yolov8s_960/weights/last.pt \ epochs=200 \ resume=True

训练途中如果发现数据划分有问题或者需要调整参数,不用从头开始。用last.pt断点续训可以保留已经学到的权重,只修改epochs或者lr继续跑。我一般的操作节奏是:先用yolov8s跑一版baseline,记录mAP50和mAP50-95;然后换成yolov8m或yolov8l提精度上限;最后固定模型规模,做一次超参数微调。这里注意resume=True时不需要再传data.yaml,模型权重里已经记住了数据集配置。

5. 鱼数据集训练的避坑指南:5个最容易翻车的地方

5.1 图片文件名与标注文件名错位导致的漏检

现象:训练正常跑,但训练日志里"skipping object loss"或者验证时鱼目标大量漏检,单类AP剧烈波动。

原因:这个数据集同时提供VOC和YOLO格式,很多人解压后手动整理目录时把图片重命名过(比如加了前缀),但labels目录里的txt没跟着改。YOLO训练时images和labels通过"同名配对"机制匹配——图片叫fish_001.jpg,就去找fish_001.txt,找不到就跳过该图。如果全图都被跳过,模型等于没学到任何东西。

解决:写一段校验脚本,遍历images目录下每张图片,检查labels目录下是否存在同名txt,并报告缺失数量。这个步骤必须在训练前做,不要信"压缩包解压就能用"这种话。我见过有人训练完一轮,loss一直不降,最后发现600张图片的标注文件全部因为改名对不上,模型在纯黑背景上学了个寂寞。

5.2 类别编号错位导致的"预测正确但标签错误"

现象:训练loss正常下降,验证mAP也好看,但实际推理时发现模型输出的鱼种名称和标注对不上,比如把带鱼识别成鲈鱼,而且置信度很高。

原因:VOC转YOLO时class_map字典的排序和data.yaml里names的排序不一致。比如VOC里"带鱼"在XML中排在第一位,你给它分配class_id=0;但data.yaml里names第一个元素写的却是"鲈鱼"。模型学习的是"class_id=0对应某种视觉特征",推理输出时你用names去解释,自然错位。这个问题最隐蔽,因为你只看loss和mAP根本发现不了。

解决:写一个幂等校验脚本。随机抽5张图,读取VOC XML里的name字段,读取对应YOLO txt里的class_id,再查data.yaml里names[class_id]是否和XML里的name一致。确保这5张校验全部通过。注意class_map的生成规则要固定,建议按鱼种名称的字典序排序生成映射,并在data.yaml里用同一个脚本输出names,从源头保证一致。

5.3 小样本鱼种的AP为0:类别不平衡的典型表现

现象:训练完看per-class结果,样本量大的鱼种mAP50能达到0.85以上,某个只有20张图片的鱼种AP直接是0,模型完全学不会。

原因:31个鱼种分布不均,YOLO默认对所有类别等权训练。小样本类别在每轮epoch里只出现两三次,Mosaic增强和随机翻转后有效样本更少,梯度贡献被大样本类淹没。20张的样本量连shuffle后分成batch都不够,模型很难学到该鱼种的判别性特征。

解决:优先保证小样本类的图片全部用于训练(验证集从大样本类里出);训练时开启YOLOv8的class weights,按类别实例数的倒数给loss加权;对小样本类做离线增强,比如复制粘贴目标(把小鱼实例粘贴到其他图片的背景区域),增加每轮出现的次数。注意增强后的图片要重新生成标注,不要直接改原图。

5.4 验证集划分不当导致指标虚高

现象:训练时mAP50显示0.93,肉眼检测效果也凑合,但换了一批真实场景图片测试,mAP直接掉到0.4。

原因:这个数据集来自真实采集,同一个鱼塘或者同一批鱼的视频抽帧,不同帧之间背景高度相似。随机划分训练集和验证集时,训练集里可能包含某条鱼的画面,验证集里也有它的不同角度帧,模型在验证集上"见过"这条鱼,相当于开卷考试。

解决:按数据来源分组——如果压缩包目录本身按视频或场景组织了子目录,按照场景划分而不是按图片划分;如果没有场景信息,用聚类方法按图片的相似度(颜色直方图或者特征向量)分组,再划分train/val。另一个补救办法是,验证时用baseline模型在val上跑一遍,记录每张图的检测置信度,找出置信度异常高的图片确认是否存在数据泄露。

5.5 yolo训练loss为NaN:学习率和梯度问题

现象:训练第一个epoch loss就是nan,或者跑到第20轮突然变成nan,之后权重全部废掉。

原因:最常见的是学习率过高导致梯度炸掉,尤其在迁移学习阶段前几轮,预训练权重和鱼数据分布差异大,梯度爆炸概率不低。另一个隐藏原因是标注坐标出现负数或者宽高为0,归一化后产生无效值,模型计算损失时取对数导致的nan。

解决:把初始学习率从0.01降到0.001,batch也跟着减半;检查全部标注txt,有没有class_id大于30的行,有没有x_center为负数或者width为0的目标框。加一个清洗脚本,把非法标注行直接过滤掉,不要试图修正,标注本身有问题,修正出来的坐标也是错的。通常清洗完标注再做训练,nan就消失了。

6. 用混淆矩阵和难例分析优化鱼种分类:从mAP数字到可解释性

训练跑出不错的mAP只是上半场,真正考验模型的是那些长得像的鱼种——比如两种鲳科鱼外观几乎一样,模型把A认成B是视觉特征太接近,不是模型不行。这时候要看混淆矩阵和难例图片,针对性地调整数据或者策略。

YOLOv8训练结束后,在results目录下会自动生成confusion_matrix.png。这张图是31乘31的矩阵,横轴是真实类别,纵轴是预测类别,对角线越亮越好。重点关注对角线附近非零的格子,比如第5行第7列突然有个亮点,说明第5类鱼经常被误判成第7类。再把误判的图片抽出来看,如果两类鱼在颜色、纹理上确实接近,常规做法是把这两类合并成一个超类;如果人眼能明显区分,说明模型特征提取不够细,需要换更大的模型或者用更高分辨率输入。

另一个实用技巧是跑一遍val集推理,把预测置信度在0.25到0.75之间的边界样本全部导出,这些是"勉强识别"的模棱两可样本。逐张看这些样本,你会发现它们要么是目标被遮挡,要么是光线极差,要么是鱼在快速游动导致运动模糊。针对这三类难例,思路是:遮挡样本可以通过增加部分遮挡数据增强来模拟;光线差的样本引入亮度扰动和对比度变换;运动模糊样本追加少量高斯模糊增强。这些都不用重新标数据,改增强策略就能让边界样本向高置信度区域移动。

# 检查混淆矩阵是否存在,确认训练输出完整 ls fish_experiment/fish_yolov8s_960/ | grep confusion

训练收敛后我会养成的习惯是:把每类鱼的AP单独排序,从AP最低的类开始倒序优化,而不是一次把所有类都调一遍。低AP类先看出错模式,是误检到背景还是和其他类混淆,不同的出错模式对应完全不同的优化动作。误检到背景说明模型把背景纹理学进去了,需要检查标注框是否贴得太紧;和其他类混淆上面已经说了,看混淆矩阵。这样一轮一轮地针对单类调参数,比盲目改全局学习率或者加数据量要高效得多。这个数据集2798张的体量,配合上面的排查流程,足够支撑一个可靠的水产鱼类识别基线模型;后续如果效果还不够,优先补低AP类的图片,而不是平均地加数据——数据预算花在刀刃上,效率高得多。希望这篇拆解能帮你把这个鱼数据集用起来,少走点我当初踩过的弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询