首页 / 资讯中心 / 文章详情

细粒度鱼类识别全流程:数据清洗、迁移学习与部署避坑指南

细粒度鱼类识别全流程:数据清洗、迁移学习与部署避坑指南 ★ FEATURED ARTICLE
简介《基于深度学习的鱼类分类算法研究》是一篇来自《计算机应用与软件》期刊、由华东师范大学研究者撰写的学术论文面向计算机视觉、渔业信息化等领域的研究者与开发者。论文先回顾1990年以来基于形状特征、LDA、BGOT、SVM等传统鱼类识别方法的发展指出其依赖人工设计特征、难以迁移的局限随后系统阐述卷积神经网络基本原理并基于该网络提出迁移学习结合支持向量机的混合分类模型PreCNNSVM。实验以Fish4Knowledge为数据集、TensorFlow为训练框架最终取得98.6%的分类准确率显著优于传统方法为小规模图像数据集的自动特征学习与识别提供了可行方案。资源为1个PDF文件体积仅1.56MB阅读方便已有105人学习下载适合需要快速了解深度学习鱼类识别算法及实验设计思路的读者。1. 鱼类分类不是“标准图像分类”为什么识别鱼比认猫狗难一截做视觉分类的人第一次接到鱼类识别大概率觉得手到擒来——拿ImageNet预训练模型换个分类头就能开跑。真跑起来验证集上能到70%已经算烧高香。问题不在深度学习本身而是鱼类识别属于分类算法里的细粒度场景鲈鱼和石斑鱼的差异可能只集中在斑纹和鳍形之间水下偏色、姿态多样、背景杂乱这几件事叠加起来对数据策略和训练参数的要求比普通图像分类苛刻得多。下面按我会实际落地这个课题的顺序把数据准备、模型选型、训练避坑、评估部署串成一条可复现的路径。适合做毕业设计、渔业监测或养殖自动化项目的工程师和同学参考。2. 先把数据收拾明白鱼类数据集的清洗、标注与类别平衡2.1 先搞清楚你的分类项按“物种”还是按“形态”分鱼类分类这个课题第一个要决策的不是选什么网络而是把分类体系定下来。渔业场景里同一个物种在不同发育阶段长得很不一样比如很多养殖场会把“鱼苗”和“成鱼”单独列类因为投喂策略和病害管理完全不同这时的标签实际上按“形态阶段”划分而不是严格的生物学物种。分类项一旦定错后面再强的模型也救不回来。常见的做法是先写一份类目说明文档每个类放三到五张典型图标注员照着分避免按个人感觉标出大量交叉标签。数据收集渠道一般有四类公开数据集、渔港或养殖池的固定监控、水族箱或水箱的定点摄像、水下机器人的游弋拍摄。这四类来源的成像角度和背景复杂度差异很大直接影响模型在真实场景的表现。公开数据集最省事但背景通常单一鱼的位置也相对居中自己的监控数据最贴合需求缺点是类目分布不可控。我一般建议两条腿走路先拿公开数据把基线跑通同时搭建自己的采集流程后续逐步替换训练分布。2.2 数据清洗与标注最消耗热情的环节清洗流程我通常分三轮。第一轮丢弃明显空镜头、严重模糊和重复帧这类问题在视频抽帧数据里占比经常超过10%第二轮按文件名抽样人工核对标签把贴错类的图挑出去第三轮是等第一个弱模型训练出来之后用它的低置信度预测反向筛数据把模型“拿不准”的样本重新过一遍人工。做到前两轮就够应付中小项目第三轮能帮大项目省下大量标注时间。标注方式上鱼类分类不一定需要画检测框。很多开源数据集直接给整图标签每张图里鱼已经是主体如果你的原始数据是视频流建议先用现成检测器把鱼裁出来再做分类标定注意裁剪边界别把鱼鳍和尾部切掉——这两个部位往往是细粒度分类的关键线索。数据盘点脚本是开工第一步直接看类别分布、尺寸分布和亮度分布下面这段Python能直接盘点目录式数据import os from collections import Counter from PIL import Image, ImageStat img_dir fish_dataset cls_counter Counter() size_counter Counter() total 0 for cls_name in os.listdir(img_dir): cls_path os.path.join(img_dir, cls_name) if not os.path.isdir(cls_path): continue for fname in os.listdir(cls_path): if fname.lower().endswith((.jpg, .jpeg, .png)): img Image.open(os.path.join(cls_path, fname)) w, h img.size stat ImageStat.Stat(img) mean_brightness sum(stat.mean[:3]) / 3 cls_counter[cls_name] 1 size_counter[f{w}x{h}] 1 total 1 for cls_name, cnt in cls_counter.most_common(): print(f{cls_name}: {cnt}) print(ftotal: {total}) for size_name, cnt in size_counter.most_common(5): print(f{size_name}: {cnt})这段脚本逐目录读取图片用Pillow统计每张图的尺寸和平均亮度最后打印各类别样本数和最常见的几种分辨率。类别数量一眼就能看出哪些类严重不足平均亮度整体偏低且伴随偏蓝时说明这个数据集来源的水域普遍偏深后续要做色彩校正或针对性增强。参数说明图片扩展名按实际数据改平均亮度这里只取了RGB三通道的均值实际项目里最好把三个通道的均值分开打印用来判断偏色方向。2.3 数据增强要按水下场景调别照搬ImageNet的默认习惯鱼类分类的增强策略和普通目标分类有明显差别。第一水下图像天然偏蓝绿色直接用常规的ColorJitter随机改色彩可能让模型学到错误的不变性。第二旋转角度要克制鱼体有方向性尾部到头部的摆动通常不超过45度旋转增强给到90度会把原本差异就不大的背鳍花纹彻底打乱。第三水面反光形成的高光区域会吸引注意力训练时配合随机遮挡或擦除增强能提高对高光场景的耐受度。我惯用的torchvision配置参考如下RandomResizedCrop的scale取0.6到1.0而不是预训练常用的0.08——鱼类分类里鱼的比例通常较大裁太小会把特征切没HorizontalFlip打开ColorJitter里brightness0.3、contrast0.3、saturation0.2、hue0.05。这组参数对水下偏色相对温和不会把蓝色调直接拽成绿色。CutMix和MixUp在类别数多且每类超过200张时值得试能明显压低过拟合样本量不足时别上容易让模型学不到刚性的细粒度纹理。注意验证和测试阶段不要用任何随机增强最多做resize和归一化否则验证指标会来回抖动没法判断模型真实水平。2.4 训练集/验证集划分别把同一段视频的连续帧切到两边视频抽帧得到的鱼类数据集有一个很隐蔽的坑很多人直接按帧随机切分训练集和验证集结果同一段视频的连续帧同时出现在两边。模型可能通过背景、水面波纹或鱼群分布完成“记忆”而不是真正学会区分物种。等模型换到新场景时准确率明显缩水这就是典型的“看起来不错、用起来拉胯”。常见做法是按视频片段分组划分把同一个clip的所有帧放进同一个组切分时按组来走保证训练集和验证集没有重叠组。实现上可以用sklearn的GroupShuffleSplitimport numpy as np from sklearn.model_selection import GroupShuffleSplit frames np.array(sorted(all_frame_paths)) groups np.array([extract_clip_id(p) for p in frames]) gss GroupShuffleSplit(n_splits1, test_size0.15, random_state42) train_idx, valid_idx next(gss.split(frames, groupsgroups)) overlap set(groups[train_idx]) set(groups[valid_idx]) print(ftrain clips: {len(set(groups[train_idx]))}, valid clips: {len(set(groups[valid_idx]))}) print(foverlap clips: {len(overlap)})这里extract_clip_id需要根据你的文件名写提取函数比如“clip59_frame0180.jpg”就提取“clip59”。GroupShuffleSplit按组切分末尾打印训练和验证两边的clip集合交集overlap不为0说明分组函数写错了。参数说明test_size在0.15到0.2之间比较合适太大浪费样本太小验证集波动大random_state固定后所有实验都用同一个划分模型之间的对比才有意义。3. 模型选型与训练参数用迁移学习跑出能用的基线3.1 骨干网络怎么选ResNet50、EfficientNet还是Swin鱼类分类数据集规模通常不大从零训练一个深度网络基本不现实迁移学习是主流做法。骨干网络选择主要看三点预训练权重是否好找、显存占用是否可控、在细粒度任务里的历史表现。ResNet50是默认基线torchvision里一行就能加载预训练权重224输入下显存占用量低适合作为第一版跑通流程。EfficientNet-B3/B4在相同计算量下往往比ResNet50精度略高但对增强策略更敏感训练中后期容易出现验证指标抖动。Swin Transformer数据量充足时优势明显样本太少则容易过拟合。三种网络的选择参考下表模型输入分辨率建议数据量显存占用适用定位ResNet502242万张以上低第一版基线务实之选EfficientNet-B3224-3003万张以上中追求推理速度与精度折中Swin-T2245万张以上高大规模细粒度分类验证这里说的“数据量”不是总数而是覆盖到每个类别、且背景环境多样后的有效样本数。类别数如果超过50类建议在数据层面先做分层或预筛比如按体型分组先分大类再分物种两级结构比单级分类更容易收敛。数据量达不到表里水平时老老实实用ResNet50别一上来就追新架构。3.2 迁移学习两阶段先冻结主干再解冻微调迁移学习的标准做法是加载预训练权重后替换最后的全连接层鱼类分类的类别数通常只有几十类把输出维度改成自己的类别数即可。但常见误用是一上来就全量微调学习率给到1e-3结果主干被破坏训练两三个epoch就过拟合。我习惯分成两阶段第一阶段冻结backbone只更新分类头把新分类头训到收敛第二阶段解冻layer4和分类头用小学习率精调网络后半段。下面这段PyTorch代码是这套流程的基本框架import torch import torch.nn as nn import torch.optim as optim from torchvision import models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) num_classes 12 model.fc nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer optim.AdamW(model.fc.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss(label_smoothing0.1) # 第二阶段在第一阶段收敛后解冻最后两层微调 # for name, param in model.named_parameters(): # if not param.requires_grad and layer4 in name: # param.requires_grad True # optimizer optim.AdamW([ # {params: model.layer4.parameters(), lr: 1e-4}, # {params: model.fc.parameters(), lr: 1e-3}, # ], weight_decay1e-4)逻辑说明先用ImageNet预训练权重初始化ResNet50把最后全连接层替换成自己的12类输出随后冻结全部主干参数只让分类头参与梯度计算优化器也只传入分类头参数。这个阶段跑5到10个epoch等验证集指标不再明显上涨后进入注释里的第二阶段解冻layer4主干用1e-4、分类头用1e-3的学习率继续训练。参数说明weight_decay在细粒度任务里设1e-4比1e-5更抗过拟合label_smoothing取0.1能柔化模型输出对类别相似度高的鱼类分类有明显帮助。batch size我一般取32到64ResNet50配224输入在12G显存上跑64没问题细粒度任务不建议小于16统计噪声太大。第二阶段的学习率是整个训练里最值得盯的参数没有之一。主干解冻后lr给到1e-4比较安全给到1e-3会看到验证指标过山车分类头可以用更大学习率因为它是随机初始化的需要更快适应任务。训练时用CosineAnnealingLR把学习率从1e-4降到1e-5对细粒度分类经常有2到3个点的提升。如果想省事也可以用ReduceLROnPlateau监听验证集loss遇到平台期自动降学习率。3.3 损失函数与训练监控只看准确率容易被带偏交叉熵是最稳妥的起点但如果类别分布明显不均衡要先在数据采样层面做处理。常见做法是给每个类设置采样权重让模型每个epoch看到各类样本数接近。数据层面拉平后仍然存在难分样本再考虑Focal Loss它能降低易分样本的梯度贡献把注意力引向困难样本不过Focal Loss有gamma和alpha两个超参要调直接用默认值往往比交叉熵还差。我的经验是数据采样能解决的事不优先交给损失函数。训练过程中的监控项准确率其实最不可靠。我一般同时打印train loss、val loss和每个类的recall。train loss持续下降但val loss不再下降就是过拟合信号某个类recall特别低说明这个类的类内差异超出了模型当前容量需要检查是否有错标数据或样本量不足。训练环境方面单张消费级显卡足够跑ResNet50的整轮实验云端实例主要用于多卡并行或长期后台任务环境配置时留意torchvision与PyTorch版本对齐预训练权重加载接口不一致会直接报错这是深度学习环境配置里最常见的坑之一。4. 实战避坑鱼类分类训练里最常踩的五个坑4.1 验证集准确率90%换一批真实照片只剩六成现象在整理好的数据集上验证集准确率接近90%换成另一个水域、另一台相机拍的照片准确率直接跌到60%上下。原因训练集大多来自视频抽帧鱼的姿态和背景高度重复模型学到的是画面背景、水体纹理和鱼群分布的组合特征而不是纯粹的物种特征。这是数据来源单一导致的分布漂移。解决把采集源头扩成多个水域、多个时间段增强时加入背景噪声比如叠加随机的波纹纹理评估时单独留出一个“陌生场景”子集只做测试绝不允许进入训练。这套做法能提前暴露泛化问题而不是等上线后才发现。4.2 模型把所有鱼都判成数量最多的类现象类别分布严重不均衡时模型把绝大多数输入都预测成样本量最大的那类其他类recall几乎为0。原因交叉熵在类别不均衡下会让模型学会输出先验概率如果最小类只有50张最大类有5000张模型只需一直猜最大类就能在训练集上刷出高准确率。解决先按类别做重采样训练把每个epoch内各类样本量拉平同时给损失函数加类别权重或者在数据层面做复制。重采样后如果小类仍然学不动再看小类内部是不是标签噪声太高鱼苗和成鱼混标的情况很常见。必要时候把小类合并进“其他”类也是一种可接受的工程取舍。4.3 train loss一直降验证集前三轮就开始反弹现象训练到第三四个epochtrain loss平滑下降val loss和验证准确率反向走过拟合来得非常早。原因数据量不足以支撑深度网络的全量表达或第二阶段解冻层数过多、学习率太大。细粒度分类的类别差异本来就微弱模型很快把训练集的表面纹理直接背了下来。解决减少解冻层数只微调最后一段layer4主干学习率从1e-4下调到3e-5增强策略从保守增强切换到MixUp/CutMix如果数据总量低于每类100张优先回头扩充样本而不是继续调超参。这个问题在鱼类分类里比普通图像分类更常见因为类间差异小模型更容易钻空子。4.4 同一条鱼在两个片段里一张判对一张判错现象同一个视频片段里前几帧鱼在画面中央时分类正确后几帧鱼游到边缘、被水草遮挡时分类错误。更隐性的表现是训练集和验证集来自同一段视频验证指标虚高。原因数据划分时按帧随机切分造成训练与验证之间的信息泄漏模型记住了视频片段特有的背景和水流特征却没有真正学会判别鱼的部位。解决严格以“拍摄片段”为最小划分单位按第2.4节的GroupShuffleSplit方案处理。评估时按片段统计准确率而不是按帧统计如果片段级别的准确率明显低于帧级别大概率存在帧间记忆效应需要重新清洗数据。4.5 模型在蓝色水下场景失效颜色域迁移问题现象模型在数据集里表现正常一到蓝色调特别重的深水环境预测分布完全错乱加了ColorJitter反而更差。原因水下不同深度的光衰减方向不一样浅水偏绿、深水偏蓝而ColorJitter的随机抖动是无方向的它生成的颜色扰动和真实偏色方向不一致相当于给模型灌了错误先验。解决先统计真实水下照片各通道均值算出数据集的偏色中心再围绕这个中心做有方向的色彩抖动也可以用灰度化和亮度均衡来弱化颜色依赖迫使模型更多依赖纹理和形状。训练集里如果混有浅水和深水两种环境建议按颜色域分组评估至少分开看指标不能用一个整体数字掩盖问题。5. 让模型在真实水域站得住评估体系与鲁棒性验证5.1 准确率会骗人宏平均比全局准确率更可靠鱼类分类普遍存在类别不均衡全局准确率会被样本量大的类拉高。评估至少要同时看三组数全局准确率、宏平均F1、最小类recall。宏平均把每个类当作等权重对象少量类表现差也能直接暴露最小类recall则反映模型的底线。使用scikit-learn可以直接输出完整报告from sklearn.metrics import confusion_matrix, classification_report preds [] labels [] for images, targets in valid_loader: images, targets images.to(device), targets.to(device) with torch.no_grad(): outputs model(images) preds.extend(outputs.argmax(dim1).cpu().numpy()) labels.extend(targets.cpu().numpy()) cm confusion_matrix(labels, preds) print(cm) print(classification_report(labels, preds, target_namesclass_names, digits3))逻辑说明这段推理代码跑完整验证集把每个batch的预测和真实标签累积起来最后打印混淆矩阵和完整分类报告。混淆矩阵能看出哪些类被互相混淆比如鲈鱼和石斑鱼频繁落进同一块区域说明判别特征还不够分离。参数说明target_names传入按类别顺序排列的类名digits设为3保留三位小数方便对比微调前后的微小提升。打印时建议同时落一份文本到日志文件方便后续回溯。在细粒度分类里最值得关注的不是整体F1而是易混淆类对的混淆率。两类之间混淆占比超过15%就要考虑是否合并类别或者额外补充专门针对这两个类的难例样本。5.2 按拍摄地点做分组验证野外泛化的真正试金石随机划分验证集解决的是“模型有没有记住训练数据”但解决不了“换个地方还能不能认”。真正上线前要按拍摄地点或水域做分组验证训练集用一个水库的数据验证集用另一个水库的数据这种评估结果才能反映野外泛化能力。实现上可以在GroupShuffleSplit基础上加分层逻辑保证训练和验证的类别比例相近sklearn的StratifiedGroupKFold正好适合。每折训练完记录验证集指标最后取均值。如果按水域分组的验证结果比随机划分低8个百分点以上说明模型对外观环境过于敏感优先补充目标场景的真实照片而不是继续调参。5.3 用失败样本反推模型短板Grad-CAM比指标更有说服力评估的最终目的是找到模型短板。把所有错误样本收集起来按类别聚类你会看到两类典型问题一是背景混乱导致误判二是相似物种的细节没有对齐。此时用Grad-CAM对错误样本做可视化观察模型聚焦区域如果热点落在鱼腹和水草上说明模型没有学到背鳍、尾鳍这些判别区域需要检查遮挡情况或调整裁剪策略。常见的做法是把Grad-CAM热力图叠加到原图上人工核对10到20张记录共性问题。这种可视化检查在论文里是展示价值的一部分在工程里则是决定“要不要继续投入数据”的依据。我看到热点跑到背景上的时候通常先检查数据清洗是否干净再决定是否加大裁剪力度。6. 把模型从笔记本挪到甲板轻量化与推理部署6.1 导出ONNX与INT8量化训练完成只是第一步。真实项目里模型要跑在渔港工控机或养殖场边缘盒子上导出和量化是必经之路。常见做法是PyTorch导出ONNX再转推理框架格式import torch model.eval() dummy_input torch.randn(1, 3, 224, 224, devicedevice) torch.onnx.export( model, dummy_input, fish_classifier.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}} )逻辑说明dummy_input决定推理时的输入形状dynamic_axes把batch维设为动态这样部署端既可以单张推理也可以一次送多张图。input_names和output_names要和推理端读取接口对齐接口名字可以随意但两边必须一致否则排查起来很繁琐。ONNX继续转INT8量化后模型体积约缩四分之三推理速度提升明显但细粒度分类对量化敏感某些类别掉点可能超5个百分点。建议量化前后各跑一遍验证集混淆矩阵对比掉点严重就退回FP16。量化校准集要用和实际场景分布一致的图片不能拿训练集随手凑否则校准范围是偏的。6.2 部署后建立主动学习闭环部署不是终点。真实环境会不断产生难例常见做法是给推理服务加低置信度保存机制预测低于阈值就存图人工标注后回流训练集比一次性堆数据更省成本。鱼小、遮挡严重的场景检测加分类的两段式架构比端到端分类更实用先定位鱼体再分类分类只看裁剪图干扰大幅减少。做这个方向几年给我教训最深的是评估协议先于模型设计。早期我被随机划分的验证集哄得高高兴兴换成陌生水域的照片立刻原形毕露后来把按水域分组验证写成固定流程每次实验都必须过这一关才敢说模型真的能用。希望这套从数据到部署的路径对你有帮助也帮你早点避开我踩过的坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站