Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 58

Implementing Adam from Scratch

Recommended resources

Hello! In our last lesson, you supercharged your optimizer by implementing Momentum, giving your training process a "memory" of past gradients to accelerate convergence. You saw how it helps navigate ravines and plateaus in the loss landscape more effectively than vanilla gradient descent.

Today, we will implement the final and most powerful optimizer in this module: Adam (Adaptive Moment Estimation). Adam is the de-facto standard optimizer for most deep learning applications. It cleverly combines the idea of momentum with adaptive learning rates, creating a robust and efficient algorithm. By the end of this lesson, you will have implemented the Adam optimizer from scratch and integrated it into your neural network training loop.

1. The Two Pillars of Adam

Adam builds upon two key ideas, one of which you're already familiar with:

  1. Momentum: Uses an exponentially weighted average of past gradients to accelerate in the right direction. This is an estimate of the first moment (the mean) of the gradients.
  2. RMSprop: Adapts the learning rate for each parameter individually. It uses an exponentially weighted average of the squared gradients to scale the updates. This is an estimate of the second moment (the uncentered variance).

Let's watch a video that breaks down how Adam is derived from these two predecessors.

Adam Optimizer from Scratch in Python

The video 'Adam Optimizer from Scratch in Python' by Deep Learning with Yacine provides a fantastic explanation of Adam's components.

Watch from 03:53 to 10:00. The first part (until 05:52) will be a good review of the Momentum concept you've already implemented. The second part (from 05:52 to 10:00) introduces Adagrad and RMSprop, which provide the adaptive learning rate component. Focus on understanding how RMSprop uses squared gradients to scale the updates.

As the video explained, Adam combines these two concepts to get the best of both worlds. The result is an optimizer that not only moves faster but also intelligently adjusts its step size for every single parameter in your network.

Adam Optimizer Path on a Contour Plot
This contour plot shows the path an Adam optimizer takes. Notice how smoothly and directly it navigates the loss landscape to find the minimum, avoiding the oscillations of simpler methods.

2. The Adam Algorithm: Math and Intuition

Let's assemble the complete Adam algorithm. At each timestep , for each parameter (e.g., a weight matrix ), we perform the following steps:

  1. Calculate Gradients: Compute using backpropagation (you've already done this).

  2. Update First Moment (Momentum):

    This is exactly the momentum update you learned in the last lesson. stores the exponentially weighted average of the gradients. is typically set to 0.9.

  3. Update Second Moment (RMSprop):

    This is the new part. stores the exponentially weighted average of the squared gradients. means element-wise squaring. is typically set to 0.999.

  4. Apply Bias Correction: Since and are initialized to zero, they are biased towards zero during the first few iterations. To counteract this, we compute corrected values:

    Here, is the current iteration number. As increases, the denominators approach 1, and the correction effect fades away.

  5. Update Parameters: Finally, we update the weights using the bias-corrected moment estimates.

    • is the learning rate.
    • We divide the momentum term by the square root of the squared gradient term . This is the adaptive learning rate part. If past gradients for a parameter have been large (large ), the update step is made smaller. If they've been small, the update is larger.
    • is a very small number (e.g., ) to prevent division by zero.

For a clear walkthrough of these steps and the corresponding code, the following article is an excellent guide.

How to Implement Adam Gradient Descent from Scratch ...

The article 'How to Implement Adam Gradient Descent from Scratch' from GeeksforGeeks provides the mathematical formulas and Python code structure we will use.

Read the section titled 'Mathematical Intuition Behind Adam's Gradient Descent'. It lays out all the equations we just discussed in a clean format. This will serve as a reference for our implementation.


3. Implementing Adam From Scratch

Now, let's get to the code. Your experience with Python, NumPy, and your existing training framework will make this straightforward. We will create two new functions, initialize_adam and update_parameters_with_adam, which are direct extensions of the momentum functions from our last lesson.

Step 1: Initialize Adam Parameters

First, we need a function to initialize the moment estimates, v and s, for all our model's parameters. They should all start as zero arrays with the same dimensions as the parameters they correspond to.

import numpy as np

def initialize_adam(parameters):
    """
    Initializes v and s as dictionaries with keys ("v_dW1", "v_db1", ...) and
    ("s_dW1", "s_db1", ...), and values as numpy arrays of zeros of the same 
    shape as the corresponding gradients/parameters.
    
    Args:
        parameters (dict): Contains the parameters "W1", "b1", etc.
    
    Returns:
        v (dict): Contains the exponentially weighted average of the gradient.
        s (dict): Contains the exponentially weighted average of the squared gradient.
    """
    num_layers = len(parameters) // 2
    v = {}
    s = {}
    
    for l in range(1, num_layers + 1):
        v[f"v_dW{l}"] = np.zeros_like(parameters[f"W{l}"])
        v[f"v_db{l}"] = np.zeros_like(parameters[f"b{l}"])
        s[f"s_dW{l}"] = np.zeros_like(parameters[f"W{l}"])
        s[f"s_db{l}"] = np.zeros_like(parameters[f"b{l}"])
        
    return v, s

This looks very similar to initialize_velocities, but now it creates two dictionaries.

Step 2: Implement the Adam Update Rule

This is the main function. It will take the parameters, gradients, the v and s dictionaries, the current timestep t, and the hyperparameters as input. It then implements the 5-step process we just discussed.

The following video shows a live coding session that implements this logic.

Adam Optimizer from Scratch in Python

Let's watch the implementation part of the 'Adam Optimizer from Scratch in Python' video.

Watch from 10:00 to 14:27. The presenter codes the Adam update logic step-by-step. Notice how each line of code directly corresponds to one of the formulas we covered.

Now, let's write our own version that fits neatly into your existing multi-layer network structure.

def update_parameters_with_adam(parameters, gradients, v, s, t, learning_rate=0.01,
                                beta1=0.9, beta2=0.999, epsilon=1e-8):
    """
    Update parameters using Adam.
    
    Args:
        parameters (dict): Contains current parameters.
        gradients (dict): Contains gradients from backprop.
        v (dict): Adam's first moment estimates.
        s (dict): Adam's second moment estimates.
        t (int): The current iteration number (for bias correction).
        learning_rate (float): The learning rate.
        beta1 (float): Exponential decay hyperparameter for the first moment.
        beta2 (float): Exponential decay hyperparameter for the second moment.
        epsilon (float): Hyperparameter to prevent division by zero.
    
    Returns:
        parameters (dict): Updated parameters.
        v (dict): Updated first moment estimates.
        s (dict): Updated second moment estimates.
    """
    num_layers = len(parameters) // 2
    v_corrected = {}
    s_corrected = {}
    
    for l in range(1, num_layers + 1):
        # 1. Update first moment estimates (v)
        v[f"v_dW{l}"] = beta1 * v[f"v_dW{l}"] + (1 - beta1) * gradients[f"dW{l}"]
        v[f"v_db{l}"] = beta1 * v[f"v_db{l}"] + (1 - beta1) * gradients[f"db{l}"]
        
        # 2. Compute bias-corrected first moment estimates
        v_corrected[f"v_dW{l}"] = v[f"v_dW{l}"] / (1 - beta1**t)
        v_corrected[f"v_db{l}"] = v[f"v_db{l}"] / (1 - beta1**t)
        
        # 3. Update second moment estimates (s)
        s[f"s_dW{l}"] = beta2 * s[f"s_dW{l}"] + (1 - beta2) * (gradients[f"dW{l}"]**2)
        s[f"s_db{l}"] = beta2 * s[f"s_db{l}"] + (1 - beta2) * (gradients[f"db{l}"]**2)
        
        # 4. Compute bias-corrected second moment estimates
        s_corrected[f"s_dW{l}"] = s[f"s_dW{l}"] / (1 - beta2**t)
        s_corrected[f"s_db{l}"] = s[f"s_db{l}"] / (1 - beta2**t)
        
        # 5. Update parameters
        parameters[f"W{l}"] -= learning_rate * (v_corrected[f"v_dW{l}"] / (np.sqrt(s_corrected[f"s_dW{l}"]) + epsilon))
        parameters[f"b{l}"] -= learning_rate * (v_corrected[f"v_db{l}"] / (np.sqrt(s_corrected[f"s_db{l}"]) + epsilon))
        
    return parameters, v, s
Test your understanding!

In the parameter update step, why do we use the bias-corrected moments (v_corrected, s_corrected) for the update, but return the uncorrected moments (v, s) from the function?

Show answer

The bias correction is a transient adjustment specific to the current timestep t. It's only used to calculate the correctly scaled update for this one step. The moment estimates themselves (v and s) must be carried over to the next iteration in their uncorrected form, because they represent the ongoing exponentially weighted average. In the next iteration, they will be combined with the new gradient and then corrected again using the new timestep t+1.

Step 3: Integrate into Your Training Loop

The final step is to incorporate these functions into your main training loop.

  1. Before the loop, call initialize_adam to create the v and s dictionaries.
  2. Initialize a timestep counter, t = 0.
  3. Inside the loop, after calculating gradients, increment t and call update_parameters_with_adam.

Your training loop's core logic will now look like this:

# In your main training function...
# ...
# Hyperparameters
learning_rate = 0.0075
beta1 = 0.9
beta2 = 0.999
epsilon = 1e-8

# 1. Initialize parameters
parameters = initialize_parameters(layer_dims)

# 2. Initialize Adam variables
v, s = initialize_adam(parameters)
t = 0 # Initialize timestep counter

# 3. Start the training loop
for i in range(num_iterations):
    # Forward prop, compute cost, backprop...
    AL, cache = forward_propagation(X, parameters)
    cost = compute_cost(AL, Y)
    gradients = back_propagation(AL, Y, cache)
    
    # Increment timestep and update parameters with Adam
    t = t + 1
    parameters, v, s = update_parameters_with_adam(parameters, gradients, v, s, t, learning_rate, beta1, beta2, epsilon)
    
    # ... rest of the loop (print cost, etc.)

Conclusion

Congratulations! You have now implemented the Adam optimizer from scratch. This is a significant milestone, as Adam is a cornerstone of modern deep learning. You've successfully combined two powerful concepts—momentum and adaptive per-parameter learning rates—into a single, elegant algorithm.

Key Takeaways:

  • Adam (Adaptive Moment Estimation) combines the benefits of Momentum (using past gradients to accelerate) and RMSprop (using past squared gradients to adapt learning rates).
  • It maintains two moving averages: the first moment () for the mean of gradients and the second moment () for the uncentered variance of gradients.
  • Bias correction is crucial in the early stages of training to counteract the zero-initialization of and .
  • The final update rule uses the corrected first moment as the numerator (like momentum) and the square root of the corrected second moment as the denominator (to scale the learning rate).

Preview of the Next Lesson:
You have now built a powerful neural network and mastered several optimization techniques. In our next module, we'll begin the capstone project. You'll first learn to implement early stopping to prevent overfitting automatically. Then, you will structure your code into a reusable pipeline to systematically train and evaluate every single algorithm you've built throughout this course on a benchmark dataset, creating a comprehensive report of their performance.