梯度下降优化算法中,可能会碰到以下情况:
- 碰到平缓区域,梯度值较小,参数优化变慢
- 碰到“鞍点”,梯度为0,参数无法优化
- 碰到局部最小值,参数不是最优
因此更加先进的优化算法,来改善参数更新策略,使得训练过程更稳定、更快速,并更容易找到更优的解,从而有效降低损失函数。
1. 指数移动加权平均
1.1 公式定义
指数移动加权平均(Exponential Moving Average, EMA)是一种用于计算时间序列数据平均值的统计方法,其中最近的观测值被赋予更大的权重,而较旧的观测值权重按指数方式递减。公式如下:
其中:
- \(S_t\):时间 \(t\) 的指数加权平均值。
- \(Y_t\):时间 \(t\) 的实际观测值。
- \(\beta\):平滑因子(介于0和1之间),控制权重的衰减速度。
展开 \(S_t\):
可以看到,每个历史观测值 \(Y_{t-k}\) 的权重是 \((1 - \beta) \beta^k\),这是一个指数递减的权重:
- \(\beta\) 越大(接近1),过去的平均值对当前的影响越大,新的观测值的影响越小,因此平均值变化更平缓(更“平滑”)。
- \(\beta\) 越小(接近0),新的观测值的影响越大,平均值对近期变化更敏感,波动更大。
1.2 计算示例
模拟一个月(30天)的每日温度,并用指数移动平均(EMA)方法来平滑这些温度数据:
import torch
import matplotlib.pyplot as plt
ELEMENT_NUMBER = 30 # 定义元素数量为30
# 1. 实际平均温度
def dayTemperature():
# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度,范围在10左右波动
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)
# 绘制平均温度
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, temperature, color='r', label='Daily Temperature')
plt.scatter(days, temperature)
plt.xlabel('Day')
plt.ylabel('Temperature')
plt.title('Actual Daily Temperature')
plt.legend()
plt.show()
# 2. 指数移动平均温度
def EMATemperature(beta=0.9):
torch.manual_seed(0) # 固定随机数种子
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10 # 产生30天的随机温度
exp_weight_avg = []
for idx, temp in enumerate(temperature, 1): # 从下标1开始(实际是第0天)
# 第一个元素的EMA值等于自身
if idx == 1:
exp_weight_avg.append(temp)
continue
# 后续元素的 EMA 值等于上一个 EMA * β + 当前气温乘以(1-β)
new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
exp_weight_avg.append(new_temp)
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, exp_weight_avg, color='b', label='EMA Temperature')
plt.scatter(days, temperature, color='r', label='Daily Temperature')
plt.xlabel('Day')
plt.ylabel('Temperature')
plt.title(f'Exponential Moving Average (beta={beta})')
plt.legend()
plt.show()
# 测试
dayTemperature() # 绘制实际温度
EMATemperature() # 绘制EMA温度(默认beta=0.9)
EMATemperature(beta=0.5) # 绘制EMA温度(beta=0.5)2. 动量算法
2.1 公式定义
动量算法(Momentum)让优化过程逐渐趋于平缓:
- 若梯度方向持续一致 → 动量累积 → 加速下降
- 若梯度方向忽快忽慢 → 动量平均作用 → 减少震荡
梯度计算公式:
| 符号 | 含义 |
|---|---|
| \(s_t\) | 当前时刻 指数加权平均的梯度(动量) |
| \(s_{t-1}\) | 上一时刻的动量 |
| \(g_t\) | 当前梯度(loss 对参数的偏导) |
| \(\beta\) | 动量系数(一般取 0.9 或 0.99),值越大,表示越信任历史加权累积的权重,惯性越强 |
梯度更新公式:
| 符号 | 含义 |
|---|---|
| \(w_t\) | 当前权重参数 |
| \(w_{t-1}\) | 上一时刻权重参数 |
| \(\eta\) | 学习率(learning rate) |
| \(s_t\) | 累积动量(平滑梯度) |
普通梯度下降是:
但 Momentum 用的是 动量 \(s_t\) 而不是瞬时梯度 \(g_t\)。
2.2 计算示例
实验:比较普通梯度下降(GD)与动量(Momentum)在一个条件数较大的二次函数上的表现:
在函数 \(f(x,y) = 0.5*(a*x^2 + b*y^2)\) 上做优化(a >> b 造成峡谷/震荡)
使用相同学习率分别运行普通 GD 和 Momentum(beta=0.9)
绘制出等高线与两条参数轨迹,以及每次迭代的 loss 曲线
import numpy as np
import matplotlib.pyplot as plt
# 目标函数参数(条件数较大)
a = 100.0
b = 1.0
def loss(w):
x, y = w
return 0.5*(a*x**2 + b*y**2)
def grad(w):
x, y = w
return np.array([a*x, b*y])
# 优化器实现
def grad_descent(w0, lr, steps):
w = w0.copy()
traj = [w.copy()]
losses = [loss(w)]
for t in range(steps):
g = grad(w)
w = w - lr * g
traj.append(w.copy())
losses.append(loss(w))
return np.array(traj), np.array(losses)
def momentum(w0, lr, beta, steps):
w = w0.copy()
v = np.zeros_like(w)
traj = [w.copy()]
losses = [loss(w)]
for t in range(steps):
g = grad(w)
v = beta * v + (1 - beta) * g # 指数加权平均梯度
w = w - lr * v
traj.append(w.copy())
losses.append(loss(w))
return np.array(traj), np.array(losses)
# 初始点与超参数
w0 = np.array([-3.0, 3.0])
lr = 0.01 # 学习率(对比同一学习率下的表现)
beta = 0.9
steps = 200
traj_gd, losses_gd = grad_descent(w0, lr, steps)
traj_mom, losses_mom = momentum(w0, lr, beta, steps)
# 绘图1:等高线 + 两条轨迹
# 生成网格与等高线
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(a*X**2 + b*Y**2)
plt.figure(figsize=(7,6))
cs = plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label='GD trajectory')
plt.plot(traj_mom[:,0], traj_mom[:,1], marker='x', markersize=3, linewidth=1, label='Momentum trajectory (beta=0.9)')
plt.scatter([w0[0]], [w0[1]], marker='s') # 起点
plt.title('Trajectories on loss contours')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.axis('equal')
plt.grid(True)
plt.show()
# 绘图2:loss vs iteration
plt.figure(figsize=(7,5))
plt.semilogy(range(len(losses_gd)), losses_gd, linewidth=1, label='GD loss')
plt.semilogy(range(len(losses_mom)), losses_mom, linewidth=1, label='Momentum loss (beta=0.9)')
plt.title('Loss vs Iteration (log scale)')
plt.xlabel('Iteration')
plt.ylabel('Loss (log)')
plt.legend()
plt.grid(True)
plt.show()
# 输出一些数值比较:最终的坐标和损失
final_info = {
'GD_final_w': traj_gd[-1].tolist(),
'Momentum_final_w': traj_mom[-1].tolist(),
'GD_final_loss': float(losses_gd[-1]),
'Momentum_final_loss': float(losses_mom[-1])
}
final_info3. RMSProp 算法
3.1 公式定义
RMSProp(Root Mean Square Propagation)是 AdaGrad 的改进版本。
- AdaGrad:对每个参数维度累积梯度平方,使陡峭方向学习率变小、平缓方向学习率相对大
- 但 AdaGrad 累积所有历史梯度平方 → 会越来越大 → 学习率会越来越小 → 最终训练停滞
RMSProp 算法不对所有历史梯度平方平均,而是使用指数加权平均(EWMA)。
首先初始化:
- 学习率:\(\eta\)
- 初始参数:\(w_0\)
- 小常数:\(\epsilon = 10^{-10}\)(用于数值稳定性)
- 梯度平方的指数加权项:\(s_0 = 0\)
从 batch 中取 m 个样本,梯度计算公式:
指数加权平均累积梯度平方:
| 符号 | 含义 |
|---|---|
| \(s_t\) | 梯度平方的指数加权平均(类似“最近梯度平方”) |
| \(\beta\) | 一般取 0.9,用于平滑 |
| \(g_t ⊙ g_t\) | 梯度按元素平方:即 \(g_t^2\) |
计算自适应学习率(RMS 部分):
参数更新公式:
3.2 计算示例
比较 RMSProp 与 普通GD 的优化轨迹和收敛情况:
import numpy as np
import matplotlib.pyplot as plt
# 目标函数
def loss(w):
x, y = w
return 0.5*(100*x**2 + y**2)
def grad(w):
x, y = w
return np.array([100*x, y])
def gd(w0, lr, steps):
w = w0.copy()
traj = [w.copy()]
losses = [loss(w)]
for _ in range(steps):
g = grad(w)
w = w - lr * g
traj.append(w.copy())
losses.append(loss(w))
return np.array(traj), np.array(losses)
def rmsprop(w0, lr, beta, eps, steps):
w = w0.copy()
s = np.zeros_like(w)
traj = [w.copy()]
losses = [loss(w)]
for _ in range(steps):
g = grad(w)
s = beta*s + (1-beta)*(g*g)
w = w - lr * g / (np.sqrt(s + eps))
traj.append(w.copy())
losses.append(loss(w))
return np.array(traj), np.array(losses)
# 参数
w0 = np.array([-3.0, 3.0])
lr = 0.01
beta = 0.9
eps = 1e-8
steps = 200
traj_gd, loss_gd = gd(w0, lr, steps)
traj_rms, loss_rms = rmsprop(w0, lr, beta, eps, steps)
# 图1:轨迹
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(100*X**2 + Y**2)
plt.figure(figsize=(7,6))
plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label="GD")
plt.plot(traj_rms[:,0], traj_rms[:,1], marker='x', markersize=3, linewidth=1, label="RMSProp")
plt.scatter([w0[0]], [w0[1]], marker='s')
plt.title("Trajectories on Loss Contours")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.axis("equal")
plt.grid(True)
plt.show()
# 图2:Loss vs Iteration
plt.figure(figsize=(7,5))
plt.semilogy(loss_gd, linewidth=1, label="GD loss")
plt.semilogy(loss_rms, linewidth=1, label="RMSProp loss")
plt.title("Loss vs Iteration (log scale)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()
{
"GD_final_loss": float(loss_gd[-1]),
"RMSProp_final_loss": float(loss_rms[-1]),
"GD_final_w": traj_gd[-1].tolist(),
"RMSProp_final_w": traj_rms[-1].tolist()
}4. Adam 算法
4.1 公式定义
Adam 是目前深度学习中最常用的优化算法之一,它结合了:
- Momentum(动量法):使用一阶矩,即梯度的指数加权平均
- RMSProp:使用二阶矩,即梯度平方的指数加权平均
- 偏差校正(bias correction):使指数加权平均在前期更准确
Adam 同时维护两个指数加权平均,计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。
一阶矩计算:
二阶矩计算:
其中:
- \(m_t\):一阶矩(梯度平均)
- \(s_t\):二阶矩(平方梯度平均)
- \(g_t\):当前梯度
- \(\beta_{1}\) 通常取 0.9
- \(\beta_{2}\) 通常取 0.999
- 初始化 \(m_0 = 0, s_0 = 0\)
前期的 \(m_t\) 和 \(s_t\) 会偏向 0,因为指数加权平均初始值为 0,所以要进行偏差修正:
参数更新:
其中:
- \(\hat{m}_t\):偏差修正后的一阶矩,控制方向
- \(\hat{s}_t\):偏差修正后的二阶矩,控制自适应学习率
- \(\epsilon\):防止除零(一般为 1e-8)
- \(\alpha\):初始学习率(一般为 0.001)
4.2 计算示例
import numpy as np
import matplotlib.pyplot as plt
# 目标函数:强不等轴二次函数(常用于展示优化器差异)
def loss(w):
x, y = w
return 0.5 * (100*x**2 + y**2)
def grad(w):
x, y = w
return np.array([100*x, y])
# 普通梯度下降
def gd(w0, lr, steps):
w = w0.copy()
traj = [w.copy()]
losses = [loss(w)]
for _ in range(steps):
g = grad(w)
w = w - lr * g
traj.append(w.copy())
losses.append(loss(w))
return np.array(traj), np.array(losses)
# Adam
def adam(w0, lr, beta1, beta2, eps, steps):
w = w0.copy()
m = np.zeros_like(w)
s = np.zeros_like(w)
traj = [w.copy()]
losses = [loss(w)]
for t in range(1, steps+1):
g = grad(w)
m = beta1*m + (1-beta1)*g
s = beta2*s + (1-beta2)*(g*g)
# 偏差修正
m_hat = m / (1 - beta1**t)
s_hat = s / (1 - beta2**t)
w = w - lr * m_hat / (np.sqrt(s_hat) + eps)
traj.append(w.copy())
losses.append(loss(w))
return np.array(traj), np.array(losses)
# 参数设置
w0 = np.array([-3.0, 3.0])
steps = 200
lr_gd = 0.01
lr_adam = 0.01
beta1 = 0.9
beta2 = 0.999
eps = 1e-8
traj_gd, loss_gd = gd(w0, lr_gd, steps)
traj_adam, loss_adam = adam(w0, lr_adam, beta1, beta2, eps, steps)
xs = np.linspace(-3.5, 3.5, 400)
ys = np.linspace(-3.5, 3.5, 400)
X, Y = np.meshgrid(xs, ys)
Z = 0.5*(100*X**2 + Y**2)
plt.figure(figsize=(7,6))
plt.contour(X, Y, Z, levels=30)
plt.plot(traj_gd[:,0], traj_gd[:,1], marker='o', markersize=3, linewidth=1, label="GD")
plt.plot(traj_adam[:,0], traj_adam[:,1], marker='x', markersize=3, linewidth=1, label="Adam")
plt.scatter([w0[0]], [w0[1]], marker='s')
plt.title("Trajectories of GD vs Adam")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.grid(True)
plt.axis("equal")
plt.show()
plt.figure(figsize=(7,5))
plt.semilogy(loss_gd, linewidth=1, label="GD loss")
plt.semilogy(loss_adam, linewidth=1, label="Adam loss")
plt.title("Loss vs Iteration (log scale)")
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.legend()
plt.grid(True)
plt.show()
{
"GD_final_loss": float(loss_gd[-1]),
"Adam_final_loss": float(loss_adam[-1]),
"GD_final_w": traj_gd[-1].tolist(),
"Adam_final_w": traj_adam[-1].tolist()
}