日用品图像分类毕设实战:数据清洗、模型微调与CLI部署

发布时间:2026/9/10 14:51:43
日用品图像分类毕设实战:数据清洗、模型微调与CLI部署 简介本资源是一套面向计算机相关专业本科生的深度学习实战项目聚焦日用品图像分类与识别任务适用于课程设计、本科毕设及入门级AI项目实践。压缩包共11个文件含10个Python源码涵盖数据预处理、特征提取、CNN/LSTM模型构建、分类训练与测试全流程及1份结构清晰的README.md说明文档整体仅11KB轻量易读便于快速理解项目逻辑与代码组织方式。已有72人下载学习适合零基础学生通过可运行代码掌握图像分类完整链路包括MNIST、Fashion-MNIST、CIFAR-10等经典数据集适配经验以及自定义日用品数据的迁移学习实现思路。项目代码经实测可直接运行附带模块化函数封装与统计分析脚本显著降低调试门槛是理解深度学习工程落地的优质教学范例。1. 这不是调个 pre-trained 模型就完事的“毕设流水线”一个真正能跑通、可答辩、经得起提问的日用品图像分类项目本科毕设里“基于深度学习的日用品图像分类与识别”是高频选题但大量提交物止步于 Jupyter Notebook 里几行torchvision.models.resnet18(pretrainedTrue) 一个测试图 predict 输出标签——模型没微调、数据没清洗、评估没指标、部署没痕迹。真正合格的毕设源码包必须体现完整闭环从原始图片采集/整理规范、数据增强策略选择依据、模型结构修改逻辑比如为什么改最后全连接层输出数、训练过程监控loss/acc 曲线是否合理、混淆矩阵分析具体错判类型是牙刷和剃须刀易混还是不同品牌洗发水瓶身相似导致误判再到最终可独立运行的 inference 脚本。本文聚焦这个 ZIP 包里最常被忽略却决定答辩成败的四个硬核环节数据集构建的工业级规范、ResNet 系列模型在日用品小样本下的轻量化改造、验证集上 F1-score 与 per-class accuracy 的双维度评估、以及脱离 notebook 的 CLI 推理接口设计。适合正在写毕设、卡在“能跑但讲不清原理”阶段的同学也适合想快速复现一个有真实业务感图像分类基线的开发者。2. 日用品图像数据集从手机随手拍到可训练数据集的三步清洗法日用品图像分类的最大陷阱不是模型选错而是数据质量失控。手机拍摄的牙膏、纸巾、洗衣液等实物图天然存在光照不均、背景杂乱、角度倾斜、包装反光等问题。直接丢进 DataLoader 训练模型学到的很可能是“白色背景”或“桌面纹理”而非“日用品本身特征”。必须建立可复现的数据预处理流水线。2.1 原始数据采集与目录结构标准化本科毕设常见错误是把所有图片塞进一个文件夹靠文件名区分类别如toothpaste_001.jpg。这无法被 PyTorchImageFolder自动解析且易因命名不一致导致漏类。正确做法是强制采用两级目录结构dataset/ ├── train/ │ ├── toothpaste/ # 牙膏含不同品牌、颜色、管状/膏状 │ │ ├── tp_brand_a_001.jpg │ │ └── tp_brand_b_002.jpg │ ├── tissue/ # 纸巾抽纸、卷纸、湿巾 │ │ ├── tissue_roll_001.jpg │ │ └── tissue_wet_002.jpg │ └── detergent/ # 洗衣液瓶装、袋装、不同容量 ├── val/ │ ├── toothpaste/ │ ├── tissue/ │ └── detergent/ └── test/ # 独立测试集答辩时现场演示用 ├── toothpaste/ ├── tissue/ └── detergent/提示train/val/test划分比例建议 7:2:1。val用于早停early stopping和超参调整test仅在最终评估时使用全程不可见于训练过程。2.2 基于 OpenCV 的批量预处理脚本解决光照与背景干扰单纯依赖torchvision.transforms中的RandomHorizontalFlip或ColorJitter无法解决日用品图片的核心问题——强反光与复杂背景。需在数据加载前进行确定性清洗。以下脚本对train/下所有子目录执行统一操作# preprocess_dataset.py import cv2 import os import numpy as np from pathlib import Path def remove_background_and_normalize(img_path: str, output_path: str): 针对日用品图片的专用预处理去背景光照归一化 img cv2.imread(img_path) if img is None: return # 步骤1转HSV空间利用饱和度(S)和明度(V)分离前景日用品通常高S/V hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) s_channel hsv[:, :, 1] v_channel hsv[:, :, 2] # 步骤2自适应阈值分割避免固定阈值对不同光照失效 s_thresh cv2.adaptiveThreshold(s_channel, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) v_thresh cv2.adaptiveThreshold(v_channel, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 步骤3合并S/V掩膜形态学闭运算填充空洞 mask cv2.bitwise_and(s_thresh, v_thresh) kernel np.ones((3,3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 步骤4用掩膜抠图背景填纯白非黑避免CNN误学黑色背景 result cv2.bitwise_and(img, img, maskmask) result[mask 0] [255, 255, 255] # 白色背景 # 步骤5CLAHE增强对比度专治日用品瓶身反光导致的细节丢失 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) yuv cv2.cvtColor(result, cv2.COLOR_BGR2YUV) yuv[:,:,0] clahe.apply(yuv[:,:,0]) result cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR) cv2.imwrite(output_path, result) # 批量处理 root_dir Path(dataset/train) for class_dir in root_dir.iterdir(): if not class_dir.is_dir(): continue for img_file in class_dir.glob(*.jpg): output_file Path(dataset_cleaned/train) / class_dir.name / img_file.name output_file.parent.mkdir(parentsTrue, exist_okTrue) remove_background_and_normalize(str(img_file), str(output_file))参数说明与调试要点clipLimit2.0CLAHE 的对比度限制值。日用品瓶身反光强烈设为 2.0 可避免过增强产生噪点若图片普遍偏暗可试 3.0。tileGridSize(8,8)CLAHE 分块大小。日用品图像分辨率通常在 512x512 左右8x8 是平衡局部对比度与全局一致性的经验值。关键逻辑用 HSV 空间而非 RGB 直接阈值是因为日用品颜色丰富蓝瓶洗衣液、红盒纸巾RGB 通道易受光照影响而 S/V 通道对色彩恒常性更鲁棒。2.3 数据增强策略为什么不用 RandomRotation日用品在货架上摆放角度固定正立、标签朝前RandomRotation会生成大量现实中不存在的倒置、侧倾样本导致模型学到错误先验。应替换为更符合物理现实的增强组合from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), # 统一分辨率避免后续crop失真 transforms.CenterCrop(224), # 日用品主体居中crop比random更合理 transforms.ColorJitter(brightness0.2, # 光照变化模拟不同店铺灯光 contrast0.2, saturation0.2, hue0.1), transforms.RandomHorizontalFlip(p0.5), # 镜像对称合理如牙膏管左右对称 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet 预训练均值 std[0.229, 0.224, 0.225]) ])注意CenterCrop替代RandomResizedCrop是关键。后者会随机缩放裁剪可能切掉日用品关键标签区域而CenterCrop保证每次取图中心符合人眼观察习惯。3. 模型改造与训练ResNet18 的轻量化微调实战直接加载resnet18(pretrainedTrue)并替换fc层是基础操作但本科毕设常忽略两个致命细节类别数动态适配和冻结策略的阶段性调整。日用品类别通常 5~15 个远少于 ImageNet 的 1000 类盲目全参数微调易过拟合。3.1 动态构建分类头支持任意类别数的模块化设计硬编码nn.Linear(512, 10)会导致换数据集时必须改代码。应封装为可配置类# models/resnet_custom.py import torch.nn as nn from torchvision.models import resnet18 class CustomResNet18(nn.Module): def __init__(self, num_classes: int, dropout_rate: float 0.3): super().__init__() self.base_model resnet18(pretrainedTrue) # 冻结前4个残差块conv1 ~ layer3只训练layer4和分类头 for param in self.base_model.parameters(): param.requires_grad False for param in self.base_model.layer4.parameters(): param.requires_grad True # 替换原fc层增加Dropout防过拟合适配小样本日用品数据 self.base_model.fc nn.Sequential( nn.Dropout(dropout_rate), nn.Linear(self.base_model.fc.in_features, 128), nn.ReLU(inplaceTrue), nn.Dropout(dropout_rate), nn.Linear(128, num_classes) ) def forward(self, x): return self.base_model(x) # 使用示例 model CustomResNet18(num_classes8) # 8种日用品牙膏、牙刷、洗发水、沐浴露、纸巾、洗衣液、洗手液、剃须刀关键参数解释dropout_rate0.3日用品数据集规模小每类常200张Dropout 是比 L2 正则更有效的防过拟合手段。冻结策略layer4包含高层语义特征如“瓶身标签文字”、“包装盒图案”而layer1~3学习的是通用边缘/纹理冻结它们可大幅减少训练参数量从 11M 降至约 2.5M加速收敛且降低显存占用。3.2 分阶段训练先解冻 layer4再全模型微调一次性解冻所有层易导致预训练权重被破坏。推荐两阶段训练阶段解冻层学习率Epochs目标Stage 1layer4fc1e-315让高层特征适配新任务Stage 2全部层1e-410微调底层纹理特征以提升细节判别力# train_stages.py def train_stage(model, dataloaders, stage_name, lr, epochs): optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lrlr) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size7, gamma0.1) for epoch in range(epochs): model.train() for inputs, labels in dataloaders[train]: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 验证 val_acc validate(model, dataloaders[val]) print(fStage {stage_name} | Epoch {epoch1}/{epochs} | Val Acc: {val_acc:.4f}) scheduler.step() # Stage 1: 只训练 layer4 和 fc train_stage(model, dataloaders, 1, lr1e-3, epochs15) # Stage 2: 解冻全部层 for param in model.base_model.parameters(): param.requires_grad True train_stage(model, dataloaders, 2, lr1e-4, epochs10)提示Stage 2 的lr1e-4必须比 Stage 1 小 10 倍否则底层权重更新幅度过大会破坏预训练特征提取能力。4. 评估不止看 Accuracy混淆矩阵与 per-class F1-score 的深度解读毕设答辩时老师必问“你这个 92% 准确率是哪几类分得准哪几类总混淆” 如果只汇报 overall accuracy等于放弃解释权。必须生成可交互的评估报告。4.1 计算 per-class metrics 的标准流程from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt def evaluate_model(model, dataloader, class_names): model.eval() all_preds [] all_labels [] with torch.no_grad(): for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 生成详细分类报告含 precision, recall, f1-score report classification_report(all_labels, all_preds, target_namesclass_names, output_dictTrue) # 绘制混淆矩阵热力图 cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.title(Confusion Matrix) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.savefig(confusion_matrix.png, dpi300, bbox_inchestight) return report # 调用 class_names [toothpaste, toothbrush, shampoo, body_wash, tissue, detergent, hand_soap, razor] report evaluate_model(model, dataloaders[test], class_names)输出解读重点答辩话术看 F1-score 最低的类别例如tissue纸巾F10.78远低于平均值 0.92。查混淆矩阵发现它常被误判为detergent洗衣液因为两者都常为白色塑料瓶装。这说明模型未学会区分瓶身标签文字——下一步应增加 OCR 文本特征融合或收集更多带清晰标签的纸巾图。看召回率Recall偏低的类别如razor剃须刀Recall0.65意味着 35% 的剃须刀被漏检。检查误判方向发现多被判为toothbrush牙刷二者形状相似。此时应强化数据增强中的RandomAffine轻微旋转/缩放或在损失函数中为razor类设置更高权重。4.2 三个必展示的评估图表图表作用答辩价值混淆矩阵热力图直观显示各类别间混淆关系证明你理解模型失败模式非盲目堆准确率per-class F1-score 柱状图量化每个类别的综合性能回应“某类识别不准”的质疑展示分析深度训练 loss/val_acc 曲线验证训练过程无过拟合/欠拟合证明你掌握模型收敛性判断非调参玄学# plot_training_curves.py def plot_training_history(train_losses, val_accuracies): fig, ax1 plt.subplots(figsize(10, 6)) color tab:red ax1.set_xlabel(Epoch) ax1.set_ylabel(Training Loss, colorcolor) ax1.plot(train_losses, colorcolor, labelTrain Loss) ax1.tick_params(axisy, labelcolorcolor) ax2 ax1.twinx() # 共享x轴 color tab:blue ax2.set_ylabel(Validation Accuracy, colorcolor) ax2.plot(val_accuracies, colorcolor, labelVal Acc) ax2.tick_params(axisy, labelcolorcolor) fig.tight_layout() plt.title(Training Loss Validation Accuracy) plt.savefig(training_curve.png, dpi300, bbox_inchestight) # 在训练循环中记录 train_losses [] val_accuracies [] for epoch in range(epochs): # ... training code ... train_losses.append(epoch_loss) val_acc validate(model, dataloader_val) val_accuracies.append(val_acc) plot_training_history(train_losses, val_accuracies)5. 从 Notebook 到可交付 CLI一个命令完成推理的工程化封装毕设演示环节打开 Jupyter Notebook 点 run 是灾难。必须提供脱离开发环境的独立推理接口体现工程能力。5.1 构建命令行工具argparse 模型加载一体化# infer.py import argparse import torch from PIL import Image from torchvision import transforms import json def main(): parser argparse.ArgumentParser(description日用品图像分类推理工具) parser.add_argument(--image, typestr, requiredTrue, help输入图片路径) parser.add_argument(--model, typestr, defaultbest_model.pth, help模型权重路径) parser.add_argument(--classes, typestr, defaultclasses.json, help类别映射文件路径) args parser.parse_args() # 加载类别映射 with open(args.classes, r) as f: class_names json.load(f) # [toothpaste, toothbrush, ...] # 加载模型 model torch.load(args.model, map_locationcpu) # cpu加载避免GPU依赖 model.eval() # 图像预处理与训练时完全一致 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]) ]) # 推理 img Image.open(args.image).convert(RGB) img_tensor transform(img).unsqueeze(0) # 添加 batch 维度 with torch.no_grad(): outputs model(img_tensor) probs torch.nn.functional.softmax(outputs, dim1) confidence, pred_idx torch.max(probs, 1) print(f预测类别: {class_names[pred_idx.item()]}) print(f置信度: {confidence.item():.4f}) if __name__ __main__: main()使用方式答辩现场直接敲# 1. 将模型权重、类别文件、测试图放入同一目录 $ ls infer.py best_model.pth classes.json demo_tissue.jpg # 2. 一行命令完成推理 $ python infer.py --image demo_tissue.jpg 预测类别: tissue 置信度: 0.98235.2 classes.json 的生成与维护规范类别文件必须与训练时ImageFolder的目录顺序严格一致否则索引错位。禁止手写 JSON用脚本自动生成# generate_classes_json.py from torchvision.datasets import ImageFolder import json dataset ImageFolder(rootdataset/test) # 读取 test 目录结构 class_names dataset.classes # 按目录字母序排列[detergent, hand_soap, ...] with open(classes.json, w) as f: json.dump(class_names, f, indent2) print(classes.json generated:, class_names)提示ImageFolder的classes属性按子目录名字典序排序非创建顺序。确保你的detergent/、hand_soap/等目录名首字母能正确排序如用01_detergent强制排序。5.3 模型导出为 TorchScript为未来部署铺路虽然毕设不要求部署但导出.pt文件是专业性的体现且可被 ONNX 或 TensorRT 进一步转换# export_model.py import torch from models.resnet_custom import CustomResNet18 # 实例化模型需指定类别数 model CustomResNet18(num_classes8) model.load_state_dict(torch.load(best_model.pth)) model.eval() # 创建示例输入必须与训练时尺寸一致 example_input torch.randn(1, 3, 224, 224) # batch1, rgb, h, w # 导出为 TorchScript traced_model torch.jit.trace(model, example_input) traced_model.save(resnet18_jit.pt) print(TorchScript model saved: resnet18_jit.pt)导出后可用以下命令验证$ python -c import torch; mtorch.jit.load(resnet18_jit.pt); print(m(torch.randn(1,3,224,224)))这行命令能在任何装有 PyTorch 的机器上运行无需源码、无需环境配置——这才是毕设源码包该有的交付标准。本文还有配套的精品资源点击获取