use crate::Result;
#[derive(Debug, Clone)]
pub enum LearningRate {
Constant(f64),
Decreasing { initial: f64, decay: f64 },
Exponential { initial: f64, decay: f64 },
Adaptive { initial: f64, epsilon: f64 },
}
impl LearningRate {
fn get_rate(&self, iteration: usize) -> f64 {
match self {
LearningRate::Constant(rate) => *rate,
LearningRate::Decreasing { initial, decay } => {
initial / (1.0 + decay * iteration as f64)
}
LearningRate::Exponential { initial, decay } => {
initial * decay.powi(iteration as i32)
}
LearningRate::Adaptive { initial, .. } => *initial,
}
}
}
pub struct GradientDescent {
learning_rate: LearningRate,
max_iterations: usize,
tolerance: f64,
momentum: f64,
verbose: bool,
}
impl Default for GradientDescent {
fn default() -> Self {
GradientDescent {
learning_rate: LearningRate::Constant(0.01),
max_iterations: 1000,
tolerance: 1e-6,
momentum: 0.0,
verbose: false,
}
}
}
impl GradientDescent {
pub fn new() -> Self {
Self::default()
}
pub fn with_learning_rate(mut self, lr: LearningRate) -> Self {
self.learning_rate = lr;
self
}
pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
pub fn with_tolerance(mut self, tol: f64) -> Self {
self.tolerance = tol;
self
}
pub fn with_momentum(mut self, momentum: f64) -> Self {
self.momentum = momentum;
self
}
pub fn verbose(mut self) -> Self {
self.verbose = true;
self
}
pub fn minimize<F, G>(
&self,
f: F,
grad_f: G,
initial: &[f64],
) -> Result<OptimizationResult>
where
F: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> Vec<f64>,
{
let mut x = initial.to_vec();
let mut velocity = vec![0.0; x.len()];
let mut accumulated_squared_gradients = vec![0.0; x.len()];
let mut history = Vec::new();
for iteration in 0..self.max_iterations {
let current_value = f(&x);
let gradient = grad_f(&x);
if self.verbose {
history.push((x.clone(), current_value));
}
let grad_norm: f64 = gradient.iter().map(|g| g * g).sum::<f64>().sqrt();
if grad_norm < self.tolerance {
return Ok(OptimizationResult {
parameters: x,
value: current_value,
iterations: iteration,
gradient_norm: grad_norm,
converged: true,
history,
});
}
let lr = self.learning_rate.get_rate(iteration);
match &self.learning_rate {
LearningRate::Adaptive { epsilon, .. } => {
for i in 0..x.len() {
accumulated_squared_gradients[i] += gradient[i] * gradient[i];
let adjusted_lr = lr / (accumulated_squared_gradients[i].sqrt() + epsilon);
velocity[i] = self.momentum * velocity[i] - adjusted_lr * gradient[i];
x[i] += velocity[i];
}
}
_ => {
for i in 0..x.len() {
velocity[i] = self.momentum * velocity[i] - lr * gradient[i];
x[i] += velocity[i];
}
}
}
}
let final_value = f(&x);
let final_gradient = grad_f(&x);
let grad_norm: f64 = final_gradient.iter().map(|g| g * g).sum::<f64>().sqrt();
Ok(OptimizationResult {
parameters: x,
value: final_value,
iterations: self.max_iterations,
gradient_norm: grad_norm,
converged: grad_norm < self.tolerance,
history,
})
}
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub parameters: Vec<f64>,
pub value: f64,
pub iterations: usize,
pub gradient_norm: f64,
pub converged: bool,
pub history: Vec<(Vec<f64>, f64)>,
}
pub struct StochasticGD {
learning_rate: LearningRate,
max_epochs: usize,
batch_size: usize,
tolerance: f64,
momentum: f64,
}
impl StochasticGD {
pub fn new(batch_size: usize) -> Self {
StochasticGD {
learning_rate: LearningRate::Decreasing {
initial: 0.01,
decay: 0.001,
},
max_epochs: 100,
batch_size,
tolerance: 1e-6,
momentum: 0.9,
}
}
pub fn minimize<G>(
&self,
grad_f: G,
initial: &[f64],
n_samples: usize,
) -> Result<OptimizationResult>
where
G: Fn(&[f64], &[usize]) -> Vec<f64> + Sync,
{
let mut x = initial.to_vec();
let mut velocity = vec![0.0; x.len()];
let mut total_iterations = 0;
for _epoch in 0..self.max_epochs {
let n_batches = n_samples.div_ceil(self.batch_size);
for batch_idx in 0..n_batches {
let start = batch_idx * self.batch_size;
let end = (start + self.batch_size).min(n_samples);
let batch_indices: Vec<usize> = (start..end).collect();
let gradient = grad_f(&x, &batch_indices);
let lr = self.learning_rate.get_rate(total_iterations);
for i in 0..x.len() {
velocity[i] = self.momentum * velocity[i] - lr * gradient[i];
x[i] += velocity[i];
}
total_iterations += 1;
}
let grad_norm: f64 = velocity.iter().map(|v| v * v).sum::<f64>().sqrt();
if grad_norm < self.tolerance {
return Ok(OptimizationResult {
parameters: x,
value: 0.0,
iterations: total_iterations,
gradient_norm: grad_norm,
converged: true,
history: Vec::new(),
});
}
}
Ok(OptimizationResult {
parameters: x,
value: 0.0,
iterations: total_iterations,
gradient_norm: 0.0,
converged: false,
history: Vec::new(),
})
}
}
pub fn numerical_gradient<F>(f: F, x: &[f64], epsilon: f64) -> Vec<f64>
where
F: Fn(&[f64]) -> f64,
{
let mut gradient = Vec::with_capacity(x.len());
for i in 0..x.len() {
let mut x_plus = x.to_vec();
let mut x_minus = x.to_vec();
x_plus[i] += epsilon;
x_minus[i] -= epsilon;
let grad_i = (f(&x_plus) - f(&x_minus)) / (2.0 * epsilon);
gradient.push(grad_i);
}
gradient
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quadratic() {
let f = |x: &[f64]| (x[0] - 3.0) * (x[0] - 3.0);
let grad_f = |x: &[f64]| vec![2.0 * (x[0] - 3.0)];
let gd = GradientDescent::new()
.with_learning_rate(LearningRate::Constant(0.1))
.with_max_iterations(1000);
let result = gd.minimize(f, grad_f, &[0.0]).unwrap();
assert!(result.converged);
assert!((result.parameters[0] - 3.0).abs() < 0.01);
}
#[test]
fn test_rosenbrock() {
let f = |x: &[f64]| {
let a = 1.0 - x[0];
let b = x[1] - x[0] * x[0];
a * a + 100.0 * b * b
};
let grad_f = |x: &[f64]| {
vec![
-2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0] * x[0]),
200.0 * (x[1] - x[0] * x[0]),
]
};
let gd = GradientDescent::new()
.with_learning_rate(LearningRate::Constant(0.001))
.with_momentum(0.9)
.with_max_iterations(10000);
let result = gd.minimize(f, grad_f, &[0.0, 0.0]).unwrap();
assert!(result.value < 0.1);
}
#[test]
fn test_numerical_gradient() {
let f = |x: &[f64]| x[0] * x[0] + 2.0 * x[1] * x[1];
let num_grad = numerical_gradient(f, &[1.0, 2.0], 1e-5);
let analytical_grad = vec![2.0 * 1.0, 4.0 * 2.0];
for (n, a) in num_grad.iter().zip(analytical_grad.iter()) {
assert!((n - a).abs() < 1e-4);
}
}
}