优化器 |SGD |Momentum |Adagrad |RMSProp |Adam_哔哩哔哩_bilibili
一、梯度下降法
最常见的权重更新策略
基本思想:先设定一个学习率 η,参数沿梯度的反方向移动。假设需要更新的参数为w,梯度为g,则其更新策略可表示为: (学习率η * 梯度g表示权重更新幅度,用原始权重 - 更新幅度 = 更新后的权重)
梯度下降法有三种不同的形式:
- BGD(Batch Gradient Descent):批量梯度下降,每次参数更新使用 所有样本
- SGD(Stochastic Gradient Descent):随机梯度下降,每次参数更新只使用 1个样本
- MBGD(Mini-Batch Gradient Descent):小批量梯度下降,每次参数更新使用 小部分数据样本(mini_batch)
训练时一般使用小批量梯度下降算法,即选择一个batchsize的数据进行训练
这三个优化算法在训练的时候虽然所采用的的数据量不同,但是他们在进行参数优化的时候,采用的方法是相同的:
- step 1 :
- step 2 :求梯度的平均值
- step 3 : 更新权重 :
优点:
- 算法简洁,当学习率取值恰当时,可以收敛到 全局最优点(凸函数) 或 局部最优点(非凸函数)。
缺点:
对超参数学习率比较敏感:过小导致收敛速度过慢,过大又越过极值点
学习率除了敏感,有时还会因其在迭代过程中保持不变,很容易造成算法被卡在鞍点的位置。
在较平坦的区域,由于梯度接近于0,优化算法会因误判,在还未到达极值点时,就提前结束迭代,陷入局部极小值。
注意,鞍点不是局部最小值,是指个方向梯度都为0的位置
针对上述缺点,更优的优化算法从梯度方面和学习率方面对参数更新方式进行优化。
1.1 一维梯度下降法
以 目标函数(损失函数) 为例来看一看梯度下降是如何工作的(这里 x 为参数)
迭代方法为:
即
- :当前参数。
- :学习率。
- :损失函数在当前参数 θtθt 处的梯度。
使用如下代码来观察 x 是如何迭代的(我们已知最小化f(x)的解为x=0):
这里 x 为模型参数,使用 x=10 作为初始值,并设 学习率 η=0.2,使用梯度下降法 对 x 迭代10次
import numpy as np
import matplotlib.pyplot as pltx = 10
lr = 0.2
result = [x]for i in range(10):x -= lr * 2 * xresult.append(x)f_line = np.arange(-10, 10, 0.1)
plt.plot(f_line, [x * x for x in f_line])
plt.plot(result, [x * x for x in result], '-o')
plt.title('learning rate = {}'.format(lr))
plt.xlabel('x')
plt.ylabel('f(x)')
plt.show()
不同学习率下的结果:
- 如果使用的学习率太小,将导致 x 的更新非常缓慢,需要更多的迭代。
- 相反,当使用过大的学习率, x 的迭代不能保证降低 f(x) 的值,例如,当学习率为=1.1时,超出了最优解 x=0,并逐渐发散;
1.2 多维梯度下降法
多维指有多个参数需要优化,即考虑 的情况。
多元损失函数,它的梯度也是多元的,是一个由d个偏导数组成的向量(即对每一个参数求偏导):
然后选择合适的学率进行梯度下降:
下面通过代码可视化它的参数更新过程。构造一个目标函数,并有二维向量 作为输入,标量作为输出。损失函数的梯度为 。
使用梯度下降,观察从初始位置[-5,-2]的更新轨迹:
import numpy as np
import matplotlib.pyplot as pltdef loss_func(x1, x2): # 定义目标函数return x1 ** 2 + 2 * x2 ** 2x1, x2 = -5, -2
eta = 0.4
num_epochs = 20
result = [(x1, x2)]for epoch in range(num_epochs):gd1 = 2 * x1gd2 = 4 * x2x1 -= eta * gd1x2 -= eta * gd2result.append((x1, x2))# print('x1:', result1)
# print('\n x2:', result2)plt.figure(figsize=(8, 4))
plt.plot(*zip(*result), '-o', color='#ff7f0e')
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
plt.contour(x1, x2, loss_func(x1, x2), colors='#1f77b4')
plt.title('learning rate = {}'.format(eta))
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()
二、动量Momentum
核心思想:在参数更新时,不仅考虑当前梯度,还会参考之前的更新方向(类似物理中的动量概念)。让参数的更新具有惯性。
1. 首先计算动量向量:
- vt:动量向量(存储累积的梯度信息)。
- γ:动量超参数(常取 0.9),表示之前更新方向的影响程度。
- ∇f(θt):当前步的梯度。
- η:学习率。
2. 然后用动量向量更新参数:
优点:
1、加快收敛能帮助参数在 正确的方向上加速前进
2、他可以帮助跳出局部最小值(如下图,下降具有惯性,则会继续往前走一点,发现还能下降,如果没有惯性,它就会被困在局部最小值)
实验一:学习率为0.4的传统梯度下降
损失函数:,的初始值分别为-5,-2,学习率设为0.4
使用不带动量的传统梯度下降法,观察下降过程
预期分析:因为 x1 和 x2 的系数分别是 0.1 和 2, 这就使得 x1 和 x2 的梯度值相差一个量级,如果使用相同的学习率,x2的更新幅度会较x1的更大些。
import numpy as np
import matplotlib.pyplot as pltdef loss_func(x1, x2): #定义目标函数return 0.1 * x1 ** 2 + 2 * x2 ** 2x1, x2 = -5, -2
eta = 0.4
num_epochs = 20
result = [(x1, x2)]for epoch in range(num_epochs):gd1 = 0.2 * x1gd2 = 4 * x2x1 -= eta * gd1x2 -= eta * gd2result.append((x1, x2))plt.plot(*zip(*result), '-o', color='#ff7f0e')
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
plt.contour(x1, x2, loss_func(x1, x2), colors='#1f77b4')
plt.title('learning rate = {}'.format(eta))
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()'''---------------二维图绘制--------------------'''
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation# 定义目标函数
def loss_func(x1, x2):return 0.1 * x1 ** 2 + 2 * x2 ** 2# 梯度下降参数
x1, x2 = -5, -2 # 初始值
eta = 0.6 # 学习率
num_epochs = 20 # 迭代次数
result = [(x1, x2)]# 梯度下降迭代过程
for epoch in range(num_epochs):gd1 = 0.2 * x1 # x1 的梯度gd2 = 4 * x2 # x2 的梯度x1 -= eta * gd1x2 -= eta * gd2result.append((x1, x2))# 将结果拆分为 x1 和 x2 的轨迹
result_x1, result_x2 = zip(*result)# 创建网格和目标函数的轮廓图
x1_vals, x2_vals = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
loss_vals = loss_func(x1_vals, x2_vals)# 创建图形
fig, ax = plt.subplots(figsize=(8, 6))
contour = ax.contour(x1_vals, x2_vals, loss_vals, levels=30, cmap='jet')
point, = ax.plot([], [], 'ro', label="Trajectory") # 当前点
path, = ax.plot([], [], 'r-', alpha=0.5) # 轨迹路径ax.set_title(f'Gradient Descent (learning rate = {eta})')
ax.set_xlabel('x1')
ax.set_ylabel('x2')ax.set_xlim(-6, 2) # x1 轴范围
ax.set_ylim(-1500, 1000) # x2 轴范围ax.legend()# 动画更新函数
def update(frame):if frame == 0:path.set_data([], []) # 清除轨迹point.set_data([result_x1[frame]], [result_x2[frame]]) # 更新当前点path.set_data(list(result_x1[:frame + 1]), list(result_x2[:frame + 1])) # 更新轨迹return point, path# 创建动画
ani = FuncAnimation(fig, update, frames=len(result), interval=500, blit=True)# 保存动图(可选)
ani.save('gradient_descent2.gif', writer='imagemagick')# 显示动图
plt.show()
plt.ion() # 开启交互模式'''--------------------三维图绘制-------------------------'''
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from mpl_toolkits.mplot3d import Axes3D# 定义目标函数
def loss_func(x1, x2):return 0.1 * x1 ** 2 + 2 * x2 ** 2# 梯度下降参数
x1, x2 = -5, -2 # 初始值
eta = 0.6 # 学习率
num_epochs = 20 # 迭代次数
result = [(x1, x2, loss_func(x1, x2))] # 保存 (x1, x2, z) 的轨迹# 梯度下降迭代过程
for epoch in range(num_epochs):gd1 = 0.2 * x1 # x1 的梯度gd2 = 4 * x2 # x2 的梯度x1 -= eta * gd1x2 -= eta * gd2result.append((x1, x2, loss_func(x1, x2))) # 保存更新后的点# 将结果拆分为 x1, x2, z 的轨迹
result_x1, result_x2, result_z = zip(*result)# 创建网格和目标函数表面
x1_vals = np.linspace(-20, 20, 100) # 扩大 x1 的范围
x2_vals = np.linspace(-20, 20, 100) # 扩大 x2 的范围
x1_grid, x2_grid = np.meshgrid(x1_vals, x2_vals)
z_grid = loss_func(x1_grid, x2_grid)# 创建图形
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')# 绘制目标函数的三维表面
ax.plot_surface(x1_grid, x2_grid, z_grid, cmap='viridis', alpha=0.8)
trajectory, = ax.plot([], [], [], 'r-', label='Trajectory', linewidth=2)
point, = ax.plot([], [], [], 'ro', label='Current Point')# 设置轴范围和 z 轴范围
ax.set_xlim(-20, 20)
ax.set_ylim(-20, 20)
ax.set_zlim(0, 2000) # 扩大 z 轴范围# 动画更新函数
def update(frame):trajectory.set_data(result_x1[:frame + 1], result_x2[:frame + 1])trajectory.set_3d_properties(result_z[:frame + 1])point.set_data([result_x1[frame]], [result_x2[frame]])point.set_3d_properties([result_z[frame]])return trajectory, point# 创建动画
ani = FuncAnimation(fig, update, frames=len(result), interval=500, blit=False)
# 保存动图(可选)
ani.save('3d_gradient_descent2.gif', writer='imagemagick')plt.show()
实验二:学习率为0.6的传统梯度下降
这时,我们会陷入一个两难的选择:
- 如果选择较小的学习率。x_1 收敛缓慢
- 如果选择较大的学习率,x_1方向会收敛很快,但在 x2 方向不会收敛
实验三:带动量的下降法
动量参数0.5,学习率0.4
动量更新公式:
其中,目标函数的梯度为:
参数更新公式:
左 0.4 右0.6
三、Adagrad 自适应学习率优化算法
之前随机梯度下降法,对所有的参数,都是使用相同的、固定的学习率进行优化的,但是不同的参数的梯度差异可能很大,使用相同的学习率,效果不会很好
举例:
- 假设损失函数是 , 初值分别为 x1=40,x2=20
(通过观察,我们即可知道,x_1=0, x_2=0就是两个参数的极值点 )
- 将要移动的幅度 小于将移动的幅度
而 x1 距离离极值点 x1=0 是较远的,所以,我们使用梯度下降法,效果并不会好
核心思想:根据参数的历史梯度信息,为每个参数维度设置不同的学习率。梯度更新的维度越大,对应的学习率就越小;梯度更新较小的维度对应的学习率则会保持较大。
Adagrad 的公式如下:
学习率变为:
权重更新如下:
- :当前时刻 t的梯度。
- :梯度平方的累积和,用于记录历史信息。初始值为0
- :全局初始学习率(用户设定的超参数)。
- :一个小的平滑项,避免分母为零(通常取 )。
- :学习率会随历史梯度变化逐步减小
优点:
- 稀疏特征的处理:Adagrad 对频繁更新的参数会减小学习率,而对不常更新的参数会保持较大的学习率,因此特别适用于处理稀疏数据。
- 自动调节学习率:用户无需手动调整学习率,算法会根据梯度自动调整。
缺点:
- 历史梯度的平方和会不断累积,使得学习率随着训练逐渐减小,导致后期可能过早停止学习。
实验
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation# 定义目标函数
def loss_func(x1, x2):return 0.1 * x1 ** 2 + 2 * x2 ** 2# 初始化参数
x1, x2 = -5.0, -2.0 # 参数初始值
eta = 0.4 # 全局初始学习率
epsilon = 1e-8 # 平滑因子,防止除零
num_epochs = 50 # 迭代次数
result = [(x1, x2)] # 保存轨迹 (x1, x2)# Adagrad 累积梯度平方初始化
grad_squared_sum_x1 = 0
grad_squared_sum_x2 = 0# Adagrad 迭代过程
for epoch in range(num_epochs):# 计算梯度grad_x1 = 0.2 * x1 # 对 x1 的梯度grad_x2 = 4 * x2 # 对 x2 的梯度# 累积历史梯度平方grad_squared_sum_x1 += grad_x1 ** 2grad_squared_sum_x2 += grad_x2 ** 2# 自适应调整学习率adjusted_eta_x1 = eta / (np.sqrt(grad_squared_sum_x1) + epsilon)adjusted_eta_x2 = eta / (np.sqrt(grad_squared_sum_x2) + epsilon)# 参数更新x1 -= adjusted_eta_x1 * grad_x1x2 -= adjusted_eta_x2 * grad_x2# 保存更新后的参数result.append((x1, x2))# 将结果拆分为 x1 和 x2 的轨迹
result_x1, result_x2 = zip(*result)# 创建网格和目标函数的轮廓图
x1_vals, x2_vals = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
loss_vals = loss_func(x1_vals, x2_vals)# 创建图形
fig, ax = plt.subplots(figsize=(8, 6))
contour = ax.contour(x1_vals, x2_vals, loss_vals, levels=30, cmap='jet')
point, = ax.plot([], [], 'ro', label="Trajectory") # 当前点
path, = ax.plot([], [], 'r-', alpha=0.5) # 轨迹路径ax.set_title(f'Adagrad Optimization (eta = {eta})')
ax.set_xlabel('x1')
ax.set_ylabel('x2')ax.set_xlim(-6, 2) # x1 轴范围
ax.set_ylim(-3, 1) # x2 轴范围ax.legend()# 动画更新函数
def update(frame):if frame == 0:path.set_data([], []) # 清除轨迹point.set_data([result_x1[frame]], [result_x2[frame]]) # 更新当前点path.set_data(list(result_x1[:frame + 1]), list(result_x2[:frame + 1])) # 更新轨迹return point, path# 创建动画
ani = FuncAnimation(fig, update, frames=len(result), interval=500, blit=True)# 保存动图(可选)
ani.save('adagrad_optimization.gif', writer='pillow')# 显示动图
plt.show()
四、RMSProp Root Mean Square Propagation 均方根传播
核心思想:RMSProp 是对 Adagrad的一种改进。Adagrad 在处理稀疏数据时表现良好,但由于它会不断累积梯度平方和,导致学习率不断减小,最终可能停止学习。RMSProp 通过对梯度平方的 指数加权移动平均 (Exponential Moving Average, EMA),解决了这一问题。
1.梯度计算 :
2. 梯度平方的指数加权移动平均:
3. 参数更新规则:
- 是当前时刻 t 的梯度
- 是梯度平方的指数加权移动平均
- 是衰减率(通常取 0.9 或类似值),用于控制历史梯度对当前更新的影响
- 是当前梯度的平方
- 是学习率
- 是平滑项,避免分母为零(通常取 )。
解决 Adagrad 的学习率衰减问题:Adagrad 会累积所有历史梯度平方,导致学习率持续下降,RMSProp 使用指数加权移动平均,限制了学习率的下降幅度。
五、Adam
Adam 是一种深度学习中非常常用的优化算法,结合了 Momentum 动量法 和 RMSProp 算法 的优点。
Adam 的核心思想
动量更新:
- Adam 使用梯度的一阶移动平均值(类似于 Momentum)来加速收敛,特别是在鞍点或长沟谷中。
自适应学习率:
- Adam 使用梯度的二阶矩(平方)的指数加权移动平均值(类似于 RMSProp),为每个参数维度动态调整学习率。
偏差校正:
- Adam 对一阶和二阶矩的估计进行偏差修正,确保算法在初始阶段的估计值更准确。
通过结合动量法和 RMSProp,Adam 既能在复杂损失函数中快速找到收敛方向,又能避免学习率过快或过慢的问题。
1. 梯度计算
2. 一阶矩估计(梯度的指数加权移动平均)
3. 二阶矩估计(梯度平方的指数加权移动平均)
4. 偏差矫正
5. 参数更新
- 是当前时刻 t 的梯度
- 是一阶矩(动量)的估计,表示梯度的加权平均。
- 是一阶矩的衰减率,通常取 0.9。
- 是二阶矩(梯度平方)的估计,用于动态调整学习率。
- 是二阶矩的衰减率,通常取 0.999。
- t 是当前迭代的步数
- 是学习率
- 是平滑项,避免分母为零(通常取 )。
为什么要偏差矫正?
在 Adam 优化中,由于一阶和二阶矩的初始值为零,在前几步会发生「偏差收缩」,导致值比真实值小,影响优化效率。偏差校正通过调整公式,修正了这种偏差。
举例说明:
实验
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation# 定义目标函数
def loss_func(x1, x2):return 0.1 * x1 ** 2 + 2 * x2 ** 2# 初始化参数
x1, x2 = -5.0, -2.0 # 参数初始值
eta = 0.4 # 学习率
beta1 = 0.9 # 一阶矩衰减率
beta2 = 0.999 # 二阶矩衰减率
epsilon = 1e-8 # 平滑因子,防止除零
num_epochs = 50 # 迭代次数
result = [(x1, x2)] # 保存轨迹 (x1, x2)# Adam 的一阶矩和二阶矩初始化
m_x1, m_x2 = 0, 0 # 一阶矩初始化
v_x1, v_x2 = 0, 0 # 二阶矩初始化# Adam 优化过程
for t in range(1, num_epochs + 1): # 从 1 开始,方便偏差校正# 计算梯度grad_x1 = 0.2 * x1 # 对 x1 的梯度grad_x2 = 4 * x2 # 对 x2 的梯度# 一阶矩(动量)的更新m_x1 = beta1 * m_x1 + (1 - beta1) * grad_x1m_x2 = beta1 * m_x2 + (1 - beta1) * grad_x2# 二阶矩(梯度平方)的更新v_x1 = beta2 * v_x1 + (1 - beta2) * (grad_x1 ** 2)v_x2 = beta2 * v_x2 + (1 - beta2) * (grad_x2 ** 2)# 偏差校正m_hat_x1 = m_x1 / (1 - beta1 ** t)m_hat_x2 = m_x2 / (1 - beta1 ** t)v_hat_x1 = v_x1 / (1 - beta2 ** t)v_hat_x2 = v_x2 / (1 - beta2 ** t)# 参数更新x1 -= eta * m_hat_x1 / (np.sqrt(v_hat_x1) + epsilon)x2 -= eta * m_hat_x2 / (np.sqrt(v_hat_x2) + epsilon)# 保存更新后的参数result.append((x1, x2))# 将结果拆分为 x1 和 x2 的轨迹
result_x1, result_x2 = zip(*result)# 创建网格和目标函数的轮廓图
x1_vals, x2_vals = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
loss_vals = loss_func(x1_vals, x2_vals)# 创建图形
fig, ax = plt.subplots(figsize=(8, 6))
contour = ax.contour(x1_vals, x2_vals, loss_vals, levels=30, cmap='jet')
point, = ax.plot([], [], 'ro', label="Trajectory") # 当前点
path, = ax.plot([], [], 'r-', alpha=0.5) # 轨迹路径ax.set_title(f'Adam Optimization (eta = {eta})')
ax.set_xlabel('x1')
ax.set_ylabel('x2')ax.set_xlim(-6, 2) # x1 轴范围
ax.set_ylim(-3, 1) # x2 轴范围ax.legend()# 动画更新函数
def update(frame):if frame == 0:path.set_data([], []) # 清除轨迹point.set_data([result_x1[frame]], [result_x2[frame]]) # 更新当前点path.set_data(list(result_x1[:frame + 1]), list(result_x2[:frame + 1])) # 更新轨迹return point, path# 创建动画
ani = FuncAnimation(fig, update, frames=len(result), interval=500, blit=True)# 保存动图(可选)
ani.save('adam_optimization.gif', writer='pillow')# 显示动图
plt.show()
六、总结
算法 | 特点 |
SGD | 固定学习率,易受噪声影响 |
Momentum | 引入动量,累积历史梯度 |
Adagrad | 自适应学习率,累积梯度平方 |
RMSProp | 使用梯度平方的指数加权平均调整学习率 |
Adam | 结合 Momentum 和 RMSProp,使用一阶和二阶矩调整学习率 |