You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何改进过拟合的PyTorch黑白图像分类TingVGG模型

问题描述

我正在开展一项4类黑白图像的分类任务,训练集每类包含3000张图像,测试集总计600张图像。目前已完成模型训练,但测试效果不佳,希望了解如何改进该模型。以下是完整代码及训练结果:

数据变换定义

data_transform = transforms.Compose([
    transforms.Grayscale(num_output_channels=1),
    transforms.Resize(size=(150, 150)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5], std=[0.5]),
])

使用ImageFolder加载图像

train_data = datasets.ImageFolder(root=train_dir,
                                  transform=data_transform, # Transform the data
                                  target_transform=None) # Transform the Label

test_data = datasets.ImageFolder(root=test_dir,
                                  transform=data_transform, # Transform the data
                                  target_transform=None) # Transform the Label
train_data, test_data

将数据转换为DataLoader

BATCH_SIZE = 8

train_dataloader = DataLoader(
    dataset= train_data,
    batch_size=BATCH_SIZE, # How many images our model can see at the time
    num_workers=8,  # Number of CPU Cores
    shuffle=True
)

test_dataloader = DataLoader(
    dataset= test_data,
    batch_size=BATCH_SIZE, # How many images our model can see at the time
    num_workers=8,  # Number of CPU Cores
    shuffle=False
)

定义模型类

class TingVGG(nn.Module):
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int) -> None:
        super().__init__()
        self.conv_block1 = nn.Sequential(
            
        nn.Conv2d(in_channels=input_shape,out_channels=hidden_units,kernel_size=3,stride=1,padding=1),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=2, stride=2),
        nn.Conv2d(in_channels=hidden_units, out_channels=hidden_units, kernel_size=3,stride=1,padding=1),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=2, stride=2),
        nn.Conv2d(in_channels=hidden_units, out_channels=hidden_units, kernel_size=3,stride=1,padding=1),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=2, stride=2)
        
        )
               
        self.dropout = nn.Dropout(0.4)
        self.classifier = nn.Sequential(nn.Flatten(), nn.Linear(in_features=hidden_units*18*18 ,out_features=output_shape))
        
        
    def forward(self, x: torch.Tensor):
        x = self.conv_block1(x)
        x = self.dropout(x)
        x = self.classifier(x)
        return x

训练模型

# 设置随机种子
torch.manual_seed(42)
torch.cuda.manual_seed(42)

# 设置训练轮数
NUM_EPOCHS = 10

# 创建并初始化TingVGG模型
model_0 = TingVGG(input_shape=1, # 输入图像的通道数 (c, h, w) -> 3
                  hidden_units=128,
                  output_shape=len(train_data.classes)).to(device)

# 设置损失函数与优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(params= model_0.parameters(),
                             lr= 0.001)

# 启动计时器
start_time = time.time()

# 训练模型0
model_0_results = train(model= model_0,
                        train_dataloader= train_dataloader,
                        test_dataloader= test_dataloader,
                        optimizer= optimizer,
                        loss_fn= loss_fn,
                        epochs= NUM_EPOCHS
                        )

训练输出

10%|█         | 1/10 [02:30<22:37, 150.88s/it]
Epoch: 1 | train_loss: 0.3549 | train_acc: 0.8668 | test_loss: 0.3059 | test_acc: 0.8842
 20%|██        | 2/10 [04:57<19:48, 148.59s/it]
Epoch: 2 | train_loss: 0.1707 | train_acc: 0.9420 | test_loss: 0.2648 | test_acc: 0.9062
 30%|███       | 3/10 [07:24<17:14, 147.83s/it]
Epoch: 3 | train_loss: 0.1153 | train_acc: 0.9627 | test_loss: 0.2790 | test_acc: 0.8962
 40%|████      | 4/10 [09:52<14:46, 147.71s/it]
Epoch: 4 | train_loss: 0.0900 | train_acc: 0.9695 | test_loss: 0.2719 | test_acc: 0.8979
 50%|█████     | 5/10 [12:19<12:18, 147.65s/it]
Epoch: 5 | train_loss: 0.0760 | train_acc: 0.9758 | test_loss: 0.2927 | test_acc: 0.8950
 60%|██████    | 6/10 [14:47<09:50, 147.57s/it]
Epoch: 6 | train_loss: 0.0616 | train_acc: 0.9814 | test_loss: 0.3326 | test_acc: 0.8942
 70%|███████   | 7/10 [17:15<07:23, 147.76s/it]
Epoch: 7 | train_loss: 0.0488 | train_acc: 0.9838 | test_loss: 0.3086 | test_acc: 0.8946
 80%|████████  | 8/10 [19:42<04:55, 147.60s/it]
Epoch: 8 | train_loss: 0.0534 | train_acc: 0.9835 | test_loss: 0.3186 | test_acc: 0.9017
 90%|█████████ | 9/10 [22:10<02:27, 147.66s/it]
Epoch: 9 | train_loss: 0.0422 | train_acc: 0.9878 | test_loss: 0.3317 | test_acc: 0.9012
100%|██████████| 10/10 [24:38<00:00, 147.80s/it]
Epoch: 10 | train_loss: 0.0433 | train_acc: 0.9878 | test_loss: 0.3853 | test_acc: 0.9038

训练与测试的损失及准确率

训练与测试的损失及准确率

混淆矩阵代码

# 导入tqdm用于进度条
from tqdm.auto import tqdm

# 1. 使用训练好的模型进行预测
y_preds = []
model_0.eval()
with torch.inference_mode():
  for X, y in tqdm(test_dataloader, desc="生成预测结果"):
    # 将数据与标签发送到目标设备
    X, y = X.to(device), y.to(device)
    # 前向传播
    y_logit = model_0(X)
    # 将预测结果从logits转为预测概率,再转为预测标签
    y_pred = torch.softmax(y_logit, dim=1).argmax(dim=1)
    # 将预测结果移至CPU用于评估
    y_preds.append(y_pred.cpu())
# 将预测结果列表拼接为张量
y_pred_tensor = torch.cat(y_preds)



from torchmetrics import ConfusionMatrix
from mlxtend.plotting import plot_confusion_matrix

# 2. 创建混淆矩阵实例并对比预测结果与真实标签
confmat = ConfusionMatrix(num_classes=len(class_names), task='multiclass')
confmat_tensor = confmat(preds=y_pred_tensor,
                         target=torch.Tensor(test_data.targets))
# 3. 绘制混淆矩阵
fig, ax = plot_confusion_matrix(
    conf_mat=confmat_tensor.numpy(), # matplotlib偏好使用NumPy格式
    class_names=class_names, # 将行列标签转为类别名称
    figsize=(10, 7)
);

混淆矩阵结果

混淆矩阵


模型改进方案

从训练结果看,训练准确率接近99%但测试准确率仅约90%,且测试损失持续上升,明显存在过拟合问题,结合任务特点,可从以下几个方向优化:

1. 增强数据扩充

当前数据变换仅做了灰度化、Resize和归一化,缺乏数据增强,可添加针对黑白图像的变换:

data_transform = transforms.Compose([
    transforms.Grayscale(num_output_channels=1),
    transforms.Resize(size=(150, 150)),
    # 添加以下增强操作
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomVerticalFlip(p=0.5),
    transforms.RandomRotation(degrees=15),
    transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
    transforms.ColorJitter(brightness=0.2, contrast=0.2), # 针对灰度图的亮度对比度调整
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5], std=[0.5]),
])

注意:测试集不要使用随机增强,保持原有变换即可。

2. 调整模型结构与正则化

(1)增加卷积块深度或调整通道数

当前模型只有一个卷积块,可参考VGG结构增加卷积块,同时调整通道数逐步提升:

class TingVGG(nn.Module):
    def __init__(self, input_shape: int, hidden_units: int, output_shape: int) -> None:
        super().__init__()
        self.conv_block1 = nn.Sequential(
            nn.Conv2d(input_shape, hidden_units, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(hidden_units, hidden_units, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )
        self.conv_block2 = nn.Sequential(
            nn.Conv2d(hidden_units, hidden_units*2, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(hidden_units*2, hidden_units*2, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )
        self.conv_block3 = nn.Sequential(
            nn.Conv2d(hidden_units*2, hidden_units*4, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(hidden_units*4, hidden_units*4, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )
        # 在卷积块后添加BatchNorm
        self.bn1 = nn.BatchNorm2d(hidden_units)
        self.bn2 = nn.BatchNorm2d(hidden_units*2)
        self.bn3 = nn.BatchNorm2d(hidden_units*4)
        
        self.dropout = nn.Dropout(0.5)
        # 重新计算分类器输入特征数(根据新的卷积块输出尺寸)
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(hidden_units*4*18*18, 512), # 需根据实际输出尺寸调整
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(512, output_shape)
        )
        
    def forward(self, x: torch.Tensor):
        x = self.conv_block1(x)
        x = self.bn1(x)
        x = self.conv_block2(x)
        x = self.bn2(x)
        x = self.conv_block3(x)
        x = self.bn3(x)
        x = self.dropout(x)
        x = self.classifier(x)
        return x

(2)添加更多正则化手段

  • 提升Dropout率至0.5,同时在分类器中增加Dropout层
  • 对卷积层和全连接层添加L2正则化(在优化器中设置weight_decay):
optimizer = torch.optim.Adam(params=model_0.parameters(), lr=0.001, weight_decay=1e-4)

3. 调整训练策略

(1)使用学习率调度器

随着训练轮数增加,降低学习率,帮助模型收敛到更优的局部最小值:

from torch.optim.lr_scheduler import StepLR
scheduler = StepLR(optimizer, step_size=3, gamma=0.1)

# 在训练循环中添加:
for epoch in range(NUM_EPOCHS):
    # 训练步骤...
    scheduler.step()

(2)提前停止训练

当测试损失连续多个epoch不再下降甚至上升时,停止训练,避免过拟合:

best_test_loss = float('inf')
patience = 3
patience_counter = 0

for epoch in range(NUM_EPOCHS):
    # 训练与验证步骤,得到当前test_loss
    if test_loss < best_test_loss:
        best_test_loss = test_loss
        patience_counter = 0
        torch.save(model_0.state_dict(), 'best_model.pth')
    else:
        patience_counter +=1
        if patience_counter >= patience:
            print("提前停止训练")
            break

(3)调整批次大小

当前BATCH_SIZE=8过小,可尝试提升至16或32,让模型每次看到更多样本,稳定梯度更新。

4. 利用预训练模型迁移学习

对于图像分类任务,预训练模型在通用图像特征提取上表现更优,可针对黑白图像调整预训练模型:

from torchvision import models

# 加载预训练的VGG16,修改输入通道为1
model = models.vgg16(pretrained=True)
# 修改第一个卷积层的输入通道
model.features[0] = nn.Conv2d(1, 64, kernel_size=(3,3), stride=(1,1), padding=(1,1))
# 替换分类器头部
num_ftrs = model.classifier[6].in_features
model.classifier[6] = nn.Linear(num_ftrs, len(train_data.classes))
model = model.to(device)

冻结部分底层卷积层,仅训练顶层和分类器,减少计算量并避免过拟合。

5. 分析混淆矩阵

从混淆矩阵结果看,重点关注错误率高的类别,收集这些类别的样本,分析是否存在标注错误、样本特征不明显等问题,针对性补充数据或调整数据增强策略。


内容的提问来源于stack exchange,提问作者Emad Younan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 02:59:52