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.

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, ¶m, &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, ¶m, &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
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.