基于CNN的海洋垃圾识别分类:从数据清洗到模型部署的毕业设计实战

发布时间:2026/10/9 15:27:25
基于CNN的海洋垃圾识别分类:从数据清洗到模型部署的毕业设计实战 简介这份资源是面向计算机相关专业学生与项目实战学习者的毕业设计完整方案主题为基于卷积神经网络的海洋垃圾识别分类。项目利用CNN自动提取图像特征对海洋垃圾进行识别与分类可用于毕业设计、课程设计或期末大作业也适合希望入门深度学习图像处理的开发者。压缩包共101个文件约74.62MB包含20个Python源码文件、7个h5模型权重、6个csv数据表、9个xml标注文件、7个md说明文档以及ipynb实验笔记、png/jpg图像素材和配置文件等覆盖模型构建、数据处理与界面设计等模块。目前已有180人学习下载。项目经过严格调试可直接运行配套文档记录了系统架构、开发流程与常见问题解决方案读者能借此掌握CNN在图像分类中的落地流程理解从数据集训练到模型优化的完整链路减少自行搭建的工作量同时保证毕设质量。1. 从一张糊成马赛克的海洋垃圾照片说起去年帮一个做环保方向的朋友看他的毕业设计他扔过来一个压缩包说“模型训练完了但识别率死活上不去你帮我看看”。我打开他的数据集一看好家伙同一个塑料瓶的照片有的被裁得只剩瓶盖有的背景全是礁石还有几张明显是从视频里截出来的糊得连人眼都分不清是塑料袋还是水母。这就是海洋垃圾识别分类最真实的起点——不是模型不够深而是数据本身就带着一身“海腥味”。这个基于卷积神经网络的海洋垃圾识别分类项目本质上解决的就是这类问题把水下拍摄或海岸巡检拍到的垃圾图像自动分到塑料、金属、玻璃、纸类、橡胶等几个大类里。它适合两类人一类是正在做Python毕业设计、需要一套能跑通、能写进论文、能答辩的完整源码和文档的同学另一类是刚接触CNN实战、想拿一个非玩具数据集练手的工程师。项目本身不复杂但里面关于数据清洗、类别不平衡、小目标识别的处理才是真正值钱的地方。2. 卷积神经网络做垃圾识别的选型逻辑为什么不是直接上YOLO2.1 分类任务和检测任务的边界在哪很多人一提到海洋垃圾识别第一反应是上YOLO做目标检测框出每个垃圾再分类。但毕业设计场景下你要先想清楚一件事你的数据标注到什么程度。如果数据集只给了整张图的类别标签没有边界框那强行上检测模型就是给自己挖坑。这个项目走的是图像分类路线输入一张图输出一个类别概率分布标注成本低训练速度快在答辩时也更容易讲清楚网络结构。常见做法是先用分类模型把“有没有垃圾、大概是什么类型”跑通再考虑要不要升级到检测。我一般会建议如果你的数据集里每张图基本只有一个主体垃圾分类就够了如果一张图里同时有塑料瓶、渔网、金属罐那才需要检测。这个项目的源码里用的是经典CNN backbone加全局平均池化最后接全连接分类头结构不花哨但胜在稳定、易复现。2.2 为什么选迁移学习而不是从零训练海洋垃圾图像的数据量通常不大公开数据集也就几千到一万多张从零训练一个ResNet50过拟合几乎是必然的。项目里采用的是迁移学习策略加载在ImageNet上预训练好的权重冻结前面的卷积层只训练最后的分类层然后再解冻部分高层做微调。这样做的好处是即使你的数据集只有两三千张也能在几十个epoch内收敛到一个可用的精度。参数上初始学习率一般设在1e-3到1e-4之间冻结阶段用1e-3微调阶段降到1e-4或1e-5。batch size根据显存来8GB显存跑224×224的输入batch size设32比较稳。优化器用AdamW比纯Adam更不容易过拟合权重衰减设1e-4。这些参数不是玄学是多次翻车之后总结出来的安全区间。import torch import torch.nn as nn from torchvision import models def build_model(num_classes6, freeze_backboneTrue): # 加载预训练ResNet50weightsIMAGENET1K_V2对应torchvision新接口 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 冻结前面所有卷积层只训练最后的全连接 if freeze_backbone: for param in model.parameters(): param.requires_grad False # 替换最后的分类头输入特征数2048输出为垃圾类别数 in_features model.fc.in_features model.fc nn.Sequential( nn.Linear(in_features, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.4), # dropout防止过拟合0.4是经验值 nn.Linear(512, num_classes) ) return model # 实例化假设有6类垃圾 model build_model(num_classes6, freeze_backboneTrue)这段代码的关键点在于requires_grad False把预训练权重锁住只让新加的分类头参与梯度更新。Dropout(0.4)是防止小数据集过拟合的常用手段如果你发现训练集准确率远高于验证集可以把这个值调到0.5。ResNet50_Weights.IMAGENET1K_V2是torchvision新版的权重接口旧版用pretrainedTrue但新代码建议用新接口避免警告。2.3 数据增强在海洋垃圾场景下的特殊处理通用图像增强里的随机裁剪、翻转、颜色抖动在海洋垃圾数据上不能无脑用。比如水平翻转对塑料瓶没问题但对带有文字标签的垃圾可能改变语义颜色抖动如果幅度太大会把透明塑料和玻璃的色调差异抹掉。项目里用的增强策略是随机旋转±15度、随机水平翻转、轻微颜色抖动亮度0.2、对比度0.2、饱和度0.2再加上随机擦除。随机擦除模拟的是水下遮挡对海洋场景特别有用。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), # 先放大再裁剪保留更多细节 transforms.RandomCrop(224), # 随机裁剪到224×224 transforms.RandomHorizontalFlip(p0.5), # 一半概率水平翻转 transforms.RandomRotation(15), # ±15度旋转 transforms.ColorJitter(0.2, 0.2, 0.2), # 亮度、对比度、饱和度抖动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), # ImageNet统计值 transforms.RandomErasing(p0.25) # 随机擦除模拟遮挡 ])注意Resize到256再RandomCrop到224这是标准做法比直接Resize到224保留了更多空间信息。Normalize用的均值和方差是ImageNet的统计值因为用了预训练权重必须保持一致。RandomErasing的p值不要设太高0.25左右比较合适太高会让模型学不到完整物体特征。3. 从原始图片到训练集数据管道的搭建与坑3.1 目录结构和标签映射的约定项目源码里通常采用ImageFolder风格的目录结构每个类别一个文件夹文件夹名就是类别名。但海洋垃圾数据集经常出现类别名不统一的情况比如“plastic”和“Plastic”混用“metal”和“metal_can”混用。我一般会先写一个脚本把所有文件夹名统一成小写、去空格、去特殊字符然后再生成标签映射表。# 假设原始数据在 raw_data/ 下先统一命名 cd raw_data for dir in */; do new_name$(echo $dir | tr [:upper:] [:lower:] | tr -d | tr -d -) if [ $dir ! $new_name/ ]; then mv $dir $new_name fi done这个bash脚本做的是批量重命名把大写转小写、去掉空格和连字符。别小看这一步我见过太多人因为文件夹名里有个空格导致ImageFolder读取时路径解析出错排查半天以为是代码问题。3.2 类别不平衡的处理重采样还是加权损失海洋垃圾数据里塑料类通常占一半以上玻璃和橡胶可能只有几百张。直接训练的话模型会倾向于预测多数类。项目里给了两种方案一种是WeightedRandomSampler做重采样让每个batch里各类别比例均衡另一种是在损失函数里给少数类更高的权重。我一般推荐重采样因为实现简单效果直观。from torch.utils.data import WeightedRandomSampler import numpy as np # 假设train_dataset是ImageFolder实例 targets [label for _, label in train_dataset.samples] class_counts np.bincount(targets) class_weights 1.0 / class_counts sample_weights [class_weights[t] for t in targets] sampler WeightedRandomSampler( weightssample_weights, num_sampleslen(sample_weights), replacementTrue # 允许重复采样少数类 ) train_loader DataLoader( train_dataset, batch_size32, samplersampler, # 注意用了sampler就不能再设shuffleTrue num_workers4 )class_weights取类别频率的倒数频率越低权重越高。replacementTrue表示有放回采样少数类会被反复抽到。num_samples设成和数据集一样大保证每个epoch看到的样本总数不变。这里有个坑用了sampler之后DataLoader的shuffle参数必须去掉否则会报错。3.3 训练循环里的验证集划分和早停验证集不能随便从训练集里抽因为同一张原始图片可能被增强出多个版本如果增强后的图同时出现在训练和验证集里验证精度会虚高。正确做法是先按原始图片划分训练集和验证集再对训练集做增强。项目里用的是random_split按8:2划分然后分别套用不同的transform。from torch.utils.data import random_split # 先加载无增强的数据集用于划分 full_dataset ImageFolder(rootdata/processed, transformNone) train_size int(0.8 * len(full_dataset)) val_size len(full_dataset) - train_size train_subset, val_subset random_split(full_dataset, [train_size, val_size]) # 再分别套用增强 train_subset.dataset.transform train_transform # 这个写法有坑见下文 val_subset.dataset.transform val_transform上面这种写法其实有问题random_split返回的Subset共享同一个dataset对象直接改dataset.transform会把训练集和验证集的transform都改掉。正确做法是自定义一个Dataset包装类或者用Subset加索引映射。项目源码里用的是自定义TransformSubset类这里不展开但你要知道这个坑的存在。提示验证集的transform只做Resize和Normalize不要加随机翻转和擦除否则验证结果不稳定。4. 训练、评估与推理把模型跑起来的关键步骤4.1 训练脚本的参数配置与日志记录训练脚本一般会暴露几个关键参数--data_dir、--epochs、--batch_size、--lr、--freeze_epochs。freeze_epochs指的是前多少个epoch只训练分类头之后解冻微调。我一般设10个epoch冻结再20个epoch微调总共30个epoch。日志里要记录训练损失、验证损失、验证准确率最好再用tensorboard或wandb画曲线。import argparse import torch.optim as optim parser argparse.ArgumentParser() parser.add_argument(--data_dir, typestr, defaultdata/processed) parser.add_argument(--epochs, typeint, default30) parser.add_argument(--batch_size, typeint, default32) parser.add_argument(--lr, typefloat, default1e-3) parser.add_argument(--freeze_epochs, typeint, default10) args parser.parse_args() # 优化器只更新requires_gradTrue的参数 optimizer optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lrargs.lr, weight_decay1e-4 ) # 学习率调度余弦退火让学习率平滑下降 scheduler optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxargs.epochs, eta_min1e-6 )filter(lambda p: p.requires_grad, model.parameters())这行很重要冻结阶段只把分类头的参数传给优化器避免更新被冻结的卷积层。CosineAnnealingLR让学习率按余弦曲线下降比阶梯下降更平滑最后降到1e-6适合微调阶段。4.2 评估指标准确率不够要看混淆矩阵海洋垃圾分类里把玻璃误判成塑料的代价和把塑料误判成玻璃的代价是不一样的。所以光看总体准确率不够要看每个类别的精确率、召回率和F1。项目文档里给了混淆矩阵的绘制代码这里说下怎么读如果塑料类的召回率很高但精确率低说明模型把很多其他类也预测成了塑料可能是塑料类样本太多导致的。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt def evaluate(model, val_loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) preds torch.argmax(outputs, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 打印每个类别的详细指标 print(classification_report(all_labels, all_preds, target_namesval_loader.dataset.classes)) # 画混淆矩阵 cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsval_loader.dataset.classes, yticklabelsval_loader.dataset.classes) plt.ylabel(True) plt.xlabel(Predicted) plt.show()classification_report会输出每个类别的precision、recall、f1-score直接看哪一类拖后腿。混淆矩阵用seaborn画出来一眼就能看出模型把什么误判成了什么。如果某一类的召回率低于0.6就要考虑是不是该类样本太少或者增强策略不合适。4.3 单张图片推理和批量推理的写法训练完之后推理脚本要能接受单张图片路径也能接受一个文件夹。单张推理的预处理必须和验证集一致Resize到256CenterCrop到224Normalize。不要用训练时的随机增强否则同一张图每次预测结果都不一样。from PIL import Image def predict_single(image_path, model, device, class_names): model.eval() img Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), # 中心裁剪不是随机裁剪 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) input_tensor transform(img).unsqueeze(0).to(device) # 增加batch维度 with torch.no_grad(): output model(input_tensor) prob torch.softmax(output, dim1) pred_idx torch.argmax(prob, dim1).item() return class_names[pred_idx], prob[0][pred_idx].item()unsqueeze(0)是把单张图的维度从[3, 224, 224]变成[1, 3, 224, 224]因为模型要求输入有batch维度。torch.softmax把logits转成概率方便看置信度。如果置信度低于0.5建议人工复核不要直接采信。5. 避坑与排查那些让精度掉10个点的细节5.1 现象训练损失正常下降验证准确率一直卡在60%原因最常见的是数据泄漏。同一张原始图片经过不同增强后分别进了训练集和验证集。模型在训练集里见过验证集的“变体”验证精度虚高但换一批新图就崩。另一个原因是验证集的transform和训练集不一致比如验证集也加了随机翻转导致评估结果不稳定。解决先按原始图片划分训练验证集再分别套用transform。验证集只用Resize、CenterCrop、Normalize。检查random_split之后有没有共享dataset对象的问题。5.2 现象模型把玻璃全预测成塑料原因玻璃和透明塑料在视觉上确实像尤其是水下拍摄时颜色和纹理差异被水体吸收掉了。另外如果玻璃类样本太少模型会倾向于把不确定的样本归到多数类。解决对玻璃类做针对性增强比如增加对比度、锐化边缘。在损失函数里给玻璃类更高的权重或者用focal loss。如果数据允许补充一些玻璃在岸上拍摄的清晰图片。5.3 现象训练到第5个epoch时loss突然变成NaN原因学习率太高或者数据里有损坏的图片。海洋垃圾数据集经常从网上爬取有些图片是0字节或者格式损坏PIL读取时返回NoneToTensor时就会出问题。解决在Dataset的__getitem__里加try-except遇到损坏图片就返回一张全黑的占位图并打印警告。学习率从1e-3降到1e-4再试。另外AdamW的weight_decay不要设太大1e-4是安全值1e-2容易导致NaN。5.4 现象推理时同一张图两次预测结果不同原因推理脚本里误用了训练时的transform包含了RandomHorizontalFlip或RandomErasing。这些随机操作在推理时应该全部关掉。解决推理和验证用同一套确定性transform只做Resize、CenterCrop、Normalize。如果用了model.eval()但还有dropout检查是不是忘了加torch.no_grad()。5.5 现象GPU显存够但训练速度特别慢原因DataLoader的num_workers设成了0数据加载在主进程里串行执行GPU一直在等数据。另一个原因是图片分辨率太大没有提前Resize每次都在线处理大图。解决num_workers设成4或8根据CPU核心数来。提前把图片Resize到256×256存到磁盘训练时直接读取小图能快3到5倍。如果磁盘IO是瓶颈可以考虑把数据打包成LMDB格式。6. 把模型推到能答辩的水平几个提分技巧6.1 用TTA做推理增强TTATest Time Augmentation是在推理时对同一张图做多种变换把预测概率平均。比如原图、水平翻转图、轻微旋转图各预测一次取平均。这个技巧在分类任务上通常能提1到3个点而且实现简单答辩时也是个加分项。def predict_with_tta(image_path, model, device, class_names): model.eval() img Image.open(image_path).convert(RGB) base_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 三种变换原图、水平翻转、旋转10度 transforms_list [ base_transform, transforms.Compose([transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p1.0), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])]), transforms.Compose([transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.RandomRotation((10, 10)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])]) ] probs [] with torch.no_grad(): for t in transforms_list: input_tensor t(img).unsqueeze(0).to(device) output model(input_tensor) probs.append(torch.softmax(output, dim1)) avg_prob torch.mean(torch.stack(probs), dim0) pred_idx torch.argmax(avg_prob, dim1).item() return class_names[pred_idx], avg_prob[0][pred_idx].item()注意RandomHorizontalFlip(p1.0)和RandomRotation((10, 10))在推理时是确定性的因为p1.0表示一定翻转旋转角度固定为10度。这样每次推理结果一致不会出现“同一张图两次结果不同”的问题。6.2 用Grad-CAM可视化模型关注区域答辩时老师经常会问“模型到底学到了什么”。Grad-CAM可以把模型最后卷积层的梯度加权回传到原图高亮出模型关注的区域。如果模型关注的是垃圾本身而不是背景说明学对了如果关注的是水面反光或礁石那就要考虑数据清洗或增加背景多样性。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import numpy as np # 目标层选最后一个卷积块 target_layers [model.layer4[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) # 准备输入 rgb_img np.array(img.resize((224, 224))) / 255.0 input_tensor base_transform(img).unsqueeze(0).to(device) grayscale_cam cam(input_tensorinput_tensor)[0] visualization show_cam_on_image(rgb_img, grayscale_cam, use_rgbTrue)target_layers选model.layer4[-1]这是ResNet50最后一个卷积块特征图分辨率是7×7足够定位但不够精细。如果想让热力图更细可以选layer3但语义信息会弱一些。show_cam_on_image把热力图叠加到原图上红色区域是模型最关注的地方。6.3 模型导出和部署的注意事项如果答辩时需要演示实时识别可以把模型导出成ONNX格式用ONNX Runtime推理速度比PyTorch快不少。导出时注意输入尺寸固定为[1, 3, 224, 224]动态batch在ONNX里支持不好。torch.onnx.export( model, # 模型 torch.randn(1, 3, 224, 224).to(device), # 示例输入 ocean_trash_classifier.onnx, # 输出文件名 input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, # 动态batch output: {0: batch_size}}, opset_version11 )opset_version11兼容性最好别用太新的版本有些部署环境不支持。dynamic_axes允许batch维度动态变化但实际部署时建议固定batch size性能更稳。从那以后我每次拿到一个新的图像分类数据集都会先跑一遍数据完整性检查、类别分布统计和可视化抽样确认没有损坏图片、没有类别名混乱、没有训练验证集泄漏再开始写模型代码。这个习惯帮我省下了至少三次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询