EasyOptimization

Implement Nesterov Momentum (NAG)

Optimization

Easy

Problem

Perform one Nesterov momentum update using a gradient already evaluated at the look-ahead position.

v_t = \mu v_{t-1} + \eta g_t

w_t = w_{t-1} - v_t

Here, w_{t-1} is the current parameter array, v_{t-1} is the previous velocity, g_t is the supplied look-ahead gradient, \eta is lr, and \mu is momentum. Return a dictionary containing new_w and new_v as NumPy arrays.

Theory

Vanilla gradient descent updates parameters using only the current gradient:

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

This has several problems:

Momentum fixes all of these by adding a velocity term:

v_t = \mu \cdot v_{t-1} + \eta \cdot g_t

w_t = w_{t-1} - v_t

Think of a ball rolling down a hill:


The Problem with Standard Momentum

Standard momentum evaluates the gradient at the current position w_{t-1}, then uses the velocity to move. But the velocity is about to carry the parameters somewhere new. By the time the update is applied, the gradient information is slightly stale.

This causes overshooting. When the optimizer approaches a minimum:

  1. The velocity is large (built up from the downhill run)
  2. The gradient at the current position says "keep going" (still on the slope)
  3. The velocity carries the parameters past the minimum
  4. Now the gradient reverses, but the velocity still points in the old direction
  5. It takes several steps for the gradient to overcome the accumulated velocity
  6. The optimizer oscillates around the minimum before settling

The more momentum you have (larger \mu), the worse the overshooting.


Nesterov's Insight: Look Before You Leap

Nesterov Accelerated Gradient (NAG), proposed by Yurii Nesterov in 1983, has an elegant fix:

Instead of evaluating the gradient where you are, evaluate it where momentum is about to take you.

The algorithm:

Step 1: Compute the look-ahead position (where momentum would take you):

w_{\text{look}} = w_{t-1} - \mu \cdot v_{t-1}

This is not an update. It is a hypothetical: "if I just applied my current velocity, where would I end up?"

Step 2: Compute the gradient at the look-ahead position:

g_{\text{look}} = g(w_{\text{look}})

Instead of asking "what is the gradient here?", we ask "what is the gradient there (where I am heading)?"

Step 3: Update velocity using this look-ahead gradient:

v_t = \mu \cdot v_{t-1} + \eta \cdot g_{\text{look}}

Step 4: Update parameters:

w_t = w_{t-1} - v_t


Why Looking Ahead Reduces Overshooting

Imagine rolling toward a valley with a hill on the other side:

Standard momentum:

Nesterov momentum:

The difference is subtle but compounds over many steps. Nesterov momentum consistently converges faster and more smoothly.


A Side-by-Side Comparison

Minimizing f(x) = x^2 (minimum at x = 0). Gradient: g(x) = 2x.

Starting: w = 5.0, v = 1.0, \mu = 0.9, \eta = 0.01

Standard momentum:

Nesterov momentum:

The differences:

These differences are small per step but accumulate significantly over hundreds or thousands of steps.


Convergence Theory

For convex functions, Nesterov momentum has a provably better convergence rate than standard momentum:

This is a major theoretical result. O(1/t^2) is actually the optimal rate for first-order methods on convex functions. No gradient-based method can do better (without additional information like second-order derivatives).

For non-convex problems (like deep learning), the theoretical guarantees are weaker, but Nesterov momentum still consistently performs better in practice.


Where Nesterov Momentum Shows Up

Examples

Example 1

Input
w = [1.0, -1.0], v = [0.0, 0.0], grad = [0.5, -0.25], lr = 0.1, momentum = 0.9
Output
{"new_w": [0.95, -0.975], "new_v": [0.05, -0.025]}
Explanation
With no previous velocity, the first update is the learning-rate-scaled gradient.

Example 2

Input
w = [1.0, 2.0], v = [0.5, -0.3], grad = [0.1, 0.2], lr = 0.1, momentum = 0.9
Output
{"new_w": [0.54, 2.25], "new_v": [0.46, -0.25]}

Example 3

Input
w = [2.0], v = [0.0], grad = [0.0], lr = 0.1, momentum = 0.9
Output
{"new_w": [2.0], "new_v": [0.0]}

Hints

  1. Compute new_v = momentum * v + lr * grad first.
  2. Subtract new_v from the current parameters to obtain new_w.

Requirements

Constraints

Starter Code

import numpy as np

def nesterov_momentum_step(w: list, v: list, grad: list, lr: float = 0.01, momentum: float = 0.9) -> dict:
    """
    Returns a dictionary with new_w and new_v.
    """
    # Write code here
    pass

Test Cases

CaseMatches
First stepExample 1public
With existing velocityExample 2public
Zero gradientExample 3public