- 数据增强
- 卷积神经网络定义的写法
- batch归一化:调整一个批次的分布,常用与图像数据
- 特征图:只有卷积操作输出的才叫特征图
- 调度器:直接修改基础学习率
作业:尝试手动修改下不同的调度器和CNN的结构,观察训练的差异。
由于MLP将所有的像素全部展平留下了全局信息,忽略了局部信息。但是,对于图像数据来说,相邻像素间存在强相关性,因此引入了卷积神经网络(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, Adagrad | StepLR, 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))



239

被折叠的 条评论
为什么被折叠?



