☰
D2L 优化算法实战:Adagrad 自适应梯度算法——稀疏特征、预调节与逐坐标学习率
2026/10/1 9:11:46 网站建设 项目流程
  • 文档
  • 教程
  • 人工智能
  • 深度学习
  • NLP
  • 计算机视觉
  • 强化学习

【免费下载链接】d2l-en

Interactive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.

项目地址:https://gitcode.com/gh_mirrors/d2/d2l-en
点击查看免费下载

Adagrad(Adaptive Gradient,自适应梯度算法)是《Dive into Deep Learning》(D2L)优化章节中承上启下的关键算法,它在动量法(momentum)之后、RMSProp/Adadelta/Adam 之前登场,核心贡献是为每个参数坐标维护独立的自适应学习率:梯度大的坐标被大幅缩小更新步长,梯度小的坐标则获得更温和的待遇。本文以 chapter_optimization/adagrad.md 为主体,结合仓库中 d2l 工具包的实现源码,从稀疏特征场景、预调节(preconditioning)视角、算法推导、二维实验、从零实现到框架原生优化器调用,完整还原 Adagrad 的原理与实战,并对照 chapter_optimization/minibatch-sgd.md 的实验环境验证其训练效果。

一、问题背景:稀疏特征与全局学习率的矛盾

考虑一个典型的语言模型训练场景:为了获得好的精度,通常需要在训练过程中不断降低学习率,典型做法是让学习率按 $\mathcal{O}(t^{-\frac{1}{2}})$ 或更慢的速度衰减。现在引入"稀疏特征"(sparse features)——即只偶尔出现的特征。这在自然语言处理中非常常见,例如单词preconditioning(预调节)出现的频率远低于learning(学习);在计算广告(computational advertising)和个性化协同过滤(personalized collaborative filtering)等领域也同样普遍,因为大量物品只对少数人感兴趣。

稀疏特征对应的参数,只有在特征真正出现时才会收到有意义的梯度更新。如果使用全局递减的学习率,就会出现两难局面:

  • 频繁特征:出现次数多,参数很快收敛到最优值附近;
  • 稀疏特征:出现次数少,在最优值确定之前,参数还没能被足够频繁地观测和更新。

换句话说,全局递减的学习率对频繁特征来说下降得太慢,对稀疏特征来说又下降得太快。

一个朴素的对策是"按特征计数来当钟表":统计每个特征被观测到的次数,用它来调节学习率。即不再使用形如

$$\eta = \frac{\eta_0}{\sqrt{t + c}}$$

的全局学习率,而是对每个特征 $i$ 使用

$$\eta_i = \frac{\eta_0}{\sqrt{s(i, t) + c}},$$

其中 $s(i, t)$ 统计到时刻 $t$ 为止特征 $i$ 的非零出现次数。这个方案实现起来几乎没有额外开销,但它在另一种场景下会失效:当数据并非"真稀疏",而只是梯度经常很小、偶尔很大时,很难划清"某次观测算不算一个特征出现"的界线。

Adagrad(由 Duchi、Hazan 和 Singer 于 2011 年提出,参见仓库 d2l.bib 中的Duchi.Hazan.Singer.2011条目)用"历史梯度平方的累积和"替代了这个粗糙的计数器 $s(i, t)$,即:

$$s(i, t+1) = s(i, t) + \left(\partial_i f(\mathbf{x})\right)^2.$$

这样做有两个好处:

  1. 无需判定阈值:不再需要决定"梯度多大才算大";
  2. 随梯度幅值自动缩放:经常产生大梯度的坐标被显著缩小学习率,梯度小的坐标得到更温和的处理。

在实践中,这一机制对计算广告及类似问题非常有效。不过它也隐藏了 Adagrad 的额外优势,这些优势最好放在"预调节"(preconditioning)的框架下理解。

二、预调节:从条件数到梯度代理

凸优化问题适合分析算法的性质——大多数非凸问题难以给出有意义的理论保证,但直觉与洞见往往可以迁移。考虑最小化如下二次型目标函数:

$$f(\mathbf{x}) = \frac{1}{2} \mathbf{x}^\top \mathbf{Q} \mathbf{x} + \mathbf{c}^\top \mathbf{x} + b.$$

如 chapter_optimization/momentum.md 所述,可以利用特征分解 $\mathbf{Q} = \mathbf{U}^\top \boldsymbol{\Lambda} \mathbf{U}$ 将其重写为每个坐标可独立求解的简化问题:

$$f(\mathbf{x}) = \bar{f}(\bar{\mathbf{x}}) = \frac{1}{2} \bar{\mathbf{x}}^\top \boldsymbol{\Lambda} \bar{\mathbf{x}} + \bar{\mathbf{c}}^\top \bar{\mathbf{x}} + b,$$

其中 $\bar{\mathbf{x}} = \mathbf{U} \mathbf{x}$,$\bar{\mathbf{c}} = \mathbf{U} \mathbf{c}$。该问题的最小点为 $\bar{\mathbf{x}} = -\boldsymbol{\Lambda}^{-1} \bar{\mathbf{c}}$,最小值为 $-\frac{1}{2} \bar{\mathbf{c}}^\top \boldsymbol{\Lambda}^{-1} \bar{\mathbf{c}} + b$。由于 $\boldsymbol{\Lambda}$ 是包含 $\mathbf{Q}$ 特征值的对角矩阵,计算变得非常容易。

扰动敏感性与条件数:如果对 $\mathbf{c}$ 施加轻微扰动,我们希望 $f$ 的最小点也只发生轻微变化——但事实并非如此。$\mathbf{c}$ 的轻微变化确实只引起 $\bar{\mathbf{c}}$ 的轻微变化,但对于 $f$(以及 $\bar{f}$)的最小点却并非如此:当特征值 $\boldsymbol{\Lambda}_i$ 很大时,$\bar{x}_i$ 和 $\bar{f}$ 的最小值只发生微小变化;而当 $\boldsymbol{\Lambda}_i$ 很小时,$\bar{x}_i$ 的变化可能非常剧烈。最大特征值与最小特征值之比被称为优化问题的条件数(condition number):

$$\kappa = \frac{\boldsymbol{\Lambda}_1}{\boldsymbol{\Lambda}_d}.$$

条件数 $\kappa$ 越大,越难以精确求解优化问题——必须小心处理大动态范围内的各种取值。

理论上的理想方案:能否通过扭曲空间让所有特征值都变成 1?理论上很简单,只需用 $\mathbf{Q}$ 的特征值和特征向量把问题从 $\mathbf{x}$ 重缩放到 $\mathbf{z} \stackrel{\textrm{def}}{=} \boldsymbol{\Lambda}^{\frac{1}{2}} \mathbf{U} \mathbf{x}$,在新坐标系中 $\mathbf{x}^\top \mathbf{Q} \mathbf{x}$ 简化为 $|\mathbf{z}|^2$。可惜这并不实用:计算特征值和特征向量通常比求解原问题本身昂贵得多。

实用的折中——对角线缩放:精确计算特征值昂贵,但猜测甚至近似计算它们,也比什么都不做强得多。特别地,可以用 $\mathbf{Q}$ 的对角元来重缩放:

$$\tilde{\mathbf{Q}} = \textrm{diag}^{-\frac{1}{2}}(\mathbf{Q}) \mathbf{Q} \textrm{diag}^{-\frac{1}{2}}(\mathbf{Q}).$$

此时 $\tilde{\mathbf{Q}}{ij} = \mathbf{Q}{ij} / \sqrt{\mathbf{Q}{ii} \mathbf{Q}{jj}}$,特别地 $\tilde{\mathbf{Q}}_{ii} = 1$ 对所有 $i$ 成立。在大多数情况下这能显著改善条件数;对前文提到的轴对齐问题,甚至可以完全消除困难。

深度学习中的真正障碍:我们通常连目标函数的二阶导数都无法获取——对于 $\mathbf{x} \in \mathbb{R}^d$,即使在一个小批量(minibatch)上计算二阶导数也可能需要 $\mathcal{O}(d^2)$ 的空间与计算量,在实践中不可行。Adagrad 的巧妙之处在于:用梯度本身的幅值作为 Hessian 对角线上那个"难以捉摸的量"的代理——既便宜又有效。

为什么这样可行?观察 $\bar{f}(\bar{\mathbf{x}})$ 的梯度:

$$\partial_{\bar{\mathbf{x}}} \bar{f}(\bar{\mathbf{x}}) = \boldsymbol{\Lambda} \bar{\mathbf{x}} + \bar{\mathbf{c}} = \boldsymbol{\Lambda} \left(\bar{\mathbf{x}} - \bar{\mathbf{x}}_0\right),$$

其中 $\bar{\mathbf{x}}_0$ 是 $\bar{f}$ 的最小点。可见梯度的幅值同时依赖于 $\boldsymbol{\Lambda}$ 和与最优点的距离。如果 $\bar{\mathbf{x}} - \bar{\mathbf{x}}_0$ 不变,梯度幅值就足够了。由于 Adagrad 是随机梯度下降类算法,即使在最优点附近也会看到非零方差的梯度,因此可以放心地把梯度的方差作为 Hessian 尺度的廉价代理(严格的分析篇幅很长,详见Duchi.Hazan.Singer.2011)。

三、算法形式化:逐坐标自适应学习率

将上述讨论形式化。用变量 $\mathbf{s}_t$ 累积历史梯度平方(即梯度方差的近似),更新规则如下:

$$\begin{aligned} \mathbf{g}t & = \partial{\mathbf{w}} l(y_t, f(\mathbf{x}_t, \mathbf{w})), \ \mathbf{s}t & = \mathbf{s}{t-1} + \mathbf{g}_t^2, \ \mathbf{w}t & = \mathbf{w}{t-1} - \frac{\eta}{\sqrt{\mathbf{s}_t + \epsilon}} \cdot \mathbf{g}_t. \end{aligned}$$

这里所有运算都是按坐标逐元素进行的:$\mathbf{v}^2$ 的第 $i$ 个分量为 $v_i^2$;$\frac{1}{\sqrt{v}}$ 的第 $i$ 个分量为 $\frac{1}{\sqrt{v_i}}$;$\mathbf{u} \cdot \mathbf{v}$ 的第 $i$ 个分量为 $u_i v_i$。其中 $\eta$ 是学习率,$\epsilon$ 是保证不除以零的加法常数,初始状态 $\mathbf{s}_0 = \mathbf{0}$。

与动量法类似,Adagrad 需要维护一个辅助变量(这里是 $\mathbf{s}_t$),用于实现每个坐标独立的学习率。这并不会显著增加 Adagrad 相对 SGD 的代价——因为主要开销通常在于计算 $l(y_t, f(\mathbf{x}_t, \mathbf{w}))$ 及其导数。

学习率衰减行为:由于 $\mathbf{s}_t$ 累积梯度平方,它大体上以线性速度增长(实践中因梯度初期会减小,略慢于线性),从而产生逐坐标调整的 $\mathcal{O}(t^{-\frac{1}{2}})$ 学习率。对凸问题而言这完全够用;但在深度学习中,我们往往希望学习率衰减得更慢一些。正是这个动机催生了后续章节讨论的一系列 Adagrad 变体:chapter_optimization/rmsprop.md、chapter_optimization/adadelta.md 与 chapter_optimization/adam.md。

四、二维凸二次问题实验:验证自适应行为

在动手实现完整版之前,先在一个凸二次问题上观察 Adagrad 的行为。沿用动量法实验中的目标函数:

$$f(\mathbf{x}) = 0.1 x_1^2 + 2 x_2^2.$$

下面是三个框架(MXNet / PyTorch / TensorFlow)的实验代码框架,后续的二维更新函数由#@tab all统一给出:

#@tab mxnet %matplotlib inline from d2l import mxnet as d2l import math from mxnet import np, npx npx.set_np()
#@tab pytorch %matplotlib inline from d2l import torch as d2l import math import torch
#@tab tensorflow %matplotlib inline from d2l import tensorflow as d2l import math import tensorflow as tf
#@tab all def adagrad_2d(x1, x2, s1, s2): eps = 1e-6 g1, g2 = 0.2 * x1, 4 * x2 s1 += g1 ** 2 s2 += g2 ** 2 x1 -= eta / math.sqrt(s1 + eps) * g1 x2 -= eta / math.sqrt(s2 + eps) * g2 return x1, x2, s1, s2 def f_2d(x1, x2): return 0.1 * x1 ** 2 + 2 * x2 ** 2 eta = 0.4 d2l.show_trace_2d(f_2d, d2l.train_2d(adagrad_2d))

注意:$x_1$ 方向的梯度系数为 $0.2$(对应目标函数中的 $0.1 \times 2$),$x_2$ 方向为 $4$,梯度越大的坐标其平方累积越快,学习率收缩也越快。与adagrad_2d紧密配合的train_2d与show_trace_2d定义在 d2l/torch.py(MXNet 版见 d2l/mxnet.py,TensorFlow 版见 d2l/tensorflow.py):train_2d从 $(-5, -2)$ 出发迭代 20 步,并将每一步的位置记录进results,其中s1、s2是留给 Momentum、Adagrad、RMSProp 等算法共用的内部状态变量;show_trace_2d则把迭代轨迹绘制在目标函数的等高线上。

在 $\eta = 0.4$ 时,迭代轨迹比普通 SGD 平滑得多。但由于 $\mathbf{s}_t$ 的累积效应,学习率持续衰减,迭代后期自变量几乎不再移动。

把学习率提高到 $\eta = 2$ 后,行为明显更好:

#@tab all eta = 2 d2l.show_trace_2d(f_2d, d2l.train_2d(adagrad_2d))

这一对比已经暗示:即使在无噪声情况下,Adagrad 的学习率衰减也可能相当激进,需要确保参数能充分收敛。

五、从零实现 Adagrad

与动量法一样,Adagrad 需要维护一个与参数同形状的状态变量 $\mathbf{s}$(累积梯度平方)。三个框架下的初始化与更新实现如下:

#@tab mxnet def init_adagrad_states(feature_dim): s_w = d2l.zeros((feature_dim, 1)) s_b = d2l.zeros(1) return (s_w, s_b) def adagrad(params, states, hyperparams): eps = 1e-6 for p, s in zip(params, states): s[:] += np.square(p.grad) p[:] -= hyperparams['lr'] * p.grad / np.sqrt(s + eps)
#@tab pytorch def init_adagrad_states(feature_dim): s_w = d2l.zeros((feature_dim, 1)) s_b = d2l.zeros(1) return (s_w, s_b) def adagrad(params, states, hyperparams): eps = 1e-6 for p, s in zip(params, states): with torch.no_grad(): s[:] += torch.square(p.grad) p[:] -= hyperparams['lr'] * p.grad / torch.sqrt(s + eps) p.grad.data.zero_()
#@tab tensorflow def init_adagrad_states(feature_dim): s_w = tf.Variable(d2l.zeros((feature_dim, 1))) s_b = tf.Variable(d2l.zeros(1)) return (s_w, s_b) def adagrad(params, grads, states, hyperparams): eps = 1e-6 for p, s, g in zip(params, states, grads): s[:].assign(s + tf.math.square(g)) p[:].assign(p - hyperparams['lr'] * g / tf.math.sqrt(s + eps))

实现要点(三个框架完全一致):

  • eps = 1e-6是防止除零的加法常数(对应公式中的 $\epsilon$);
  • 状态s以原地更新方式累积p.grad(或框架传入的grads)的平方;
  • 参数更新步长为hyperparams['lr'] * p.grad / sqrt(s + eps),即每个坐标用自己的梯度平方和缩放学习率;
  • PyTorch 版在更新后用p.grad.data.zero_()清零梯度,而 MXNet / TensorFlow 由自动微分机制按需管理梯度。

与 chapter_optimization/minibatch-sgd.md 中的实验相比,这里使用了更大的学习率来训练模型:

#@tab all data_iter, feature_dim = d2l.get_data_ch11(batch_size=10) d2l.train_ch11(adagrad, init_adagrad_states(feature_dim), {'lr': 0.1}, data_iter, feature_dim);

这里用到的数据与训练框架均来自仓库工具包:

  • get_data_ch11(见 d2l/torch.py、d2l/mxnet.py、d2l/tensorflow.py)下载airfoil(NASA 翼型自噪声)数据集,取前n=1500个样本、batch_size=10,并按均值/标准差标准化后封装为数据迭代器,返回(data_iter, feature_dim),其中特征维度为data.shape[1]-1(即 5);
  • train_ch11(见 d2l/torch.py 等)初始化 $w \sim \mathcal{N}(0, 0.01)$、$b = 0$,用线性回归 + 平方损失训练 2 个 epoch,训练循环中依次执行loss.backward()后调用自定义的trainer_fn([w, b], states, hyperparams),每累计 200 个样本记录一次损失,最终打印最终损失与每 epoch 耗时。

六、简洁实现:调用框架原生优化器

除了从零实现,D2L 也给出了直接调用各框架内置 Adagrad 优化器的"简洁实现"方式:

#@tab mxnet d2l.train_concise_ch11('adagrad', {'learning_rate': 0.1}, data_iter)
#@tab pytorch trainer = torch.optim.Adagrad d2l.train_concise_ch11(trainer, {'lr': 0.1}, data_iter)
#@tab tensorflow trainer = tf.keras.optimizers.Adagrad d2l.train_concise_ch11(trainer, {'learning_rate' : 0.1}, data_iter)
  • MXNet/Gluon:通过gluon.Trainer(net.collect_params(), 'adagrad', hyperparams)创建优化器(见 d2l/mxnet.py 中train_concise_ch11的实现),每步调用trainer.step(X.shape[0]);
  • PyTorch:train_concise_ch11接受优化器类,内部执行trainer_fn(net.parameters(), **hyperparams)构造torch.optim.Adagrad,训练循环中调用optimizer.zero_grad()与optimizer.step()(见 d2l/torch.py);
  • TensorFlow/Keras:trainer_fn(**hyperparams)直接构造tf.keras.optimizers.Adagrad,训练循环中通过GradientTape计算梯度并调用optimizer.apply_gradients(zip(grads, params))(见 d2l/tensorflow.py)。

三种框架统一以learning_rate = 0.1训练同样的 airfoil 回归任务,即可复现与从零实现一致的结果。需要留意的是,各框架原生优化器的默认超参数(如 $\epsilon$、初始累积值等)与其标准实现保持一致,具体默认值以对应框架文档为准;在本实验中显式指定的学习率0.1明显大于 chapter_optimization/minibatch-sgd.md 中 SGD 的取值,这也印证了 Adagrad 自适应缩放会天然压小有效步长,需要配合更大的基础学习率使用。

七、总结:Adagrad 的适用场景与局限

综合全文,Adagrad 的关键结论可以归纳为:

  1. 逐坐标动态降低学习率:每个参数坐标拥有独立的自适应学习率,而非全局共享一个;
  2. 用梯度幅值调节进度:梯度大的坐标被配以更小的学习率,实现自动补偿;
  3. 梯度是二阶信息的实用代理:深度学习问题中精确计算二阶导数通常因内存与算力约束而不可行,梯度幅值/方差是划算的替代;
  4. 缓解病态结构:当优化问题结构很不均匀(条件数大)时,Adagrad 有助于减轻这种畸变带来的困难;
  5. 稀疏特征场景尤为有效:对不频繁出现的特征,学习率可以衰减得更慢,保证其参数得到充分更新;
  6. 深度学习中可能过于激进:Adagrad 有时会把学习率降得过快,缓解这一问题的策略将在 chapter_optimization/adam.md(及其前的 chapter_optimization/rmsprop.md、chapter_optimization/adadelta.md)中讨论——它们正是针对"累积平方和单调增长"这一缺点的改进。

八、延伸思考与练习

  1. 证明对正交矩阵 $\mathbf{U}$ 和向量 $\mathbf{c}$:$|\mathbf{c} - \mathbf{\delta}|_2 = |\mathbf{U} \mathbf{c} - \mathbf{U} \mathbf{\delta}|_2$。为什么这意味着扰动的大小在正交变量变换下不变?
  2. 分别对 $f(\mathbf{x}) = 0.1 x_1^2 + 2 x_2^2$ 以及旋转 45 度后的目标函数 $f(\mathbf{x}) = 0.1 (x_1 + x_2)^2 + 2 (x_1 - x_2)^2$ 运行 Adagrad,观察行为是否不同,并思考原因。
  3. 证明 Gerschgorin 圆定理:矩阵 $\mathbf{M}$ 的特征值 $\lambda_i$ 至少对某个 $j$ 满足 $|\lambda_i - \mathbf{M}{jj}| \leq \sum{k \neq j} |\mathbf{M}_{jk}|$。
  4. 结合 Gerschgorin 定理,分析对角预调节矩阵 $\textrm{diag}^{-\frac{1}{2}}(\mathbf{M}) \mathbf{M} \textrm{diag}^{-\frac{1}{2}}(\mathbf{M})$ 的特征值分布会得到怎样的结论。
  5. 将 Adagrad 应用于一个真实的深度网络,例如 chapter_convolutional-neural-networks/lenet.md 中的 LeNet 在 Fashion-MNIST 上的训练,观察其收敛曲线。
  6. 如果要让 Adagrad 的学习率衰减不那么激进,应当如何修改算法?可以结合后续的 RMSProp、Adadelta 与 Adam 章节对照验证你的修改思路。

通过以上练习,可以更深入地理解 Adagrad 在凸优化中的理论性质,以及它在深度学习场景下"自适应但偏激进"的行为边界,为后续学习更先进的自适应优化算法打下基础。

  • 文档
  • 教程
  • 人工智能
  • 深度学习
  • NLP
  • 计算机视觉
  • 强化学习

【免费下载链接】d2l-en

Interactive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.

项目地址:https://gitcode.com/gh_mirrors/d2/d2l-en
点击查看免费下载

相关推荐

上一篇:如何快速上手ERPNext:免费开源ERP系统新手完整指南
下一篇:免费快速转换扫描PDF为可搜索文档的终极解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询