深度学习进阶版test2__(Datawhale X 李宏毅苹果书 AI夏令营)

深度学习优化器与自适应学习率详解

在深度学习中,优化器和学习率的选择对模型训练效果至关重要。本文将深入探讨常用的深度学习优化器,如RMSprop和Adam,并介绍自适应学习率的概念与实现。通过代码示例、图示以及实际案例,帮助你更好地理解和应用这些优化器。

优化器简介

优化器决定了模型参数更新的方式。不同的优化器有不同的特点,适用于不同的数据分布和任务。常用的优化器包括SGD(随机梯度下降)、RMSprop、Adam等。选择合适的优化器对于模型训练的效率和效果至关重要。

1.1 梯度下降法(Gradient Descent)

梯度下降法是最基本的优化方法。其核心思想是沿着损失函数梯度的反方向调整模型参数,使得损失函数逐步减小。

公式表示

1.2 RMSprop

RMSprop 是一种自适应学习率方法,它通过对每个参数分别设置学习率,解决了梯度下降法中存在的学习率难以调节的问题。

公式表示

import torch
import torch.nn as nn
import torch.optim as optim

# 一个简单的线性模型
model = nn.Linear(10, 2)

# 损失函数
criterion = nn.CrossEntropyLoss()

# 优化器:SGD
optimizer = optim.SGD(model.parameters(), lr=0.01)

 解释:代码中,我们使用RMSprop优化器,设置了学习率为0.01,动量因子为0.9。每次迭代时,首先清零梯度,然后计算损失并反向传播计算梯度,最后更新模型参数。在这个例子中,我们定义了一个简单的线性模型,并使用SGD作为优化器。SGD是一种基础的优化方法,但在实际应用中可能会出现收敛慢、不稳定等问题。

1.3 自适应学习率基础

在优化过程中,我们通常使用梯度下降法来更新参数:

其中,θ​ 表示第 t 次迭代时的参数,η 是学习率,gt是梯度。

代码示例:梯度下降
import numpy as np

# 假设我们有一个简单的二次损失函数
def loss_function(theta):
    return theta ** 2

# 假设我们从初始值 theta = 10 开始优化
theta = 10
learning_rate = 0.1

for t in range(100):
    gradient = 2 * theta  # 计算梯度
    theta = theta - learning_rate * gradient  # 更新参数
    print(f"Step {t}: theta = {theta}, loss = {loss_function(theta)}")

2. 自适应学习率优化器

自适应学习率优化器通过动态调整学习率来提高训练效率和稳定性。以下是两个常用的自适应学习率优化器:

2.1 RMSprop:自适应学习率

RMSprop 是一种优化器,它通过动态调整学习率来应对不同梯度带来的不稳定性。RMSprop 会计算一个参数的历史梯度的均方根(Root Mean Square),并用这个值来调整每个参数的学习率。

RMSprop优化器通过对历史梯度平方的指数加权平均来调整学习率,从而在噪声较大的非凸问题中表现出色。

# 优化器:RMSprop
optimizer = optim.RMSprop(model.parameters(), lr=0.01)

RMSprop 更新公式如下:

其中,σt 是历史梯度的均方根,ϵ 是一个小的常数,防止除零错误。

代码示例:RMSprop
import numpy as np

# 初始化参数
theta = 10
learning_rate = 0.01
epsilon = 1e-8
sigma = 0
decay_rate = 0.9

for t in range(100):
    gradient = 2 * theta  # 计算梯度
    sigma = decay_rate * sigma + (1 - decay_rate) * gradient ** 2  # 更新历史梯度的均方根
    theta = theta - learning_rate / np.sqrt(sigma + epsilon) * gradient  # 更新参数
    print(f"Step {t}: theta = {theta}, loss = {loss_function(theta)}")

 RMSprop非常适合在深度学习中使用,特别是在具有不稳定梯度的神经网络中。它通过自动调整学习率,避免了传统SGD中容易出现的收敛问题。

2.2 Adam:结合动量与自适应学习率

Adam(Adaptive Moment Estimation)是目前最常用的优化器之一。它结合了RMSprop的自适应学习率和动量的概念,能够更有效地收敛。Adam优化器结合了RMSprop和动量(Momentum)的思想,使用两个动量项来分别估计梯度的一阶和二阶矩。

# 优化器:Adam
optimizer = optim.Adam(model.parameters(), lr=0.001)

Adam优化器结合了动量法和RMSprop的优点,通过自适应调整学习率并考虑梯度的动量,提供了更加稳定和高效的训练效果。

公式表示

 

其中,mtm_tmt​ 是动量,vtv_tvt​ 是二阶动量,β1\beta_1β1​ 和 β2\beta_2β2​ 是动量的超参数。

代码示例:Adam
import numpy as np

# 初始化参数
theta = 10
learning_rate = 0.01
epsilon = 1e-8
beta1 = 0.9
beta2 = 0.999
m = 0
v = 0

for t in range(1, 101):
    gradient = 2 * theta  # 计算梯度
    m = beta1 * m + (1 - beta1) * gradient  # 更新一阶动量
    v = beta2 * v + (1 - beta2) * gradient ** 2  # 更新二阶动量
    m_hat = m / (1 - beta1 ** t)  # 偏差校正
    v_hat = v / (1 - beta2 ** t)  # 偏差校正
    theta = theta - learning_rate * m_hat / (np.sqrt(v_hat) + epsilon)  # 更新参数
    print(f"Step {t}: theta = {theta}, loss = {loss_function(theta)}")

解释:Adam优化器在代码中被使用,其默认参数设置为β1=0.9和β2=0.999,这使得它能够高效处理稀疏梯度并在不稳定的训练过程中表现良好。 Adam是目前深度学习中最常用的优化器之一,它不仅能在大多数情况下表现出良好的效果,还可以快速收敛到较优解。

2.3 学习率衰减与预热

除了学习率下降以外,还有一个经典的学习率调度方式——预热。预热方法是让学习率先变大后变小。学习率上升和下降的速度是超参数,需要根据具体情况进行调整。

图 3.28 展示了学习率衰减的预热方式,这种方法通常用于复杂模型的训练,例如残差网络(ResNet)、BERT 和 Transformer 等。最初设置较小的学习率是为了收集更多的误差表面信息,之后逐渐增加学习率再减小,以更好地训练模型。

学习率预热的必要性

1. 学习率衰减

学习率衰减是一种常见的策略,即随着训练的进行,逐步减小学习率,从而在接近收敛时进行更小幅度的参数调整。

预热对于某些优化器如 Adam、RMSprop 或 AdaGrad 特别重要,因为它们需要统计足够的数据来获得精准的梯度估计值。起初较低的学习率有助于收集这些信息,然后逐渐提高学习率进行更精准的优化。

代码示例

scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)

for epoch in range(100):
    for input, target in dataset:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    scheduler.step()

解释:在此代码中,每训练10个epoch,学习率将衰减至原来的10%。这样可以在训练初期使用较大学习率加快收敛,后期使用较小学习率精细调整参数。

如果对预热感兴趣,推荐进一步研究 Adam 的进阶版本——RAdam。

2. 预热策略

预热策略是在训练开始时使用较小的学习率,并逐渐增加到预设值,以避免训练初期不稳定的参数更新。

代码示例

scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda epoch: epoch / 10)

for epoch in range(100):
    for input, target in dataset:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    scheduler.step()

解释:这里我们使用了预热策略,前10个epoch学习率逐渐增加,之后保持不变。这种策略对避免模型初期训练的不稳定性非常有效。

3. 实际案例:不同优化器的比较

为了直观地展示不同优化器的效果,我们在一个简单的数据集上进行实验,比较RMSprop、Adam和SGD三种优化器的表现。

数据集与模型: 我们使用一个简单的线性回归模型进行实验,数据集为生成的随机数据。

代码示例

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

# 数据集生成
torch.manual_seed(0)
X = torch.randn(100, 1)
y = 3 * X + 4 + torch.randn(100, 1) * 0.5

# 模型定义
model = nn.Linear(1, 1)

# 优化器定义
optimizers = {
    "SGD": optim.SGD(model.parameters(), lr=0.01),
    "RMSprop": optim.RMSprop(model.parameters(), lr=0.01),
    "Adam": optim.Adam(model.parameters(), lr=0.01)
}

loss_fn = nn.MSELoss()
losses = {key: [] for key in optimizers.keys()}

# 训练过程
for name, optimizer in optimizers.items():
    for epoch in range(100):
        model.train()
        optimizer.zero_grad()
        y_pred = model(X)
        loss = loss_fn(y_pred, y)
        loss.backward()
        optimizer.step()
        losses[name].append(loss.item())

# 结果绘图
for name, loss in losses.items():
    plt.plot(loss, label=name)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.title("Optimizer Comparison")
plt.show()

 结果分析: 通过绘制的损失曲线,我们可以观察到不同优化器在收敛速度和最终损失上的表现差异。Adam通常表现出快速收敛的特点,而RMSprop在处理稀疏梯度时具有优势。

4. 优化总结

从最基础的梯度下降法,我们逐步演化出更加复杂的优化器。例如 Adam,它结合了动量 mtm_tmt​ 和均方根 σtσ_tσt​ 来调整参数更新步伐。通过这种方式,我们能够更有效地优化深度学习模型。

公式 (3.27) 总结了这种优化方法:

其中,mit考虑了所有的梯度方向,而 σit则只关注梯度的大小,两者结合提供了一个更平衡的参数更新方式。

这些概念的掌握不仅有助于理解深度学习中的优化过程,也为实际项目中的优化策略提供了理论支持。

分类问题与损失函数的选择

在深度学习中,分类问题和回归问题是最常见的两种任务。我们已经讨论了回归问题的处理方法,现在我们来探讨分类问题,并解释为什么在分类任务中,通常使用交叉熵损失函数而不是均方误差。

1. 分类与回归的关系

分类问题可以被视为回归问题的一个特例。回归任务中,模型输入一个特征向量 x,输出一个标量 ,我们希望 y与目标值 y尽可能接近。在分类任务中,我们输入 x,期望模型输出一个与正确类别最接近的数值 。一种简单的方法是将每个类用一个数字表示(例如,类1表示为1,类2表示为2),并通过回归方法来逼近目标类别。然而,这种方法在类别间没有自然顺序或距离的情况下会导致问题。

为了解决这个问题,分类任务通常使用独热向量(one-hot vector)来表示类别。独热向量的每个元素对应一个类别,目标类别的位置为1,其余位置为0。例如,三个类分别可以表示为 [1,0,0][1, 0, 0][1,0,0]、[0,1,0][0, 1, 0][0,1,0]、[0,0,1][0, 0, 1][0,0,1]。在这种表示下,类别之间的距离是相等的,这消除了用数字表示类别时可能引入的错误关系假设。

2. 带有 Softmax 的分类

在实际的分类任务中,模型输入 x 经过一系列的线性变换和激活函数后,输出一个未归一化的得分向量 ​。然而,由于独热向量的每个元素都是0或1,模型输出的  可能包含任意实数,这不利于计算与目标向量的相似度。

为了解决这个问题,我们通常在分类模型的最后一层使用 Softmax 函数。Softmax 函数将未归一化的输出转换为概率分布,使得输出向量 的每个元素都在0到1之间,并且其和为1。Softmax 的计算公式如下:

这种转换不仅将模型的输出归一化,还放大了输出之间的差距,使得较大的值更接近1,较小的值更接近0。

3. 为什么要使用 Softmax?

Softmax 的一个重要作用是将模型的输出转化为一个概率分布,这使得我们可以直接与独热向量进行比较。此外,Softmax 还放大了输出之间的差距,这有助于模型在训练过程中更明确地区分不同类别。

在两类分类任务中,通常使用 Sigmoid 函数代替 Softmax。Sigmoid 和 Softmax 在二分类问题中的效果是等价的,但 Softmax 更适用于多类分类问题。

Softmax与交叉熵损失函数

Softmax函数将模型的输出转换为概率分布,而交叉熵损失函数则用于衡量预测分布与真实分布之间的差异。

# 使用Softmax和交叉熵损失函数
criterion = nn.CrossEntropyLoss()

 交叉熵损失函数在分类任务中表现得非常好,特别是在多分类问题中。它能够有效地惩罚错误的预测,并推动模型朝着正确的方向调整。

4. 分类损失函数的选择:交叉熵 vs 均方误差

在分类任务中,模型输出 后,我们需要计算它与目标向量 y 之间的误差。常见的损失函数有均方误差(MSE)和交叉熵(Cross-Entropy)。尽管 MSE 计算简单,但在分类问题中通常更常用交叉熵损失函数,其公式如下:

交叉熵的优势在于,它不仅能够准确地衡量两个概率分布之间的差异,还能够有效避免梯度消失的问题。例如,在均方误差下,如果初始的预测  和目标 y 相差较大,损失函数的梯度可能非常小,导致训练过程停滞。但交叉熵的梯度在同样的情况下仍然较大,能更好地引导模型朝正确的方向优化。

图 3.36 比较了使用均方误差和交叉熵作为损失函数时,损失表面的不同形态。可以看到,交叉熵下的损失表面有明确的梯度,优化器可以顺利地“走”向目标区域。而在均方误差下,损失表面可能会有较大的平坦区域,梯度接近于零,导致优化过程困难。

因此,在分类问题中,选择交叉熵作为损失函数可以有效减少训练的难度,提高模型的收敛速度和效果。

HW3(CNN)卷积神经网络-图像分类

卷积神经网络(CNN)是处理图像数据的强大工具。它通过卷积层提取图像的空间特征,结合池化层减少计算量,从而在图像分类任务中表现出色。

在运行大模型结果

 

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

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
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder, VisionDataset
from tqdm.auto import tqdm
import random

 

这些库涵盖了从数据处理、神经网络构建、图像转换到显示进度条等功能,为深度学习任务的实现提供了支持。

  • numpypandas: 用于数据处理和分析。
  • torch: PyTorch库的核心,用于张量操作和深度学习模型的构建。
  • torchvision.transforms: 用于图像预处理,如调整大小、归一化等。
  • PIL: 用于图像加载和处理。
  • tqdm: 用于显示训练过程中的进度条。

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

myseed = 6666
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(myseed)
torch.manual_seed(myseed)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(myseed)

2. 数据准备与预处理

数据的准备包括加载和预处理。以下是图像预处理的操作:

test_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
])

train_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    # TODO:可以在这里添加数据增强操作。
    transforms.ToTensor(),
])

train_tfm 用于训练阶段,test_tfm 用于测试和验证阶段。

自定义数据集类 FoodDataset 用于加载图像数据:

class FoodDataset(Dataset):
    def __init__(self, path, tfm=test_tfm, files=None):
        super(FoodDataset).__init__()
        self.path = path
        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):
        fname = self.files[idx]
        im = Image.open(fname)
        im = self.transform(im)
        try:
            label = int(fname.split("/")[-1].split("_")[0])
        except:
            label = -1
        return im, label

 然后创建训练和验证数据加载器:

train_set = FoodDataset("./hw3_data/train", tfm=train_tfm)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=0, pin_memory=True)

valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)
valid_loader = DataLoader(valid_set, batch_size=64, shuffle=True, num_workers=0, pin_memory=True)

3. 定义模型

定义一个卷积神经网络(CNN)模型 Classifier

class Classifier(nn.Module):
    def __init__(self):
        super(Classifier, self).__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 64, 3, 1, 1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),

            nn.Conv2d(64, 128, 3, 1, 1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),

            nn.Conv2d(128, 256, 3, 1, 1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),

            nn.Conv2d(256, 512, 3, 1, 1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),

            nn.Conv2d(512, 512, 3, 1, 1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),
        )
        self.fc = nn.Sequential(
            nn.Linear(512*4*4, 1024),
            nn.ReLU(),
            nn.Linear(1024, 512),
            nn.ReLU(),
            nn.Linear(512, 11)
        )

    def forward(self, x):
        out = self.cnn(x)
        out = out.view(out.size()[0], -1)
        return self.fc(out)

该模型包含5个卷积层、批归一化、ReLU激活函数、最大池化层,以及3个全连接层。

4. 定义损失函数和优化器

device = "cuda" if torch.cuda.is_available() else "cpu"
model = Classifier().to(device)

batch_size = 64
n_epochs = 8
patience = 5

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)

选择交叉熵损失函数和Adam优化器,并设置学习率和L2正则化。

5. 训练模型

模型的训练过程包括多轮训练,每轮包含多个批次的数据。

for epoch in range(n_epochs):
    model.train()
    train_loss = []
    train_accs = []

    for batch in tqdm(train_loader):
        imgs, labels = batch
        logits = model(imgs.to(device))
        loss = criterion(logits, labels.to(device))

        optimizer.zero_grad()
        loss.backward()
        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. 评估模型

在验证集上评估模型:

    model.eval()
    valid_loss = []
    valid_accs = []

    for batch in tqdm(valid_loader):
        imgs, labels = batch
        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)

    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}")

通过监控验证集上的表现,可以保存效果最好的模型。

7. 保存模型

    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"连续 {stale} 次验证没有提升,提前停止训练。")
            break

 训练结束后保存最佳模型,并提前停止策略。

5. 结论与实践建议

在实践中,选择优化器时应根据具体任务的特点进行调整。对于简单任务,SGD可能已经足够,而对于复杂的深度学习模型,自适应学习率优化器(如RMSprop和Adam)通常表现更好。此外,针对分类任务,Softmax与交叉熵损失函数的组合是一种常见且有效的选择。

最后,通过在实际数据集上进行实验,比较不同优化器的表现,可以帮助我们更好地理解和应用这些优化工具。

扩展阅读


这篇博客涵盖了深度学习优化器、自适应学习率、分类任务中的损失函数选择,以及卷积神经网络的应用,并通过代码示例帮助读者更好地理解相关概念。如果有需要进一步调整的地方,请告诉我!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值