简介:面向深度学习初学者的AlexNet花卉分类实战项目,聚焦图像分类任务,从数据准备到模型训练与预测形成完整闭环。压缩包共2000个文件,以1995张花卉图片为主体,辅以4个Python脚本和1个JSON索引文件,整体约270.61MB。train.py负责启动训练流程,model.py实现AlexNet经典五卷积三全连接结构并应用ReLU、Dropout与数据增强技巧,predict.py支持单张或批量图片的花朵类别预测,class_indices.json则提供类别与索引的映射关系,checkpoints目录用于保存训练参数。资源支持用户按需更换自己的数据集,因此不仅能上手花卉分类,还可迁移至人脸、物体等图像识别任务,适合在动手实践中理解深度学习核心原理。目前已有121人学习下载,内容组织清晰,是入门CV实战的可靠参考。
1. 从 AlexNet 到花卉分类:一份能换数据集的深度学习起手式
图像分类入门最容易掉进两个坑:要么直接搬预训练大模型,跑个迁移学习就以为完事;要么自己设计网络,从零调到怀疑人生。AlexNet 处于中间位置——它有足够深的骨架让你理解卷积网络的工作方式,又不像后来的残差网络那样黑匣子。真正跑过一个花卉分类的项目实战就知道:这个 2012 年的骨架在今天依然是最适合做"可换数据集"的练手模型。这篇笔记从一个可替换自己数据集的 AlexNet 花卉分类项目出发,把网络结构、训练参数、换数据的坑一次性讲完。适合刚过完深度学习理论、想动手跑图像分类,又不想被框架细节淹没的从业者。
2. 网络骨架的选型逻辑:五层卷积加三层全连接意味着什么
2.1 AlexNet 的结构拆解与参数量画像
AlexNet 在架构上比 VGG 和 ResNet 要原始得多,但这恰恰让它更容易被改造成自己的分类器。标准结构是 5 个卷积层、3 个池化层、3 个全连接层,最后接 1000 类的输出层。第一层卷积核 11×11,步长 4,直接对大图下采样;第二层 5×5;后面三层都是 3×3。前三层卷积后面各带一个 MaxPool,前两个卷积层之后还有 Local Response Normalization——这个 LRN 在今天的 PyTorch 工程里基本被 BatchNorm 替代了,实践中可以直接舍弃。
它大约有 6000 万参数,但记住一个关键事实:参数主要集中在三个全连接层上,卷积层只承担了特征提取。这个分布意味着,如果换成你自己的小数据集,真正需要重点调整的只有全连接层和输出层。很多开源实现会把全连接层的神经元数从 4096 砍到 1024,花卉分类这种中低复杂度场景下反而收敛更快,准确率不会明显下降。
2.2 为什么它对中小数据集反而友好
一个反直觉的结论是:对花卉这种类内差异大、类间差异也大的数据集,AlexNet 的拟合能力正好够用,不会像 ResNet 那样轻易过拟合。花卉图像的特点是同一种花在不同光照、角度下差异很大,这对模型的泛化能力是个考验;但数据量通常在每类几百到几千张,不够喂饱深残差网络。AlexNet 没有残差连接、没有 Inception 模块这些高级设计,它的学习过程更线性,出了问题也更好定位。
此外,AlexNet 原本就是为 ImageNet 设计的,第一层 11×11 卷积对 224×224 的输入会快速压缩空间分辨率,这本身就带有一种隐式的正则效果。后续网络普遍改用更小卷积核堆叠来获得同样感受野,但要堆很深才能替代,深了在小数据集上反而容易崩。我个人的经验是:换自己的数据集时,先拿 AlexNet 建立基线准确率,再去尝试更复杂的结构对比收益,这个习惯能省掉很多无效实验。
2.3 一个 PyTorch 可运行的精简实现
下面这个结构去掉了 LRN,用 BatchNorm 稳定训练,同时把全连接层收窄到 1024,适合替换成任意类别数的数据集:
import torch.nn as nn class AlexNetFlower(nn.Module): """可换数据集的 AlexNet 变体,去 LRN,加 BN,收缩全连接层""" def __init__(self, num_classes=5): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(64, 192, kernel_size=5, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(192, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), ) self.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 6 * 6, 1024), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(1024, 512), nn.ReLU(inplace=True), nn.Linear(512, num_classes), ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x)这段代码的逻辑说明:卷积部分把输入从 224×224×3 逐层压缩到 6×6×256,然后展平喂给全连接分类器。inplace=True的 ReLU 省显存,在 8GB 显卡上跑 batch size 64 没有问题。两个 Dropout 是关键,AlexNet 原设计的精华就在这——全连接层占参数量大头,不做 Dropout 很容易过拟合。
参数说明:num_classes改成你自己的花卉类别数即可;最后三个卷积层输出通道是 384、256、256,这是老版本实现里的经典配置,小数据集不需要动;如果想进一步压缩,把两个 1024 改成 512 是性价比最高的减参手段,验证集准确率几乎不受影响。
3. 换自己的数据集:目录规范、划分脚本与类别边界陷阱
3.1 数据集目录组织与预处理建议
这份资源默认的数据组织方式是 PyTorch 标准的ImageFolder模式:根目录下每个子文件夹一个类别,子文件夹里的图片就是该类别的全部样本。好处是torchvision.datasets.ImageFolder直接能读,不需要额外写数据加载器。换成自己的数据集时,只要按同样的目录结构摆放照片即可。
我的建议是目录名用英文或拼音,避免中文路径在部分 Windows 环境下引发编码问题。之前有开发者把目录命名为"月季""菊花",在 Linux 上训练没问题,换到 Windows 机器后ImageFolder读取直接报 Unicode 错误,这个花了不少时间排查。其次是图片格式尽量统一成 jpg 或 png,Exif 方向信息会导致一部分手机竖拍照片被读成横的,后面会在避坑章节具体说明。
预处理上,训练集和验证集不要用同一套图像尺寸策略。训练集用 RandomResizedCrop 做随机裁剪,模拟不同距离拍摄的花卉;验证集用 CenterCrop 保证评估稳定。整个流程可以用下面的脚本完成从原始照片到规范数据集的划分。
3.2 一个自动划分训练集和验证集的脚本
import os import random import shutil from pathlib import Path def split_dataset(src_dir, dst_dir, train_ratio=0.8, seed=42): """按类别文件夹将图片划分为 train / val 两部分""" random.seed(seed) src_dir = Path(src_dir) dst_dir = Path(dst_dir) for class_dir in src_dir.iterdir(): if not class_dir.is_dir(): continue imgs = [p for p in class_dir.iterdir() if p.suffix.lower() in {'.jpg', '.jpeg', '.png'}] random.shuffle(imgs) split_idx = int(len(imgs) * train_ratio) for split_name, part in [('train', imgs[:split_idx]), ('val', imgs[split_idx:])]: out_dir = dst_dir / split_name / class_dir.name out_dir.mkdir(parents=True, exist_ok=True) for img in part: shutil.copy2(img, out_dir / img.name) print(f'{class_dir.name}: train={split_idx}, val={len(imgs) - split_idx}') if __name__ == '__main__': split_dataset('flower_raw', 'flower_data')这个脚本的逻辑:遍历原始目录下每个类别文件夹,打乱图片顺序后按比例切分,再把文件复制到flower_data/train/类别名与flower_data/val/类别名下。为什么用copy2而不用move——我建议保留原始照片目录,方便重新划分,也防止脚本出 bug 把原始数据搞乱。
参数说明:train_ratio=0.8表示每类 80% 进训练集,数据量偏少时建议改成 0.85 或 0.9;seed=42控制随机性,保证两次划分结果一致,这对复现实验很重要。验证集每类最好不少于 30 张,如果某类不足 40 张总图,这个脚本会给出警告级别的比例失衡,需要人工介入。
3.3 类别不均衡与相似品种的边界问题
花卉分类的天然难点不在背景干扰,而在品种间相似性。玫瑰、月季、蔷薇在视觉上高度重叠,只做分类不做细粒度识别的话,标签本身就存在主观争议。遇到这种情况,先看训练集里这些相似类别的数量分布,如果其中某类只有另一类的三分之一,模型大概率会把样本数少的类别学成一个"附属类"——这个类别的准确率会非常难看。
均衡手段优先用数据增强而不是删图:对样本少的类别施加更强的 ColorJitter 和 RandomRotation,相当于给模型提供多样本幻觉。只有增强之后依旧不收敛,再考虑从原始数据层面去合并语义上难分的类别。真实项目中,把"月季"和"玫瑰"合并成一个大类"蔷薇科"是常见做法,分类体验比强行细分好得多。
另外一个容易忽略的是拍摄尺度:手机近距离拍摄的花卉特写和公园里远景花丛,在 AlexNet 的第一层 11×11 卷积下会产生不同的响应模式。训练集里两类尺度都要覆盖,否则模型会学到"背景多的是远景、背景少的是近景"这种错误胜率特征,而不是学花的纹理。检查尺度覆盖最简单的方法是随机抽 50 张训练图,目测长宽比分布。
4. 训练流程与五处必调的参数
4.1 数据增强与归一化的搭配方式
深度学习图像分类的数据增强策略在这类项目里已经形成固定套路:RandomResizedCrop(224)、RandomHorizontalFlip、ColorJitter,加上 ImageNet 均值和标准差归一化。对花卉数据,把 ColorJitter 的 brightness 和 saturation 范围从默认的 0.2 放大到 0.3 会更有效果——自然光线下同一朵花的颜色本身就变化很大,增强越强,模型对这种变化的适应越好。
归一化不能用自己数据集算出来的均值方差替代 ImageNet 统计值,除非你从头训练不加载任何预训练权重。用 ImageNet 的mean=[0.485, 0.456, 0.406]、std=[0.229, 0.224, 0.225],再配合在 ImageNet 上预训练过的特征层,迁移学习的起点就是合理的。如果自己算均值方差,数值差异会让预训练权重的分布错位,模型需要更长时间才能校正回来。
4.2 训练主循环与检查点保存
import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0., 0, 0 for images, labels in 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) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total def validate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0., 0, 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) total_loss += loss.item() * images.size(0) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total这个训练循环的说明:optimizer.zero_grad()在每个 batch 前清零梯度,遗漏这一步会导致梯度跨 batch 累加,loss 曲线呈锯齿状且无法收敛——这是最经典的手误之一。返回值里 loss 除以了样本总数而不是 batch 数,方便在不同 batch size 下横向对比 loss 曲线。
参数说明:batch_size=64在 8GB 显存下刚好,输入若改成 128 或 256 分辨率,batch 要相应降到 32;数据加载的num_workers=4起步,Windows 系统不建议超过 8,否则可能触发内存泄漏。验证阶段一定要包torch.no_grad(),否则验证集同样会构建计算图,显存占用直接翻倍。
4.3 五处影响收敛的参数与调法
第一个是学习率。加载预训练权重后,全连接层要从随机初始化开始学,卷积层权重是现成的,所以两层的学习率应该不同。常见做法是分类器学习率 0.001,特征层 0.0001,用参数分组实现。全连接层用 Adam,卷积特征层用 SGD 加动量,两者混合在工程上完全没问题。
第二个是 batch size。花卉数据集每类几百张时,batch size 取 32 到 64 之间最稳定。超过 128 后验证集准确率会出现可观察的下降,因为每个 batch 的类别分布太均匀,梯度方向缺少多样性,等价于降低了数据增强的效果。
第三个是 Dropout 概率。前面代码里设了 0.5,这是 AlexNet 原论文的值。但如果你的数据集很小,每类只有 100 张左右,0.5 依然会欠拟合,这时候把两个 Dropout 都降到 0.3 反而更好。Dropout 概率跟数据量呈反向关系——数据越少,越要强化特征层的学习能力。
第四个是优化器。直接说结论:从零训练用 Adam;加载预训练权重后想冲高准确率,用 SGD(lr=0.001, momentum=0.9, weight_decay=5e-4)。前者是发动机会很快到 90% 准确率,但天花板比 SGD 低 1 到 2 个百分点。花类数据噪声大,weight_decay 不能超过 1e-3,否则会压制 Conv 层的特征响应。
第五个是标签处理。类别数较少时(5 类左右),标签平滑label_smoothing=0.1值得打开。花卉数据集里确实存在标错的样本,这个参数让模型不迷信 hard label,给错误标签留了回旋余地。PyTorch 的交叉熵损失直接传这个参数即可,不需要额外实现。
4.4 断点续训与最优模型保存
训练超过 30 轮时,中途断电或显存崩溃几乎必然发生。每次 epoch 结束保存model_state_dict、optimizer_state_dict、epoch、best_acc四个对象到同一个检查点文件,是深度学习工程里最基本的后悔药:
best_acc = 0.0 for epoch in range(start_epoch, epochs): train_loss, train_acc = train_one_epoch(...) val_loss, val_acc = validate(...) if val_acc > best_acc: best_acc = val_acc torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_acc': best_acc, }, 'checkpoints/flower_best.pth') print(f'epoch {epoch}: saved best, acc={val_acc:.4f}')这个保存逻辑说明:保存的粒度是 epoch 而不是 batch,训练中断后最多丢失一个 epoch 的结果。加载续训时用torch.load读取检查点,再分别load_state_dict到 model 和 optimizer,区分训练模式与推理模式的加载差异。
参数说明:best_acc的更新条件用的是验证集准确率而不是训练集,这样保存的模型一定是泛化能力最强的版本。如果想做更严格的早停,可以在连续 8 个 epoch 验证集准确率不增长时终止训练,torch.save之前调optimizer.zero_grad()防止最后一轮梯度残留被写进检查点。
5. 避坑与验证:三类翻车现场与一个模型体检技巧
5.1 三类高频报错的现象、原因与解决
先记录第一类:训练 loss 正常下降,但验证集准确率始终像一条直线。这个现象通常发生在目录名带中文或特殊字符时,ImageFolder内部把类别名当作索引键,标签排序混乱导致训练集和验证集的类别对应不一致。解决方法是先打印dataset.class_to_idx,再与验证集对比,确保两边映射一致。从那以后我每次新数据集都要走一遍这个打印检查。
第二类是 Loss 在某个 epoch 后突然变成 NaN。最常见原因是学习率过大,模型在梯度方向跳跃过大后数值溢出。我一般会在训练循环里临时加一条判断:if not torch.isfinite(loss): break,然后检查是哪个 batch 触发的问题。如果 loss 在起步几个 batch 就 NaN,而你有weight_decay,把 learning rate 调低一个数量级再试,通常直接解决。
第三类是验证集准确率很高,但拿手机拍的真实花卉照片预测一塌糊涂。这类翻车往往出在训练集的数据口径上——网上爬的图大多是抠好图的展示图,背景干净、花占画面很大,而真实场景是花嵌在树叶、泥土和杂物之间。解决方法是训练集和验证集混合不同拍摄场景,每类加入至少 20% 的手机实拍图,并配合 RandomResizedCrop 做尺度扰动。
5.2 过拟合与欠拟合的判别方法
训练集准确率 99%、验证集 82%,是典型的过拟合。先看 Dropout 是否被调低过,再把 weight_decay 从 5e-4 提到 1e-3,验证集往往能上浮 2 到 3 个点。训练集和验证集双双卡在 85% 附近不动,则是欠拟合,优先把学习率调到 0.01 试一轮,不要加数据量——加数据只会拉长训练时间。
另一种隐蔽情况是训练集准确率一直高位振荡而验证集波动大,这多与 batch size 相关。上图里 batch size 从 64 改成 128 之后,两个指标都会稳定得多,准确率也可能小幅上调。判断的标准很简单:如果样本总数在 500 到 3000 之间,64 是一个不容易出错的 batch 大小。
5.3 模型体检:用混淆矩阵和几张错图做一次会诊
训练结束后,在验证集上生成混淆矩阵,比任何曲线都直接。错图我先看混淆矩阵的主对角线外聚集在哪两类之间,再抽取 5 到 10 张错误样本打印出来。比如玫瑰被识别成月季占了错误的一半,说明这两类花的图像特征在模型眼里高度重合。此时再回到 3.2 节的均衡手段处理,或者考虑合并语义类别。
from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in val_loader: outputs = model(images.to(device)) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) print(cm) # 行: 真实标签, 列: 预测标签这段输出的逻辑:混淆矩阵对角线越集中模型越可用。如果某一类几乎完全被另一类吞并,就要回到数据集层面而不是继续调参。参数说明:all_preds和all_labels的收集必须在torch.no_grad()内完成,否则会消耗大量显存;混淆矩阵之外的错误图片打印,用torchvision.utils.save_image拼接保存,比逐个写入省时间。
从那以后我再也没有直接拿 val_acc 作为结束训练的判据——先看混淆矩阵,再翻错图,才决定是收工还是继续调。一个 5 类的花卉分类项目,走到这一步基本已经完成,替换成自己的数据集也只是重复第三章的目录规范和第四章的脚本而已。如果你拿到的资源里带了训练好的权重,那先从验证脚本开始跑通,再逐步替换成自己的数据,这条路比从零开始训练要少走一半弯路。希望帮到你。
本文还有配套的精品资源,点击获取