1. 简介 & 数据集介绍

该实验先调用官方的VGG-16网络框架对模型进行训练,测试准确率仅有%;之后手动搭建VGG-16网络框架使测试准确率达到%。

VGG-16是由牛津大学视觉几何组于2014年提出的深度卷积神经网络,凭借其16层(13个卷积层和3个全连接层)的深层结构在ImageNet竞赛中脱颖而出。其核心设计特色在于统一使用3x3小卷积核通过堆叠来增强非线性表达并减少参数,配合ReLU激活函数和最大池化层,实现了对大规模图像特征的高效提取与分类。

数据集包含17个明星,其中每个明星有100张照片

2. 环境

  • 语言环境:Python 3.12.7
  • 编译器:Jupyter Notebook
  • 深度学习环境:torch—2.8.0 + cu126 / torchvision—0.23.1+cu126

3. 代码实现

3.1 前期准备

3.1.1 设置GPU & 导入库

import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets
import os,PIL,pathlib,warnings,random
from torchvision.models import vgg16
import copy
import matplotlib.pyplot as plt
import warnings
from datetime import datetime
from PIL import Image

warnings.filterwarnings("ignore")
plt.rcParams['font.sans-serif']    = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['figure.dpi']         = 100

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

在这里插入图片描述

3.1.2 标签查看

data_dir = './Data/48-data/'
data_dir = pathlib.Path(data_dir)

data_paths  = list(data_dir.glob('*'))
classeNames = [str(path).split("\\")[2] for path in data_paths]
classeNames

在这里插入图片描述

3.2 VGG-16 模型

3.2.1 图片转换读取

train_transforms = transforms.Compose([
    transforms.Resize([224, 224]), 
    transforms.RandomHorizontalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

total_data = datasets.ImageFolder("./Data/48-data/",transform=train_transforms)
total_data

在这里插入图片描述

total_data.class_to_idx

在这里插入图片描述

train_size = int(0.8 * len(total_data))
test_size  = len(total_data) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
train_dataset, test_dataset

在这里插入图片描述

3.2.2 数据集划分

batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=1)
test_dl = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=1)
for X, y in test_dl:
    print("Shape of X [N, C, H, W]: ", X.shape)
    print("Shape of y: ", y.shape, y.dtype)
    break

在这里插入图片描述

3.2.3 VGG-16 模型建立

# VGG-16模型
print("Using {} device".format(device))

model = vgg16(pretrained = True).to(device)

for param in model.parameters():
    param.requires_grad = False 
    
model.classifier._modules['6'] = nn.Linear(4096,len(classeNames))
model.to(device)  
model

在这里插入图片描述

3.2.4 训练 & 测试函数

# 训练循环
def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    train_loss, train_acc = 0, 0 
    
    for X, y in dataloader: 
        X, y = X.to(device), y.to(device)
        pred = model(X)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        train_acc  += (pred.argmax(1) == y).type(torch.float).sum().item()
        train_loss += loss.item()
            
    train_acc  /= size
    train_loss /= num_batches

    return train_acc, train_loss

# 测试循环
def test (dataloader, model, loss_fn):
    size        = len(dataloader.dataset)
    num_batches = len(dataloader)
    test_loss, test_acc = 0, 0

    with torch.no_grad():
        for imgs, target in dataloader:
            imgs, target = imgs.to(device), target.to(device)
            
            target_pred = model(imgs)
            loss        = loss_fn(target_pred, target)
            
            test_loss += loss.item()
            test_acc  += (target_pred.argmax(1) == target).type(torch.float).sum().item()

    test_acc  /= size
    test_loss /= num_batches

    return test_acc, test_loss

3.2.5 参数设定 & 正式训练

learn_rate = 1e-4 # 初始学习率

# 调用官方动态学习率接口
lambda1 = lambda epoch: 0.92 ** (epoch // 4)
optimizer = torch.optim.SGD(model.parameters(), lr=learn_rate)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda1)

loss_fn    = nn.CrossEntropyLoss()
epochs     = 40

train_loss = []
train_acc  = []
test_loss  = []
test_acc   = []

best_acc = 0

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

    if epoch_test_acc > best_acc:
        best_acc   = epoch_test_acc
        best_model = copy.deepcopy(model)
    
    train_acc.append(epoch_train_acc)
    train_loss.append(epoch_train_loss)
    test_acc.append(epoch_test_acc)
    test_loss.append(epoch_test_loss)

    lr = optimizer.state_dict()['param_groups'][0]['lr']
    
    template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
    print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, 
                          epoch_test_acc*100, epoch_test_loss, lr))
    
# 保存最佳模型
PATH = './best_model6.pth'
torch.save(best_model.state_dict(), PATH)

print('Done')

在这里插入图片描述

3.2.6 结果可视化

current_time = datetime.now()

epochs_range = range(epochs)

plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)

plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(current_time)

plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

在这里插入图片描述

3.2.7 预测指定本地照片

classes = list(total_data.class_to_idx)

def predict_one_image(image_path, model, transform, classes):
    test_img = Image.open(image_path).convert('RGB')
    plt.imshow(test_img)

    test_img = transform(test_img)
    img = test_img.to(device).unsqueeze(0)
    
    model.eval()
    output = model(img)

    _,pred = torch.max(output,1)
    pred_class = classes[pred]
    print(f'预测结果是:{pred_class}')

# 预测训练集中的某张照片
predict_one_image(image_path='./Data/48-data/Johnny Depp/003_64926b97.jpg', model=model, transform=train_transforms, classes=classes)

在这里插入图片描述
预测结果有误

# 模型评估
best_model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, best_model, loss_fn)
epoch_test_acc, epoch_test_loss

在这里插入图片描述

# 查看是否与我们记录的最高准确率一致
epoch_test_acc

在这里插入图片描述

4. 代码优化

4.1 问题分析

4.1.1 参数冻结过度

冻结所有特征层(for param in model.parameters(): param.requires_grad = False),只训练分类器最后一层。这适合简单任务,但对于面部识别(ImageNet预训练 vs 特定名人数据集),高层特征需细调。结果:模型无法适应新数据,欠拟合,准确率卡在20%。

4.1.2 数据增强不足

变换仅包括Resize、RandomHorizontalFlip、ToTensor和Normalize。没有旋转、颜色抖动等增强。小数据集(1800张,每类~106张)易过拟合,泛化差。

4.1.3 优化器和调度器配置不当

原使用SGD(lr=1e-4),适合大批量但在小数据集收敛慢。
LambdaLR调度器(0.92^(epoch//4))固定衰减,不基于性能(如损失/准确率),可能过早/过晚调整lr,导致训练波动。
更新片段切换到Adam(好主意),但ReduceLROnPlateau配置错误:
mode=‘max’(最大化metrics,如准确率),但step()需传入metrics(e.g., accuracy),而第一次scheduler.step()无参数,引发TypeError。
两次step()调用(一次无参,一次传入准确率),逻辑混乱。patience=3太短,可能导致lr过快衰减。

4.2 解决方案

4.2.1 改进参数冻结

手写VGG-16类,便于控制。加载预训练权重时过滤classifier[6]({k: v for k, v in pretrained_dict.items() if k in model_dict and ‘classifier.6’ not in k}),确保兼容。
部分解冻:冻结前4个卷积块(features[:24]),解冻最后1个(features[24:])和分类器。允许高层特征适应面部数据,同时保留低层通用特征。预期:减少欠拟合,提高准确率20-30%。

# Manual implementation of VGG-16
class VGG16(nn.Module):
    def __init__(self, num_classes=1000):
        super(VGG16, self).__init__()
        self.features = nn.Sequential(
            # Conv Block 1
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Conv Block 2
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Conv Block 3
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Conv Block 4
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            
            # Conv Block 5
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

# Initialize model
model = VGG16(num_classes=num_classes).to(device)

# Load pretrained weights from torchvision (adapt to our manual model)
from torchvision.models import vgg16
pretrained_model = vgg16(pretrained=True)
pretrained_dict = pretrained_model.state_dict()

# Filter out the classifier weights since dimensions differ
model_dict = model.state_dict()
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict and 'classifier.6' not in k}
model_dict.update(pretrained_dict)
model.load_state_dict(model_dict)

# Unfreeze the last two conv blocks for fine-tuning
for param in model.features[:24].parameters():
    param.requires_grad = False
for param in model.features[24:].parameters():
    param.requires_grad = True

4.2.2 增强数据变换

添加RandomRotation(15)和ColorJitter(brightness/contrast/saturation/hue=0.2),增加数据多样性。模拟光照、角度变化,适合面部识别小数据集。预期:降低过拟合,提高泛化5-10%。

# Enhanced data transforms with more augmentation
train_transforms = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(15),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load dataset
total_data = datasets.ImageFolder(data_dir, transform=train_transforms)

4.2.3 优化器和调度器优化

切换到Adam(lr=1e-4, weight_decay=1e-5),自适应调整每个参数的学习率,添加L2正则防止过拟合。比SGD更快收敛。
使用ReduceLROnPlateau(mode=‘min’, factor=0.5, patience=5, verbose=True),基于测试损失(min模式)自动调整lr。只需一次scheduler.step(test_loss),避免TypeError。patience=5更稳健,verbose打印变化。预期:智能衰减lr,稳定训练,避免波动。

# Optimizer and loss
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)
loss_fn = nn.CrossEntropyLoss()

# Learning rate scheduler
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5)

4.2 训练结果与可视化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

更多推荐