use crate::core::*;
use crate::math::LbfgsMath;
use crate::line::*;
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct LbfgsParam {
pub m: usize,
pub epsilon: f64,
pub past: usize,
pub delta: f64,
pub max_iterations: usize,
pub max_evaluations: usize,
pub linesearch: LineSearch,
pub orthantwise: bool,
pub owlqn: Orthantwise,
pub initial_inverse_hessian: f64,
pub max_step_size: f64,
pub damping: bool,
}
impl Default for LbfgsParam {
fn default() -> Self {
LbfgsParam {
m: 6,
epsilon: 1e-5,
past: 0,
delta: 1e-5,
max_iterations: 0,
max_evaluations: 0,
orthantwise: false,
owlqn: Orthantwise::default(),
linesearch: LineSearch::default(),
initial_inverse_hessian: 1.0,
max_step_size: 1.0,
damping: false,
}
}
}
pub struct Problem<'a, E>
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
pub x: &'a mut [f64],
pub fx: f64,
pub gx: Vec<f64>,
xp: Vec<f64>,
gp: Vec<f64>,
pg: Vec<f64>,
d: Vec<f64>,
eval_fn: E,
owlqn: Option<Orthantwise>,
evaluated: bool,
neval: usize,
}
impl<'a, E> Problem<'a, E>
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
pub fn new(x: &'a mut [f64], eval: E, owlqn: Option<Orthantwise>) -> Self {
let n = x.len();
Problem {
fx: 0.0,
gx: vec![0.0; n],
xp: vec![0.0; n],
gp: vec![0.0; n],
pg: vec![0.0; n],
d: vec![0.0; n],
evaluated: false,
neval: 0,
x,
eval_fn: eval,
owlqn,
}
}
pub fn dginit(&self) -> Result<f64> {
if self.owlqn.is_none() {
let dginit = self.gx.vecdot(&self.d);
if dginit > 0.0 {
warn!(
"The current search direction increases the objective function value. dginit = {:-0.4}",
dginit
);
}
Ok(dginit)
} else {
Ok(self.pg.vecdot(&self.d))
}
}
pub fn update_search_direction(&mut self) {
if self.owlqn.is_some() {
self.d.vecncpy(&self.pg);
} else {
self.d.vecncpy(&self.gx);
}
}
pub fn search_direction(&self) -> &[f64] {
&self.d
}
pub fn search_direction_mut(&mut self) -> &mut [f64] {
&mut self.d
}
pub fn dg_unchecked(&self) -> f64 {
self.gx.vecdot(&self.d)
}
pub fn evaluate(&mut self) -> Result<()> {
self.fx = (self.eval_fn)(&self.x, &mut self.gx)?;
if let Some(owlqn) = self.owlqn {
self.fx += owlqn.x1norm(&self.x)
}
self.evaluated = true;
self.neval += 1;
Ok(())
}
pub fn number_of_evaluation(&self) -> usize {
self.neval
}
pub fn evaluated(&self) -> bool {
self.evaluated
}
pub fn clone_from(&mut self, src: &Problem<E>) {
self.x.clone_from_slice(&src.x);
self.gx.clone_from_slice(&src.gx);
self.fx = src.fx;
}
pub fn take_line_step(&mut self, step: f64) {
self.x.veccpy(&self.xp);
self.x.vecadd(&self.d, step);
if let Some(owlqn) = self.owlqn {
owlqn.project(&mut self.x, &self.xp, &self.gp);
}
}
pub fn gnorm(&self) -> f64 {
if self.owlqn.is_some() {
self.pg.vec2norm()
} else {
self.gx.vec2norm()
}
}
pub fn xnorm(&self) -> f64 {
self.x.vec2norm()
}
pub fn orthantwise(&self) -> bool {
self.owlqn.is_some()
}
pub fn revert(&mut self) {
self.x.veccpy(&self.xp);
self.gx.veccpy(&self.gp);
}
pub fn save_state(&mut self) {
self.xp.veccpy(&self.x);
self.gp.veccpy(&self.gx);
}
pub fn constrain_search_direction(&mut self) {
if let Some(owlqn) = self.owlqn {
owlqn.constrain(&mut self.d, &self.pg);
}
}
pub fn update_owlqn_gradient(&mut self) {
if let Some(owlqn) = self.owlqn {
owlqn.pseudo_gradient(&mut self.pg, &self.x, &self.gx);
}
}
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct Progress<'a> {
pub x: &'a [f64],
pub gx: &'a [f64],
pub fx: f64,
pub xnorm: f64,
pub gnorm: f64,
pub step: f64,
pub niter: usize,
pub neval: usize,
pub ncall: usize,
}
impl<'a> Progress<'a> {
fn new<E>(prb: &'a Problem<E>, niter: usize, ncall: usize, step: f64) -> Self
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
Progress {
x: &prb.x,
gx: &prb.gx,
fx: prb.fx,
xnorm: prb.xnorm(),
gnorm: prb.gnorm(),
neval: prb.number_of_evaluation(),
ncall,
step,
niter,
}
}
}
pub struct Report {
pub fx: f64,
pub xnorm: f64,
pub gnorm: f64,
pub neval: usize,
}
impl Report {
fn new<E>(prb: &Problem<E>) -> Self
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
Self {
fx: prb.fx,
xnorm: prb.xnorm(),
gnorm: prb.gnorm(),
neval: prb.number_of_evaluation(),
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct Orthantwise {
pub c: f64,
pub start: i32,
pub end: i32,
}
impl Default for Orthantwise {
fn default() -> Self {
Orthantwise {
c: 1.0,
start: 0,
end: -1,
}
}
}
impl Orthantwise {
fn start_end(&self, x: &[f64]) -> (usize, usize) {
let start = self.start as usize;
let end = if self.end < 0 {
x.len()
} else {
self.end as usize
};
(start, end)
}
fn x1norm(&self, x: &[f64]) -> f64 {
let (start, end) = self.start_end(x);
let mut s = 0.0;
for i in start..end {
s += self.c * x[i].abs();
}
s
}
fn pseudo_gradient(&self, pg: &mut [f64], x: &[f64], g: &[f64]) {
let (start, end) = self.start_end(x);
let c = self.c;
for i in 0..start {
pg[i] = g[i];
}
for i in start..end {
if x[i] < 0.0 {
pg[i] = g[i] - c;
} else if 0.0 < x[i] {
pg[i] = g[i] + c;
} else {
if g[i] < -c {
pg[i] = g[i] + c;
} else if c < g[i] {
pg[i] = g[i] - c;
} else {
pg[i] = 0.;
}
}
}
for i in end..g.len() {
pg[i] = g[i];
}
}
fn project(&self, x: &mut [f64], xp: &[f64], gp: &[f64]) {
let (start, end) = self.start_end(xp);
for i in start..end {
let sign = if xp[i] == 0.0 { -gp[i] } else { xp[i] };
if x[i] * sign <= 0.0 {
x[i] = 0.0
}
}
}
fn constrain(&self, d: &mut [f64], pg: &[f64]) {
let (start, end) = self.start_end(pg);
for i in start..end {
if d[i] * pg[i] >= 0.0 {
d[i] = 0.0;
}
}
}
}
#[derive(Default, Debug, Clone)]
pub struct Lbfgs {
param: LbfgsParam,
}
impl Lbfgs {
pub fn with_epsilon(mut self, epsilon: f64) -> Self {
assert!(epsilon.is_sign_positive(), "Invalid parameter epsilon specified.");
self.param.epsilon = epsilon;
self
}
pub fn with_initial_step_size(mut self, b: f64) -> Self {
assert!(
b.is_sign_positive(),
"Invalid beta parameter for scaling the initial step size."
);
self.param.initial_inverse_hessian = b;
self
}
pub fn with_max_step_size(mut self, s: f64) -> Self {
assert!(s.is_sign_positive(), "Invalid max_step_size parameter.");
self.param.max_step_size = s;
self
}
pub fn with_damping(mut self, damped: bool) -> Self {
self.param.damping = damped;
self
}
pub fn with_orthantwise(mut self, c: f64, start: usize, end: usize) -> Self {
assert!(
c.is_sign_positive(),
"Invalid parameter orthantwise c parameter specified."
);
warn!("Only the backtracking line search is available for OWL-QN algorithm.");
self.param.orthantwise = true;
self.param.owlqn.c = c;
self.param.owlqn.start = start as i32;
self.param.owlqn.end = end as i32;
self
}
pub fn with_linesearch_ftol(mut self, ftol: f64) -> Self {
assert!(ftol >= 0.0, "Invalid parameter ftol specified.");
self.param.linesearch.ftol = ftol;
self
}
pub fn with_linesearch_gtol(mut self, gtol: f64) -> Self {
assert!(
gtol >= 0.0 && gtol < 1.0 && gtol > self.param.linesearch.ftol,
"Invalid parameter gtol specified."
);
self.param.linesearch.gtol = gtol;
self
}
pub fn with_gradient_only(mut self) -> Self {
self.param.linesearch.gradient_only = true;
self.param.damping = true;
self.param.linesearch.algorithm = LineSearchAlgorithm::BacktrackingStrongWolfe;
self
}
pub fn with_max_linesearch(mut self, n: usize) -> Self {
self.param.linesearch.max_linesearch = n;
self
}
pub fn with_linesearch_xtol(mut self, xtol: f64) -> Self {
assert!(xtol >= 0.0, "Invalid parameter xtol specified.");
self.param.linesearch.xtol = xtol;
self
}
pub fn with_linesearch_min_step(mut self, min_step: f64) -> Self {
assert!(min_step >= 0.0, "Invalid parameter min_step specified.");
self.param.linesearch.min_step = min_step;
self
}
pub fn with_max_iterations(mut self, niter: usize) -> Self {
self.param.max_iterations = niter;
self
}
pub fn with_max_evaluations(mut self, neval: usize) -> Self {
self.param.max_evaluations = neval;
self
}
pub fn with_fx_delta(mut self, delta: f64, past: usize) -> Self {
assert!(delta >= 0.0, "Invalid parameter delta specified.");
self.param.past = past;
self.param.delta = delta;
self
}
pub fn with_linesearch_algorithm(mut self, algo: &str) -> Self {
match algo {
"MoreThuente" => self.param.linesearch.algorithm = LineSearchAlgorithm::MoreThuente,
"BacktrackingArmijo" => self.param.linesearch.algorithm = LineSearchAlgorithm::BacktrackingArmijo,
"BacktrackingStrongWolfe" => self.param.linesearch.algorithm = LineSearchAlgorithm::BacktrackingStrongWolfe,
"BacktrackingWolfe" | "Backtracking" => {
self.param.linesearch.algorithm = LineSearchAlgorithm::BacktrackingWolfe
}
_ => unimplemented!(),
}
self
}
}
impl Lbfgs {
pub fn minimize<E, G>(self, x: &mut [f64], eval_fn: E, mut prgr_fn: G) -> Result<Report>
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
G: FnMut(&Progress) -> bool,
{
let mut state = self.build(x, eval_fn)?;
info!("start lbfgs loop...");
for _ in 0.. {
if state.is_converged() {
break;
}
let prgr = state.get_progress();
let cancel = prgr_fn(&prgr);
if cancel {
info!("The minimization process has been canceled.");
break;
}
state.propagate()?;
}
Ok(state.report())
}
}
pub struct LbfgsState<'a, E>
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
vars: LbfgsParam,
prbl: Option<Problem<'a, E>>,
end: usize,
step: f64,
k: usize,
lm_arr: Vec<IterationData>,
pf: Vec<f64>,
ncall: usize,
}
impl Lbfgs {
pub fn build<'a, E>(self, x: &'a mut [f64], eval_fn: E) -> Result<LbfgsState<'a, E>>
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
let param = &self.param;
let lm_arr = (0..param.m).map(|_| IterationData::new(x.len())).collect();
let owlqn = if param.orthantwise {
Some(param.owlqn.clone())
} else {
None
};
let mut problem = Problem::new(x, eval_fn, owlqn);
problem.evaluate()?;
problem.update_owlqn_gradient();
problem.update_search_direction();
let h0 = param.initial_inverse_hessian;
let step = problem.search_direction().vec2norminv() * h0;
let damping = param.damping;
if damping {
info!("Powell damping Enabled.");
}
let state = LbfgsState {
vars: self.param.clone(),
prbl: Some(problem),
end: 0,
step,
k: 0,
lm_arr,
pf: vec![],
ncall: 0,
};
Ok(state)
}
}
impl<'a, E> LbfgsState<'a, E>
where
E: FnMut(&[f64], &mut [f64]) -> Result<f64>,
{
pub fn is_converged(&mut self) -> bool {
let prgr = self.get_progress();
let converged = satisfying_stop_conditions(&self.vars, prgr);
converged
}
pub fn report(&self) -> Report {
Report::new(self.prbl.as_ref().expect("problem for report"))
}
pub fn propagate(&mut self) -> Result<Progress> {
self.k += 1;
if self.k == 1 {
let progress = self.get_progress();
return Ok(progress);
}
let problem = self.prbl.as_mut().expect("problem for propagate");
problem.save_state();
self.ncall = self
.vars
.linesearch
.find(problem, &mut self.step)
.context("Failure during line search")?;
problem.update_owlqn_gradient();
let it = &mut self.lm_arr[self.end];
let gamma = it.update(
&problem.x,
&problem.xp,
&problem.gx,
&problem.gp,
self.step,
self.vars.damping,
);
problem.update_search_direction();
let d = problem.search_direction_mut();
self.end = lbfgs_two_loop_recursion(&mut self.lm_arr, d, gamma, self.vars.m, self.k - 1, self.end);
let dnorm = d.vec2norm();
self.step = self.vars.max_step_size.min(dnorm) / dnorm;
problem.constrain_search_direction();
let progress = self.get_progress();
Ok(progress)
}
fn get_progress(&self) -> Progress {
let problem = self.prbl.as_ref().expect("problem for progress");
Progress::new(&problem, self.k, self.ncall, self.step)
}
}
fn lbfgs_two_loop_recursion(
lm_arr: &mut [IterationData],
d: &mut [f64], gamma: f64, m: usize,
k: usize,
end: usize,
) -> usize {
let end = (end + 1) % m;
let mut j = end;
let bound = m.min(k);
for _ in 0..bound {
j = (j + m - 1) % m;
let it = &mut lm_arr[j as usize];
it.alpha = it.s.vecdot(&d) / it.ys;
d.vecadd(&it.y, -it.alpha);
}
d.vecscale(gamma);
for _ in 0..bound {
let it = &mut lm_arr[j as usize];
let beta = it.y.vecdot(d) / it.ys;
d.vecadd(&it.s, it.alpha - beta);
j = (j + 1) % m;
}
end
}
#[derive(Clone)]
struct IterationData {
alpha: f64,
s: Vec<f64>,
y: Vec<f64>,
ys: f64,
}
impl IterationData {
fn new(n: usize) -> Self {
IterationData {
alpha: 0.0,
ys: 0.0,
s: vec![0.0; n],
y: vec![0.0; n],
}
}
fn update(&mut self, x: &[f64], xp: &[f64], gx: &[f64], gp: &[f64], step: f64, damping: bool) -> f64 {
self.s.vecdiff(x, xp);
self.y.vecdiff(gx, gp);
let ys = self.y.vecdot(&self.s);
let yy = self.y.vecdot(&self.y);
self.ys = ys;
let sigma2 = 0.6;
let sigma3 = 3.0;
if damping {
debug!("Applying Powell damping, sigma2 = {}, sigma3 = {}", sigma2, sigma3);
let mut bs = gp.to_vec();
bs.vecscale(-step);
let sbs = self.s.vecdot(&bs);
if ys < (1.0 - sigma2) * sbs {
trace!("damping case1");
let theta = sigma2 * sbs / (sbs - ys);
bs.vecscale(1.0 - theta);
bs.vecadd(&self.y, theta);
self.y.veccpy(&bs);
} else if ys > (1.0 + sigma3) * sbs {
trace!("damping case2");
let theta = sigma3 * sbs / (ys - sbs);
bs.vecscale(1.0 - theta);
bs.vecadd(&self.y, theta);
} else {
trace!("damping case3");
}
}
ys / yy
}
}
#[inline]
fn satisfying_stop_conditions(param: &LbfgsParam, prgr: Progress) -> bool {
if satisfying_max_iterations(&prgr, param.max_iterations)
|| satisfying_max_evaluations(&prgr, param.max_evaluations)
|| satisfying_scaled_gnorm(&prgr, param.epsilon)
{
return true;
}
false
}
#[inline]
fn satisfying_scaled_gnorm(prgr: &Progress, epsilon: f64) -> bool {
if prgr.gnorm / prgr.xnorm.max(1.0) <= epsilon {
info!("L-BFGS reaches convergence.");
true
} else {
false
}
}
#[inline]
fn satisfying_max_iterations(prgr: &Progress, max_iterations: usize) -> bool {
if max_iterations == 0 {
false
} else if prgr.niter >= max_iterations {
warn!("max iterations reached!");
true
} else {
false
}
}
#[inline]
fn satisfying_max_evaluations(prgr: &Progress, max_evaluations: usize) -> bool {
if max_evaluations == 0 {
false
} else if prgr.neval >= max_evaluations {
warn!("Max allowed evaluations reached!");
true
} else {
false
}
}
#[inline]
fn satisfying_max_gnorm(prgr: &Progress, max_gnorm: f64) -> bool {
prgr.gx.vec2norm() <= max_gnorm
}
#[inline]
fn satisfying_delta<'a>(prgr: &Progress, pf: &'a mut [f64], delta: f64) -> bool {
let k = prgr.niter;
let fx = prgr.fx;
let past = pf.len();
if past < 1 {
return false;
}
if past <= k {
let rate = (pf[(k % past) as usize] - fx).abs() / fx;
if rate < delta {
info!("The stopping criterion.");
return true;
}
}
pf[(k % past) as usize] = fx;
false
}