资讯动态

ResNet18动物图像分类实战:从数据处理到Flask部署

发布时间:2026/9/13 17:16:28 来源:尧图企业网站定制
简介基于ResNet与Python的动物图像分类项目面向深度学习初学者和图像分类开发者解决从数据准备、模型训练到在线预测的完整流程问题。压缩包共26个文件大小约41.74MB包含8个Python脚本覆盖数据集生成、模型训练、预测推理、Flask Web界面、桌面窗口程序等1个最佳模型权重文件resnet18_e_best.pth以及11张PNG图片、HTML前端模板、训练日志等目录结构清晰便于对照学习。目前已有118人学习使用。通过该资源使用者可获得一套可运行的动物分类系统源码核心基于ResNet18实现配有训练好的模型权重、测试图片和可视化截图适合课程设计、毕业设计或深度学习入门实战。项目涉及PyTorch、数据增强、Flask部署等关键知识点结合代码与演示素材能帮助读者快速复现实验并迁移到其他图像分类任务中。1. 拿到这个动物分类系统先别急着跑 train.py解压出“基于resnet和python的动物图像分类系统”后我最初以为这又是一个把预训练模型包装成Web服务的玩具工程。翻完 train.py 和 myflask.py 才发现项目并没有停在“调库加载 resnet18”这一步而是把数据整理、均值统计、训练迭代、权重保存、Flask 推理全部串了起来甚至还在 logs 目录里留下了完整的 TensorBoard events 文件。这种从零跑通一个 ResNet 图像分类闭环的做法最值得拆解的地方不是网络结构本身而是数据、训练、部署这三个环节在真实小项目里是怎么衔接的。对于已经会用 Python、想完整搭建动物图像分类系统或类似迁移学习工程的开发者这份代码提供了很好的参照系。2. ResNet18 的预处理与数据集构建generate_dataset 与 calc_mean 的职责2.1 用 ImageFolder 目录结构把散图整理成可训练样本项目里同时出现了 assets、images、generate_dataset.py 这几个文件。assets 存放的是最原始的动物图片images 很可能是划分后待用数据。PyTorch 的torchvision.datasets.ImageFolder要求数据集必须组织成“根目录/类别名/图片文件”的层级generate_dataset.py 就是完成这件事的。常见做法是把数据按train/类别名和val/类别名拆开这样训练和验证时都能直接交给 DataLoader不用手动维护标签索引。# generate_dataset.py 的核心逻辑将 assets 中每个子目录里的图片按比例划分 import os import shutil from pathlib import Path def split_dataset(src_dir: str, dst_dir: str, val_ratio: float 0.2): src Path(src_dir) for class_dir in src.iterdir(): if not class_dir.is_dir(): continue images list(class_dir.glob(*.jpg)) list(class_dir.glob(*.png)) val_count int(len(images) * val_ratio) for idx, image in enumerate(images): split_name val if idx val_count else train target_dir Path(dst_dir) / split_name / class_dir.name target_dir.mkdir(parentsTrue, exist_okTrue) shutil.copy(image, target_dir / image.name)执行后会得到images/train/dog/xxx.jpg、images/val/dog/xxx.jpg这样的结构。我一般会把 val_ratio 设成 0.15 到 0.2每个类别如果少于 30 张则需要考虑用数据增强去凑样本量。这里按序号前 20% 作为验证集会遇到一个问题如果同一次爬虫产出的图片在文件名上连续验证集可能偏向某一时段的光照条件。更稳妥的做法是先用random.shuffle(images)打乱再切分。2.2 数据增强让 ResNet18 在小数据上不快速过拟合项目里 asset 图片数量有限直接训练很容易让模型记住每张图的背景和姿态。utils.py 里一般会封装一个get_transform把训练时的增强和验证时的标准化分开。基于 ResNet18 输入尺寸 224x224常见训练增强组合如下from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.RandomRotation(degrees10), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])验证集不要加RandomRotation和ColorJitter否则验证准确率会受随机性干扰。Resize 到统一尺寸是最低要求ResNet18 最后的全局平均池化需要固定输入张量的空间维度如果不固定尺寸forward 时会直接报尺寸不匹配错误。增强操作常用参数说明Resize(224, 224)保持 ResNet18 输入尺寸一致RandomHorizontalFlipp0.5适合左右对称的动物类别ColorJitterbrightness0.2, contrast0.2提高对不同光照的鲁棒性RandomRotationdegrees10旋转过大会破坏动物形态语义2.3 calc_mean.py 为什么值得单独跑一遍很多迁移学习项目会直接使用 ImageNet 的 mean[0.485, 0.456, 0.406] 和 std[0.229, 0.224, 0.225]。但动物图片场景里如果原始数据整体偏暗、偏绿或者包含大量笼舍背景ImageNet 统计值会让标准化后的输入分布发生偏移。calc_mean.py 做的事情就是扫描训练集统计每张图片转成 Tensor 后的通道均值和标准差再传回Normalize。# calc_mean.py 中的统计逻辑 import torch def compute_mean_std(data_loader): channel_sum torch.zeros(3) channel_sq_sum torch.zeros(3) total_pixels 0 for images, _ in data_loader: # images 已经是 ToTensor 之后的 0~1 范围 batch_size images.size(0) channel_sum images.sum(dim[0, 2, 3]) channel_sq_sum (images ** 2).sum(dim[0, 2, 3]) total_pixels batch_size * images.size(2) * images.size(3) mean channel_sum / total_pixels std torch.sqrt(channel_sq_sum / total_pixels - mean ** 2) return mean, std这里的dim[0,2,3]表示对 batch、高度、宽度三个维度求和留下通道维度。计算得到的是 0~1 区间内的均值和标准差如果之前用transforms.ToTensor()做过归一化那这两个值可以直接填入Normalize。如果数据集类别差异很大建议每个类别单独计算一次统计值再按样本量加权平均避免某个类别样本过多而主导全局统计。3. train.py 逐行拆解优化器、检查点与 ResNet 收敛策略3.1 加载 torchvision 里的 ResNet18 并替换全连接层train.py 里最核心的动作是models.resnet18(pretrainedTrue)。这个预训练模型是在 ImageNet 1000 类上训练过的动物类别对 ImageNet 来说不陌生所以复用权重可以大幅缩短收敛过程。但输出层必须替换成项目自己的类别数否则模型的输出维度仍然是 1000。import torch.nn as nn import torchvision.models as models num_classes 7 # 项目里的动物类别数量 model models.resnet18(pretrainedTrue) # 替换最后一层全连接 model.fc nn.Linear(model.fc.in_features, num_classes)这里有一个容易被忽略的细节model.fc.in_features在 ResNet18 中是 512ResNet50 则是 2048。如果手动写死 512 而换了模型结构会在加载 state_dict 时出现尺寸不匹配。建议永远使用model.fc.in_features获取输入维度。3.2 优化器、损失函数与超参数表训练脚本里优化器的选择对 ResNet 微调影响很大。SGD 配合 momentum 在预训练权重上通常比 Adam 更容易收敛到更平滑的局部最优也更容易和ReduceLROnPlateau这类学习率调度器配合。损失函数使用CrossEntropyLoss它内部已经做了 softmax 和 log 操作不需要在模型之后再接 softmax。超参数典型值选择理由batch_size16动物图像数据量小batch 过大容易形成稳定噪声下降方向lr0.001预训练权重迁移时不应再用过大的学习率momentum0.9SGD 加速收敛减少震荡weight_decay5e-4防止全连接层记住训练集噪声epochs20~30预训练模型通常不需要百轮训练lr_schedulerStepLR(step_size8, gamma0.5)后期学习率降低有利于稳定 loss3.3 训练循环与 best 权重保存项目里出现的resnet18_e_best.pth就是按照验证准确率保存下来的最优权重。训练循环代码通常长这样def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss 0.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) return total_loss / len(train_loader.dataset) def evaluate(model, val_loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return correct / total best_acc 0.0 for epoch in range(1, 21): train_loss train_one_epoch(model, train_loader, criterion, optimizer, device) val_acc evaluate(model, val_loader, device) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), resnet18_e_best.pth)torch.save(model.state_dict(), ...)保存的只是权重字典不包含模型结构。加载时必须在 predict.py 里重新构建一个结构一样的 ResNet18再调用load_state_dict。如果保存时用了torch.save(model, ...)那就保存了完整模型但这种方式在跨版本 PyTorch 时更容易出兼容问题。3.4 训练时别忽略 device 与随机种子我见过不少项目在 CPU 上训练正常搬到 GPU 后结果不一致最大原因是验证阶段没有使用torch.no_grad()。评估循环里如果带着梯度计算 val loss不仅浪费显存还会轻微影响 BN 层状态。另外训练前固定随机种子能保证每次跑出的数据顺序一致方便调试。import random import os import torch def set_seed(seed42): random.seed(seed) os.environ[PYTHONHASHSEED] str(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)set_seed 只在初始化模型和 DataLoader 之前调用才有效。PYTHONHASHSEED会影响 Python 字典和集合的哈希顺序若不设置即使 DataLoader 的 shuffleTrue 后使用固定 random seed某些数据加载进程仍可能产生不同排序。4. 从 pth 到 Web 服务predict.py 与 myflask.py 的调用链4.1 predict.py 单张图片的完整预处理predict.py 的作用是把一张普通图片变成 ResNet18 能接受的张量。它需要和训练时的val_transform保持完全一致。如果训练时对图片做了 Resize((224,224))推理时却只把图片短边缩放那么进入网络前张量的空间尺寸不是 224x224会直接触发RuntimeError: size mismatch。另一个常见坑是没有处理 PNG 的 RGBA 通道导致ToTensor()后变成 4 通道输入。from PIL import Image from torchvision import transforms def preprocess_image(img_path): 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]) ]) image Image.open(img_path).convert(RGB) tensor transform(image).unsqueeze(0) return tensorconvert(RGB)会把 RGBA 和灰度图统一转成 RGB 三通道这是最容易遗忘但影响最直接的一步。unsqueeze(0)的作用是把 3D 张量变成形状为 (1,3,224,224) 的 4D 张量因为 PyTorch 的 forward 默认第一个维度是 batch。4.2 把模型推理包装成一个 Flask 接口myflask.py 里用 Flask 暴露了一个 HTTP 接口。前端templates/index.html提供文件上传入口后端把接收到的图片存成tmp_up.jpg再调用 predict 函数返回 JSON。整体结构是from flask import Flask, request, jsonify, render_template app Flask(__name__) model None device torch.device(cuda if torch.cuda.is_available() else cpu) app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict_api(): file request.files.get(file) if file is None: return jsonify({error: no file uploaded}), 400 file.save(tmp_up.jpg) label, prob predict_image(tmp_up.jpg, model, device) return jsonify({class: label, probability: prob})这里必须注意模型加载时机。如果每次请求都执行一次torch.load在高并发场景下会产生大量 IO 和显存分配。更好的做法是在 Flask 应用启动前先加载resnet18_e_best.pth到全局变量model请求进来只做 forward。对于单用户本地演示全局加载一次就够了。4.3 启动服务并用命令验证接口在项目根目录执行python myflask.py默认监听 5000 端口。浏览器打开http://127.0.0.1:5000上传一张动物图片页面会显示识别类别和置信度。如果想用命令行快速验证接口是否正常curl -X POST -F file./images/test_dog.jpg http://127.0.0.1:5000/predict返回结果类似{class: dog, probability: 0.976}如果返回KeyError: file说明请求字段名不是file需要检查 myflask.py 里request.files的取值名称。如果probability一直是 1.0多半是训练集泄露或者测试图片和训练集来自同一来源不能说明模型泛化好。5. 用 TensorBoard 读 events 文件验证 ResNet 训练过程不是运气5.1 启动 TensorBoard 查看 logs 目录项目 logs 目录下有一个events.out.tfevents.1700214215...文件这是 PyTorch 的SummaryWriter在训练时写入的。直接执行tensorboard --logdirlogs --port6006打开http://localhost:6006后SCALARS 页面能直接看到 train_loss 和 val_acc 的变化曲线。不要只看最终准确率重点观察 val_acc 是否在中间某个 epoch 达到峰值后开始下降。如果 loss 持续下降但 val_acc 不再上升说明模型已经过拟合此时resnet18_e_best.pth保存的往往是过拟合前的位置。5.2 用 Python 直接解析 tfevent 文件不启动 Web UI 时可以直接读取 events 文件里的指标数据from tensorboard.backend.event_processing.event_accumulator import EventAccumulator ea EventAccumulator(logs) ea.Reload() print(ea.scalars.Keys()) acc_events ea.Scalars(val_acc) for event in acc_events: print(fstep{event.step}, value{event.value:.4f})EventAccumulator会扫描 logs 目录下所有 tfevent 文件Scalars(val_acc)返回按 step 排序的事件列表。这个解析方法比打开 TensorBoard 更适合做自动化测试比如在训练结束后自动找到 val_acc 最大时的 step再与保存的权重路径对应。如果你的日志键名是acc而不是val_acc需要先用ea.scalars.Keys()查看实际键名。5.3 用日志判断训练策略是否需要调整TensorBoard 里如果看到验证准确率曲线在 20 轮后仍然波动优先尝试降低学习率而不是增加 epoch。我从这个项目里学到的一个实用技巧是计算 val_acc 与 train_acc 的差值若差值从第 10 轮开始持续变大就把lr_scheduler的 step_size 缩短比如从 8 改成 5同时把 weight_decay 从 5e-4 调到 1e-3。另外如果events.out.tfevents...文件无法被 TensorBoard 识别检查 logs 目录路径是否有中文名改成英文路径就能解决。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价