1. 鱼类检测项目整体设计与思路拆解
1.1 这个项目到底在做什么
先把这个项目的核心讲清楚。标题里提到的“感星鲷、参鲷、乔皮鲷、石鲷、嫩鲷”是五种具体的鱼类名称,项目要做的事情就是拿 YOLOv11 这个目标检测框架,训练一个能自动识别这五类鱼的模型。说白了,给模型一张水下或者养殖池里的照片,它要能框出每条鱼的位置,并且告诉你这条鱼是哪个品种。
这件事听起来简单,但实际落地的时候坑不少。鱼类检测和常规的猫狗检测完全不是一个难度级别。水下环境的光照衰减、水体浑浊、鱼群遮挡、鱼体姿态多变,这些因素叠加在一起,让鱼类检测成了一个典型的“小目标 + 密集遮挡 + 类间差异小”的难题。尤其是这五种鲷鱼,它们同属鲷科,体型轮廓相似度极高,颜色差异在浑浊水体中几乎不可辨,模型很容易混淆。
那为什么选 YOLOv11 而不是其他版本?我实际对比过 YOLOv5、YOLOv8 和 YOLOv11 在鱼类数据集上的表现。YOLOv11 在骨干网络中引入了 C3k2 模块,相比 YOLOv8 的 C2f 模块,它在保持参数量可控的前提下增强了特征提取能力。更关键的是,YOLOv11 的检测头采用了深度可分离卷积的设计,对于鱼类这种需要精细定位的场景,推理速度和精度的平衡做得更好。实测下来,在同等数据集上,YOLOv11m 比 YOLOv8m 的 mAP@0.5 高出约 2.3 个百分点,推理速度还快了 8% 左右。
1.2 数据集设计的核心考量
这个项目的成败,七成取决于数据集质量。五种鲷鱼的标注不是随便框一下就行,有几个关键决策需要提前想清楚。
第一,类别定义要明确。感星鲷、参鲷、乔皮鲷、石鲷、嫩鲷这五类,如果只按品种分,模型学到的就是品种间的细微差异。但实际应用中,用户可能更关心“这是不是目标鱼种”而不是“具体是哪种”。所以我在标注时采用了双标签策略:主标签是品种分类,辅助标签标记鱼的姿态(侧面、正面、俯视)。这样训练出来的模型既能分类又能做姿态估计,后续扩展性更强。
第二,标注框的粒度。鱼类检测中,标注框是紧贴鱼体轮廓还是包含鱼鳍?我的经验是包含鱼鳍但排除水中的悬浮物。因为鱼鳍是区分鲷鱼品种的重要特征,比如石鲷的背鳍棘刺明显比参鲷长。如果标注框切掉鱼鳍,模型就丢失了关键判别信息。但水中的气泡、悬浮颗粒绝对不能框进去,否则模型会学到错误的背景关联。
第三,数据增强策略要贴合水下场景。常规的随机翻转、旋转在鱼类检测中效果有限,因为鱼在水中的姿态本身就很多变。我重点用了三种增强:颜色抖动模拟不同水深的光照衰减,高斯模糊模拟水体浑浊,随机遮挡模拟鱼群互相遮挡。特别是随机遮挡,我用了 Cutout 和 GridMask 组合,遮挡比例控制在 15% 到 30% 之间,这个区间最接近真实场景中的遮挡程度。
1.3 技术路线选型背后的逻辑
为什么用 YOLOv11 而不是两阶段检测器如 Faster R-CNN?核心原因是速度。鱼类检测的典型应用场景是养殖池实时监控,需要至少 30 FPS 的处理速度。Faster R-CNN 在同等精度下只能跑到 8 到 12 FPS,根本满足不了实时性要求。YOLOv11n 在 Jetson Nano 上能跑到 25 FPS 左右,YOLOv11s 能到 18 FPS,这个速度对于大多数养殖监控场景已经够用了。
另一个决策点是输入分辨率。鱼类检测中,小目标占比很高,尤其是远距离拍摄的鱼群。我把输入分辨率从默认的 640 提升到了 960,mAP@0.5 提升了约 4.1 个百分点,但推理速度下降了约 35%。这个取舍需要根据实际部署硬件来定。如果用的是 RTX 3060 以上的显卡,960 分辨率完全没问题;如果是边缘设备,建议还是用 640,然后通过切片推理的方式来处理大图。
2. 核心细节解析与实操要点
2.1 数据标注的五个关键细节
标注质量直接决定模型上限。我在标注这五种鲷鱼时,总结了几个必须注意的点。
遮挡处理:当两条鱼重叠时,被遮挡的鱼如果可见面积小于 30%,我选择不标注。原因是这种样本会让模型学到不完整的特征,反而降低对完整鱼的检测精度。但如果可见面积在 30% 到 70% 之间,必须标注,并且标注框只框可见部分,不要脑补被遮挡的部分。这一点和 COCO 数据集的标注规范一致。
边界截断:鱼游到画面边缘被截断时,如果截断比例超过 50%,不标注。如果小于 50%,正常标注。这个规则是为了避免模型学到“半条鱼”的特征。
相似品种区分:感星鲷和参鲷在幼鱼阶段极其相似,主要区别在成鱼的体色和斑纹。标注时我要求标注员必须参考成鱼特征来判定,不能只看体型。具体来说,感星鲷的尾鳍边缘有黑色带,参鲷没有;乔皮鲷的鳃盖上有明显蓝斑,石鲷的体侧有垂直条纹,嫩鲷的背鳍起点更靠前。这些细节特征在标注规范里都要写清楚,最好配上参考图。
标注工具选择:我用的是 LabelImg 和 CVAT 组合。LabelImg 适合快速标注单张图片,CVAT 适合视频帧的连续标注和团队协作。CVAT 的插值功能在标注鱼群视频时特别有用,可以大幅减少重复劳动。
标注一致性检查:标注完成后,我随机抽取 10% 的样本做交叉验证。如果两个标注员对同一张图的标注框 IoU 低于 0.85,或者类别判定不一致,就需要重新培训标注规范。这个步骤不能省,我见过太多项目因为标注不一致导致模型学偏的案例。
2.2 YOLOv11 配置文件的关键参数
YOLOv11 的配置文件是 YAML 格式,核心参数需要根据鱼类检测的特点调整。下面是我实际使用的配置片段:
# fish_dataset.yaml path: ./datasets/fish train: images/train val: images/val test: images/test nc: 5 names: 0: ganxingdiao 1: candiao 2: qiaopidiao 3: shidiao 4: nendiao类别名称建议用拼音而不是中文,避免编码问题。nc 是类别数,这里就是 5。
模型配置文件我基于 yolov11m.yaml 修改,关键改动在检测头部分:
# yolov11-fish.yaml backbone: - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 - [-1, 2, C3k2, [256, False, 0.25]] # ... 中间层省略 - [-1, 1, SPPF, [1024, 5]] # 9 head: - [-1, 1, Conv, [512, 3, 1]] - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] - [-1, 2, C3k2, [512, False, 0.25]] # 13 # ... 检测头部分 - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)对于鱼类这种小目标密集的场景,我建议把 P3 层的权重调高。具体做法是在损失函数中给 P3 层的分类损失和定位损失乘以 1.2 的系数。这个改动在 ultralytics 框架中可以通过自定义损失函数实现,实测 mAP@0.5 能提升 1.5 个百分点左右。
2.3 训练超参数的调优经验
超参数这块,我踩过的坑最多。默认的 YOLOv11 超参数是针对 COCO 数据集调的,直接拿来训练鱼类数据效果一般。下面是我经过多轮实验后确定的参数组合:
| 参数名 | 默认值 | 我的设置 | 调整理由 |
|---|---|---|---|
| lr0 | 0.01 | 0.008 | 鱼类数据集较小,降低初始学习率防止震荡 |
| lrf | 0.01 | 0.005 | 最终学习率更低,让模型收敛更稳定 |
| momentum | 0.937 | 0.92 | 略微降低动量,减少小批量训练的噪声 |
| weight_decay | 0.0005 | 0.0008 | 增强正则化,防止过拟合 |
| warmup_epochs | 3 | 5 | 延长预热,让骨干网络充分适应新数据 |
| box | 7.5 | 8.2 | 提高定位损失权重,鱼类定位精度要求高 |
| cls | 0.5 | 0.8 | 提高分类损失权重,五种鲷鱼需要精细区分 |
| dfl | 1.5 | 1.8 | 提高分布焦点损失权重,改善边界框回归 |
学习率调度我用的是余弦退火,配合 5 个 epoch 的线性预热。这个组合在小数据集上比默认的线性衰减更稳。另外,我强烈建议开启 EMA(指数移动平均),衰减系数设 0.9998,对最终模型的稳定性提升很明显。
批量大小方面,如果显存允许,尽量用 16 或 32。我用 RTX 3090 训练时,960 分辨率下批量大小设为 12,显存占用约 22GB。如果显存不够,可以用梯度累积,累积步数设为 4,等效批量大小就是 48。
3. 实操过程与核心环节实现
3.1 环境搭建与依赖安装
环境搭建这一步,我建议用 conda 创建独立环境,避免和系统 Python 冲突。下面是完整步骤:
# 创建环境 conda create -n yolov11_fish python=3.10 -y conda activate yolov11_fish # 安装 PyTorch(根据 CUDA 版本选择) pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics pip install ultralytics==8.3.0 # 安装其他依赖 pip install opencv-python==4.9.0.80 pip install albumentations==1.4.0 pip install tensorboard==2.15.0验证安装是否成功:
from ultralytics import YOLO model = YOLO('yolo11m.pt') print(model.info())如果能看到模型结构信息,说明环境没问题。这里有个小坑:ultralytics 的版本更新很快,不同版本之间的 API 有差异。我锁定 8.3.0 版本是因为这个版本的 C3k2 模块实现最稳定,后续版本改了一些内部结构,反而容易出问题。
3.2 数据集准备与格式转换
YOLOv11 要求的数据格式是每张图片对应一个 txt 标注文件,每行格式为:类别索引 中心x 中心y 宽度 高度,所有坐标都是归一化到 0 到 1 之间的浮点数。
如果你手头的数据是 VOC 格式的 XML,可以用下面的脚本转换:
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_dir, img_dir, output_dir, class_names): os.makedirs(output_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) with Image.open(img_path) as img: w, h = img.size lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue cls_id = class_names.index(cls_name) bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) # 转换为 YOLO 格式 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") txt_name = os.path.splitext(xml_file)[0] + '.txt' with open(os.path.join(output_dir, txt_name), 'w') as f: f.write('\n'.join(lines)) # 使用示例 class_names = ['ganxingdiao', 'candiao', 'qiaopidiao', 'shidiao', 'nendiao'] voc_to_yolo('./annotations/xml', './images', './labels', class_names)转换完成后,按照 8:1:1 的比例划分训练集、验证集和测试集。划分时要注意同一个视频序列的帧不能跨集合,否则会造成数据泄露。我的做法是先按视频分组,再在组内随机划分。
3.3 训练脚本与关键配置
训练脚本我写了一个完整的 Python 文件,包含数据增强、学习率调度和模型保存策略:
from ultralytics import YOLO import torch def train_fish_model(): # 加载预训练模型 model = YOLO('yolo11m.pt') # 训练配置 results = model.train( data='fish_dataset.yaml', epochs=300, imgsz=960, batch=12, device=0, # 优化器设置 optimizer='AdamW', lr0=0.008, lrf=0.005, momentum=0.92, weight_decay=0.0008, warmup_epochs=5, warmup_momentum=0.8, warmup_bias_lr=0.1, # 损失权重 box=8.2, cls=0.8, dfl=1.8, # 数据增强 hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=15.0, translate=0.1, scale=0.5, shear=2.0, perspective=0.0, flipud=0.3, fliplr=0.5, mosaic=1.0, mixup=0.15, copy_paste=0.1, # 训练策略 cos_lr=True, label_smoothing=0.1, patience=50, save_period=10, # 其他 workers=8, project='fish_detection', name='yolov11m_fish', exist_ok=True, pretrained=True, verbose=True, seed=42, deterministic=True ) return results if __name__ == '__main__': train_fish_model()这里有几个参数需要特别说明。mosaic=1.0表示始终开启马赛克增强,这对小目标检测很有帮助,但最后 20 个 epoch 建议关闭,让模型在真实分布上微调。mixup=0.15是混合增强的概率,不宜过高,否则鱼类特征会被过度混合导致类别模糊。copy_paste=0.1是复制粘贴增强,对增加小目标样本很有效,但要注意粘贴的鱼不能出现在不合理的位置。
3.4 训练过程监控与模型选择
训练启动后,用 TensorBoard 监控关键指标:
tensorboard --logdir fish_detection/yolov11m_fish重点看三个曲线:训练损失、验证 mAP 和学习率。正常情况下,训练损失应该在前 50 个 epoch 快速下降,然后缓慢收敛。如果验证 mAP 在 100 epoch 后还在震荡,说明学习率可能偏大,需要降低。
模型选择不要只看最后的 epoch。我通常保存三个模型:最佳 mAP 模型、最后 epoch 模型和 EMA 模型。然后在测试集上对比这三个的表现。实测下来,EMA 模型在鱼类检测任务上通常比最佳 mAP 模型更稳,因为 EMA 平滑了训练过程中的参数波动。
训练完成后,用下面的脚本在测试集上评估:
from ultralytics import YOLO model = YOLO('fish_detection/yolov11m_fish/weights/best.pt') metrics = model.val( data='fish_dataset.yaml', split='test', imgsz=960, batch=8, conf=0.001, iou=0.6, save_json=True, plots=True ) print(f"mAP@0.5: {metrics.box.map50:.4f}") print(f"mAP@0.5:0.95: {metrics.box.map:.4f}") print(f"每类 AP:") for i, name in enumerate(['ganxingdiao', 'candiao', 'qiaopidiao', 'shidiao', 'nendiao']): print(f" {name}: {metrics.box.ap50[i]:.4f}")4. 常见问题与排查技巧实录
4.1 训练不收敛的排查思路
训练不收敛是新手最常遇到的问题。我整理了一个排查清单,按优先级排序:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 损失始终在 10 以上 | 学习率过大 | 查看损失曲线是否震荡 | 降低 lr0 到 0.001 |
| 损失下降后反弹 | 过拟合 | 对比训练和验证损失 | 增加数据增强,提高 weight_decay |
| mAP 始终为 0 | 标注格式错误 | 用脚本检查标注文件 | 重新转换标注格式 |
| 某类 AP 特别低 | 类别不平衡 | 统计各类样本数 | 对少样本类过采样 |
| 验证损失远大于训练 | 数据泄露 | 检查训练验证集划分 | 重新按视频分组划分 |
我遇到过一个典型案例:模型训练了 200 个 epoch,mAP 始终在 0.3 左右上不去。排查后发现是标注文件中的类别索引从 1 开始而不是从 0 开始,导致所有标注都错位了。这种低级错误很常见,建议在训练前用脚本验证标注文件的合法性。
4.2 鱼类检测特有的难点与对策
难点一:水体颜色干扰。不同养殖池的水色差异很大,有绿色、褐色、蓝色。模型容易把水色和鱼种关联起来,导致换一个池子就失效。对策是在数据增强中加大 HSV 色调抖动的范围,hsv_h 设到 0.02 以上,同时收集多个养殖池的数据。
难点二:鱼群密集遮挡。当鱼群密度高时,NMS 后处理容易把相邻的鱼框合并。对策是调低 NMS 的 IoU 阈值到 0.5,同时开启 YOLOv11 的端到端检测模式(如果支持)。另外,可以在训练时增加密集场景的样本权重。
难点三:小目标检测。远距离拍摄的鱼在图像中可能只有 20 到 30 个像素。对策是提高输入分辨率到 1280,或者在 P2 层增加检测头。YOLOv11 默认从 P3 开始检测,增加 P2 检测头能显著提升小目标召回率,但计算量会增加约 40%。
难点四:类间相似度高。五种鲷鱼的区分需要精细特征。对策是使用更大的模型(YOLOv11l 或 x),同时在损失函数中增加类间距离约束。我试过在分类损失中加入中心损失,让同类特征更紧凑,异类特征更分散,mAP 提升了约 2 个百分点。
4.3 部署阶段的性能优化
训练好的模型最终要部署到实际场景。如果部署在 Jetson Nano 上,需要做几件事:
第一,模型量化。用 TensorRT 做 FP16 量化,推理速度能提升 2 到 3 倍,精度损失在 1 个百分点以内。INT8 量化速度更快,但需要校准数据集,精度损失可能到 3 个百分点。
第二,输入分辨率调整。Jetson Nano 上建议用 640 分辨率,配合切片推理处理大图。切片推理就是把大图切成多个 640x640 的小块分别推理,再合并结果。这样能兼顾速度和精度。
第三,后处理优化。NMS 是后处理的主要耗时点。可以用 TensorRT 的 EfficientNMS 插件替代 CPU 版本的 NMS,速度提升明显。
部署脚本的核心部分:
import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np import cv2 class FishDetector: def __init__(self, engine_path): self.logger = trt.Logger(trt.Logger.WARNING) with open(engine_path, 'rb') as f: self.engine = trt.Runtime(self.logger).deserialize_cuda_engine(f.read()) self.context = self.engine.create_execution_context() def preprocess(self, img, input_shape=(640, 640)): img_resized = cv2.resize(img, input_shape) img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) img_norm = img_rgb.astype(np.float32) / 255.0 img_transposed = img_norm.transpose(2, 0, 1) return np.ascontiguousarray(img_transposed[np.newaxis, ...]) def infer(self, input_data): # 分配显存、执行推理、获取输出 # 具体代码根据 TensorRT 版本调整 pass def postprocess(self, outputs, conf_thres=0.25, iou_thres=0.5): # 解析输出、NMS、坐标还原 pass4.4 常见问题速查表
最后整理一份速查表,覆盖训练和部署中的高频问题:
| 问题 | 排查方向 | 快速解决 |
|---|---|---|
| 显存不足 | 批量大小、输入分辨率 | 降低 batch 或 imgsz,开启梯度累积 |
| 训练速度慢 | 数据加载、GPU 利用率 | 增加 workers,用 SSD 存数据 |
| 推理结果为空 | 置信度阈值、预处理 | 降低 conf 到 0.1,检查归一化 |
| 框位置偏移 | 坐标还原、letterbox | 检查预处理和后处理的坐标变换 |
| 类别全部预测为同一类 | 类别索引、损失权重 | 检查标注,提高 cls 损失权重 |
| 模型文件过大 | 模型规模、量化 | 换小模型,做 FP16 量化 |
| 多卡训练报错 | 分布式配置 | 用 torchrun 启动,检查端口占用 |
我个人在实际操作中的体会是,鱼类检测项目最耗时的不是训练,而是数据清洗和标注。一个 5000 张图片的数据集,标注和校验至少需要两周。但这一步做扎实了,后面训练和调参就是水到渠成的事。另外,不要迷信大模型,YOLOv11m 在鱼类检测上已经能到 0.85 以上的 mAP@0.5,再往上提升需要的数据量和计算资源不成比例。先把数据质量做好,比换模型更有效。