use crate::{
Sample,
Booster,
WeakLearner,
State,
Classifier,
CombinedHypothesis,
common::utils,
research::Research,
};
use grb::prelude::*;
pub struct SoftBoost<'a, F> {
sample: &'a Sample,
pub(crate) dist: Vec<f64>,
gamma_hat: f64,
tolerance: f64,
sub_tolerance: f64,
nu: f64,
env: Env,
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 mut env = Env::new("").unwrap();
env.set(param::OutputFlag, 0).unwrap();
let uni = 1.0 / n_sample as f64;
let dist = vec![uni; n_sample];
let tolerance = uni;
let gamma_hat = 1.0;
SoftBoost {
sample,
dist,
gamma_hat,
tolerance,
sub_tolerance: 1e-6,
nu: 1.0,
env,
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
}
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
}
pub fn opt_val(&self) -> f64 {
self.gamma_hat
}
}
impl<F> SoftBoost<'_, F>
where F: Classifier,
{
fn set_weights(&self)
-> std::result::Result<Vec<f64>, grb::Error>
{
let mut model = Model::with_env("", &self.env)?;
let n_sample = self.sample.shape().0;
let n_hypotheses = self.hypotheses.len();
let wt_vec = (0..n_hypotheses).map(|i| {
let name = format!("w[{i}]");
add_ctsvar!(model, name: &name, bounds: 0_f64..).unwrap()
}).collect::<Vec<_>>();
let xi_vec = (0..n_sample).map(|i| {
let name = format!("xi[{i}]");
add_ctsvar!(model, name: &name, bounds: 0.0_f64..).unwrap()
}).collect::<Vec<_>>();
let rho = add_ctsvar!(model, name: "rho", bounds: ..)?;
let target = self.sample.target();
let iter = target.into_iter()
.zip(xi_vec.iter())
.enumerate();
for (i, (&y, &xi)) in iter {
let expr = wt_vec.iter()
.zip(&self.hypotheses[..])
.map(|(&w, h)| w * h.confidence(self.sample, i))
.grb_sum();
let name = format!("sample[{i}]");
model.add_constr(&name, c!(y * expr >= rho - xi))?;
}
model.add_constr(
"sum_is_1", c!(wt_vec.iter().grb_sum() == 1.0)
)?;
model.update()?;
let param = 1.0 / self.nu;
let objective = rho - param * xi_vec.iter().grb_sum();
model.set_objective(objective, Maximize)?;
model.update()?;
model.optimize()?;
let status = model.status()?;
if status != Status::Optimal {
panic!("Cannot solve the primal problem. Status: {status:?}");
}
let weights = wt_vec.into_iter()
.map(|w| model.get_obj_attr(attr::X, &w).unwrap())
.collect::<Vec<_>>();
Ok(weights)
}
fn update_params_mut(&mut self) -> Option<()> {
loop {
let mut model = Model::with_env("", &self.env).unwrap();
let cap = 1.0 / self.nu;
let vars = self.dist.iter()
.copied()
.enumerate()
.map(|(i, d)| {
let lb = - d;
let ub = cap - d;
let name = format!("delta[{i}]");
add_ctsvar!(model, name: &name, bounds: lb..ub)
.unwrap()
})
.collect::<Vec<Var>>();
model.update().unwrap();
self.hypotheses.iter()
.enumerate()
.for_each(|(j, h)| {
let expr = vars.iter()
.zip(self.dist.iter().copied())
.zip(self.sample.target().into_iter())
.enumerate()
.map(|(i, ((v, d), y))| {
let p = h.confidence(self.sample, i);
y * p * (d + *v)
})
.grb_sum();
let name = format!("h[{j}]");
model.add_constr(
&name, c!(expr <= self.gamma_hat - self.tolerance)
).unwrap();
});
model.add_constr(
"zero_sum", c!(vars.iter().grb_sum() == 0.0)
).unwrap();
model.update().unwrap();
let n_sample = self.sample.shape().0 as f64;
let objective = self.dist.iter()
.zip(vars.iter())
.map(|(&d, &v)| {
let lin_coef = (n_sample * d).ln() + 1.0;
lin_coef * v + (v * v) * (1.0 / (2.0 * d))
})
.grb_sum();
model.set_objective(objective, Minimize).unwrap();
model.update().unwrap();
model.optimize().unwrap();
let status = model.status().unwrap();
if status == Status::Infeasible || status == Status::InfOrUnbd {
return None;
}
if status != Status::Optimal {
panic!("Status is {status:?}. something wrong.");
}
let mut l2 = 0.0;
for (v, d) in vars.iter().zip(self.dist.iter_mut()) {
let val = model.get_obj_attr(attr::X, v).unwrap();
*d += val;
l2 += val * val;
}
let l2 = l2.sqrt();
if l2 < self.sub_tolerance {
break;
}
}
if self.dist.iter().any(|&d| d == 0.0) {
return None;
}
Some(())
}
}
impl<F> Booster<F> for SoftBoost<'_, F>
where F: Classifier + Clone,
{
fn preprocess<W>(
&mut self,
_weak_learner: &W,
)
where W: WeakLearner<Hypothesis = F>
{
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;
}
fn boost<W>(
&mut self,
weak_learner: &W,
iteration: usize,
) -> State
where W: WeakLearner<Hypothesis = F>,
{
if self.max_iter < iteration {
return State::Terminate;
}
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 State::Terminate;
}
State::Continue
}
fn postprocess<W>(
&mut self,
_weak_learner: &W,
) -> CombinedHypothesis<F>
where W: WeakLearner<Hypothesis = F>
{
self.weights = self.set_weights().unwrap();
CombinedHypothesis::from_slices(&self.weights[..], &self.hypotheses[..])
}
}
impl<H> Research<H> for SoftBoost<'_, H>
where H: Classifier + Clone,
{
fn current_hypothesis(&self) -> CombinedHypothesis<H> {
let weights = self.set_weights().unwrap();
CombinedHypothesis::from_slices(&weights[..], &self.hypotheses[..])
}
}