#![allow(non_snake_case)]
use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
use scirs2_core::random::RandNormal;
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Untrained},
types::Float,
};
use crate::activation::ActivationFunction;
use crate::loss::LossFunction;
#[derive(Debug, Clone)]
pub struct MultiOutputMLP<S = Untrained> {
state: S,
hidden_layer_sizes: Vec<usize>,
activation: ActivationFunction,
output_activation: ActivationFunction,
loss_function: LossFunction,
learning_rate: Float,
max_iter: usize,
tolerance: Float,
random_state: Option<u64>,
alpha: Float, batch_size: Option<usize>,
early_stopping: bool,
validation_fraction: Float,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct MultiOutputMLPTrained {
weights: Vec<Array2<Float>>,
biases: Vec<Array1<Float>>,
n_features: usize,
n_outputs: usize,
hidden_layer_sizes: Vec<usize>,
activation: ActivationFunction,
output_activation: ActivationFunction,
loss_curve: Vec<Float>,
n_iter: usize,
}
impl MultiOutputMLP<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
hidden_layer_sizes: vec![100],
activation: ActivationFunction::ReLU,
output_activation: ActivationFunction::Linear,
loss_function: LossFunction::MeanSquaredError,
learning_rate: 0.001,
max_iter: 200,
tolerance: 1e-4,
random_state: None,
alpha: 0.0001,
batch_size: None,
early_stopping: false,
validation_fraction: 0.1,
}
}
pub fn hidden_layer_sizes(mut self, sizes: Vec<usize>) -> Self {
self.hidden_layer_sizes = sizes;
self
}
pub fn activation(mut self, activation: ActivationFunction) -> Self {
self.activation = activation;
self
}
pub fn output_activation(mut self, activation: ActivationFunction) -> Self {
self.output_activation = activation;
self
}
pub fn loss_function(mut self, loss_function: LossFunction) -> Self {
self.loss_function = loss_function;
self
}
pub fn learning_rate(mut self, learning_rate: Float) -> Self {
self.learning_rate = learning_rate;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tolerance(mut self, tolerance: Float) -> Self {
self.tolerance = tolerance;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
pub fn alpha(mut self, alpha: Float) -> Self {
self.alpha = alpha;
self
}
pub fn batch_size(mut self, batch_size: Option<usize>) -> Self {
self.batch_size = batch_size;
self
}
pub fn early_stopping(mut self, early_stopping: bool) -> Self {
self.early_stopping = early_stopping;
self
}
pub fn validation_fraction(mut self, validation_fraction: Float) -> Self {
self.validation_fraction = validation_fraction;
self
}
}
impl Default for MultiOutputMLP<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for MultiOutputMLP<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, Array2<Float>> for MultiOutputMLP<Untrained> {
type Fitted = MultiOutputMLP<MultiOutputMLPTrained>;
#[allow(non_snake_case)] fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<Float>) -> SklResult<Self::Fitted> {
let (n_samples, n_features) = X.dim();
let (n_samples_y, n_outputs) = y.dim();
if n_samples != n_samples_y {
return Err(SklearsError::InvalidInput(
"X and y must have the same number of samples".to_string(),
));
}
if n_samples == 0 {
return Err(SklearsError::InvalidInput(
"Cannot fit with zero samples".to_string(),
));
}
let mut rng = match self.random_state {
Some(seed) => scirs2_core::random::seeded_rng(seed),
None => scirs2_core::random::seeded_rng(42),
};
let mut layer_sizes = vec![n_features];
layer_sizes.extend(&self.hidden_layer_sizes);
layer_sizes.push(n_outputs);
let mut weights = Vec::new();
let mut biases = Vec::new();
for i in 0..layer_sizes.len() - 1 {
let input_size = layer_sizes[i];
let output_size = layer_sizes[i + 1];
let scale = (2.0 / (input_size + output_size) as Float).sqrt();
let normal_dist = RandNormal::new(0.0, scale).expect("operation should succeed");
let mut weight_matrix = Array2::<Float>::zeros((output_size, input_size));
for i in 0..output_size {
for j in 0..input_size {
weight_matrix[[i, j]] = rng.sample(normal_dist);
}
}
let bias_vector = Array1::<Float>::zeros(output_size);
weights.push(weight_matrix);
biases.push(bias_vector);
}
let mut loss_curve = Vec::new();
let X_owned = X.to_owned();
let y_owned = y.to_owned();
for epoch in 0..self.max_iter {
let (activations, _) = self.forward_pass(&X_owned, &weights, &biases)?;
let predictions = activations.last().expect("collection should not be empty");
let loss = self.loss_function.compute_loss(predictions, &y_owned);
loss_curve.push(loss);
if epoch > 0 && (loss_curve[epoch - 1] - loss).abs() < self.tolerance {
break;
}
self.backward_pass(&X_owned, &y_owned, &mut weights, &mut biases)?;
}
let trained_state = MultiOutputMLPTrained {
weights,
biases,
n_features,
n_outputs,
hidden_layer_sizes: self.hidden_layer_sizes.clone(),
activation: self.activation,
output_activation: self.output_activation,
loss_curve,
n_iter: self.max_iter,
};
Ok(MultiOutputMLP {
state: trained_state,
hidden_layer_sizes: self.hidden_layer_sizes,
activation: self.activation,
output_activation: self.output_activation,
loss_function: self.loss_function,
learning_rate: self.learning_rate,
max_iter: self.max_iter,
tolerance: self.tolerance,
random_state: self.random_state,
alpha: self.alpha,
batch_size: self.batch_size,
early_stopping: self.early_stopping,
validation_fraction: self.validation_fraction,
})
}
}
impl MultiOutputMLP<Untrained> {
#[allow(clippy::type_complexity)]
#[allow(non_snake_case)] fn forward_pass(
&self,
X: &Array2<Float>,
weights: &[Array2<Float>],
biases: &[Array1<Float>],
) -> SklResult<(Vec<Array2<Float>>, Vec<Array2<Float>>)> {
let mut activations = vec![X.clone()];
let mut z_values = Vec::new();
for (i, (weight, bias)) in weights.iter().zip(biases.iter()).enumerate() {
let current_input = activations.last().expect("collection should not be empty");
let z = current_input.dot(&weight.t()) + bias.view().insert_axis(Axis(0));
z_values.push(z.clone());
let activation_fn = if i == weights.len() - 1 {
self.output_activation
} else {
self.activation
};
let activated = activation_fn.apply_2d(&z);
activations.push(activated);
}
Ok((activations, z_values))
}
fn backward_pass(
&self,
X: &Array2<Float>,
y: &Array2<Float>,
weights: &mut [Array2<Float>],
biases: &mut [Array1<Float>],
) -> SklResult<()> {
let (activations, z_values) = self.forward_pass(X, weights, biases)?;
let n_samples = X.nrows() as Float;
let output_predictions = activations.last().expect("collection should not be empty");
let mut delta = output_predictions - y;
for i in (0..weights.len()).rev() {
let current_activation = &activations[i];
let weight_gradient = delta.t().dot(current_activation) / n_samples;
let bias_gradient = delta
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let regularized_weight_gradient = weight_gradient + self.alpha * &weights[i];
weights[i] = &weights[i] - self.learning_rate * regularized_weight_gradient;
biases[i] = &biases[i] - self.learning_rate * bias_gradient;
if i > 0 {
let activation_fn = if i == weights.len() - 1 {
self.output_activation
} else {
self.activation
};
let derivative_approx = match activation_fn {
ActivationFunction::ReLU => {
z_values[i - 1].map(|&val| if val > 0.0 { 1.0 } else { 0.0 })
}
ActivationFunction::Sigmoid => {
let sigmoid_vals = &activations[i];
sigmoid_vals.map(|&val| val * (1.0 - val))
}
ActivationFunction::Tanh => {
let tanh_vals = &activations[i];
tanh_vals.map(|&val| 1.0 - val * val)
}
_ => Array2::ones(z_values[i - 1].dim()),
};
delta = delta.dot(&weights[i]) * derivative_approx;
}
}
Ok(())
}
}
impl Predict<ArrayView2<'_, Float>, Array2<Float>> for MultiOutputMLP<MultiOutputMLPTrained> {
#[allow(non_snake_case)] 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(
"X has different number of features than training data".to_string(),
));
}
let X_owned = X.to_owned();
let (activations, _) = self.forward_pass_trained(&X_owned)?;
let predictions = activations
.last()
.expect("collection should not be empty")
.clone();
Ok(predictions)
}
}
impl MultiOutputMLP<MultiOutputMLPTrained> {
#[allow(clippy::type_complexity)]
#[allow(non_snake_case)] fn forward_pass_trained(
&self,
X: &Array2<Float>,
) -> SklResult<(Vec<Array2<Float>>, Vec<Array2<Float>>)> {
let mut activations = vec![X.clone()];
let mut z_values = Vec::new();
for (i, (weight, bias)) in self
.state
.weights
.iter()
.zip(self.state.biases.iter())
.enumerate()
{
let current_input = activations.last().expect("collection should not be empty");
let z = current_input.dot(&weight.t()) + bias.view().insert_axis(Axis(0));
z_values.push(z.clone());
let activation_fn = if i == self.state.weights.len() - 1 {
self.state.output_activation
} else {
self.state.activation
};
let activated = activation_fn.apply_2d(&z);
activations.push(activated);
}
Ok((activations, z_values))
}
pub fn loss_curve(&self) -> &[Float] {
&self.state.loss_curve
}
pub fn n_iter(&self) -> usize {
self.state.n_iter
}
pub fn weights(&self) -> &[Array2<Float>] {
&self.state.weights
}
pub fn biases(&self) -> &[Array1<Float>] {
&self.state.biases
}
}
pub type MultiOutputMLPClassifier<S = Untrained> = MultiOutputMLP<S>;
impl MultiOutputMLPClassifier<Untrained> {
pub fn new_classifier() -> Self {
Self::new()
.output_activation(ActivationFunction::Sigmoid)
.loss_function(LossFunction::BinaryCrossEntropy)
}
}
pub type MultiOutputMLPRegressor<S = Untrained> = MultiOutputMLP<S>;
impl MultiOutputMLPRegressor<Untrained> {
pub fn new_regressor() -> Self {
Self::new()
.output_activation(ActivationFunction::Linear)
.loss_function(LossFunction::MeanSquaredError)
}
}