Run this notebook online:Binder or Colab: Colab

11.9. Adadelta

Adadelta是AdaGrad的另一种变体( Section 11.7), 主要区别在于前者减少了学习率适应坐标的数量。 此外,广义上Adadelta被称为没有学习率,因为它使用变化量本身作为未来变化的校准。 Adadelta算法是在 [Zeiler, 2012]中提出的。

11.9.1. Adadelta算法

简而言之,Adadelta使用两个状态变量,\(\mathbf{s}_t\)用于存储梯度二阶导数的漏平均值,\(\Delta\mathbf{x}_t\)用于存储模型本身中参数变化二阶导数的泄露平均值。请注意,为了与其他出版物和实现的兼容性,我们使用作者的原始符号和命名(没有其他真正理由为什么应该使用不同的希腊变量来表示在动量中用于相同用途的参数,即AdaGrad、RMSProp和Adadelta)。

以下是Adadelta的技术细节。鉴于参数du jour是\(\rho\),我们获得了与 Section 11.8类似的以下泄漏更新:

(11.9.1)\[\begin{aligned} \mathbf{s}_t & = \rho \mathbf{s}_{t-1} + (1 - \rho) \mathbf{g}_t^2. \end{aligned}\]

Section 11.8的区别在于,我们使用重新缩放的梯度\(\mathbf{g}_t'\)执行更新,即

(11.9.2)\[\begin{split}\begin{aligned} \mathbf{x}_t & = \mathbf{x}_{t-1} - \mathbf{g}_t'. \\ \end{aligned}\end{split}\]

那么,调整后的梯度\(\mathbf{g}_t'\)是什么?我们可以按如下方式计算它:

(11.9.3)\[\begin{split}\begin{aligned} \mathbf{g}_t' & = \frac{\sqrt{\Delta\mathbf{x}_{t-1} + \epsilon}}{\sqrt{{\mathbf{s}_t + \epsilon}}} \odot \mathbf{g}_t, \\ \end{aligned}\end{split}\]

其中\(\Delta \mathbf{x}_{t-1}\)是重新缩放梯度的平方\(\mathbf{g}_t'\)的泄漏平均值。我们将\(\Delta \mathbf{x}_{0}\)初始化为\(0\),然后在每个步骤中使用\(\mathbf{g}_t'\)更新它,即

(11.9.4)\[\begin{aligned} \Delta \mathbf{x}_t & = \rho \Delta\mathbf{x}_{t-1} + (1 - \rho) {\mathbf{g}_t'}^2, \end{aligned}\]

\(\epsilon\)(例如\(10^{-5}\)这样的小值)是为了保持数字稳定性而加入的。

11.9.2. 代码实现

Adadelta需要为每个变量维护两个状态变量,即\(\mathbf{s}_t\)\(\Delta\mathbf{x}_t\)。这将产生以下实施。

%load ../utils/djl-imports
%load ../utils/plot-utils
%load ../utils/Functions.java
%load ../utils/GradDescUtils.java
%load ../utils/Accumulator.java
%load ../utils/StopWatch.java
%load ../utils/Training.java
%load ../utils/TrainingChapter11.java
NDList initAdadeltaStates(int featureDimension) {
    NDManager manager = NDManager.newBaseManager();
    NDArray sW = manager.zeros(new Shape(featureDimension, 1));
    NDArray sB = manager.zeros(new Shape(1));
    NDArray deltaW = manager.zeros(new Shape(featureDimension, 1));
    NDArray deltaB = manager.zeros(new Shape(1));
    return new NDList(sW, deltaW, sB, deltaB);
}

public class Optimization {
    public static void adadelta(NDList params, NDList states, Map<String, Float> hyperparams) {
        float rho = hyperparams.get("rho");
        float eps = (float) 1e-5;
        for (int i = 0; i < params.size(); i++) {
            NDArray param = params.get(i);
            NDArray state = states.get(2 * i);
            NDArray delta = states.get(2 * i + 1);
            // Update parameter, state, and delta
            // In-place updates with the '__'i methods (ex. muli)
            // state = rho * state + (1 - rho) * param.gradient^2
            state.muli(rho).addi(param.getGradient().square().mul(1 - rho));
            // rescaledGradient = ((delta + eps)^(1/2) / (state + eps)^(1/2)) * param.gradient
            NDArray rescaledGradient = delta.add(eps).sqrt()
                .div(state.add(eps).sqrt()).mul(param.getGradient());
            // param -= rescaledGradient
            param.subi(rescaledGradient);
            // delta = rho * delta + (1 - rho) * g^2
            delta.muli(rho).addi(rescaledGradient.square().mul(1 - rho));
        }
    }
}

对于每次参数更新,选择\(\rho = 0.9\)相当于10个半衰期。由此我们得到:

AirfoilRandomAccess airfoil = TrainingChapter11.getDataCh11(10, 1500);

public TrainingChapter11.LossTime trainAdadelta(float rho, int numEpochs) throws IOException, TranslateException {
    int featureDimension = airfoil.getColumnNames().size();
    Map<String, Float> hyperparams = new HashMap<>();
    hyperparams.put("rho", rho);
    return TrainingChapter11.trainCh11(Optimization::adadelta,
                                       initAdadeltaStates(featureDimension),
                                       hyperparams, airfoil,
                                       featureDimension, numEpochs);
}

TrainingChapter11.LossTime lossTime = trainAdadelta(0.9f, 2);
loss: 0.247, 0.096 sec/epoch

为了简洁实现,我们只需使用Trainer类中的adadelta算法。

Optimizer adadelta = Optimizer.adadelta().optRho(0.9f).build();

TrainingChapter11.trainConciseCh11(adadelta, airfoil, 2);
INFO Training on: 1 GPUs.
INFO Load MXNet Engine Version 1.8.0 in 0.084 ms.
Training:    100% |████████████████████████████████████████| Accuracy: 0.67, L2Loss: 0.49
loss: 0.479, 0.161 sec/epoch

11.9.3. 小结

  • Adadelta没有学习率参数。相反,它使用参数本身的变化率来调整学习率。

  • Adadelta需要两个状态变量来存储梯度的二阶导数和参数的变化。

  • Adadelta使用泄漏的平均值来保持对适当统计数据的运行估计。

11.9.4. 练习

  1. 调整\(\rho\)的值,会发生什么?

  2. 展示如何在不使用\(\mathbf{g}_t'\)的情况下实现算法。为什么这是个好主意?

  3. Adadelta真的是学习率为0吗?你能找到Adadelta无法解决的优化问题吗?

  4. 将Adadelta的收敛行为与AdaGrad和RMSProp进行比较。