简介:一套面向鱼类图像识别与图像分类任务的已标注数据集,图片总量约13000张,覆盖大头鲤鱼、金鱼、疥鱼、银鲈等31个常见类别,类别划分细致,能支撑多分类模型的训练需求。数据集已按训练集、验证集、测试集划分,每个类别均独立存放,配合附带的json文件即可核对类别标签与映射关系,也适合直接输入CNN分类网络或YOLOv5分类管线进行训练与评估,可有效缩短数据准备时间。压缩包采用7z格式,共2000个文件,以jpg、jpeg、png图像为主,另含1个py可视化脚本和1个json标注文件,整体约132.57MB,体积适中,下载后无需额外寻找数据即可开展实验。资源内提供show可视化脚本,运行后能按类别快速浏览样本,便于检查图像质量与标注分布;json文件同时给出31类的具体名称,省去手动整理类别表。目前已有680人学习下载,适合深度学习初学者、算法工程师及水产相关研究人员用于分类模型验证、教学案例复现或算法对比实验,同样适用于毕业设计与课题预研。
1. 鱼类图像识别数据集:为什么先别急着训练模型
拿到一个「已标注,约13000张数据」的鱼类图像识别数据集,第一反应往往是直接丢进训练脚本里跑。但做过几个数据集的人都会劝你慢一点——图像识别项目里,数据集的标注质量、类别分布和格式统一性,对最终模型效果的影响常常大于模型结构本身。13000张听起来不少,但分散到几十个鱼种、不同生长阶段、不同拍摄条件下,每个类别的有效样本可能只有几百张,这个体量决定了你适合选什么任务、什么模型、什么训练策略。
这篇笔记面向的是手里已经有同类数据集、或者准备在渔业监测、水族识别、水产养殖场景里做图像识别落地的工程师。我会按「拿到数据后先确认什么 → 怎么把标注转成能直接训练的结构 → 训练参数怎么定 → 哪些坑最容易踩 → 怎么把数据集价值再榨一轮」的顺序,把这条路完整走一遍。全程不依赖某个特定框架,代码和思路你可以原样搬到自己的工作流里。
2. 先看标注再看张数:13000张数据集的四个确认项
2.1 分类任务还是检测任务:决定后面所有动作
「图像识别」是个含糊说法,落到工程上首先要区分是图像分类还是目标检测。分类任务只回答「图里是什么鱼」,检测任务还要回答「鱼在哪、有几条」。13000张的体量,分类任务完全够用,检测任务则要看每张图里标注框的数量和密度——如果平均每张图只有一条鱼、一个框,那检测器能学到的空间位置信息很有限。
常见的做法是先看标注文件的结构。COCO格式的 JSON 里有categories和annotations两个核心字段,categories的数量就是类别数,annotations里每条记录对应一个标注框。YOLO 格式的 txt 文件每行是一行class x_center y_center width height(归一化坐标)。如果是 CSV 或 XML(VOC格式),同样先统计类别数和框数。我一般会写一个极短的脚本把这几个数字先拉出来,比肉眼翻文件可靠得多。
import json from collections import Counter with open("annotations.json", "r", encoding="utf-8") as f: coco = json.load(f) cat_id2name = {c["id"]: c["name"] for c in coco["categories"]} cat_counter = Counter() box_counter = 0 for ann in coco["annotations"]: cat_counter[cat_id2name[ann["category_id"]]] += 1 box_counter += 1 print("类别数:", len(cat_counter)) print("标注框总数:", box_counter) print("每个类别的框数:", dict(cat_counter.most_common()))这段代码做的事很简单:把 COCO 标注里的类别 ID 映射回名字,统计每个类别的标注框数量。这里有一个关键点——标注框数量不等于图片数量,一张图可能有多条鱼,所以「13000张数据」对应多少框,必须靠这个脚本确认。如果框数远大于图片数,说明是密集检测场景,训练时要注意小目标较多的问题;如果框数接近图片数,说明每张图基本就一条鱼,模型会更偏向分类特征。
2.2 类别平衡度与标注字段:容易被忽略的两个基本面
鱼类数据集的类别分布极容易失衡。常见经济鱼种(比如鲫鱼、鲤鱼、罗非鱼)样本可能占了大半,稀有鱼种只有几十张。这时候直接训练,模型会对大类别过拟合,小类别基本学不到。最直接的办法是看类别直方图,设定一个阈值:某类别框数低于总数 5% 就该考虑做类别加权或者数据增强补偿。
标注字段也要仔细看。除了类别和框坐标,有没有iscrowd遮挡标记?有没有attributes里记录的鱼体姿态、生长阶段、是否被遮挡?有这些字段意味着你可以做更细的子任务拆分(比如鱼病检测、幼鱼计数),没有也不影响主任务,但如果这些字段存在而你没利用,就浪费了标注成本。另外注意检查有没有空的标注文件、有没有坐标越界(框超出图像宽高)、有没有宽度或高度为 0 的退化框,这些数据异常会直接让训练 loss 变成 NaN。
我常用一个顺序:先跑 2.1 的统计脚本,再写一个 20 行左右的脚本检查框的坐标合法性。坐标越界特别常见,尤其是从网上爬来的数据集,标注工具导出时偶尔会把框的右下角坐标写成整图尺寸。这类脏数据不用删除,裁剪到图像边界就行。
2.3 标注格式统一:YOLO、COCO、VOC 三种格式的选型逻辑
手头数据集的标注格式决定了你用什么框架训练,或者反过来——你定了框架,就得把数据转成对应格式。YOLO 系列(YOLOv5/v8)默认吃 txt 格式,每个图像同名 txt,一行一个框,坐标是相对图像的归一化值。COCO 格式是 JSON 整体打包,适合用 Detectron2、MMDetection 这类框架,也适合做训练集/验证集划分时按 annotation ID 索引。VOC 格式是每张图一个 XML,老项目里很常见,现在直接用的人少了。
我的建议很直接:除非你有必须用 MMDetection 的理由,否则一律转成 YOLO 格式。原因有三个:一是 PyTorch 生态里 YOLO 系列的训练、验证、导出链路最顺;二是归一化坐标对数据增强操作(Mosaic、RandAugment)友好,不需要在增强时实时换算绝对坐标;三是 txt 文件可以直接打开看,排错成本低。COCO 和 VOC 就作为中间格式保留——很多开源数据集的原始发布格式是 COCO,你转一次存一份标准化产物,以后要换框架不用重新从原始标注折腾。
3. 把标注转成 YOLO 格式:转换脚本与四个边界坑
3.1 转换脚本骨架:从 COCO JSON 到 YOLO txt
假设你拿到的是 COCO 格式标注,目标是把每张图的标注写成一个同名 txt 文件,同时生成classes.txt保存类别列表。下面这个脚本是常见做法,核心步骤就三步:建类别映射表、遍历每张图、把绝对坐标转成归一化坐标。
import json import os def coco_to_yolo(coco_json, output_dir, img_dir): with open(coco_json, "r", encoding="utf-8") as f: coco = json.load(f) os.makedirs(output_dir, exist_ok=True) cat_id2cls = {c["id"]: idx for idx, c in enumerate(coco["categories"])} with open(os.path.join(output_dir, "classes.txt"), "w", encoding="utf-8") as f: for c in coco["categories"]: f.write(c["name"] + "\n") img_id2info = {img["id"]: img for img in coco["images"]} anns_by_img = {} for ann in coco["annotations"]: anns_by_img.setdefault(ann["image_id"], []).append(ann) for img_id, anns in anns_by_img.items(): img_info = img_id2info[img_id] w, h = img_info["width"], img_info["height"] txt_path = os.path.join(output_dir, img_info["file_name"].rsplit(".", 1)[0] + ".txt") with open(txt_path, "w", encoding="utf-8") as f: for ann in anns: cat_id = cat_id2cls[ann["category_id"]] x, y, bw, bh = ann["bbox"] # COCO bbox 是 [x, y, width, height] # 边界处理 x1 = max(0.0, x) y1 = max(0.0, y) x2 = min(w, x + bw) y2 = min(h, y + bh) if x2 <= x1 or y2 <= y1: continue # 转归一化坐标 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h nw = (x2 - x1) / w nh = (y2 - y1) / h f.write(f"{cat_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n") print(f"已转换 {len(anns_by_img)} 张图片的标注到 {output_dir}")这个脚本里有两个容易被忽略的细节。第一是 COCO 的bbox字段是「左上角 x、左上角 y、宽度、高度」,而 YOLO 需要的是「中心点 x、中心点 y、宽度、高度」,很多人在这里直接套公式把坐标写错。第二是坐标归一化前先做了边界裁剪,这样能避免越界框在 Mosaic 增强时产生负值坐标。classes.txt的类别顺序必须和转换后的类别 ID 一一对应,这一步错了,训练出来的模型预测结果会张冠李戴。
3.2 数据集目录组织与训练集/验证集划分
转换完标注,下一步是把数据集组织成训练框架能直接读的目录结构。以 YOLOv5/v8 的习惯为例,数据集根目录下放images/train、images/val、labels/train、labels/val四个文件夹,配套一个data.yaml描述路径、类别数和类别名。划分比例一般用 8:2 或 9:1,但要保证验证集里覆盖全部类别,尤其是稀有类别必须在验证集里至少出现一次。
import os import random import shutil random.seed(42) img_dir = "fish_dataset/images" label_dir = "fish_dataset/labels" train_ratio = 0.8 imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] random.shuffle(imgs) split_idx = int(len(imgs) * train_ratio) train_imgs, val_imgs = imgs[:split_idx], imgs[split_idx:] for split, img_list in [("train", train_imgs), ("val", val_imgs)]: os.makedirs(f"fish_dataset/images/{split}", exist_ok=True) os.makedirs(f"fish_dataset/labels/{split}", exist_ok=True) for img_name in img_list: shutil.copy(os.path.join(img_dir, img_name), f"fish_dataset/images/{split}/{img_name}") label_name = img_name.rsplit(".", 1)[0] + ".txt" src_label = os.path.join(label_dir, label_name) if os.path.exists(src_label): shutil.copy(src_label, f"fish_dataset/labels/{split}/{label_name}")这里我固定了随机种子,目的是让每次划分结果一致,否则同一份数据换一次划分就变一次验证集指标,你永远说不清模型效果提升到底是来自模型改进还是数据分布变化。还有一个细节:如果原图里有没有标注文件的图片(理论上不该有),上面的脚本会跳过 label 拷贝,但图片依然进了训练集,训练时 YOLO 会把它当背景图处理。要不要保留这类图取决于你的业务——检测任务里适度加一些纯背景图能降低误检,但如果这类图太多,模型会偏向把一切当背景。
3.3 转换后的验证:权重文件还没训练,先验证数据
转换完别急着开训。先做一次「可视化验证」——把标注框画回图上,肉眼抽查 20~30 张。这一步能发现坐标转换是否正确、类别 ID 是否映射错位、归一化方向有没有搞反。工具上可以用 OpenCV 直接画,也可以用yolo自带的val模式在建好 data.yaml 后跑一次。
import cv2 img = cv2.imread("fish_dataset/images/train/fish_001.jpg") h, w = img.shape[:2] with open("fish_dataset/labels/train/fish_001.txt", "r") as f: for line in f: cls, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.imwrite("check_vis.jpg", img)注意,这个验证脚本里我把归一化坐标乘回图像宽高再画框,用的就是 YOLO 格式的还原公式。如果你在抽查时发现框的位置偏了半个身位,多半不是画框代码的问题,而是转换脚本里中心点坐标算错了。另外要特别看「鱼尾朝左的鱼」和「鱼尾朝右的鱼」——如果标注框总是包住身体却漏掉鱼尾,那不是格式问题,是原始标注的框本身就偏小,这种情况后面训练损失会居高不下。
4. 用 YOLOv8 训练鱼类识别模型:关键参数与数据增强策略
4.1 模型选型与输入分辨率:13000张能撑起什么规模
13000张图像、单类别或几十个类别,YOLOv8n(nano)或 YOLOv8s(small)是合理起点。nano 参数量约 3.2M,s 约 11.2M,在鱼类这种背景相对简单、目标尺度变化不极端的数据上,nano 已经能跑出可用精度,训练速度和部署体积都友好。别一上来就用 YOLOv8x(约 68M 参数),不是因为精度不好,而是 13000 张的体量喂不饱大模型,容易过拟合到训练集的纹理细节上。
输入分辨率建议设成 640 或 960。鱼体是细长形状,如果原图里鱼占画面比例很小(比如水下监控大场景),640 分辨率下小鱼可能只有十几个像素宽,检测器很难稳定学到特征。这时候有两个选择:一是把输入分辨率提到 960,二是把原图按鱼体区域裁剪成子图再训练。我的经验是优先提分辨率,因为裁剪子图会改变鱼体和环境的相对关系,推理时也得做同样处理,链路变长,坑更多。
4.2 训练命令与必调参数:epochs、batch、anchor 与早停
YOLOv8 的训练命令比 v5 简洁,核心参数集中在一条 CLI 里。下面是一个适合 13000 张检测数据集的起步配置。
yolo detect train \ model=yolov8n.pt \ data=fish_dataset/data.yaml \ imgsz=640 \ epochs=100 \ batch=32 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ warmup_epochs=3 \ patience=15 \ augment=True \ mosaic=1.0 \ close_mosaic=10 \ box=7.5 \ cls=0.5 \ dfl=1.5 \ project=run_fish \ name=exp_yolov8n逐个说下关键参数。epochs=100配合patience=15做早停——如果连续 15 个 epoch 验证集 mAP 没提升就自动停,实际可能跑到 60~70 个 epoch 就收敛。batch=32取决于显存,8GB 显存跑 640 分辨率加 nano 模型这个值刚好;显存不够优先减 batch,不要减分辨率。mosaic=1.0是 YOLOv8 的 Mosaic 增强,把四张图拼一起训练,对小目标检测提升明显;close_mosaic=10表示最后 10 个 epoch 关闭 Mosaic,让模型从正常构图的数据上微调收尾,这个组合是官方默认推荐也确实是稳定收敛的关键。
还有一个容易忽略的点:box、cls、dfl是损失函数的权重系数,默认值 7.5/0.5/1.5 适合大多数检测场景。如果你的鱼类数据集里类别不平衡很严重,可以把cls从 0.5 提到 1.0,让小类别也能在 loss 里占足够权重,但注意不要一下提太多,否则大类别会被压制。
4.3 数据增强的取舍:鱼类数据的特殊之处
鱼类图像有两个特点直接影响增强策略。一是鱼体表面有鳞片反光和纹理,过强的色彩抖动(hsv_h、hsv_s)会增加鳞片纹理的随机性,模型可能学到「反光=鱼」这种错误关联;二是水下图像的对比度和色偏和自然图像差异很大,如果数据集里混了水下和清晰水族箱两种拍摄环境,增强参数必须考虑环境一致性。
我的做法是控制色彩增强幅度,重点用几何增强。hsv_h=0.01、hsv_s=0.5、hsv_v=0.4左右是安全区间;几何增强里fliplr=0.5(水平翻转)对鱼类特别有效——鱼没有左右不对称的强语义,翻过来还是鱼,等于类内样本直接翻倍。scale=0.5和translate=0.1用来模拟鱼在不同距离、不同位置下的尺度变化,这与真实养殖场景里鱼游动位置不确定的特性吻合。
旋转增强要谨慎。鱼在水里不是永远水平游,但过大的旋转角(比如超过 30 度)会让鱼体框(细长矩形)和旋转后的鱼体严重不贴合,框内混入大量背景噪声。建议degrees控制在 0 或 10 以内——如果后续部署场景里鱼确实是歪的,再单独加角度样本,不要靠增强硬扛。
5. 鱼类数据集训练避坑记录:五个最常见的翻车现场
5.1 标签噪声:类别标错但框是准的,模型怎么都收敛不了
表现是训练 loss 持续下降,但验证集 mAP 卡在某个值上不去,或者个别类别的 precision/recall 一高一低反复横跳。原因是标注里存在系统性错标——比如某类鱼幼体和成体外形差异大,被标成两个类,而训练集和验证集的错标比例不一致。解决的办法是训练前抽样可视化标注,用训练好的模型跑一遍训练集,把预测类别和 GT 类别不一致的样本挑出来人工复核。这类样本通常集中在 3~5 个易混淆类别上,优先处理它们比全部复核高效得多。
5.2 类别严重不平衡:稀有鱼种 AP 直接是 0
现象是训练日志里大类别(比如罗非鱼)的 AP 到了 0.8,但有一个小类别 AP 从始至终是 0。原因不是模型学不会,而是这个类别样本太少,且这些样本在验证集里也少,评估时正样本数量不足以产生有意义的 AP。解决办法分两步走:第一步在 loss 上做类别重加权(把cls提高或者用class_weight参数);第二步做针对小类别的过采样——把包含稀有鱼种的图片复制若干份,配合几何增强生成变体。如果某个类别样本少于 100 张,建议认真考虑是否值得单独建类——合并成「其他鱼类」也许对你的业务更实际。
5.3 框的大小分布异常:小目标全靠运气
表现是模型能检出大鱼,但小鱼总是漏检,且漏检集中在图像下半部分。原因通常是数据里鱼的大小分布和部署场景不一致——比如训练数据以水族箱拍摄为主,鱼占画面比重大,而部署时是养殖池广角摄像头,鱼小且密集。解决思路有两个方向:一是把训练输入分辨率提高(640 到 960),让小鱼在特征图上的像素数增加;二是调整 Mosaic 增强里的小目标采样策略,让每个批里都包含一些小尺寸的目标。如果这两招都不够,就得考虑收集部署场景的真实图像做半自动标注补充。
5.4 过拟合的隐秘形态:验证 loss 不涨但 mAP 波动大
通常大家判断过拟合看验证 loss,但在小数据集上更常见的信号是:验证 loss 平稳,但 mAP 从第 40 个 epoch 开始剧烈波动。这不是随机性,而是模型在训练集后期开始记住个别困难样本的纹理特征,对验证集里相似但不等同的样本表现不稳定。检查方法是看每个类别的 per-class AP 曲线,如果只有个别类别波动,针对该类别的样本做增强;如果全部类别波动,说明整体数据量不够,优先考虑用预训练权重做迁移学习而不是从头训练。YOLOv8 默认加载的yolov8n.pt是 COCO 预训练权重,在新数据集上继续训的时候,前 10 个 epoch 的lr0过高会破坏预训练特征,这也是波动的一个来源。
5.5 标注框覆盖范围不一致:鱼鳍和鱼尾的取舍
表现是同一类别里,有的框紧贴鱼身,有的框包住全部鱼鳍,有的框只框了头到尾柄的躯干部分。模型被这种不一致的标注搞糊涂,训练时回归损失反复震荡。原因是标注规范没有定义清楚「鱼的边界」——不同标注员理解不同。如果这份数据集在转成 YOLO 前保留了原始标注员 ID 字段,可以按标注员分组统计框的宽高比分布,差异大的组优先复核。没有标注员信息就只能按图片的拍摄来源分组抽检。这个问题看起来小,实际对 mAP 的影响可能超过模型版本的差异。
6. 把 13000 张的数据集再往下挖:难例挖掘与半自动标注的闭环
训练完一版模型,别急着宣成绩效。一个更值得花时间的动作是拿训练好的模型去跑一遍所有训练数据,把「预测置信度低但 GT 有目标」和「预测置信度高但 GT 没有目标」的样本列出来,按置信度排序后抽样人工看。前者是难例——模型没学够,可能需要补充类似样本或者数据增强;后者是疑似误检——模型学到了某些环境特征(比如水草、气泡)当成鱼。这个难例清单是你下一轮迭代最值钱的资产,比调参重要得多。
from ultralytics import YOLO model = YOLO("run_fish/exp_yolov8n/weights/best.pt") low_conf_tp = [] false_positives = [] # 对训练集做一次推理,收集特征 for img_path in train_img_paths: results = model.predict(img_path, conf=0.25, verbose=False) for r in results: for box in r.boxes: conf = float(box.conf[0]) cls = int(box.cls[0]) # 凭经验记录前 200 个低置信度的真阳性 if conf < 0.4: low_conf_tp.append((img_path, cls, conf)) # 高置信度但坐标在图像边缘的框,可能是局部误检 if conf > 0.7 and is_edge_box(box.xyxy[0].tolist()): false_positives.append((img_path, cls, conf))这段代码更像一个「数据体检」工具——conf=0.25是推理阈值,收集conf < 0.4的检测结果,是想找出那些模型虽然检测对了但自己信心不足的样本,这些样本往往意味着特征不充分或者标注框偏了。边缘误检的启发式判断在鱼类场景里特别有用:水下监控图像的边缘经常有光照变化或异物,模型在边缘位置容易产生高置信度的假正检。把这批样本拉出来,如果确认是误检,加边界负样本到训练集里,下一轮迭代会明显改善。
这一轮做完,你可以再考虑一个更进阶的方向:用当前模型做半自动标注,辅助扩展数据集。常见做法是让模型在真实部署场景的新图上跑推理,人工只修正置信度高于阈值的框——修正后进训练集,低于阈值的丢弃。这样 13000 张的起点可以低成本滚到两三万张,数据分布也越来越接近真实环境。我自己做类似数据集的时候,用这个流程迭代过三轮,检测精度从第一轮的 mAP 0.72 提到第三轮的 0.81,其中超过一半的提升来自难例筛选和第二轮数据扩充,而不是换模型。
最后说个教训:13000 张的标注数据集,最大的风险不是数据不够,而是你太信任标注。我吃过一次亏,拿一份标注质量不错的数据集直接训练,模型在测试集上表现很好,一上现场就翻车——后来才发现训练数据里鱼体反光样本多,现场环境偏暗没什么反光。从那以后我每次拿到数据集,第一件事永远是抽样可视化确认标注风格,再决定要不要调整训练参数。希望你这次不用再走一遍这个弯路。
本文还有配套的精品资源,点击获取