公司动态

李宏毅机器学习HW03实战:从数据到模型的完整图像分类项目解析

📅 2026/8/24 2:51:35
李宏毅机器学习HW03实战:从数据到模型的完整图像分类项目解析
1. 项目背景与核心价值如果你正在学习李宏毅老师的机器学习课程那么HW03绝对是一个绕不开的“硬骨头”。这门课的作业向来以理论扎实、实践性强著称而HW03更是其中的典型代表它通常聚焦于一个核心的机器学习任务比如图像分类、文本分类或者回归预测要求你从零开始完成数据预处理、模型搭建、训练调优到结果提交的全流程。网上虽然能找到一些零散的代码片段但往往缺乏系统性的思路梳理和关键细节的剖析导致很多同学“知其然不知其所以然”代码跑通了但背后的门道却没摸清。这篇内容就是为你解决这个痛点。我将以一个拥有多年算法工程经验的视角带你深度拆解HW03。我们不止步于分享能跑通的代码更要深入解析每一行代码背后的设计逻辑、每一个超参数选择的考量以及我在实际调试过程中踩过的那些“坑”。无论是数据增强策略的微妙影响、模型架构中容易被忽略的细节还是损失函数在特定任务下的表现我都会结合具体案例把原理讲透把步骤拆细。我们的目标是让你在完成作业的同时真正掌握处理一个完整机器学习项目的通用方法论和实战技巧而不仅仅是交一份作业。2. HW03典型任务剖析以图像分类为例李宏毅老师的作业设计非常注重与前沿应用的结合。HW03近年来常以图像分类任务为载体例如使用CIFAR-10、Food-101的子集或某个特定领域的数据集。这类任务看似基础却涵盖了监督学习中最核心的环节。2.1 任务定义与数据理解拿到作业的第一件事不是急着写代码而是彻底理解任务。作业说明通常是PDF或Markdown文件会明确指定数据集、评价指标如准确率Accuracy和提交格式。以图像分类为例你需要立刻明确以下几点数据集结构训练集、验证集、测试集的图片是如何组织的是每个类一个文件夹还是由一个CSV文件标注图片的尺寸、格式是否统一理解数据组织方式是编写数据加载器DataLoader的前提。评价指标除了准确率是否还关注每个类别的精确率Precision、召回率Recall或F1-score这直接影响你后续分析模型短板的方向。数据规模与类别不平衡快速浏览一下数据看看每个类别的样本数量是否大致均衡。如果存在严重不平衡比如某个类只有几十张图而其他类有几千张那么你需要提前思考应对策略例如在损失函数中使用类别权重Class Weight或采用过采样/欠采样技术。注意很多同学会忽略验证集Validation Set的构建。作业有时会提供官方的验证集有时则需要你从训练集中自行划分例如8:2。自行划分时务必确保划分是随机的并且每个类别在训练集和验证集中都有分布。一个常见的错误是按文件顺序划分导致某个类别完全出现在验证集中。2.2 环境搭建与依赖管理工欲善其事必先利其器。一个清晰、可复现的环境是高效完成作业的基石。我强烈建议使用虚拟环境如conda或venv来管理项目依赖。# 使用 conda 创建环境的示例 conda create -n ml_hw03 python3.8 conda activate ml_hw03接下来安装核心依赖。对于基于PyTorch的HW03这也是目前的主流你的requirements.txt或安装命令可能包含pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 根据CUDA版本调整 pip install pandas numpy matplotlib scikit-learn tqdm pillow为什么是这些库torchtorchvision模型构建、训练和标准图像数据处理的基石。pandas方便地读取和处理CSV格式的标签文件。numpy底层数值计算。matplotlib可视化训练过程损失/准确率曲线、查看数据增强效果。scikit-learn用于计算更详细的分类报告如混淆矩阵、F1-score以及可能用到的数据预处理工具如标准化。tqdm为循环添加进度条提升长时间训练时的体验。pillow(PIL)一个更轻量、有时必需的图像处理库。将依赖明确记录在requirements.txt中不仅方便自己重装环境也是项目规范性的体现。3. 数据管道构建从原始图片到模型输入数据管道Data Pipeline是连接数据和模型的桥梁其稳定性和效率直接影响整个项目的开发体验。这里我们详细拆解使用PyTorch构建管道的过程。3.1 自定义Dataset类PyTorch通过torch.utils.data.Dataset抽象类来定义数据源。你需要继承它并实现三个核心方法__init__,__len__,__getitem__。import os from PIL import Image import torch from torch.utils.data import Dataset class CustomImageDataset(Dataset): def __init__(self, root_dir, transformNone, modetrain): Args: root_dir (string): 数据根目录。 transform (callable, optional): 应用于图像的变换/增强。 mode (str): train, val 或 test用于区分数据路径。 self.root_dir root_dir self.transform transform self.mode mode # 假设目录结构为root_dir/train/class_0/*.jpg, root_dir/val/class_1/*.jpg ... # 或者有标注文件 train.csv, val.csv self.image_paths [] self.labels [] if mode in [train, val]: # 方式一通过文件夹结构读取 class_folders sorted(os.listdir(os.path.join(root_dir, mode))) for label_idx, class_name in enumerate(class_folders): class_dir os.path.join(root_dir, mode, class_name) for img_name in os.listdir(class_dir): if img_name.endswith((.jpg, .png, .jpeg)): self.image_paths.append(os.path.join(class_dir, img_name)) self.labels.append(label_idx) # 方式二通过CSV文件读取更常见于作业 # import pandas as pd # df pd.read_csv(os.path.join(root_dir, f{mode}.csv)) # self.image_paths df[image_path].tolist() # self.labels df[label].tolist() else: # test mode # 测试集可能没有标签 test_dir os.path.join(root_dir, test) for img_name in sorted(os.listdir(test_dir)): # 排序保证输出顺序固定 if img_name.endswith((.jpg, .png, .jpeg)): self.image_paths.append(os.path.join(test_dir, img_name)) self.labels.append(-1) # 或 None作为占位符 def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] # 使用PIL打开图像确保转换为RGB即使是灰度图也转成3通道 image Image.open(img_path).convert(RGB) label self.labels[idx] if self.transform: image self.transform(image) # 测试集可能返回图像和文件名用于后续生成提交结果 if self.mode test: return image, os.path.basename(img_path) else: return image, label关键细节解析Image.open().convert(RGB)这一步至关重要。数据集中的图片格式可能不统一如RGBA PNG或有损JPEG统一转换为RGB三通道可以避免后续张量形状不一致的错误。排序在测试集的__init__中对os.listdir的结果进行sorted是为了保证每次运行时image_paths的顺序一致这样生成的预测结果文件才能与官方测试集顺序对应这是很多同学提交结果出错的原因。标签映射如果标签是字符串如cat,dog需要在__init__中构建一个从类别名到整数索引label_idx的映射字典并在__getitem__中进行转换。整数标签是PyTorch交叉熵损失函数nn.CrossEntropyLoss所要求的。3.2 设计数据增强与标准化变换数据增强是提升模型泛化能力、防止过拟合的利器尤其在训练数据量有限的学生作业中效果可能非常显著。我们需要为训练集和验证/测试集定义不同的变换管道。from torchvision import transforms # 训练集变换增强 标准化 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪并缩放到固定尺寸 transforms.RandomHorizontalFlip(p0.5), # 随机水平翻转 transforms.RandomRotation(degrees15), # 随机旋转 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), # 随机颜色抖动 transforms.ToTensor(), # 将PIL图像转换为[C, H, W]范围的Tensor transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), # ImageNet统计量 ]) # 验证/测试集变换仅保留必要的预处理和标准化不做随机增强 val_test_transform transforms.Compose([ transforms.Resize(256), # 将短边缩放到256 transforms.CenterCrop(224), # 中心裁剪到224x224 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])为什么这么设计训练集增强RandomResizedCrop、RandomHorizontalFlip是最常用且有效的增强能模拟物体位置、视角的变化。ColorJitter能增加模型对光照、颜色变化的鲁棒性。参数如p0.5,degrees15可以根据任务调整对于对称性不强的物体如文字、非对称建筑水平翻转可能不合适。验证/测试集不变换评估模型性能必须在一致、确定的数据上进行。因此我们只做确定性的Resize和CenterCrop确保每次评估同一张图片的输入是相同的。标准化参数mean[0.485, 0.456, 0.406],std[0.229, 0.224, 0.225]是ImageNet数据集上百万张图片统计得到的全局均值标准差。即使你用的不是ImageNet数据也强烈建议使用这个值。因为预训练模型如ResNet的权重是在用这些值标准化的数据上训练得到的输入保持相同的分布才能更好地利用预训练知识。如果你用自己的数据集从头训练可以计算自己数据集的统计量。3.3 组装DataLoaderDataset负责定义如何读取单个样本DataLoader则负责将样本组织成批batch并提供多进程加速读取、随机打乱等功能。from torch.utils.data import DataLoader # 创建Dataset实例 train_dataset CustomImageDataset(root_dir./data, transformtrain_transform, modetrain) val_dataset CustomImageDataset(root_dir./data, transformval_test_transform, modeval) test_dataset CustomImageDataset(root_dir./data, transformval_test_transform, modetest) # 创建DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue)参数详解与避坑指南batch_size常见范围是32, 64, 128。越大每次参数更新越稳定但消耗显存越多且可能影响泛化能力。一般根据GPU显存调整可以从64开始尝试。shuffle训练集必须设为True打乱数据顺序防止模型学习到与样本顺序相关的虚假模式。验证集和测试集必须设为False以保证评估的可重复性。num_workers用于数据加载的子进程数。在Linux/Mac上设置为CPU核心数如4, 8可以显著加速数据加载避免训练时GPU等待数据。在Windows上有时设置为0能避免多进程相关的错误。pin_memoryTrue当使用GPU时将此参数设为True可以将数据从主机内存直接锁页到GPU内存减少一次数据拷贝提升数据从CPU到GPU的传输速度。这是一个容易被忽略但能提升训练速度的小技巧。踩坑实录我曾遇到一个诡异的问题训练时损失正常下降但验证准确率始终在随机水平10类任务约10%徘徊。排查了很久最后发现是验证集的DataLoader错误地设置了shuffleTrue。这导致每次验证时模型看到的“验证集”顺序都是乱的但评估时又是按原始顺序计算指标造成了完全错误的评估结果。所以请务必再三检查shuffle参数。4. 模型选择、搭建与迁移学习策略对于HW03这类作业从头开始训练一个复杂的模型如ResNet通常不现实因为数据量有限容易过拟合且训练时间很长。因此迁移学习Transfer Learning是首选且最有效的策略。4.1 模型选择为何是ResNet在图像分类任务中ResNet及其变体ResNet-18, ResNet-50是经过无数实践检验的“基准模型”。它结构清晰在ImageNet上预训练的权重质量高且PyTorch官方torchvision.models提供了便捷的接口。对于作业规模的数据ResNet-18约1100万参数通常就足够了训练快显存占用小。如果追求更高精度且计算资源允许可以尝试ResNet-50。4.2 迁移学习的两种微调方式迁移学习的核心思想是利用在大规模数据集如ImageNet上预训练好的模型权重作为起点针对我们的新任务进行微调Fine-tuning。微调通常有两种策略特征提取器Feature Extractor冻结预训练模型的所有层将其requires_grad设为False只训练我们新添加的、用于适应新任务的全连接层分类头。这种方式训练速度快计算资源消耗少适合新数据与预训练数据ImageNet差异较大或者新数据量非常少的情况。整体微调Fine-tune the whole model不冻结任何层允许预训练模型的所有参数与新添加的分类头一起参与训练。这种方式通常能获得更高的精度但需要更多的训练时间和计算资源也更容易过拟合适合新数据与ImageNet有一定相似度如同为自然图像且数据量相对充足的情况。对于HW03我推荐采用一种折中且有效的策略先进行几轮“特征提取器”式的训练让分类头快速适应新任务然后解冻模型的部分或全部层进行“整体微调”。这既能加速初期收敛又能充分利用预训练知识。4.3 代码实现以ResNet-18为例import torch.nn as nn import torchvision.models as models def get_model(num_classes, pretrainedTrue, fine_tune_strategypartial): 获取并配置预训练ResNet-18模型。 Args: num_classes (int): 新任务的类别数。 pretrained (bool): 是否加载ImageNet预训练权重。 fine_tune_strategy (str): feature_extract, full, 或 partial。 Returns: model (nn.Module): 配置好的模型。 # 加载预训练模型 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) # 获取全连接层fc的输入特征维度 num_ftrs model.fc.in_features # 替换最后的全连接层以适应新的类别数 model.fc nn.Linear(num_ftrs, num_classes) # 根据微调策略设置参数是否需要梯度 if fine_tune_strategy feature_extract: # 冻结除最后一层fc外的所有参数 for param in model.parameters(): param.requires_grad False # 只有新加的fc层需要训练 for param in model.fc.parameters(): param.requires_grad True elif fine_tune_strategy full: # 所有参数都需要训练 for param in model.parameters(): param.requires_grad True elif fine_tune_strategy partial: # 一种常见策略冻结前面的卷积块微调后面的层 # 以ResNet-18为例它有4个主要卷积层layer1, layer2, layer3, layer4 # 我们可以冻结layer1和layer2微调layer3, layer4和fc for name, param in model.named_parameters(): if layer1 in name or layer2 in name or bn in name: # 也常冻结BatchNorm层 param.requires_grad False else: param.requires_grad True else: raise ValueError(fUnsupported fine_tune_strategy: {fine_tune_strategy}) return model # 使用示例 model get_model(num_classes10, pretrainedTrue, fine_tune_strategypartial) model model.to(device) # 将模型移动到GPU或CPU关键点解析model.fc.in_features这是一个非常实用的属性它告诉我们预训练模型最后一层全连接层的输入维度ResNet-18是512这样我们在替换新的全连接层时就不需要硬编码这个数字。fine_tune_strategypartial这是我个人最推荐的策略。在卷积神经网络中浅层网络如layer1,layer2学习到的是通用、底层的特征如边缘、纹理这些特征在不同任务间可迁移性很强。深层网络如layer3,layer4学习到的是与特定任务相关的高级语义特征。因此冻结浅层、微调深层可以在保留通用特征的同时让模型更好地适应新任务的具体语义。在实践中这个策略往往比单纯的特征提取或整体微调效果更好收敛也更快。BatchNorm层的处理在微调时特别是当新数据集与ImageNet分布差异较大时预训练模型中的BatchNorm层bn的running mean和running variance可能不适用。一种做法是连同其参数一起冻结如上例另一种做法是让它们参与训练但使用较小的学习率。如果微调后模型表现不稳定可以尝试解冻BatchNorm层。5. 训练循环的工程化实现与调试技巧训练循环是机器学习项目的引擎一个健壮、可监控的训练循环能极大提升开发效率。下面我们实现一个包含验证、模型保存、学习率调度和可视化功能的完整训练流程。5.1 核心训练与验证函数import torch import torch.nn as nn import torch.optim as optim from tqdm import tqdm import matplotlib.pyplot as plt def train_one_epoch(model, train_loader, criterion, optimizer, device, epoch): 训练一个epoch model.train() # 切换到训练模式影响Dropout, BatchNorm等层 running_loss 0.0 correct 0 total 0 pbar tqdm(train_loader, descfEpoch {epoch} [Train]) for inputs, labels in pbar: 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 outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() # 更新进度条描述 pbar.set_postfix({Loss: loss.item(), Acc: 100.*correct/total}) epoch_loss running_loss / total epoch_acc 100. * correct / total return epoch_loss, epoch_acc def validate(model, val_loader, criterion, device): 在验证集上评估模型 model.eval() # 切换到评估模式 running_loss 0.0 correct 0 total 0 with torch.no_grad(): # 禁用梯度计算节省内存和计算 for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) running_loss loss.item() * inputs.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() val_loss running_loss / total val_acc 100. * correct / total return val_loss, val_acc避坑指南model.train()和model.eval()这是新手极易出错的地方。在训练时必须调用model.train()这会启用Dropout、BatchNorm等层的训练行为如计算当前batch的统计量。在验证和测试时必须调用model.eval()这会固定Dropout和BatchNorm层使用训练阶段积累的running statistics。如果验证时忘记调用model.eval()BatchNorm层会继续用当前小batch的数据更新统计量导致结果不稳定且通常会更差。5.2 主训练流程与关键组件def main_training_loop(config): 主训练流程 device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) # 1. 数据加载 train_loader, val_loader get_data_loaders(config[data_dir], config[batch_size]) # 2. 模型、损失函数、优化器 model get_model(num_classesconfig[num_classes], fine_tune_strategypartial).to(device) criterion nn.CrossEntropyLoss() # 多分类任务标准损失函数 optimizer optim.Adam(model.parameters(), lrconfig[lr], weight_decayconfig[weight_decay]) # 3. 学习率调度器 scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, verboseTrue ) # 或者使用StepLR: scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) # 4. 早停Early Stopping与模型保存 best_val_acc 0.0 patience_counter 0 train_history, val_history {loss: [], acc: []}, {loss: [], acc: []} for epoch in range(config[num_epochs]): # 训练 train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device, epoch) train_history[loss].append(train_loss) train_history[acc].append(train_acc) # 验证 val_loss, val_acc validate(model, val_loader, criterion, device) val_history[loss].append(val_loss) val_history[acc].append(val_acc) print(fEpoch {epoch}: Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% | fVal Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%) # 学习率调度基于验证损失 scheduler.step(val_loss) # 保存最佳模型 if val_acc best_val_acc: best_val_acc val_acc patience_counter 0 torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), val_acc: val_acc, }, config[save_path] _best.pth) print(f - Best model saved with val_acc: {val_acc:.2f}%) else: patience_counter 1 # 早停判断 if patience_counter config[patience]: print(fEarly stopping triggered at epoch {epoch}) break # 5. 绘制训练曲线 plot_training_curves(train_history, val_history, config[save_path]) return model, best_val_acc核心组件深度解析优化器选择Adam优化器因其自适应学习率特性在大多数深度学习任务中都是默认的、效果不错的选择。weight_decay参数是L2正则化用于防止过拟合一般设置为一个较小的值如1e-4。学习率调度器ReduceLROnPlateau在监控指标如验证损失不再改善时降低学习率。patience5表示连续5个epoch验证损失未下降则触发学习率衰减乘以factor0.5。这是一个非常实用的策略。StepLR固定步长衰减每step_size个epoch将学习率乘以gamma。更简单但不够自适应。学习率是超参数之王。初始学习率lr通常设置在1e-4到1e-3之间进行尝试。对于微调学习率通常比从头训练小一个数量级如1e-4。早停Early Stopping这是防止过拟合的最有效手段之一。当验证集性能在连续patience个epoch内没有提升时就停止训练。这能避免模型在训练集上继续“死记硬背”而损害泛化能力。保存下来的best_model就是在验证集上表现最好的模型直接用于测试或提交。模型保存我们不仅保存模型参数model.state_dict()还保存了优化器状态optimizer.state_dict()和当前epoch等信息。这样如果训练意外中断我们可以从检查点checkpoint恢复继续训练而不是从头开始。5.3 可视化训练过程可视化是调试和理解的利器。绘制损失和准确率曲线能直观反映模型的学习状态。def plot_training_curves(train_history, val_history, save_path): fig, axes plt.subplots(1, 2, figsize(12, 4)) epochs range(1, len(train_history[loss]) 1) # 损失曲线 axes[0].plot(epochs, train_history[loss], b-, labelTraining Loss) axes[0].plot(epochs, val_history[loss], r-, labelValidation Loss) axes[0].set_title(Loss Curves) axes[0].set_xlabel(Epochs) axes[0].set_ylabel(Loss) axes[0].legend() axes[0].grid(True) # 准确率曲线 axes[1].plot(epochs, train_history[acc], b-, labelTraining Accuracy) axes[1].plot(epochs, val_history[acc], r-, labelValidation Accuracy) axes[1].set_title(Accuracy Curves) axes[1].set_xlabel(Epochs) axes[1].set_ylabel(Accuracy (%)) axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.savefig(save_path _curves.png, dpi150) plt.show()如何解读曲线理想情况训练损失稳步下降验证损失也同步下降最终两者都趋于平稳。训练和验证准确率同步上升并收敛。过拟合训练损失持续下降训练准确率持续上升但验证损失在某个点后开始上升验证准确率停滞甚至下降。这意味着模型记住了训练集的噪声而非学习通用模式。解决方案加强数据增强、增加Dropout、加大权重衰减、使用早停。欠拟合训练损失和验证损失都很高且下降缓慢准确率也低。这意味着模型能力不足或训练不充分。解决方案换用更复杂的模型、延长训练时间、减小正则化强度、检查数据或标签是否有问题。6. 测试集推理与结果提交训练出最佳模型后最后一步是在测试集上进行推理并生成符合作业要求的提交文件。6.1 推理代码实现def predict_test_set(model, test_loader, device, label_mapNone): 在测试集上进行预测 model.eval() all_filenames [] all_predictions [] with torch.no_grad(): for images, filenames in tqdm(test_loader, descPredicting): images images.to(device) outputs model(images) _, predicted outputs.max(1) # 获取预测类别索引 all_predictions.extend(predicted.cpu().numpy()) all_filenames.extend(filenames) # 如果需要将索引映射回类别名 if label_map is not None: all_predictions [label_map[idx] for idx in all_predictions] return all_filenames, all_predictions # 加载最佳模型进行推理 checkpoint torch.load(./best_model.pth) model.load_state_dict(checkpoint[model_state_dict]) model.to(device) filenames, predictions predict_test_set(model, test_loader, device) # 假设标签映射如果预测的是索引 # label_map {0: cat, 1: dog, ...} # predictions [label_map[idx] for idx in predictions]关键细节model.eval()和torch.no_grad()推理时必须使用以节省内存并保证行为一致。顺序一致性我们在自定义Dataset的测试集部分对文件名进行了排序这里DataLoader的shuffleFalse保证了filenames的顺序与预测结果predictions严格对应。这是生成正确提交文件的基础。6.2 生成提交文件作业通常要求提交一个CSV文件格式如filename, label。import pandas as pd def save_predictions_to_csv(filenames, predictions, output_pathsubmission.csv): 将预测结果保存为CSV文件 df pd.DataFrame({ filename: filenames, label: predictions }) # 确保按照原始文件名顺序排序虽然Dataset已经排序但这里再加一道保险 # 如果filename包含路径可能需要先提取纯文件名 df[filename] df[filename].apply(lambda x: os.path.basename(x)) df df.sort_values(filename).reset_index(dropTrue) df.to_csv(output_path, indexFalse) print(fPredictions saved to {output_path}) print(df.head()) # 保存结果 save_predictions_to_csv(filenames, predictions, hw03_submission.csv)提交前的重要检查文件数量确保生成的CSV文件行数等于测试集图片数量。文件名匹配随机抽查几行确认CSV中的filename确实存在于测试集文件夹中。格式规范检查CSV文件是否有表头标签是整数还是字符串是否与作业要求完全一致一个常见的错误是表头多了一个空格或者使用了中文标点。模型状态确认用于推理的模型是model.eval()状态并且加载的是验证集上性能最好的那个检查点best_model.pth而不是最后一个epoch的模型。7. 进阶优化与问题排查实战即使按照上述流程走通你可能还会遇到各种问题。这里分享几个进阶技巧和常见问题的排查思路。7.1 超参数调优的实用策略对于学生作业时间有限不建议进行大规模的网格搜索。可以采用一种高效的“粗调微调”策略固定其他调学习率学习率是最重要的超参数。在[1e-5, 1e-4, 1e-3]范围内尝试2-3个值快速跑5个epoch观察初始几个epoch的损失下降情况。选择那个能使损失稳定、快速下降的学习率。调整Batch Size在GPU显存允许范围内尝试增大batch_size如32, 64, 128。更大的batch通常使训练更稳定但可能会降低泛化能力。可以观察验证集性能的变化。权重衰减Weight Decay尝试[0, 1e-5, 1e-4]。如果发现训练集准确率远高于验证集过拟合可以适当增大权重衰减。数据增强强度如果模型在训练集上表现很好但验证集差可以增强数据增强如增加旋转角度、颜色抖动幅度。如果模型学习困难两者都差可以减弱数据增强。一个实用的工具是使用torch.utils.tensorboard或更轻量的wandbWeights Biases来跟踪不同超参数组合下的实验曲线直观对比。7.2 常见问题与排查清单当你遇到模型性能不佳时可以按以下清单逐项排查问题现象可能原因排查与解决思路Loss为NaN或突然变得巨大学习率过高、梯度爆炸、数据中存在异常值如NaN的像素1. 大幅降低学习率如除以10。2. 添加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。3. 检查数据加载过程确保图像被正确转换为Tensor且值在合理范围如[0,1]或标准化后。训练Loss不下降学习率过低、模型架构错误、数据标签错误、优化器问题1. 增大学习率。2. 用一个极小的数据集如每个类别5张图过拟合测试如果连训练集都无法拟合loss不降说明模型或代码有根本性错误。3. 检查损失函数输入模型输出和标签的形状和数据类型是否正确。4. 打印模型前向传播的输出看看是否都是零或极小值。验证准确率远低于训练准确率过拟合模型复杂度过高、训练数据不足、正则化不足1. 增强数据增强。2. 增加Dropout层如果在全连接层后。3. 增大权重衰减weight_decay。4. 使用更早的停止点早停。5. 尝试更简单的模型如ResNet-18换成更小的网络。训练和验证准确率都低欠拟合模型能力不足、训练时间不够、数据增强过强、特征提取层冻结过多1. 增加训练轮数。2. 减弱数据增强。3. 解冻更多预训练层进行微调。4. 尝试更复杂的模型但需警惕过拟合。5. 检查数据预处理和标签是否正确。GPU内存溢出CUDA out of memorybatch_size太大、模型太大、中间变量未释放1. 减小batch_size。2. 使用梯度累积Gradient Accumulation每N个小batch才更新一次权重模拟大batch效果。3. 在验证和推理时使用torch.no_grad()。4. 将不需要的张量及时移出GPU.cpu()或使用del释放。7.3 梯度累积技巧当GPU显存不足以支撑理想的batch_size时梯度累积是一个非常有用的技巧。它允许你使用较小的实际batch进行前向和反向传播但多次累积梯度后再进行一次参数更新等效于使用了更大的batch。accumulation_steps 4 # 累积4步等效batch_size扩大4倍 optimizer.zero_grad() # 在epoch开始时清零梯度 for i, (inputs, labels) in enumerate(train_loader): inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) loss loss / accumulation_steps # 将损失按累积步数缩放 loss.backward() # 梯度累积 if (i 1) % accumulation_steps 0: optimizer.step() # 执行优化步骤 optimizer.zero_grad() # 清零梯度为下一轮累积做准备通过这样一个从数据准备、模型构建、训练调试到结果提交的完整闭环解析你不仅能够顺利完成HW03更能建立起解决一个真实世界机器学习问题的系统性思维和实战能力。记住代码只是工具理解其背后的设计决策和问题排查思路才是从作业走向项目、从学习走向应用的关键。