Day 45 简单CNN

@浙大疏锦行

一、数据增强(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}")
相关推荐
自不量力的A同学2 小时前
苹果发布开源 AI 模型 SHARP
人工智能
Hcoco_me2 小时前
机器学习核心概念与主流算法(通俗详细版)
人工智能·算法·机器学习·数据挖掘·聚类
Herlie2 小时前
AI 创业这三年:我的三次认知迭代与自我修正
大数据·人工智能
感谢地心引力2 小时前
【AI】加入AI绘图的视频封面快速编辑器
人工智能·python·ai·ffmpeg·音视频·pyqt·gemini
min1811234562 小时前
具身智能(Embodied AI)逼近:机器人如何更好地理解物理世界?
人工智能·机器人
空中湖2 小时前
[特殊字符] 圣诞愿望池 - 一个充满魔力的在线许愿平台
人工智能·机器学习
Jorunk2 小时前
【读论文】DNN-Based Acoustic Modeling for Russian Speech Recognition Using Kaldi
人工智能·神经网络·dnn
跟YY哥学Jira2 小时前
2026 Atlassian 认证体系重大变革:全面拥抱云时代与 AI 战略
人工智能·经验分享·项目管理·atlassian·认证·jira
凤希AI伴侣2 小时前
界面重构与本地化实践:凤希AI伴侣的自动化演进思考 凤希AI伴侣2025年12月21日
人工智能·重构·自动化·凤希ai伴侣