Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

optimizers.py

10_optimizers/optimizers.py  ·  189 lines  ·  view on GitHub

"""
Optimizers from Scratch
Interview question: "Implement Adam optimizer"

Mathematical Formulation (Adam):
1. First moment (momentum): m_t = β₁ × m_{t-1} + (1-β₁) × g_t
2. Second moment (variance): v_t = β₂ × v_{t-1} + (1-β₂) × g_t²
3. Bias correction: m̂_t = m_t / (1 - β₁^t), v̂_t = v_t / (1 - β₂^t)
4. Update: θ_t = θ_{t-1} - α × m̂_t / (√v̂_t + ε)

Why it works:
- Adaptive learning rates per parameter
- Momentum smooths updates
- Second moment adapts to gradient variance
- Bias correction fixes initial bias
"""
import numpy as np

class SGD:
    """Stochastic Gradient Descent"""
    
    def __init__(self, learning_rate: float = 0.01):
        self.learning_rate = learning_rate
    
    def update(self, params: np.ndarray, gradients: np.ndarray) -> np.ndarray:
        """Simple SGD update"""
        params -= self.learning_rate * gradients
        return params

class SGDWithMomentum:
    """
    SGD with Momentum
    v_t = beta * v_{t-1} + gradient
    params = params - lr * v_t
    """
    
    def __init__(self, learning_rate: float = 0.01, momentum: float = 0.9):
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.velocity = None
    
    def update(self, params: np.ndarray, gradients: np.ndarray) -> np.ndarray:
        """Update with momentum"""
        if self.velocity is None:
            self.velocity = np.zeros_like(params)
        
        self.velocity = self.momentum * self.velocity + gradients
        params -= self.learning_rate * self.velocity
        return params

class RMSprop:
    """
    RMSprop: Adaptive learning rate
    v_t = beta * v_{t-1} + (1-beta) * gradient^2
    params = params - lr * gradient / sqrt(v_t + eps)
    """
    
    def __init__(self, learning_rate: float = 0.001, beta: float = 0.9,
                 epsilon: float = 1e-8):
        self.learning_rate = learning_rate
        self.beta = beta
        self.epsilon = epsilon
        self.v = None
    
    def update(self, params: np.ndarray, gradients: np.ndarray) -> np.ndarray:
        """Update with RMSprop"""
        if self.v is None:
            self.v = np.zeros_like(params)
        
        self.v = self.beta * self.v + (1 - self.beta) * (gradients**2)
        params -= self.learning_rate * gradients / (np.sqrt(self.v) + self.epsilon)
        return params

class Adam:
    """
    Adam: Adaptive Moment Estimation
    Combines momentum (first moment) and RMSprop (second moment)
    Most popular optimizer
    """
    
    def __init__(self, learning_rate: float = 0.001,
                 beta1: float = 0.9, beta2: float = 0.999,
                 epsilon: float = 1e-8):
        self.learning_rate = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.epsilon = epsilon
        self.m = None  # First moment (momentum)
        self.v = None  # Second moment (variance)
        self.t = 0     # Time step
    
    def update(self, params: np.ndarray, gradients: np.ndarray) -> np.ndarray:
        """Update parameters using Adam"""
        if self.m is None:
            self.m = np.zeros_like(params)
            self.v = np.zeros_like(params)
        
        self.t += 1
        
        # Update biased first moment (momentum)
        self.m = self.beta1 * self.m + (1 - self.beta1) * gradients
        
        # Update biased second moment (variance)
        self.v = self.beta2 * self.v + (1 - self.beta2) * (gradients**2)
        
        # Bias correction (important!)
        m_hat = self.m / (1 - self.beta1**self.t)
        v_hat = self.v / (1 - self.beta2**self.t)
        
        # Update parameters
        params -= self.learning_rate * m_hat / (np.sqrt(v_hat) + self.epsilon)
        
        return params

class AdamW:
    """
    AdamW: Adam with decoupled weight decay
    Better weight decay than Adam
    """
    
    def __init__(self, learning_rate: float = 0.001,
                 beta1: float = 0.9, beta2: float = 0.999,
                 weight_decay: float = 0.01, epsilon: float = 1e-8):
        self.learning_rate = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.weight_decay = weight_decay
        self.epsilon = epsilon
        self.m = None
        self.v = None
        self.t = 0
    
    def update(self, params: np.ndarray, gradients: np.ndarray) -> np.ndarray:
        """Update with AdamW"""
        if self.m is None:
            self.m = np.zeros_like(params)
            self.v = np.zeros_like(params)
        
        self.t += 1
        
        # Update moments (same as Adam)
        self.m = self.beta1 * self.m + (1 - self.beta1) * gradients
        self.v = self.beta2 * self.v + (1 - self.beta2) * (gradients**2)
        
        # Bias correction
        m_hat = self.m / (1 - self.beta1**self.t)
        v_hat = self.v / (1 - self.beta2**self.t)
        
        # Adam update
        params -= self.learning_rate * m_hat / (np.sqrt(v_hat) + self.epsilon)
        
        # Decoupled weight decay (applied separately)
        params -= self.learning_rate * self.weight_decay * params
        
        return params


# Usage Example
if __name__ == "__main__":
    print("Optimizers Comparison")
    print("=" * 60)
    
    # Simple optimization problem: minimize x^2
    x_sgd = np.array([5.0])
    x_momentum = np.array([5.0])
    x_adam = np.array([5.0])
    
    sgd = SGD(learning_rate=0.1)
    momentum = SGDWithMomentum(learning_rate=0.1, momentum=0.9)
    adam = Adam(learning_rate=0.1)
    
    print("Minimizing f(x) = x^2 (optimal x = 0)")
    print(f"Initial x: {x_sgd[0]}")
    print()
    
    for step in range(10):
        # Gradient of x^2 is 2*x
        grad = 2 * x_sgd
        
        x_sgd = sgd.update(x_sgd, grad)
        x_momentum = momentum.update(x_momentum, grad)
        x_adam = adam.update(x_adam, grad)
        
        if step % 2 == 0:
            print(f"Step {step}:")
            print(f"  SGD: {x_sgd[0]:.4f}")
            print(f"  Momentum: {x_momentum[0]:.4f}")
            print(f"  Adam: {x_adam[0]:.4f}")