👉声明

  • 本文为 365 天深度学习训练营 内部参考文章的个人学习记录,代码与实验在本地完成。
  • 数据集来源训练营提供的 48-data.zip(本地解压/整理为 6-data/);数据集请勿对外公开传播。
  • 原作者:K同学啊
  • 本篇为个人在 P6 关卡上的学习与实践记录

第 P6 周:VGG-16 算法 — PyTorch 实现人脸识别

🏡 我的环境(参考)

  • 语言:Python 3.8+ / 3.11(以本机为准)
  • 编译器:Jupyter Lab
  • 深度学习:PyTorch、torchvision(需支持 torchvision.models.vgg16
  • 数据:6-data/(由训练营人脸数据整理而来,目录内为「每位名人一个子文件夹」)

🍺 本周要求

  • 保存训练过程中测试集上最佳模型权重(state_dict)。
  • 调用官方 VGG-16torchvision.models.vgg16),并将分类头改为本数据集类别数。

🍻 拔高(可选)

  • 将测试集准确率推到约 60%(小数据多分类 + 从头训练 VGG 往往只在十几个百分点,更现实的做法是使用 ImageNet 预训练权重 + 数据增强 + 微调,见下文与 p6.ipynb)。
  • 手动搭建 VGG-16 框架(与预训练对照),notebook 第八节附有 ManualVGG16 示例。

一、本周学习内容概览

本周任务是在人脸图像多分类数据上,使用 VGG-16 完成特征提取与分类:

  • 数据6-data/ 下按人物姓名为子文件夹(如 Brad Pitt/Tom Hanks/ 等),共 17 类;当前样本量在各类间大致平衡(多数类 100 张,个别类略多)。
  • 划分:无现成 train/test 子目录时,在代码中对全部样本做 8:2 分层随机划分(优先 sklearn.model_selection.train_test_split(..., stratify=)),训练集使用 Resize + RandomCrop + 水平翻转,测试集仅 Resize 到 224,再配合 ImageNet 均值方差标准化。
  • 模型:使用 官方 vgg16,加载 ImageNet 预训练权重(兼容 VGG16_Weights.IMAGENET1K_V1 与旧版 pretrained=True),将 classifier 最后一层改为 Linear(..., num_classes)
  • 优化CrossEntropyLoss + SGD(momentum=0.9, weight_decay=1e-4),初始学习率 1e-4;动态学习率采用 LambdaLRlambda epoch: 0.92 ** (epoch // 4)
  • 最佳权重:每个 epoch 在测试集上评估,若优于历史最佳则保存为 best_model_p6.pth

具体 Loss/Accuracy 曲线与最终 Best Test Acc 以本机运行 p6.ipynb 为准。


二、必要代码与关键点说明

1. 设备

import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device

2. 数据:按索引划分 + 不同变换

核心思路:ImageFolder 根目录读入后得到 base_dataset.samples / targets,对索引做划分,再用包装类在 __getitem__ 里施加 train_transformstest_transform(避免「训练集和测试集共用一种增强」)。

分层划分示例:

from pathlib import Path
from torchvision import datasets, transforms
import numpy as np

try:
    from sklearn.model_selection import train_test_split
except ImportError:
    train_test_split = None

data_root = Path("6-data")
base_dataset = datasets.ImageFolder(str(data_root), transform=None)
num_classes = len(base_dataset.classes)

train_transforms = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.RandomCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
test_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]),
])

IndexedImageFolder 详见 p6.ipynb 数据单元。

3. 官方 VGG-16 与替换分类头

import torch.nn as nn
import torchvision


def build_vgg16(num_classes: int, pretrained: bool = True):
    model = None
    if pretrained:
        try:
            from torchvision.models import vgg16, VGG16_Weights
            model = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
        except Exception:
            model = torchvision.models.vgg16(pretrained=True)
    else:
        try:
            from torchvision.models import vgg16
            model = vgg16(weights=None)
        except Exception:
            model = torchvision.models.vgg16(pretrained=False)
    in_features = model.classifier[-1].in_features
    model.classifier[-1] = nn.Linear(in_features, num_classes)
    return model

4. 训练与测试(单份定义即可)

训练函数内在 model.train() 下前向与反传;测试在 model.eval() + torch.no_grad() 下只做前向。

请添加图片描述

5. 动态学习率(LambdaLR)

learn_rate = 1e-4
optimizer = torch.optim.SGD(model.parameters(), lr=learn_rate, momentum=0.9, weight_decay=1e-4)
lambda1 = lambda epoch: 0.92 ** (epoch // 4)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda1)

每个 epoch 推荐顺序:train → test → scheduler.step()

6. 正式训练并保存最佳权重

epochs = 40
best_acc = 0.0
best_path = "best_model_p6.pth"
train_loss, train_acc = [], []
test_loss, test_acc = [], []

for epoch in range(epochs):
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
    epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
    scheduler.step()

    train_acc.append(epoch_train_acc)
    train_loss.append(epoch_train_loss)
    test_acc.append(epoch_test_acc)
    test_loss.append(epoch_test_loss)

    if epoch_test_acc > best_acc:
        best_acc = epoch_test_acc
        torch.save(model.state_dict(), best_path)

    lr = optimizer.param_groups[0]["lr"]
    print(
        f"Epoch:{epoch+1:2d}, Train_acc:{epoch_train_acc*100:.1f}%, Train_loss:{epoch_train_loss:.3f}, "
        f"Test_acc:{epoch_test_acc*100:.1f}%, Test_loss:{epoch_test_loss:.3f}, Lr:{lr:.2E}"
    )

print("Done")
print(f"Best Test Acc: {best_acc*100:.2f}% | Saved to: {best_path}")

与 TensorFlow 的 ModelCheckpoint 类似之处:都按验证指标保留最优;PyTorch 更常见的是只存 state_dict 而非整图+优化器(除非要做断点续训)。


三、结果可视化

使用 matplotlib 绘制 train_acc / test_acc 与对应 Loss,横轴为 epoch;若在 WSL/Linux 下没有 SimHei,中文可能显示为方框,可安装字体或改用英文字样。


四、指定图片预测与加载最佳模型

  • 预测:对 test_dataset 随机下标取 (tensor, label),与训练一致的通道顺序在 Tensor 上 unsqueeze(0)model(x)
  • 加载build_vgg16(num_classes, pretrained=False) 搭建同结构网络,再 load_state_dict(torch.load(best_path, map_location=device)),最后在测试集上复算准确率以确认与训练时最佳 epoch 一致。

五、关于「测试集 60%」与原文 ~17% 现象

训练营原文在学习率偏小、增强不足、且相当于从零训练大型网络时,容易出现 Train/Test 只有约百分之十几 的情况。要在人脸多分类上达到 ~60% 测试准确率,实践中通常需要:

  • ImageNet 预训练 backbone + 仅微调分类头或全流程小学习率微调;
  • 训练端增强(随机裁剪、翻转等)与适当 weight decay
  • 若仍不足:可尝试 先冻结 features 若干 epoch 再解冻AdamW、更长 epochs、或更强的随机增强策略。

若故意将 USE_PRETRAINED = False 做对照实验,则更容易复现「低准确率、难收敛」的现象,这与理论预期一致。


六、个人总结

本周系统过了一遍 VGG-16 的结构特点(重复 3×3 卷积堆叠、池化降采样、全连接分类头),并在真实人脸数据上完成 PyTorch 训练闭环:划分数据、DataLoader、官方模型替换最后一层、LambdaLR按测试集保存最佳 best_model_p6.pth。与 P4/P5 相比,人脸识别类别更多,更依赖预训练与正则;同时再次体会到 路径与 num_classes 必须自数据集自动推断,避免 Linear(..., 0) 引发 GPU 侧断言类错误。后续若冲更高准确率,会在当前 p6.ipynb 基础上尝试两阶段冻结/解冻与更细的学习率策略。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐