Day 41 简单CNN

今日任务:
  1. 数据增强
  2. 卷积神经网络定义的写法
  3. batch归一化:调整一个批次的分布,常用与图像数据
  4. 特征图:只有卷积操作输出的才叫特征图
  5. 调度器:直接修改基础学习率

作业:尝试手动修改下不同的调度器和CNN的结构,观察训练的差异。

由于MLP将所有的像素全部展平留下了全局信息,忽略了局部信息。但是,对于图像数据来说,相邻像素间存在强相关性,因此引入了卷积神经网络(CNN)

学习资料:

【综述】一文读懂卷积神经网络(CNN)

图解,卷积神经网络(CNN可视化)

CNN的基本结构

与MLP不同,CNN认为一个像素与其相邻像素的关系最紧密,与遥远像素的关系较弱。它的核心思想是:局部连接参数共享

CNN有三个核心构件:

  • 卷积层特征提取器。通过小的滑动窗口(卷积核),在图像上滑动,通过计算点乘来检测局部特征。浅层网络检测简单特征,深层网络组合简单特征得到复杂特征。
  • 池化层信息压缩。通过下采样操作,减少冗余信息,常用最大池化或平均池化两种方法。
  • 线性层:将提取出的高级特征图展平,输入全连接网络,进行分类操作。

CNN定义的基本流程

数据增强

数据预处理的加强版,之前在预处理阶段只采用了两个基本的操作(归一化和标准化)。实际上,可以通过对图像进行变换操作从而增加数据的多样性,也能让模型更加关注关键特征,而不是无关特征(如位置、色彩)。

常见的修改策略如下:

  • 几何变换:如旋转、缩放、平移、剪裁、裁剪、翻转
  • 像素变换:如修改颜色、亮度、对比度、饱和度、色相、高斯模糊(模拟对焦失败)、增加噪声、马赛克
  • 语义增强(暂时不用):mixup,对图像进行结构性改造、cutout随机遮挡等

注意:数据增强不会改变数据量,而是通过变换后替换原数据。这样,每次训练的图片存在一定差异,但是每次训练的数据数目是相同的。

模型定义

定义模型时,分成卷积块和全连接层两种。其中全连接层的定义与之前相同,这里说明卷积块。

对于每一个卷积块,包含以下部分:

输入 -> 卷积1 -> BN1 -> [ReLU] ->池化1 ->  下一层

(1)卷积层

输入图像后,通过卷积核筛选特征,输出特征图。

  • 卷积核大小:Filter size,如3x3、5x5、7x7等。
  • 输入通道数:输入图片的通道数,如1(单通道图片)、3(RGB图片)、4(RGBA图片)等。
  • 输出通道数:卷积核的个数,即输出的通道数。如本模型中通过 32→64→128 逐步增加特征复杂度
  • 步长(stride):卷积核的滑动步长,默认为1。
  • 填充(padding):在图像周围填充一圈值,可以让输出特征图保持与输入一样的大小

特征图的尺寸:Output Size = (Input Size - Filter Size + 2 * Padding) / Stride + 1

注:通过可视化中间层的特征图,理解 CNN 如何从底层特征(如边缘)逐步提取高层语义特征(如物体部件、整体结构),对应于深度学习的可解释性

(2)批量归一化层(可选)

可以加速模型收敛提升泛化能力,一般在卷积层后。

使用的原因:深层网络中,随着前层参数更新,后层输入分布会发生变化,导致模型需要不断适应新分布,训练难度增加。通过对每个批次的输入数据进行标准化(均值为 0、方差为 1),可以让各层的输入分布稳定,便于训练。

深度学习中的归一化有2类:

  • Batch Normalization:一般用于图像数据,图像数据通常是批量处理,batch_size固定
  • Layer Normalization:一般用于文本数据,文本数据的长度一般不同,所以使用对单个样本的所有隐藏单元进行归一化,不依赖批次的方法。

(3)激活函数

使用最常用的ReLU函数,引入非线性,使网络能够学习复杂模式。

由于卷积和归一化都是线性或仿射操作,而线性操作的组合(叠加)仍然是线性操作(多层线性操作本质为单层线性)。因此,需要将线性卷积提取的简单特征,通过非线性组合变成复杂的、有判别性的特征,即使用激活函数。

(4)池化层

MaxPool2d(最大池化),扩大感受野,增强平移不变性

模型定义完整代码:三层卷积 + 两层全连接层

# 4- 模型定义
class CNN(nn.Module):
    def __init__(self):
        super(CNN,self).__init__()
        # ----------卷积块1的定义-------------
        self.conv1 = nn.Conv2d(
            in_channels=3, # 输入通道数
            out_channels=32, # 输出通道数
            kernel_size=3, # 卷积核的大小,3*3
            stride=1, # 步长
            padding=1 # 填充
        ) # 卷积层的定义,输出32*32*32
        self.bn1 = nn.BatchNorm2d(num_features=32) # 批量归一化
        self.relu1 = nn.ReLU() # 激活函数
        self.maxpool1 = nn.MaxPool2d(kernel_size=2,stride=2) # 池化层,输出32*16*16
        # ---------卷积块2的定义--------------
        self.conv2 = nn.Conv2d(
            in_channels=32, # 输入通道数
            out_channels=64, # 输出通道数
            kernel_size=3, # 卷积核的大小,3*3
            stride=1, # 步长
            padding=1 # 填充 
        ) # 卷积层的定义,输出64*16*16
        self.bn2 = nn.BatchNorm2d(num_features=64) # 批量归一化
        self.relu2 = nn.ReLU() # 激活函数
        self.maxpool2 = nn.MaxPool2d(kernel_size=2,stride=2) # 池化层,输出64*8*8
        # -----------卷积块3的定义---------------
        self.conv3 = nn.Conv2d(
            in_channels=64, # 输入通道数
            out_channels=128, # 输出通道数
            kernel_size=3, # 卷积核的大小,3*3
            stride=1, # 步长
            padding=1 # 填充 
        ) # 卷积层的定义,输出128*8*8
        self.bn3 = nn.BatchNorm2d(num_features=128) # 批量归一化
        self.relu3 = nn.ReLU() # 激活函数
        self.maxpool3 = nn.MaxPool2d(kernel_size=2,stride=2) # 池化层,输出128*4*4
        # 全连接层的定义
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(128*4*4,512) # 第一层
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(512,10)

    def forward(self,x):
        # 卷积层1
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)
        x = self.maxpool1(x)
        # 卷积层2
        x = self.conv2(x)
        x = self.bn2(x)
        x = self.relu2(x)
        x = self.maxpool2(x)
        # 卷积层3
        x = self.conv3(x)
        x = self.bn3(x)
        x = self.relu3(x)
        x = self.maxpool3(x)
        # 全连接层
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.relu3(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x
    
# 设置设备
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('使用的设备:{}'.format(device))

model = CNN().to(device)

调度器

调度器的全称是学习率调度器。它的主要作用是在训练过程中动态地调整学习率

学习率:优化器(如SGD、Adam)在更新模型参数时的步长大小。在训练初期,如果学习率太小,可能收敛速度极慢,训练时间长;学习率太大,可能无法收敛,甚至在最优点附近来回震荡。在训练后期,需要调整学习率,以较小的步长逼近最优点。因此,固定的学习率存在劣势。

特性优化器调度器
核心职责

参数更新 

决定如何根据梯度更新模型权重

学习率调整 

决定何时以及如何调整学习率大小

工作阶段每个batch训练后立即工作通常在每个epoch结束后工作
控制对象模型权重(Weights & Biases)优化器的超参数(主要是学习率)
是否必需 - 没有优化器无法训练 - 可以使用固定学习率,但强烈推荐使用
常见示例SGD, Adam, RMSprop, AdagradStepLR, CosineAnnealingLR, ReduceLROnPlateau

调度器通过预设的策略,在训练的不同阶段自动调整这个步长大小。常见的调度器有:

  • ReduceLROnPlateau:默认首选,简单有效,自动适应训练状态
  • StepLR 或 MultiStepLR:简单任务
  • CosineAnnealingWarmRestarts:需要周期性探索,适合大模型
  • OneCycleLR:快速训练,减少手动调参
# 定义调度器
#引入学习率调度器,在训练过程中动态调整学习率--训练初期使用较大的 LR 快速降低损失,训练后期使用较小的 LR 更精细地逼近全局最优解。
# 在每个 epoch 结束后,需要手动调用调度器来更新学习率,可以在训练过程中调用 scheduler.step()
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer=optimizer,        # 指定要控制的优化器(这里是Adam)
    mode='min',       # 监测的指标是"最小化"(如损失函数)
    patience=3,       # 如果连续3个epoch指标没有改善,才降低LR
    factor=0.5        # 降低LR的比例(新LR = 旧LR × 0.5)
)

##其它调度器
# scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)  
# # 每5个epoch,LR = LR × 0.1  

# scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=[10, 20, 30], gamma=0.5)  
# # 当epoch=10、20、30时,LR = LR × 0.5  

# scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10, eta_min=0.0001)  
# # LR在[0.0001, LR_initial]之间按余弦曲线变化,周期为2×T_max  

总结

今日简单CNN训练Cifar-10的完整代码(没有修改调度器和CNN结构的版本):

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader
from torchvision import datasets,transforms

# 1-预处理
# 对于训练集使用数据增强,提高模型泛化能力
train_transform = transforms.Compose([
    transforms.RandomCrop(32,padding=4), # 随机裁剪
    transforms.RandomHorizontalFlip(), # 随机水平翻转,p=0.5
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 随机颜色抖动:亮度、对比度、饱和度和色调随机变化
    transforms.RandomRotation(15),  # 随机旋转图像(最大角度为15度)
    transforms.ToTensor(),  # 将PIL图像或numpy数组转换为张量
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) # 标准化
])
# 对于测试集,不做数据加强处理
test_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])

# 2-导入数据
train_dataset = datasets.CIFAR10(
    root='./data',
    train=True,
    download=True,
    transform=train_transform,
)

test_dataset = datasets.CIFAR10(
    root='./data',
    train=False,
    transform=test_transform
)

# 3- 创建dataloader
batch_size = 64
train_loader = DataLoader(train_dataset,batch_size,shuffle=True)
test_loader = DataLoader(test_dataset,batch_size,shuffle=False)

# 4- 模型定义
class CNN(nn.Module):
    def __init__(self):
        super(CNN,self).__init__()
        # ----------卷积块1的定义-------------
        self.conv1 = nn.Conv2d(
            in_channels=3, # 输入通道数
            out_channels=32, # 输出通道数
            kernel_size=3, # 卷积核的大小,3*3
            stride=1, # 步长
            padding=1 # 填充
        ) # 卷积层的定义,输出32*32*32
        self.bn1 = nn.BatchNorm2d(num_features=32) # 批量归一化
        self.relu1 = nn.ReLU() # 激活函数
        self.maxpool1 = nn.MaxPool2d(kernel_size=2,stride=2) # 池化层,输出32*16*16
        # ---------卷积块2的定义--------------
        self.conv2 = nn.Conv2d(
            in_channels=32, # 输入通道数
            out_channels=64, # 输出通道数
            kernel_size=3, # 卷积核的大小,3*3
            stride=1, # 步长
            padding=1 # 填充 
        ) # 卷积层的定义,输出64*16*16
        self.bn2 = nn.BatchNorm2d(num_features=64) # 批量归一化
        self.relu2 = nn.ReLU() # 激活函数
        self.maxpool2 = nn.MaxPool2d(kernel_size=2,stride=2) # 池化层,输出64*8*8
        # -----------卷积块3的定义---------------
        self.conv3 = nn.Conv2d(
            in_channels=64, # 输入通道数
            out_channels=128, # 输出通道数
            kernel_size=3, # 卷积核的大小,3*3
            stride=1, # 步长
            padding=1 # 填充 
        ) # 卷积层的定义,输出128*8*8
        self.bn3 = nn.BatchNorm2d(num_features=128) # 批量归一化
        self.relu3 = nn.ReLU() # 激活函数
        self.maxpool3 = nn.MaxPool2d(kernel_size=2,stride=2) # 池化层,输出128*4*4
        # 全连接层的定义
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(128*4*4,512) # 第一层
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(512,10)

    def forward(self,x):
        # 卷积层1
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)
        x = self.maxpool1(x)
        # 卷积层2
        x = self.conv2(x)
        x = self.bn2(x)
        x = self.relu2(x)
        x = self.maxpool2(x)
        # 卷积层3
        x = self.conv3(x)
        x = self.bn3(x)
        x = self.relu3(x)
        x = self.maxpool3(x)
        # 全连接层
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.relu3(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x
    
# 设置设备
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('使用的设备:{}'.format(device))

model = CNN().to(device)

# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr=0.001)

scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer=optimizer,        # 指定要控制的优化器(这里是Adam)
    mode='min',       # 监测的指标是"最小化"(如损失函数)
    patience=3,       # 如果连续3个epoch指标没有改善,才降低LR
    factor=0.5        # 降低LR的比例(新LR = 旧LR × 0.5)
)

# 5-训练
def train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs):
    model.train() # 进入训练模式

    all_iter_losses = []
    iteration_indices = []

    train_history_loss = []
    train_history_acc = []
    test_history_loss = []
    test_history_acc = []

    for epoch in range(epochs):
        running_loss = 0
        total = 0
        correct = 0
        for batch_idx,(data,target) in enumerate(train_loader):
            data,target = data.to(device),target.to(device)

            optimizer.zero_grad() # 清零
            output = model(data) # 前向传播
            loss = criterion(output,target) # 计算损失值
            loss.backward() # 反向传播
            optimizer.step() # 更新参数

            # 存储
            loss_value = loss.item()
            all_iter_losses.append(loss_value)
            iteration_indices.append(epoch*len(train_loader) + batch_idx + 1)

            # 统计
            running_loss += loss_value
            total += target.size(0)
            _,predicted = output.max(1)
            correct += predicted.eq(target).sum().item()
            
            # 每100个批次打印一次训练信息
            if (batch_idx + 1) % 100 == 0:
                print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} '
                      f'| 单Batch损失: {loss_value:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}')

            # 计算
            epoch_train_loss = running_loss / len(train_loader)
            epoch_train_acc = 100.*correct / total
            epoch_test_loss,epoch_test_acc = test(model, test_loader, criterion, device) # 测试集

            # 记录历史数据
            train_history_loss.append(epoch_train_loss)
            train_history_acc.append(epoch_train_acc)
            test_history_loss.append(epoch_test_loss)
            test_history_acc.append(epoch_test_acc)

            # 更新学习率调度器
            if scheduler:
                scheduler.step(epoch_test_loss)
            # 打印准确率
            print(f'Epoch {epoch+1}/{epochs} 完成 | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%')

    # 可视化            
    plot_iteration_loss(all_iter_losses,iteration_indices)
    plot_epoch_metrics(train_history_acc, test_history_acc, train_history_loss, test_history_loss)

    return epoch_test_acc


# 6-测试
def test(model, test_loader, criterion, device):
    model.eval()
    test_losses = 0
    test_total = 0
    test_correct = 0
    with torch.no_grad(): # 关闭梯度
        for data,target in test_loader:
            data,target = data.to(device),target.to(device)
            out = model(data) 
            loss = criterion(out,target)
            # 统计
            test_losses += loss.item()
            test_total += target.size(0)
            _,predicted = out.max(1)
            test_correct += predicted.eq(target).sum().item()
            # 计算
            avg_loss = test_losses / len(test_loader)
            accuracy = 100.*test_correct / test_total
    return avg_loss,accuracy
    
# 7-可视化
def plot_iteration_loss(losses,indices):
    plt.figure(figsize=(10,6))
    plt.plot(indices,losses,alpha=0.7,label='Iteration Loss')
    plt.xlabel('Iteration')
    plt.ylabel('Loss')
    plt.title('Train Loss of Each Iteration')
    plt.legend()
    plt.grid(True)
    plt.show()

def plot_epoch_metrics(train_acc, test_acc, train_loss, test_loss):
    epochs = range(1, len(train_acc) + 1)
    
    plt.figure(figsize=(12, 4))
    
    # 绘制准确率曲线
    plt.subplot(1, 2, 1)
    plt.plot(epochs, train_acc, 'b-', label='训练准确率')
    plt.plot(epochs, test_acc, 'r-', label='测试准确率')
    plt.xlabel('Epoch')
    plt.ylabel('准确率 (%)')
    plt.title('训练和测试准确率')
    plt.legend()
    plt.grid(True)
    
    # 绘制损失曲线
    plt.subplot(1, 2, 2)
    plt.plot(epochs, train_loss, 'b-', label='训练损失')
    plt.plot(epochs, test_loss, 'r-', label='测试损失')
    plt.xlabel('Epoch')
    plt.ylabel('损失值')
    plt.title('训练和测试损失')
    plt.legend()
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

# 8-调用
epochs = 20
print("开始使用CNN训练模型...")
final_acc = train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs)
print('训练完成!最终测试准确率:{:.2f}%'.format(final_acc))

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值