#[cfg(not(feature="gurobi"))]
use super::perturbed_lp_model::LPModel;
#[cfg(feature="gurobi")]
use super::gurobi_lp_model::LPModel;
use crate::{
Sample,
Booster,
WeakLearner,
Classifier,
WeightedMajority,
common::{
utils,
checker,
frank_wolfe::{FrankWolfe, FWType},
},
research::Research,
};
use std::mem;
use std::cell::RefCell;
use std::ops::ControlFlow;
pub struct MLPBoost<'a, F> {
sample: &'a Sample,
half_tolerance: f64,
n_sample: usize,
nu: f64,
eta: f64,
primary: FrankWolfe,
secondary: Option<RefCell<LPModel>>,
weights: Vec<f64>,
hypotheses: Vec<F>,
terminated: usize,
max_iter: usize,
gamma: f64,
}
impl<'a, F> MLPBoost<'a, F> {
pub fn init(sample: &'a Sample) -> Self {
let n_sample = sample.shape().0;
assert!(n_sample != 0);
let half_tolerance = 0.005;
let nu = 1.0;
let eta = (n_sample as f64 / nu).ln() / half_tolerance;
let primary = FrankWolfe::new(eta, nu, FWType::ShortStep);
Self {
sample,
half_tolerance,
n_sample,
nu,
eta,
primary,
secondary: None,
weights: Vec::new(),
hypotheses: Vec::new(),
terminated: usize::MAX,
max_iter: usize::MAX,
gamma: 1.0,
}
}
pub fn nu(mut self, nu: f64) -> Self {
assert!(1.0 <= nu && nu <= self.n_sample as f64);
self.nu = nu;
self.primary.nu(self.nu);
self
}
pub fn frank_wolfe(mut self, fw_type: FWType) -> Self {
self.primary.fw_type(fw_type);
self
}
#[inline(always)]
pub fn tolerance(mut self, tolerance: f64) -> Self {
self.half_tolerance = tolerance / 2.0;
self
}
#[inline(always)]
fn eta(&mut self) {
let ln_m = (self.n_sample as f64 / self.nu).ln();
self.eta = ln_m / self.half_tolerance;
self.primary.eta(self.eta);
}
fn init_solver(&mut self) {
let ub = 1.0 / self.nu;
let lp_model = RefCell::new(LPModel::init(self.eta, self.n_sample, ub));
self.secondary = Some(lp_model);
}
fn init_params(&mut self) {
self.eta();
self.init_solver();
}
pub fn max_loop(&self) -> usize {
let ln_m = (self.n_sample as f64 / self.nu).ln();
(8.0_f64 * ln_m / self.half_tolerance.powi(2)).ceil() as usize
}
pub fn terminated(&self) -> usize {
self.terminated
}
}
impl<F> MLPBoost<'_, F>
where F: Classifier,
{
fn secondary_update(&self, opt_h: Option<&F>) -> Vec<f64> {
self.secondary.as_ref()
.expect("Failed to call `.as_ref()` to `self.secondary`")
.borrow_mut()
.update(self.sample, opt_h)
}
fn objval(&self, weights: &[f64]) -> f64 {
let dist = utils::exp_distribution(
self.eta, self.nu, self.sample, weights, &self.hypotheses,
);
let edge = utils::edge_of_weighted_hypothesis(
self.sample, &dist[..], weights, &self.hypotheses[..],
);
let entropy = utils::entropy_from_uni_distribution(&dist[..]);
edge + (entropy / self.eta)
}
fn better_weight(&mut self, w1: Vec<f64>, w2: Vec<f64>)
{
let v1 = self.objval(&w1[..]);
let v2 = self.objval(&w2[..]);
self.weights = if v1 >= v2 { w1 } else { w2 };
}
}
impl<F> Booster<F> for MLPBoost<'_, F>
where F: Classifier + Clone + PartialEq,
{
type Output = WeightedMajority<F>;
fn name(&self) -> &str {
"MLPBoost"
}
fn info(&self) -> Option<Vec<(&str, String)>> {
let (n_sample, n_feature) = self.sample.shape();
let ratio = self.nu * 100f64 / n_sample as f64;
let nu = utils::format_unit(self.nu);
let info = Vec::from([
("# of examples", format!("{n_sample}")),
("# of features", format!("{n_feature}")),
("Tolerance", format!("{}", 2f64 * self.half_tolerance)),
("Max iteration", format!("{}", self.max_iter)),
("Capping (outliers)", format!("{nu} ({ratio: >7.3} %)")),
("Primary", format!("{}", self.primary.current_type())),
("Secondary", format!("LPBoost"))
]);
Some(info)
}
fn preprocess<W>(
&mut self,
_weak_learner: &W,
)
where W: WeakLearner<Hypothesis = F>
{
self.sample.is_valid_binary_instance();
self.n_sample = self.sample.shape().0;
self.init_params();
self.max_iter = self.max_loop();
self.terminated = self.max_iter;
self.hypotheses = Vec::new();
self.weights = Vec::new();
self.gamma = 1.0;
}
fn boost<W>(
&mut self,
weak_learner: &W,
iteration: usize,
) -> ControlFlow<usize>
where W: WeakLearner<Hypothesis = F>,
{
if self.max_iter < iteration {
return ControlFlow::Break(self.max_iter);
}
let dist = utils::exp_distribution(
self.eta, self.nu,
self.sample, &self.weights[..], &self.hypotheses[..],
);
let h = weak_learner.produce(self.sample, &dist);
let edge_h = utils::edge_of_hypothesis(self.sample, &dist, &h);
self.gamma = self.gamma.min(edge_h);
if iteration == 1 {
self.hypotheses.push(h);
self.weights.push(1.0_f64);
let _ = self.secondary_update(self.hypotheses.last());
return ControlFlow::Continue(())
}
let objval = self.objval(&self.weights[..]);
if self.gamma - objval <= self.half_tolerance {
self.terminated = iteration;
return ControlFlow::Break(self.terminated);
}
let mut opt_h = None;
let pos = self.hypotheses.iter()
.position(|f| *f == h)
.unwrap_or(self.hypotheses.len());
if pos == self.hypotheses.len() {
self.hypotheses.push(h);
self.weights.push(0.0);
opt_h = self.hypotheses.last();
}
let weights = mem::take(&mut self.weights);
let prim = self.primary.next_iterate(
iteration, self.sample, &dist[..],
&self.hypotheses[..], pos, weights,
);
let seco = self.secondary_update(opt_h);
self.better_weight(prim, seco);
checker::check_capped_simplex_condition(&self.weights[..], 1.0);
ControlFlow::Continue(())
}
fn postprocess<W>(
&mut self,
_weak_learner: &W,
) -> Self::Output
where W: WeakLearner<Hypothesis = F>
{
WeightedMajority::from_slices(&self.weights[..], &self.hypotheses[..])
}
}
impl<H> Research for MLPBoost<'_, H>
where H: Classifier + Clone,
{
type Output = WeightedMajority<H>;
fn current_hypothesis(&self) -> Self::Output {
WeightedMajority::from_slices(&self.weights[..], &self.hypotheses[..])
}
}