【深度学习实验】—— Inception v1算法实战

文章目录

  • [1. 简介](#1. 简介)
  • [2. 环境](#2. 环境)
  • [3. 代码实现](#3. 代码实现)
    • [3.1 前期准备](#3.1 前期准备)
      • [3.1.1 设置GPU & 导入库](#3.1.1 设置GPU & 导入库)
      • [3.1.2 数据加载](#3.1.2 数据加载)
    • [3.2 模型建立与训练](#3.2 模型建立与训练)
      • [3.2.1 定义 InceptionV1 网络模型](#3.2.1 定义 InceptionV1 网络模型)
      • [3.2.2 模型结构概览](#3.2.2 模型结构概览)
      • [3.2.3 定义训练和测试函数](#3.2.3 定义训练和测试函数)
      • [3.2.4 训练模型](#3.2.4 训练模型)
      • 训练配置:
      • 训练策略:
  • [4. 模型评估](#4. 模型评估)
    • [4.1 可视化训练过程](#4.1 可视化训练过程)
    • [4.2 加载最优模型并评估](#4.2 加载最优模型并评估)
  • 5.总结

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 设备。

python 复制代码
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
bash 复制代码
cpu

3.1.2 数据加载

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

python 复制代码
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:训练集包含轻量增强,测试集保持确定性预处理。

python 复制代码
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
bash 复制代码
Raw samples: 1661
Classes: {'0Normal': 0, '2Mild': 1, '4Severe': 2}
Raw class counts: {'0Normal': 1017, '2Mild': 232, '4Severe': 412}

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

python 复制代码
total_data.class_to_idx
bash 复制代码
{'0Normal': 0, '2Mild': 1, '4Severe': 2}

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

python 复制代码
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)}'
bash 复制代码
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 的形状。

python 复制代码
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
bash 复制代码
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)。

python 复制代码
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)
python 复制代码
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
bash 复制代码
BatchNorm2d layers: 57
bash 复制代码
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 个参数,且全部可训练。

python 复制代码
summary.summary(model, (3, 224, 224))
bash 复制代码
----------------------------------------------------------------
        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() 函数 --- 统计模型在测试集上的预测类别分布,用于观察模型是否塌缩到单一类别。
python 复制代码
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 预测分布,辅助判断是否出现类别塌缩
python 复制代码
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.')
bash 复制代码
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 可视化训练过程

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

python 复制代码
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 加载最优模型并评估

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

python 复制代码
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
bash 复制代码
(0.9047619047619048, 0.5591584973347684)

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

python 复制代码
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())))
bash 复制代码
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 版本需要重新完整训练后评估最终性能。如果测试准确率仍然偏低,建议进一步尝试预训练权重微调、学习率调度器、交叉验证,以及更系统的数据质量检查。

相关推荐
XMAIPC_Robot4 小时前
软硬协同实时控制|RK3588业务调度+FPGA硬件时序,ethercat实现半导体设备微秒级响应(125us)
linux·arm开发·人工智能·fpga开发
用户938515635074 小时前
告别“屎山”与“幻觉”:从“Vibe Coding”到“胶水编程”的AI协作实践指南
人工智能
北五环小裁缝4 小时前
Coding工具搜索不够全?我做个 deuseek:一条命令补齐全网搜索
人工智能·搜索引擎
扑克中的黑桃A4 小时前
Dify + DeepSeek 搭了一个“职场规矩智能体”,专治各种尴尬
人工智能
临床数据科学和人工智能兴趣组4 小时前
RStudio的Console(控制台)是一个非常重要的组件
人工智能·机器学习·数据分析·r语言·r语言-4.2.1
AOwhisky4 小时前
Python 学习笔记(第十五期)——运维自动化(下·后篇):堡垒机实战——paramiko高阶篇
运维·python·学习·云原生·自动化·运维开发
观察员4 小时前
带你用一个 Python 图书系统,通关封装继承多态
python
科技互联.4 小时前
2026年7月对话一家AI增长培训公司:如何将技术著作权转化为可落地课程
人工智能
DisonTangor4 小时前
【微软开源】Mage-Flow 深度解读:微软4B参数图像生成与编辑模型,单卡A100仅需0.59秒
人工智能·microsoft·ai作画·开源·aigc
烬羽4 小时前
递归老写崩?一个"退回"公式,把回溯题变成填空题
javascript·深度学习·算法