use crate::core::inner::{InitialState, WarmStart};
use crate::core::math::{
NegInPlace, Scalar, ScaleInPlace, ScaledAdd, VectorLen,
};
use crate::core::problem::{CostFunction, Gradient, Problem};
use crate::core::solver::Solver;
use crate::core::state::{FirstOrderState, State};
use crate::core::termination::Termination;
use crate::line_search::{Constant, LineSearch, LineSearchOutcome};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GradientDescent<L, V, F: Scalar = f64> {
line_search: L,
beta: F,
velocity: Option<V>,
}
impl<V, F: Scalar> GradientDescent<Constant<F>, V, F> {
pub fn new(alpha: F) -> Self {
Self {
line_search: Constant(alpha),
beta: F::zero(),
velocity: None,
}
}
}
impl<L, V, F: Scalar> GradientDescent<L, V, F> {
pub fn with_line_search(line_search: L) -> Self {
Self {
line_search,
beta: F::zero(),
velocity: None,
}
}
pub fn with_momentum(mut self, beta: F) -> Self {
self.beta = beta;
self
}
}
impl<P, V, F, L> Solver<P, FirstOrderState<V, F>> for GradientDescent<L, V, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
V: ScaledAdd<F> + NegInPlace + ScaleInPlace<F> + Clone + VectorLen,
L: LineSearch<P, V, F, Error = P::Error>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, Self::Error> {
state.reset();
self.line_search.reset();
self.velocity = None;
let (cost, grad) = problem.cost_and_gradient(state.param())?;
state
.set_evaluation(cost, grad)
.expect("gradient dimension differs from parameter");
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<crate::SolverStep<FirstOrderState<V, F>>, Self::Error> {
let (prev_cost, grad) = state
.take_evaluation()
.expect("solver requires initialized progress");
let mut direction = grad.clone();
direction.neg_in_place();
let line_search_result = self.line_search.next_with_evaluation(
problem,
state.param(),
prev_cost,
&grad,
&direction,
)?;
let alpha = match line_search_result.outcome {
LineSearchOutcome::Step(alpha) => alpha,
LineSearchOutcome::Failed => {
state
.set_evaluation(prev_cost, grad)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"Gradient-descent line search failed.",
)),
)));
}
};
if self.beta == F::zero() {
if let Some(evaluation) = line_search_result.evaluation {
state
.replace(
evaluation.param,
evaluation.cost,
evaluation.gradient,
)
.expect("gradient dimension differs from parameter");
return Ok(crate::SolverStep::from((state, None)));
}
state.seed_param_mut().scaled_add(alpha, &direction);
} else {
let velocity = match self.velocity.take() {
Some(mut v) => {
v.scale_in_place(self.beta);
v.scaled_add(alpha, &direction);
v
}
None => {
direction.scale_in_place(alpha);
direction
}
};
state.seed_param_mut().scaled_add(F::one(), &velocity);
self.velocity = Some(velocity);
}
let (cost, grad) = problem.cost_and_gradient(state.param())?;
state
.set_evaluation(cost, grad)
.expect("gradient dimension differs from parameter");
Ok(crate::SolverStep::from((state, None)))
}
}
impl<L, V, F> InitialState<V> for GradientDescent<L, V, F>
where
F: Scalar,
V: Clone,
{
type State = FirstOrderState<V, F>;
fn seed(&self, x: &V) -> FirstOrderState<V, F> {
FirstOrderState::new(x.clone())
}
}
impl<L, V, F> WarmStart<V> for GradientDescent<L, V, F>
where
F: Scalar,
V: Clone,
{
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::state::State;
use crate::{Executor, FirstOrderState};
struct Quadratic;
impl crate::Constraints for Quadratic {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
impl CostFunction for Quadratic {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(x.iter().map(|v| v * v).sum())
}
}
impl Gradient for Quadratic {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(x.iter().map(|v| 2.0 * v).collect())
}
}
struct IllConditioned;
impl crate::Constraints for IllConditioned {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
impl CostFunction for IllConditioned {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(x[0] * x[0] + 100.0 * x[1] * x[1])
}
}
impl Gradient for IllConditioned {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![2.0 * x[0], 200.0 * x[1]])
}
}
#[test]
fn with_momentum_zero_is_plain_descent_first_step() {
let mut solver = GradientDescent::new(0.1).with_momentum(0.0);
let mut p = Problem::new(Quadratic);
let state = solver
.init(&mut p, FirstOrderState::new(vec![1.0]))
.unwrap();
let (state, _, reason) =
solver.next_iter(&mut p, state).unwrap().into_parts();
assert!(reason.is_none());
assert!((state.param()[0] - 0.8).abs() < 1e-12);
}
#[test]
fn momentum_accelerates_over_plain_descent_when_ill_conditioned() {
let start = vec![1.0, 1.0];
let iters = 200;
let alpha = 0.004;
let plain = Executor::new(
IllConditioned,
GradientDescent::new(alpha),
FirstOrderState::new(start.clone()),
)
.max_iter(iters)
.run()
.unwrap();
let momentum = Executor::new(
IllConditioned,
GradientDescent::new(alpha).with_momentum(0.9),
FirstOrderState::new(start),
)
.max_iter(iters)
.run()
.unwrap();
assert!(
momentum.cost() < plain.cost(),
"momentum cost {} should beat plain {}",
momentum.cost(),
plain.cost()
);
}
#[test]
fn momentum_velocity_resets_between_runs() {
let start = vec![2.0, -1.0];
let mut solver = GradientDescent::new(0.05).with_momentum(0.8);
let run = |solver: &mut GradientDescent<Constant, Vec<f64>>| {
let mut p = Problem::new(Quadratic);
let mut state = solver
.init(&mut p, FirstOrderState::new(start.clone()))
.unwrap();
for _ in 0..10 {
let (next, _, _) =
solver.next_iter(&mut p, state).unwrap().into_parts();
state = next;
}
state.param().clone()
};
let first = run(&mut solver);
let second = run(&mut solver);
for (a, b) in first.iter().zip(second.iter()) {
assert!((a - b).abs() < 1e-12, "first={a}, second={b}");
}
}
}