Adama梯度下降图:从数学原理到Python实战实现

1次阅读
没有评论

共计 4632 个字符,预计需要花费 12 分钟才能阅读完成。

image.webp

背景痛点

理解梯度下降算法的收敛过程对于机器学习初学者至关重要。很多教材和教程中提供的静态图表虽然能展示最终结果,但无法直观地呈现参数在每一步迭代中的变化轨迹。这种动态过程对于理解学习率、收敛速度以及算法稳定性等概念非常有帮助。

Adama 梯度下降图:从数学原理到 Python 实战实现

Adama 梯度下降算法作为一种自适应学习率的优化方法,其动态调整学习率的特性更需要通过可视化来展示。本文将带你从零开始实现 Adama 梯度下降的动态可视化,让你不仅能理解算法原理,还能获得可直接复用的代码工具。

数学原理

Adama 梯度下降是对传统随机梯度下降 (SGD) 的改进,它通过维护梯度的移动平均值来自适应调整每个参数的学习率。其权重更新规则如下:

$$
\begin{aligned}
m_t &= \beta_1 m_{t-1} + (1-\beta_1) g_t \
v_t &= \beta_2 v_{t-1} + (1-\beta_2) g_t^2 \
\hat{m}t &= \frac{m_t}{1-\beta_1^t} \
\hat{v}_t &= \frac{v_t}{1-\beta_2^t} \
\theta_t &= \theta

\end{aligned}
$$} – \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon

与普通 SGD 的简单更新规则相比,Adama 通过引入一阶矩估计 ($m_t$) 和二阶矩估计 ($v_t$) 来适应不同参数的不同学习率需求,从而在复杂损失函数上表现更好。

Python 实现代码

下面是完整的 Python 实现,使用 Matplotlib 的 FuncAnimation 实现动态可视化:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from typing import Tuple, List, Callable

class AdamOptimizerVisualizer:
    """Adama 梯度下降可视化工具类"""

    def __init__(self, 
                 loss_fn: Callable[[np.ndarray], float],
                 grad_fn: Callable[[np.ndarray], np.ndarray],
                 x_range: Tuple[float, float] = (-5, 5),
                 y_range: Tuple[float, float] = (-5, 5),
                 learning_rate: float = 0.1,
                 beta1: float = 0.9,
                 beta2: float = 0.999,
                 epsilon: float = 1e-8):
        """
        初始化优化器可视化工具

        参数:
            loss_fn: 损失函数
            grad_fn: 梯度函数
            x_range: x 轴范围
            y_range: y 轴范围
            learning_rate: 学习率
            beta1: 一阶矩估计的衰减率
            beta2: 二阶矩估计的衰减率
            epsilon: 数值稳定性常数
        """
        self.loss_fn = loss_fn
        self.grad_fn = grad_fn
        self.x_range = x_range
        self.y_range = y_range
        self.eta = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.epsilon = epsilon

        # 初始化轨迹记录
        self.theta_history: List[np.ndarray] = []
        self.loss_history: List[float] = []

        # 初始化 Adama 参数
        self.m = np.zeros(2)
        self.v = np.zeros(2)
        self.t = 0

        # 创建图形
        self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2, figsize=(12, 5))

    def compute_contour(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """计算损失函数的等高线"""
        x = np.linspace(*self.x_range, 100)
        y = np.linspace(*self.y_range, 100)
        X, Y = np.meshgrid(x, y)
        Z = np.zeros_like(X)
        for i in range(X.shape[0]):
            for j in range(X.shape[1]):
                Z[i,j] = self.loss_fn(np.array([X[i,j], Y[i,j]]))
        return X, Y, Z

    def update(self, theta: np.ndarray) -> np.ndarray:
        """执行一步 Adama 更新"""
        self.t += 1
        grad = self.grad_fn(theta)

        # Adama 更新规则
        self.m = self.beta1 * self.m + (1 - self.beta1) * grad
        self.v = self.beta2 * self.v + (1 - self.beta2) * grad**2

        # 偏差修正
        m_hat = self.m / (1 - self.beta1**self.t)
        v_hat = self.v / (1 - self.beta2**self.t)

        # 参数更新
        theta = theta - self.eta * m_hat / (np.sqrt(v_hat) + self.epsilon)
        return theta

    def init_animation(self):
        """初始化动画"""
        X, Y, Z = self.compute_contour()

        # 绘制等高线
        self.ax1.clear()
        self.ax1.contour(X, Y, Z, levels=20, cmap='viridis')
        self.ax1.set_xlabel('θ₀')
        self.ax1.set_ylabel('θ₁')
        self.ax1.set_title('参数空间轨迹')

        # 初始化轨迹线
        self.line, = self.ax1.plot([], [], 'r-', lw=2)
        self.point, = self.ax1.plot([], [], 'ro')

        # 初始化损失曲线
        self.ax2.clear()
        self.ax2.set_xlabel('迭代次数')
        self.ax2.set_ylabel('损失值')
        self.ax2.set_title('损失函数下降曲线')
        self.loss_line, = self.ax2.plot([], [], 'b-')

        return self.line, self.point, self.loss_line

    def animate(self, i):
        """动画帧更新函数"""
        if i == 0:
            # 初始参数
            theta = np.array([self.x_range[0] + 0.1, self.y_range[0] + 0.1])
            self.theta_history = [theta]
            self.loss_history = [self.loss_fn(theta)]
        else:
            # 执行一步更新
            theta = self.update(self.theta_history[-1])
            self.theta_history.append(theta)
            self.loss_history.append(self.loss_fn(theta))

        # 更新参数空间轨迹
        theta_hist = np.array(self.theta_history)
        self.line.set_data(theta_hist[:,0], theta_hist[:,1])
        self.point.set_data(theta_hist[-1,0], theta_hist[-1,1])

        # 更新损失曲线
        self.loss_line.set_data(range(len(self.loss_history)), self.loss_history)
        self.ax2.relim()
        self.ax2.autoscale_view()

        return self.line, self.point, self.loss_line

    def run_animation(self, iterations: int = 100, interval: int = 200):
        """运行动画"""
        self.anim = FuncAnimation(
            self.fig, 
            self.animate, 
            frames=iterations,
            init_func=self.init_animation,
            interval=interval,
            blit=True,
            repeat=False
        )
        plt.tight_layout()
        plt.show()

# 示例使用
if __name__ == "__main__":
    # 定义简单的二次损失函数
    def loss_fn(theta: np.ndarray) -> float:
        return 0.1 * theta[0]**2 + theta[1]**2

    def grad_fn(theta: np.ndarray) -> np.ndarray:
        return np.array([0.2 * theta[0], 2 * theta[1]])

    # 创建并运行动画
    visualizer = AdamOptimizerVisualizer(
        loss_fn=loss_fn,
        grad_fn=grad_fn,
        learning_rate=0.5,
        x_range=(-5, 5),
        y_range=(-5, 5)
    )
    visualizer.run_animation(iterations=100)

参数实验

通过修改上述代码中的 learning_rate 参数,我们可以观察不同学习率下 Adama 算法的收敛表现:

  1. 学习率 =0.1:收敛稳定但速度较慢,需要更多迭代次数
  2. 学习率 =0.5:收敛速度适中,轨迹平滑
  3. 学习率 =1.0:初期收敛快,但后期可能在最小值附近震荡
  4. 学习率 =2.0:可能发散或出现剧烈震荡

Adama 算法的优势在于能够自适应调整每个参数的学习率,因此相比 SGD 对初始学习率的选择不那么敏感。

避坑指南

在实际实现过程中,可能会遇到以下问题:

  1. 动画卡顿
  2. 减少网格点数量(如从 100×100 降到 50×50)
  3. 增加 interval 参数值,降低帧率
  4. 使用 blit=True 选项只重绘变化的部分

  5. 三维曲面绘制性能

  6. 避免在动画循环中重新计算等高线
  7. 预计算所有数据后再创建动画
  8. 考虑使用 plot_surface 替代contour,根据硬件选择更高效的方法

  9. 梯度爆炸处理

  10. 添加梯度裁剪:grad = np.clip(grad, -1, 1)
  11. 检查损失函数定义是否合理
  12. 调整 epsilon 参数值增加数值稳定性

延伸思考

为了更深入理解优化算法,可以尝试以下扩展:

  1. 实现带动量的 SGD(Momentum SGD)可视化,比较其与 Adama 的轨迹差异
  2. 尝试不同的损失函数(如 Rosenbrock 函数),观察算法在复杂曲面的表现
  3. 添加学习率衰减策略,观察其对收敛的影响
  4. 实现多种优化算法的比较面板,直观展示不同算法的特性

通过这些实验,你将更直观地理解不同优化算法的优缺点,为实际项目中的算法选择打下坚实基础。

结语

本文详细介绍了 Adama 梯度下降算法的原理和 Python 实现方法,通过动态可视化帮助理解这一重要优化算法的工作机制。希望这份教程能成为你机器学习学习路上的实用工具,也鼓励你基于这些代码进行更多实验和探索。在实际项目中,理解算法背后的工作原理往往比简单地调用 API 更为重要。

正文完
 0
评论(没有评论)