- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
文章目录
- [1. 简介](#1. 简介)
- [2. 环境](#2. 环境)
- [3. 代码实现](#3. 代码实现)
- [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.总结
-
InceptionV1 引入 BatchNorm :在基础卷积模块中加入
BatchNorm2d后,模型包含 57 个 BN 层,有助于稳定从零训练时的激活分布和梯度更新。 -
数据处理更加严格:原始数据中存在大量完全重复图像,因此本实验按 MD5 内容分组后再划分训练/测试集,避免同一张图的副本同时出现在训练集和测试集。
-
训练集类别均衡 :原始训练集存在明显类别不均衡,均衡过采样后训练集变为
{0Normal: 812, 2Mild: 812, 4Severe: 812},多数类基线从约 61% 降至 33.3%。 -
眼底图像预处理:加入黑边裁剪和自动对比度增强,减少背景区域对模型训练的干扰,同时避免随机裁剪裁掉边缘病灶。
-
后续改进方向:当前 V4 版本需要重新完整训练后评估最终性能。如果测试准确率仍然偏低,建议进一步尝试预训练权重微调、学习率调度器、交叉验证,以及更系统的数据质量检查。