1. 项目概述从零开始的Kaggle花卉分类挑战如果你对数据科学和机器学习感兴趣但面对海量的理论教程和复杂的代码库感到无从下手那么Kaggle的入门级比赛就是你最好的“新手村”。今天我想分享的是我参加Kaggle平台上一个名为“Petals to the Metal”的花卉图像分类比赛的完整实验记录。这个比赛的目标非常直观给你一万多张来自104种不同花卉的图片训练一个模型让它能准确识别出任意一张新图片中的花卉种类。为什么选择这个比赛作为入门首先图像分类是计算机视觉的基石任务理解了这个后续的目标检测、图像分割等任务就有了基础。其次“Petals to the Metal”被官方标记为“入门级”这意味着它的数据集干净、任务定义清晰、评价标准明确没有太多数据清洗和特征工程的“脏活累活”能让初学者把精力集中在核心的模型构建和训练流程上。最后Kaggle平台提供了免费的GPU算力通过Notebook和丰富的公开代码Kernel让你可以站在巨人的肩膀上快速起步这对于个人学习者或计算资源有限的朋友来说简直是福音。我记录下整个过程不是为了展示一个多么惊艳的分数事实上我的排名很普通而是想还原一个真实的新手从注册账号、理解赛题、跑通第一个baseline基线模型到一步步优化、踩坑、最终提交结果的完整心路历程。你会发现很多障碍并非来自高深的算法而是来自环境配置、数据路径、版本兼容这些“琐事”。我希望这份记录能帮你绕过这些坑把力气花在真正该花的地方。2. 赛题理解与数据初探2.1 赛题核心多类别图像分类“Petals to the Metal”本质上是一个经典的多类别图像分类问题。具体来说输入一张RGB彩色花卉图片。输出该图片所属的花卉类别标签共有104个类别。评价指标分类准确率。即模型预测正确的样本数占总样本数的比例。这是最直观的指标对于类别分布相对均衡的数据集这个比赛的数据集基本均衡非常有效。理解评价指标至关重要因为它直接指导了我们模型优化的方向。我们的所有努力无论是调整模型结构、修改损失函数还是做数据增强最终目标都是为了提高在“测试集”上的分类准确率。Kaggle平台会将参赛者提交的、对隐藏测试集的预测结果进行评分并排名。2.2 数据集结构与观察在Kaggle比赛中第一步永远是仔细阅读比赛说明Overview和数据描述Data。下载数据后我通常会先花时间进行探索性数据分析虽然这里是图像数据但目录结构同样重要。比赛数据通常包含以下几个部分训练集包含图片文件和对应的标签文件通常是train.csv。train.csv里有两列image_id图片文件名不含后缀和label花卉类别编号如0, 1, 2...。测试集只包含图片文件没有标签。我们的任务就是为这些图片生成预测标签并提交。提交样例一个展示了提交文件格式的sample_submission.csv文件。我首先用Python的Pandas和OS库快速浏览了一下数据import pandas as pd import os # 加载训练标签 train_df pd.read_csv(/kaggle/input/tpu-getting-started/train.csv) print(f训练集样本数: {len(train_df)}) print(f花卉类别数: {train_df[label].nunique()}) print(train_df.head()) # 查看图片目录 train_path /kaggle/input/tpu-getting-started/train_images print(f训练图片数量: {len(os.listdir(train_path))}) # 查看某个类别的图片示例 sample_class train_df[label].iloc[0] sample_images train_df[train_df[label]sample_class][image_id].tolist()[:3] print(f类别 {sample_class} 的示例图片: {sample_images})通过这段简单的代码我确认了数据量一万多张训练图、类别数104并检查了文件是否能正常对应。接下来我会随机可视化几张图片观察其特点尺寸不一图片的宽度和高度各不相同这在输入神经网络前需要统一处理如Resize。主体居中大部分花卉图片主体都位于中央背景相对干净这对模型学习比较友好。光照和角度多样图片是在不同光照条件和拍摄角度下采集的这要求模型必须具备一定的泛化能力也提示我们可以使用数据增强来模拟这些变化。注意在Kaggle Notebook中数据路径通常是固定的格式为/kaggle/input/比赛名称/。直接使用这个路径即可无需自己上传数据。这是Kaggle平台的一大便利之处。3. 环境搭建与工具链选择3.1 Kaggle Notebook零配置的云端利器对于初学者我强烈推荐直接使用Kaggle自带的Notebook环境。它开箱即用无需自己安装CUDA、cuDNN等令人头疼的驱动也省去了租用云服务器的成本和配置麻烦。你只需要点击“New Notebook”一个预装了TensorFlow、PyTorch、Scikit-learn等主流数据科学库的Jupyter环境就准备好了。更重要的是Kaggle Notebook每周提供约30小时的免费GPU配额通常是Tesla P100或T4。对于训练图像分类模型来说GPU是必需品能极大缩短实验周期。开启GPU的方法很简单在Notebook的设置中将“Accelerator”选项从“None”改为“GPU”即可。当然Notebook也有局限比如运行时间限制、网络隔离等。但对于“Petals to the Metal”这个规模的数据集和模型免费GPU配额完全够用是性价比最高的选择。3.2 深度学习框架PyTorch vs. TensorFlow两个主流框架都可以完成这个任务。我选择PyTorch原因如下动态图更直观PyTorch的 eager execution 模式让调试像写普通Python代码一样方便每一步操作的结果都可以立即打印查看对新手非常友好。API设计简洁torch.nn、torch.optim、torch.utils.data这几个模块分工明确构建训练循环的逻辑清晰。社区活跃PyTorch在学术研究和竞赛中越来越流行很多最新的模型实现和技巧都会优先发布PyTorch版本。当然TensorFlow尤其是Keras API同样优秀代码可能更简洁。选择哪个更多是个人偏好。本实验记录将以PyTorch为例。3.3 核心工具库清单除了PyTorch我们还需要一些帮手Pandas NumPy数据处理和数值计算的基础。Matplotlib Seaborn用于数据可视化和绘制训练曲线。OpenCV 或 Pillow图像读取和预处理。Pillow的接口更Pythonic我更喜欢用。TorchvisionPyTorch的视觉工具包这是重中之重。它提供了常用的预训练模型如ResNet, EfficientNet。标准的数据集转换工具如裁剪、翻转、归一化。便捷的数据加载器DataLoader。在Kaggle Notebook中这些库通常都已预装。你可以通过!pip list命令查看。如果需要额外安装使用!pip install package_name即可非常方便。4. 基线模型构建站在巨人的肩膀上4.1 为什么使用预训练模型从头开始训练一个深度卷积神经网络来识别104类花卉需要巨大的数据量和计算资源而且容易过拟合。我们采用迁移学习策略使用在ImageNet包含1000个类别1400万张图片上预训练好的模型作为起点。ImageNet预训练模型已经学会了识别通用视觉特征如边缘、纹理、形状。花卉识别任务与ImageNet任务高度相关我们可以复用这些底层特征只替换并重新训练模型的最后一层分类头使其适应我们的104分类任务。这能让我们用较小的数据集和较短的训练时间获得一个性能相当不错的模型。4.2 模型选择与初始化Torchvision提供了许多预训练模型。对于入门比赛我推荐从ResNet34或EfficientNet-B0开始。ResNet34经典、稳定、速度快是一个可靠的基准。EfficientNet-B0在精度和速度的平衡上更优是现代竞赛中的常客。我选择了EfficientNet-B0。以下是初始化模型的代码import torch import torch.nn as nn from torchvision import models # 检查GPU是否可用 device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) # 加载预训练的EfficientNet-B0并获取其输入特征维度 model models.efficientnet_b0(pretrainedTrue) # 冻结模型的所有参数在初始阶段只训练分类头 for param in model.parameters(): param.requires_grad False # 替换分类器最后一层 # EfficientNet-B0的原始分类器是 model.classifier它是一个Sequential模块最后是一个Linear层。 # 我们需要修改这个Linear层使其输出104维我们的类别数。 num_ftrs model.classifier[1].in_features # 获取原全连接层的输入特征数 model.classifier[1] nn.Linear(num_ftrs, 104) # 替换为新的全连接层 # 将新的分类器层参数设置为需要训练 for param in model.classifier.parameters(): param.requires_grad True # 将模型移动到GPU model model.to(device)这里的关键操作是冻结requires_grad False和替换分类头。冻结预训练层的参数可以防止在初始训练阶段破坏已经学到的好的特征。我们只训练新添加的、随机初始化的全连接层。4.3 数据预处理与加载管道数据如何喂给模型同样重要。我们需要一个高效的数据加载和预处理管道。这里用到torchvision.transforms和torch.utils.data.DataLoader。from torchvision import transforms from torch.utils.data import Dataset, DataLoader from PIL import Image import pandas as pd # 1. 定义数据集类 class FlowerDataset(Dataset): def __init__(self, csv_file, img_dir, transformNone): self.data_frame pd.read_csv(csv_file) self.img_dir img_dir self.transform transform def __len__(self): return len(self.data_frame) def __getitem__(self, idx): img_name os.path.join(self.img_dir, self.data_frame.iloc[idx, 0] .jpg) image Image.open(img_name).convert(RGB) # 确保是三通道 label self.data_frame.iloc[idx, 1] if self.transform: image self.transform(image) return image, label # 2. 定义数据变换预处理和数据增强 # 训练集变换增强 归一化 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪并缩放到224x224 transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.RandomRotation(15), # 随机旋转 transforms.ToTensor(), # 转换为Tensor并归一化到[0,1] transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # ImageNet的均值和标准差 ]) # 验证集变换只做归一化不做增强 val_transform transforms.Compose([ transforms.Resize(256), # 先缩放到稍大尺寸 transforms.CenterCrop(224), # 再从中心裁剪 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 3. 创建数据集和数据加载器 train_dataset FlowerDataset(csv_file/kaggle/input/tpu-getting-started/train.csv, img_dir/kaggle/input/tpu-getting-started/train_images, transformtrain_transform) # 划分训练集和验证集例如 90% 训练10% 验证 train_size int(0.9 * len(train_dataset)) val_size len(train_dataset) - train_size train_subset, val_subset torch.utils.data.random_split(train_dataset, [train_size, val_size]) # 注意验证集也需要应用val_transform但random_split会保留原dataset的transform。 # 我们需要手动为验证子集指定新的transform。 val_subset.dataset.transform val_transform train_loader DataLoader(train_subset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_subset, batch_size32, shuffleFalse, num_workers2)关键点解析数据增强只在训练时使用RandomResizedCrop,RandomHorizontalFlip等。它通过对训练图片进行随机变换来增加数据多样性是防止过拟合、提升模型泛化能力的廉价且有效的方法。归一化参数[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]是ImageNet数据集的均值和标准差。因为我们的模型是在ImageNet上预训练的输入数据保持相同的分布有利于模型稳定。Batch Size根据GPU内存调整。32是一个常用的起始值。太大可能导致内存溢出太小可能训练不稳定。num_workers用于数据加载的子进程数可以加快数据读取速度。在Kaggle Notebook中设置为2或4通常比较安全。5. 训练循环与模型调优实战5.1 定义损失函数与优化器对于多分类问题我们使用交叉熵损失。优化器选择Adam它自适应调整学习率通常比传统的SGD收敛更快对新手更友好。import torch.optim as optim criterion nn.CrossEntropyLoss() # 只优化分类器的参数 optimizer optim.Adam(model.classifier.parameters(), lr0.001) # 学习率调度器在训练过程中动态降低学习率有助于模型后期精细调整 scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1)5.2 编写训练与验证函数一个标准的训练循环包括前向传播、计算损失、反向传播、参数更新。同时我们需要一个单独的验证循环来监控模型在未见过的数据上的表现防止过拟合。def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() # 切换到训练模式启用Dropout等 running_loss 0.0 correct 0 total 0 for images, labels in dataloader: images, labels images.to(device), labels.to(device) # 清零梯度 optimizer.zero_grad() # 前向传播 outputs model(images) loss criterion(outputs, labels) # 反向传播和优化 loss.backward() optimizer.step() # 统计 running_loss loss.item() * images.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 def validate(model, dataloader, criterion, device): model.eval() # 切换到评估模式关闭Dropout等 running_loss 0.0 correct 0 total 0 with torch.no_grad(): # 关闭梯度计算节省内存和计算 for images, labels in dataloader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.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_acc5.3 执行训练并监控现在我们可以开始训练了。通常先进行几轮“微调”即只训练分类头。num_epochs 10 train_losses, train_accs [], [] val_losses, val_accs [], [] for epoch in range(num_epochs): print(fEpoch {epoch1}/{num_epochs}) print(- * 10) # 训练阶段 train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) train_losses.append(train_loss) train_accs.append(train_acc) print(fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f}) # 验证阶段 val_loss, val_acc validate(model, val_loader, criterion, device) val_losses.append(val_loss) val_accs.append(val_acc) print(fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f}) # 更新学习率 scheduler.step() print()训练几轮后如果验证准确率趋于稳定我们可以解冻部分或全部预训练层用更小的学习率进行全模型微调以期获得更好的性能。# 解冻所有层进行全模型微调 for param in model.parameters(): param.requires_grad True # 使用更小的学习率避免破坏已有的特征 optimizer optim.Adam(model.parameters(), lr1e-5) scheduler optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1) # 继续训练5-10个epoch实操心得一定要绘制训练曲线用Matplotlib画出每个epoch的训练/验证损失和准确率。这是诊断模型状态是否过拟合、欠拟合最直观的工具。如果训练准确率远高于验证准确率就是过拟合的典型信号需要加强数据增强、添加Dropout或提前停止。6. 模型集成与预测提交6.1 测试时增强与模型集成为了获得更鲁棒、更准确的预测在最终提交前我们可以使用一些技巧测试时增强对一张测试图片进行多种变换如水平翻转、多尺度裁剪将多个结果进行平均作为最终预测。这能有效提升模型稳定性。模型集成训练多个不同的模型例如使用不同的预训练网络ResNet50、EfficientNet-B3或不同的随机种子然后将它们的预测概率进行平均或投票。这是竞赛中提升分数的“大杀器”。由于是入门实验我采用了简单的TTAdef predict_with_tta(model, image_path, transform, device, tta_times5): model.eval() image Image.open(image_path).convert(RGB) all_outputs [] # 定义TTA变换列表这里以随机裁剪为例 tta_transforms [] for _ in range(tta_times): tta_transforms.append(transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])) with torch.no_grad(): for tta_transform in tta_transforms: input_tensor tta_transform(image).unsqueeze(0).to(device) # 增加batch维度 output model(input_tensor) all_outputs.append(output) # 对多次预测结果取平均 avg_output torch.mean(torch.stack(all_outputs), dim0) probabilities torch.nn.functional.softmax(avg_output, dim1) _, predicted_class torch.max(probabilities, 1) return predicted_class.item(), probabilities.cpu().numpy()6.2 生成提交文件最后一步读取测试集所有图片用训练好的模型进行预测并生成符合Kaggle要求的提交文件。import os test_dir /kaggle/input/tpu-getting-started/test_images submission {image_id: [], label: []} model.eval() simple_transform val_transform # 使用和验证集相同的简单变换 with torch.no_grad(): for img_name in os.listdir(test_dir): img_path os.path.join(test_dir, img_name) # 使用TTA或简单预测 # pred_label, _ predict_with_tta(model, img_path, simple_transform, device) # 或者使用简单预测更快 image Image.open(img_path).convert(RGB) image_tensor simple_transform(image).unsqueeze(0).to(device) output model(image_tensor) _, pred_label torch.max(output, 1) submission[image_id].append(img_name.split(.)[0]) # 去掉.jpg后缀 submission[label].append(pred_label.item()) # 创建DataFrame并保存为CSV submission_df pd.DataFrame(submission) submission_df.to_csv(submission.csv, indexFalse) print(submission_df.head())生成的submission.csv文件包含两列image_id和label。直接在Kaggle比赛页面的“Submit Predictions”处上传这个文件系统就会自动评分并给出排名。7. 常见问题与避坑指南在实验过程中我遇到了不少典型问题这里总结一下希望能帮你节省时间。7.1 内存溢出CUDA out of memory这是最常遇到的错误。原因Batch Size太大或模型太大超出了GPU显存。解决减小batch_size如从32降到16。使用更小的模型如从EfficientNet-B3换到B0。在代码开头添加torch.cuda.empty_cache()清理缓存。使用梯度累积每计算N个小batch的梯度才更新一次权重相当于用时间换空间。7.2 验证准确率远低于训练准确率过拟合原因模型过于复杂记住了训练数据的噪声而非一般规律。解决加强数据增强增加更多的随机变换如颜色抖动、随机擦除等。添加正则化在模型中添加Dropout层或在优化器中设置权重衰减。早停监控验证集损失当其在连续几个epoch不再下降时停止训练。简化模型使用更小的预训练模型。7.3 训练损失不下降或准确率无变化原因学习率设置不当或模型参数被冻结。解决检查学习率。太大可能导致震荡太小可能导致收敛缓慢。尝试1e-3, 1e-4等不同值。检查requires_grad。确保你想要训练的那些层的参数确实被设置为True。检查数据流。打印一个batch的数据和标签看是否正确加载。7.4 Kaggle Notebook 常见问题无法连接互联网Kaggle Notebook默认无法访问外网这意味着!pip install某些不在预装列表里的包可能会失败。解决方案是使用Kaggle Datasets上传所需的whl文件或寻找预装好的替代包。Session 超时Notebook有9小时或12小时的运行时间限制。对于长时训练务必定期保存中间模型checkpoint可以使用torch.save(model.state_dict(), ‘checkpoint.pth’)。数据集路径错误务必使用Kaggle规定的输入路径格式/kaggle/input/competition-name/。在“Data”标签页下可以查看准确的路径。7.5 提升分数的进阶思路当你的基线模型跑通后可以尝试以下方法冲击更高排名使用更大的预训练模型如EfficientNet-B4, B5或最新的ConvNeXt。更复杂的数据增强使用albumentations库它提供了更丰富、更高效的增强操作。交叉验证将训练集分成5折训练5个模型然后对它们的预测结果进行集成这比单模型更稳健。伪标签用训练好的模型对测试集进行预测将高置信度的预测结果作为伪标签加入训练集进行第二轮训练。学习率预热与余弦退火使用更先进的学习率调度策略如线性预热后接余弦退火有助于模型收敛到更优的局部最小值。8. 实验总结与个人体会回顾整个“Petals to the Metal”实验从看到题目的一头雾水到成功提交第一份预测最大的收获不是那个具体的分数而是走通了一个完整的机器学习项目Pipeline。这个过程清晰地分为几个阶段问题定义、数据准备、模型构建、训练调优、预测提交。每一个阶段都有其关键任务和常见陷阱。对我个人而言最深的一点体会是在初期工程实现能力比算法理论更重要。知道ResNet的原理固然好但能熟练地用PyTorch加载它、修改它、训练它才是让想法落地的第一步。Kaggle这样的平台极大地降低了工程门槛让你能聚焦于模型和算法本身。另一个重要的心得是迭代和记录。不要指望第一个模型就能取得好成绩。我的流程通常是快速建立一个最简单的基线比如用ResNet18不做任何调优- 确保它能跑通得到第一个分数 - 然后以此为基准一次只做一个改动比如换模型、加数据增强、调学习率并记录下这个改动带来的分数变化。这样你才能清楚地知道什么方法是有效的。善用Kaggle Notebook的版本功能为每个重要的实验步骤保存一个版本。最后对于想入门Kaggle或深度学习的朋友我的建议是从这种结构清晰的入门赛开始不要怕分数低。把目标定为“完整走一遍流程”而不是“冲到金牌”。在实战中你会遇到所有教程里都不会细讲的“魔鬼细节”解决它们的过程就是你真正成长的时刻。当你看到自己训练的模型能正确识别出各种花卉时那种成就感就是坚持下去的最好动力。