Introduction to ADAM

Hello! Today, we will explore the ADAM (Adaptive Moment Estimation) algorithm. This advanced optimization algorithm is a favorite among machine learning practitioners as it combines the advantages of two other extensions of Stochastic Gradient Descent (SGD): Root Mean Square Propagation (RMSprop) and Adaptive Gradient Algorithm (AdaGrad). Our primary focus today is understanding ADAM, and we will also build it from scratch in C++ to optimize multivariable functions.

Understanding ADAM

Before we dive into ADAM, let us recall that classic gradient descent methods like SGD and even sophisticated versions like Momentum and RMSProp have some limitations. These limitations relate to sensitivity to learning rates, the issue of vanishing gradients, and the absence of individual adaptive learning rates for different parameters.

ADAM, a promising choice for an optimization algorithm, combines the merits of RMSProp and AdaGrad. It maintains a per-parameter learning rate adapted based on the average of recent magnitudes of the gradients for the weights (similar to RMSProp) and the average of recent gradients (like Momentum). This mechanism enables the algorithm to traverse quickly over the low gradient regions and slow down near the optimal points.

ADAM Mathematically
ADAM in C++ Code

Let's now consolidate the ADAM concept into C++ code, using the bias-corrected version. We will define an ADAM function, which takes the gradients, the decay rates beta1 and beta2, a numerical constant epsilon, the learning rate, previous estimates of m and v (initialized to 0), and the current epoch as input, and returns the updated parameters, along with the updated m and v.

#include <vector>
#include <cmath>
#include <tuple>

std::tuple<std::vector<double>, std::vector<double>, std::vector<double>>
ADAM(double beta1, double beta2, double epsilon, const std::vector<double>& grad,
     const std::vector<double>& m_prev, const std::vector<double>& v_prev,
     double learning_rate, int epoch) {

    std::vector<double> m(grad.size());
    std::vector<double> v(grad.size());
    std::vector<double> updates(grad.size());

    // Update biased first and second moment estimates
    for (size_t i = 0; i < grad.size(); ++i) {
        m[i] = beta1 * m_prev[i] + (1 - beta1) * grad[i];
        v[i] = beta2 * v_prev[i] + (1 - beta2) * grad[i] * grad[i];
    }

    // Compute bias-corrected moment estimates and parameter updates
    double m_correction = 1.0 - std::pow(beta1, epoch + 1);
    double v_correction = 1.0 - std::pow(beta2, epoch + 1);

    for (size_t i = 0; i < grad.size(); ++i) {
        double m_hat = m[i] / m_correction;
        double v_hat = v[i] / v_correction;
        updates[i] = learning_rate * m_hat / (std::sqrt(v_hat) + epsilon);
    }

    return {updates, m, v};
}

Note: The bias correction is essential and is always included in the standard ADAM algorithm. This ensures that the moment estimates are unbiased, especially during the initial steps of optimization.

Application of ADAM on Multivariable Function Optimization

Now, let's test ADAM by finding the minimum of a multivariable function f(x, y) = x^2 + y^2. The corresponding gradients are df/dx = 2*x and df/dy = 2*y. With an initial starting point at (x, y) = (3, 4), selected reasonable values for beta1=0.9, beta2=0.999, epsilon=1e-8, learning_rate=0.001 and an epoch size of 150, we can start minimizing our function.

#include <vector>
#include <iostream>
#include <cmath>
#include <tuple>

double f(double x, double y) {
    return x * x + y * y;
}

std::vector<double> df(double x, double y) {
    return {2 * x, 2 * y};
}

std::tuple<std::vector<double>, std::vector<double>, std::vector<double>>
ADAM(double beta1, double beta2, double epsilon, const std::vector<double>& grad,
     const std::vector<double>& m_prev, const std::vector<double>& v_prev,
     double learning_rate, int epoch) {

    std::vector<double> m(grad.size());
    std::vector<double> v(grad.size());
    std::vector<double> updates(grad.size());

    for (size_t i = 0; i < grad.size(); ++i) {
        m[i] = beta1 * m_prev[i] + (1 - beta1) * grad[i];
        v[i] = beta2 * v_prev[i] + (1 - beta2) * grad[i] * grad[i];
    }

    double m_correction = 1.0 - std::pow(beta1, epoch + 1);
    double v_correction = 1.0 - std::pow(beta2, epoch + 1);

    for (size_t i = 0; i < grad.size(); ++i) {
        double m_hat = m[i] / m_correction;
        double v_hat = v[i] / v_correction;
        updates[i] = learning_rate * m_hat / (std::sqrt(v_hat) + epsilon);
    }

    return {updates, m, v};
}

int main() {
    std::vector<double> coordinates = {3.0, 4.0};
    double learning_rate = 0.001;
    double beta1 = 0.9;
    double beta2 = 0.999;
    double epsilon = 1e-8;
    int max_epochs = 150;

    std::vector<double> m_prev = {0, 0};
    std::vector<double> v_prev = {0, 0};

    for (int epoch = 0; epoch <= max_epochs; ++epoch) {
        std::vector<double> grad = df(coordinates[0], coordinates[1]);
        auto [updates, m_new, v_new] = ADAM(beta1, beta2, epsilon, grad, m_prev, v_prev, learning_rate, epoch);

        for (size_t i = 0; i < coordinates.size(); ++i) {
            coordinates[i] -= updates[i];
        }

        m_prev = m_new;
        v_prev = v_new;

        if (epoch % 30 == 0) {
            std::cout << "Epoch " << epoch << ", current state: ["
                      << coordinates[0] << ", " << coordinates[1] << "]" << std::endl;
        }
    }

    return 0;
}

The output of this code is the following:

Epoch 0, current state: [2.98, 3.98]
Epoch 30, current state: [2.39207, 3.38882]
Epoch 60, current state: [1.85805, 2.83632]
Epoch 90, current state: [1.39802, 2.33613]
Epoch 120, current state: [1.01651, 1.89185]
Epoch 150, current state: [0.71242, 1.50458]
ADAM vs Others

ADAM (Adaptive Moment Estimation) optimizer is generally more efficient than many other optimization algorithms such as SGD (Stochastic Gradient Descent) or RMSprop.

Overall, while the actual efficiency of ADAM compared to other optimizing algorithms can depend on the specific task or dataset, it often performs well in terms of both speed and accuracy across a variety of tasks.

Conclusion

Congratulations! You've now understood ADAM and how to code it in C++. With its sound mathematical foundations and impressive empirical results, ADAM constitutes an excellent stepping-stone into the fascinating world of machine learning optimization.

Remember, practice solidifies comprehension and consolidates understanding. Remember to attempt the upcoming hands-on exercises to reinforce these new burgeoning concepts. Until next time, happy coding!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal