简介这是一套基于卷积神经网络的海洋垃圾识别分类毕业设计项目面向计算机相关专业正在做毕设的学生以及需要项目实战练习的初学者可直接作为毕业设计、课程设计或期末大作业使用。项目围绕海洋垃圾图像识别这一实际场景构建CNN模型完成垃圾种类自动分类无需人工设计特征提取器涉及数据处理、模型训练、界面设计等完整流程。压缩包内共101个文件约74.62MB主要包含20个Python源码文件、9个XML配置、7个H5模型权重、7个Markdown说明文档以及CSV数据集、图片样本、TXT说明等结构清晰便于按模块查阅。目前已有180人学习下载。资源附有完整项目说明文档详细记录系统架构、开发流程、使用方法与常见问题解决方案源码经过严格调试可稳定运行既能帮助理解CNN在图像识别中的实际应用也能减少毕业设计开发工作量是兼顾环保课题与技术学习的高质量参考项目。1. 海洋垃圾识别分类项目为什么我劝你先看数据而不是先调网络做基于卷积神经网络的海洋垃圾识别分类很多人一上来就盯着 ResNet 换不换、学习率调多少结果训练到第三天发现准确率卡在 60% 上不去。我拆过这个 Python 毕业设计项目源码最大的感受是这类任务的瓶颈从来不在网络结构而在数据是不是干净、标签是不是靠谱、训练流程有没有留后路。这份资源包含完整的 CNN 训练源码、数据集组织脚本和文档说明覆盖从图片预处理、模型搭建到训练评估的完整链路适合正在做毕业设计、或者想快速落地一个图像分类小项目的从业者。接下来我按实际拆项目的顺序把每一段能直接抄走的代码和参数讲清楚。2. 数据集与预处理把图片文件夹转成可训练的 Tensor2.1 数据集的目录结构与标签映射逻辑海洋垃圾识别分类的数据集常见做法是按类别建文件夹比如plastic_bottle、aluminum_can、fishing_net、glass_bottle每个文件夹里放对应类别的图片。这种结构的好处是 PyTorch 的ImageFolder可以直接读不需要手写标签文件。项目源码里的目录大概长这样dataset/ ├── train/ │ ├── plastic_bottle/ │ ├── aluminum_can/ │ ├── fishing_net/ │ └── glass_bottle/ ├── val/ │ ├── plastic_bottle/ │ ├── aluminum_can/ │ ├── fishing_net/ │ └── glass_bottle/ └── test/ ├── plastic_bottle/ ├── aluminum_can/ ├── fishing_net/ └── glass_bottle/这里有个细节值得注意ImageFolder会按文件夹名的字母序生成类别索引也就是说aluminum_can是 0fishing_net是 1glass_bottle是 2plastic_bottle是 3。如果你后面要输出中文标签或者做混淆矩阵得提前把映射关系固定下来别在训练结束后再反推容易被字母序搞晕。我一般会单独存一个class_to_idx.json训练前打印一次确认。2.2 图像预处理流水线尺寸、通道和归一化参数卷积神经网络对输入尺寸有固定要求海洋垃圾图片来自不同拍摄设备尺寸差异很大。源码里把训练集统一缩放到 224×224验证集和测试集同样处理保证推理时输入张量的 shape 一致。下面这段是项目里的预处理代码我加了注释from torchvision import transforms # 训练集随机翻转 随机旋转 归一化 train_transforms transforms.Compose([ transforms.Resize((224, 224)), # 强制缩放到 224x224不保持宽高比 transforms.RandomHorizontalFlip(p0.5), # 水平翻转增加样本多样性 transforms.RandomRotation(degrees15), # 随机旋转正负 15 度 transforms.ToTensor(), # HWC - CHW像素值归一化到 [0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet 均值 std[0.229, 0.224, 0.225]) # ImageNet 标准差 ]) # 验证集/测试集不增强只做尺寸调整和归一化 val_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里面的关键是Normalize的参数。项目用了 ImageNet 的均值和标准差而不是自己算数据集的均值和标准差。两种做法都能跑但要注意预训练权重比如 torchvision 里带权重的 ResNet默认是在 ImageNet 归一化参数下训练的如果你自己重新算均值方差加载预训练权重后分布就对不上微调效果会打折扣。如果你的网络完全从零训练那用 ImageNet 的归一化参数也是安全的不冲突。2.3 数据增强怎么做才不翻车海洋垃圾图片有个特点同类垃圾在不同光照、不同水质背景下差异很大增强策略要保守。源码里只用了水平翻转和 15 度以内的随机旋转没有用 RandomCrop也没有用色彩抖动。原因很简单垃圾识别任务里类别特征集中在物体的轮廓和纹理上过度裁剪会切掉关键区域反而让模型学到背景而非垃圾本身。from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_dataset ImageFolder(rootdataset/train, transformtrain_transforms) val_dataset ImageFolder(rootdataset/val, transformval_transforms) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(f训练集样本数: {len(train_dataset)}) print(f类别映射: {train_dataset.class_to_idx})num_workers在 Windows 上建议设成 0否则容易报BrokenPipeError。Linux 上设 4 或 8 都行。pin_memoryTrue只在 GPU 训练时有意义能减少 CPU 到 GPU 的传输时间。如果你是纯 CPU 跑这个参数设不设无所谓。3. CNN 模型搭建与参数选型卷积核、残差和优化器的取舍3.1 为什么毕业设计不用预训练也能跑网络结构设计思路很多毕业设计项目直接套用 torchvision 里预训练好的 ResNet50然后只改全连接层。这个方案的优点是收敛快、精度高但缺点是你解释不清每一层在干什么。答辩的时候老师问“你的创新点在哪”你只能说“我用了迁移学习”。这份源码给的是一条更稳妥的路从零搭建一个轻量 CNN结构可解释训练速度快在中等规模数据集上准确率够用。轻量网络的设计思路是三段式先卷积提特征再池化降维度最后全连接做分类。卷积层用 3×3 卷积核padding 设为 1保证特征图尺寸不缩小然后用 ReLU 激活引入非线性。每两个卷积层后面接一个最大池化把特征图尺寸减半。这样做的好处是网络容量适中不容易过拟合训练时间也短。3.2 模型代码拆解卷积层、池化层与全连接层的参数设置下面是源码里模型定义的核心部分我保留了注释并调整了格式import torch.nn as nn class GarbageCNN(nn.Module): def __init__(self, num_classes4): super(GarbageCNN, self).__init__() # 第一个卷积块3-64 通道 self.conv1 nn.Sequential( nn.Conv2d(3, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 224 - 112 ) # 第二个卷积块64-128 通道 self.conv2 nn.Sequential( nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 112 - 56 ) # 第三个卷积块128-256 通道 self.conv3 nn.Sequential( nn.Conv2d(256, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 56 - 28 ) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) # 不管输入多大压成 1x1 self.classifier nn.Sequential( nn.Dropout(p0.5), nn.Linear(256, 128), nn.ReLU(inplaceTrue), nn.Linear(128, num_classes) ) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x self.avgpool(x) x x.view(x.size(0), -1) # 展平 x self.classifier(x) return x注意一个细节conv2和conv3的输入通道我写的是128和256但前面承接的输出实际上是 64 和 128。这是源码里已经算好的衔接第一层卷积输出 64 通道conv2的第一层 Conv2d 接收 64代码里写成 128 是错的。实际上第二块应该是nn.Conv2d(64, 128, ...)第三块是nn.Conv2d(128, 256, ...)。我拆项目时反复核对过这是一个很容易抄错的点新手经常会照抄 shape 然后卡在维度不匹配上。inplaceTrue能省一点内存但不是必须的。3.3 损失函数与优化器交叉熵、Adam 的参数初始化多分类任务用交叉熵损失优化器用 Adam这是最稳的组合。源码里的设置如下import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1)lr1e-3是从零训练时一个比较安全的起点。如果你用预训练权重做微调学习率要降到 1e-4 甚至 1e-5否则会把预训练的特征冲掉。weight_decay1e-4是 L2 正则项能在一定程度上抑制过拟合但设太大会让 loss 下不去。StepLR每 10 个 epoch 把学习率降到原来的 0.1 倍适合训练轮数在 30 到 50 之间的场景。如果你的数据集很小建议把step_size调小到 5让学习率早点降下来。这里还有一个隐性知识点CrossEntropyLoss内部已经包含了 softmax所以模型最后一层不需要额外加nn.Softmax()。你只在推理阶段想看到每个类别的概率分布时才手动对输出做一次 softmax。训练时加了 softmax 反而会影响数值稳定性。4. 训练与评估loss 曲线、混淆矩阵和早停的实战用法4.1 训练脚本的参数配置与学习率调度训练流程是这份源码里最值得完整保留的部分。整体思路是每个 epoch 跑一遍训练集算 loss 和准确率然后跑一遍验证集记录验证 loss如果验证 loss 连续若干轮不下降就触发早停保存最优模型权重。def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy这里的loss.item()是返回当前 batch 的标量值乘上images.size(0)是把这个 batch 的 loss 还原成总和最后再除以总样本数得到平均 loss。为什么不直接loss.item()累加因为不同 batch 的样本数可能不一样直接累加会让最后一个 batch 的权重偏大。这个细节很多人不注意但会导致绘制的 loss 曲线有轻微跳动。4.2 评估指标准确率之外的 recall 和混淆矩阵准确率在类别均衡时是够用的但海洋垃圾数据集往往不均衡。比如plastic_bottle图片多fishing_net图片少模型可能把所有图片都判成塑料瓶准确率还是 70% 以上这时候混淆矩阵比准确率更能说明问题。源码里用 sklearn 直接算混淆矩阵from sklearn.metrics import confusion_matrix, classification_report import torch def evaluate(model, val_loader, device, class_names): 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) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) print(cm) print(classification_report(all_labels, all_preds, target_namesclass_names))torch.no_grad()是推理模式的开关告诉 PyTorch 不需要计算梯度能省下不少显存和计算时间。新手最容易忘的就是这个导致评估时显存比训练还高白白翻车。classification_report会输出每个类别的 precision、recall 和 f1-score这比只看准确率直观得多。4.3 训练日志与可视化怎么判断模型真的在收敛源码里的训练日志每个 epoch 打印一次 loss 和准确率格式大概是Epoch [10/30], Train Loss: 0.4823, Train Acc: 82.45%, Val Loss: 0.5321, Val Acc: 79.32%如果你发现训练 loss 不断下降但验证 loss 反而上升这基本就是过拟合的征兆。正常情况是两者同步下降并且验证准确率保持在可接受范围。源码里没有装 tensorboard而是用简单的日志文件和 matplotlib 画 loss 曲线减少依赖。我觉得这个选择对毕业设计更友好答辩时直接把 loss 曲线图放在 PPT 里比现场演示 TensorBoard 更省事。另一个容易忽视的点是模型保存策略。源码里不是每个 epoch 都存权重而是只在验证准确率创新高时保存best_acc 0.0 for epoch in range(num_epochs): train_loss, train_acc train_one_epoch(...) val_loss, val_acc evaluate(...) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(f模型已保存验证准确率: {val_acc:.4f})这样保存出来的best_model.pth就是整个训练过程中验证集表现最好的那一次而不是最后一个 epoch 的结果。很多项目最后保存的是最后一轮权重万一最后一轮已经过拟合了部署时精度就会下滑得很难看。5. 避坑训练海洋垃圾识别模型最常见的五个翻车点5.1 类别不均衡导致 loss 震荡不下现象训练集的 loss 在某个值附近来回震荡验证准确率上不去尤其是fishing_net这类少数类几乎不被预测出来。原因数据集中塑料瓶图片数量远大于渔网模型倾向于把大多数样本预测成塑料瓶因为这样整体 loss 更低。交叉熵损失在类别极度不均衡时对多数类的主导地位非常敏感。解决给CrossEntropyLoss传入weight参数让少数类的损失权重更大。常见做法是先统计每个类别的样本数然后按样本数的倒数归一化设置权重。import torch.nn as nn class_counts [1200, 300, 500, 800] # 每个类别的样本数 total sum(class_counts) weights [total / c for c in class_counts] # 样本越少权重越大 weights torch.tensor(weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightweights)注意CrossEntropyLoss的weight参数是一个 tensor要在构建时传入而且要保证和类别索引顺序一致。如果你用的是ImageFolder类别顺序是字母序这点需要先确认。5.2 过拟合验证集 loss 回升怎么早停现象训练 loss 稳定下降但验证 loss 降到某个点后开始回升验证准确率同步下滑。原因网络容量超过数据量所能约束的范围模型开始记住训练集中的噪声和细节而不是泛化特征。解决除了weight_decay和Dropout最直接的办法是早停。在验证 loss 连续 5 个 epoch 没有创新低时停止训练并回滚到之前保存的最优权重。这个策略能省去反复试跑的时间也是源码里采用的方案。5.3 图片尺寸不一致导致张量 shape 报错现象训练到中途报错提示Expected 3D tensor but got 4D或者size mismatch。原因某些图片是 RGBA 四通道或者尺寸异常小预处理阶段Resize后没有正确变成 3 通道张量。ImageFolder默认用PIL.Image打开图片RGBA 图会保留 4 个通道和预训练的 3 通道输入不匹配。解决在自定义 Dataset 里加一个转换函数统一转成 RGBfrom PIL import Image def load_rgb_image(path): img Image.open(path).convert(RGB) # RGBA / 灰度图全部转成 RGB return img然后在构建 Dataset 时用这个函数读图。这个坑在真实数据集里出现的概率很高尤其是从网上爬取或手机拍摄的图片根本不会在乎通道数。5.4 GPU 显存不足与 batch size 调整现象CUDA out of memory训练刚开始就中断。原因batch size 设太大加上输入图片是 224×224中间特征图占用显存超出显卡容量。更隐蔽的情况是num_workers和pin_memory同时开启额外的数据加载进程也占显存。解决把 batch size 从 32 降到 16 或 8同时把pin_memory关掉。如果显存仍然不够可以把224×224改成160×160特征图小一半显存占用明显下降。这个改动对精度的影响在小数据集上通常可以接受。5.5 数据集泄露增强后的图片混进验证集现象训练准确率接近 100%验证准确率也很高但实际部署测试时效果很差。原因数据集划分时没有先切分再增强而是对整个数据集做了增强导致训练集和验证集里包含同一张原始图片的不同变换版本。模型相当于提前看到了验证集的答案。解决先按文件夹切分训练集和验证集再分别应用各自的 transform。项目源码里两个 transform 是分开定义的这个设计是对的但使用者在重新组织数据时容易打破这个边界。每次划分完数据随手打印一张验证集图片检查一下有没有训练集的影子。6. 导出与单图预测把训练好的模型变成能交付的脚本训练完成后best_model.pth只是一堆权重离“能给别人用”还差一步。常见做法是写一个独立的预测脚本加载权重对单张图片做预处理、推理、后处理输出类别和置信度。项目源码里预测脚本的逻辑大概如下import torch from PIL import Image from torchvision import transforms class_names [aluminum_can, fishing_net, glass_bottle, plastic_bottle] def predict(image_path, model, device): transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) img transform(img).unsqueeze(0).to(device) # 加一个 batch 维度 model.eval() with torch.no_grad(): outputs model(img) probs torch.softmax(outputs, dim1) confidence, pred_idx torch.max(probs, 1) return class_names[pred_idx.item()], confidence.item() if __name__ __main__: model GarbageCNN(num_classes4) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() name, conf predict(test.jpg, model, torch.device(cpu)) print(f预测类别: {name}, 置信度: {conf:.4f})注意map_locationcpu这个参数在别人机器上没有 GPU 时特别重要。如果不加在只有 CPU 的环境下直接加载会在load_state_dict时报错。另外验证阶段model.eval()一定要在加载权重之后调用否则 Dropout 层的随机行为会让每次预测结果都不一样这个现象很难排查。从那以后我每次做完训练都会强制走一遍单图预测流程用三张训练集图片和一张新图分别测一下。训练集图片预测不准说明权重保存或加载有问题新图预测不准说明泛化能力不足。这套验证习惯帮我规避过至少三次部署现场翻车希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?