t27.aiEnglish

RMSProp: шаг без масштаба

Вы узнаете

Как RMSProp делит каждый шаг на скользящий корень среднего квадрата, чтобы шаг больше не зависел от величины градиента.

Правильный размер шага зависит от величины градиента, а разные веса видят градиенты, отличающиеся в миллион раз. RMSProp хранит скользящее среднее квадрата градиента, кэш, и делит каждый шаг на его корень, поэтому у шага нет единиц: при постоянном градиенте 0.001, 0.5 или 1000 вес на шаге 1 сдвигается ровно на те же 0.0316 и идёт тем же путём, доходя до -0.0914 после 100 шагов при lr 0.01. Затухание задаёт, как медленно наполняется кэш: при 0.99 те же 100 шагов заканчиваются на -1.0261. Спека rmsprop.t27 вычисляет вес и кэш.

Попробовать

Переключайтесь между тремя градиентами и убедитесь, что линии не двигаются; затем поднимите затухание до 0.99.

Открыть интерактивный урок →

RMSProp: a step that does not care how big the gradient is
RMSProp: a step that does not care how big the gradient is ↗

Feed one weight a constant gradient of 0.001, 0.5 or 1000: RMSProp divides by the root of its running mean square, so all three walk the same path. Change the decay and the path bends.

specs/ml/optimizer/rmsprop.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// RMSprop optimizer with moving average of squared gradients | φ² + 1/φ² = 3 | TRINITY
// Restored 2026-10-05 from trinity specs/algo/rmsprop.tri. The port kept only
// `grad`, squared grad[0], and threw it away; the test read an undefined
// default_input().

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

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

    const DEFAULT_DECAY : f32 = 0.9;

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

    pub const RMSpropConfig = struct {
        learning_rate : f32,
        decay : f32,
        epsilon : f32,
    };

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

    // step(grad, param, cache, config) -> void, in place:
    //   cache = decay * cache + (1 - decay) * grad^2
    //   param -= lr * grad / (sqrt(cache) + epsilon)
    // The .tri formula reads lr, decay and epsilon but its parameter list
    // omits them; they come from config here.
    fn step(grad: []const f32, param: []f32, cache: []f32, config: RMSpropConfig) -> void {
        var i : usize = 0;
        while (i < grad.len) {
            const g = grad[i];
            cache[i] = config.decay * cache[i] + (1.0 - config.decay) * g * g;
            param[i] = param[i] - config.learning_rate * g / (@sqrt(cache[i]) + config.epsilon);
            i = i + 1;
        }
    }

    // ═══════════════════════════════════════════════════════════
    // TDD: Tests
    // ═══════════════════════════════════════════════════════════

    // run `steps` steps of constant gradient g on one parameter from 1.0
    fn param_after(g: f32, steps: usize, decay: f32) -> f32 {
        const grad : [1]f32 = [g];
        var param : [1]f32 = [1.0];
        var cache : [1]f32 = [0.0];
        var k : usize = 0;
        while (k < steps) {
            step(&grad, &param, &cache, RMSpropConfig{ .learning_rate = 0.01, .decay = decay, .epsilon = 0.00000001 });
            k = k + 1;
        }
        return param[0];
    }

    fn cache_after(g: f32, steps: usize) -> f32 {
        const grad : [1]f32 = [g];
        var param : [1]f32 = [1.0];
        var cache : [1]f32 = [0.0];
        var k : usize = 0;
        while (k < steps) {
            step(&grad, &param, &cache, RMSpropConfig{ .learning_rate = 0.01, .decay = DEFAULT_DECAY, .epsilon = 0.00000001 });
            k = k + 1;
        }
        return cache[0];
    }

    // first step from cache 0: cache = 0.1 g^2, so the move is lr / sqrt(0.1)
    // = 0.0316228 whatever the size of g
    test first_step_is_scale_free
        given small = param_after(0.001, 1, DEFAULT_DECAY)
        when large = param_after(1000.0, 1, DEFAULT_DECAY)
        then @abs(small - (1.0 - 0.0316228)) < 0.00001 and @abs(large - small) < 0.00001

    // moving average of a constant: cache_n = (1 - 0.9^n) g^2
    test cache_is_moving_average
        given result = cache_after(2.0, 3)
        when expected = (1.0 - 0.729) * 4.0
        then @abs(result - expected) < 0.00001

    test moves_against_the_gradient
        given up = param_after(0.5, 2, DEFAULT_DECAY)
        when down = param_after(-0.5, 2, DEFAULT_DECAY)
        then up < 1.0 and down > 1.0 and @abs((1.0 - up) - (down - 1.0)) < 0.00001

    // ═══════════════════════════════════════════════════════════
    // TDD: Invariants
    // ═══════════════════════════════════════════════════════════

    invariant rmsprop_decay_in_unit_interval
        assert DEFAULT_DECAY > 0.0 and DEFAULT_DECAY < 1.0

Открыть спеку урока в плеере ↗

Все уроки