简介:这套资源面向花卉识别与图像分类实践场景,将16种花卉、共32000张224×224彩色图片的数据集与基于PyTorch搭建的训练源码整合在一起,适合正在学习深度学习图像分类、需要真实数据集进行模型训练与效果验证的开发者。压缩包内共110个文件,以Python源码、txt说明文件为主,并包含启动脚本、数据集文件及少量预编译模块,整体大小584.72MB,结构完整可直接开展实验。训练源码汇集了23种主流图片分类模型,训练时可根据设备与任务需求灵活选择不同网络结构,便于对比准确率与收敛速度,对理解迁移学习、模型选型及调参方法很有帮助。数据集按批次发布,本批涵盖一年蓬、蒲公英、油菜花、诸葛菜等16种花卉,若需更多类别可另行下载其他批次。目前已有1165人学习下载,适合作为花卉识别入门训练、课程设计或算法验证的实用资源。
1. 花卉识别:一个看似简单、实则最考验数据功底的 CV 入门项目
花卉识别是计算机视觉里最典型的细粒度图像分类任务。和猫狗分类不同,花卉之间的差异往往只在花瓣形状、颜色分布、叶片纹理这些局部细节上,同一科属的不同品种在深度学习模型眼里可能只差几个像素。这个标题里的“花卉图片集(01)”点出了最核心的痛点:数据不是拿来就能用的,你需要自己动手把原始图片整理成训练集、验证集、测试集,再配上标签文件,模型才能开工。
很多初学者在这个项目上翻车,不是因为模型选得不好,而是数据预处理和训练参数设置出了问题——比如标签和文件名对不上、类别不平衡、训练集和验证集有重复图片、图像尺寸没有统一。本文从数据集构建、训练源码解读、参数调优到推理部署走一遍完整流程,让新手能跟着复现出可用的花卉分类模型,让熟手能直接拿走一套可靠的数据处理和训练模板。
2. 花卉图片集(01)的整理与标注:决定模型上限的第一道工序
2.1 数据集的目录结构设计:训练、验证、测试三分离
在动手写任何训练代码之前,第一步是把原始图片集整理成标准的分类数据集目录。PyTorch 的torchvision.datasets.ImageFolder能直接识别按类别分文件夹的数据集,这也是最省事、最不容易出错的方案。
flowers_dataset/ ├── train/ │ ├── rose/ │ │ ├── rose_001.jpg │ │ ├── rose_002.jpg │ │ └── ... │ ├── daisy/ │ ├── sunflower/ │ └── ... ├── val/ │ ├── rose/ │ ├── daisy/ │ └── ... └── test/ ├── rose/ ├── daisy/ └── ...注意:train、val、test 三个目录下的类别子文件夹名称必须完全一致,否则
ImageFolder生成标签时会出现类别索引错位。
常见的做法是先把所有图片集中在all_images/下,按类别建好子目录,然后用脚本按比例随机分割。我一般用 7:2:1 划分 train:val:test,这个比例在中小规模数据集上既能保证训练样本充足,又能让验证集和测试集统计意义足够。
2.2 批量重命名与格式统一:写一个可复用的预处理脚本
从标题“图片集(01)”推断,这多半是整理过的第一批发图,文件名可能是相机原始编号,也可能是下载时的乱序编号。直接用原始文件名训练没有问题,但统一重命名后更方便排查问题——尤其在标签错乱时,能一眼看出某张图应该属于哪个类别。下面这个脚本可以一键完成重命名、格式转换和尺寸检查。
import os import shutil from PIL import Image def normalize_images(src_root, dst_root, target_size=(224, 224)): """ 将 src_root 下的图片统一重命名、转成 RGB、缩放到 target_size 后存入 dst_root 目录结构: src_root/类别名/图片文件 -> dst_root/类别名/类别名_编号.jpg """ for cls_name in os.listdir(src_root): cls_src = os.path.join(src_root, cls_name) if not os.path.isdir(cls_src): continue cls_dst = os.path.join(dst_root, cls_name) os.makedirs(cls_dst, exist_ok=True) valid_ext = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} img_files = [f for f in os.listdir(cls_src) if os.path.splitext(f)[1].lower() in valid_ext] img_files.sort() # 保证编号顺序稳定 for idx, fname in enumerate(img_files): src_path = os.path.join(cls_src, fname) try: with Image.open(src_path) as img: img = img.convert('RGB') img = img.resize(target_size, Image.BILINEAR) new_name = f"{cls_name}_{idx:04d}.jpg" img.save(os.path.join(cls_dst, new_name), 'JPEG', quality=95) except Exception as e: print(f"[跳过] {src_path}: {e}") if __name__ == '__main__': normalize_images('raw_images', 'processed_images')这段代码里有三个关键设计。第一个是用img.convert('RGB')强制转成三通道,这能滤掉灰度图、RGBA 图(带透明通道的 PNG)导致的训练报错“通道数不匹配”。第二个是统一缩放到 224×224,这是 ResNet、MobileNet 这类分类网络的默认输入尺寸,提前缩放可以避免训练时每个 batch 都要做 resize,能省不少 CPU 时间。第三个是用try-except跳过损坏图片,而不是直接中断整个脚本——实际数据集里总有几张截断下载或编码异常的图,让脚本停下来等你手动处理最浪费时间。
2.3 类别标签映射与样本量统计:训练前必须核对的清单
整理完图片后,写一个统计脚本,输出每类图片数量、总样本数、类别到索引的映射,并检查是否有类别图片数量过少(比如少于 50 张)。这一步看似不起眼,但能提前暴露“某类只有 3 张图”这种让训练直接崩掉的问题。
import os from collections import Counter def inspect_dataset(root): """ 统计 root 下每个类别的图片数量,打印类别索引映射 root 的结构: root/类别名/图片文件 """ class_names = sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) class_to_idx = {name: idx for idx, name in enumerate(class_names)} print(f"共 {len(class_names)} 个类别") print("类别索引映射:", class_to_idx) counts = Counter() for cls_name in class_names: cls_path = os.path.join(root, cls_name) counts[cls_name] = len([f for f in os.listdir(cls_path) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]) for cls_name, cnt in counts.items(): flag = " <-- 注意:样本过少" if cnt < 50 else "" print(f"{cls_name}: {cnt} 张{flag}") inspect_dataset('processed_images/train')提示:如果某个类别的样本量确实很少,优先考虑数据增强(翻转、裁剪、色彩抖动)而不是直接删除该类别。
3. 花卉识别训练源码:从数据加载到模型训练的最小可运行框架
3.1 数据加载与增强策略:torchvision 的 transforms 怎么配置才不玄学
训练源码的核心并不在模型定义部分,而在DataLoader和transforms的配置上。数据增强直接决定模型能不能泛化到真实场景里的花卉照片——大晴天、阴天、手机拍的、单反拍的,光照和背景差异远大于花卉本身的品种差异。我常用的增强策略如下。
from torchvision import transforms # 训练集:随机增强,让模型见过更多“变体” train_transforms = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集/测试集:只做确定性变换,保证结果可复现 eval_transforms = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])这里有个关键区别:训练集的RandomResizedCrop会随机裁剪不同区域再缩放到 224,这等于免费生成了大量局部特写样本,对花卉识别特别有用——因为很多品种的判别特征就在花蕊或花瓣边缘;而验证集必须用固定的Resize + CenterCrop,否则每次评估结果都会因为随机裁剪而抖动,你没法判断模型是真的变好了还是运气好。
Normalize用的 mean 和 std 是 ImageNet 数据集的统计值,这不是拍脑袋定的,而是因为后面要加载在 ImageNet 上预训练过的模型权重,输入分布必须和预训练时保持一致。如果你从头训练不加载预训练权重,理论上可以自己算数据集的 mean 和 std,但效果通常不会比 ImageNet 统计值更好。
3.2 模型选型:为什么说 ResNet18 是花卉识别性价比最高的起点
花卉识别属于细粒度分类,理论上需要能捕捉细微差异的模型,但实际工程里,数据量往往才是瓶颈。如果你的数据集只有几千张图,直接上 ResNet50 或 EfficientNet-B4 大概率过拟合——训练集准确率 99%,验证集只有 70%。我通常用 ResNet18 作为第一个基线模型,理由有三条:结构简单容易调试、参数量只有 1100 万左右不容易过拟合、在 ImageNet 上的预训练权重随处可得。
import torch import torch.nn as nn from torchvision import models def create_model(num_classes, pretrained=True): """ 创建 ResNet18 分类模型,替换最后一层全连接 num_classes: 花卉类别总数 pretrained: True 表示加载 ImageNet 预训练权重(强烈推荐) """ model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) in_features = model.fc.in_features # 替换全连接层:原输出 1000 类,改为花卉类别数 model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model model = create_model(num_classes=10) print(model)替换最后一层全连接时加了一个Dropout(0.3),这是一个容易被忽略但很实用的细节。迁移学习中,预训练模型的前面几层已经学会了通用的边缘、纹理、形状特征,我们只训练最后一层分类头;但因为训练数据量小,最后的线性层很容易过拟合,在fc前加 Dropout 等于给分类头加了一层正则化。如果后续发现验证集准确率明显低于训练集,可以把这个 dropout 概率从 0.3 调到 0.5,或者加一层隐层,但不要一开始就加大——会拖慢收敛速度。
3.3 训练循环与关键超参:学习率、batch size、 epoch 怎么配
训练脚本是整个源码的主干。下面这段代码可以作为直接复用的模板,它实现了标准的训练循环:前向传播、计算损失、反向传播、更新权重、每个 epoch 结束做一次验证。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in dataloader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() epoch_loss = running_loss / total epoch_acc = correct / total return epoch_loss, epoch_acc # 参数配置 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') batch_size = 32 learning_rate = 1e-4 num_epochs = 30 num_classes = 10 # 数据集加载(假设已按第 2 节整理好目录结构) train_dataset = ImageFolder('processed_images/train', transform=train_transforms) val_dataset = ImageFolder('processed_images/val', transform=eval_transforms) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True) # 模型、损失函数、优化器 model = create_model(num_classes=num_classes).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=learning_rate) # 只训练最后一层(前几层冻结),前期收敛更快 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True for epoch in range(num_epochs): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) print(f"Epoch {epoch+1}/{num_epochs} | Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f}")这段代码里最值得琢磨的是“冻结前几层,只训练 fc 层”的策略。迁移学习的标准做法是:先用小学习率(1e-4 左右)只训练新加的分类头几个 epoch,等损失降下来后,再解冻全部层、用更小的学习率(1e-5)微调整个网络。上面这段代码只实现了第一阶段,因为对大多数中小型花卉数据集来说,只训练分类头已经能到 90% 以上的准确率,全量微调的收益有限,但训练时间会翻好几倍。
另外注意num_workers=4的作用:数据加载和增强是 CPU 操作,如果设成 0,GPU 每处理完一个 batch 都要干等 CPU 准备下一个 batch,训练速度会明显变慢。4 到 8 是常见配置,超过 CPU 核数反而会因线程切换导致性能下降。
3.4 完整训练流程:从冻结训练到全量微调的两阶段法
把 3.3 的代码扩成完整的两阶段训练,这是我在多个花卉数据集上验证过最稳的路径。第一阶段只训练分类头,第二阶段解冻全部层用更低学习率微调。
# 第一阶段:只训练 fc 层 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4) for epoch in range(10): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) print(f"[阶段1] Epoch {epoch+1} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f}") # 第二阶段:解冻全部层,低学习率微调 for param in model.parameters(): param.requires_grad = True optimizer = optim.Adam(model.parameters(), lr=1e-5) for epoch in range(20): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) print(f"[阶段2] Epoch {epoch+1} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f}")注意:第二阶段学习率从 1e-4 降到 1e-5 是必须的。预训练权重已经接近最优解,学习率太大会直接破坏学好的特征,验证集准确率反而会下降。
4. 花卉识别训练避坑:数据集和训练中的 5 个高频踩坑记录
4.1 坑一:训练集随机划分导致同一张图出现在训练集和验证集
现象:验证集准确率高达 99%,但把新拍的花卉照片放进测试集,准确率掉到 70% 以下。
原因:用random.shuffle划分数据时,如果原始图片集中同一个品种的多张照片是连续拍摄的(背景、角度几乎一样),随机划分会把相似度极高的图片同时分到训练集和验证集,模型等于“开卷考试”。
解决:划分前先按图片的文件名或者拍摄时间聚类,确保同一个文件夹、同一批拍摄的照片整体进训练集或整体进验证集。更规范的做法是直接用sklearn.model_selection.StratifiedShuffleSplit按类别做分层划分,并固定随机种子:
from sklearn.model_selection import train_test_split import numpy as np # 假设 file_list[i] 属于 class_list[i] X = np.array(file_list) y = np.array(class_list) X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 )stratify参数保证训练集和验证集中每个类别的比例和原始数据集一致,避免某个类别在验证集中一张都没有。random_state=42固定随机种子保证每次划分结果一致,方便复现实验。
4.2 坑二:图像尺寸不一致导致训练中断
现象:训练到一半报RuntimeError: The size of tensor a (224) must match the size of tensor b (250)。
原因:数据集中混入了尺寸异常的图片,或者 transforms 没有正确处理。比如CenterCrop(224)要求输入图片短边必须大于等于 224,否则会报错。
解决:在预处理阶段统一做Resize(256)后再CenterCrop(224),不要直接对原始尺寸做CenterCrop。同时加一层保护逻辑:
transform = transforms.Compose([ transforms.Resize((256, 256)), # 强制统一尺寸,避免短边不足 transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(...) ])4.3 坑三:类别不平衡导致模型忽略小样本类别
现象:少数类别(比如某种罕见兰花)的召回率接近 0,模型几乎把所有图片都预测为样本量最大的类别。
原因:CrossEntropyLoss默认对每个类别一视同仁,样本多的类别在 loss 中占比更大,模型自然偏向学大类别。
解决:在CrossEntropyLoss中传入类别权重,让少数类别的错误被放大。权重的计算方式是类别样本数的倒数或总样本数 / 类别数 / 该类样本数。
from sklearn.utils.class_weight import compute_class_weight import numpy as np class_weights = compute_class_weight( class_weight='balanced', classes=np.unique(all_labels), y=all_labels ) class_weights = torch.tensor(class_weights, dtype=torch.float32).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)加了类别权重后,训练初期的 loss 值看起来会偏高,这是正常的——模型在被迫关注小类别。如果之后验证集整体准确率下降但小类别召回率上升,说明类别均衡起了作用,此时可以对大类别做欠采样或对小类别做过采样,把整体准确率和小类别召回率平衡到可接受范围。
4.4 坑四:加载预训练权重时类别数不匹配
现象:RuntimeError: Error(s) in loading state_dict for ResNet: size mismatch for fc.weight: copying a param with shape torch.Size([1000, 512]) from checkpoint, the shape in current model is torch.Size([5, 512])。
原因:torchvision.models.resnet18(pretrained=True)默认输出 1000 类的权重,替换fc层后结构变了,直接load_state_dict必然报错。出现这个报错恰恰说明你的修改生效了——权重加载时发现最后全连接层维度对不上。
解决:如果用的是models.resnet18(weights=...)方式创建模型,替换fc层之后权重会自动跳过不匹配层,不会报错。如果手动写了load_state_dict,需要加strict=False参数:
state_dict = torch.load('resnet18_pretrained.pth') model.load_state_dict(state_dict, strict=False) # 跳过维度不匹配的 fc 层4.5 坑五:训练 loss 不下降但准确率乱跳
现象:前几个 epoch 的 loss 稳定在 2.3 左右不动,准确率在 10%-20% 之间乱跳。
原因:学习率过高导致优化器在损失曲面来回震荡,或者没有冻结预训练层、直接用大学习率微调整个网络。
解决:先把学习率降到 1e-4 以下,确认 loss 开始单调下降后,再逐步调高。如果仍然不降,检查数据加载器是否正常——打印几个 batch 的图片和标签,确认图片没有被翻转成上下颠倒或者标签和图片错位。
5. 花卉识别模型的验证与推理:从 PyTorch 模型到实际可用的分类器
5.1 编写推理脚本:单张图片分类的完整代码
训练完成后,把模型保存下来,写一个推理脚本用于实际预测。这里有一个工程陷阱:torch.save(model.state_dict())只保存权重参数,不保存模型结构,加载时必须先创建模型再载入权重。而torch.save(model)整体保存则可能因为类定义路径变化导致反序列化失败。我的建议是统一用state_dict方案。
import torch from PIL import Image from torchvision import transforms def load_model(model_path, num_classes, device): model = create_model(num_classes=num_classes) state_dict = torch.load(model_path, map_location=device) model.load_state_dict(state_dict) model.to(device) model.eval() return model def predict_image(model, image_path, class_names, device): transform = transforms.Compose([ transforms.Resize(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(image_path).convert('RGB') input_tensor = transform(img).unsqueeze(0) # 加 batch 维度 input_tensor = input_tensor.to(device) with torch.no_grad(): outputs = model(input_tensor) probabilities = torch.softmax(outputs, dim=1) top_prob, top_class = torch.topk(probabilities, 3) results = [] for i in range(3): idx = top_class[0][i].item() prob = top_prob[0][i].item() results.append((class_names[idx], prob)) return results # 使用示例 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') class_names = ['daisy', 'rose', 'sunflower', ...] results = predict_image(model, 'test_images/unknown_flower.jpg', class_names, device) for name, prob in results: print(f"{name}: {prob:.4f}")几个细节值得注意。torch.no_grad()在推理时必须加,否则 PyTorch 会为每个中间变量构建计算图,内存占用成倍增加而且没有任何用处。torch.topk返回概率最高的前三个类别,这在花卉识别场景比只返回最高分更实用——因为相似品种之间概率差距可能只有几个百分点,返回 Top-3 能帮你判断模型是“确定”还是“犹豫”。
5.2 分类结果评估:准确率、混淆矩阵与单类指标
训练结束后的评估不能只看整体准确率。对花卉识别这种细粒度任务,混淆矩阵能直接告诉你模型在哪些品种之间容易混淆——比如“月季”和“玫瑰”被反复搞混,说明它们的特征过于相似,需要考虑增加数据或换更强的模型。我习惯用 sklearn 生成混淆矩阵并可视化。
import numpy as np from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt def evaluate_model(model, dataloader, class_names, device): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for inputs, labels in dataloader: inputs = inputs.to(device) outputs = model(inputs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) # 混淆矩阵 cm = confusion_matrix(all_labels, all_preds) print("混淆矩阵:\n", cm) # 每个类别的精确率、召回率、F1 report = classification_report(all_labels, all_preds, target_names=class_names) print(report)如果发现混淆矩阵中某两个类别频繁互认,可以尝试两个方向:第一,增加这两个类别的训练样本,特别是不同角度、不同光照下的图片;第二,把模型从 ResNet18 升级到 ResNet50,更强的特征提取能力有可能捕捉到更细微的差异。
5.3 模型导出:从 PyTorch 到 TorchScript 或 ONNX 的部署转换
训练和验证都通过后,如果要把模型集成到移动端或服务端,需要把 PyTorch 模型转换成 TorchScript 或 ONNX 格式。这里以 ONNX 导出为例,因为它跨平台兼容性最好,能在 TensorRT、OpenVINO 等推理框架中直接使用。
model.eval() onnx_path = 'flower_classifier.onnx' dummy_input = torch.randn(1, 3, 224, 224).to(device) # 导出 ONNX torch.onnx.export( model, dummy_input, onnx_path, export_params=True, opset_version=11, do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} ) print(f"ONNX 模型已导出到 {onnx_path}")提示:
dynamic_axes允许推理时动态变化 batch 大小,这在部署服务时非常有用。如果固定 batch 为 1,可以去掉这个参数,推理性能会稍微提升。
6. 提升花卉识别精度的三个进阶技巧:从“能跑”到“能用”
当你的基础模型已经能达到 85%-90% 的验证集准确率时,接下来的三个技巧能帮你再往上提几个百分点。这些不是理论推演,而是我实际调参时反复验证有效的操作。
第一个技巧是学习率衰减策略。上面第 3 节的训练代码用了固定学习率,但实际训练中,随着 epoch 增加,固定学习率会在接近最优解时来回震荡,导致损失无法收敛到更小值。用 PyTorch 内置的学习率调度器能自动降低学习率:
from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler = ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=3, verbose=True) for epoch in range(num_epochs): train_loss, train_acc = train_one_epoch(...) val_loss, val_acc = evaluate(...) scheduler.step(val_acc) # 以验证集准确率为监控指标patience=3表示连续 3 个 epoch 验证集准确率没有提升时,学习率减半。这种策略比你手动盯着跑到第多少个 epoch 去改学习率要可靠得多。
第二个技巧是数据增强加 MixUp。MixUp 是简单有效的正则化方法:把两张训练图片按比例混合,标签也按同样比例混合。对花卉识别来说,这能迫使模型关注更鲁棒的特征,而不是死记某张图的背景纹理。
def mixup_data(x, y, alpha=0.2): """MixUp 增强:按 Beta 分布采样混合系数""" lam = np.random.beta(alpha, alpha) batch_size = x.size()[0] index = torch.randperm(batch_size).to(x.device) mixed_x = lam * x + (1 - lam) * x[index] mixed_y = lam * y + (1 - lam) * y[index] return mixed_x, mixed_y注意 MixUp 的alpha不宜太大,0.2 左右比较合适。值太大会让混合后的图片“四不像”,模型反而学不到有效特征。
第三个技巧是 Test Time Augmentation(TTA),也就是推理时做多次增强取平均。对单张图片分别做水平翻转、中心裁剪、缩放后再预测,把多次预测结果取平均。这个方法能稳定提升准确率约 1-2 个百分点,尤其当测试图片的角度和训练集差异较大时效果明显。代价是推理时间翻三到五倍,离线评估时值得用,线上实时推理建议谨慎取舍。
最后说一个我自己的习惯:每次训练实验,我都会把模型权重、训练配置(学习率、batch size、epoch 数)、验证集准确率三样东西记在一个表格里。这么做的好处是,当你尝试了十几次超参调整后,翻开表格一眼就能看出哪个配置最稳定,而不是靠记忆“上次好像用的 1e-4 还是 5e-5”。花卉识别这个项目最大的魅力就是,数据、代码、参数全都自己可控,每一次改进都能看到明确的量化反馈。希望这篇笔记能帮你在自己的数据集上少走几步弯路。
本文还有配套的精品资源,点击获取