简介:本资源是一套面向计算机相关专业本科生的深度学习实战项目,聚焦日用品图像分类与识别任务,适用于课程设计、本科毕设及入门级AI项目实践。压缩包共11个文件,含10个Python源码(涵盖数据预处理、特征提取、CNN/LSTM模型构建、分类训练与测试全流程)及1份结构清晰的README.md说明文档,整体仅11KB,轻量易读,便于快速理解项目逻辑与代码组织方式。已有72人下载学习,适合零基础学生通过可运行代码掌握图像分类完整链路,包括MNIST、Fashion-MNIST、CIFAR-10等经典数据集适配经验,以及自定义日用品数据的迁移学习实现思路。项目代码经实测可直接运行,附带模块化函数封装与统计分析脚本,显著降低调试门槛,是理解深度学习工程落地的优质教学范例。
1. 这不是调个 pre-trained 模型就完事的“毕设流水线”:一个真正能跑通、可答辩、经得起提问的日用品图像分类项目
本科毕设里,“基于深度学习的日用品图像分类与识别”是高频选题,但大量提交物止步于 Jupyter Notebook 里几行torchvision.models.resnet18(pretrained=True)+ 一个测试图 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, mask=mask) result[mask == 0] = [255, 255, 255] # 白色背景 # 步骤5:CLAHE增强对比度(专治日用品瓶身反光导致的细节丢失) clahe = cv2.createCLAHE(clipLimit=2.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(parents=True, exist_ok=True) remove_background_and_normalize(str(img_file), str(output_file))参数说明与调试要点:
clipLimit=2.0:CLAHE 的对比度限制值。日用品瓶身反光强烈,设为 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(brightness=0.2, # 光照变化模拟不同店铺灯光 contrast=0.2, saturation=0.2, hue=0.1), transforms.RandomHorizontalFlip(p=0.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(pretrained=True)并替换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(pretrained=True) # 冻结前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(inplace=True), nn.Dropout(dropout_rate), nn.Linear(128, num_classes) ) def forward(self, x): return self.base_model(x) # 使用示例 model = CustomResNet18(num_classes=8) # 8种日用品:牙膏、牙刷、洗发水、沐浴露、纸巾、洗衣液、洗手液、剃须刀关键参数解释:
dropout_rate=0.3:日用品数据集规模小(每类常<200张),Dropout 是比 L2 正则更有效的防过拟合手段。- 冻结策略:
layer4包含高层语义特征(如“瓶身标签文字”、“包装盒图案”),而layer1~3学习的是通用边缘/纹理,冻结它们可大幅减少训练参数量(从 11M 降至约 2.5M),加速收敛且降低显存占用。
3.2 分阶段训练:先解冻 layer4,再全模型微调
一次性解冻所有层易导致预训练权重被破坏。推荐两阶段训练:
| 阶段 | 解冻层 | 学习率 | Epochs | 目标 |
|---|---|---|---|---|
| Stage 1 | layer4+fc | 1e-3 | 15 | 让高层特征适配新任务 |
| Stage 2 | 全部层 | 1e-4 | 10 | 微调底层纹理特征以提升细节判别力 |
# train_stages.py def train_stage(model, dataloaders, stage_name, lr, epochs): optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=lr) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.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(f"Stage {stage_name} | Epoch {epoch+1}/{epochs} | Val Acc: {val_acc:.4f}") scheduler.step() # Stage 1: 只训练 layer4 和 fc train_stage(model, dataloaders, "1", lr=1e-3, epochs=15) # Stage 2: 解冻全部层 for param in model.base_model.parameters(): param.requires_grad = True train_stage(model, dataloaders, "2", lr=1e-4, epochs=10)提示:Stage 2 的
lr=1e-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_names=class_names, output_dict=True) # 绘制混淆矩阵热力图 cm = confusion_matrix(all_labels, all_preds) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.title('Confusion Matrix') plt.ylabel('True Label') plt.xlabel('Predicted Label') plt.savefig('confusion_matrix.png', dpi=300, bbox_inches='tight') 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(纸巾)F1=0.78,远低于平均值 0.92。查混淆矩阵发现它常被误判为detergent(洗衣液),因为两者都常为白色塑料瓶装。这说明模型未学会区分瓶身标签文字——下一步应增加 OCR 文本特征融合,或收集更多带清晰标签的纸巾图。 - 看召回率(Recall)偏低的类别:如
razor(剃须刀)Recall=0.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', color=color) ax1.plot(train_losses, color=color, label='Train Loss') ax1.tick_params(axis='y', labelcolor=color) ax2 = ax1.twinx() # 共享x轴 color = 'tab:blue' ax2.set_ylabel('Validation Accuracy', color=color) ax2.plot(val_accuracies, color=color, label='Val Acc') ax2.tick_params(axis='y', labelcolor=color) fig.tight_layout() plt.title('Training Loss & Validation Accuracy') plt.savefig('training_curve.png', dpi=300, bbox_inches='tight') # 在训练循环中记录 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", type=str, required=True, help="输入图片路径") parser.add_argument("--model", type=str, default="best_model.pth", help="模型权重路径") parser.add_argument("--classes", type=str, default="classes.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_location='cpu') # 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, dim=1) 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(root="dataset/test") # 读取 test 目录结构 class_names = dataset.classes # 按目录字母序排列:['detergent', 'hand_soap', ...] with open("classes.json", "w") as f: json.dump(class_names, f, indent=2) 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_classes=8) model.load_state_dict(torch.load("best_model.pth")) model.eval() # 创建示例输入(必须与训练时尺寸一致) example_input = torch.randn(1, 3, 224, 224) # batch=1, 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; m=torch.jit.load('resnet18_jit.pt'); print(m(torch.randn(1,3,224,224)))"这行命令能在任何装有 PyTorch 的机器上运行,无需源码、无需环境配置——这才是毕设源码包该有的交付标准。
本文还有配套的精品资源,点击获取