ARTICLE DETAIL

资讯详情

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

基于CNN的水瓶水位图像分类实战:从数据集到模型训练

基于CNN的水瓶水位图像分类实战:从数据集到模型训练 简介面向机器学习初学者与图像分类开发者这个水瓶水位识别实践包以CNN为核心配套水瓶图像数据集和可直接运行的代码目标是帮助用户快速掌握从图像输入到水位类别输出的完整分类流程。压缩包共488个文件体积约64.94MB其中429个jpeg与57个png图像构成数据集主体内含308张满水位水瓶照片覆盖多种瓶子尺寸、形状与拍摄角度适合检验模型在不同视觉条件下的识别能力另外1个Python脚本与1个基于ResNet50的IPython Notebook梳理了数据读取、预处理、模型定义、训练验证等核心步骤用户可在Jupyter中一边运行一边观察各阶段输出便于理解卷积网络在图像分类中的工作方式。当前已有185人学习此资源可用于课程实验、毕业设计或迁移学习、超参数调优等进阶实践具有较强的参考价值。1. 用 CNN 判断水瓶水位这件事比你想的更值得较真把水瓶图像分类和水位联系起来听起来是个 Demo 级问题实际做下去会发现它横跨了图像采集、标注质量、模型选型和小样本过拟合一整条链路。基于水位的机器学习水瓶图像分类本质上是训练一个 CNN 卷积神经网络让它根据照片里水瓶内液面的绝对或相对位置输出空瓶、低水位、半满、高水位、满瓶这样的离散类别。它不只是一次期末考试作业也是工业上液位检测、自动分拣、质检环节里最容易复现的入门落地点。适合谁读正在学深度学习但只跑过 MNIST 的初学者想做数据集整理和 Notebook 实验管理的工程师以及需要在边缘设备上部署轻量图像分类模型的开发者。你会发现这个任务最容易踩的坑不在模型结构而在水位线的定义、光照对瓶身反光的影响、以及数据集类别不平衡。这篇内容会把“怎么做”拆成可执行步骤从数据集构建到 CNN 训练再到 Notebook 里的实验追踪和源码组织方式全部给出可抄作业的命令和代码。就算你手头没有任何现成素材也能用手机拍几十张照片把流程跑通。2. 构建水位水瓶数据集类别定义、采集规范与预处理脚本数据集的原始质量直接决定 CNN 能学到什么。先别急着写模型花一小时把数据定义清楚后面能省下十倍调参时间。2.1 水位类别怎么定从二分类到四分类的边界条件基于水位的分类不是一个完全客观的标签任务。同一个瓶子从侧面看液面是一条线从斜上方看是椭圆从底部看完全看不到水位。所以在做数据集之前必须固定两个前提摄像头视角以及液面相对瓶身的比例。常见做法是分成四个类别empty、low、half、full。如果瓶子是透明材质的还要决定是否区分“满瓶”和“几乎满瓶”因为液面靠近瓶口时瓶口螺纹会产生反光干扰机器很难区分 90% 和 100% 的水位。我一般建议在初期只做三类空瓶、半瓶水位在瓶身中部 ±20%、满瓶水位超过瓶身 80%。三类问题能让模型快速收敛之后再细化到五类都来得及。如果任务要求连续水位值那就不是分类问题而是回归问题标题里写的是“分类”所以这里采用离散类别。类别定好后用一份 CSV 文件记录每个图像的路径和类别比用文件夹名做标签更灵活因为同一张照片可以属于多个实验组且 CSV 便于后续做数据划分和统计分析。2.2 拍摄与标注固定机位、光源和瓶子规格数据集的数量不需要很大。CNN 在图像分类上并不是天生需要十万张图才能工作对于这种结构化的瓶子图像每类 50~100 张总共 200~400 张配合数据增强就能训练出一个像样的分类器。前提是背景不要过于杂乱最好用纯色纸板或墙面做背景。采集时要记录这些参数写入数据集的 meta.csv参数推荐值 / 说明拍摄距离30~50cm让瓶子占据画面 60% 以上光源方向不要正对瓶身避免高光掩盖液面瓶子颜色透明或淡色初期不要用磨砂瓶水位标签用体积比例标注不依赖视觉猜测标注方式可以直接用文件夹分类也可以用 LabelImg 画框后再裁切。但水位分类的注意力不应该在瓶身整体而在液面位置所以如果使用目标检测框会导致 CNN 同时关注瓶盖、标签、背景分类效果反而下降。最稳的做法是先把瓶子中心区域裁剪成正方形再进入分类网络。2.3 预处理脚本裁剪、缩放和归一化的最小实现拿到原始照片后先用一个 Python 脚本统一处理。下面这段代码完成三件事读取全部图像、按类别比例拆分训练验证集、把图像缩放到128x128并保存为 NumPy 数组。使用 NumPy 数组而非直接读原图是为了在训练时减少磁盘 I/O。import os import cv2 import numpy as np from sklearn.model_selection import train_test_split def load_and_preprocess(data_root, img_size(128, 128)): images, labels [], [] class_names sorted(os.listdir(data_root)) class_to_id {name: i for i, name in enumerate(class_names)} for cls_name in class_names: cls_dir os.path.join(data_root, cls_name) for fname in os.listdir(cls_dir): fpath os.path.join(cls_dir, fname) img cv2.imread(fpath) if img is None: continue img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, img_size) img img.astype(np.float32) / 255.0 images.append(img) labels.append(class_to_id[cls_name]) X np.array(images) y np.array(labels) return X, y, class_names X, y, class_names load_and_preprocess(water_bottle_dataset/) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) print(f训练集大小: {X_train.shape}, 验证集大小: {X_val.shape}) print(f类别映射: {class_names})这段代码的关键参数是test_size0.2和stratifyy。stratify保证每个类别在训练集和验证集中的比例一致否则空瓶类图片如果多模型会倾向于把所有输出都预测为“空瓶”。图像归一化到[0,1]是为了配合 CNN 中常用的 ReLU 激活函数和交叉熵损失避免输入数值过大导致梯度爆炸。预处理阶段另一个容易遗漏的动作是删除模糊照片。水瓶是透明物体自动对焦经常落在背景上导致液面边缘模糊。可以在代码里加入拉普拉斯方差判断小于阈值的图像直接丢弃阈值在 100~200 之间需要根据照片实测这里不展开。3. CNN 模型设计为什么图像分类不用前馈神经网络而用卷积层传统的前馈神经网络Fully Connected Network在处理图像时会把每个像素当作独立特征假设相邻像素之间没有关系。但图像中的水位线是一组空间连续的边缘像素前馈网络必须逐个像素学习权重参数数量惊人且无法学习“液面是一条水平线”这样的局部模式。CNN 通过卷积核在空间上滑动用很少的参数提取局部特征这就是它成为图像分类首选的原因。3.1 针对水位任务的 CNN 结构选择水位分类的特殊性在于瓶身是竖直的液面是水平的且液面位置在不同图片中差异明显。所以网络不必做得很深ImageNet 上的几百层结构在这里是浪费。我用得最顺手的基线是一个四层卷积块加两层全连接的 Net参数量在 20 万上下单张 CPU 推理时间不到 50ms。import tensorflow as tf from tensorflow.keras import layers, models def build_water_level_cnn(input_shape(128, 128, 3), num_classes4): model models.Sequential([ layers.Conv2D(16, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.GlobalAveragePooling2D(), layers.Dropout(0.3), layers.Dense(num_classes, activationsoftmax) ]) return model cnn_model build_water_level_cnn() cnn_model.summary()paddingsame保证卷积前后尺寸不变便于串联池化层。我特意在最后一层用了GlobalAveragePooling2D而不是直接接 Flatten因为 GAP 把每个特征图变成一个标量极大降低全连接层的参数总量对防止过拟合效果显著。Dropout(0.3)是训练时随机丢弃 30% 的神经元让网络不依赖单个节点的输出这点在小数据集上至关重要。3.2 不同水位特征的映射逻辑从浅层到深层CNN 学习到的东西是有规律的。第一层卷积核通常学会边缘和颜色渐变对应瓶身轮廓和液面边界第二、三层会把边缘组合成纹理比如液面与瓶壁交界处的月牙形高光更深层则会关注全局结构比如“整个瓶身的下半部分是否透明”。因此输入图像中的水面位置需要足够居中否则网络会去学瓶子摆放角度而不是水位高低。这里有一个参数值得注意输入分辨率。128x128是速度和精度的折中。如果液面位置标注误差在 3 像素以内用128x128足够但如果要区分 85% 和 95% 这两种高水位建议用224x224并在第一层卷积使用步长 2 的降采样防止感受野过小导致液面细节丢失。3.3 分类头与损失函数的匹配四个类别用softmax categorical_crossentropy三个类别也一样。如果你把类别当成有序数据比如 empty0, low1, half2, full3会有人建议用回归或者有序回归但实测中这种简单映射在瓶子形状多样时并不稳定。直接按 unordered class 处理让 CNN 自己学类别间的“语义距离”准确率反而更高。cnn_model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losstf.keras.losses.SparseCategoricalCrossentropy(), metrics[accuracy] )SparseCategoricalCrossentropy 配合整型标签 y_train省去了 one-hot 编码的步骤。学习率选择 1e-3 是 Adam 优化器的惯用默认值但后面训练时会根据验证集表现降低到 1e-4。这一点在 Notebook 里通常会被新手忽略导致损失曲线震荡不止。4. 训练驱动的实验循环在 Notebook 里管理运行记录与调参路径写代码跑通训练只完成了 30% 的工作另外 70% 是围绕实验的记录和对比。用 Jupyter Notebook 跑 CNN 训练有一个天然优势单元格可以保留每次运行得到的准确率、损失曲线和混淆矩阵而源码.zip里的.py文件则负责打包最终模型和推理逻辑。两者分工明确Notebook 负责探索Python 脚本负责固化。4.1 训练的最小完整代码与回调参数训练过程不能只写一行model.fit()至少需要加入模型保存、早停和学习率衰减。下面的代码可以直接放进 Notebook 的一个单元格里执行from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau checkpoint ModelCheckpoint( best_water_cnn.keras, monitorval_accuracy, save_best_onlyTrue, modemax ) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) lr_scheduler ReduceLROnPlateau( monitorval_loss, factor0.5, patience4, min_lr1e-6 ) history cnn_model.fit( X_train, y_train, batch_size16, epochs50, validation_data(X_val, y_val), callbacks[checkpoint, early_stop, lr_scheduler], verbose1 )batch_size16在小数据集上是合理选择因为瓶子图像本身信息量不大没必要用大 batch 追求训练速度反而小 batch 会给梯度引入噪声帮助模型跳出局部最优。patience10表示验证集损失连续 10 轮不下降就停止训练这是防止过拟合的直接手段。ReduceLROnPlateau在验证指标停滞时把学习率减半配合早停可以让模型在接近收敛时更精细地调整参数。训练完成后绘制损失和准确率曲线是必须做的一步。不要只会看最终准确率如果训练损失持续下降但验证损失在第 8 轮开始抬头说明模型已经开始背诵训练集此时应回头检查数据增强策略和 Dropout 比例。4.2 数据增强解决水瓶数据集稀缺的关键手段对瓶子图像而言随机旋转、平移和水平翻转都是安全的增强操作因为水位分类不受瓶身左右朝向影响。但要小心随机裁剪和亮度变化。裁剪可能把瓶口截掉让模型误认为“没有瓶口”是满瓶的标志亮度变化如果范围过大会让透明瓶身的高光区失真模型转而学习环境光而不是水位线。datagen tf.keras.preprocessing.image.ImageDataGenerator( rotation_range10, width_shift_range0.1, height_shift_range0.1, horizontal_flipTrue, brightness_range(0.8, 1.2), fill_modenearest ) datagen.fit(X_train)这里rotation_range10是角度范围width_shift_range0.1和height_shift_range0.1表示水平和垂直方向最多平移 10% 的像素宽度。fill_modenearest用于填充平移后留下的空白区域如果瓶子放在纯色背景下也可以用fill_modeconstant并指定cval255填充白色。推荐先用纯色背景拍摄这样增强后的图像不会出现奇怪的黑边。4.3 评估模型混淆矩阵和每一类的 P/R/F1仅仅打印accuracy是不够的。如果full类样本比half类多模型可能牺牲少数类来提升整体准确率。用 sklearn 直接计算混淆矩阵和分类报告from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred cnn_model.predict(X_val) y_pred_labels np.argmax(y_pred, axis1) print(classification_report(y_val, y_pred_labels, target_namesclass_names)) print(confusion_matrix(y_val, y_pred_labels))在输出结果里重点关注half类别的 recall。经验上半瓶水位最容易被误判为低水位因为两者之间的边界在瓶身中部偏下时本身就不清晰。如果这类错误频繁说明数据标注时low和half的样本没有拉开距离需要重新定义“水位达到瓶身 40% 以下算 low40%~70% 算 half”这样的硬规则并重新检查标注。另外验证集是从原图中切分出来的同一个瓶子在不同光照下拍出的 5 张照片可能同时进训练集和验证集导致评估结果虚高。严格做法是以瓶子为单位划分一个瓶子的所有照片只能进一边。这个在数据采集时就要提前规划否则后续只能手动调整数据集。5. Notebook 实验记录与源码发布的组织规范拿到一份基于水位的机器学习水瓶图像分类-CNN数据集Notebook源码.zip里面通常会同时包含.ipynb文件和.py文件。前者适合逐步阅读和教学后者适合作为可执行工程。两份内容如果不一致阅读者就会陷入困惑。我在本地管理这类项目时固定使用一套目录结构和命名约定。5.1 一个可复现的目录设计water_bottle_cnn/ ├── data/ │ ├── raw_images/ # 原始照片按类别分子文件夹 │ ├── processed/ # 预处理后的 npy 数组 │ └── meta.csv # 图像路径、标签、拍摄参数 ├── notebooks/ │ ├── 01_data_explore.ipynb │ ├── 02_train_cnn.ipynb │ └── 03_evaluate.ipynb ├── src/ │ ├── preprocess.py │ ├── model.py │ └── train.py ├── models/ │ └── best_water_cnn.keras └── requirements.txtNotebook 文件按数字前缀命名明确执行顺序。src下的 Python 脚本不能直接 import 一个只有 01137 行的 Notebook所以要把预处理、模型定义、训练逻辑分别拆到三个文件中Notebook 里只负责传参和展示结果。requirements.txt固定核心库的版本至少包括tensorflow2.8、opencv-python、scikit-learn、numpy、matplotlib和jupyterlab。5.2 在 Notebook 里保留关键中间变量训练得到的历史对象history.history是一个字典包含逐轮的 loss 和 accuracy。不要只在 Notebook 里画个图就完了把它序列化保存成 JSON这样复现实验时能直接对比不同学习率的结果。import json with open(models/history_baseline.json, w) as f: json.dump(history.history, f, indent2)下次调整模型结构后加载历史 JSON 文件用一条命令画出两次实验的对比曲线import matplotlib.pyplot as plt with open(models/history_baseline.json) as f: hist_base json.load(f) with open(models/history_dropout06.json) as f: hist_drop json.load(f) plt.plot(hist_base[val_accuracy], labelbaseline) plt.plot(hist_drop[val_accuracy], labeldropout0.6) plt.legend() plt.show()这种组织方式让源码和 Notebook 成为互相补充的孪生体而不是两套独立内容。5.3 验证最终模型的单张推理打开一张新的水瓶照片用与训练完全相同的预处理步骤送入模型这一步最容易出错的是忘记缩放系数。训练时用了astype(np.float32) / 255.0推理时也要先除以 255否则输入值域是 0~255模型输出会全部变成同一个类别。def predict_water_level(image_path, model, img_size(128, 128), class_namesNone): img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, img_size) img img.astype(np.float32) / 255.0 img np.expand_dims(img, axis0) prob model.predict(img, verbose0)[0] pred_idx int(np.argmax(prob)) return class_names[pred_idx], prob[pred_idx] print(predict_water_level(test_images/bottle_001.jpg, cnn_model, class_namesclass_names))输出结果包含类别名和置信度。如果置信度低于 0.6建议在代码里加一条判断避免把低置信度预测直接当作最终结果。更好的方式是同时输出模型最后一层卷积的特征图热力图但 Grad-CAM 需要额外实现这里不再展开。对于刚接触 CNN 的读者先掌握单张推理、批量评估和实验记录这三件事已经足够让项目达到交付标准。本文还有配套的精品资源点击获取
返回列表