
李宏毅机器学习hw3我觉得是这门课里最值得认真做的一次作业。如果你正在跟李宏毅老师的机器学习课程做到第三次作业时大概会有一个明显感受前两次还在跟线性回归、逻辑回归这种“经典热身”打交道到了hw3突然就切换到卷积神经网络CNN做图像分类了。难度跨度确实不小但恰恰是这一次作业能帮你把“模型怎么搭”“训练怎么调”“效果怎么提”这条完整链路跑通一遍。我当初做完hw3之后再回头去看课程里讲CNN、讲数据增强、讲Batch Normalization的内容理解完全不一样了——很多概念是“做一遍才真正懂的”。这篇文章我不打算复述作业说明而是基于我自己做完hw3的完整经历把任务拆解、模型设计、训练调参、避坑技巧这些都梳理一遍。不管你是正在跟课的学生还是单纯想练手CNN图像分类的初学者这篇文章应该能帮你少走不少弯路。1. hw3到底在做什么任务拆解与核心思路1.1 从作业要求看本质这不是一道“调库题”hw3的任务表面上看很简单给你一批食物图片训练一个模型把图片分类到11个类别里。数据集用的是Food-11包含面包、乳制品、甜点、鸡蛋、炸食、肉类、面食、海鲜、汤类、蔬菜沙拉和米饭这11类食物图片。训练集大约9866张验证集3430张测试集3347张。但如果你只把它当成“用PyTorch调用现成CNN跑个分类”那就太亏了。这份作业真正想考察的能力是你能不能根据数据特点设计一个合理的图像分类方案并把它训练到可用的精度。作业里明确给了几个加分项——数据增强、BN层、以及尝试不同架构如ResNet、DenseNet等这些提示其实就是课程核心内容的实践映射。我个人的理解是hw3隐藏的三个考察点分别是对CNN基础组件的掌握程度卷积层、池化层、全连接层、激活函数这些不是背概念而是要能根据任务选择并组合。对训练过程的理解学习率怎么设、batch size怎么选、过拟合怎么判断和缓解这些直接影响最终精度。对模型改进的sense从baseline到提升精度每一步改动是否有依据而不是盲目堆参数。1.2 数据特点分析为什么这个任务“看着简单做着微妙”Food-11这个数据集有它自己的脾气。我在实际处理时发现几个明显特点第一类别间相似度高。比如汤类和蔬菜沙拉从缩略图上看真的很难分有些汤里飘着蔬菜叶有些沙拉带汤汁模型很容易被这类样本搞糊涂。第二图片尺寸和构图差异大。数据集中有些是食物特写有些是整桌餐食模型需要学会关注“主体区域”而不是背景。第三类别分布不完全均衡。虽然整体分布还行但某些类别比如汤类图片数量偏少训练时需要留意。这些特点决定了什么决定了数据增强不是锦上添花而是必需品。后面我会详细说怎么做好数据增强。1.3 环境准备用最少的时间搭好可复现的环境hw3官方示例代码是基于PyTorch的我强烈建议你直接用PyTorch别用TensorFlow。不是TensorFlow不好而是这份作业的参考代码、社区讨论、调试经验绝大多数都是PyTorch生态的跟课阶段用跟主流一致的工具能省掉大量无谓的“翻译”成本。版本方面我实测下来的组合是Python 3.8 或 3.9不要用太新的版本某些库的兼容性会让你头大PyTorch 1.10 或 1.121.x系列足够稳定2.x也可以用但没必要追新torchvision 0.11 或 0.12CUDA 11.3 配合对应cuDNN如果你用GPU的话提示如果本地没有GPU用Google Colab的免费GPU也完全够跑hw3。我在Colab上跑过完整的训练流程一个epoch大概一两分钟取决于模型复杂度完全可接受。唯一要注意的是Colab的session会断训练中间记得保存checkpoint。2. CNN图像分类的“为什么”核心原理与设计逻辑2.1 卷积层到底在干什么从“看像素”到“看特征”很多同学学到CNN时会觉得卷积是个很玄的东西但其实它的核心思想非常朴素与其让模型直接看整张图片的每个像素不如让它先学会看局部的小模式再层层组合成更高层级的语义。举个例子第一层卷积可能学到的是边缘、颜色块、纹理这些“零件”第二层卷积把零件组合成“圆形”“条纹”“颗粒感”等稍复杂的模式再往后的层可能就能识别出“面包的焦黄色泽”“蔬菜的绿色轮廓”“汤面的油光”这种接近语义的特征。这就是为什么CNN在图像上远比全连接网络有效——它的归纳偏置局部连接权值共享天然符合图像的结构特点。hw3中你用的每个卷积层本质上都在做这样的事从训练数据中自动学习一组卷积核滤波器让它们能提取出对分类有用的视觉特征。2.2 池化层和步长的作用控制计算量与平移鲁棒性池化层Pooling经常被一笔带过但它对模型性能的影响非常大。最大池化Max Pooling做的事情是在一个小窗口内取最大值作为这个区域的代表。为什么要这么做两个原因一是降低特征图尺寸减少后续层的计算量二是增强平移鲁棒性——如果同一个特征出现在窗口的左上角或右下角池化后的结果是一样的模型不会因为物体的微小位移而改变判断。步长Stride的作用类似。步长为2的卷积可以替代池化做下采样而且现代网络如ResNet里确实更喜欢用步长卷积而不是池化因为它能让模型自己学习下采样的方式而不是用固定的取最大值操作。在hw3中官方示例用的是SimpleBaseline——几层卷积加最大池化最后接全连接层。这个结构作为baseline完全够用但如果想提升精度把池化替换成步长为2的卷积层是一个不错的改进方向。2.3 Batch Normalization为什么它能“神奇地”加速收敛hw3作业说明里明确提到BN层是加分项。BNBatch Normalization的作用可以用一句话概括把每一层的输入拉回到均值为0、方差为1的分布让网络各层训练时面对的输入分布更稳定。这样说可能还是有点抽象我换个方式解释。想象你在训练一个深层网络每层都在不断更新参数这意味着每一层的输入分布其实在训练过程中一直在变这叫Internal Covariate Shift内部协变量偏移。这种变化会让训练变得不稳定、收敛变慢。BN做的事情就是在每层输入后面加一个“标准化缩放平移”操作让网络对参数变化的敏感度降低从而可以用更大的学习率、更快地收敛。我在hw3里实测的感受是加了BN层的模型训练loss下降速度明显快于不加BN的版本而且最终精度也更高。BN几乎是“免费的午餐”在你设计的CNN里加上它基本没有坏处唯一要注意的是在训练和推理模式下行为不同PyTorch会自动处理无需手动干预。2.4 用生活化类比理解CNN的完整流程把整条链路比作一个质检流水线会更直观原始图片是“原材料堆”卷积层像是“分拣工人”每个工人负责找一种特征边缘、颜色、纹理等池化层像是“粗筛”把重复和不重要的信息丢掉留下关键特征激活函数如ReLU像是“质检标准”只让有正向信号的零件通过全连接层像是“最终质检主管”把前面所有零件的信息汇总给出“这是哪类食物”的结论Softmax是“贴标签环节”输出每个类别的概率所有概率加起来等于1。所以当你设计网络时其实是在设计这条流水线需要多少个分拣工人卷积核数量、分几道工序卷积层数、每道工序看多大区域卷积核大小、什么时候粗筛池化/步长、最后由谁来做结论全连接层维度。2.5 从Baseline到有效模型架构演进的核心思路hw3给了SimpleBaseline作为起点但真正想把精度做到80%以上我的经验是分三个阶段演进第一阶段跑通baseline。用官方的SimpleBaseline结构先不管精度确保整个数据加载、训练、验证、保存模型的流程是通的。这一步的关键是“能跑”不是“跑得好”。第二阶段在baseline基础上加BN和数据增强。这两个改动的性价比最高基本不需要增加太多计算量就能稳定提升3-5%的精度。第三阶段尝试更深的架构或迁移学习。用torchvision里现成的ResNet18或ResNet50加载ImageNet预训练权重然后只替换最后的全连接层输出为11类。这个方案如果调好了精度能直接跳到85%以上是hw3拿高分的“捷径”。3. 从零搭建你的hw3模型完整实操过程3.1 数据加载与预处理做好这一步后面少踩一半的坑数据这块我的建议是直接用torchvision的datasets.ImageFolder来加载。先用脚本把Food-11数据集整理成以下目录结构food-11/ training/ 00_bread/ 01_dairy_product/ 02_dessert/ 03_egg/ 04_fried_food/ 05_meat/ 06_noodles_pasta/ 07_seafood/ 08_soup/ 09_vegetable_fruit/ 10_rice/ validation/ ... testing/ ...这样ImageFolder就能自动按子目录名生成标签不需要手动写标签映射逻辑。完整的DataLoader代码大致是这样的import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集数据增强归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集只缩放归一化 valid_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]) ]) train_dataset datasets.ImageFolder(food-11/training, transformtrain_transform) valid_dataset datasets.ImageFolder(food-11/validation, transformvalid_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) valid_loader DataLoader(valid_dataset, batch_size32, shuffleFalse, num_workers4)这里有几个细节需要特别注意Resize的目标尺寸。我选的是224×224因为这是ImageNet预训练模型的默认输入尺寸。如果你用的是自己从零训练的CNN用128×128甚至96×96也可以计算量能减少不少。Normalize的均值和方差为什么是[0.485, 0.456, 0.406]。这三个值不是随便写的是ImageNet数据集的RGB三通道均值。如果用了ImageNet预训练权重标准化方式必须和预训练时一致否则迁移学习效果会打折扣。如果用的是自己从零训练的模型可以改用自己数据集的均值方差或者干脆不归一化但效果通常会差一点。数据增强的强度。RandomHorizontalFlip随机水平翻转对食物图片是安全的因为食物不存在“左右颠倒”的语义问题。RandomRotation(15)表示最多旋转15度这个值别设太大否则面包转90度还认得出来吗旋转角度过大会让模型学到错误的特征。ColorJitter里的亮度、对比度、饱和度抖动参数我建议都设在0.2左右太小没效果太大会让颜色失真严重。3.2 模型结构设计从SimpleBaseline到可训练的CNN官方给的SimpleBaseline结构大致是三层卷积加池化最后接全连接层。我在这个基础上做了调整设计了一个更适合hw3的“中间路线”模型import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes11): super(SimpleCNN, self).__init__() # 第一个卷积块 self.conv_block1 nn.Sequential( nn.Conv2d(3, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 224 - 112 ) # 第二个卷积块 self.conv_block2 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 112 - 56 ) # 第三个卷积块 self.conv_block3 nn.Sequential( nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) # 56 - 28 ) # 分类头 self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(256, num_classes) ) def forward(self, x): x self.conv_block1(x) x self.conv_block2(x) x self.conv_block3(x) x self.classifier(x) return x这个结构和官方baseline相比主要改动是每个卷积块用两层3×3卷积通道数从64到128再到256递增特征图尺寸减半通道数加倍这是CNN设计的经典范式并且每层卷积后都加了BN和ReLU。关于这个设计的几个问题我解释一下“为什么”为什么用3×3卷积而不是5×5或7×7两个3×3卷积堆叠的感受野等于一个5×5卷积但参数量更少2×3×3×C²与1×5×5×C²的对比而且中间多了一次非线性激活表达能力更强。这就是VGG系列网络的核心设计思路。为什么每个block先用两个卷积再接池化多加一层卷积让block有更强的特征提取能力池化操作放在最后负责下采样。如果先池化再卷积信息会丢失得更早不利于模型学习细粒度特征。为什么最后用AdaptiveAvgPool2d((1,1))而不是Flatten后直接接全连接Global Average Pooling全局平均池化把每个特征图压缩成一个标量把整个特征图的空间信息平均成一个值。这样做的好处是不管输入图片尺寸如何最终进入全连接层的维度都是固定的网络架构的灵活性大大提升。另外GAP本质上强制模型学到“每个类别对应一个特征图”的语义泛化性往往比直接Flatten更好。3.3 训练策略学习率、损失函数与优化器选择训练配置我推荐这套组合是我在hw3上跑过多次后觉得最稳的import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau model SimpleCNN(num_classes11) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3, verboseTrue)这里的关键选择和分析如下损失函数用CrossEntropyLoss。多分类问题的标准选择它内部已经包含了Softmax操作PyTorch的CrossEntropyLoss是LogSoftmaxNLLLoss的组合所以模型最后的全连接层输出不需要再手动接Softmax。优化器用Adam而不是SGD。Adam自带自适应学习率收敛速度快对学习率不那么敏感对新手友好。但如果你想把精度榨到极致训练后期可以切换到SGD动量momentum0.9因为SGD的泛化性通常更好。我的建议是先用Adam快速收敛到一个不错的精度然后把优化器换成SGD把模型微调几步也许能再涨0.5-1%。weight_decay设为1e-4做L2正则化。这个值我用下来比较中庸能有效抑制过拟合又不会让模型欠拟合。hw3的训练集不算特别大模型有一定复杂度时过拟合风险不低加一点weight_decay是低成本高收益的做法。ReduceLROnPlateau调度器当验证集loss连续3个epoch不下降时学习率降低一半。这个机制太实用了——它让你不用手动盯着loss去调学习率让训练过程更稳定。3.4 完整的训练与验证循环import copy num_epochs 30 best_acc 0.0 for epoch in range(num_epochs): # 训练阶段 model.train() train_loss 0.0 train_correct 0 train_total 0 for inputs, labels in train_loader: inputs inputs.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) train_total labels.size(0) train_correct (predicted labels).sum().item() # 验证阶段 model.eval() val_loss 0.0 val_correct 0 val_total 0 with torch.no_grad(): for inputs, labels in valid_loader: inputs inputs.to(device) labels labels.to(device) outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() train_loss train_loss / train_total train_acc train_correct / train_total val_loss val_loss / val_total val_acc val_correct / val_total print(fEpoch [{epoch1}/{num_epochs}] fTrain Loss: {train_loss:.4f} Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} Acc: {val_acc:.4f}) scheduler.step(val_loss) # 保存最佳模型 if val_acc best_acc: best_acc val_acc best_model_wts copy.deepcopy(model.state_dict()) model.load_state_dict(best_model_wts) torch.save(model.state_dict(), best_model.pth)这段代码里有几个实操细节值得展开model.train()和model.eval()不能忘记切。这个太重要了。训练模式下启用了Dropout和BN的batch统计量推理模式下Dropout关闭、BN使用全局均值方差。如果忘了切到eval模式就跑验证BN层在评估时会用当前batch的统计量而不是全局统计量导致验证结果不稳定甚至偏低。这个坑我踩过当时验证集准确率忽高忽低排了半天才发现是模式没切。验证阶段必须包在torch.no_grad()里。这能大幅减少显存占用和计算量因为不需要计算梯度。你不加这个也能跑但显存可能爆速度也会慢很多。Deepcopy保存最佳权重。因为state_dict()返回的是一个dict对象直接赋值best model.state_dict()是引用传递后面模型继续训练时best也会跟着变。必须用copy.deepcopy创建独立副本或者用两种方法更简单每次保存到pth文件比如torch.save(model.state_dict(), best_model.pth)。4. 数据增强、迁移学习与精度调优4.1 数据增强的进阶玩法别只停留在官方示例官方hw3示例中做了简单的裁剪和翻转但如果你想拿高分数据增强还有更多可玩的空间。我实测下来效果比较好的增强组合有train_transform_advanced transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.1), transforms.RandomAffine(degrees0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])逐个解释一下RandomResizedCrop比直接Resize更实用。它在每张图上随机裁剪一块区域再缩放到224×224相当于让模型看到同一张食物的不同局部。这不仅增加数据多样性还能缓解“模型只关注图片中心区域”的偏见。scale(0.7, 1.0)表示裁剪区域占原图面积的70%到100%这个范围对食物图片是合理的太小的裁剪比例会让模型看不清食物主体。RandomAffine的translate做轻微的随机平移。你可以想象实际拍照时食物不会总在正中间平移增强让模型对目标位置不敏感。关于颜色抖动的hue色调参数要特别注意。hue0.1意味着色调最多偏转0.1这个值不大属于安全范围。但如果你把hue设到0.5面包可能变成蓝色面包这就不合理了。颜色增强要适可而止毕竟食物的颜色是重要的分类特征。4.2 迁移学习实战用ResNet50把精度推到85%以上当你的自建CNN精度卡在75%-80%上不去时迁移学习是打破瓶颈的最有效手段。原理很简单ResNet50已经在ImageNet1000类、120万张图上预训练过它学到了通用视觉特征。你要做的只是把最后分类层换成11类输出微调整个模型——这样相当于站在巨人的肩膀上不需要从头学“怎么看边缘、怎么看纹理”。用torchvision加载预训练模型的代码import torchvision.models as models resnet50 models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) num_features resnet50.fc.in_features resnet50.fc nn.Linear(num_features, 11)这段代码有几个关键点weights参数指定预训练权重版本。旧版的写法是pretrainedTrue新版本torchvision推荐用weights方式更明确。IMAGENET1K_V1是原版ImageNet-1k上的权重。替换fc层。resnet50.fc是最后一层全连接原始输出是1000维换成11维。值得一提的是只替换fc层其他层的预训练参数都保留然后整体微调。训练时我建议先用较小的学习率如1e-4因为预训练模型已经在一个大数据集上学到了不错的特征表示你只需要微调不需要让大梯度破坏这些特征。如果从零训练用1e-3没问题但迁移学习阶段用1e-4更稳。另外还有一个常用的技巧分阶段解冻。初始训练时只训练fc层其他层参数冻结跑几个epoch让新分类头先稳定下来然后再解冻所有层一起微调。这样可以避免刚开始时反向传播的大梯度直接破坏预训练模型的特征提取能力。4.3 过拟合的识别与针对性解法hw3上过拟合的表现主要有两种训练loss持续下降但验证loss不再下降甚至上升训练精度接近100%但验证精度卡在70%左右。如果出现这些情况以下手段按优先级从高到低尝试加强数据增强这是最温和也最有效的方案给模型“看”更多虚拟样本。加Dropout层在分类头里加一个nn.Dropout(0.5)让全连接层的神经元随机失活强迫网络学冗余特征。增加weight_decay从1e-4调到5e-4更强的L2正则化。降低模型复杂度如果用了ResNet50过拟合试着换成ResNet18。早停监控验证集指标连续N个epoch不提升就终止训练保存最优模型。我的实际经验是hw3这个任务如果你是自建CNN过拟合通常不太严重毕竟训练集接近1万张对小型CNN来说够了但如果你用预训练ResNet50且层全解冻过拟合风险就会上来。数据增强和Dropout是最优先的解法。4.4 精度提升方向速查表如果你想知道“下一步该往哪个方向调”我整理了一个决策速查表按照投入产出比排序当前状态最优下一步预期提升Baseline能跑通精度65%-70%加BN层 基础数据增强5%-8%已有BN和增强精度75%左右加深网络更多卷积层或更深通道3%-5%自建CNN精度卡在80%切到迁移学习预训练ResNet18/505%-10%迁移学习精度85%但继续涨不动更精细的调参lr schedule、SGD动量、更丰富增强1%-3%想冲击90%以上模型集成、测试时增强TTA、用更大模型如EfficientNet1%-2%这个表不是我拍脑袋写的是基于我在hw3上反复试验的一个大致经验区间。每个人的数据划分和随机种子不同具体数值会有浮动但方向基本可靠。5. 训练过程中的踩坑实录与排查方法5.1 损失函数变成NaN问题出在哪训练时loss突然变成NaNNot a Number这是新手最常遇到的“灵异事件”。常见原因和排查方式我列在这里学习率过大导致的梯度爆炸。这是最常见的原因。解决的方案把学习率降低一个数量级试试比如1e-3降到1e-4。Adam虽然对学习率不敏感但学习率过大一样会炸。输入数据存在NaN。图片数据一般不会出现NaN但如果你做了某些自定义变换比如除法操作分母为零就可能引入NaN。排查方法在训练循环里加断言检查inputs或者打印一份输入的统计值看看有没有inf/NaN。label越界。CrossEntropyLoss要求labels的范围是[0, num_classes-1]。如果你的标签文件是从1开始的比如1到11没有减1直接传入那第11类就超出范围了loss可能变成NaN。这个坑很隐蔽Food-11原始数据集的文件夹命名是00到10但如果你用了别的数据源需要仔细核对标签范围。5.2 准确率一直上不去先检查这些基础项如果你的模型训练了几个epoch准确率纹丝不动先别急着改模型架构先按这个顺序排查确认标签和数据是对齐的。用脚本随机挑几批数据打印图片和对应标签肉眼检查有没有错位。ImageFolder按文件名排序应该没问题但如果你自己写了数据加载逻辑这个很容易出错。确认模型输入尺寸和图像实际尺寸匹配。用了224×224的Resize但模型第一个卷积层期望的输入是64×64这时模型会直接报错。如果没报错检查好image shape有没有问题。确认optimizer.zero_grad()有没有被调用。如果漏了这一行梯度会在每次backward时累加训练会非常不稳定准确率上不去。新手经常忽略这一点。确认数据增强没有“增强过头”。太强的色彩抖动会让模型根本看不清原始颜色信息训练loss降得很慢。我在ColorJitter里把saturation调到0.8时训练明显变慢了降到0.3才恢复正常。5.3 训练速度太慢怎么办如果你在Colab上训练觉得太慢优先级最高的优化方式减小输入尺寸。从224×224降到160×160或128×128计算量能减少将近一半精度损失通常不到1%。hw3的食物分类不太依赖极细节的纹理128×128完全可接受。减小batch size或增大batch size这里的调整要在GPU显存允许范围内做取舍。batch size大梯度估计更稳定GPU利用率高batch size小则更快的更新频率。Colab上16GB显存跑ResNet50可以试试batch size 32到64。混合精度训练。PyTorch提供autocast自动混合精度Float16的计算速度在支持Tensor Core的GPU上快很多。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for inputs, labels in train_loader: inputs inputs.to(device) labels labels.to(device) optimizer.zero_grad() with autocast(): outputs model(inputs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()混合精度在Colab T4 GPU上实测能提速30%-50%而且精度几乎不掉。这是性价比极高的优化手段。5.4 类别不均衡问题的处理技巧Food-11的类别分布虽然不算严重不均衡但如果你发现某些类别比如汤类精度明显偏低可以试这些手段加权损失函数根据每个类别的样本数给loss加权重让样本少的类别对loss贡献更大。class_counts [len(train_dataset.targets[train_dataset.targets i]) for i in range(11)] class_weights 1.0 / torch.tensor(class_counts, dtypetorch.float32) class_weights class_weights / class_weights.sum() # 归一化 criterion nn.CrossEntropyLoss(weightclass_weights.to(device))类别平衡采样用WeightedRandomSampler让每个batch里各类别出现的概率相等而不是按原始分布抽取。这会让模型每步训练都能看到各个类别不至于被样本多的类别“带偏”。5.5 测试集标签生成的最后一公里hw3的最终成果是一份测试集预测结果通常要求输出每个测试图片的类别标签。官方提供sample_submission.csv格式你需要为每一张测试图片生成预测并写入CSV。这段代码可以参考import pandas as pd from PIL import Image model.eval() predictions [] with torch.no_grad(): for line in open(food-11/testing.txt): img_path line.strip() img Image.open(img_path).convert(RGB) img valid_transform(img).unsqueeze(0).to(device) output model(img) pred output.argmax(dim1).item() predictions.append(pred) # 注意测试集图片的文件名/ID要和提交格式对应 # 这里按你的实际测试集命名方式调整一个重要的细节在测试阶段不要用训练时的数据增强。测试时只需要做Resize和归一化因为增强会引入额外的随机性让预测不稳定。如果你追求极致精度可以做测试时增强TTA即把同一张图做多次不同的增强翻转、缩放对多次预测结果取平均然后把平均后概率最大的类作为最终预测。TTA通常能提升1%左右的精度但预测时间会成倍增加自己权衡。6. 从hw3延伸开去这套方法论还能用在哪6.1 从食物分类到通用图像分类hw3的任务本质是“图像分类”。做完这次作业你就掌握了一套完整的“图像分类项目全流程”数据整理、数据增强、模型选择、训练调参、防止过拟合、预测提交。这套方法论可以直接迁移到其他图像分类任务上比如垃圾图片分类、植物病害识别、产品质检分类等。本质上都是同一件事给定一张图告诉我是哪一类。唯一的差别在于数据集的具体特点和分类难度细粒度分类比如“不同品种的猫”会比“猫和狗”难很多但使用的方法论是完全一致的。6.2 从手动设计到自动化调参的进阶方向如果你做完hw3还觉得不过瘾可以试试用一些自动调参工具继续优化比如Optuna贝叶斯优化的超参数搜索框架自动帮你找“最优学习率和batch size”。Weights Biaseswandb记录每次实验的精度、loss曲线方便不同实验之间横向对比。这些工具都能让你把“手工调参”升级为“系统化调参”也是业界常用的小工具。6.3 从CNN到视觉Transformer的眺望李宏毅老师课程中应该也提到了Self-Attention和Transformerhw3做完之后如果你对视觉模型有更深的好奇可以试试Vision TransformerViT模型。ViT将图片切成一个个patch再用Transformer的self-attention机制建模patch之间的关系。在大型数据集上ViT的效果可以超越CNN但在Food-11这种中等规模数据集上预训练ViT的迁移效果要谨慎评估。从我的实际测试来看在hw3上经过精调的ResNet50仍然比ViT-B/16更容易取得好结果因为ViT需要更精细的训练技巧更强的正则化、更大的数据增强小数据集上CNN的归纳偏置优势更明显。6.4 应用场景与影响范围思考hw3训练出来的模型也许只是课堂作业但图像分类能力的实际应用场景非常广。比如电商场景中自动给商品图片打类目标签节省人工标注成本垃圾分类项目中自动识别可回收物、厨余垃圾等医疗影像领域的初步筛查辅助虽然那需要更严谨的模型验证但底层技术原理是共通的。我每次做完一个像hw3这样的完整项目最大的收获不只是模型精度提高了多少而是建立了一套“遇到问题怎么分析、怎么实验验证、怎么迭代改进”的思维框架。这种能力在真实的机器学习项目中比任何具体模型都更值钱。7. 写在最后hw3教会我的三件事回顾整个hw3有三件看似不起眼但让我印象深刻的事。第一数据增强远比调模型结构更提分。我在第一版模型上苦调网络结构精度始终在72%徘徊后来只是把数据增强从“基础翻转”升级到“随机裁剪颜色抖动”精度就跳到了79%。这个体验让我养成了习惯任何图像任务先用当前最强的数据增强方案再谈模型结构。第二不要忽略训练代码的基础细节。model.train()和model.eval()的切换、optimizer.zero_grad()的调用、验证阶段用torch.no_grad()包裹、正确保存最佳模型这些都是“不起眼的细节”但任何一个出错都会让结果莫名其妙地变差。所谓“经验”大部分就是在这些细节上踩过的坑。第三最有效的学习方法不是看很多教程而是完整做完一个项目。在看李宏毅老师的课程视频时CNN的每个知识点我都觉得听懂了但直到自己亲手搭网络、训练、调优、提交结果那些知识点才真正变成了我的能力。如果你正在做hw3别急着追求最好的精度先把完整流程跑通再一步步优化这趟下来你收获的绝对不只是一个漂亮的准确率数字。最后再分享一个小技巧做hw3时建议养成每次实验前记录一下模型结构、超参数、数据增强方式这三项的核心变动的习惯。我自己的做法是每次实验编号记录跑完看一眼验证集精度。长时间积累下来观察模型性能变化的趋势比单纯记一堆孤立数字更有价值。祝你的hw3顺利拿到满意的精度更重要的是享受一步步把模型调好的过程。