""" RMSprop (Root Mean Square Propagation) optimizer implementation. RMSprop is an adaptive learning rate optimizer that maintains a moving average of squared gradients to normalize the gradient. It was proposed by Geoffrey Hinton in his Coursera course on Neural Networks. Key idea: Instead of using a fixed learning rate, RMSprop adapts the learning rate for each parameter by dividing by a running average of recent gradient magnitudes. Update rules: v(t) = rho * v(t-1) + (1 - rho) * gradient^2 param = param - (learning_rate / sqrt(v(t) + epsilon)) * gradient Where: v(t) = moving average of squared gradients rho = decay factor (typically 0.9) learning_rate = step size epsilon = small value to avoid division by zero Reference: https://en.wikipedia.org/wiki/Stochastic_gradient_descent#RMSProp >>> rmsprop([0.0], [0.1], 0.01) # doctest: +ELLIPSIS [-0.031...] >>> rmsprop([1.0, -1.0], [0.5, -0.5], 0.01) # doctest: +ELLIPSIS [0.968..., -0.968...] """ import math def rmsprop( params: list[float], gradients: list[float], learning_rate: float, rho: float = 0.9, epsilon: float = 1e-8, moving_avg: list[float] | None = None, ) -> list[float]: """ Perform one step of the RMSprop optimization algorithm. :param params: Current parameter values to be updated. :param gradients: Gradients of the loss with respect to each parameter. :param learning_rate: Step size for the update (must be positive). :param rho: Decay factor for the moving average (default 0.9). :param epsilon: Small constant to avoid division by zero (default 1e-8). :param moving_avg: Running average of squared gradients. Updated in place each call. Initialized to zeros if not provided. :return: Updated parameter values after one RMSprop step. :raises ValueError: If params and gradients have different lengths. :raises ValueError: If learning_rate, rho, or epsilon are out of range. >>> rmsprop([0.0], [0.0], 0.01) [0.0] >>> rmsprop([1.0], [0.0], 0.01) [1.0] >>> len(rmsprop([1.0, 2.0, 3.0], [0.1, 0.2, 0.3], 0.01)) == 3 True >>> rmsprop([1.0], [0.5], learning_rate=0.01) # doctest: +ELLIPSIS [0.968...] """ if len(params) != len(gradients): msg = ( f"params and gradients must have the same length, " f"got {len(params)} and {len(gradients)}" ) raise ValueError(msg) if learning_rate <= 0: msg = f"learning_rate must be positive, got {learning_rate}" raise ValueError(msg) if not 0.0 <= rho < 1.0: msg = f"rho must be in [0, 1), got {rho}" raise ValueError(msg) if epsilon <= 0: msg = f"epsilon must be positive, got {epsilon}" raise ValueError(msg) # Initialize moving average of squared gradients to zeros if moving_avg is None: moving_avg = [0.0] * len(params) updated_params = [] for i, (param, grad) in enumerate(zip(params, gradients)): # Update moving average IN PLACE so state persists across calls moving_avg[i] = rho * moving_avg[i] + (1 - rho) * grad**2 # Compute adaptive update param = param - (learning_rate / math.sqrt(moving_avg[i] + epsilon)) * grad updated_params.append(param) return updated_params if __name__ == "__main__": import doctest doctest.testmod() print("RMSprop Optimizer Demo") print("=" * 40) print("Minimizing f(x) = x^2 (minimum at x = 0)") print("Gradient: f'(x) = 2x | Learning rate: 0.1\n") param = [5.0] moving_avg = [0.0] print(f"{'Step':>6} | {'param':>12} | {'f(x)':>12}") print("-" * 38) print(f"{'0':>6} | {param[0]:>12.6f} | {param[0] ** 2:>12.6f}") for step in range(1, 101): gradient = [2 * param[0]] param = rmsprop(param, gradient, learning_rate=0.1, moving_avg=moving_avg) if step % 20 == 0: print(f"{step:>6} | {param[0]:>12.6f} | {param[0] ** 2:>12.8f}") print(f"\nConverged to x = {param[0]:.8f} (true minimum = 0.0)")