Datawhale AI 夏令营(第五期) 李宏毅苹果书 Task 2 《深度学习详解(进阶)》

Task 2.1 《深度学习详解》- 3.3&4&5 自适应学习率

神经网络训练不起来怎么办(3):自动调整学习速率(Learning Rate)_哔哩哔哩_bilibili

教材内容分析了深度学习中优化算法的演变过程,从最初的梯度下降法到自适应学习率方法(AdaGrad、RMSProp、Adam)以及学习率调度策略(学习率衰减、预热)。通过理解这些算法的原理和优缺点,我们可以更好地选择和调整优化器,以提高深度学习模型的训练效率和效果。

3.3 自适应学习率

传统的梯度下降法存在一些问题,例如:

  • 学习率固定: 适用于所有参数,无法根据不同参数的特性进行调整。
  • 易陷入局部最小值或鞍点: 梯度下降法无法有效区分局部最小值和鞍点,容易陷入其中。

为了解决这些问题,出现了自适应学习率方法,其核心思想是为每个参数设置不同的学习率,并根据梯度的大小动态调整学习率。常用的自适应学习率方法包括:

  • AdaGrad: 根据过去所有梯度的平方的平均值来调整学习率。优点是简单易用,缺点是学习率会随着时间逐渐减小,可能导致训练后期收敛速度变慢。
  • RMSProp: 在 AdaGrad 的基础上引入了超参数 α,用于控制过去梯度的重要性。优点是学习率可以根据梯度的大小动态调整,并且能够快速适应误差表面的变化。
  • Adam: 结合了 RMSProp 和动量方法,能够更好地利用过去梯度的信息,并能够自适应调整学习率。优点是收敛速度快,性能稳定,是目前最常用的优化器之一。

3.4 学习率调度

自适应学习率方法虽然能够根据梯度的大小动态调整学习率,但仍然存在一些问题,例如:

  • 学习率衰减过快: 在训练后期,学习率可能会衰减过快,导致收敛速度变慢。
  • 学习率调整不及时: 在误差表面变化较大的区域,学习率可能无法及时调整,导致训练过程不稳定。

为了解决这些问题,出现了学习率调度策略,其核心思想是控制学习率随时间的变化。常用的学习率调度策略包括:

  • 学习率衰减: 随着训练过程的进行,逐渐减小学习率,使模型能够更加精细地调整参数,并最终收敛到最优解。
  • 预热: 在训练初期,逐渐增加学习率,帮助模型更快地找到误差表面的方向,并避免陷入局部最小值或鞍点。

3.5 优化总结

通过将自适应学习率方法和学习率调度策略相结合,我们可以构建更加完善的优化算法。例如,Adam 优化器结合了 RMSProp 和动量方法,并引入了学习率调度策略,能够更好地适应不同的训练场景,并取得更好的训练效果。

问题:尽管优化算法取得了很大的进步,但仍然存在一些问题需要解决,例如:

  • 超参数调整: 自适应学习率方法和学习率调度策略都需要设置超参数,如何选择合适的超参数仍然是一个挑战。
  • 训练不稳定: 在某些情况下,优化算法可能会导致训练过程不稳定,例如梯度爆炸或梯度消失。
  • 泛化能力: 优化算法对训练数据的分布和噪声敏感,可能会影响模型的泛化能力。

总结:优化算法是深度学习模型训练过程中的重要组成部分,其选择和调整对模型的训练效率和效果有着至关重要的影响。通过理解各种优化算法的原理和优缺点,并选择合适的算法和参数,我们可以更好地训练深度学习模型,并取得更好的效果。

Task 2.2 《深度学习详解》- 3.6 分类

神经网络训练不起来怎么办(4):损失函数(Loss)也可能有影响_哔哩哔哩_bilibili

深入探讨了深度学习中的分类问题,并对比了回归问题,详细解释了分类问题的特点、模型构建、损失函数选择以及优化策略。

1. 分类与回归的区别

  • 回归问题: 输入一个向量 x,输出一个标量 ˆy,目标是让 ˆy 与目标值 y 尽可能接近。例如,根据房屋特征预测房价。
  • 分类问题: 输入一个向量 x,输出一个类别标签,目标是让输出类别与真实类别尽可能一致。例如,根据图像内容判断是否包含猫。

2. 分类问题建模

  • 标签表示: 分类问题中,标签通常用独热向量表示,每个类别对应一个元素,其中一个元素为 1,其余为 0。例如,三个类别的标签分别表示为 [1, 0, 0], [0, 1, 0], [0, 0, 1]。
  • 模型构建: 分类模型通常使用多层神经网络,最后一层为 softmax 层。softmax 层将网络的输出进行归一化,使得每个输出值在 0 到 1 之间,且所有输出值之和为 1,表示各个类别的概率分布。
  • 损失函数: 分类问题常用的损失函数为交叉熵损失,它衡量预测概率分布与真实概率分布之间的差异。

3. 交叉熵损失的优势

  • 优化难度: 与均方误差相比,交叉熵损失在误差较大时梯度更陡峭,有利于模型优化。
  • 最大化似然: 交叉熵损失可以理解为最大化似然函数,即模型参数使得预测概率分布与真实概率分布最接近。

4. 优化策略

  • 动量优化: 使用动量优化器可以加速模型收敛,提高训练效率。
  • 批量归一化: 批量归一化可以缓解内部协变量偏移问题,提高模型泛化能力。

5. 疑问

  • 类别不平衡问题: 当数据集中各个类别样本数量不均衡时,模型可能会倾向于预测样本数量较多的类别,导致模型性能下降。
  • 类别数量较多时的模型设计: 当类别数量较多时,softmax 层的计算复杂度会增加,需要考虑更高效的模型设计。
  • 多标签分类问题: 当一个样本可能属于多个类别时,需要使用多标签分类模型,并设计相应的损失函数。
  • 模型解释性: 深度学习模型通常缺乏解释性,难以理解模型内部的工作原理。
  • 对抗样本攻击: 深度学习模型容易受到对抗样本攻击,需要设计鲁棒的模型来抵御攻击。

Task 2.3 (实践任务):HW3(CNN)卷积神经网络-图像分类

Step1:准备算力

Step2:一键运行Notebook

1、获得的数据集和代码文件

启动创建的实例,等待几分钟直到启动完成,然后点击进入JupyterLab。接着,点击“Terminal”打开命令行窗口,输入以下代码并按下回车键。稍等片刻,数据集和代码文件(notebook)将会自动下载,大约需要一分钟。

git clone https://www.modelscope.cn/datasets/Datawhale/LeeDL-HW3-CNN.git

2、一键运行代码

点击打开LeeDL-HW3-CNN文件夹

点击HW3-ImageClassification.ipynb

等待约12分钟后即可获得结果。通过单元格(cell)查看模型的训练准确率。生成的`submission.csv`文件包含分类结果,可提交至Kaggle进行评估。

代码将打印出当前模型的结构,并绘制两个数据集的图像分析和分类结果分布图,以帮助更好地理解模型的表现。

Step3:运行完成后记得停止实例

代码详解

卷积神经网络是深度学习中的一个非常重要的分支,本作业提供了进行图像分类任务的基本范式。

  • 准备数据

  • 训练模型

  • 应用模型

要完成一个深度神经网络训练模型的代码,大概需要完成下面的内容:

  1. 导入所需要的库/工具包

  2. 数据准备与预处理

  3. 定义模型

  4. 定义损失函数和优化器等其他配置

  5. 训练模型

  6. 评估模型

  7. 进行预测

此范式不仅适用于图像分类任务,对于广泛的深度学习任务也是适用的。

1. 导入所需要的库/工具包

这段代码导入了进行图像处理和深度学习任务所需的各种Python库和模块,涵盖了数据处理、神经网络构建、数据集操作、图像转换和显示进度条等功能,为后续的模型训练和评估做好准备。

# 导入必要的库
import numpy as np
import pandas as pd
import torch
import os
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
# “ConcatDataset” 和 “Subset” 在进行半监督学习时可能是有用的。
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder, VisionDataset
# 这个是用来显示进度条的。
from tqdm.auto import tqdm
import random

此外,为了确保实验的可重复性,设置随机种子,并对CUDA进行配置以确保确定性:

# 设置随机种子以确保实验结果的可重复性
myseed = 6666

# 确保在使用CUDA时,卷积运算具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

# 为numpy和pytorch设置随机种子
np.random.seed(myseed)
torch.manual_seed(myseed)

# 如果使用CUDA,为所有GPU设置随机种子
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(myseed)
2. 数据准备与预处理

数据准备包括从指定路径加载图像数据,并对其进行预处理。作业中对图像的预处理操作包括调整大小和将图像转换为Tensor格式。为了增强模型的鲁棒性,可以对训练集进行数据增强。相关代码如下:

# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor。
test_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
])

# 不过,在测试阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
    # 将图像调整为固定大小(高度和宽度均为128)
    transforms.Resize((128, 128)),
    # TODO:你可以在这里添加一些图像增强的操作。

    # ToTensor()应该是所有变换中的最后一个。
    transforms.ToTensor(),
])
class FoodDataset(Dataset):
    """
    用于加载食品图像数据集的类。

    该类继承自Dataset,提供了对食品图像数据集的加载和预处理功能。
    它可以自动从指定路径加载所有的jpg图像,并对这些图像应用给定的变换。
    """

    def __init__(self, path, tfm=test_tfm, files=None):
        """
        初始化FoodDataset实例。

        参数:
        - path: 图像数据所在的目录路径。
        - tfm: 应用于图像的变换方法(默认为测试变换)。
        - files: 可选参数,用于直接指定图像文件的路径列表(默认为None)。
        """
        super(FoodDataset).__init__()
        self.path = path
        # 列出目录下所有jpg文件,并按顺序排序
        self.files = sorted([os.path.join(path, x) for x in os.listdir(path) if x.endswith(".jpg")])
        if files is not None:
            self.files = files  # 如果提供了文件列表,则使用该列表
        self.transform = tfm  # 图像变换方法

    def __len__(self):
        """
        返回数据集中图像的数量。

        返回:
        - 数据集中的图像数量。
        """
        return len(self.files)

    def __getitem__(self, idx):
        """
        获取给定索引的图像及其标签。

        参数:
        - idx: 图像在数据集中的索引。

        返回:
        - im: 应用了变换后的图像。
        - label: 图像对应的标签(如果可用)。
        """
        fname = self.files[idx]
        im = Image.open(fname)
        im = self.transform(im)  # 应用图像变换

        # 尝试从文件名中提取标签
        try:
            label = int(fname.split("/")[-1].split("_")[0])
        except:
            label = -1  # 如果无法提取标签,则设置为-1(测试数据无标签)

        return im, label
# 构建训练和验证数据集
# "loader" 参数定义了torchvision如何读取数据
train_set = FoodDataset("./hw3_data/train", tfm=train_tfm)
# 创建训练数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
# 构建验证数据集
# "loader" 参数定义了torchvision如何读取数据
valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)
# 创建验证数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
3. 定义模型

这段代码定义了一个图像分类器类(Classifier),继承自PyTorch的nn.Module。该分类器通过一系列卷积层、批归一化层、激活函数和池化层构建卷积神经网络(CNN),用于提取图像特征。随后,这些特征被输入到全连接层进行分类,最终输出11个类别的概率,用于图像分类任务。

class Classifier(nn.Module):
    """
    定义一个图像分类器类,继承自PyTorch的nn.Module。
    该分类器包含卷积层和全连接层,用于对图像进行分类。
    """
    def __init__(self):
        """
        初始化函数,构建卷积神经网络的结构。
        包含一系列的卷积层、批归一化层、激活函数和池化层。
        """
        super(Classifier, self).__init__()
        # 定义卷积神经网络的序列结构
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 64, 3, 1, 1),  # 输入通道3,输出通道64,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(64),        # 批归一化,作用于64个通道
            nn.ReLU(),                 # ReLU激活函数
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(64, 128, 3, 1, 1), # 输入通道64,输出通道128,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(128),        # 批归一化,作用于128个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(128, 256, 3, 1, 1), # 输入通道128,输出通道256,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(256),        # 批归一化,作用于256个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(256, 512, 3, 1, 1), # 输入通道256,输出通道512,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(512),        # 批归一化,作用于512个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(512, 512, 3, 1, 1), # 输入通道512,输出通道512,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(512),        # 批归一化,作用于512个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 最大池化,池化窗口大小2,步长2,填充0
        )
        # 定义全连接神经网络的序列结构
        self.fc = nn.Sequential(
            nn.Linear(512*4*4, 1024),    # 输入大小512*4*4,输出大小1024
            nn.ReLU(),
            nn.Linear(1024, 512),        # 输入大小1024,输出大小512
            nn.ReLU(),
            nn.Linear(512, 11)           # 输入大小512,输出大小11,最终输出11个类别的概率
        )

    def forward(self, x):
        """
        前向传播函数,对输入进行处理。
        
        参数:
        x -- 输入的图像数据,形状为(batch_size, 3, 128, 128)
        
        返回:
        输出的分类结果,形状为(batch_size, 11)
        """
        out = self.cnn(x)               # 通过卷积神经网络处理输入
        out = out.view(out.size()[0], -1)  # 展平输出,以适配全连接层的输入要求
        return self.fc(out)             # 通过全连接神经网络得到最终输出
4. 定义损失函数和优化器等其他配置

这段代码实现了图像分类模型的初始化和训练配置,目的是准备好训练环境和参数。它选择合适的设备(GPU或CPU),设置模型、批量大小、训练轮数、提前停止策略,定义了损失函数和优化器,为后续的模型训练奠定了基础。

# 根据GPU是否可用选择设备类型
device = "cuda" if torch.cuda.is_available() else "cpu"

# 初始化模型,并将其放置在指定的设备上
model = Classifier().to(device)

# 定义批量大小
batch_size = 64

# 定义训练轮数
n_epochs = 8

# 如果在'patience'轮中没有改进,则提前停止
patience = 5

# 对于分类任务,我们使用交叉熵作为性能衡量标准
criterion = nn.CrossEntropyLoss()

# 初始化优化器,您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
5. 训练模型

这段代码实现了一个图像分类模型的训练和验证循环,目的是通过多轮训练(epochs)逐步优化模型的参数,以提高其在验证集上的性能,并保存效果最好的模型。训练阶段通过前向传播、计算损失、反向传播和参数更新来优化模型,验证阶段评估模型在未见过的数据上的表现。如果验证集的准确率超过了之前的最好成绩,保存当前模型,并在连续多轮验证性能未提升时提前停止训练。

# 初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0

for epoch in range(n_epochs):
    # ---------- 训练阶段 ----------
    # 确保模型处于训练模式
    model.train()

    # 这些用于记录训练过程中的信息
    train_loss = []
    train_accs = []

    for batch in tqdm(train_loader):
        # 每个批次包含图像数据及其对应的标签
        imgs, labels = batch
        # imgs = imgs.half()
        # print(imgs.shape,labels.shape)

        # 前向传播数据。(确保数据和模型位于同一设备上)
        logits = model(imgs.to(device))

        # 计算交叉熵损失。
        # 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
        loss = criterion(logits, labels.to(device))

        # 清除上一步中参数中存储的梯度
        optimizer.zero_grad()

        # 计算参数的梯度
        loss.backward()

        # 为了稳定训练,限制梯度范数
        grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)

        # 使用计算出的梯度更新参数
        optimizer.step()

        # 计算当前批次的准确率
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确率
        train_loss.append(loss.item())
        train_accs.append(acc)

    train_loss = sum(train_loss) / len(train_loss)
    train_acc = sum(train_accs) / len(train_accs)

    # 打印信息
    print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
6. 评估模型

训练完成后,需要在测试集上评估模型的性能。通过计算准确率来衡量模型在测试集上的表现。

# ---------- 验证阶段 ----------
    # 确保模型处于评估模式,以便某些模块如dropout能够正常工作
    model.eval()

    # 这些用于记录验证过程中的信息
    valid_loss = []
    valid_accs = []

    # 按批次迭代验证集
    for batch in tqdm(valid_loader):
        # 每个批次包含图像数据及其对应的标签
        imgs, labels = batch
        # imgs = imgs.half()

        # 我们在验证阶段不需要梯度。
        # 使用 torch.no_grad() 加速前向传播过程。
        with torch.no_grad():
            logits = model(imgs.to(device))

        # 我们仍然可以计算损失(但不计算梯度)。
        loss = criterion(logits, labels.to(device))

        # 计算当前批次的准确率
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确率
        valid_loss.append(loss.item())
        valid_accs.append(acc)
        # break

    # 整个验证集的平均损失和准确率是所记录值的平均
    valid_loss = sum(valid_loss) / len(valid_loss)
    valid_acc = sum(valid_accs) / len(valid_accs)

    # 打印信息
    print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

    # 更新日志
    if valid_acc > best_acc:
        with open(f"./{_exp_name}_log.txt", "a"):
            print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
    else:
        with open(f"./{_exp_name}_log.txt", "a"):
            print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

    # 保存模型
    if valid_acc > best_acc:
        print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
        torch.save(model.state_dict(), f"{_exp_name}_best.ckpt")  # 只保存最佳模型以防止输出内存超出错误
        best_acc = valid_acc
        stale = 0
    else:
        stale += 1
        if stale > patience:
            print(f"连续 {patience} 轮没有改进,提前停止")
            break
7. 进行预测

最后的代码构建一个测试数据集和数据加载器,以便高效地读取数据。实例化并加载预训练的分类器模型,并将其设置为评估模式。在不计算梯度的情况下,遍历测试数据,使用模型进行预测,并将预测标签存储在列表中。将预测结果与测试集的ID生成一个DataFrame,并将其保存为submission.csv文件。

# 构建测试数据集
# "loader"参数指定了torchvision如何读取数据
test_set = FoodDataset("./hw3_data/test", tfm=test_tfm)
# 创建测试数据加载器,批量大小为batch_size,不打乱数据顺序,不使用多线程,启用pin_memory以提高数据加载效率
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)

# 实例化分类器模型,并将其转移到指定的设备上
model_best = Classifier().to(device)

# 加载模型的最优状态字典
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt"))

# 将模型设置为评估模式
model_best.eval()

# 初始化一个空列表,用于存储所有预测标签
prediction = []

# 使用torch.no_grad()上下文管理器,禁用梯度计算
with torch.no_grad():
    # 遍历测试数据加载器
    for data, _ in tqdm(test_loader):
        # 将数据转移到指定设备上,并获得模型的预测结果
        test_pred = model_best(data.to(device))
        # 选择具有最高分数的类别作为预测标签
        test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1)
        # 将预测标签添加到结果列表中
        prediction += test_label.squeeze().tolist()

# 创建测试csv文件
def pad4(i):
    """
    将输入数字i转换为长度为4的字符串,如果长度不足4,则在前面补0。
    :param i: 需要转换的数字
    :return: 补0后的字符串
    """
    return "0" * (4 - len(str(i))) + str(i)

# 创建一个空的DataFrame对象
df = pd.DataFrame()
# 使用列表推导式生成Id列,列表长度等于测试集的长度
df["Id"] = [pad4(i) for i in range(len(test_set))]
# 将预测结果赋值给Category列
df["Category"] = prediction
# 将DataFrame对象保存为submission.csv文件,不保存索引
df.to_csv("submission.csv", index=False)        

优化方向(建议学习Task3后进行尝试~)

代码的最后一部分提供了数据增强/图像增广的示例,并结合t-SNE算法对增强后的特征进行降维和可视化。这种可视化方法有助于分析数据分布、评估数据增强的效果,并为进一步优化模型分类精度提供直观的指导。

t-SNE(t-Distributed Stochastic Neighbor Embedding)是一种非线性降维技术,广泛应用于数据可视化。它的核心思想是通过在高维空间中保持数据点之间的局部相似性,将数据映射到低维(通常是二维或三维)空间,从而使得不同类别的数据点在低维空间中形成清晰的聚类结构。这种技术特别适用于高维特征数据的可视化,有助于直观地理解数据的内在结构。

优化卷积神经网络(CNN)模型的过程涵盖了多个方面,这些方面的改进可以显著提升模型的性能和泛化能力。常见的优化方向包括:

  1. 优化网络结构:设计更深或更宽的网络,引入残差连接等现代架构。

  2. 使用正则化技术:如L2正则化、Dropout、Batch Normalization等,以防止过拟合。

  3. 优化激活函数:选择适当的激活函数如ReLU、Leaky ReLU、Swish等,以加速训练并提升模型表现。

  4. 优化算法:采用先进的优化算法如Adam、RMSprop或学习率调度器。

  5. 数据增强:通过各种数据增强技术扩展训练数据,提高模型的鲁棒性。

  6. 约束初始化权重:利用He或Xavier初始化,确保训练的稳定性。

  7. 损失函数调整:选择或设计合适的损失函数,以更好地反映模型目标。

  8. 模型压缩:通过剪枝、量化或知识蒸馏来减少模型复杂度,提高推理速度。

  9. 混合精度训练:结合半精度和单精度浮点数训练,加快训练速度并减少显存占用。

  10. 硬件加速:利用GPU、TPU等硬件加速器,以显著提升训练和推理的效率。

优化网络结构

优化网络结构是提升卷积神经网络(CNN)性能的重要步骤。CNN通常由多个模块或组块结构组成,通过合理设计和优化这些结构,可以显著增强模型的学习能力和泛化性能。以下是几种优化网络结构的策略:

  1. 增加网络深度与调整卷积核大小 增加网络的深度(即增加卷积层的数量)和调整卷积核的大小是提升模型学习能力的常见方法。更深的网络能够捕捉到更复杂和抽象的特征,而不同大小的卷积核则可以提取多尺度的信息。比如,使用3x3的小卷积核可以更精确地捕捉局部细节,而5x5或7x7的卷积核则能够获取更大范围的上下文信息。此外,通过在网络中结合不同尺寸的卷积核,还可以增强模型对多尺度特征的感知能力。

  2. 引入先进的结构如残差连接 残差网络(ResNet)通过引入跳跃连接(skip connections),有效解决了深度网络中的梯度消失和梯度爆炸问题。这些残差连接允许信息在网络中直接跨层传递,使得极深的网络也能顺利训练。这不仅提高了网络的稳定性,还加快了收敛速度,从而进一步提升模型的整体表现。此外,残差结构在多个现代深度网络中得到了广泛应用,证明了其在处理深度网络优化挑战中的有效性。

  3. 应用现代卷积神经网络架构 采用经过验证的现代卷积神经网络架构是提升模型性能的有效途径。比如:

    1. AlexNet:作为深度学习在图像分类领域的开创性架构,AlexNet通过较大的卷积核和ReLU激活函数引领了深度学习的应用潮流。

    2. VGG:VGG通过使用多层3x3卷积核构建深层网络,既简化了网络设计又提升了模型的准确性,成为多个任务的基准模型。

    3. Inception:Inception网络引入了多路径结构,使得网络能够在不同的尺度上并行提取特征,有效提高了计算效率和模型的表现力。

    4. ResNet:ResNet的残差结构成功解决了深度网络中常见的优化难题,使得训练非常深的网络成为可能,并在多个计算机视觉任务中表现卓越。

学有余力的同学可以看:7. 现代卷积神经网络 — 动手学深度学习 2.0.0 documentation

调整为ResNet

import torch.nn as nn
import torch.nn.functional as F

class BasicBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        identity = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        
        if self.downsample is not None:
            identity = self.downsample(x)
        
        out += identity
        out = self.relu(out)
        return out

class ResNetClassifier(nn.Module):
    def __init__(self, block, layers, num_classes=11):
        super(ResNetClassifier, self).__init__()
        self.in_channels = 64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512, num_classes)

    def _make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        if stride != 1 or self.in_channels != out_channels:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels, kernel_size=1, stride=stride),
                nn.BatchNorm2d(out_channels),
            )

        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        self.in_channels = out_channels
        for _ in range(1, blocks):
            layers.append(block(out_channels, out_channels))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

# 使用ResNet18风格的层数
model = ResNetClassifier(BasicBlock, [2, 2, 2, 2])

数据/图像增强

图像数据增强是提升卷积神经网络(CNN)性能的重要策略之一,尤其是在数据量有限的情况下。通过对现有训练数据进行各种变换,数据增强可以有效扩展数据集规模,增强模型的鲁棒性和泛化能力。

  • 几何变换(Geometric Transformations)

    • 旋转(Rotation):随机旋转图像一定角度,有助于CNN学习旋转不变性,使得模型能够更好地处理不同角度的对象。

    • 平移(Translation):对图像进行随机水平或垂直方向的平移,帮助模型更好地应对对象位置的变化。

    • 翻转(Flipping):常见的操作是水平翻转,用于应对左右对称的对象,如人脸、动物等。

    • 缩放(Scaling):随机缩放图像可以帮助模型学习处理不同尺度的对象,增强模型的尺度不变性。

    • 剪切(Shearing):通过改变图像形状,模拟对象的倾斜或变形,这有助于模型在面对非标准形状的对象时仍能做出正确的判断。

from torchvision import transforms

# 旋转:随机旋转图像一定角度
transform_rotate = transforms.RandomRotation(degrees=30)

# 平移:随机水平或垂直方向的平移
transform_translate = transforms.RandomAffine(degrees=0, translate=(0.1, 0.1))

# 翻转:水平翻转
transform_flip = transforms.RandomHorizontalFlip(p=0.5)

# 缩放:随机缩放图像
transform_scale = transforms.RandomResizedCrop(size=224, scale=(0.8, 1.0))

# 剪切:随机剪切图像
transform_shear = transforms.RandomAffine(degrees=0, shear=20)
  • 噪声添加(Adding Noise)

    • 高斯噪声(Gaussian Noise):在图像中添加高斯噪声,可以使模型对真实世界中的噪声数据更加稳健。

    • 椒盐噪声(Salt and Pepper Noise):通过随机在图像中加入白色和黑色像素点,模拟图像传感器噪声,提升模型的抗噪能力。

import torch
import torchvision.transforms.functional as F
from PIL import Image
import numpy as np

# 高斯噪声
class AddGaussianNoise(object):
    def __init__(self, mean=0.0, std=1.0):
        self.mean = mean
        self.std = std

    def __call__(self, img):
        noise = torch.randn(img.size()) * self.std + self.mean
        noisy_img = img + noise
        return noisy_img.clamp(0, 1)

# 椒盐噪声
class AddSaltPepperNoise(object):
    def __init__(self, prob=0.01):
        self.prob = prob

    def __call__(self, img):
        img_np = np.array(img)
        num_salt = np.ceil(self.prob * img_np.size * 0.5)
        num_pepper = np.ceil(self.prob * img_np.size * 0.5)

        coords = [np.random.randint(0, i - 1, int(num_salt)) for i in img_np.shape]
        img_np[coords[0], coords[1], :] = 1

        coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in img_np.shape]
        img_np[coords[0], coords[1], :] = 0

        return Image.fromarray(img_np)

transform_gaussian_noise = transforms.Compose([
    transforms.ToTensor(),
    AddGaussianNoise(mean=0, std=0.1),
    transforms.ToPILImage()
])

transform_salt_pepper_noise = transforms.Compose([
    AddSaltPepperNoise(prob=0.01)
])
  • 裁剪和填充(Cropping and Padding)

    • 随机裁剪(Random Cropping):从图像中随机裁剪出一部分并进行训练,有助于模型聚焦于图像中的重要区域。

    • 填充(Padding):在图像周围添加像素,使得裁剪后的图像恢复到原始大小,这样可以保持数据的一致性。

from torchvision import transforms

# 随机裁剪
transform_random_crop = transforms.RandomCrop(size=224)

# 填充:在图像周围添加像素
transform_padding = transforms.Pad(padding=4)
  • 高级数据增强技术

    • 混合增强(MixUp):通过将两张图像的像素值进行加权平均,生成新的训练样本,从而使模型在处理类间关系时更加稳健。

    • 随机擦除(Random Erasing):随机遮挡图像中的部分区域,迫使模型在信息不完整的情况下仍能正确分类,增强模型的鲁棒性。

    • 对抗样本生成(Adversarial Augmentation):生成对抗样本,通过微小的扰动引起模型错误分类,从而使模型对这种干扰更加不敏感。

import torchvision.transforms as transforms

# 混合增强:需要在数据加载时处理
def mixup_data(x, y, alpha=1.0):
    '''Returns mixed inputs, pairs of targets, and lambda'''
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1

    batch_size = x.size()[0]
    index = torch.randperm(batch_size)

    mixed_x = lam * x + (1 - lam) * x[index, :]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam

# 随机擦除
transform_random_erasing = transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0)

# 对抗样本生成(Adversarial Augmentation):需要使用外部库如`advertorch`,下面是一个简单的例子
from advertorch.attacks import LinfPGDAttack

def adversarial_attack(model, x, y):
    adversary = LinfPGDAttack(
        model, loss_fn=torch.nn.CrossEntropyLoss(), eps=0.3,
        nb_iter=40, eps_iter=0.01, rand_init=True, clip_min=0.0, clip_max=1.0,
        targeted=False)
    adv_perturbation = adversary.perturb(x, y)
    return adv_perturbation

学有余力的同学可以看:13.1.1. 常用的图像增广方法 ¶

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值