一、数据增强(Data Augmentation)
核心目的
通过对训练集图像进行随机变换(如翻转、旋转、裁剪),扩充有效训练数据、防止过拟合、提升模型泛化能力;测试集仅做必要的归一化 / 缩放,不做随机增强(保证评估结果稳定)。
1. 常用增强手段
| 增强方式 | 作用 | 适用场景 |
|---|---|---|
| RandomHorizontalFlip | 随机水平翻转(50% 概率) | 通用(如猫狗分类、MNIST) |
| RandomRotation | 随机旋转(如 ±15°) | 手写数字、物体分类 |
| RandomResizedCrop | 随机裁剪后缩放(模拟不同视角) | 彩色图像(如 ImageNet) |
| ColorJitter | 随机调整亮度 / 对比度 / 饱和度 | 彩色图像 |
| ToTensor + Normalize | 转为张量 + 归一化(均值 / 标准差) | 所有图像(必须) |
2. 实战代码
import torch
from torchvision import transforms
from torch.utils.data import Dataset
from PIL import Image
import os
# 区分训练/测试集的增强策略
def get_augmentation(is_train=True, img_size=(28,28), is_gray=True):
"""
获取数据增强管道
:param is_train: 训练集True/测试集False
:param img_size: 图像尺寸
:param is_gray: 是否灰度图
"""
# 基础变换(所有数据集都需要)
basic_transforms = [
transforms.Resize(img_size), # 缩放至指定尺寸
transforms.ToTensor(), # HWC→CHW,0-255→0-1
]
# 归一化(灰度图用单通道均值/标准差,彩色用RGB均值/标准差)
if is_gray:
basic_transforms.append(transforms.Normalize(mean=[0.5], std=[0.5])) # 灰度图归一化到[-1,1]
else:
basic_transforms.append(transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])) # ImageNet标准
# 训练集增强(随机变换)
if is_train:
train_aug = [
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomRotation(degrees=15), # 随机旋转±15°
transforms.RandomAffine(degrees=0, translate=(0.1,0.1)), # 随机平移
] + basic_transforms
return transforms.Compose(train_aug)
# 测试集增强(仅基础变换,无随机)
else:
return transforms.Compose(basic_transforms)
# 整合增强的Dataset
class AugImageDataset(Dataset):
def __init__(self, img_dir, labels, img_size=(28,28), is_gray=True, is_train=True):
self.img_dir = img_dir
self.labels = labels
self.img_paths = [os.path.join(img_dir, f"{i}.png") for i in range(len(labels))]
self.transform = get_augmentation(is_train, img_size, is_gray)
def __len__(self):
return len(self.img_paths)
def __getitem__(self, idx):
img = Image.open(self.img_paths[idx]).convert('L' if self.is_gray else 'RGB')
img = self.transform(img) # 应用数据增强
label = torch.tensor(self.labels[idx], dtype=torch.long)
return img, label
# 测试增强效果
if __name__ == "__main__":
# 模拟100个样本
labels = [0]*50 + [1]*50
dataset = AugImageDataset(img_dir="./mnist_imgs", labels=labels, is_train=True)
img, label = dataset[0]
print(f"增强后图像形状:{img.shape}") # (1,28,28)(灰度)/ (3,28,28)(彩色)
二、CNN 定义
1. BN 层的核心作用
- 加速收敛:使每一层的输入分布稳定,无需手动调学习率;
- 防止过拟合:轻微正则化效果,可减少 Dropout 的使用;
- 缓解梯度消失:激活函数输入更合理,避免梯度趋近于 0。
2. BN 层的规范写法
| 层 | 代码 | 核心作用(通俗解释) | 输入→输出尺寸(28×28 灰度图) |
|---|---|---|---|
| 输入 | - | 原始图像(B,1,28,28) | (B,1,28,28) → 不变 |
| 卷积层 1 | conv1 |
用 16 个 3×3 窗口 "扫图",提取边缘 / 纹理等基础特征 | (B,1,28,28) → (B,16,28,28) |
| ReLU 激活 | F.relu() |
引入非线性,让模型能学习复杂模式(去掉负数) | 尺寸不变 |
| 池化层 | pool |
把图像缩小一半(14×14),减少计算量,保留关键特征 | (B,16,28,28) → (B,16,14,14) |
| 卷积层 2 | conv2 |
用 32 个窗口提取更复杂的特征(如线条组合) | (B,16,14,14) → (B,32,14,14) |
| ReLU 激活 | F.relu() |
继续引入非线性 | 尺寸不变 |
| 池化层 | pool |
再缩小一半(7×7) | (B,32,14,14) → (B,32,7,7) |
| 展平 | x.view(-1, 32*7*7) |
把 4 维特征 "摊平" 成 1 维,喂给全连接层 | (B,32,7,7) → (B,1568) |
| 全连接层 1 | fc1 |
整合所有特征,输出 128 维特征向量 | (B,1568) → (B,128) |
| ReLU 激活 | F.relu() |
继续引入非线性 | 尺寸不变 |
| 全连接层 2 | fc2 |
输出分类结果(如 10 类就输出 10 个值) | (B,128) → (B,10) |
BN 层需放在卷积层后、激活函数前 (行业最佳实践),公式:Conv → BN → ReLU → Pool
3. 含 BN 的 CNN 完整定义
import torch.nn as nn
import torch.nn.functional as F
class CNNWithBN(nn.Module):
"""
含批量归一化的CNN(适配灰度/彩色)
:param in_channels: 输入通道(1=灰度,3=彩色)
:param num_classes: 分类类别数
:param img_size: 输入图像尺寸 (H,W)
"""
def __init__(self, in_channels=1, num_classes=10, img_size=(28,28)):
super().__init__()
# 卷积块1:Conv → BN → ReLU → MaxPool
self.conv1 = nn.Conv2d(in_channels, 16, 3, padding=1)
self.bn1 = nn.BatchNorm2d(16) # BN层(输入通道数=16)
self.pool1 = nn.MaxPool2d(2, 2)
# 卷积块2:Conv → BN → ReLU → MaxPool
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.bn2 = nn.BatchNorm2d(32) # BN层(输入通道数=32)
self.pool2 = nn.MaxPool2d(2, 2)
# 计算全连接层输入维度
h, w = img_size
fc_in_dim = 32 * (h//4) * (w//4)
# 全连接层(含Dropout防止过拟合)
self.fc1 = nn.Linear(fc_in_dim, 128)
self.bn3 = nn.BatchNorm1d(128) # 全连接层用1D BN
self.dropout = nn.Dropout(0.2)
self.fc2 = nn.Linear(128, num_classes)
def forward(self, x):
# 卷积块1
x = self.conv1(x)
x = self.bn1(x) # BN层
x = F.relu(x) # 激活
x = self.pool1(x) # 池化
# 卷积块2
x = self.conv2(x)
x = self.bn2(x)
x = F.relu(x)
x = self.pool2(x)
# 展平
x = x.view(x.size(0), -1)
# 全连接层
x = self.fc1(x)
x = self.bn3(x) # 全连接层BN
x = F.relu(x)
x = self.dropout(x)
x = self.fc2(x)
return F.softmax(x, dim=1)
# 测试模型
if __name__ == "__main__":
# 灰度图模型
gray_cnn = CNNWithBN(in_channels=1, num_classes=10, img_size=(28,28))
gray_input = torch.randn(8, 1, 28, 28)
gray_output = gray_cnn(gray_input)
print(f"灰度模型输出形状:{gray_output.shape}") # (8,10)
# 彩色图模型
color_cnn = CNNWithBN(in_channels=3, num_classes=2, img_size=(224,224))
color_input = torch.randn(4, 3, 224, 224)
color_output = color_cnn(color_input)
print(f"彩色模型输出形状:{color_output.shape}") # (4,2)
4. BN 层关键注意事项
- 训练时
model.train():BN 层会计算批次均值 / 方差; - 测试时
model.eval():BN 层使用训练时统计的全局均值 / 方差(必须切换模式,否则结果错误); - 卷积层用
BatchNorm2d,全连接层用BatchNorm1d,维度要匹配。
三、特征图(Feature Map)
1. 特征图核心概念
特征图是卷积层的输出张量,对应输入图像经过卷积核提取后的特征表示:
- 第 1 层卷积:提取边缘、纹理、颜色等基础特征;
- 深层卷积:提取更复杂的特征(如形状、部件、物体);
- 形状:
(Batch, 输出通道数, 特征图高度, 特征图宽度)。
2. 特征图提取与可视化
import matplotlib.pyplot as plt
import numpy as np
def visualize_feature_maps(model, input_img, layer_name="conv1"):
"""
提取并可视化指定层的特征图
:param model: CNN模型
:param input_img: 单张输入图像 (1, C, H, W)
:param layer_name: 要可视化的卷积层名
"""
# 1. 注册钩子,提取特征图
feature_maps = []
def hook_fn(module, input, output):
feature_maps.append(output)
# 找到指定层并注册钩子
for name, module in model.named_modules():
if name == layer_name:
hook = module.register_forward_hook(hook_fn)
break
# 2. 前向传播,触发钩子
model.eval()
with torch.no_grad():
model(input_img)
# 3. 移除钩子
hook.remove()
# 4. 可视化特征图(取前16个通道)
fm = feature_maps[0].squeeze(0) # (C, H, W)
n_rows = 4
n_cols = 4
fig, axes = plt.subplots(n_rows, n_cols, figsize=(10,10))
for i, ax in enumerate(axes.flat):
if i >= fm.shape[0]:
break
ax.imshow(fm[i].cpu().numpy(), cmap="gray")
ax.axis("off")
ax.set_title(f"Channel {i+1}")
plt.suptitle(f"Feature Maps of {layer_name}")
plt.tight_layout()
plt.show()
# 测试可视化
if __name__ == "__main__":
# 加载模型
model = CNNWithBN(in_channels=1, num_classes=10)
# 单张输入图像(1,1,28,28)
input_img = torch.randn(1, 1, 28, 28)
# 可视化conv1的特征图
visualize_feature_maps(model, input_img, layer_name="conv1")
# 可视化conv2的特征图
visualize_feature_maps(model, input_img, layer_name="conv2")
可视化效果说明
conv1特征图:以边缘、纹理为主,能看到图像的基础轮廓;conv2特征图:更抽象,能看到组合后的特征(如线条、拐角)。
四、学习率调度器(Learning Rate Scheduler)
1. 核心作用
动态调整学习率:训练初期用大学习率快速收敛,后期用小学习率精细优化,避免模型陷入局部最优。
2. 常用调度器及使用方法
| 调度器类型 | 核心逻辑 | 适用场景 |
|---|---|---|
| StepLR | 每 step_size 轮,学习率 ×gamma | 通用,简单易调 |
| ReduceLROnPlateau | 监控验证集指标,无提升则降低学习率 | 追求最优效果,自适应调整 |
| CosineAnnealingLR | 学习率按余弦曲线周期性变化 | 大数据集、长训练周期 |
3. 调度器整合到训练循环
import torch.optim as optim
from torch.utils.data import DataLoader
# 1. 准备数据(模拟)
labels = [0]*100 + [1]*100
dataset = AugImageDataset(img_dir="./mnist_imgs", labels=labels, is_train=True)
train_loader = DataLoader(dataset, batch_size=32, shuffle=True)
# 2. 初始化模型、优化器、损失函数
model = CNNWithBN(in_channels=1, num_classes=2).to("cuda" if torch.cuda.is_available() else "cpu")
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
# 3. 定义调度器(二选一)
# 调度器1:StepLR(每5轮学习率×0.1)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
# 调度器2:ReduceLROnPlateau(监控验证损失,3轮无提升则×0.1)
# scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=3, verbose=True)
# 4. 训练循环(整合调度器)
num_epochs = 10
for epoch in range(num_epochs):
model.train()
train_loss = 0.0
for imgs, labels in train_loader:
imgs = imgs.to(model.device)
labels = labels.to(model.device)
optimizer.zero_grad()
outputs = model(imgs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
# 5. 更新学习率
scheduler.step() # StepLR:每轮更新
# scheduler.step(val_loss) # ReduceLROnPlateau:传入验证损失更新
# 打印当前学习率
current_lr = optimizer.param_groups[0]['lr']
print(f"Epoch {epoch+1}, Loss: {train_loss/len(train_loader):.4f}, LR: {current_lr:.6f}")