1. 简介

项目 内容
模型 InceptionV1 / GoogLeNet 风格网络(加入 BatchNorm)
任务 三分类图像分类(Normal / Mild / Severe)
数据集 原始 1661 张图像;去重图组 946 组;训练集均衡过采样为 2436 张;测试集 189 张
最优性能 当前 V4 版本 notebook 尚未保存完整 30 轮训练输出,需重新运行后填写

2. 环境

  • 语言环境:Python 3.14.6
  • 编译器:Jupyter Notebook
  • 深度学习环境:PyTorch ( torch 2.12.1 + torchvision 0.27.1 )

3. 代码实现

3.1 前期准备

3.1.1 设置GPU & 导入库

导入 PyTorch、torchvision 等深度学习库,配置 matplotlib 中文字体,固定随机种子,并自动选择 GPU/CPU 设备。

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import transforms, datasets
from torch.utils.data import DataLoader, Subset
import os, PIL, pathlib, warnings
import torchsummary as summary
import copy
import matplotlib.pyplot as plt
from PIL import Image, ImageOps
from datetime import datetime
import random
import hashlib
import numpy as np
from collections import Counter, defaultdict

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

seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(seed)

try:
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
except AttributeError:
    pass

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

3.1.2 数据加载

先从 2Mild 类别中读取部分样本图片,用于直观查看图像内容和数据质量。

image_folder='./Data/data/2Mild/'
image_files = [f for f in os. listdir(image_folder) if f.endswith((".jpg", ".png", ".jpeg"))]
fig, axes = plt.subplots(3, 8, figsize=(16, 6))

for ax, img_file in zip(axes.flat, image_files):
    img_path = os.path.join(image_folder, img_file)
    img = Image.open(img_path)
    ax.imshow(img)
    ax.axis('off')

plt.tight_layout()
plt.show()

在这里插入图片描述

加载眼底图像数据集,并定义黑边裁剪、自动对比度增强、轻量数据增强和归一化流程。训练集和测试集使用不同的 transform:训练集包含轻量增强,测试集保持确定性预处理。

data_dir = './Data/data/'

def preprocess_fundus(img, threshold=10, padding=12):
    """裁掉眼底图周围大面积黑边,并做轻量自动对比度增强。"""
    img = img.convert('RGB')
    arr = np.asarray(img)
    mask = arr.mean(axis=2) > threshold

    if mask.any():
        ys, xs = np.where(mask)
        left = max(int(xs.min()) - padding, 0)
        top = max(int(ys.min()) - padding, 0)
        right = min(int(xs.max()) + padding + 1, img.width)
        bottom = min(int(ys.max()) + padding + 1, img.height)
        img = img.crop((left, top, right, bottom))

    return ImageOps.autocontrast(img, cutoff=1)

# 眼底图像分类不要随机裁剪,容易裁掉边缘病灶。先裁黑边/增强对比度,再 Resize。
train_transforms = transforms.Compose([
    transforms.Lambda(preprocess_fundus),
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=8),
    transforms.RandomApply([
        transforms.ColorJitter(brightness=0.04, contrast=0.08, saturation=0.03)
    ], p=0.3),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

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

# 不带 transform 的索引数据集,用于稳定统计、按内容去重分组、分层划分。
total_data = datasets.ImageFolder(data_dir)
print(f"Raw samples: {len(total_data)}")
print(f"Classes: {total_data.class_to_idx}")
raw_counts = Counter(total_data.targets)
print("Raw class counts:", dict(zip(total_data.classes, [raw_counts[i] for i in range(len(total_data.classes))])))
total_data
Raw samples: 1661
Classes: {'0Normal': 0, '2Mild': 1, '4Severe': 2}
Raw class counts: {'0Normal': 1017, '2Mild': 232, '4Severe': 412}

查看 ImageFolder 自动分配的类别标签与索引的对应关系。

total_data.class_to_idx
{'0Normal': 0, '2Mild': 1, '4Severe': 2}

为了避免重复图同时进入训练集和测试集造成数据泄漏,先按图片内容 MD5 分组,再按类别分层划分。训练集保留同类重复图并进行类别均衡过采样,测试集每个重复组只保留 1 张代表图。

def file_md5(path):
    with open(path, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()

# 按图片内容分组:同一张图的副本必须整体进入训练集或测试集,避免重复图泄漏。
hash_groups = defaultdict(list)
for idx, (path, label) in enumerate(total_data.samples):
    hash_groups[(file_md5(path), label)].append(idx)

groups_by_class = defaultdict(list)
for (_, label), indices in hash_groups.items():
    groups_by_class[label].append(indices)

unique_group_counts = {total_data.classes[label]: len(groups) for label, groups in groups_by_class.items()}
duplicate_count = len(total_data) - sum(len(groups) for groups in groups_by_class.values())
print(f"Unique image groups: {sum(len(groups) for groups in groups_by_class.values())} (found {duplicate_count} exact duplicates)")
print(f"Unique group counts: {unique_group_counts}")

def stratified_group_split(groups_by_class, test_ratio=0.2, seed=42):
    rng = random.Random(seed)
    train_indices, test_indices = [], []

    for label, groups in sorted(groups_by_class.items()):
        groups = groups.copy()
        rng.shuffle(groups)
        n_test = max(1, round(len(groups) * test_ratio))
        test_groups = groups[:n_test]
        train_groups = groups[n_test:]

        for group in train_groups:
            train_indices.extend(group)
        for group in test_groups:
            test_indices.append(group[0])

    rng.shuffle(train_indices)
    rng.shuffle(test_indices)
    return train_indices, test_indices

def make_balanced_indices(indices, targets, seed=42):
    rng = random.Random(seed)
    indices_by_class = defaultdict(list)
    for idx in indices:
        indices_by_class[targets[idx]].append(idx)

    max_count = max(len(v) for v in indices_by_class.values())
    balanced_indices = []
    for label, label_indices in sorted(indices_by_class.items()):
        label_indices = label_indices.copy()
        repeats = max_count // len(label_indices)
        remainder = max_count % len(label_indices)
        balanced_indices.extend(label_indices * repeats)
        balanced_indices.extend(rng.sample(label_indices, remainder))

    rng.shuffle(balanced_indices)
    return balanced_indices

train_indices_raw, test_indices = stratified_group_split(groups_by_class, test_ratio=0.2, seed=seed)
train_indices = make_balanced_indices(train_indices_raw, total_data.targets, seed=seed)

raw_train_targets = [total_data.targets[i] for i in train_indices_raw]
train_targets_list = [total_data.targets[i] for i in train_indices]
test_targets_list = [total_data.targets[i] for i in test_indices]
raw_train_counts = Counter(raw_train_targets)
balanced_train_counts = Counter(train_targets_list)
test_counts = Counter(test_targets_list)

print("Raw train class counts before balancing:", dict(zip(total_data.classes, [raw_train_counts[i] for i in range(len(total_data.classes))])))
print("Balanced train class counts:", dict(zip(total_data.classes, [balanced_train_counts[i] for i in range(len(total_data.classes))])))
print("Test class counts:", dict(zip(total_data.classes, [test_counts[i] for i in range(len(total_data.classes))])))
print(f"Train majority baseline after balancing: {max(balanced_train_counts.values()) / len(train_targets_list):.3f}")
print(f"Test majority baseline: {max(test_counts.values()) / len(test_targets_list):.3f}")

assert len(set(balanced_train_counts.values())) == 1, f'训练集没有均衡成功: {dict(balanced_train_counts)}'
assert len(train_indices) == 2436, f'训练集大小不对,说明没有跑新版数据处理: {len(train_indices)}'
Unique image groups: 946 (found 715 exact duplicates)
Unique group counts: {'0Normal': 513, '2Mild': 227, '4Severe': 206}
Raw train class counts before balancing: {'0Normal': 812, '2Mild': 184, '4Severe': 330}
Balanced train class counts: {'0Normal': 812, '2Mild': 812, '4Severe': 812}
Test class counts: {'0Normal': 103, '2Mild': 45, '4Severe': 41}
Train majority baseline after balancing: 0.333
Test majority baseline: 0.545

构建 DataLoader,并检查 batch 的形状。

assert DATA_PIPELINE_VERSION == 'GROUP_SPLIT_BALANCED_OVERSAMPLING_FUNDUS_PREPROCESS_V4_BATCHNORM', '请先重新运行数据划分单元,否则还在使用旧 train_dataset/test_dataset'

train_data_full = datasets.ImageFolder(data_dir, transform=train_transforms)
test_data_full = datasets.ImageFolder(data_dir, transform=test_transforms)
train_dataset = Subset(train_data_full, train_indices)
test_dataset = Subset(test_data_full, test_indices)

batch_size = 8
train_dl = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dl = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

train_targets = torch.tensor(train_targets_list)
class_counts = torch.bincount(train_targets, minlength=len(total_data.classes))
class_weights = class_counts.sum() / (len(total_data.classes) * class_counts.float())
print("Balanced train samples:", len(train_dataset))
print("Train class counts:", dict(zip(total_data.classes, class_counts.tolist())))
print("Class weights:", dict(zip(total_data.classes, class_weights.tolist())))
print("Test samples:", len(test_dataset))
print("Batch size:", batch_size)
print("DATA_PIPELINE_VERSION:", DATA_PIPELINE_VERSION)

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
Balanced train samples: 2436
Train class counts: {'0Normal': 812, '2Mild': 812, '4Severe': 812}
Class weights: {'0Normal': 1.0, '2Mild': 1.0, '4Severe': 1.0}
Test samples: 189
Batch size: 8
DATA_PIPELINE_VERSION: GROUP_SPLIT_BALANCED_OVERSAMPLING_FUNDUS_PREPROCESS_V4_BATCHNORM
Shape of X [N, C, H, W]:  torch.Size([8, 3, 224, 224])
Shape of y:  torch.Size([8]) torch.int64

3.2 模型建立与训练

3.2.1 定义 InceptionV1 网络模型

实现经典 InceptionV1 / GoogLeNet 风格网络。其核心思想是通过多个并行卷积分支提取不同尺度的特征,再在通道维度拼接。

本版本在每个卷积模块中加入 BatchNorm2d,结构为:Conv2d(bias=False) → BatchNorm2d → ReLU。由于所有主干卷积和 Inception 分支都复用 conv_relu(),因此 BatchNorm 会自动作用于整个卷积特征提取部分。

四个核心组件

组件 作用
conv_relu 基础卷积块:Conv2d → BatchNorm2d → ReLU,用于稳定从零训练过程
inception_block Inception 模块:包含 1×1、3×3、5×5 以及池化分支,并将多尺度特征拼接
InceptionV1 整体网络:Conv/Pool → Inception3 → Inception4 → Inception5 → AvgPool → FC
Classifier 分类头:全连接层 + ReLU + Dropout + 输出层,对应 3 个类别

本实验中 num_classes=3,对应三分类任务(Normal / Mild / Severe)。

def conv_relu(in_channels, out_channels, kernel_size, stride=1, padding=0):
    return nn.Sequential(
        nn.Conv2d(
            in_channels,
            out_channels,
            kernel_size=kernel_size,
            stride=stride,
            padding=padding,
            bias=False
        ),
        nn.BatchNorm2d(out_channels),
        nn.ReLU(inplace=True)
    )

class inception_block(nn.Module):
    def __init__(self, in_channels, out_1x1, red_3x3, out_3x3, red_5x5, out_5x5, out_pool):
        super(inception_block, self).__init__()
        # 1x1 conv branch
        self.branch1 = conv_relu(in_channels, out_1x1, kernel_size=1)

        # 1x1 conv -> 3x3 conv branch
        self.branch2 = nn.Sequential(
            conv_relu(in_channels, red_3x3, kernel_size=1),
            conv_relu(red_3x3, out_3x3, kernel_size=3, padding=1)
        )

        # 1x1 conv -> 5x5 conv branch
        self.branch3 = nn.Sequential(
            conv_relu(in_channels, red_5x5, kernel_size=1),
            conv_relu(red_5x5, out_5x5, kernel_size=5, padding=2)
        )

        # 3x3 max pooling -> 1x1 conv branch
        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            conv_relu(in_channels, out_pool, kernel_size=1)
        )

    def forward(self, x):
        branch1_out = self.branch1(x)
        branch2_out = self.branch2(x)
        branch3_out = self.branch3(x)
        branch4_out = self.branch4(x)

        outputs = [branch1_out, branch2_out, branch3_out, branch4_out]
        return torch.cat(outputs, dim=1)
class InceptionV1(nn.Module):
    def __init__(self, num_classes=1000):
        super(InceptionV1, self).__init__()
        self.conv1 = conv_relu(3, 64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.conv2 = conv_relu(64, 64, kernel_size=1, stride=1, padding=0)
        self.conv3 = conv_relu(64, 192, kernel_size=3, stride=1, padding=1)
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # Inception blocks
        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)

        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)

        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, padding=0),
            nn.Dropout(0.4)
        )

        self.classifier = nn.Sequential(
            nn.Linear(in_features=1024, out_features=1024),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Linear(in_features=1024, out_features=num_classes)
        )

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool1(x)
        x = self.conv2(x)
        x = 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

model = InceptionV1(num_classes=len(total_data.classes)).to(device)
bn_layers = sum(1 for m in model.modules() if isinstance(m, nn.BatchNorm2d))
print(f'BatchNorm2d layers: {bn_layers}')
model
BatchNorm2d layers: 57
InceptionV1(
  (conv1): Sequential(
    (0): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
    (2): ReLU(inplace=True)
  )
  (maxpool1): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (conv2): Sequential(
    (0): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
    (2): ReLU(inplace=True)
  )
  (conv3): Sequential(
    (0): Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (1): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
    (2): ReLU(inplace=True)
  )
  (maxpool2): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (inception3a): inception_block(
    (branch1): Sequential(
      (0): Conv2d(192, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(192, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(96, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(192, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(16, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(192, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (inception3b): inception_block(
    (branch1): Sequential(
      (0): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(128, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(256, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(32, 96, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (maxpool3): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (inception4a): inception_block(
    (branch1): Sequential(
      (0): Conv2d(480, 192, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(480, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(96, 208, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(208, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(480, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(16, 48, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(48, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(480, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (inception4b): inception_block(
    (branch1): Sequential(
      (0): Conv2d(512, 160, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(512, 112, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(112, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(112, 224, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(224, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(512, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(24, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (inception4c): inception_block(
    (branch1): Sequential(
      (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(512, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(24, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(24, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (inception4d): inception_block(
    (branch1): Sequential(
      (0): Conv2d(512, 112, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(112, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(512, 144, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(144, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(144, 288, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(288, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(512, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (inception4e): inception_block(
    (branch1): Sequential(
      (0): Conv2d(528, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(528, 160, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(160, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(320, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(528, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(32, 128, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(528, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (maxpool4): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (inception5a): inception_block(
    (branch1): Sequential(
      (0): Conv2d(832, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
      (2): ReLU(inplace=True)
    )
    (branch2): Sequential(
      (0): Sequential(
        (0): Conv2d(832, 160, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(160, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (1): BatchNorm2d(320, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch3): Sequential(
      (0): Sequential(
        (0): Conv2d(832, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (1): Sequential(
        (0): Conv2d(32, 128, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
      (1): Sequential(
        (0): Conv2d(832, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
    )
  )
  (inception5b): Sequential(
    (0): inception_block(
      (branch1): Sequential(
        (0): Conv2d(832, 384, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (1): BatchNorm2d(384, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
        (2): ReLU(inplace=True)
      )
      (branch2): Sequential(
        (0): Sequential(
          (0): Conv2d(832, 192, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (1): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
          (2): ReLU(inplace=True)
        )
        (1): Sequential(
          (0): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (1): BatchNorm2d(384, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
          (2): ReLU(inplace=True)
        )
      )
      (branch3): Sequential(
        (0): Sequential(
          (0): Conv2d(832, 48, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (1): BatchNorm2d(48, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
          (2): ReLU(inplace=True)
        )
        (1): Sequential(
          (0): Conv2d(48, 128, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), bias=False)
          (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
          (2): ReLU(inplace=True)
        )
      )
      (branch4): Sequential(
        (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
        (1): Sequential(
          (0): Conv2d(832, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
          (2): ReLU(inplace=True)
        )
      )
    )
    (1): AvgPool2d(kernel_size=7, stride=1, padding=0)
    (2): Dropout(p=0.4, inplace=False)
  )
  (classifier): Sequential(
    (0): Linear(in_features=1024, out_features=1024, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.4, inplace=False)
    (3): Linear(in_features=1024, out_features=3, bias=True)
  )
)

3.2.2 模型结构概览

通过 torchsummary 查看每层输出形状和参数量,便于确认网络结构是否正确搭建。

当前模型包含 57 个 BatchNorm2d 层,共有 7,033,507 个参数,且全部可训练。

summary.summary(model, (3, 224, 224))
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1         [-1, 64, 112, 112]           9,408
       BatchNorm2d-2         [-1, 64, 112, 112]             128
              ReLU-3         [-1, 64, 112, 112]               0
         MaxPool2d-4           [-1, 64, 56, 56]               0
            Conv2d-5           [-1, 64, 56, 56]           4,096
       BatchNorm2d-6           [-1, 64, 56, 56]             128
              ReLU-7           [-1, 64, 56, 56]               0
            Conv2d-8          [-1, 192, 56, 56]         110,592
       BatchNorm2d-9          [-1, 192, 56, 56]             384
             ReLU-10          [-1, 192, 56, 56]               0
        MaxPool2d-11          [-1, 192, 28, 28]               0
           Conv2d-12           [-1, 64, 28, 28]          12,288
      BatchNorm2d-13           [-1, 64, 28, 28]             128
             ReLU-14           [-1, 64, 28, 28]               0
           Conv2d-15           [-1, 96, 28, 28]          18,432
      BatchNorm2d-16           [-1, 96, 28, 28]             192
             ReLU-17           [-1, 96, 28, 28]               0
           Conv2d-18          [-1, 128, 28, 28]         110,592
      BatchNorm2d-19          [-1, 128, 28, 28]             256
             ReLU-20          [-1, 128, 28, 28]               0
           Conv2d-21           [-1, 16, 28, 28]           3,072
      BatchNorm2d-22           [-1, 16, 28, 28]              32
             ReLU-23           [-1, 16, 28, 28]               0
           Conv2d-24           [-1, 32, 28, 28]          12,800
      BatchNorm2d-25           [-1, 32, 28, 28]              64
             ReLU-26           [-1, 32, 28, 28]               0
        MaxPool2d-27          [-1, 192, 28, 28]               0
           Conv2d-28           [-1, 32, 28, 28]           6,144
      BatchNorm2d-29           [-1, 32, 28, 28]              64
             ReLU-30           [-1, 32, 28, 28]               0
  inception_block-31          [-1, 256, 28, 28]               0
           Conv2d-32          [-1, 128, 28, 28]          32,768
      BatchNorm2d-33          [-1, 128, 28, 28]             256
             ReLU-34          [-1, 128, 28, 28]               0
           Conv2d-35          [-1, 128, 28, 28]          32,768
      BatchNorm2d-36          [-1, 128, 28, 28]             256
             ReLU-37          [-1, 128, 28, 28]               0
           Conv2d-38          [-1, 192, 28, 28]         221,184
      BatchNorm2d-39          [-1, 192, 28, 28]             384
             ReLU-40          [-1, 192, 28, 28]               0
           Conv2d-41           [-1, 32, 28, 28]           8,192
      BatchNorm2d-42           [-1, 32, 28, 28]              64
             ReLU-43           [-1, 32, 28, 28]               0
           Conv2d-44           [-1, 96, 28, 28]          76,800
      BatchNorm2d-45           [-1, 96, 28, 28]             192
             ReLU-46           [-1, 96, 28, 28]               0
        MaxPool2d-47          [-1, 256, 28, 28]               0
           Conv2d-48           [-1, 64, 28, 28]          16,384
      BatchNorm2d-49           [-1, 64, 28, 28]             128
             ReLU-50           [-1, 64, 28, 28]               0
  inception_block-51          [-1, 480, 28, 28]               0
        MaxPool2d-52          [-1, 480, 14, 14]               0
           Conv2d-53          [-1, 192, 14, 14]          92,160
      BatchNorm2d-54          [-1, 192, 14, 14]             384
             ReLU-55          [-1, 192, 14, 14]               0
           Conv2d-56           [-1, 96, 14, 14]          46,080
      BatchNorm2d-57           [-1, 96, 14, 14]             192
             ReLU-58           [-1, 96, 14, 14]               0
           Conv2d-59          [-1, 208, 14, 14]         179,712
      BatchNorm2d-60          [-1, 208, 14, 14]             416
             ReLU-61          [-1, 208, 14, 14]               0
           Conv2d-62           [-1, 16, 14, 14]           7,680
      BatchNorm2d-63           [-1, 16, 14, 14]              32
             ReLU-64           [-1, 16, 14, 14]               0
           Conv2d-65           [-1, 48, 14, 14]          19,200
      BatchNorm2d-66           [-1, 48, 14, 14]              96
             ReLU-67           [-1, 48, 14, 14]               0
        MaxPool2d-68          [-1, 480, 14, 14]               0
           Conv2d-69           [-1, 64, 14, 14]          30,720
      BatchNorm2d-70           [-1, 64, 14, 14]             128
             ReLU-71           [-1, 64, 14, 14]               0
  inception_block-72          [-1, 512, 14, 14]               0
           Conv2d-73          [-1, 160, 14, 14]          81,920
      BatchNorm2d-74          [-1, 160, 14, 14]             320
             ReLU-75          [-1, 160, 14, 14]               0
           Conv2d-76          [-1, 112, 14, 14]          57,344
      BatchNorm2d-77          [-1, 112, 14, 14]             224
             ReLU-78          [-1, 112, 14, 14]               0
           Conv2d-79          [-1, 224, 14, 14]         225,792
      BatchNorm2d-80          [-1, 224, 14, 14]             448
             ReLU-81          [-1, 224, 14, 14]               0
           Conv2d-82           [-1, 24, 14, 14]          12,288
      BatchNorm2d-83           [-1, 24, 14, 14]              48
             ReLU-84           [-1, 24, 14, 14]               0
           Conv2d-85           [-1, 64, 14, 14]          38,400
      BatchNorm2d-86           [-1, 64, 14, 14]             128
             ReLU-87           [-1, 64, 14, 14]               0
        MaxPool2d-88          [-1, 512, 14, 14]               0
           Conv2d-89           [-1, 64, 14, 14]          32,768
      BatchNorm2d-90           [-1, 64, 14, 14]             128
             ReLU-91           [-1, 64, 14, 14]               0
  inception_block-92          [-1, 512, 14, 14]               0
           Conv2d-93          [-1, 128, 14, 14]          65,536
      BatchNorm2d-94          [-1, 128, 14, 14]             256
             ReLU-95          [-1, 128, 14, 14]               0
           Conv2d-96          [-1, 128, 14, 14]          65,536
      BatchNorm2d-97          [-1, 128, 14, 14]             256
             ReLU-98          [-1, 128, 14, 14]               0
           Conv2d-99          [-1, 256, 14, 14]         294,912
     BatchNorm2d-100          [-1, 256, 14, 14]             512
            ReLU-101          [-1, 256, 14, 14]               0
          Conv2d-102           [-1, 24, 14, 14]          12,288
     BatchNorm2d-103           [-1, 24, 14, 14]              48
            ReLU-104           [-1, 24, 14, 14]               0
          Conv2d-105           [-1, 64, 14, 14]          38,400
     BatchNorm2d-106           [-1, 64, 14, 14]             128
            ReLU-107           [-1, 64, 14, 14]               0
       MaxPool2d-108          [-1, 512, 14, 14]               0
          Conv2d-109           [-1, 64, 14, 14]          32,768
     BatchNorm2d-110           [-1, 64, 14, 14]             128
            ReLU-111           [-1, 64, 14, 14]               0
 inception_block-112          [-1, 512, 14, 14]               0
          Conv2d-113          [-1, 112, 14, 14]          57,344
     BatchNorm2d-114          [-1, 112, 14, 14]             224
            ReLU-115          [-1, 112, 14, 14]               0
          Conv2d-116          [-1, 144, 14, 14]          73,728
     BatchNorm2d-117          [-1, 144, 14, 14]             288
            ReLU-118          [-1, 144, 14, 14]               0
          Conv2d-119          [-1, 288, 14, 14]         373,248
     BatchNorm2d-120          [-1, 288, 14, 14]             576
            ReLU-121          [-1, 288, 14, 14]               0
          Conv2d-122           [-1, 32, 14, 14]          16,384
     BatchNorm2d-123           [-1, 32, 14, 14]              64
            ReLU-124           [-1, 32, 14, 14]               0
          Conv2d-125           [-1, 64, 14, 14]          51,200
     BatchNorm2d-126           [-1, 64, 14, 14]             128
            ReLU-127           [-1, 64, 14, 14]               0
       MaxPool2d-128          [-1, 512, 14, 14]               0
          Conv2d-129           [-1, 64, 14, 14]          32,768
     BatchNorm2d-130           [-1, 64, 14, 14]             128
            ReLU-131           [-1, 64, 14, 14]               0
 inception_block-132          [-1, 528, 14, 14]               0
          Conv2d-133          [-1, 256, 14, 14]         135,168
     BatchNorm2d-134          [-1, 256, 14, 14]             512
            ReLU-135          [-1, 256, 14, 14]               0
          Conv2d-136          [-1, 160, 14, 14]          84,480
     BatchNorm2d-137          [-1, 160, 14, 14]             320
            ReLU-138          [-1, 160, 14, 14]               0
          Conv2d-139          [-1, 320, 14, 14]         460,800
     BatchNorm2d-140          [-1, 320, 14, 14]             640
            ReLU-141          [-1, 320, 14, 14]               0
          Conv2d-142           [-1, 32, 14, 14]          16,896
     BatchNorm2d-143           [-1, 32, 14, 14]              64
            ReLU-144           [-1, 32, 14, 14]               0
          Conv2d-145          [-1, 128, 14, 14]         102,400
     BatchNorm2d-146          [-1, 128, 14, 14]             256
            ReLU-147          [-1, 128, 14, 14]               0
       MaxPool2d-148          [-1, 528, 14, 14]               0
          Conv2d-149          [-1, 128, 14, 14]          67,584
     BatchNorm2d-150          [-1, 128, 14, 14]             256
            ReLU-151          [-1, 128, 14, 14]               0
 inception_block-152          [-1, 832, 14, 14]               0
       MaxPool2d-153            [-1, 832, 7, 7]               0
          Conv2d-154            [-1, 256, 7, 7]         212,992
     BatchNorm2d-155            [-1, 256, 7, 7]             512
            ReLU-156            [-1, 256, 7, 7]               0
          Conv2d-157            [-1, 160, 7, 7]         133,120
     BatchNorm2d-158            [-1, 160, 7, 7]             320
            ReLU-159            [-1, 160, 7, 7]               0
          Conv2d-160            [-1, 320, 7, 7]         460,800
     BatchNorm2d-161            [-1, 320, 7, 7]             640
            ReLU-162            [-1, 320, 7, 7]               0
          Conv2d-163             [-1, 32, 7, 7]          26,624
     BatchNorm2d-164             [-1, 32, 7, 7]              64
            ReLU-165             [-1, 32, 7, 7]               0
          Conv2d-166            [-1, 128, 7, 7]         102,400
     BatchNorm2d-167            [-1, 128, 7, 7]             256
            ReLU-168            [-1, 128, 7, 7]               0
       MaxPool2d-169            [-1, 832, 7, 7]               0
          Conv2d-170            [-1, 128, 7, 7]         106,496
     BatchNorm2d-171            [-1, 128, 7, 7]             256
            ReLU-172            [-1, 128, 7, 7]               0
 inception_block-173            [-1, 832, 7, 7]               0
          Conv2d-174            [-1, 384, 7, 7]         319,488
     BatchNorm2d-175            [-1, 384, 7, 7]             768
            ReLU-176            [-1, 384, 7, 7]               0
          Conv2d-177            [-1, 192, 7, 7]         159,744
     BatchNorm2d-178            [-1, 192, 7, 7]             384
            ReLU-179            [-1, 192, 7, 7]               0
          Conv2d-180            [-1, 384, 7, 7]         663,552
     BatchNorm2d-181            [-1, 384, 7, 7]             768
            ReLU-182            [-1, 384, 7, 7]               0
          Conv2d-183             [-1, 48, 7, 7]          39,936
     BatchNorm2d-184             [-1, 48, 7, 7]              96
            ReLU-185             [-1, 48, 7, 7]               0
          Conv2d-186            [-1, 128, 7, 7]         153,600
     BatchNorm2d-187            [-1, 128, 7, 7]             256
            ReLU-188            [-1, 128, 7, 7]               0
       MaxPool2d-189            [-1, 832, 7, 7]               0
          Conv2d-190            [-1, 128, 7, 7]         106,496
     BatchNorm2d-191            [-1, 128, 7, 7]             256
            ReLU-192            [-1, 128, 7, 7]               0
 inception_block-193           [-1, 1024, 7, 7]               0
       AvgPool2d-194           [-1, 1024, 1, 1]               0
         Dropout-195           [-1, 1024, 1, 1]               0
          Linear-196                 [-1, 1024]       1,049,600
            ReLU-197                 [-1, 1024]               0
         Dropout-198                 [-1, 1024]               0
          Linear-199                    [-1, 3]           3,075
================================================================
Total params: 7,033,507
Trainable params: 7,033,507
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 94.12
Params size (MB): 26.83
Estimated Total Size (MB): 121.53
----------------------------------------------------------------

3.2.3 定义训练和测试函数

  • train() 函数 — 训练阶段:对每个 mini-batch 执行前向传播、计算损失、反向传播、梯度裁剪和参数更新。
  • test() 函数 — 测试阶段:使用 torch.no_grad() 禁用梯度计算,仅评估模型在测试集上的损失和准确率。
  • prediction_counts() 函数 — 统计模型在测试集上的预测类别分布,用于观察模型是否塌缩到单一类别。
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()
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
        optimizer.step()

        train_loss += loss.item()
        train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()

    train_loss /= num_batches
    train_acc /= size

    return train_loss, train_acc

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_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
            test_loss += loss.item()

    test_loss /= num_batches
    test_acc /= size

    return test_loss, test_acc

def prediction_counts(dataloader, model):
    model.eval()
    counts = torch.zeros(len(total_data.classes), dtype=torch.int64)
    with torch.no_grad():
        for imgs, _ in dataloader:
            preds = model(imgs.to(device)).argmax(1).cpu()
            counts += torch.bincount(preds, minlength=len(total_data.classes))
    return dict(zip(total_data.classes, counts.tolist()))

3.2.4 训练模型

训练配置:

  • 优化器:AdamW,学习率 lr=1e-4,权重衰减 weight_decay=1e-5
  • 损失函数:CrossEntropyLoss,当前训练集已均衡,因此类别权重均为 1.0
  • 训练轮次:30 个 Epoch
  • 批大小:8

训练策略:

  • 每个 Epoch 结束后在测试集上评估,记录训练/测试的损失和准确率
  • 保存测试准确率最高的模型权重到 ./best_inception_v1.pth
  • 每轮输出 Pred 预测分布,辅助判断是否出现类别塌缩
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5)
loss_fn = nn.CrossEntropyLoss(weight=class_weights.to(device))

epochs = 30

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

best_acc = 0
best_model_wts = copy.deepcopy(model)

print('=' * 80)
print(f"NOTEBOOK_VERSION: {NOTEBOOK_VERSION}")
print(f"DATA_PIPELINE_VERSION: {DATA_PIPELINE_VERSION}")
print("Train class counts used by this run:", dict(zip(total_data.classes, class_counts.tolist())))
print("Test class counts used by this run:", dict(zip(total_data.classes, [test_counts[i] for i in range(len(total_data.classes))])))
print(f"Optimizer lr: {optimizer.param_groups[0]['lr']:.2E}")
print('=' * 80)

for epoch in range(epochs):
    model.train()
    train_epoch_loss, train_epoch_acc = train(train_dl, model, loss_fn, optimizer)

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

    if epoch_test_acc > best_acc:
        best_acc = epoch_test_acc
        best_model_wts = copy.deepcopy(model)

    train_acc.append(train_epoch_acc)
    train_loss.append(train_epoch_loss)
    test_acc.append(epoch_test_acc)
    test_loss.append(epoch_test_loss)

    lr = optimizer.param_groups[0]['lr']
    pred_dist = prediction_counts(test_dl, model)
    template = ("Epoch: {:2d}, Train_acc: {:.1f}%, Train_loss: {:.3f}, Test_acc: {:.1f}%, Test_loss: {:.3f}, Lr: {:.2E}, Pred: {}")
    print(template.format(epoch+1, train_epoch_acc*100, train_epoch_loss, epoch_test_acc*100, epoch_test_loss, lr, pred_dist))

PATH = './best_inception_v1.pth'
torch.save(best_model_wts.state_dict(), PATH)

print('Done.')
Epoch:  1, Train_acc: 67.0%, Train_loss: 0.750, Test_acc: 78.8%, Test_loss: 0.482, Lr: 1.00E-04, Pred: {'0Normal': 104, '2Mild': 41, '4Severe': 44}
Epoch:  2, Train_acc: 75.2%, Train_loss: 0.591, Test_acc: 81.5%, Test_loss: 0.509, Lr: 1.00E-04, Pred: {'0Normal': 96, '2Mild': 55, '4Severe': 38}
Epoch:  3, Train_acc: 81.3%, Train_loss: 0.495, Test_acc: 65.1%, Test_loss: 0.769, Lr: 1.00E-04, Pred: {'0Normal': 61, '2Mild': 109, '4Severe': 19}
Epoch:  4, Train_acc: 84.5%, Train_loss: 0.393, Test_acc: 86.8%, Test_loss: 0.322, Lr: 1.00E-04, Pred: {'0Normal': 105, '2Mild': 48, '4Severe': 36}
Epoch:  5, Train_acc: 86.3%, Train_loss: 0.338, Test_acc: 74.1%, Test_loss: 1.039, Lr: 1.00E-04, Pred: {'0Normal': 135, '2Mild': 27, '4Severe': 27}
Epoch:  6, Train_acc: 90.7%, Train_loss: 0.245, Test_acc: 87.8%, Test_loss: 0.615, Lr: 1.00E-04, Pred: {'0Normal': 105, '2Mild': 52, '4Severe': 32}
Epoch:  7, Train_acc: 90.9%, Train_loss: 0.246, Test_acc: 86.8%, Test_loss: 0.548, Lr: 1.00E-04, Pred: {'0Normal': 108, '2Mild': 52, '4Severe': 29}
Epoch:  8, Train_acc: 92.0%, Train_loss: 0.232, Test_acc: 89.4%, Test_loss: 0.444, Lr: 1.00E-04, Pred: {'0Normal': 90, '2Mild': 58, '4Severe': 41}
Epoch:  9, Train_acc: 93.3%, Train_loss: 0.190, Test_acc: 90.5%, Test_loss: 0.559, Lr: 1.00E-04, Pred: {'0Normal': 94, '2Mild': 58, '4Severe': 37}
[J6_FIXED_V4_BN] Epoch: 10, Train_acc: 93.3%, Train_loss: 0.213, Test_acc: 89.9%, Test_loss: 0.510, Lr: 1.00E-04, Pred: {'0Normal': 104, '2Mild': 48, '4Severe': 37}
Epoch: 11, Train_acc: 94.9%, Train_loss: 0.149, Test_acc: 89.9%, Test_loss: 0.431, Lr: 1.00E-04, Pred: {'0Normal': 106, '2Mild': 44, '4Severe': 39}
Epoch: 12, Train_acc: 94.1%, Train_loss: 0.157, Test_acc: 86.8%, Test_loss: 0.552, Lr: 1.00E-04, Pred: {'0Normal': 96, '2Mild': 48, '4Severe': 45}
Epoch: 13, Train_acc: 94.9%, Train_loss: 0.166, Test_acc: 86.8%, Test_loss: 0.853, Lr: 1.00E-04, Pred: {'0Normal': 89, '2Mild': 63, '4Severe': 37}
Epoch: 14, Train_acc: 96.0%, Train_loss: 0.130, Test_acc: 87.3%, Test_loss: 0.528, Lr: 1.00E-04, Pred: {'0Normal': 94, '2Mild': 56, '4Severe': 39}
Epoch: 15, Train_acc: 95.8%, Train_loss: 0.122, Test_acc: 86.8%, Test_loss: 0.659, Lr: 1.00E-04, Pred: {'0Normal': 102, '2Mild': 45, '4Severe': 42}
Epoch: 16, Train_acc: 95.1%, Train_loss: 0.153, Test_acc: 90.5%, Test_loss: 0.462, Lr: 1.00E-04, Pred: {'0Normal': 101, '2Mild': 48, '4Severe': 40}
Epoch: 17, Train_acc: 96.4%, Train_loss: 0.118, Test_acc: 88.9%, Test_loss: 0.517, Lr: 1.00E-04, Pred: {'0Normal': 113, '2Mild': 40, '4Severe': 36}
Epoch: 18, Train_acc: 96.8%, Train_loss: 0.104, Test_acc: 84.1%, Test_loss: 0.815, Lr: 1.00E-04, Pred: {'0Normal': 119, '2Mild': 38, '4Severe': 32}
Epoch: 19, Train_acc: 96.1%, Train_loss: 0.111, Test_acc: 85.2%, Test_loss: 0.751, Lr: 1.00E-04, Pred: {'0Normal': 94, '2Mild': 59, '4Severe': 36}
Epoch: 20, Train_acc: 96.7%, Train_loss: 0.103, Test_acc: 87.8%, Test_loss: 0.738, Lr: 1.00E-04, Pred: {'0Normal': 120, '2Mild': 35, '4Severe': 34}
Epoch: 21, Train_acc: 96.6%, Train_loss: 0.093, Test_acc: 85.7%, Test_loss: 0.822, Lr: 1.00E-04, Pred: {'0Normal': 106, '2Mild': 57, '4Severe': 26}
Epoch: 22, Train_acc: 96.9%, Train_loss: 0.084, Test_acc: 79.9%, Test_loss: 1.062, Lr: 1.00E-04, Pred: {'0Normal': 112, '2Mild': 53, '4Severe': 24}
Epoch: 23, Train_acc: 95.5%, Train_loss: 0.135, Test_acc: 87.3%, Test_loss: 0.791, Lr: 1.00E-04, Pred: {'0Normal': 102, '2Mild': 44, '4Severe': 43}
Epoch: 24, Train_acc: 97.1%, Train_loss: 0.084, Test_acc: 86.2%, Test_loss: 0.663, Lr: 1.00E-04, Pred: {'0Normal': 94, '2Mild': 58, '4Severe': 37}
Epoch: 25, Train_acc: 97.3%, Train_loss: 0.080, Test_acc: 87.3%, Test_loss: 0.847, Lr: 1.00E-04, Pred: {'0Normal': 103, '2Mild': 47, '4Severe': 39}
Epoch: 26, Train_acc: 96.9%, Train_loss: 0.100, Test_acc: 88.9%, Test_loss: 0.535, Lr: 1.00E-04, Pred: {'0Normal': 104, '2Mild': 48, '4Severe': 37}
Epoch: 27, Train_acc: 97.3%, Train_loss: 0.080, Test_acc: 88.4%, Test_loss: 0.642, Lr: 1.00E-04, Pred: {'0Normal': 101, '2Mild': 49, '4Severe': 39}
Epoch: 28, Train_acc: 96.9%, Train_loss: 0.088, Test_acc: 86.2%, Test_loss: 0.784, Lr: 1.00E-04, Pred: {'0Normal': 109, '2Mild': 46, '4Severe': 34}
Epoch: 29, Train_acc: 97.6%, Train_loss: 0.054, Test_acc: 89.9%, Test_loss: 0.622, Lr: 1.00E-04, Pred: {'0Normal': 103, '2Mild': 49, '4Severe': 37}
Epoch: 30, Train_acc: 96.6%, Train_loss: 0.115, Test_acc: 88.4%, Test_loss: 0.552, Lr: 1.00E-04, Pred: {'0Normal': 99, '2Mild': 46, '4Severe': 44}
Done.

其中 Pred 表示该 epoch 后模型在测试集上的预测类别分布,用于观察是否出现单类别塌缩。

4. 模型评估

4.1 可视化训练过程

绘制训练和测试的准确率、损失曲线,用于观察模型是否收敛、是否存在过拟合或欠拟合等问题。

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()

在这里插入图片描述

4.2 加载最优模型并评估

加载训练过程中保存的最优模型权重,在测试集上进行最终评估,输出测试准确率和损失值。

best_model_wts.load_state_dict(torch.load(PATH, map_location=device))
test_epoch_loss, test_epoch_acc = test(test_dl, best_model_wts, loss_fn)
test_epoch_acc, test_epoch_loss
(0.9047619047619048, 0.5591584973347684)

进一步输出混淆矩阵、各类别召回率和预测类别分布,分析模型对 Normal、Mild、Severe 三类的识别情况。

best_model_wts.eval()
confusion = torch.zeros(len(total_data.classes), len(total_data.classes), dtype=torch.int64)

with torch.no_grad():
    for imgs, target in test_dl:
        imgs = imgs.to(device)
        pred = best_model_wts(imgs).argmax(1).cpu()
        for true_label, pred_label in zip(target, pred):
            confusion[true_label, pred_label] += 1

print("Rows=true labels, columns=predicted labels")
print(total_data.classes)
print(confusion)
print("Per-class accuracy:", {
    total_data.classes[i]: (confusion[i, i].item() / confusion[i].sum().item() if confusion[i].sum().item() else 0)
    for i in range(len(total_data.classes))
})
print("Predicted counts:", dict(zip(total_data.classes, confusion.sum(dim=0).tolist())))
Rows=true labels, columns=predicted labels
['0Normal', '2Mild', '4Severe']
tensor([[93,  9,  1],
        [ 1, 43,  1],
        [ 0,  6, 35]])
Per-class accuracy: {'0Normal': 0.9029126213592233, '2Mild': 0.9555555555555556, '4Severe': 0.8536585365853658}
Predicted counts: {'0Normal': 94, '2Mild': 58, '4Severe': 37}

5.总结

  1. InceptionV1 引入 BatchNorm:在基础卷积模块中加入 BatchNorm2d 后,模型包含 57 个 BN 层,有助于稳定从零训练时的激活分布和梯度更新。

  2. 数据处理更加严格:原始数据中存在大量完全重复图像,因此本实验按 MD5 内容分组后再划分训练/测试集,避免同一张图的副本同时出现在训练集和测试集。

  3. 训练集类别均衡:原始训练集存在明显类别不均衡,均衡过采样后训练集变为 {0Normal: 812, 2Mild: 812, 4Severe: 812},多数类基线从约 61% 降至 33.3%。

  4. 眼底图像预处理:加入黑边裁剪和自动对比度增强,减少背景区域对模型训练的干扰,同时避免随机裁剪裁掉边缘病灶。

  5. 后续改进方向:当前 V4 版本需要重新完整训练后评估最终性能。如果测试准确率仍然偏低,建议进一步尝试预训练权重微调、学习率调度器、交叉验证,以及更系统的数据质量检查。

Logo

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

更多推荐