作业 HW03 全面讲解:卷积神经网络图像分类

你好!这个作业的目标是构建一个卷积神经网络(CNN) 来对食物图片进行分类(共 11 类)。你已经有了一定的理论知识,但对 PyTorch 和具体实现还不太熟悉。下面我会从头开始,逐模块讲解这份代码,并解释背后的关键概念。

1. 作业总体要求

  • Easy(2分):跑通基础 CNN 模型,达到基本准确率。
  • Medium(2分):改进模型结构或使用数据增强,提升性能。
  • Hard(2分):利用提供的无标签数据做半监督学习,进一步提升。

重要限制:不允许使用预训练模型(如 ResNet 的 pretrained=True 是不允许的),也不允许使用外部数据或测试集标签。

2. 数据集介绍

数据集是 food-11,共 11 类食物图片。数据已经划分好:

  • food-11/training/labeled:有标签的训练图片(用于监督学习)
  • food-11/training/unlabeled:无标签的图片(用于半监督学习,Hard 难度)
  • food-11/validation:验证集(有标签,用于调参和监控过拟合)
  • food-11/testing:测试集(无标签,用于最终提交)

每张图片都是 RGB 彩色图,原始尺寸不一,代码中会统一缩放到 128x128

3. 核心概念速览(适合初学者)

在深入代码前,先简单说明几个必备概念:

  • 张量 (Tensor):PyTorch 中的基本数据单位,类似多维数组。图片会变成形状为 (batch_size, channels, height, width) 的张量。
  • 卷积层 (Conv2d):用可学习的卷积核扫描图片,提取局部特征(边缘、纹理等)。
  • 池化层 (MaxPool2d):下采样,减小特征图尺寸,增加感受野,同时防止过拟合。
  • 全连接层 (Linear):将提取到的特征映射到类别分数(logits)。
  • 激活函数 (ReLU):引入非线性,让网络能学习复杂模式。
  • 损失函数 (CrossEntropyLoss):衡量预测和真实标签的差距,包含 softmax 和交叉熵。
  • 优化器 (Adam):根据梯度更新网络参数,减小损失。
  • DataLoader:批量加载数据,支持多线程、打乱顺序等。

4. 代码逐块讲解

4.1 下载与解压数据
!gdown --id '1awF7pZ9Dz7X1jn1_QAiKN-_v56veCEKy' --output food-11.zip
!unzip -q food-11.zip
  • ! 表示在 Jupyter/Colab 中执行系统命令。
  • gdown 从 Google Drive 下载文件。
  • 解压后会得到 food-11/ 文件夹。
4.2 导入所需库
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, DatasetFolder
from PIL import Image
  • torch:PyTorch 核心。
  • torch.nn:神经网络层、损失函数等。
  • transforms:图像预处理操作(缩放、旋转、转张量等)。
  • DataLoader:批量迭代器。
  • DatasetFolder:适合按文件夹组织类别的数据集(每个子文件夹名是类别)。
  • PIL:读取图片。
4.3 数据预处理(Transforms)
train_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
])
test_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
])
  • transforms.Compose 将多个变换串联。
  • Resize:统一尺寸为 128×128。
  • ToTensor():将 PIL 图片(像素值 0~255)转为张量(形状 C×H×W,值范围 0~1)。必须放在最后

Medium 改进点:可以在 train_tfm 中添加数据增强,例如随机水平翻转、旋转、颜色抖动等,能有效提升泛化能力。

4.4 构建 Dataset 和 DataLoader
train_set = DatasetFolder("food-11/training/labeled", loader=lambda x: Image.open(x), extensions="jpg", transform=train_tfm)
valid_set = DatasetFolder("food-11/validation", ...)
unlabeled_set = DatasetFolder("food-11/training/unlabeled", ...)
test_set = DatasetFolder("food-11/testing", ...)

train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2)
valid_loader = DataLoader(valid_set, batch_size=128, shuffle=True, num_workers=2)
test_loader = DataLoader(test_set, batch_size=128, shuffle=False)
  • DatasetFolder 会自动扫描文件夹,将每个图片文件路径和其所在的子文件夹名(类别)对应起来。类别被自动编码为 0~10 的整数。
  • loader 参数定义如何读取图片文件:这里用 Image.open
  • batch_size:每次输入模型多少张图片。越大 GPU 内存占用越多,但梯度更稳定。
  • shuffle=True:训练时打乱顺序,避免模型记住顺序。
  • num_workers:用几个子进程加载数据,加快速度。
4.5 模型定义(Classifier 类)
class Classifier(nn.Module):
    def __init__(self):
        # 卷积部分
        self.cnn_layers = nn.Sequential(
            nn.Conv2d(3, 64, 3, 1, 1),   # 输入3通道,输出64通道,卷积核3x3
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 尺寸减半
            # ... 后续类似
        )
        # 全连接部分
        self.fc_layers = nn.Sequential(
            nn.Linear(256 * 8 * 8, 256),
            nn.ReLU(),
            nn.Linear(256, 11)           # 最终输出11类
        )

计算特征图尺寸的公式(重要):

  • 输入 (3, 128, 128)
  • Conv2d: 卷积后尺寸不变(padding=1, stride=1)→ (64, 128, 128)
  • MaxPool2d(2,2): 尺寸减半 → (64, 64, 64)
  • 第二组 Conv→(128,64,64),MaxPool→(128,32,32)
  • 第三组 Conv→(256,32,32),MaxPool(4,4)→(256,8,8)
  • 最后展平为 256×8×8 = 16384,再送入全连接层。

注意:如果修改了卷积层或者输入尺寸,必须重新计算展平后的维度,否则 nn.Linear 会报错。

Medium 改进点

  • 增加/减少卷积层数。
  • 使用更深的结构(但不要用预训练模型)。
  • 加入 Dropout 防止过拟合。
  • 调整卷积核数量、大小。
4.6 训练循环
device = "cuda" if torch.cuda.is_available() else "cpu"
model = Classifier().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
n_epochs = 80
  • device:自动选择 GPU(如果可用)否则 CPU。
  • CrossEntropyLoss:内部先做 softmax 再计算交叉熵,所以模型最后一层不用加 softmax。
  • Adam 优化器:lr 学习率,weight_decay 正则化。
  • epoch:所有训练数据过一遍模型。

训练循环结构(每个 epoch):

  1. model.train():启用训练模式(BatchNorm 和 Dropout 生效)。
  2. 遍历 train_loader 的每个 batch:
    • 将图片和标签移到 device
    • logits = model(imgs)
    • 计算 loss
    • optimizer.zero_grad() 清空旧梯度
    • loss.backward() 反向传播计算梯度
    • clip_grad_norm_ 梯度裁剪,避免梯度爆炸
    • optimizer.step() 更新参数
    • 计算 batch 准确率并记录
  3. 计算平均 loss 和准确率,打印。
  4. model.eval():评估模式(不启用 Dropout,BatchNorm 使用全局统计)。
  5. torch.no_grad() 禁用梯度计算(节省内存/加速),在验证集上计算 loss 和准确率。

为什么要有验证集? 检查模型是否过拟合(训练准确率远高于验证准确率)或欠拟合(两者都低)。可以根据验证准确率调整超参数或提前停止。

4.7 测试与生成提交文件
model.eval()
predictions = []
for imgs, _ in test_loader:
    with torch.no_grad():
        logits = model(imgs.to(device))
    pred = logits.argmax(dim=-1)   # 取概率最大的类别索引
    predictions.extend(pred.cpu().numpy().tolist())
  • 测试集没有真实标签,所以 _ 占位。
  • 收集所有预测后,写入 predict.csv
with open("predict.csv", "w") as f:
    f.write("Id,Category\n")
    for i, pred in enumerate(predictions):
        f.write(f"{i},{pred}\n")
  • 格式要求:第一行是 Id,Category,每行对应测试集图片的顺序(从 0 开始)。
4.8 半监督学习部分(Hard 难度)

代码中有一个 get_pseudo_labels 函数目前是空的,需要你实现。原理

  • 使用当前训练好的模型对无标签数据进行预测。
  • 对于预测置信度(softmax 输出的最大概率)超过某个阈值(如 0.65)的样本,将其伪标签当作真实标签,与原始有标签数据合并,一起训练。

实现思路

def get_pseudo_labels(dataset, model, threshold=0.65):
    model.eval()
    softmax = nn.Softmax(dim=-1)
    pseudo_data = []  # 存放 (图像路径, 伪标签)
    dataloader = DataLoader(dataset, batch_size=64, shuffle=False, num_workers=2)
    for imgs, _ in tqdm(dataloader):
        with torch.no_grad():
            logits = model(imgs.to(device))
            probs = softmax(logits)
            max_probs, preds = torch.max(probs, dim=1)
        # 筛选置信度高于阈值的样本
        mask = max_probs >= threshold
        # 将符合的样本和伪标签构建成新的 Dataset
        # 注意:需要将图像数据及其伪标签打包
    # 返回一个 DatasetFolder 或自定义 Dataset

实际操作中,由于 DatasetFolder 期望从文件夹读取,一种简单方法是:

  • 遍历原无标签数据集,把高置信度的图片复制到一个临时目录,并按伪标签的类别放入子文件夹。
  • 再用 DatasetFolder 加载这个临时目录。
  • 但更高效的做法是自定义 Dataset 类,直接返回图片和伪标签。

训练时使用:在每个 epoch 开始前调用 get_pseudo_labels 生成伪标签数据集,然后与原始训练集拼接:

concat_dataset = ConcatDataset([train_set, pseudo_set])
train_loader = DataLoader(concat_dataset, batch_size=128, shuffle=True)

注意事项

  • 阈值不宜过低,否则噪声太多;也不宜过高,否则获得太少伪标签。可以尝试 0.7~0.9。
  • 最好在模型训练几个 epoch 之后(准确率较高时)再开始生成伪标签。
  • 伪标签数据集大小会随着训练逐渐增加,可以更新数据加载器。

5. 如何逐步完成作业(从 Easy 到 Hard)

Easy:跑通 baseline
  • 直接运行给出的完整代码(补全缺失的 !unzip 等命令)。
  • 不需要修改任何结构,只需要理解流程。
  • 期望验证准确率大约在 0.5~0.6 左右(取决于随机种子)。
Medium:改进模型或数据增强
  • 数据增强:在 train_tfm 中添加:
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(15),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    
    注意:测试时不要用这些增强。
  • 模型改进
    • 加深网络(增加 Conv2d 层)。
    • 调整卷积核数量(如 64→128→256→512)。
    • 加入 Dropout (nn.Dropout(p=0.5)) 在全连接层之前。
    • 使用 BatchNorm 帮助收敛。
  • 超参数调整:学习率调度(如 torch.optim.lr_scheduler.ReduceLROnPlateau)。
  • 目标:验证准确率提升到 0.7 以上。
Hard:半监督学习
  • 实现 get_pseudo_labels 函数。
  • 设置 do_semi = True
  • 调整阈值、起始 epoch(例如前 20 个 epoch 不做 pseudo-label,之后每 5 个 epoch 重新生成一次)。
  • 目标:利用无标签数据进一步提升准确率(可能需要训练更久,例如 120 个 epoch)。

6. 常见问题与调试技巧

  1. 维度不匹配错误:检查 nn.Linear 的输入维度是否等于卷积输出展平后的尺寸。可以在 forward 中打印 x.shape 来调试。
  2. 训练 loss 不下降
    • 学习率太大或太小(尝试 1e-3, 1e-4)。
    • 数据预处理错误(例如忘记 ToTensor,或者归一化不当)。
    • 模型没有正确移到 GPU。
  3. 过拟合:训练准确率很高,验证准确率低。解决:增加 Dropout、数据增强、减小模型容量、权重衰减。
  4. 显存不足:减小 batch_size 或图片尺寸(如 96x96),或者减小模型通道数。

7. 推荐的学习路径

  1. 先跑通 Easy:确保能生成 predict.csv 并提交(如果课程有 Kaggle 页面)。
  2. 理解每个函数:尝试修改一个参数,观察结果变化。
  3. 实现 Medium:加上数据增强,观察验证准确率提升。
  4. 挑战 Hard:实现伪标签,体会半监督学习的威力。
  5. 查阅文档:遇到陌生 API 时,去 PyTorch 官方文档 看例子。

8. 最后叮嘱

  • 不要作弊:使用预训练模型或测试集信息会被严惩。
  • 善用 Colab:免费 GPU 可以大幅加速训练。
  • 多打印信息:在训练循环中加入 print(f'Epoch {epoch}, batch {i}') 等,有助于定位卡死位置。
  • 保存模型:训练过程中可以保存验证准确率最高的模型:
    torch.save(model.state_dict(), "best_model.pt")
    
    测试时加载这个模型。

希望这份讲解能帮你顺利入门 PyTorch 并完成作业!如果你对某个具体部分(例如卷积尺寸计算、如何实现伪标签加载)还有疑问,可以继续追问,我会给出更详细的示例代码。加油!

实际作业

参考:
https://blog.csdn.net/loco_monkey/article/details/125565805?ops_request_misc=elastic_search_misc&request_id=d5942a9f4d08b7a065ecdece39c11f73&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-125565805-null-null.142v102pc_search_result_base1&utm_term=%E6%9D%8E%E5%AE%8F%E6%AF%85%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0hw3&spm=1018.2226.3001.4187

1、手动下载文件(fq进行):

https://mega.nz/#!zt1TTIhK!ZuMbg5ZjGWzWX1I6nEUbfjMZgCmAgeqJlwDkqdIryfg
在这里插入图片描述

2、在colab上面执行

本地电脑跑不起来

问题

1、DataLoader警告由无GPU引起,设置 num_workers=0, pin_memory=False 即可。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐