#[cfg(not(feature="gurobi"))]
use super::qp_model::QPModel;
#[cfg(feature="gurobi")]
use super::gurobi_qp_model::QPModel;
use crate::{
Sample,
Booster,
WeakLearner,
Classifier,
WeightedMajority,
common::utils,
common::checker,
research::Research,
};
use std::cell::RefCell;
use std::ops::ControlFlow;
pub struct SoftBoost<'a, F> {
sample: &'a Sample,
pub(crate) dist: Vec<f64>,
gamma_hat: f64,
tolerance: f64,
sub_tolerance: f64,
nu: f64,
qp_model: Option<RefCell<QPModel>>,
hypotheses: Vec<F>,
max_iter: usize,
terminated: usize,
weights: Vec<f64>,
}
impl<'a, F> SoftBoost<'a, F>
where F: Classifier
{
pub fn init(sample: &'a Sample) -> Self {
let n_sample = sample.shape().0;
assert!(n_sample != 0);
let uni = 1.0 / n_sample as f64;
let dist = vec![uni; n_sample];
let tolerance = uni;
SoftBoost {
sample,
dist,
gamma_hat: 1.0,
tolerance,
sub_tolerance: 1e-6,
nu: 1.0,
qp_model: None,
hypotheses: Vec::new(),
weights: Vec::new(),
max_iter: usize::MAX,
terminated: usize::MAX,
}
}
#[inline(always)]
pub fn nu(mut self, nu: f64) -> Self {
let n_sample = self.sample.shape().0 as f64;
assert!((1.0..=n_sample).contains(&nu));
self.nu = nu;
self
}
#[inline(always)]
pub fn tolerance(mut self, tolerance: f64) -> Self {
self.tolerance = tolerance;
self
}
fn init_solver(&mut self) {
let n_sample = self.sample.shape().0;
checker::check_nu(self.nu, n_sample);
let upper_bound = 1.0 / self.nu;
let qp_model = RefCell::new(QPModel::init(n_sample, upper_bound));
self.qp_model = Some(qp_model);
}
pub fn max_loop(&mut self) -> usize {
let n_sample = self.sample.shape().0 as f64;
let temp = (n_sample / self.nu).ln();
let max_iter = 2.0 * temp / self.tolerance.powi(2);
max_iter.ceil() as usize
}
}
impl<F> SoftBoost<'_, F>
where F: Classifier,
{
fn set_weights(&self)
-> std::result::Result<Vec<f64>, ()>
{
let weights = self.qp_model.as_ref()
.expect("Failed to call `.as_ref()` to `self.qp_model`")
.borrow_mut()
.weights(self.sample, &self.hypotheses)
.collect::<Vec<_>>();
Ok(weights)
}
fn update_params_mut(&mut self) -> Option<()> {
let h = self.hypotheses.last().unwrap();
self.qp_model.as_ref()
.expect("Failed to call `.as_ref()` to `self.qp_model`")
.borrow_mut()
.update(self.sample, &mut self.dist[..], self.gamma_hat, h)
}
}
impl<F> Booster<F> for SoftBoost<'_, F>
where F: Classifier + Clone,
{
type Output = WeightedMajority<F>;
fn name(&self) -> &str {
"SoftBoost"
}
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!("{}", self.tolerance)),
("Tolerance (sub-problem)", format!("{}", self.sub_tolerance)),
("Max iteration", format!("{}", self.max_iter)),
("Capping (outliers)", format!("{nu} ({ratio: >7.3} %)"))
]);
Some(info)
}
fn preprocess<W>(
&mut self,
_weak_learner: &W,
)
where W: WeakLearner<Hypothesis = F>
{
self.sample.is_valid_binary_instance();
let n_sample = self.sample.shape().0;
let uni = 1.0 / n_sample as f64;
self.dist = vec![uni; n_sample];
self.sub_tolerance = self.tolerance / 10.0;
self.max_iter = self.max_loop();
self.terminated = self.max_iter;
self.hypotheses = Vec::new();
self.gamma_hat = 1.0;
self.init_solver();
}
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 h = weak_learner.produce(self.sample, &self.dist);
let edge = utils::edge_of_hypothesis(self.sample, &self.dist, &h);
if self.gamma_hat > edge {
self.gamma_hat = edge;
}
self.hypotheses.push(h);
if self.update_params_mut().is_none() {
self.terminated = iteration;
return ControlFlow::Break(self.terminated);
}
ControlFlow::Continue(())
}
fn postprocess<W>(
&mut self,
_weak_learner: &W,
) -> Self::Output
where W: WeakLearner<Hypothesis = F>
{
self.weights = self.set_weights()
.expect("Failed to solve the LP");
WeightedMajority::from_slices(&self.weights[..], &self.hypotheses[..])
}
}
impl<H> Research for SoftBoost<'_, H>
where H: Classifier + Clone,
{
type Output = WeightedMajority<H>;
fn current_hypothesis(&self) -> Self::Output {
let weights = self.set_weights()
.expect("Failed to solve the LP");
WeightedMajority::from_slices(&weights[..], &self.hypotheses[..])
}
}