Datawhale X 李宏毅苹果书 AI夏令营 task2

1.自适应学习率

在训练一个网络时,当我们走到临界点时,说明梯度很小,但损失不在下降不能代表梯度很小。

下图中,横轴为迭代次数,竖轴是梯度的范数,即梯度向量的长度。随着迭代次数增多,虽然损失不断下降,但是梯度大小并没有很小。

可能的情况是,损失在两个谷壁之间不断震荡,这是顿时不会下降,但是他显然并没有达到临界点。

显然是每次更新参数时更新的大小稍显大,导致错过了临界点。因此在这种情形下,可以尝试将学习率设小一点。这样便能逐渐移动到谷底。但是学习率过小也会使训练走不到终点,因为度过了比较陡的阶段后,在平坦坡度上参数会更新地很缓慢,以致于永远更新不完。

通过上面的分析,我们需要更好的梯度下降的版本。在梯度下降里若所有参数都是设置同样的学习率显然不行,需要为每一个参数定制化学习率,即引入自适应学习率的方法,给每个不同参数不同的学习率。在某个方向上,若梯度很小,我们希望学习率较大,而在另一方向上,梯度值较大,我们希望学习率较小。

1.1AdaGrad

AdaGrad(Adaptive Grandient)使典型的自适应学习率的方法,能根据梯度大小自动调整学习率。使梯度较大时,学习率减小,梯度较小时,学习率较大。

梯度下降更新参数的过程为:

计算出在第t个迭代时,参数对损失L的微分g,然后用上次更新得到的参数减去g和学习率的乘积得到新的参数,在整个过程中,学习率一致保持不变。

现在要有一个随着参数定制化的学习率,即把原有学习率η变成η /σi

对于不同的迭代,不同的参数,σ不同。

最常见的类型说根据梯度的均方根。

其中第一次更新参数过程中σ计算如下。

σi 0=|gi 0|

一定程度上,这样做是限定了每个参数第一次次更新的大小,通过自己的限定来防止每次改变的太大或太小。

第二次参数更新中,σ的计算为

\sigma = \sqrt{0.5*\left ( g0^{2}+g1^{2} \right )}

同样操作,第三次更新时,σ的计算为

\sigma = \sqrt{\left ( g0^{2}+g1^{2} +g2^{2}\right )/3}

第t+1次更新时,

\sigma = \sqrt{\left ( g0^{2}+g1^{2} +g2^{2}+....+gt^{2}\right )/\left ( t+1 \right )}

这样,节能通过每个参数的梯度的不同大小,来自动调整学习率的大小。

1.2RMSProp

同一个参数的同一个方向,学习率也需要动态调整,RMSprop即是为次为了满足这一需求。

RMSprop第一步与Adagrad一致,即

σi 0=|gi 0|

第二步更新时

\sigma _{1}^{i} = \sqrt{\alpha \left ( \left ( \sigma _{0}^{i} \right )^{2} \right )+\left ( 1-\alpha \right )\left ( g_{1}^{i}\right )^{2}}

(i代表第i个参数的学习率)

第三次:

\sigma _{2}^{i } = \sqrt{\alpha \left ( \left ( \sigma _{1}^{i} \right )^{2} \right )+\left ( 1-\alpha \right )\left ( g_{2}^{i}\right )^{2}}

同理,第t+1次

\sigma _{t}^{i} = \sqrt{\alpha \left ( \left ( \sigma _{t-1}^{i} \right )^{2} \right )+\left ( 1-\alpha \right )\left ( g_{t}^{i}\right )^{2}}

其中中0<α<1,其是一个可以调整的超参数。计算θi 1 的方法跟AdaGrad算均方根不一样, 在算均方根的时候,每一个梯度都有同等的重要性,,但在RMSprop里面,可以自己调整现在 的这个梯度的重要性。如果α设很小趋近于0,代表gi 1相较于之前算出来的梯度而言,比较 重要;如果α设很大趋近于1,代表gi 1 比较不重要,之前算出来的梯度比较重要。

1.3Adam

最常用的优化策略就是Adam,Adam相当于RMSprop加上动量,使用动量作为参数更新方向,而学习率计算与RMS prop一致。

2.学习率调度

在加上自适应性学习率后,用AdaGrad方法优化一个训练的结果如图

一开始优化时很顺利,在左转时,可以逐渐向终点走去。走到BC段时,因为横轴的方向的梯度小,所以σ会逐渐减小,导致学习率变大,从而不断前进。接下来走到红圈部分后,由于纵向的梯度一直很小,积累导致σ减小,进而学习率变大,上下晃动剧烈,经此纵向梯度又变大,σ又变大,学习率变大,不断修正。

对于上述需要不断修正的问题,可以通过学习率调度解决。之前,η是一个固定的值,而在学习率调度中η跟时间相关。

最常见的策略时学习率衰减,也成为学习率退火,随着参数的不断更新,让η越来越小。

除了学习率下降以外,还有另一个学习率调度方法,预热。预热是指让学习率先变大在变小,至于变到多大、变大的速度、变小的速度是超参数。残差网络[8]里 面是有预热的,在残差网络里面,学习率先设置成0.01,再设置成0.1,并且其论文还特别说 明,一开始用0.1反而训练不好。除了残差网络,BERT和Transformer的训练也都使用了预热。

3.优化总结

由不断改进,最初的梯度下降,可以进化到下面版本

其中m_{t}^{i} 是动量

这个版本中,不是按照某时刻的梯度方向来更新参数,而是把过去所有算出梯度的方向做一个加权总和当作更新的方向。接下来步伐大小为m_{t}^{i} / \sigma _{t}^{i}。通过\eta _{t}来实现学习率调度。

这是目前优化的完整的版本,这种优化器除了Adam以外,还有各种变形。但实际上都是通过不同方式计算m_{t}^{i} / \sigma _{t}^{i},或是使用不同学习率调度方式。

4.分类

分类与回归是深度学习中最常见的两种问题,之前预测观看次数预测属于回归问题,下介绍分类问题。

4.1分类与回归的关系

回归是输入一个向量x,输出 \widehat{y},我们希望 \widehat{y}跟某一个标签y 越接近越好,y 是要学习的目标。

而分类可当作回归来看,输入x,输出 \widehat{y}仍是一个标量,要让它跟正确答案的那个类越接近越好。我们可以把类别抽象成数字。例如

在这种情况下,我们会预设1和2有较近关系,1和3有较远关系,若实际上三种类别之间没有这种关系,则需要引入独热向量来表示类。实际上常见做法也是独热向量表示类。

例如,有三个类,标签y就是一个三维的向量,比如类1是[1,0,0]T,类2是[0,1,0]T,类 3 是[0,0,1]T。这样就没有两个类之间比较接近的问题问题。

如果目标y是一个三维的向量,那么网络也要输出三个数字才行。输出三个数值就是把本来输出一个数值的方法重复三次。把a_{1} a_{2} a_{3}乘三个不同权重,加上偏置,得到\widehat{y}

4.2 带有softmax的分类

分类实际过程为:输入x,乘W,加上b,通过激活函数σ,乘W′,再加上b′ 得到向量\widehat{y}。但实际做分类时,会把\widehat{y}通过softmax函数得到{y}',才去计算{y}'\widehat{y}的距离。

softmax函数形式为

先把所有 y取一个指数e^{y},在对其做归一化(除掉所有y的指数值的和)得到{y}'

例如:

两个类也可以直接套softmax函数。但是一般两个类时,直接取sigmoid。只有两个类时,sigmoid和softmax等价。

4.3分类损失

当把x输入到一个网络里产生\widehat{y}后,通过softmax得到{y}'再去计算{y}'和y之间距离e

计算两者距离不只一种方法,也可以是均方误差

而下面的交叉熵更常见,当两者相同时,可以最小化交叉熵的值,此时均方误差最小。最小化交叉熵其实就是最大化似然。

从优化角度,交叉熵被更常用在分类上。

5.实例练习

分布拆解分析:

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格式

Tensor为张量,和数组、向量、矩阵的格式基本一样,但是战门对GPU来设计的,可以运行在GPU上来加快计算效率。(一个可以运行在gpu上的所谓数据)

# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整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.定义模型

下定义了一个图像分类器类,继承自PyTorch的nn.Module。该分类器通过一系列卷积层,批归一化层、激活函数和池化层构建卷积神经网络,提取图像特征。

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是否可用选择设备类型
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.训练模型

下面实现了一个图像分类模型的训练和循环验证,目的是通过多轮测试逐渐优化模型参数。

# 初始化追踪器,这些不是参数,不应该被更改
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}")
# 李老师的课程原文件里面确实缺少write,如果想要log文件里面有内容,可以按照下面的参考,此部分不是重点
#if valid_acc > best_acc:
#    with open(f"./{_exp_name}_log.txt", "a") as log_file:
#        log_file.write(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> best\n")
#    print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> best")
#else:
#    with open(f"./{_exp_name}_log.txt", "a") as log_file:
#        log_file.write(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}\n")
#    print(f"[ Valid | {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.进行预测

构建一个测试数据集和数据加载器,来高效地读取数据。

# 构建测试数据集
# "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)        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值