algos 0.6.8

A collection of algorithms in Rust
Documentation
/// Stochastic Gradient Descent optimizer implementation with momentum support
///
/// SGD updates parameters in the direction of the negative gradient, with optional
/// momentum to help accelerate training and overcome local minima.
#[derive(Debug, Clone)]
pub struct SGD {
    /// Learning rate
    pub learning_rate: f64,
    /// Momentum coefficient (between 0 and 1)
    pub momentum: f64,
    /// Velocity for momentum updates
    velocity: Vec<f64>,
}

impl SGD {
    /// Creates a new SGD optimizer
    ///
    /// # Arguments
    ///
    /// * `learning_rate` - Step size for updates
    /// * `momentum` - Momentum coefficient (typically 0.9)
    ///
    /// # Example
    ///
    /// ```
    /// use algos::ml::deep::sgd::SGD;
    /// let optimizer = SGD::new(0.01, 0.9);
    /// ```
    pub fn new(learning_rate: f64, momentum: f64) -> Self {
        assert!(learning_rate > 0.0, "Learning rate must be positive");
        assert!(
            (0.0..1.0).contains(&momentum),
            "Momentum must be between 0 and 1"
        );

        SGD {
            learning_rate,
            momentum,
            velocity: Vec::new(),
        }
    }

    /// Initializes the optimizer for a given number of parameters
    ///
    /// # Arguments
    ///
    /// * `param_count` - Number of parameters to optimize
    pub fn initialize(&mut self, param_count: usize) {
        self.velocity = vec![0.0; param_count];
    }

    /// Updates parameters using SGD with momentum
    ///
    /// # Arguments
    ///
    /// * `params` - Parameters to update
    /// * `grads` - Gradients for each parameter
    ///
    /// # Returns
    ///
    /// * Updated parameters
    pub fn update(&mut self, params: &[f64], grads: &[f64]) -> Vec<f64> {
        assert_eq!(
            params.len(),
            grads.len(),
            "Parameters and gradients must have same length"
        );

        if self.velocity.is_empty() {
            self.initialize(params.len());
        }

        let mut updated_params = params.to_vec();

        for i in 0..params.len() {
            // Update velocity with momentum
            self.velocity[i] = self.momentum * self.velocity[i] - self.learning_rate * grads[i];

            // Update parameters
            updated_params[i] += self.velocity[i];
        }

        updated_params
    }

    /// Updates 2D parameters (e.g., weight matrices)
    ///
    /// # Arguments
    ///
    /// * `params` - 2D parameters to update
    /// * `grads` - 2D gradients for each parameter
    ///
    /// # Returns
    ///
    /// * Updated 2D parameters
    pub fn update_2d(&mut self, params: &[Vec<f64>], grads: &[Vec<f64>]) -> Vec<Vec<f64>> {
        assert_eq!(
            params.len(),
            grads.len(),
            "Parameters and gradients must have same dimensions"
        );

        let total_params: usize = params.iter().map(|row| row.len()).sum();
        if self.velocity.is_empty() {
            self.initialize(total_params);
        }

        let mut updated_params = params.to_vec();
        let mut velocity_idx = 0;

        for i in 0..params.len() {
            assert_eq!(
                params[i].len(),
                grads[i].len(),
                "Parameter and gradient rows must have same length"
            );

            for j in 0..params[i].len() {
                // Update velocity with momentum
                self.velocity[velocity_idx] =
                    self.momentum * self.velocity[velocity_idx] - self.learning_rate * grads[i][j];

                // Update parameters
                updated_params[i][j] += self.velocity[velocity_idx];

                velocity_idx += 1;
            }
        }

        updated_params
    }

    /// Updates 4D parameters (e.g., convolutional filters)
    ///
    /// # Arguments
    ///
    /// * `params` - 4D parameters to update
    /// * `grads` - 4D gradients for each parameter
    ///
    /// # Returns
    ///
    /// * Updated 4D parameters
    pub fn update_4d(
        &mut self,
        params: &[Vec<Vec<Vec<f64>>>],
        grads: &[Vec<Vec<Vec<f64>>>],
    ) -> Vec<Vec<Vec<Vec<f64>>>> {
        assert_eq!(
            params.len(),
            grads.len(),
            "Parameters and gradients must have same dimensions"
        );

        let total_params: usize = params
            .iter()
            .flat_map(|x| x.iter())
            .flat_map(|x| x.iter())
            .map(|x| x.len())
            .sum();

        if self.velocity.is_empty() {
            self.initialize(total_params);
        }

        let mut updated_params = params.to_vec();
        let mut velocity_idx = 0;

        for i in 0..params.len() {
            assert_eq!(params[i].len(), grads[i].len());
            for j in 0..params[i].len() {
                assert_eq!(params[i][j].len(), grads[i][j].len());
                for k in 0..params[i][j].len() {
                    assert_eq!(params[i][j][k].len(), grads[i][j][k].len());
                    for l in 0..params[i][j][k].len() {
                        // Update velocity with momentum
                        self.velocity[velocity_idx] = self.momentum * self.velocity[velocity_idx]
                            - self.learning_rate * grads[i][j][k][l];

                        // Update parameters
                        updated_params[i][j][k][l] += self.velocity[velocity_idx];

                        velocity_idx += 1;
                    }
                }
            }
        }

        updated_params
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Tests SGD initialization
    #[test]
    fn test_sgd_initialization() {
        let optimizer = SGD::new(0.01, 0.9);
        assert_eq!(optimizer.learning_rate, 0.01);
        assert_eq!(optimizer.momentum, 0.9);
        assert!(optimizer.velocity.is_empty());
    }

    /// Tests invalid learning rate
    #[test]
    #[should_panic(expected = "Learning rate must be positive")]
    fn test_invalid_learning_rate() {
        SGD::new(-0.01, 0.9);
    }

    /// Tests invalid momentum
    #[test]
    #[should_panic(expected = "Momentum must be between 0 and 1")]
    fn test_invalid_momentum() {
        SGD::new(0.01, 1.5);
    }

    /// Tests parameter update
    #[test]
    fn test_parameter_update() {
        let mut optimizer = SGD::new(0.1, 0.9);
        let params = vec![1.0, 2.0, 3.0];
        let grads = vec![0.1, 0.2, 0.3];

        let updated = optimizer.update(&params, &grads);

        assert_eq!(updated.len(), params.len());
        for i in 0..params.len() {
            assert!(updated[i] != params[i]); // Parameters should change
        }
    }

    /// Tests 2D parameter update
    #[test]
    fn test_2d_parameter_update() {
        let mut optimizer = SGD::new(0.1, 0.9);
        let params = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
        let grads = vec![vec![0.1, 0.2], vec![0.3, 0.4]];

        let updated = optimizer.update_2d(&params, &grads);

        assert_eq!(updated.len(), params.len());
        assert_eq!(updated[0].len(), params[0].len());
        for i in 0..params.len() {
            for j in 0..params[i].len() {
                assert!(updated[i][j] != params[i][j]); // Parameters should change
            }
        }
    }

    /// Tests 4D parameter update
    #[test]
    fn test_4d_parameter_update() {
        let mut optimizer = SGD::new(0.1, 0.9);
        let params = vec![vec![vec![vec![1.0; 2]; 2]; 2]; 2];
        let grads = vec![vec![vec![vec![0.1; 2]; 2]; 2]; 2];

        let updated = optimizer.update_4d(&params, &grads);

        assert_eq!(updated.len(), params.len());
        assert_eq!(updated[0].len(), params[0].len());
        assert_eq!(updated[0][0].len(), params[0][0].len());
        assert_eq!(updated[0][0][0].len(), params[0][0][0].len());

        // Check that parameters have been updated
        assert!(updated[0][0][0][0] != params[0][0][0][0]);
    }

    /// Tests momentum effect
    #[test]
    fn test_momentum() {
        let mut optimizer = SGD::new(0.1, 0.9);
        let params = vec![1.0];
        let grads = vec![1.0];

        // First update
        let updated1 = optimizer.update(&params, &grads);
        let velocity1 = optimizer.velocity[0];

        // Second update with same gradient
        let _updated2 = optimizer.update(&updated1, &grads);
        let velocity2 = optimizer.velocity[0];

        // Velocity should increase due to momentum
        assert!(velocity2.abs() > velocity1.abs());
    }
}