J6第J6周:Inception v1算法实战与解析

"""
Inception v1 (GoogLeNet) — 猴痘病识别 完整代码
==============================================
任务:
  1. 了解卷积层运算量的计算
  2. 了解卷积层的并行结构与1x1卷积核
  3. 使用 Inception v1 完成猴痘病识别

数据目录:
  D:/Adashujuxuexi/T4
    Monkeypox/    ← 猴痘图片
    Others/       ← 其他皮肤病图片
"""

# ============================================================
# 步骤1:导入必要的库
# ============================================================
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from datetime import datetime


# ============================================================
# 步骤2:定义 Inception Module(核心模块)
# ============================================================
class inception_block(nn.Module):
    """
    Inception Module 四分支并行结构:
      branch1: 1x1 conv                -> 提取局部特征
      branch2: 1x1 conv -> 3x3 conv    -> 中等感受野
      branch3: 1x1 conv -> 5x5 conv    -> 大感受野
      branch4: 3x3 maxpool -> 1x1 conv -> 池化后提取特征
    关键:1x1 卷积核实现降维,减少通道数,降低计算量
    """
    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
        super(inception_block, self).__init__()

        self.branch1 = nn.Sequential(
            nn.Conv2d(in_channels, ch1x1, kernel_size=1),
            nn.BatchNorm2d(ch1x1),
            nn.ReLU(inplace=True)
        )

        self.branch2 = nn.Sequential(
            nn.Conv2d(in_channels, ch3x3red, kernel_size=1),
            nn.BatchNorm2d(ch3x3red),
            nn.ReLU(inplace=True),
            nn.Conv2d(ch3x3red, ch3x3, kernel_size=3, padding=1),
            nn.BatchNorm2d(ch3x3),
            nn.ReLU(inplace=True)
        )

        self.branch3 = nn.Sequential(
            nn.Conv2d(in_channels, ch5x5red, kernel_size=1),
            nn.BatchNorm2d(ch5x5red),
            nn.ReLU(inplace=True),
            nn.Conv2d(ch5x5red, ch5x5, kernel_size=5, padding=2),
            nn.BatchNorm2d(ch5x5),
            nn.ReLU(inplace=True)
        )

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, pool_proj, kernel_size=1),
            nn.BatchNorm2d(pool_proj),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return torch.cat([
            self.branch1(x), self.branch2(x),
            self.branch3(x), self.branch4(x)
        ], dim=1)


# ============================================================
# 步骤3:搭建 Inception v1 完整网络
# ============================================================
class InceptionV1(nn.Module):
    def __init__(self, num_classes=2):
        super(InceptionV1, self).__init__()

        # Stem
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(64, 64, kernel_size=1)
        self.conv3 = nn.Conv2d(64, 192, kernel_size=3, padding=1)
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # Stage 3
        self.inception3a = inception_block(192, 64, 96, 128, 16, 32, 32)
        self.inception3b = inception_block(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # Stage 4
        self.inception4a = inception_block(480, 192, 96, 208, 16, 48, 64)
        self.inception4b = inception_block(512, 160, 112, 224, 24, 64, 64)
        self.inception4c = inception_block(512, 128, 128, 256, 24, 64, 64)
        self.inception4d = inception_block(512, 112, 144, 288, 32, 64, 64)
        self.inception4e = inception_block(528, 256, 160, 320, 32, 128, 128)
        self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # Stage 5
        self.inception5a = inception_block(832, 256, 160, 320, 32, 128, 128)
        self.inception5b = nn.Sequential(
            inception_block(832, 384, 192, 384, 48, 128, 128),
            nn.AvgPool2d(kernel_size=7, stride=1),
            nn.Dropout(0.4)
        )

        # 分类器
        self.classifier = nn.Sequential(
            nn.Linear(1024, 1024),
            nn.ReLU(),
            nn.Linear(1024, num_classes),
        )

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.maxpool1(x)
        x = F.relu(self.conv2(x))
        x = F.relu(self.conv3(x))
        x = self.maxpool2(x)

        x = self.inception3a(x)
        x = self.inception3b(x)
        x = self.maxpool3(x)

        x = self.inception4a(x)
        x = self.inception4b(x)
        x = self.inception4c(x)
        x = self.inception4d(x)
        x = self.inception4e(x)
        x = self.maxpool4(x)

        x = self.inception5a(x)
        x = self.inception5b(x)

        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x


# ============================================================
# 步骤4:数据预处理 & 加载
# ============================================================
class TransformSubset(torch.utils.data.Dataset):
    """对 Subset 应用不同的 transform"""
    def __init__(self, dataset, indices, transform):
        self.dataset = dataset
        self.indices = indices
        self.transform = transform

    def __getitem__(self, idx):
        from PIL import Image
        img_path, label = self.dataset.samples[self.indices[idx]]
        img = Image.open(img_path).convert('RGB')
        if self.transform:
            img = self.transform(img)
        return img, label

    def __len__(self):
        return len(self.indices)


def get_dataloaders(data_dir, batch_size=8, img_size=224):
    """
    自动处理两种目录结构:
      1. 有 train/val 子目录 -> 直接用
      2. 没有(Monkeypox/Others 直接在根目录)-> 自动 8:2 划分
    """
    train_transform = transforms.Compose([
        transforms.RandomResizedCrop(img_size),
        transforms.RandomHorizontalFlip(),
        transforms.ColorJitter(brightness=0.2, contrast=0.2),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    val_transform = transforms.Compose([
        transforms.Resize(int(img_size * 256 / 224)),
        transforms.CenterCrop(img_size),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    if os.path.isdir(os.path.join(data_dir, 'train')) and os.path.isdir(os.path.join(data_dir, 'val')):
        train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'), train_transform)
        val_dataset = datasets.ImageFolder(os.path.join(data_dir, 'val'), val_transform)
        classes = train_dataset.classes
    else:
        print("未找到 train/val 子目录,自动按 80:20 划分...")
        full_dataset = datasets.ImageFolder(data_dir, transform=train_transform)
        total = len(full_dataset)
        train_size = int(total * 0.8)
        val_size = total - train_size
        train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])
        val_dataset = TransformSubset(full_dataset, val_dataset.indices, val_transform)
        classes = full_dataset.classes

    use_pin = torch.cuda.is_available()
    train_loader = DataLoader(train_dataset, batch_size=batch_size,
                              shuffle=True, num_workers=0, pin_memory=use_pin)
    val_loader = DataLoader(val_dataset, batch_size=batch_size,
                            shuffle=False, num_workers=0, pin_memory=use_pin)

    print(f"类别: {classes}")
    print(f"训练集: {len(train_dataset)} 张 | 验证集: {len(val_dataset)} 张")
    return train_loader, val_loader, classes


# ============================================================
# 步骤5:训练 & 验证
# ============================================================
def train_one_epoch(model, loader, criterion, optimizer, device):
    model.train()
    total_loss, correct, total = 0, 0, 0
    for batch_idx, (images, labels) in enumerate(loader):
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        loss = criterion(outputs, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * images.size(0)
        _, preds = outputs.max(1)
        correct += (preds == labels).sum().item()
        total += labels.size(0)
        print(f'    Batch [{batch_idx+1}/{len(loader)}] Loss: {loss.item():.4f}', end='\r')
    print()
    return total_loss / total, correct / total


@torch.no_grad()
def validate(model, loader, criterion, device):
    model.eval()
    total_loss, correct, total = 0, 0, 0
    for images, labels in loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        loss = criterion(outputs, labels)
        total_loss += loss.item() * images.size(0)
        _, preds = outputs.max(1)
        correct += (preds == labels).sum().item()
        total += labels.size(0)
    return total_loss / total, correct / total


def plot_curves(train_losses, val_losses, train_accs, val_accs):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

    ax1.plot(train_losses, label='Train')
    ax1.plot(val_losses, label='Val')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Loss')
    ax1.set_title('Loss Curve')
    ax1.legend()

    ax2.plot(train_accs, label='Train')
    ax2.plot(val_accs, label='Val')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('Accuracy')
    ax2.set_title('Accuracy Curve')
    ax2.legend()

    # 时间水印(右下角)
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    fig.text(0.99, 0.01, timestamp, ha='right', va='bottom',
             fontsize=10, color='gray', alpha=0.7)

    plt.tight_layout()
    plt.savefig('training_curves.png', dpi=150)
    plt.show()
    print(f'训练曲线已保存 (时间水印: {timestamp})')


# ============================================================
# 步骤6:主函数
# ============================================================
def main():
    # ---- 超参数 ----
    data_dir    = 'D:/Adashujuxuexi/T4'
    num_classes = 2
    batch_size  = 8
    epochs      = 10
    lr          = 0.001
    save_path   = './best_inception_v1.pth'

    # ---- 设备 ----
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f'设备: {device}')

    # ---- 数据 ----
    train_loader, val_loader, classes = get_dataloaders(data_dir, batch_size)

    # ---- 模型 ----
    model = InceptionV1(num_classes=num_classes).to(device)
    total_params = sum(p.numel() for p in model.parameters())
    print(f'模型参数量: {total_params:,}')

    # ---- 损失 & 优化器 ----
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=lr)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)

    # ---- 训练循环 ----
    train_losses, val_losses, train_accs, val_accs = [], [], [], []
    best_acc = 0.0

    for epoch in range(epochs):
        print(f'\nEpoch [{epoch+1}/{epochs}]  LR: {optimizer.param_groups[0]["lr"]:.6f}')

        train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device)
        val_loss, val_acc = validate(model, val_loader, criterion, device)
        scheduler.step()

        train_losses.append(train_loss)
        val_losses.append(val_loss)
        train_accs.append(train_acc)
        val_accs.append(val_acc)

        print(f'  Train Loss: {train_loss:.4f}  Acc: {train_acc:.4f}')
        print(f'  Val   Loss: {val_loss:.4f}  Acc: {val_acc:.4f}')

        if val_acc > best_acc:
            best_acc = val_acc
            torch.save(model.state_dict(), save_path)
            print(f'  ✓ 最佳模型已保存')

    print(f'\n{"="*50}')
    print(f'训练完成!最佳验证准确率: {best_acc:.4f}')

    # ---- 绘制训练曲线(带时间水印)----
    plot_curves(train_losses, val_losses, train_accs, val_accs)


if __name__ == '__main__':
    main()
设备: cpu
未找到 train/val 子目录,自动按 80:20 划分...
类别: ['Monkeypox', 'Others']
训练集: 1713 张 | 验证集: 429 张
模型参数量: 7,039,122

Epoch [1/10]  LR: 0.001000
    Batch [215/215] Loss: 0.6285
  Train Loss: 0.6930  Acc: 0.5855
  Val   Loss: 0.6705  Acc: 0.6107
  ✓ 最佳模型已保存

Epoch [2/10]  LR: 0.001000
    Batch [215/215] Loss: 0.5475
  Train Loss: 0.6711  Acc: 0.6141
  Val   Loss: 0.6590  Acc: 0.5967

Epoch [3/10]  LR: 0.001000
    Batch [215/215] Loss: 0.4853
  Train Loss: 0.6596  Acc: 0.6246
  Val   Loss: 0.6823  Acc: 0.5758

Epoch [4/10]  LR: 0.001000
    Batch [215/215] Loss: 0.8752
  Train Loss: 0.6521  Acc: 0.6346
  Val   Loss: 0.6593  Acc: 0.6573
  ✓ 最佳模型已保存

Epoch [5/10]  LR: 0.001000
    Batch [215/215] Loss: 0.8479
  Train Loss: 0.6499  Acc: 0.6427
  Val   Loss: 0.6608  Acc: 0.6317

Epoch [6/10]  LR: 0.001000
    Batch [215/215] Loss: 0.6347
  Train Loss: 0.6498  Acc: 0.6439
  Val   Loss: 0.6402  Acc: 0.6527

Epoch [7/10]  LR: 0.001000
    Batch [215/215] Loss: 0.5337
  Train Loss: 0.6485  Acc: 0.6381
  Val   Loss: 0.6515  Acc: 0.6410

Epoch [8/10]  LR: 0.001000
    Batch [215/215] Loss: 0.6512
  Train Loss: 0.6607  Acc: 0.6229
  Val   Loss: 0.7119  Acc: 0.4802

Epoch [9/10]  LR: 0.001000
    Batch [215/215] Loss: 1.0085
  Train Loss: 0.6741  Acc: 0.6013
  Val   Loss: 0.6331  Acc: 0.6643
  ✓ 最佳模型已保存

Epoch [10/10]  LR: 0.001000
    Batch [215/215] Loss: 0.5512
  Train Loss: 0.6592  Acc: 0.6305
  Val   Loss: 0.6506  Acc: 0.6084

==================================================
训练完成!最佳验证准确率: 0.6643

请添加图片描述

训练曲线已保存 (时间水印: 2026-07-24 18:06:59)

本次基于 PyTorch 完成 Inception v1 网络实现猴痘皮肤病二分类任务,掌握了 Inception 模块四分支并行结构与 1×1 卷积降维减少计算量的核心原理。数据集共 2142 张图片,按 8:2 划分训练、验证集,模型参数量约 704 万,使用 Adam 优化器训练 10 轮。最终最佳验证准确率仅 66.43%,效果较差。训练过程出现验证准确率震荡、后期精度下滑,存在轻微过拟合问题。分析原因:仅简单数据增强、学习率调度策略单一、epoch 数量不足,且 CPU 训练限制调参效率。通过本次实操熟悉了图像分类完整流程,后续计划增加图像增强手段、调整学习率策略、引入预训练权重,进一步提升皮肤病识别精度。


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐