use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use scirs2_core::random::thread_rng;
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Untrained},
types::Float,
};
#[derive(Debug, Clone)]
pub struct GradientBoostingMultiOutput<S = Untrained> {
state: S,
n_estimators: usize,
learning_rate: Float,
max_depth: usize,
min_samples_split: usize,
random_state: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct GradientBoostingMultiOutputTrained {
pub initial_predictions: Array1<Float>,
pub estimators: Vec<Vec<WeakLearner>>,
pub n_features: usize,
pub n_targets: usize,
}
#[derive(Debug, Clone)]
pub struct WeakLearner {
feature_idx: usize,
threshold: Float,
left_value: Float,
right_value: Float,
}
impl GradientBoostingMultiOutput<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
n_estimators: 100,
learning_rate: 0.1,
max_depth: 3,
min_samples_split: 2,
random_state: None,
}
}
pub fn n_estimators(mut self, n_estimators: usize) -> Self {
self.n_estimators = n_estimators;
self
}
pub fn learning_rate(mut self, learning_rate: Float) -> Self {
self.learning_rate = learning_rate;
self
}
pub fn max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
pub fn min_samples_split(mut self, min_samples_split: usize) -> Self {
self.min_samples_split = min_samples_split;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
}
impl Default for GradientBoostingMultiOutput<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for GradientBoostingMultiOutput<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for GradientBoostingMultiOutput<Untrained> {
type Fitted = GradientBoostingMultiOutput<GradientBoostingMultiOutputTrained>;
fn fit(self, x: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
let (n_samples, n_features) = x.dim();
let (y_samples, n_targets) = y.dim();
if n_samples != y_samples {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
if n_samples < self.min_samples_split {
return Err(SklearsError::InvalidInput(
"Not enough samples to perform gradient boosting".to_string(),
));
}
let mut rng = thread_rng();
let mut initial_predictions = Array1::<Float>::zeros(n_targets);
for target_idx in 0..n_targets {
let target_sum: Float = y.column(target_idx).sum();
initial_predictions[target_idx] = target_sum / n_samples as Float;
}
let mut current_predictions = Array2::<Float>::zeros((n_samples, n_targets));
for target_idx in 0..n_targets {
for sample_idx in 0..n_samples {
current_predictions[[sample_idx, target_idx]] = initial_predictions[target_idx];
}
}
let mut estimators = Vec::new();
for _stage in 0..self.n_estimators {
let mut stage_estimators = Vec::new();
for target_idx in 0..n_targets {
let mut residuals = Array1::<Float>::zeros(n_samples);
for sample_idx in 0..n_samples {
residuals[sample_idx] =
y[[sample_idx, target_idx]] - current_predictions[[sample_idx, target_idx]];
}
let weak_learner = self.train_weak_learner(x, &residuals, &mut rng)?;
let weak_predictions = self.predict_weak_learner(&weak_learner, x);
for sample_idx in 0..n_samples {
current_predictions[[sample_idx, target_idx]] +=
self.learning_rate * weak_predictions[sample_idx];
}
stage_estimators.push(weak_learner);
}
estimators.push(stage_estimators);
}
Ok(GradientBoostingMultiOutput {
state: GradientBoostingMultiOutputTrained {
initial_predictions,
estimators,
n_features,
n_targets,
},
n_estimators: self.n_estimators,
learning_rate: self.learning_rate,
max_depth: self.max_depth,
min_samples_split: self.min_samples_split,
random_state: self.random_state,
})
}
}
impl GradientBoostingMultiOutput<Untrained> {
fn train_weak_learner(
&self,
x: &ArrayView2<'_, Float>,
residuals: &Array1<Float>,
rng: &mut scirs2_core::random::CoreRandom,
) -> SklResult<WeakLearner> {
let (n_samples, n_features) = x.dim();
if n_samples < 2 {
return Err(SklearsError::InvalidInput(
"Need at least 2 samples to train weak learner".to_string(),
));
}
let mut best_loss = Float::INFINITY;
let mut best_feature = 0;
let mut best_threshold = 0.0;
let mut best_left_value = 0.0;
let mut best_right_value = 0.0;
let n_trials = (n_features * 10).min(100);
for _ in 0..n_trials {
let feature_idx = rng.gen_range(0..n_features);
let feature_values: Vec<Float> = (0..n_samples).map(|i| x[[i, feature_idx]]).collect();
let min_val = feature_values
.iter()
.cloned()
.fold(Float::INFINITY, Float::min);
let max_val = feature_values
.iter()
.cloned()
.fold(Float::NEG_INFINITY, Float::max);
if (max_val - min_val).abs() < 1e-10 {
continue; }
for _ in 0..10 {
let threshold = min_val + rng.random::<Float>() * (max_val - min_val);
let mut left_residuals = Vec::new();
let mut right_residuals = Vec::new();
for sample_idx in 0..n_samples {
if x[[sample_idx, feature_idx]] <= threshold {
left_residuals.push(residuals[sample_idx]);
} else {
right_residuals.push(residuals[sample_idx]);
}
}
if left_residuals.is_empty() || right_residuals.is_empty() {
continue;
}
let left_value =
left_residuals.iter().sum::<Float>() / left_residuals.len() as Float;
let right_value =
right_residuals.iter().sum::<Float>() / right_residuals.len() as Float;
let left_loss: Float = left_residuals
.iter()
.map(|&r| (r - left_value).powi(2))
.sum();
let right_loss: Float = right_residuals
.iter()
.map(|&r| (r - right_value).powi(2))
.sum();
let total_loss = left_loss + right_loss;
if total_loss < best_loss {
best_loss = total_loss;
best_feature = feature_idx;
best_threshold = threshold;
best_left_value = left_value;
best_right_value = right_value;
}
}
}
Ok(WeakLearner {
feature_idx: best_feature,
threshold: best_threshold,
left_value: best_left_value,
right_value: best_right_value,
})
}
fn predict_weak_learner(
&self,
learner: &WeakLearner,
x: &ArrayView2<'_, Float>,
) -> Array1<Float> {
let n_samples = x.nrows();
let mut predictions = Array1::<Float>::zeros(n_samples);
for sample_idx in 0..n_samples {
if x[[sample_idx, learner.feature_idx]] <= learner.threshold {
predictions[sample_idx] = learner.left_value;
} else {
predictions[sample_idx] = learner.right_value;
}
}
predictions
}
}
impl Predict<ArrayView2<'_, Float>, Array2<Float>>
for GradientBoostingMultiOutput<GradientBoostingMultiOutputTrained>
{
fn predict(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
let (n_samples, n_features) = x.dim();
if n_features != self.state.n_features {
return Err(SklearsError::InvalidInput(format!(
"Expected {} features, got {}",
self.state.n_features, n_features
)));
}
let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_targets));
for target_idx in 0..self.state.n_targets {
for sample_idx in 0..n_samples {
predictions[[sample_idx, target_idx]] = self.state.initial_predictions[target_idx];
}
}
for stage_estimators in &self.state.estimators {
for (target_idx, weak_learner) in stage_estimators.iter().enumerate() {
for sample_idx in 0..n_samples {
let prediction =
if x[[sample_idx, weak_learner.feature_idx]] <= weak_learner.threshold {
weak_learner.left_value
} else {
weak_learner.right_value
};
predictions[[sample_idx, target_idx]] += self.learning_rate * prediction;
}
}
}
Ok(predictions)
}
}
impl GradientBoostingMultiOutput<GradientBoostingMultiOutputTrained> {
pub fn feature_importances(&self) -> Array1<Float> {
let mut importances = Array1::<Float>::zeros(self.state.n_features);
for stage_estimators in &self.state.estimators {
for weak_learner in stage_estimators {
importances[weak_learner.feature_idx] += 1.0;
}
}
let total = importances.sum();
if total > 0.0 {
importances /= total;
}
importances
}
pub fn training_loss_history(&self) -> Vec<Float> {
Vec::new()
}
}