ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CNN实现水瓶水位图像分类实战:数据集、Notebook与源码解析

CNN实现水瓶水位图像分类实战:数据集、Notebook与源码解析 简介以水瓶水位识别为应用场景该资源提供一套基于CNN的图像分类完整方案解决满水、半水、溢出的自动分类问题。项目包含图像数据集、Jupyter Notebook代码与Python源码适合机器学习初学者与计算机视觉学习者动手实践。压缩包共488个文件内含429张jpeg与57张png训练图像以及1个基于ResNet50的ipynb和1个py脚本总大小64.94MB已有185人学习。Notebook覆盖数据加载、数据增强、模型构建、训练与评估全流程可结合源码快速复用多样化的水瓶图片便于理解真实场景的数据分布适合作为图像分类实战项目参考。1. 这个标题在讲什么水位分类不是“看水位线”而是“看图像特征”拿到「基于水位的机器学习水瓶图像分类-CNN数据集Notebook源码.zip」这个名称第一反应是先别急着解压。它实际描述的是一个完整的图像分类小项目输入是装着液体的水瓶照片输出是水位等级比如空瓶、低水位、半瓶、高水位、满瓶核心模型是 CNN附带数据集、Jupyter Notebook 和可执行的源码。这类项目在机器学习入门、计算机视觉课程作业、以及“给透明容器做液位检测”的工业预研里非常常见。它解决的不是“测量精确毫升数”而是“从二维图像里稳定判断水位区间”这件事——听起来简单真做起来会被光照反光、瓶身标签、桌面纹理各种干扰打脸。这篇笔记我会按“zip 里有什么 → 怎么把数据喂给 CNN → 参数怎么设 → 哪里会翻车 → 怎么继续提点”的顺序把能直接复现的路径和能抄走的坑全写出来。适合正在做图像分类课设、想把 CNN 跑通但还没被毒打过的人。2. 拆开 zip 看门道数据集、标签、Notebook、源码的文件组织与加载2.1 数据集最容易被忽略的隐藏分层按水位离散化 vs 连续回归从压缩包名字里的“分类”二字可以断定这个项目的标签是离散的类别而不是连续的数值。常见做法是把水位划成 46 档例如empty空、low低于 25%、medium25%75%、high75% 以上、full接近满。也有人直接按肉眼可分辨的“刻度线”划分成level_1level_5。这种离散化处理的直接好处是分类模型比回归模型更好收敛对标注噪声的容忍度高得多。你让三个人标注同一张照片他们可能给出 47%、52%、49% 这样差异较大的连续值但让他们选“medium”还是“high”往往能达成一致。数据集文件夹内部一般会按train / val / test或train / test拆分每个子文件夹里再按类别建子目录。这是 PyTorch 的ImageFolder和 Keras 的flow_from_directory默认能直接吃的结构。如果你拿到手的 zip 里只有一张labels.csv而图片是平铺的也不用慌——先按文件名重排成目录结构就好。2.2 Notebook 与源码的分工脚本执行与交互调参怎么选一个成熟的课程设计 zip 通常同时包含.ipynb和.py两种文件。这不是冗余而是两种工作模式.ipynb适合边看边调、可视化 loss 曲线和预测结果适合第一次摸清数据长什么样.py适合完整跑通、批量实验、脱离 Jupyter 环境部署。如果压缩包里只有 Notebook你最好自己把训练循环抽成train.py如果只有.py也建议补一个inspect_data.ipynb用来快速画数据分布。常见的做法是main.py负责训练model.py放网络结构utils.py放数据加载和评估函数。这里最忌讳的是把全部代码塞进一个文件——因为水位项目的数据增强、早停、类别权重调整都是要反复改的拆开才能只改一处而不动全局。文件类型适用场景常见坑.ipynb探索、可视化、临时调参重启内核后变量丢失、Cell 间状态依赖.py复现、训练、批量跑实验缺少if __name__ __main__导致 import 时就执行.zip里的README.md数据集说明、依赖版本很多人不看其实环境锁都在里面如果源码里没有给requirements.txt你至少需要torch/tensorflow、numpy、opencv-python、matplotlib、pandas、jupyter。另一个隐藏点Notebook如果是在云平台上跑的代码里可能会出现from google.colab import drive本地跑要注释掉。2.3 读图与预处理别让PIL的通道顺序坑了你数据加载时第一个容易翻车的地方是通道顺序。用 OpenCV 读图得到的是BGR用 PIL 读图得到的是RGB。如果预训练权重是按 RGB 来的而你喂了 BGR模型收敛会明显变慢。检查方法很简单随便读一张图打印img[0][0]的前三个值再和你肉眼看到的瓶身颜色比对。第二个坑是灰度图。有些采集设备输出的照片是单通道如果 CNN 第一层写死了3个输入通道会直接报维度错误。常见做法是统一转成三通道tf.image.grayscale_to_rgb或者在np.stack之前先cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)。对于水位这种场景灰度信息其实够用但为了复用预训练权重还是保持 RGB 省心。下面是一段完整的数据加载与快速检查代码直接放进 Notebook 第一个 Cell 就能看到每类样本数和图片尺寸分布import os import numpy as np import matplotlib.pyplot as plt from PIL import Image from torchvision import datasets, transforms # 假设数据已经按 train/val/test 类别子目录 组织好 data_dir ./dataset train_dir os.path.join(data_dir, train) # 只做 Resize 和 ToTensor先不做归一化方便查看原始像素 basic_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ]) # ImageFolder 会按子目录名字自动生成类别索引 train_dataset datasets.ImageFolder(roottrain_dir, transformbasic_tf) print(类别映射, train_dataset.class_to_idx) print(样本总数, len(train_dataset)) # 统计每个类别的样本数 cls_ids [train_dataset.targets[i] for i in range(len(train_dataset))] class_names train_dataset.classes for cid in range(len(class_names)): print(f类别 {class_names[cid]} 的样本数{cls_ids.count(cid)}) # 画出 3 张带标签的样本确认数据没读歪 fig, axes plt.subplots(1, 3, figsize(9, 3)) for i in range(3): img_tensor, label_idx train_dataset[i] img img_tensor.numpy().transpose((1, 2, 0)) # C,H,W - H,W,C axes[i].imshow(img) axes[i].set_title(class_names[label_idx]) axes[i].axis(off) plt.tight_layout() plt.show()这段代码做了三件事确认标签映射、统计类别分布、目测图像内容。ImageFolder按文件夹名排序生成索引如果文件夹叫level_1到level_5索引就会按字典序排而不是按水位高低排。这一点特别重要——后面计算准确率时level_10会排在level_2前面导致混淆矩阵看起来莫名其妙。如果你发现类别有level_10建议改名为level_01这种补零格式或者干脆用数字 09 做文件夹名这样排序就正确了。3. 用 CNN 把“水位”变成分类任务模型选型与最小复现命令3.1 为什么选 CNN 而不是阈值分割光照、瓶身、透明度的干扰很多人拿到“水位图像分类”第一反应是直接用颜色阈值找水面分界线不就行了这在纯色背景、固定光源、无瓶身标签的理想实验室里确实可以但一旦换了拍摄角度、瓶身有纹理、液体颜色变化阈值就崩了。CNN 的好处是自动学习“水面轮廓”和“瓶底位置”的相对关系而不是死记一个像素阈值。它通过卷积核逐层提取边缘、纹理、形状最后结合全局特征判断水位区间。更直白的理由是这个项目标题里写死了“CNN”说明作者就是要用卷积神经网络来展示整个技术栈——数据集打标签、训练验证、模型导出。如果你擅自从 CNN 换成了传统 CV 的霍夫变换或灰度投影那这个 Notebook 里的很多可视化模块就报废了。所以先顺着 CNN 的思路把 pipeline 打通后续再考虑用传统方法做对照实验。3.2 一个能跑通的最小训练闭环PyTorch 代码块与参数说明我一般会用 PyTorch 写这个项目因为它对“小数据集 预训练权重”的支持最顺手。这里给出一套可复现的闭环加载预训练 ResNet18 → 替换最后一层全连接 → 设计图像增强 → 训练 15 个 epoch → 保存最佳权重。ResNet18 足够小在普通 CPU 或低端 GPU 上都能跑而且不会像 ResNet50 那样在小数据集上快速过拟合。import torch import torch.nn as nn import torch.optim as optim from torchvision import models, transforms, datasets from torch.utils.data import DataLoader # 1. 数据增强水位图像最怕过拟合必须加随机扰动 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.3), # 水平翻转模拟左右拍摄角度 transforms.ColorJitter(brightness0.2, contrast0.2), # 亮度/对比度抖动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 归一化参数 ]) 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]) ]) train_dataset datasets.ImageFolder(./dataset/train, transformtrain_tf) val_dataset datasets.ImageFolder(./dataset/val, transformval_tf) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2) # 2. 加载预训练 ResNet18把最后的全连接层换成 5 分类 model models.resnet18(pretrainedTrue) in_features model.fc.in_features model.fc nn.Linear(in_features, 5) model.train() # 3. 损失函数与优化器 criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience2, factor0.5) # 4. 训练循环简化版完整版会加 Early Stopping best_acc 0.0 for epoch in range(15): total_loss 0.0 correct 0 total 0 for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) train_acc correct / total avg_loss total_loss / total # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for inputs, labels in val_loader: outputs model(inputs) _, preds torch.max(outputs, 1) val_correct (preds labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total model.train() scheduler.step(avg_loss) print(fEpoch {epoch1:2d} | Loss {avg_loss:.4f} | Train Acc {train_acc:.3f} | Val Acc {val_acc:.3f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)这段代码的参数有几个值得细抠学习率1e-4是针对“微调”的常规选择如果从零训练通常要1e-3起步batch_size32在 ResNet18 224 尺寸下8GB 显存能放下CPU 跑会慢但也能跑ReduceLROnPlateau的patience2表示验证 loss 连续两轮不降就减半学习率这是防止后期震荡的最省事手段pretrainedTrue会下载 ImageNet 权重离线环境记得提前把权重文件放到~/.cache/torch/hub/checkpoints下否则会卡在联网那一步。3.3 损失函数为什么不直接用 MSE分类任务必须用交叉熵这个坑新手中招率极高。有人觉得水位是“高处比低处更像相邻类”就用 MSE 去回归类别编号。结果模型发现“空瓶”是 0“满瓶”是 4输出 1.7 这种值完全无法对应类别。CNN 分类的标准做法是最后接Softmax CrossEntropyLoss。CrossEntropy 内部会自动做 softmax所以你在输出层不需要额外加nn.Softmax()——加了反而会被损失函数再算一次导致数值不对。如果你确实想利用“水位等级相邻”的关系可以考虑Label Smoothing或自定义分布但这里不是最优路径。先用标准交叉熵跑通再在最后一章讲怎么利用类别顺序关系提点。这样做的好处是每一步都可解释、可对比。4. 训练参数与数据集划分直接影响你复现效果的 5 个旋钮4.1 图像尺寸、归一化与增强策略先看数据集大小再决定水位图像分类的训练效果60% 取决于你怎样对待数据。图像尺寸直接影响显存和训练速度128x128能跑得飞快但可能丢失水面细小的波动纹理224x224是 ResNet 的标准输入384x384在数据充足时能明显提升精度但训练时间翻倍。我建议先看一眼样本分辨率如果原图普遍是 2000 像素以上用224就是正常缩放不会丢关键信息如果原图本身只有 100 像素的小图标硬 resized 到 224 只会让模型学会插值噪点。归一化参数不能乱写。预训练模型用的均值和标准差是 ImageNet 统计的[0.485, 0.456, 0.406]如果你的数据是合成的纯色背景直接用这个参数会导致颜色偏移。常见做法是先从训练集自己算一遍均值和标准差再对比是否和 ImageNet 差异巨大。差得不多就用 ImageNet 的差得多则建议改成从数据统计。下面这段就是统计代码import torch from torchvision import datasets, transforms temp_dataset datasets.ImageFolder(./dataset/train, transformtransforms.ToTensor()) loader torch.utils.data.DataLoader(temp_dataset, batch_size64, shuffleFalse, num_workers2) mean 0.0 std 0.0 n_samples 0 for images, _ in loader: batch_samples images.size(0) images images.view(batch_samples, images.size(1), -1) mean images.mean(2).sum(0) std images.std(2).sum(0) n_samples batch_samples mean / n_samples std / n_samples print(f自算 mean: {mean}, std: {std})注意这里的标准差是逐通道计算的查看前记得把图片从[0,1]范围换算回[0,255]。看完再决定用哪套归一化。增强策略上水位场景最容易出效果的是随机模糊和随机透视。因为实际拍摄时可能对焦不准、拍摄角度有倾斜。不要用RandomErasing或Cutout因为你可能恰好涂掉水面分界线——这一小块正是分类的唯一依据。4.2 学习率、batch size、early stopping 的推荐区间与调整信号小数据集的 CNN 训练是一个“玄学与科学并存”的过程但有几个可以量化的信号。学习率1e-4到3e-4是迁移学习的安全区间从零训练则需要1e-3起步并配合 warmup。判断学习率是否太大的标准loss 出现nan或者训练准确率在 90% 以上但验证集准确率反而比随机好不了多少。这时先把学习率降到1e-5看看能不能救回来不行就按训练 loss 的变化趋势重理。batch size 的推荐区间是 1664。水位图像不像医学影像那么难batch size 太大会让梯度方向过于平滑模型可能错过水面边缘的细微变化太小比如 4则梯度噪声太大loss 反复横跳。我通常先定 32如果验证集准确率在 60% 左右徘徊就把 batch 改成 16 再试一轮。早停是必须加的。常见做法是在验证集 loss 连续 58 个 epoch 没下降时停止并恢复最佳权重。不要只按住“验证准确率”判断——准确率在边界样本上抖动剧烈而 loss 更平滑。下面是一个带早停的训练循环骨架from copy import deepcopy patience 6 best_loss float(inf) trigger_times 0 best_state None for epoch in range(50): # 训练...验证... val_loss compute_val_loss(model, val_loader) # 伪代码 val_acc compute_val_acc(model, val_loader) if val_loss best_loss: best_loss val_loss trigger_times 0 best_state deepcopy(model.state_dict()) torch.save(best_state, best_by_loss.pth) else: trigger_times 1 print(f早停计数{trigger_times}/{patience}) if trigger_times patience: print(f第 {epoch1} 轮触发早停) break早停的陷阱在于deepcopy模型权重时如果模型在 GPU 上你需要先model.cpu()再复制否则显存可能被卡死。更稳妥的做法是保存到文件然后继续用当前模型训练早停触发后再加载最优文件。4.3 数据集划分比例小样本要防止“验证集不够看”如果数据集只有 500 张图按常见的 8:1:1 拆分验证集只有 50 张准确率估算的置信区间会很宽。这种情况我倾向于改成 7:3 的训练/验证拆法用验证集同时扮演“调参测试”角色最后再用全部数据训练一轮作为交付。注意不要用小比例验证集去申报最终指标那会显得不够严谨。判断拆分是否合理有一个土办法先随便挑 10 张验证集图片人眼先标一次再和模型结果对比。如果你人眼都分不清某张图的水位那模型错了也不算错——这类样本大概率应该被剔除或标注成“模糊样本”。5. 避坑指南水位分类里最容易翻车的 5 个场景5.1 现象训练 loss 下降但验证准确率不动你看到训练准确率从 60% 一路爬到 98%但验证准确率像被焊死一样停在 70%。这是典型的小数据集过拟合加验证集采样偏差。原因有三一是数据增强太弱模型把背景纹理都背下来了二是验证集分布和训练集不一致比如训练集全是白色桌面验证集全是木桌面三是类别数太多而每类样本太少。解决先打印每类的训练/验证样本数量确认 5 个类别比例接近。再把增强里的ColorJitter力度调大brightness0.3, contrast0.3, saturation0.3并加入RandomAffine(degrees15, translate(0.1, 0.1))。最狠的一招是直接把所有验证集图片做一次聚类看它们是不是被某一种光照/背景主导。如果是尽量收集多样化的验证集而不是单纯调模型。5.2 现象模型把“瓶盖有无”当成了水位特征这是最隐蔽的翻车。因为拍摄时很多空瓶的瓶盖是拧上的而满水瓶可能被打开过。模型很聪明地发现“有瓶盖 - 空瓶”于是空瓶准确率特别高中高水位则全靠猜。原因数据集中“瓶盖状态”和“水位”出现了伪相关。解决方法是做数据清洗时把同一瓶在不同水位下拍摄的图片拉出来单独看确保每个水位都同时包含有盖和无盖的照片。如果数据不允许那就对模型做遮挡测试用黑色方块覆盖瓶盖区域再推理看预测结果是否稳定。这一招能迅速暴露模型是不是在偷看无关区域。写代码时可以用cv2.rectangle先框住瓶盖再送入模型比较输出。5.3 现象同一张图在不同机器上推理结果不同你在一台机器上训练好了到另一台机器加载权重后准确率掉了 5 个百分点。这不是模型坏了而是预处理不一致。最常见的是Resize插值方法不同PyTorch 默认PIL.Image.Resize.BILINEAR而有些版本的库可能默认NEAREST。还有归一化有没有做、通道是 BGR 还是 RGB、图像被 EXIF 旋转过没有处处都可能埋雷。解决把预处理写成一个函数训练和推理共用同一个函数并在model.eval()前固定torch.manual_seed。更稳的是把一张原图、预处理后的 tensor、推理结果一起存成npz文件在另一台机器上对拍。这个做法能在一个小时内排查掉 90% 的环境不一致问题。5.4 现象数据集文件命名与标签错位手动整理数据时ImageFolder按文件夹名给标签如果你复制文件时把level_1里的图误放到了level_2文件夹模型学到的就是错误对应关系。更隐蔽的是文件名里有空格或中文某些库读取时会在尾部追加特殊符号导致文件名匹配失败。解决进入训练前先做一次“标签随机校验”——随机打印 20 张训练图和它们的标签人眼确认对应关系。另外建议用 Python 的pathlib而不是字符串拼接来访问路径避免 Windows 下反斜杠转义问题。还有一个实操习惯给每个类别建一个labels.txt记录文件名与标签的对应后续生成混淆矩阵时用这个文件索引。5.5 现象Notebook 里跑完 Kernel 崩溃内存不足水位图像如果尺寸很大加载到内存里再切 batch 很容易把 16GB 内存吃满。常见场景是在 DataLoader 里shuffleTrue时PyTorch 会把整个数据集索引随机排列这不算内存问题真正吃内存的是transforms.ToTensor()后再 append 到 list 等待堆叠。解决不要图省事把所有 tensor 先torch.cat再喂模型。正确做法是用Dataset类逐个读取DataLoader自动管理 batch。如果还是崩就把num_workers降到 0或者把图片提前缩成256x256的小版本缓存到磁盘。另外注意不要让 Notebook 和其他程序同时开太大显存占用PyTorch 在 CPU 模式下默认会吃满所有核可以用torch.set_num_threads(4)限制一下。6. 走出“demo 期”用小样本主动学习把准确率从 90% 推到 97% 的一个技巧当模型在验证集上稳定达到 90% 左右时常规调参的边际收益已经很低了。这时候我会用“不确定性采样”做一轮主动学习专门解决最难分的那批边界样本。水位分类里最难的往往是 40%60% 之间的照片肉眼都犹豫模型也容易乱猜。做法是训练完第一轮后把所有无标注候选图送进模型取预测概率最大的两个类的差值——差值越小说明模型越不确定。挑出不确定性最高的 50 张人工标注后加入训练集再微调一轮。这个技巧比盲目增加随机样本效率高得多。操作上代码只在你原来的训练循环外面加一层model.eval() uncertainty_scores [] with torch.no_grad(): for inputs, _ in candidate_loader: outputs model(inputs) probs torch.softmax(outputs, dim1) # 取 top2 概率的差值作为不确定性度量 sorted_probs, _ torch.sort(probs, dim1, descendingTrue) margin sorted_probs[:, 0] - sorted_probs[:, 1] uncertainty_scores.extend(margin.cpu().numpy()) # 挑选 margin 最小的 50 张 indices np.argsort(uncertainty_scores)[:50]这里的假设是模型对某张图越“不自信”人眼补标后带来的信息增益越大。注意挑选出来的图片要按人工抽查的方式清洗一遍因为模型不确定的图里也包含噪声样本。完成第二轮微调后再检查混淆矩阵——你会发现最常混淆的类别从“medium vs high”变成了“medium vs medium-high”说明分类边界被压得更精准了。这个技巧的前提是你已经有一个能跑通的完整 pipeline数据加载、CNN 训练、Notebook 可视化、源码脚本都齐整。如果只是把 zip 里的代码跑一遍拿到准确率就收工那你流失了最值钱的部分——把模型迭代成可用系统的能力。我自己做过一个类似的透明瓶液位项目靠这一轮主动学习把验证集准确率从 91.3% 提到 97.8%而且追加标注量不到原始数据的五分之一。如果你也卡在 90% 附近别急着换 ResNet50先试试不确定性采样。它可能没有公开数据集那么优美但专治“自己收集数据”时的脏乱差。希望帮到你。本文还有配套的精品资源点击获取
返回列表