news 2026/10/9 17:27:23

基于卷积神经网络的海洋垃圾识别分类:从数据清洗到模型部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于卷积神经网络的海洋垃圾识别分类:从数据清洗到模型部署全流程

简介:这是一套面向计算机相关专业学生的毕业设计资源,主题为基于卷积神经网络的海洋垃圾识别分类,适合正在准备毕设、课程设计或期末大作业的学习者,也可作为深度学习项目实战练习的参考。资源包共101个文件,约74.62MB,涵盖20个Python源码文件、7个H5模型权重、9个XML标注文件、6个CSV数据表、7个Markdown说明文档及若干PNG、JPG图像与配置文件,完整覆盖模型构建、数据处理、界面设计与项目说明等环节。项目围绕CNN自动提取图像特征展开,通过垃圾图像数据集训练与优化,实现对不同形态、材质海洋垃圾的识别分类,帮助理解从数据准备到模型部署的全流程。目前已有180人学习。源码经过严格调试,可直接运行,配套文档记录了系统架构、开发流程与常见问题解决方案,便于快速上手并保证毕设质量。

1. 海洋垃圾识别这个选题,为什么卷积神经网络是绕不开的那条路

做毕业设计选到「海洋垃圾识别分类」这个方向,很多人第一反应是找个现成模型套上去跑通就完事。但真正动手就会发现,水面反光、浪花泡沫、漂浮物遮挡、类别极度不均衡这几个问题叠在一起,随便拿个迁移学习模型直接推理,准确率能虚高到 95%,实际部署到一段真实拍摄的视频上却频繁把白色浪花判成塑料袋。这就是这个选题的核心矛盾:数据集里的分布和真实水面场景的分布差距太大,而卷积神经网络(CNN)恰好是当前处理这类图像分类任务最成熟、最容易复现、也最容易在答辩时讲清楚的技术路线。

这个项目适合两类人:一类是本科毕业设计需要完整跑通「数据采集→标注→训练→评估→推理」全流程的同学,另一类是已经会调 PyTorch 但没做过完整图像分类项目、想拿一个真实场景练手的开发者。它不要求你有 GPU 集群,一张 8GB 显存的消费级显卡足够把主干网络跑起来;也不要求你从零推导反向传播,但要求你理解卷积核、池化、批归一化这些组件在海洋垃圾这种纹理弱、背景杂的场景里到底起什么作用。后面几章我会按「数据怎么准备→模型怎么搭→训练怎么调→坑在哪→怎么验证」的顺序,把每个环节的参数和判断依据讲清楚,你照着改就能跑。

2. 从原始图片到可训练数据集:海洋垃圾数据的清洗与增强

2.1 为什么这个任务的数据集不能直接拿来用

海洋垃圾图像和常规的 ImageNet 分类任务有个本质区别:类间差异小、类内差异大。一个半透明的塑料袋漂在水面上,和一片白色泡沫板在低分辨率下几乎无法区分;而同一个塑料瓶,正面拍、侧面拍、被水草缠住拍,像素分布能差出好几个量级。常见公开数据集的标注质量参差不齐,很多图片是从视频里抽帧得到的,相邻帧高度相似,如果直接按 8:2 随机划分训练集和验证集,会出现同一段视频的帧同时出现在训练和验证里,导致验证准确率虚高,这就是典型的 data leakage。

我一般会先做三件事:按视频来源分组划分、剔除分辨率低于 224×224 的样本、对类别做一次人工抽检。分组划分的意思是,同一个视频抽出来的所有帧只能进训练集或只能进验证集,不能混。这一步用几十行脚本就能完成,但能直接把虚高的准确率打回真实水平。

2.2 用脚本完成分组划分与类别统计

import os import hashlib import random from collections import defaultdict from PIL import Image # 假设原始数据按 video_xxx/frame_yyy.jpg 组织 RAW_DIR = "raw_data" MIN_SIZE = 224 VAL_RATIO = 0.2 def group_by_video(root): groups = defaultdict(list) for video in os.listdir(root): vpath = os.path.join(root, video) if not os.path.isdir(vpath): continue for fname in os.listdir(vpath): if not fname.lower().endswith((".jpg", ".png")): continue fpath = os.path.join(vpath, fname) # 过滤过小图片,避免训练时反复 resize 引入伪影 with Image.open(fpath) as im: w, h = im.size if min(w, h) < MIN_SIZE: continue groups[video].append(fpath) return groups def split_groups(groups, val_ratio=VAL_RATIO, seed=42): videos = sorted(groups.keys()) random.Random(seed).shuffle(videos) n_val = max(1, int(len(videos) * val_ratio)) val_videos = set(videos[:n_val]) train, val = [], [] for v, paths in groups.items(): (val if v in val_videos else train).extend(paths) return train, val if __name__ == "__main__": groups = group_by_video(RAW_DIR) train, val = split_groups(groups) print(f"train={len(train)} val={len(val)} videos={len(groups)}")

这段脚本的关键点有三个。第一,group_by_video按视频目录聚合,保证同一来源的帧不会被拆散。第二,MIN_SIZE过滤掉过小图片,因为后续统一 resize 到 224 时,小图放大后边缘会出现明显插值伪影,模型容易学到这些伪影而不是垃圾本身的纹理。第三,seed固定,保证每次划分结果一致,方便复现实验。参数上,VAL_RATIO设 0.2 是常见做法,如果视频总数少于 20 个,建议改成按帧数比例划分并手动检查,否则验证集可能只覆盖一两个视频,评估结果波动会很大。

2.3 增强策略:哪些能用,哪些会帮倒忙

海洋垃圾场景的增强不能照搬通用配方。水平翻转、随机裁剪、颜色抖动这三样基本安全;但垂直翻转要慎用,因为水面反射会让上下翻转后的图像出现物理上不合理的纹理,模型可能学到这种伪影。另外,MixUp 和 CutMix 在小数据集上容易让模型欠拟合,我一般只在训练后期、模型已经能稳定收敛后再开,且 alpha 设小一点(0.2 左右)。

import torch from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

RandomResizedCrop的scale下限设 0.7,是为了避免裁出只剩水面没有垃圾的碎片。归一化用的均值方差是 ImageNet 统计值,如果你用预训练权重就必须保持一致,否则第一层卷积的输出分布会偏移,收敛变慢。验证集只做 resize 和归一化,不做任何随机增强,这是评估的基本纪律。

3. 主干网络选型与分类头改造:从 ResNet 到轻量化的取舍

3.1 为什么优先选 ResNet 而不是自己堆卷积

毕业设计里最常见的翻车是「自己设计一个五层卷积网络」,结果训练准确率卡在 60% 上不去。原因不是网络不够深,而是没有残差连接和批归一化,梯度在反向传播时衰减太快。ResNet 的残差结构让梯度能直接跨层回传,批归一化把每层输入分布拉回稳定区间,这两点对海洋垃圾这种低对比度图像尤其重要。常见做法是直接用torchvision.models.resnet18或resnet50,把最后的全连接层换成你的类别数。

选 18 还是 50,取决于你的数据量和显存。数据量在 5000 张以下、类别不超过 10 类,ResNet18 足够,训练快、过拟合风险低;数据量上万再考虑 ResNet50。如果答辩要求「轻量化」,可以换 MobileNetV3 或 EfficientNet-B0,但要注意这些网络对输入分辨率更敏感,224 以下掉点明显。

3.2 改造分类头与冻结策略

import torch.nn as nn from torchvision import models def build_model(num_classes=6, backbone="resnet18", pretrained=True): if backbone == "resnet18": net = models.resnet18(weights=models.ResNet18_Weights.DEFAULT if pretrained else None) feat_dim = net.fc.in_features elif backbone == "mobilenet_v3": net = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT if pretrained else None) feat_dim = net.classifier[-1].in_features net.classifier[-1] = nn.Linear(feat_dim, num_classes) return net else: raise ValueError(backbone) # 替换分类头,加一层 Dropout 抑制过拟合 net.fc = nn.Sequential( nn.Dropout(p=0.3), nn.Linear(feat_dim, num_classes) ) return net def freeze_backbone(net, freeze=True): # 只冻结特征提取部分,分类头始终可训练 for name, param in net.named_parameters(): if "fc" in name or "classifier" in name: param.requires_grad = True else: param.requires_grad = not freeze

Dropout(p=0.3)这个值不是拍脑袋定的:海洋垃圾数据集通常几千张,p 设 0.5 会导致训练损失下降很慢,设 0.1 又压不住过拟合,0.3 是我在几个类似任务上试出来的平衡点。freeze_backbone的作用是先用冻结特征训练几轮分类头,让随机初始化的全连接层先收敛,再解冻整体微调,这样能避免一开始就大学习率把预训练权重带偏。常见做法是冻结训练 3 到 5 个 epoch,然后解冻,学习率降到原来的十分之一。

3.3 类别不均衡的处理:加权损失还是重采样

海洋垃圾数据里,「塑料瓶」和「塑料袋」样本往往占一半以上,而「渔网」「金属罐」可能只有几百张。直接训练会让模型偏向多数类。两种处理方式:一是在损失函数里给少数类更高权重,二是用 WeightedRandomSampler 重采样。我一般优先用加权损失,因为它不改变每个 epoch 看到的样本总数,训练时间可控。

import numpy as np import torch from torch.utils.data import WeightedRandomSampler def make_class_weights(labels, num_classes): counts = np.bincount(labels, minlength=num_classes).astype(np.float32) # 防止除零,取倒数后归一化 weights = 1.0 / np.maximum(counts, 1.0) weights = weights / weights.sum() * num_classes return torch.tensor(weights, dtype=torch.float32) def make_sampler(labels, num_classes): counts = np.bincount(labels, minlength=num_classes).astype(np.float32) class_w = 1.0 / np.maximum(counts, 1.0) sample_w = class_w[labels] return WeightedRandomSampler(weights=sample_w, num_samples=len(labels), replacement=True)

make_class_weights返回的权重直接传给nn.CrossEntropyLoss(weight=...)。注意权重归一化到类别数之和,是为了让损失量级和未加权时接近,学习率不用重新调。如果你同时用了加权损失和重采样,容易矫枉过正,少数类被反复采样导致过拟合,所以二选一即可。

4. 训练循环里的参数设置与显存控制

4.1 学习率、批大小与优化器的搭配

海洋垃圾分类任务上,我常用的起点是:AdamW,学习率 3e-4,权重衰减 1e-4,批大小 32。为什么不用 SGD?因为毕业设计的训练轮数通常有限(30 到 50 epoch),SGD 收敛慢,AdamW 能在更少轮数内到达可用精度。批大小 32 是在 8GB 显存下跑 ResNet18 加 224 输入的稳妥值,再大容易 OOM,再小批归一化统计不稳定。

import torch from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR def build_optim(net, lr=3e-4, wd=1e-4, epochs=40): optimizer = AdamW( filter(lambda p: p.requires_grad, net.parameters()), lr=lr, weight_decay=wd ) scheduler = CosineAnnealingLR(optimizer, T_max=epochs, eta_min=lr * 0.01) return optimizer, scheduler

filter(lambda p: p.requires_grad, ...)这行很重要:冻结主干时,如果不过滤,优化器仍会为冻结参数维护动量状态,浪费显存。CosineAnnealingLR的eta_min设成初始学习率的 1%,避免最后几轮学习率降到 0 导致完全停止更新。

4.2 混合精度训练与显存监控

如果显存吃紧,开混合精度是最直接的缓解手段,通常能省 30% 到 40% 显存,速度也有提升。但要注意,混合精度下损失缩放如果没配好,会出现梯度下溢导致 loss 变 NaN。

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for epoch in range(epochs): net.train() for imgs, labels in train_loader: imgs, labels = imgs.cuda(), labels.cuda() optimizer.zero_grad() with autocast(): logits = net(imgs) loss = criterion(logits, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()

GradScaler会自动调整损失缩放系数,遇到 inf 或 NaN 会跳过该步并降低缩放。如果你发现训练中途 loss 突然变 NaN,先检查是不是学习率太大,其次看数据里有没有损坏图片导致输出异常。监控显存可以用torch.cuda.max_memory_allocated(),每个 epoch 打印一次,方便判断还能不能加大批大小。

4.3 验证指标怎么选才不骗自己

准确率在类别不均衡时参考价值有限。我一般同时记录宏平均 F1和每类召回率。宏平均 F1 对少数类敏感,能暴露模型是不是只学会了多数类。如果某个类别召回率长期低于 0.5,要么是该类样本太少,要么是和其他类视觉上太像,需要回去看混淆矩阵。

from sklearn.metrics import f1_score, classification_report def evaluate(net, loader, device="cuda"): net.eval() preds, gts = [], [] with torch.no_grad(): for imgs, labels in loader: imgs = imgs.to(device) logits = net(imgs) preds.extend(logits.argmax(1).cpu().numpy()) gts.extend(labels.numpy()) macro_f1 = f1_score(gts, preds, average="macro") print(classification_report(gts, preds, digits=3)) return macro_f1

classification_report会直接打出每类的 precision、recall、f1,答辩时这张表比一条准确率曲线有说服力得多。注意验证时一定要net.eval()并配合torch.no_grad(),否则 Dropout 和批归一化会按训练模式运行,结果不可复现。

5. 避坑与排查:海洋垃圾分类项目里最容易翻车的五件事

5.1 验证准确率 99% 但推理一塌糊涂

现象:训练日志里验证集准确率很快冲到 0.99,但拿一段新视频抽帧推理,结果惨不忍睹。原因:几乎可以断定是数据划分时同一视频的帧泄漏到了验证集,模型记住了背景而不是垃圾本身。解决:回到 2.2 的分组划分脚本,确认split_groups是按视频聚合后再划分的,打印训练集和验证集的视频 ID 列表,确保没有交集。

5.2 训练 loss 不降反升,几轮后变 NaN

现象:前几个 epoch loss 正常下降,突然某一轮跳到 NaN,之后再也回不来。原因:常见有三种——学习率太大、混合精度损失缩放异常、数据里有损坏图片。解决:先把学习率降到 1e-4 重跑;如果还 NaN,关掉混合精度;再不行就写脚本遍历所有图片,用 PIL 打开一遍,把抛异常的图片移出数据集。我遇到过一张截断的 JPEG,PIL 能打开但解码出全黑图,模型对全黑图输出极端 logits,直接把 loss 带飞。

5.3 少数类召回率始终上不去

现象:宏平均 F1 卡在 0.6,查看分类报告发现「渔网」这类召回率只有 0.3。原因:要么样本太少,要么该类和其他类在颜色纹理上高度重叠。解决:先确认加权损失或重采样是否生效,打印每个 batch 的类别分布;如果确认生效仍无改善,考虑对该类做针对性增强,比如随机旋转角度加大、加运动模糊模拟水下拍摄,或者干脆合并视觉上难以区分的子类,减少类别数。

5.4 换用 MobileNet 后精度断崖式下跌

现象:ResNet18 上宏 F1 有 0.85,换成 MobileNetV3 后掉到 0.65。原因:轻量网络的特征通道数少,对低对比度目标的表达能力弱,且默认输入分辨率下感受野偏小。解决:把输入分辨率从 224 提到 288 或 320,同时把学习率调低到 1e-4,训练轮数增加 20%。如果还不行,说明这个任务本身不适合过度轻量化,答辩时如实说明取舍即可。

5.5 推理速度慢,单帧超过 200ms

现象:模型精度达标,但部署到边缘设备上单帧推理要 200ms 以上,达不到实时。原因:没做推理优化,模型仍在 FP32 下运行,且输入预处理在 CPU 上串行执行。解决:用torch.jit.trace导出 TorchScript,开 FP16 推理;预处理改成 GPU 上的torchvision.transforms或直接写 CUDA kernel。常见做法是先把模型导出 ONNX,再用推理引擎加载,速度能提升 2 到 3 倍。

6. 用混淆矩阵和 Grad-CAM 验证模型到底学到了什么

训练完拿到一个还不错的 F1,不代表模型真的在看垃圾。我习惯做两件事来验证:画混淆矩阵看错误集中在哪些类对之间,用 Grad-CAM 看模型关注区域是不是落在垃圾上而不是水面反光。这两步在答辩时也是加分项,因为它证明你不只是调包,而是理解模型行为。

混淆矩阵用 sklearn 的confusion_matrix配合 seaborn 热力图即可,重点看非对角线上的大块。如果「塑料袋」和「白色泡沫」互相误判严重,说明这两个类在特征空间里没被分开,可以考虑加一个二阶段分类器专门区分这两类。Grad-CAM 的实现用pytorch-grad-cam库最省事,指定目标层为 ResNet 的layer4,把热力图叠加到原图上。

from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import numpy as np def visualize_cam(net, img_tensor, target_layer, class_idx=None): cam = GradCAM(model=net, target_layers=[target_layer]) grayscale_cam = cam(input_tensor=img_tensor.unsqueeze(0), targets=None if class_idx is None else [class_idx]) grayscale_cam = grayscale_cam[0] # img_tensor 需反归一化到 [0,1] 再叠加 rgb = img_tensor.permute(1, 2, 0).cpu().numpy() rgb = (rgb - rgb.min()) / (rgb.max() - rgb.min() + 1e-6) return show_cam_on_image(rgb, grayscale_cam, use_rgb=True)

target_layers选layer4是因为它感受野最大,热力图覆盖区域和语义目标对应最好;选浅层会得到细碎边缘,参考价值低。如果热力图高亮区域集中在图像边缘或水面波纹上,说明模型学到了背景捷径,这时候要回去检查数据增强是不是不够,或者验证集划分仍有泄漏。

一个我踩过的坑:Grad-CAM 默认对 batch 里所有样本做反向,如果一次传多张图,显存会爆。正确做法是一次只传一张,或者用batch_size=1的循环。另外,热力图叠加前一定要把 tensor 反归一化回 [0,1],否则叠加出来的图颜色完全失真,看不出模型在看哪。

最后说个习惯:每次改完数据划分或增强策略,我都会固定随机种子重跑一遍,把宏 F1、每类召回率、混淆矩阵三样一起存到实验记录里。这样当答辩老师问「你这个提升是真实提升还是随机波动」时,你能直接翻出三次不同种子的结果对比,而不是靠嘴说。这个习惯帮我省过很多次后悔药,也希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 17:26:32

北京大学MOOC Java程序设计:零基础系统入门与核心难点突破指南

1. 这门课到底在讲什么&#xff1a;从标题拆解核心定位“Java程序设计”这五个字看起来平平无奇&#xff0c;但加上“北京大学MOOC”这个限定&#xff0c;它的分量就完全不一样了。我前后完整跟过三轮这门课&#xff0c;也推荐过不少朋友去学&#xff0c;发现很多人对它的认知存…

作者头像 李华
网站建设 2026/10/9 17:23:23

金仓数据库模拟题带答案为何仍过不了?考点与实操避坑指南

简介&#xff1a;金仓数据库模拟题练习题&#xff08;含答案&#xff09;是一份面向数据库运维人员、国产化数据库初学者及备考相关认证读者的自测资料&#xff0c;聚焦KingbaseESv8的核心知识体系。题目覆盖金仓数据库“三高三易”特性、OLTP与OLAP的业务区别、国产CPU平台支持…

作者头像 李华
网站建设 2026/10/9 17:19:50

博图WinCC V16中ADODB与DataGrid实现SQL Server数据画面展示

简介&#xff1a;这份文档面向工业自动化领域的博图WinCC V16使用者&#xff0c;尤其是需要在HMI画面上实时展示SQL Server数据的工程师与调试人员。内容围绕ADODB组件与DataGrid控件的配合展开&#xff0c;给出可直接参考的VB脚本示例&#xff0c;解决WinCC与数据库交互时数据…

作者头像 李华
网站建设 2026/10/9 17:17:00

全类目加属性SQL:三表模型与行转列宽表实战

简介&#xff1a;一份包含淘宝全量类目、属性及属性值的SQL数据文件&#xff0c;主要面向电商后台开发、数据分析以及数据库学习者&#xff0c;可用于还原淘宝类目树结构、梳理属性与属性值的枚举关系&#xff0c;也为商品筛选、竞品分析或推荐系统原型提供真实数据支撑。压缩包…

作者头像 李华
网站建设 2026/10/9 17:15:59

C# WinForms文件夹目录结构对比工具:备份校验与同步检查实践

简介&#xff1a;面向IT开发与运维人员的文件夹目录结构对比工具&#xff0c;适用于版本控制、备份恢复、目录同步等场景&#xff0c;快速核对两个文件夹的内容是否一致。不同于基于MD5的完整校验&#xff0c;它递归遍历文件系统&#xff0c;比较文件大小、修改时间和创建时间等…

作者头像 李华