HW3 CNN
作业 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):
model.train():启用训练模式(BatchNorm 和 Dropout 生效)。- 遍历
train_loader的每个 batch:- 将图片和标签移到 device
logits = model(imgs)- 计算 loss
optimizer.zero_grad()清空旧梯度loss.backward()反向传播计算梯度clip_grad_norm_梯度裁剪,避免梯度爆炸optimizer.step()更新参数- 计算 batch 准确率并记录
- 计算平均 loss 和准确率,打印。
model.eval():评估模式(不启用 Dropout,BatchNorm 使用全局统计)。- 用
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. 常见问题与调试技巧
- 维度不匹配错误:检查
nn.Linear的输入维度是否等于卷积输出展平后的尺寸。可以在 forward 中打印x.shape来调试。 - 训练 loss 不下降:
- 学习率太大或太小(尝试 1e-3, 1e-4)。
- 数据预处理错误(例如忘记 ToTensor,或者归一化不当)。
- 模型没有正确移到 GPU。
- 过拟合:训练准确率很高,验证准确率低。解决:增加 Dropout、数据增强、减小模型容量、权重衰减。
- 显存不足:减小
batch_size或图片尺寸(如 96x96),或者减小模型通道数。
7. 推荐的学习路径
- 先跑通 Easy:确保能生成
predict.csv并提交(如果课程有 Kaggle 页面)。 - 理解每个函数:尝试修改一个参数,观察结果变化。
- 实现 Medium:加上数据增强,观察验证准确率提升。
- 挑战 Hard:实现伪标签,体会半监督学习的威力。
- 查阅文档:遇到陌生 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 即可。
更多推荐



所有评论(0)