RMSProp
You will learn
How RMSProp divides each step by a running root mean square, so the step no longer depends on the gradient's size.
The right step size depends on the size of the gradient, and different weights see gradients a million times apart. RMSProp keeps a running mean of the squared gradient, the cache, and divides each step by its root, so the step has no units: with a constant gradient of 0.001, 0.5 or 1000 the weight moves by exactly the same 0.0316 on step 1 and walks the same path, reaching -0.0914 after 100 steps at lr 0.01. The decay sets how slowly the cache fills: at 0.99 the same 100 steps end at -1.0261. The spec rmsprop.t27 computes the weight and the cache.
Try it
Switch between the three gradients and check that the lines do not move; then raise the decay to 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
All lessons
Module 1 · One neuron
A neuron sums its inputs and bends the sum: ReLU, the sigmoid and tanh, and where each one stops passing a gradient back.
Module 2 · New bends
The smooth curves of current models: GELU with its dip, Swish with its knob, and ELU with a floor instead of a cliff.
Module 3 · A layer
What sits around the neurons: softmax turns scores into a choice, an embedding turns a word into numbers, dropout switches units off.
Module 4 · Pictures
Reading a picture in patches: a window averaged over its patch, max pooling, and batch normalisation of features on different scales.
Module 5 · The error
How wrong a guess is, as one number: mean squared error, cross-entropy for probabilities, and the Huber loss for outliers.
Module 6 · Downhill
Walking down the loss: gradient descent with momentum, RMSProp that ignores the gradient's size, and Adam that combines them.
Module 7 · Learning
Where the gradients come from and what they achieve: backpropagation through a layer, a network that learns XOR, and LAMB for large batches.
Module 8 · Memory and attention
Networks that read a sequence: a recurrent cell and its fading echo, the gates of an LSTM, and attention over every position at once.
Module 9 · The transformer and the reward
The parts of a transformer and of an agent: positions as clocks, one encoder block, and PPO, which learns from reward without lurching.