EasyOptimization

AdaGrad Optimizer

Optimization

Easy

Problem

Implement one AdaGrad update. First accumulate the elementwise squared gradient:

G_t = G_{t-1} + g_t^2

Then update each parameter:

w_t = w_{t-1} - \eta\frac{g_t}{\sqrt{G_t + \varepsilon}}

Here, w contains parameters, g contains the current gradients, G contains accumulated squared gradients, \eta is lr, and \varepsilon is eps. Return a dictionary containing new_w and new_G, both as NumPy arrays.

Theory

Vanilla gradient descent uses a single learning rate for every parameter in the model:

w_t = w_{t-1} - \eta \cdot g_t

Every weight, every bias, every embedding entry gets multiplied by the same \eta. This is a problem because different parameters have very different gradient characteristics:

A single \eta cannot satisfy both groups. If you set it for the frequent features, rare features learn too slowly. If you set it for the rare features, frequent features oscillate.


The Core Idea: Divide by Past Gradient Size

AdaGrad (Adaptive Gradient Algorithm, Duchi et al., 2011) gives each parameter its own effective learning rate based on a simple principle: parameters that have received large gradients in the past should get smaller learning rates now.

It keeps a running accumulator G_t for each parameter:

G_t = G_{t-1} + g_t^2

Then the update rule divides by the square root of G_t:

w_t = w_{t-1} - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t


A Step-by-Step Example

Parameters: w = [1.0, 2.0], accumulator: G = [0.0, 0.0], gradient: g = [2.0, 0.1], \eta = 0.5

Update the accumulator (element-wise):

Compute effective learning rates:

Update parameters:

Both parameters moved by the same amount (0.5), even though parameter 1's gradient was 20x larger! AdaGrad automatically equalized the step sizes.

Now suppose we do another step with gradient g = [2.0, 0.0]:

Update accumulator:

Effective learning rates:

Parameter 1's learning rate has shrunk because it keeps getting large gradients. Parameter 2's rate stayed the same because it did not receive a gradient this step. This is exactly the adaptive behavior we want.


The Monotonic Decay Problem

The accumulator G_t is a sum. It only grows. It never decreases. This means the effective learning rate \frac{\eta}{\sqrt{G_t}} only decreases over time.

After enough steps:

This is a fundamental issue:

This is why RMSProp and Adam replaced AdaGrad for most deep learning tasks. They use a decaying average instead of a sum, so the effective learning rate can recover.


Where AdaGrad Excels

Despite the decay problem, AdaGrad is the best choice in specific settings:


AdaGrad's Legacy

Even though AdaGrad is rarely used directly in modern deep learning, every major optimizer builds on its idea:

Understanding AdaGrad is understanding the foundation of the entire adaptive optimizer family.

Examples

Example 1

Input
w = [1.0, 2.0], g = [0.1, -0.2], G = [0.0, 0.0], lr = 0.1, eps = 1e-8
Output
{"new_w": [0.9, 2.1], "new_G": [0.01, 0.04]}
Explanation
Squaring the gradient updates the accumulator, then each parameter uses its accumulator-adjusted step size.

Example 2

Input
w = [1.0, 2.0], g = [0.0, 0.0], G = [0.1, 0.2], lr = 0.1, eps = 1e-8
Output
{"new_w": [1.0, 2.0], "new_G": [0.1, 0.2]}

Example 3

Input
w = [0.0], g = [1.0], G = [100.0], lr = 0.1, eps = 1e-8
Output
{"new_w": [-0.00995], "new_G": [101.0]}

Hints

  1. Compute new_G = G + g ** 2 before updating the parameters.
  2. Use np.sqrt(new_G + eps) as the elementwise denominator.

Requirements

Constraints

Starter Code

import numpy as np

def adagrad_step(w: list, g: list, G: list, lr: float = 0.01, eps: float = 1e-8) -> dict:
    """
    Returns a dictionary with new_w and new_G.
    """
    # Write code here
    pass

Test Cases

CaseMatches
First stepExample 1public
Zero gradientExample 2public
Large accumulated GExample 3public