t27.aiРусский

Adam

You will learn

How Adam combines momentum with a running scale, and why its bias correction makes step 1 a fixed size.

Adam combines the two ideas: m, a running mean of the gradient, as in momentum, and v, a running mean of its square, as in RMSProp; the step is lr m / sqrt(v). Both start at 0 and are too small early on, so Adam divides them by 1 - beta^t, and on step 1 that makes the move exactly lr times the sign of the gradient: gradients 2, -40 and 0.5 all move their weights by 0.001. On the spec's bowl (x - 3)^2 at lr 0.1, Adam rises from 0, overshoots to about 3.18 and settles at 3.00005 by step 200. The spec adam.t27 computes every step.

Try it

Give the three gradients very different sizes and read the weights after step 1; then find where the path tops out.

Open the interactive lesson →

Adam: the optimizer most networks are trained with
Adam: the optimizer most networks are trained with ↗

Watch Adam carry a weight from 0 to the minimum at 3, then set three gradients of any size: the bias correction makes the first step lr times their sign, whatever their size.

specs/ml/optimizer/adam.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Adam adapts learning rate per parameter | φ² + 1/φ² = 3 | TRINITY

// #8980: step only checked its gradients with an undeclared `math::is_finite`
// and updated nothing; the tests asserted `result != undefined`. step is now
// Adam (Kingma and Ba, 2015) with bias correction. It refuses (returns false,
// changes nothing) when a gradient is NaN or infinite, which is what the old body
// validated.

module Adam;
    use base::types;
    use math::constants;

    // ═══════════════════════════════════════════════════════════
    // 1. Constants
    // ═══════════════════════════════════════════════════════════

    const DEFAULT_BETA1 : f32 = 0.9;
    const DEFAULT_BETA2 : f32 = 0.999;
    const DEFAULT_EPSILON : f32 = 1e-8;

    // ═══════════════════════════════════════════════════════════
    // 2. Types
    // ═══════════════════════════════════════════════════════════

    pub const AdamConfig = struct {
        learning_rate : f32,
        beta1 : f32,
        beta2 : f32,
        epsilon : f32,
        weight_decay : f32,
    };

    // ═══════════════════════════════════════════════════════════
    // 3. Core Functions
    // ═══════════════════════════════════════════════════════════

    // b^t for a whole t >= 0
    fn powi(b: f32, t: u32) -> f32 {
        var r : f32 = 1.0;
        var n : u32 = 0;
        while (n < t) {
            r = r * b;
            n = n + 1;
        }
        return r;
    }

    // NaN is the one value not equal to itself; inf - inf is NaN
    fn is_finite(x: f32) -> bool {
        return x == x and (x - x) == (x - x);
    }

    // One Adam step number t (1-based) over params, with moment buffers m and v:
    //   g = grad + wd p
    //   m = b1 m + (1 - b1) g          v = b2 v + (1 - b2) g^2
    //   m_hat = m / (1 - b1^t)          v_hat = v / (1 - b2^t)
    //   p = p - lr m_hat / (sqrt(v_hat) + eps)
    fn step(params: []f32, grads: []const f32, m: []f32, v: []f32, t: u32, config: AdamConfig) -> bool {
        var i : usize = 0;
        while (i < grads.len) {
            if (!is_finite(grads[i])) {
                return false;
            }
            i = i + 1;
        }
        const c1 = 1.0 - powi(config.beta1, t);
        const c2 = 1.0 - powi(config.beta2, t);
        i = 0;
        while (i < params.len) {
            const g = grads[i] + config.weight_decay * params[i];
            m[i] = config.beta1 * m[i] + (1.0 - config.beta1) * g;
            v[i] = config.beta2 * v[i] + (1.0 - config.beta2) * g * g;
            const m_hat = m[i] / c1;
            const v_hat = v[i] / c2;
            params[i] = params[i] - config.learning_rate * m_hat / (sqrt(v_hat) + config.epsilon);
            i = i + 1;
        }
        return true;
    }

    fn cfg(lr: f32) -> AdamConfig {
        return AdamConfig{ .learning_rate = lr, .beta1 = DEFAULT_BETA1, .beta2 = DEFAULT_BETA2, .epsilon = DEFAULT_EPSILON, .weight_decay = 0.0 };
    }

    // which: 0 = param, 1 = m, 2 = v after one step on params (1, 2, 3) with grads (g0, g1, g2)
    fn first_step(g0: f32, g1: f32, g2: f32, which: u8, index: usize) -> f32 {
        var params : [3]f32 = [1.0, 2.0, 3.0];
        const grads : [3]f32 = [g0, g1, g2];
        var m : [3]f32 = [0.0] ** 3;
        var v : [3]f32 = [0.0] ** 3;
        _ = step(&params, &grads, &m, &v, 1, cfg(0.001));
        if (which == 1) {
            return m[index];
        }
        if (which == 2) {
            return v[index];
        }
        return params[index];
    }

    // a / b at run time, where IEEE gives NaN for 0 / 0 and inf for 1 / 0
    fn ratio(a: f32, b: f32) -> f32 {
        return a / b;
    }

    fn accepts(g: f32) -> bool {
        var params : [1]f32 = [1.0];
        const grads : [1]f32 = [g];
        var m : [1]f32 = [0.0];
        var v : [1]f32 = [0.0];
        return step(&params, &grads, &m, &v, 1, cfg(0.001)) and params[0] != 1.0;
    }

    // Minimise (x - 3)^2 from 0
    fn descend(steps: u32) -> f32 {
        var params : [1]f32 = [0.0];
        var m : [1]f32 = [0.0];
        var v : [1]f32 = [0.0];
        var t : u32 = 1;
        while (t <= steps) {
            const grads : [1]f32 = [2.0 * (params[0] - 3.0)];
            _ = step(&params, &grads, &m, &v, t, cfg(0.1));
            t = t + 1;
        }
        return params[0];
    }

    // ═══════════════════════════════════════════════════════════
    // TDD: Tests (from .tri behaviors)
    // ═══════════════════════════════════════════════════════════

    // Bias correction makes the first step lr * sign(g), whatever |g| is
    test step_basic_case
        given p0 = first_step(0.1, -5.0, 0.0, 0, 0)
        when p1 = first_step(0.1, -5.0, 0.0, 0, 1)
        then @abs(p0 - 0.999) < 0.000001 and @abs(p1 - 2.001) < 0.000001 and first_step(0.1, -5.0, 0.0, 0, 2) == 3.0

    // m = 0.1 g and v = 0.001 g^2 after the first step
    test step_with_gradients
        given m2 = first_step(0.1, 0.2, 0.3, 1, 2)
        when v2 = first_step(0.1, 0.2, 0.3, 2, 2)
        then @abs(m2 - 0.03) < 0.0000001 and @abs(v2 - 0.00009) < 0.0000001

    // Finite gradients are applied; NaN and inf are refused
    test step_validates_gradients
        given ok = accepts(1.5) and accepts(-2.3) and accepts(4.7)
        when refused = !accepts(ratio(0.0, 0.0)) and !accepts(ratio(1.0, 0.0))
        then ok and refused

    test step_descends_a_quadratic
        given result = descend(500)
        then @abs(result - 3.0) < 0.01

    invariant adam_betas_are_below_one
        assert DEFAULT_BETA1 < 1.0 and DEFAULT_BETA2 < 1.0 and DEFAULT_BETA1 < DEFAULT_BETA2 and DEFAULT_EPSILON > 0.0

Open the lesson's spec in the player ↗

All lessons