第 P4 周:猴痘病皮肤图像识别


一、本周学习内容概览

在前面的关卡中,我们分别用 MNIST、CIFAR‑10 等「官方内置」数据集完成了手写数字和彩色图片分类。本周 P4 的任务进一步贴近真实场景:
使用公开的「Monkeypox vs Others」皮肤病图像数据集,搭建卷积神经网络完成二分类识别,并在 GPU 上训练与评估模型。

我这次主要做了以下几件事:

  • 数据组织:在 4-data/ 目录下整理两类皮肤图像,按 4-data/Monkeypox/*.jpg4-data/Others/*.jpg 的方式存放,方便 ImageFolder 自动识别类别。
  • 数据加载:使用 pathlib.Path 处理路径(避免 split("\\") 在 Linux 下引发的 bug),配合 datasets.ImageFolder 完成猴痘/其他皮肤病图像的读取。
  • 数据预处理:将图片统一缩放到 224×224,并按照 ImageNet 常用的 mean/std 对 RGB 通道做标准化。
  • 划分数据集:把完整数据集按 8:2 比例随机划分为训练集和测试集,并用 DataLoader 组成 batch。
  • 模型构建:实现了带有 BatchNorm 的卷积神经网络 Network_bn,网络包含多层卷积、批归一化和池化层,最后通过全连接层输出 2 维 logits。
  • 训练与评估:编写 train / test 函数,记录每个 epoch 的训练/测试准确率与损失;在 20 个 epoch 后,训练集准确率达到约 92.7%,测试集准确率约为 83.2%。

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

1. 环境与设备检查

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from pathlib import Path
import matplotlib.pyplot as plt

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

print("我的环境:")
print("Python版本:", sys.version.split()[0])
print("PyTorch版本:", torch.__version__)
print("Torchvision版本:", torchvision.__version__)
print("CUDA:", torch.version.cuda if torch.cuda.is_available() else "未使用(CPU 模式)")
if torch.cuda.is_available():
    print("GPU设备:", torch.cuda.get_device_name(0))
else:
    print("GPU设备: 无(当前使用 CPU)")
print("-" * 40)

2. 数据集目录结构与路径处理

本次任务的数据集解压在 p4/4-data/ 目录下,结构大致如下:

4-data/
  Monkeypox/
    xxx1.jpg
    xxx2.jpg
  Others/
    yyy1.jpg
    yyy2.jpg

在 WSL / Linux 下,路径分隔符是 / 而不是 \,因此避免使用 split("\\") 去截路径,而是统一用 pathlib.Path

import pathlib

data_dir = pathlib.Path("4-data")
data_paths = list(data_dir.glob("*"))
classNames = [p.name for p in data_paths]   # ['Monkeypox', 'Others']
classNames

这里的 p.name 会自动返回末级目录名(如 MonkeypoxOthers),不会受到操作系统路径分隔符差异的影响。

3. 使用 ImageFolderDataLoader

利用 datasets.ImageFolder 可以直接按子文件夹名当作类别来读取图片,并结合 transforms 做预处理:

from torchvision import datasets

total_datadir = pathlib.Path("4-data")

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

total_data = datasets.ImageFolder(total_datadir, transform=train_transforms)
total_data, total_data.class_to_idx  # {'Monkeypox': 0, 'Others': 1}

随后按 8:2 划分训练集与测试集,并构建 DataLoader(见下文第三节)。

4. CNN 模型结构与损失函数(本次实现)

本次没有使用预训练模型,而是自己搭建了一个带 BatchNorm 的小型 CNN Network_bn,核心结构如下:

import torch.nn.functional as F

class Network_bn(nn.Module):
    def __init__(self):
        super(Network_bn, self).__init__()
        self.conv1 = nn.Conv2d(3, 12, kernel_size=5, stride=1, padding=0)
        self.bn1   = nn.BatchNorm2d(12)
        self.conv2 = nn.Conv2d(12, 12, kernel_size=5, stride=1, padding=0)
        self.bn2   = nn.BatchNorm2d(12)
        self.pool  = nn.MaxPool2d(2, 2)

        self.conv4 = nn.Conv2d(12, 24, kernel_size=5, stride=1, padding=0)
        self.bn4   = nn.BatchNorm2d(24)
        self.conv5 = nn.Conv2d(24, 24, kernel_size=5, stride=1, padding=0)
        self.bn5   = nn.BatchNorm2d(24)

        self.fc1   = nn.Linear(24 * 50 * 50, len(classNames))

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = self.pool(x)
        x = F.relu(self.bn4(self.conv4(x)))
        x = F.relu(self.bn5(self.conv5(x)))
        x = self.pool(x)
        x = x.view(-1, 24 * 50 * 50)
        x = self.fc1(x)
        return x

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

请添加图片描述

损失函数与优化器采用:

loss_fn    = nn.CrossEntropyLoss()
learn_rate = 1e-4
opt        = torch.optim.SGD(model.parameters(), lr=learn_rate)

三、训练与测试流程

1. 单轮训练函数(train)

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

2. 测试函数(test)

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. 主训练循环与日志

请添加图片描述

epochs     = 20
train_loss = []
train_acc  = []
test_loss  = []
test_acc   = []

for epoch in range(epochs):
    model.train()
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)

    model.eval()
    epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)

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

    template = ("Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, "
                "Test_acc:{:.1f}%,Test_loss:{:.3f}")
    print(template.format(
        epoch + 1,
        epoch_train_acc * 100, epoch_train_loss,
        epoch_test_acc * 100, epoch_test_loss
    ))
print("Done")

在本次实验中,20 个 epoch 后的代表性结果为:

  • 训练集Train_acc ≈ 92.7%Train_loss ≈ 0.26
  • 测试集Test_acc ≈ 83.2%Test_loss ≈ 0.39

请添加图片描述


四、结果可视化与分析

1. 绘制训练 / 测试曲线

import matplotlib.pyplot as plt
import warnings
from datetime import datetime

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

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=“Train Acc”)
plt.plot(epochs_range, test_acc, label=“Test Acc”)
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=“Train Loss”)
plt.plot(epochs_range, test_loss, label=“Test Loss”)
plt.legend(loc=“upper right”)
plt.title(“Training and Validation Loss”)

plt.tight_layout()
plt.show()

五、个人总结
环境方面:本次实验在 WSL2 + Linux 环境下完成,Python 版本为 3.11.13,PyTorch 为 2.5.1+cu121,Torchvision 为 0.20.1+cu121,当前硬件为 NVIDIA GeForce RTX 4060 Ti。一开始在路径与类别数上踩了几个坑(例如使用 split(“\”)、len(classeNames) 写错导致 out_features=0 和 CUDA 报错),后来统一改用 pathlib.Path 和 classNames 后就稳定了下来。
数据与模型方面:本次使用的是猴痘 vs 其他皮肤病的真实皮肤图像数据集,共约 2142 张图片,按 8:2 比例划分为训练集和测试集。使用自定义的 Network_bn 模型(多层卷积 + BatchNorm + 池化 + 全连接)在 20 个 epoch 后训练集准确率约 92.7%,测试集准确率约 83.2%,说明模型已经学到了一定的判别能力,但仍存在一定过拟合。
坑点与收获:除了前面提到的路径和类别映射问题,这次还再次巩固了「张量形状」与特征图尺寸推导的概念:从输入的 [B, 3, 224, 224] 经过多层卷积与池化,最终变成 [B, 24, 50, 50],再展平成 245050 接入全连接层。通过这些推导,更清楚了卷积核大小、步长、池化对特征尺寸的影响。
整体感受:P4 让我第一次在医学相关的小数据集上做二分类任务,感受到真实场景中数据量有限、类别可能不平衡、模型容易过拟合等问题。后面打算在当前基础上进一步尝试:引入预训练的 ResNet 进行迁移学习、在训练集中加入适度的数据增强,并微调优化器和正则化策略,看能否把测试集准确率继续往 90% 推进,同时重点关注 Monkeypox 这一类的召回率。

Logo

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

更多推荐