深度学习 | 梯度下降算法及其变体

一、最优化与深度学习

1.1、训练误差与泛化误差

         

 1.2、经验风险

         

 1.3、优化中的挑战

         

       1.3.1、局部最小值

        

        1.3.2、 鞍点

        经常是由于模型复杂度过高或者训练样本数据过少造成的 —— Overfitting

        

        1.3.3、悬崖

        

         1.3.4、长期依赖问题

        

 

二、损失函数

2.1、损失函数的起源

  • 损失函数(loss function):衡量预测值和真实值之间差异的函数
  • 损失函数的起源可以追溯到统计学和最小二乘法

 

 2.2、基础

  • 最大似然估计 Maximum Likelihood Estimation,MLE
    • 假定X服从p model
    • 若p(似然函数)为高斯分布,MLE即为最小化均方误差MSE
  • 交叉嫡损失概率分布解释 Cross Entropy
    • 交叉嫡损失从概率分布角度来说,也是最大似然估计MLE
    • 若我们不知道p model或者他不是高斯分布,此时我们可以通过训练样本的出现概率来估计,相当于缩放了上面的函数,此公式即交叉熵损失的定义
  • 最大化后验 Maximum A Posteriori
    • L2正则化 —— 先验为高斯分布
    • L1正则化 —— 先验为拉普拉斯分布
  • 贝叶斯估计 Bayesian Estimation
    • 频率派的人认为数据是含有参数的随机变量
    • 贝叶斯派认为数据是被直接观测到的,因此不是随机的

2.3、损失函数的性质

  • 可微性(differentiability) ︰函数在任意一点处都有一个导数
  • 可导性(continuity) ︰函数有连续的导函数
  • 凸函数保证损失函数有全局最小值,可以用较简单优化算法
  • 凹函数则需要使用更复杂的优化算法找最小值
  • 如何判断函数凸性?
  • 凸约束和凸优化
    • 凸约束可以将非凸问题转化成凸优化问题。
  • Jensen不等式

三、梯度下降

3.1、搜索逼近策略

        先确定方向:梯度        再确定步长:学习率

         

3.2、梯度

        梯度就是函数曲面的陡度,偏导数是某个具体方向上的陡度

        梯度就等于所有方向上偏导数的向量和

        

3.3、学习率

        学习率太小,收敛慢

        学习率太大,不收敛

        

3.4、梯度下降法 —— 初始值、梯度、学习率

        ① 确定起始点

        ②计算

        ③控制好油门 (学习率)~

 四、随机梯度下降法(Stochastic Gradient Descent)

 4.1、梯度下降法的问题

  • ·不能保证被优化函数达到全局最优解
  • ·全部训练数据上最小化损失,计算时间太长
  • ·如果函数形态复杂,可能会在局部最小值附近来回震荡·对于初始值的选择非常敏感

4.2、SGD基本思想

  • ·每次迭代中仅使用一个样本来计算梯度
  • ·根据梯度来调整参数的值

 4.3、动态学习率

        使用动态学习率可以帮助模型更快地收敛

        

 五、小批量梯度下降法(Mini-Batch Stochastic Gradient Descent)

  •  

决定批量大小的因素

  • 过大的批量虽然使得梯度估计更精确,但回报小
  • 太小的批量难以充分利用多核架构
  • 并行处理下,内存消耗和批量大小成正比
  • 2的幂次方在使用GPU时可以提高效率,故取值32-256之间
  • 注意:随机抽取

差别:

        

代码实现:

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
from tqdm import *

# 定义模型和损失函数
class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden1 = nn.Linear(1, 32)
        self.hidden2 = nn.Linear(32, 32)
        self.output = nn.Linear(32, 1)

    def forward(self, x):
        x = torch.relu(self.hidden1(x))
        x = torch.relu(self.hidden2(x))
        return self.output(x)
loss_fn = nn.MSELoss()

# 生成随机数据
np.random.seed(0)
n_samples = 1000
x = np.linspace(-5, 5, n_samples)
y = 0.3 * (x ** 2) + np.random.randn(n_samples)

# 转换为Tensor
x = torch.unsqueeze(torch.from_numpy(x).float(), 1)
y = torch.unsqueeze(torch.from_numpy(y).float(), 1)

# 将数据封装为数据集
dataset = torch.utils.data.TensorDataset(x, y)

names = ["Batch", "Stochastic", "Minibatch"] # 批量梯度下降法、随机梯度下降法、小批量梯度下降法
batch_size = [n_samples, 1, 128]
momentum= [1,0,1]
losses = [[], [], []]

# 超参数
learning_rate = 0.0001
n_epochs = 1000

# 分别训练
for i in range(3):
    model = Model()
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum[i])
    dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size[i], shuffle=True)
    for epoch in tqdm(range(n_epochs), desc=names[i], leave=True, unit=' epoch'):
        x, y = next(iter(dataloader))
        optimizer.zero_grad()
        out = model(x)
        loss = loss_fn(out, y)
        loss.backward()
        optimizer.step()
        losses[i].append(loss.item())

# 使用 Matplotlib 绘制损失值的变化趋势
for i, loss_list in enumerate(losses):
    plt.figure(figsize=(12, 4))
    plt.plot(loss_list)
    plt.ylim((0, 15))
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title(names[i])
    plt.show()
Batch: 100%|██████████| 1000/1000 [00:07<00:00, 129.91 epoch/s]
Stochastic: 100%|██████████| 1000/1000 [00:00<00:00, 2397.32 epoch/s]
Minibatch: 100%|██████████| 1000/1000 [00:01<00:00, 780.15 epoch/s]

 

六、动量法

 

6.1、物理学中的动量

        动量指的是这个物体在它运动方向上保持运动的趋势

        动量是一个向量。

                

        动量守恒定律:

                

6.2、深度学习中的动量

        一阶动量:过去各个时刻梯度的线性组合。

                

        二阶动量:过去各个时刻梯度的平方的线性组合。

                 

6.3、基本思想

        将当前的梯度与上一步的梯度加权平均来减少梯度的震荡。

        

        

6.4、优缺点

        

6.5、可视化网站:

        

 

 

 七、AdaGrad算法

 

        传统的SGD以及各种变种都是以同样的学习率来更新每个参数的,但是深度神经网络往往包含大量参数,而且这些参数并不总是用得到的。对于经常更新的参数,我们已经积累了大量知识,就不希望被新的样本影响太大,换句话说,就是对于更新很频繁的参数 可以将学习率慢一些。

        而对于更新慢的参数,我们了解到的信息太少,希望从每一个偶然出现的样本多学一些,也就是学习率大一些,

        那怎么动态的度量历史更新的频率呢?

        ———— 二阶动量

7.1、基本思想

        根据二阶动量动态调整学习率。

        gτ 为历史梯度值。有平方可以把正负去掉,累加。

        

7.2、算法流程

        1、计算 目标函数 关于当前参数的 梯度 gt,根据历史梯度计算 一阶动量 mt 和 二阶动量 vt

        2、计算当前时刻的下降梯度 η ,其中 α 为学习率,一般为了避免分母为零,会加上一个平方项。

        

        参数更新越频繁,二阶动量越大,学习率就越小。

        3、进行梯度更新:

        

 

 7.3、稀疏特征

        指的是在很多样本中只有少数出现过的特征。

        训练模型时,稀疏特征可能很少更新,导致训练不出理想结果。

7.4、优缺点

        

7.5、代码实现

import torch
import matplotlib.pyplot as plt

# 假设我们有一个简单的线性回归模型
# y = w * x + b
# 其中 w 和 b 是需要学习的参数

# 定义超参数
learning_rate = 0.01
num_epochs = 100

# 随机生成训练数据
X = torch.randn(100, 1)
y = 2 * X + 3 + torch.randn(100, 1)

# 初始化参数
w = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)

# 创建 Adagrad optimizer
optimizer = torch.optim.Adagrad([w, b], lr=learning_rate)

# 记录每次迭代的 loss
losses = []

# 训练模型
for epoch in range(num_epochs):
  # 计算预测值
  y_pred = w * X + b

  # 计算 loss
  loss = torch.mean((y_pred - y) ** 2)

  # 记录 loss
  losses.append(loss.item())

  # 清空上一步的梯度
  optimizer.zero_grad()

  # 计算梯度
  loss.backward()

  # 更新参数
  optimizer.step()

# 可视化训练过程
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.show()

 

 

 八、RMSProp / AdaDelta算法

两种对二阶动量进行优化的方法基本思想

        时序累加修改二阶动量,动态调整学习率

RMSProp 2012年提出

        AdaGrad单调递减的学习率变化过于激进,所以

        改变二阶动量计算方法的策略:

                不累积全部历史梯度,而只关注过去一段时间窗口的下降梯度。

        β2 叫做衰减率系数。

         

优缺点

         

AdaDelta 2011年提出

        避免使用手动调整学习率的方法来控制训练过程,而是自动调整学习率,

        使得训练过程更加顺畅。

        主要由两部分组成:

                梯度的积分和更新的规则。

                梯度积分:对梯度进行累加并记录

        

 优缺点

        

 

 

 九、Adam算法 ***

9.1、基本思想

        把一阶动量和二阶动量都用起来,Adaptive + Momentum

                gt 当前时间步的梯度

                mt 和 vt 一阶 二阶矩估计向量(一阶 二阶动量)

                β1 β2 两个衰减率的超参数,一般取值 0.9/0.999

                偏差校正即更新 mt 和 vt

                更新 θt ,ε保证分母不会等于0

        

 9.2梯度下降法及其变体关系

        

9.3、原理框架流程

         定义优化参数w,目标函数f(w),初始学习率 α
        开始每个epoch迭代优化:
                1、计算目标函数当前梯度

                        
                2、根据历史梯度计算一阶动量和二阶动量

                        

                3、计算当前时刻参数更新量

                        

                4、迭代更新权重参数

                        

 不同的优化算法为什么效果差这么多?

9.4、核心差异

        区别在于下降方向。

        前半部分是学习率(下降步长),后半部分是下降方向。

        SGD的下降方向就是该位置梯度方向的反方向;

        自适应学习率算法 RMSprop 为每个参数设定了不同的学习率,因此下降方向是缩放过的一阶动量的方向。

        

        下图中,横坐标表示降维后的特征空间,区域的颜色表示目标函数值的变化。

        

 

9.5、最优选择策略讨论

        不想做精细的调优,那么Adam;

        更加自如地控制优化迭代的各类参数,那么SGD;

        先用Adam快速下降,再用SGD调优;

        算法美好,数据王道! 

        

 

 

 十、梯度下降代码实现

 

10.1 梯度下降过程

10.1.1、二维平面内的梯度下降

# 导入必要的库
import torch
import matplotlib.pyplot as plt
# 定义函数
def f(x):
    return x ** 2 + 4 * x + 1

# 定义初始值
x = torch.tensor(-10., requires_grad=True)

# 迭代更新参数
learning_rate = 0.9

# 用于记录每一步梯度下降的值
xs = []
ys = []
# 开始迭代
for i in range(100):
    # 计算预测值和损失
    y = f(x)

    # 记录参数和损失
    xs.append(x.item())
    ys.append(y.item())

    # 反向传播求梯度
    y.backward()

    # 更新参数
    with torch.no_grad():
        x -= learning_rate * x.grad

        # 梯度清零
        x.grad.zero_()
        
# 打印结果
print(f'最终参数值:{x.item()}')
最终参数值:-2.000000238418579
# 显示真实的函数曲线
x_origin = torch.arange(-10, 10, 0.1)
y_origin = f(x_origin)
plt.plot(x_origin, y_origin,'b-')

# 绘制搜索过程
plt.plot(xs,ys,'r--')
plt.scatter(xs, ys, s=50, c='r')  # 圆点大小为 50,颜色为红色
plt.xlabel('x')
plt.ylabel('y')
plt.show()

 

10.1.2 三维平面内的梯度下降

# 定义函数
def f(x, y):
    return x ** 2 + 2* y ** 2

# 定义初始值
x = torch.tensor(-10., requires_grad=True)
y = torch.tensor(-10., requires_grad=True)

# 记录每一步的值
xs = []
ys = []
zs = []

# 迭代更新参数
learning_rate = 0.1
# 开始迭代
for i in range(100):
    # 计算预测值和损失
    z = f(x, y)

    # 记录参数和损失
    xs.append(x.item())
    ys.append(y.item())
    zs.append(z.item())

    # 反向传播
    z.backward()

    # 更新参数
    x.data -= learning_rate * x.grad
    y.data -= learning_rate * y.grad

    # 清空梯度
    x.grad.zero_()
    y.grad.zero_()

# 打印结果
print(f'最终参数值:x={x.item()}, y={y.item()}')
最终参数值:x=-2.0370367614930274e-09, y=-6.533180924230175e-22
# 绘制图像
ax = plt.figure().add_subplot(projection='3d')
ax.plot(xs, ys, zs, 'r-')
ax.scatter(xs, ys, zs, s=50, c='r')  # 圆点大小为 50,颜色为红色

plt.show()

# 绘制原始的二维函数图像
X, Y = torch.meshgrid(torch.arange(-10, 10, 0.1), torch.arange(-10, 10, 0.1), indexing='ij')
Z = f(X, Y)
plt.contour(X, Y, Z, levels=30)

# 绘制搜索过程曲线
plt.plot(xs, ys, 'r-')
plt.scatter(xs, ys, s=50, c='r')  # 圆点大小为 50,颜色为红色
plt.show()

 

 

10.2. 不同优化器效果对比

# 导入必要的库
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset  # 用于构造数据加载器
from torch.utils.data import random_split # 用于划分数据集
import torch.optim as optim
# 定义函数
def f(x, y):
    return x ** 2 + 2 * y ** 2

# 定义初始值
num_samples = 1000 # 1000个样本点
X = torch.rand(num_samples) # 均匀分布
Y = torch.rand(num_samples) # 均匀分布
Z = f(X,Y) +  torch.randn(num_samples)  #高斯分布扰动项

dataset = torch.stack([X, Y, Z], dim = 1)
dataset[0]
tensor([0.3720, 0.4497, 1.0605])
# 按照8:2划分数据集
train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size

train_dataset, test_dataset = random_split(dataset=dataset, lengths=[train_size, test_size])

# 将数据封装成数据加载器
train_dataloader = DataLoader(TensorDataset(train_dataset.dataset.narrow(1,0,2), train_dataset.dataset.narrow(1,2,1)),
                              batch_size=32, shuffle=False)
test_dataloader = DataLoader(TensorDataset(test_dataset.dataset.narrow(1,0,2), test_dataset.dataset.narrow(1,2,1)),
                             batch_size=32, shuffle=False)
# 定义一个简单模型
class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(2, 8)
        self.output = nn.Linear(8, 1)

    def forward(self, x):
        x = torch.relu(self.hidden(x))
        return self.output(x)
# 定义损失函数
loss_fn = nn.MSELoss()

# 初始化模型序列
opt_labels = ['SGD', 'Momentum', 'Adagrad', 'RMSprop', 'Adadelta', 'Adam']
models = [Model(), Model(), Model(), Model(), Model(), Model()] 

# 优化器列表
SGD = optim.SGD(models[0].parameters(), lr=learning_rate)
Momentum = optim.SGD(models[1].parameters(), lr=learning_rate, momentum=0.8, nesterov=True)
Adagrad = optim.Adagrad(models[2].parameters(), lr=learning_rate)
RMSprop = optim.RMSprop(models[3].parameters(), lr=learning_rate)
Adadelta = optim.Adadelta(models[4].parameters(), lr=learning_rate)
Adam = optim.Adam(models[5].parameters(), lr=learning_rate)
opts = [SGD, Momentum, Adagrad, RMSprop, Adadelta, Adam]

# 定义训练和测试误差历史记录数组
train_losses_his = [[],[],[],[],[],[]]
test_losses_his = [[],[],[],[],[],[]]

# 超参数
num_epochs = 50
learning_rate = 0.01 # 学习率
# 模型训练和测试
for epoch in range(num_epochs):
    # 当前epoch每个模型在训练集上的总损失列表
    train_losses = [0,0,0,0,0,0]
    # 遍历训练集
    for inputs, targets in train_dataloader:
        # 迭代不同的模型
        for index, model, optimizer, loss_history in zip(range(6), models, opts, train_losses_his):
            # 预测、损失函数、反向传播
            model.train()
            outputs = model(inputs)
            loss = loss_fn(outputs, targets)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            # 记录loss
            train_losses[index] += loss.item()

    
    # 当前epoch每个模型在训测试集上的总损失列表
    test_losses = [0,0,0,0,0,0]
    # 在测试数据上评估,测试模型不计算梯度
    with torch.no_grad():
        # 遍历测试集
        for inputs, targets in test_dataloader:
            # 迭代不同的模型
            for index, model, optimizer, loss_history in zip(range(6), models, opts, test_losses_his):
            # 预测、损失函数、反向传播
                model.eval()
                outputs = model(inputs)
                loss = loss_fn(outputs, targets)
                test_losses[index] += loss.item()
    
    # 计算loss并记录到历史记录中
    for i in range(6):
        train_losses[i] /= len(train_dataloader)
        train_losses_his[i].append(train_losses[i])
        test_losses[i] /= len(test_dataloader)
        test_losses_his[i].append(test_losses[i])
# 绘制训练集损失曲线
for i, l_his in enumerate(train_losses_his):
    plt.plot(l_his, label=opt_labels[i])
plt.legend(loc='best')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.show()

# 绘制测试集损失曲线
for i, l_his in enumerate(test_losses_his):
    plt.plot(l_his, label=opt_labels[i])
plt.legend(loc='best')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.show()

 

 

 十一、学习率调节器

 

需要考虑的因素:

        

        学习率是各类优化算法中的最关键的参数之一;

        学习率调节器能够在训练过程中动态调整学习率。

        

        

        

        

        

11.2、代码实现

# 导入必要的库
import torch
import numpy as np
import matplotlib.pyplot as plt
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset # 用于构造数据加载器
from torch.utils.data import random_split # 用于划分数据集

11.2.1、数据生成

# 定义函数
def f(x, y):
    return x ** 2 + 2 * y ** 2

# 定义初始值
num_samples = 1000 # 1000个样本点
X = torch.rand(num_samples) # 均匀分布
Y = torch.rand(num_samples) # 均匀分布
Z = f(X,Y) + 3 * torch.randn(num_samples)

dataset = torch.stack([X, Y, Z], dim = 1)

11.2.3、数据划分

# 按照7:3划分数据集
train_size = int(0.7 * len(dataset))
test_size = len(dataset) - train_size

train_dataset, test_dataset = random_split(dataset=dataset, lengths=[train_size, test_size])

# 将数据封装成数据加载器
train_dataloader = DataLoader(TensorDataset(train_dataset.dataset.narrow(1,0,2), train_dataset.dataset.narrow(1,2,1)), batch_size=32)
test_dataloader = DataLoader(TensorDataset(test_dataset.dataset.narrow(1,0,2), test_dataset.dataset.narrow(1,2,1)), batch_size=32)

11.2.4、模型定义

# 定义一个简单模型
class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(2, 8)
        self.output = nn.Linear(8, 1)

    def forward(self, x):
        x = torch.relu(self.hidden(x))
        return self.output(x)

11.2.5、模型训练对比

# 超参数
num_epochs = 100
learning_rate = 0.1 # 学习率,故意调大一些更直观

# 定义损失函数
loss_fn = nn.MSELoss()

# 通过一个训练对比有无学习率调节器的效果
for with_scheduler in [False, True]:

    # 定义训练和测试误差数组
    train_losses = []
    test_losses = []

    # 初始化模型
    model = Model()

    # 定义优化器
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

    # 定义学习率调节器
    scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.99)

    # 迭代训练
    for epoch in range(num_epochs):
        # 在训练数据上迭代
        model.train()
        train_loss = 0
        # 遍历训练集
        for inputs, targets in train_dataloader:
            # 预测、损失函数、反向传播
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = loss_fn(outputs, targets)
            loss.backward()
            optimizer.step()
            # 记录loss
            train_loss += loss.item()

        # 计算loss并记录到训练误差
        train_loss /= len(train_dataloader)
        train_losses.append(train_loss)

        # 在测试数据上评估,测试模型不计算梯度
        model.eval()
        test_loss = 0
        with torch.no_grad():
            # 遍历测试集
            for inputs, targets in test_dataloader:
                # 预测、损失函数
                outputs = model(inputs)
                loss = loss_fn(outputs, targets)
                # 记录loss
                test_loss += loss.item()

            # 计算loss并记录到测试误差
            test_loss /= len(test_dataloader)
            test_losses.append(test_loss)

        # 是否更新学习率
        if with_scheduler:
            scheduler.step()
    
    # 绘制训练和测试误差曲线
    plt.figure(figsize=(8, 4))
    plt.plot(range(num_epochs), train_losses, label="Train")
    plt.plot(range(num_epochs), test_losses, label="Test")
    plt.title("{0} lr_scheduler".format("With" if with_scheduler else "Without"))
    plt.legend()
#     plt.ylim((1, 2))
    plt.show()

11.2.6、常见学习率调节器

# 学习率衰减,例如每训练100次就将学习率降低为原来的一半
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=100, gamma=0.5)
# 指数衰减法,每次迭代将学习率乘上一个衰减率
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.99)
# 余弦学习率调节,optimizer初始学习率为最大学习率,eta_min是最小学习率,T_max是最大迭代次数
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=0.00001)
# 自定义学习率,通过一个lambda函数实现自定义的学习率调节器
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda epoch: 0.99 ** epoch)
# 预热
warmup_steps = 20
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda t: min(t / warmup_steps, 0.001))

 

参考

深度学习必修课:进击算法工程师【梗直哥瞿炜】_哔哩哔哩_bilibili

Deep-Learning-Code: 《深度学习必修课:进击算法工程师》配套代码 – Gitee.com

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年12月26日
下一篇 2023年12月26日

相关推荐