1#![allow(non_snake_case)] use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
10use scirs2_core::random::RandNormal;
11use sklears_core::{
12 error::{Result as SklResult, SklearsError},
13 traits::{Estimator, Fit, Predict, Untrained},
14 types::Float,
15};
16
17use crate::activation::ActivationFunction;
18use crate::loss::LossFunction;
19
20#[derive(Debug, Clone)]
51pub struct MultiOutputMLP<S = Untrained> {
52 state: S,
53 hidden_layer_sizes: Vec<usize>,
54 activation: ActivationFunction,
55 output_activation: ActivationFunction,
56 loss_function: LossFunction,
57 learning_rate: Float,
58 max_iter: usize,
59 tolerance: Float,
60 random_state: Option<u64>,
61 alpha: Float, batch_size: Option<usize>,
63 early_stopping: bool,
64 validation_fraction: Float,
65}
66
67#[derive(Debug, Clone)]
69#[allow(dead_code)] pub struct MultiOutputMLPTrained {
71 weights: Vec<Array2<Float>>,
73 biases: Vec<Array1<Float>>,
75 n_features: usize,
77 n_outputs: usize,
79 hidden_layer_sizes: Vec<usize>,
81 activation: ActivationFunction,
82 output_activation: ActivationFunction,
83 loss_curve: Vec<Float>,
85 n_iter: usize,
87}
88
89impl MultiOutputMLP<Untrained> {
90 pub fn new() -> Self {
92 Self {
93 state: Untrained,
94 hidden_layer_sizes: vec![100],
95 activation: ActivationFunction::ReLU,
96 output_activation: ActivationFunction::Linear,
97 loss_function: LossFunction::MeanSquaredError,
98 learning_rate: 0.001,
99 max_iter: 200,
100 tolerance: 1e-4,
101 random_state: None,
102 alpha: 0.0001,
103 batch_size: None,
104 early_stopping: false,
105 validation_fraction: 0.1,
106 }
107 }
108
109 pub fn hidden_layer_sizes(mut self, sizes: Vec<usize>) -> Self {
111 self.hidden_layer_sizes = sizes;
112 self
113 }
114
115 pub fn activation(mut self, activation: ActivationFunction) -> Self {
117 self.activation = activation;
118 self
119 }
120
121 pub fn output_activation(mut self, activation: ActivationFunction) -> Self {
123 self.output_activation = activation;
124 self
125 }
126
127 pub fn loss_function(mut self, loss_function: LossFunction) -> Self {
129 self.loss_function = loss_function;
130 self
131 }
132
133 pub fn learning_rate(mut self, learning_rate: Float) -> Self {
135 self.learning_rate = learning_rate;
136 self
137 }
138
139 pub fn max_iter(mut self, max_iter: usize) -> Self {
141 self.max_iter = max_iter;
142 self
143 }
144
145 pub fn tolerance(mut self, tolerance: Float) -> Self {
147 self.tolerance = tolerance;
148 self
149 }
150
151 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
153 self.random_state = random_state;
154 self
155 }
156
157 pub fn alpha(mut self, alpha: Float) -> Self {
159 self.alpha = alpha;
160 self
161 }
162
163 pub fn batch_size(mut self, batch_size: Option<usize>) -> Self {
165 self.batch_size = batch_size;
166 self
167 }
168
169 pub fn early_stopping(mut self, early_stopping: bool) -> Self {
171 self.early_stopping = early_stopping;
172 self
173 }
174
175 pub fn validation_fraction(mut self, validation_fraction: Float) -> Self {
177 self.validation_fraction = validation_fraction;
178 self
179 }
180}
181
182impl Default for MultiOutputMLP<Untrained> {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188impl Estimator for MultiOutputMLP<Untrained> {
189 type Config = ();
190 type Error = SklearsError;
191 type Float = Float;
192
193 fn config(&self) -> &Self::Config {
194 &()
195 }
196}
197
198impl Fit<ArrayView2<'_, Float>, Array2<Float>> for MultiOutputMLP<Untrained> {
199 type Fitted = MultiOutputMLP<MultiOutputMLPTrained>;
200
201 #[allow(non_snake_case)] fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<Float>) -> SklResult<Self::Fitted> {
203 let (n_samples, n_features) = X.dim();
204 let (n_samples_y, n_outputs) = y.dim();
205
206 if n_samples != n_samples_y {
207 return Err(SklearsError::InvalidInput(
208 "X and y must have the same number of samples".to_string(),
209 ));
210 }
211
212 if n_samples == 0 {
213 return Err(SklearsError::InvalidInput(
214 "Cannot fit with zero samples".to_string(),
215 ));
216 }
217
218 let mut rng = match self.random_state {
220 Some(seed) => scirs2_core::random::seeded_rng(seed),
221 None => scirs2_core::random::seeded_rng(42),
222 };
223
224 let mut layer_sizes = vec![n_features];
226 layer_sizes.extend(&self.hidden_layer_sizes);
227 layer_sizes.push(n_outputs);
228
229 let mut weights = Vec::new();
231 let mut biases = Vec::new();
232
233 for i in 0..layer_sizes.len() - 1 {
234 let input_size = layer_sizes[i];
235 let output_size = layer_sizes[i + 1];
236
237 let scale = (2.0 / (input_size + output_size) as Float).sqrt();
239 let normal_dist = RandNormal::new(0.0, scale).expect("operation should succeed");
240 let mut weight_matrix = Array2::<Float>::zeros((output_size, input_size));
241 for i in 0..output_size {
242 for j in 0..input_size {
243 weight_matrix[[i, j]] = rng.sample(normal_dist);
244 }
245 }
246 let bias_vector = Array1::<Float>::zeros(output_size);
247
248 weights.push(weight_matrix);
249 biases.push(bias_vector);
250 }
251
252 let mut loss_curve = Vec::new();
254 let X_owned = X.to_owned();
255 let y_owned = y.to_owned();
256
257 for epoch in 0..self.max_iter {
258 let (activations, _) = self.forward_pass(&X_owned, &weights, &biases)?;
260 let predictions = activations.last().expect("collection should not be empty");
261
262 let loss = self.loss_function.compute_loss(predictions, &y_owned);
264 loss_curve.push(loss);
265
266 if epoch > 0 && (loss_curve[epoch - 1] - loss).abs() < self.tolerance {
268 break;
269 }
270
271 self.backward_pass(&X_owned, &y_owned, &mut weights, &mut biases)?;
273 }
274
275 let trained_state = MultiOutputMLPTrained {
276 weights,
277 biases,
278 n_features,
279 n_outputs,
280 hidden_layer_sizes: self.hidden_layer_sizes.clone(),
281 activation: self.activation,
282 output_activation: self.output_activation,
283 loss_curve,
284 n_iter: self.max_iter,
285 };
286
287 Ok(MultiOutputMLP {
288 state: trained_state,
289 hidden_layer_sizes: self.hidden_layer_sizes,
290 activation: self.activation,
291 output_activation: self.output_activation,
292 loss_function: self.loss_function,
293 learning_rate: self.learning_rate,
294 max_iter: self.max_iter,
295 tolerance: self.tolerance,
296 random_state: self.random_state,
297 alpha: self.alpha,
298 batch_size: self.batch_size,
299 early_stopping: self.early_stopping,
300 validation_fraction: self.validation_fraction,
301 })
302 }
303}
304
305impl MultiOutputMLP<Untrained> {
306 #[allow(clippy::type_complexity)]
308 #[allow(non_snake_case)] fn forward_pass(
310 &self,
311 X: &Array2<Float>,
312 weights: &[Array2<Float>],
313 biases: &[Array1<Float>],
314 ) -> SklResult<(Vec<Array2<Float>>, Vec<Array2<Float>>)> {
315 let mut activations = vec![X.clone()];
316 let mut z_values = Vec::new();
317
318 for (i, (weight, bias)) in weights.iter().zip(biases.iter()).enumerate() {
319 let current_input = activations.last().expect("collection should not be empty");
320
321 let z = current_input.dot(&weight.t()) + bias.view().insert_axis(Axis(0));
323 z_values.push(z.clone());
324
325 let activation_fn = if i == weights.len() - 1 {
327 self.output_activation
328 } else {
329 self.activation
330 };
331
332 let activated = activation_fn.apply_2d(&z);
333 activations.push(activated);
334 }
335
336 Ok((activations, z_values))
337 }
338
339 fn backward_pass(
341 &self,
342 X: &Array2<Float>,
343 y: &Array2<Float>,
344 weights: &mut [Array2<Float>],
345 biases: &mut [Array1<Float>],
346 ) -> SklResult<()> {
347 let (activations, z_values) = self.forward_pass(X, weights, biases)?;
348 let n_samples = X.nrows() as Float;
349
350 let output_predictions = activations.last().expect("collection should not be empty");
352 let mut delta = output_predictions - y;
353
354 for i in (0..weights.len()).rev() {
356 let current_activation = &activations[i];
357
358 let weight_gradient = delta.t().dot(current_activation) / n_samples;
360 let bias_gradient = delta
361 .mean_axis(Axis(0))
362 .expect("array should have elements for mean computation");
363
364 let regularized_weight_gradient = weight_gradient + self.alpha * &weights[i];
366
367 weights[i] = &weights[i] - self.learning_rate * regularized_weight_gradient;
369 biases[i] = &biases[i] - self.learning_rate * bias_gradient;
370
371 if i > 0 {
373 let activation_fn = if i == weights.len() - 1 {
374 self.output_activation
375 } else {
376 self.activation
377 };
378
379 let derivative_approx = match activation_fn {
381 ActivationFunction::ReLU => {
382 z_values[i - 1].map(|&val| if val > 0.0 { 1.0 } else { 0.0 })
383 }
384 ActivationFunction::Sigmoid => {
385 let sigmoid_vals = &activations[i];
386 sigmoid_vals.map(|&val| val * (1.0 - val))
387 }
388 ActivationFunction::Tanh => {
389 let tanh_vals = &activations[i];
390 tanh_vals.map(|&val| 1.0 - val * val)
391 }
392 _ => Array2::ones(z_values[i - 1].dim()),
393 };
394
395 delta = delta.dot(&weights[i]) * derivative_approx;
396 }
397 }
398
399 Ok(())
400 }
401}
402
403impl Predict<ArrayView2<'_, Float>, Array2<Float>> for MultiOutputMLP<MultiOutputMLPTrained> {
404 #[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
406 let (_n_samples, n_features) = X.dim();
407
408 if n_features != self.state.n_features {
409 return Err(SklearsError::InvalidInput(
410 "X has different number of features than training data".to_string(),
411 ));
412 }
413
414 let X_owned = X.to_owned();
415 let (activations, _) = self.forward_pass_trained(&X_owned)?;
416 let predictions = activations
417 .last()
418 .expect("collection should not be empty")
419 .clone();
420
421 Ok(predictions)
422 }
423}
424
425impl MultiOutputMLP<MultiOutputMLPTrained> {
426 #[allow(clippy::type_complexity)]
428 #[allow(non_snake_case)] fn forward_pass_trained(
430 &self,
431 X: &Array2<Float>,
432 ) -> SklResult<(Vec<Array2<Float>>, Vec<Array2<Float>>)> {
433 let mut activations = vec![X.clone()];
434 let mut z_values = Vec::new();
435
436 for (i, (weight, bias)) in self
437 .state
438 .weights
439 .iter()
440 .zip(self.state.biases.iter())
441 .enumerate()
442 {
443 let current_input = activations.last().expect("collection should not be empty");
444
445 let z = current_input.dot(&weight.t()) + bias.view().insert_axis(Axis(0));
447 z_values.push(z.clone());
448
449 let activation_fn = if i == self.state.weights.len() - 1 {
451 self.state.output_activation
452 } else {
453 self.state.activation
454 };
455
456 let activated = activation_fn.apply_2d(&z);
457 activations.push(activated);
458 }
459
460 Ok((activations, z_values))
461 }
462
463 pub fn loss_curve(&self) -> &[Float] {
465 &self.state.loss_curve
466 }
467
468 pub fn n_iter(&self) -> usize {
470 self.state.n_iter
471 }
472
473 pub fn weights(&self) -> &[Array2<Float>] {
475 &self.state.weights
476 }
477
478 pub fn biases(&self) -> &[Array1<Float>] {
480 &self.state.biases
481 }
482}
483
484pub type MultiOutputMLPClassifier<S = Untrained> = MultiOutputMLP<S>;
489
490impl MultiOutputMLPClassifier<Untrained> {
491 pub fn new_classifier() -> Self {
493 Self::new()
494 .output_activation(ActivationFunction::Sigmoid)
495 .loss_function(LossFunction::BinaryCrossEntropy)
496 }
497}
498
499pub type MultiOutputMLPRegressor<S = Untrained> = MultiOutputMLP<S>;
504
505impl MultiOutputMLPRegressor<Untrained> {
506 pub fn new_regressor() -> Self {
508 Self::new()
509 .output_activation(ActivationFunction::Linear)
510 .loss_function(LossFunction::MeanSquaredError)
511 }
512}