1#![allow(non_snake_case)] use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
9use sklears_core::{
10 error::{Result as SklResult, SklearsError},
11 traits::{Estimator, Fit, Predict, Untrained},
12 types::Float,
13};
14use std::collections::VecDeque;
15
16#[derive(Debug, Clone)]
22pub struct IncrementalMultiOutputRegressionConfig {
23 pub learning_rate: Float,
25 pub alpha: Float,
27 pub fit_intercept: bool,
29 pub max_samples: usize,
31 pub adaptive_learning_rate: bool,
33 pub learning_rate_decay: Float,
35}
36
37impl Default for IncrementalMultiOutputRegressionConfig {
38 fn default() -> Self {
39 Self {
40 learning_rate: 0.01,
41 alpha: 0.0001,
42 fit_intercept: true,
43 max_samples: 10000,
44 adaptive_learning_rate: true,
45 learning_rate_decay: 0.999,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
78pub struct IncrementalMultiOutputRegression<S = Untrained> {
79 state: S,
80 config: IncrementalMultiOutputRegressionConfig,
81}
82
83#[derive(Debug, Clone)]
85pub struct IncrementalMultiOutputRegressionTrained {
86 pub coef: Array2<Float>,
88 pub intercept: Array1<Float>,
90 pub n_features: usize,
92 pub n_outputs: usize,
94 pub n_samples_seen: usize,
96 pub current_learning_rate: Float,
98 pub feature_mean: Array1<Float>,
100 pub feature_std: Array1<Float>,
102 pub config: IncrementalMultiOutputRegressionConfig,
104}
105
106impl IncrementalMultiOutputRegression<Untrained> {
107 pub fn new() -> Self {
109 Self {
110 state: Untrained,
111 config: IncrementalMultiOutputRegressionConfig::default(),
112 }
113 }
114
115 pub fn config(mut self, config: IncrementalMultiOutputRegressionConfig) -> Self {
117 self.config = config;
118 self
119 }
120
121 pub fn learning_rate(mut self, lr: Float) -> Self {
123 self.config.learning_rate = lr;
124 self
125 }
126
127 pub fn alpha(mut self, alpha: Float) -> Self {
129 self.config.alpha = alpha;
130 self
131 }
132
133 pub fn fit_intercept(mut self, fit_intercept: bool) -> Self {
135 self.config.fit_intercept = fit_intercept;
136 self
137 }
138}
139
140impl Default for IncrementalMultiOutputRegression<Untrained> {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>>
147 for IncrementalMultiOutputRegression<Untrained>
148{
149 type Fitted = IncrementalMultiOutputRegression<IncrementalMultiOutputRegressionTrained>;
150
151 fn fit(self, X: &ArrayView2<Float>, y: &ArrayView2<Float>) -> SklResult<Self::Fitted> {
152 if X.nrows() != y.nrows() {
153 return Err(SklearsError::InvalidInput(
154 "Number of samples in X and y must match".to_string(),
155 ));
156 }
157
158 let n_samples = X.nrows();
159 let n_features = X.ncols();
160 let n_outputs = y.ncols();
161
162 let mut coef = Array2::zeros((n_features, n_outputs));
164 let mut intercept = Array1::zeros(n_outputs);
165
166 let feature_mean = X
168 .mean_axis(Axis(0))
169 .expect("array should have elements for mean computation");
170 let feature_std = X.std_axis(Axis(0), 0.0);
171
172 let mut current_learning_rate = self.config.learning_rate;
173
174 for _ in 0..10 {
176 for i in 0..n_samples {
178 let x_i = X.row(i);
179 let y_i = y.row(i);
180
181 let pred = coef.t().dot(&x_i) + &intercept;
183
184 let error = &y_i - &pred;
186
187 for j in 0..n_features {
189 for k in 0..n_outputs {
190 let gradient = -error[k] * x_i[j] + self.config.alpha * coef[[j, k]];
191 coef[[j, k]] -= current_learning_rate * gradient;
192 }
193 }
194
195 if self.config.fit_intercept {
197 for k in 0..n_outputs {
198 intercept[k] += current_learning_rate * error[k];
199 }
200 }
201 }
202
203 if self.config.adaptive_learning_rate {
205 current_learning_rate *= self.config.learning_rate_decay;
206 }
207 }
208
209 Ok(IncrementalMultiOutputRegression {
210 state: IncrementalMultiOutputRegressionTrained {
211 coef,
212 intercept,
213 n_features,
214 n_outputs,
215 n_samples_seen: n_samples,
216 current_learning_rate,
217 feature_mean,
218 feature_std,
219 config: self.config,
220 },
221 config: IncrementalMultiOutputRegressionConfig::default(),
222 })
223 }
224}
225
226impl IncrementalMultiOutputRegression<IncrementalMultiOutputRegressionTrained> {
227 pub fn partial_fit(mut self, X: &ArrayView2<Float>, y: &ArrayView2<Float>) -> SklResult<Self> {
229 if X.nrows() != y.nrows() {
230 return Err(SklearsError::InvalidInput(
231 "Number of samples in X and y must match".to_string(),
232 ));
233 }
234
235 if X.ncols() != self.state.n_features {
236 return Err(SklearsError::InvalidInput(format!(
237 "Expected {} features, got {}",
238 self.state.n_features,
239 X.ncols()
240 )));
241 }
242
243 if y.ncols() != self.state.n_outputs {
244 return Err(SklearsError::InvalidInput(format!(
245 "Expected {} outputs, got {}",
246 self.state.n_outputs,
247 y.ncols()
248 )));
249 }
250
251 let n_samples = X.nrows();
252
253 let n_old = self.state.n_samples_seen as Float;
255 let n_new = n_samples as Float;
256 let n_total = n_old + n_new;
257
258 let new_mean = X
259 .mean_axis(Axis(0))
260 .expect("array should have elements for mean computation");
261 self.state.feature_mean = (&self.state.feature_mean * n_old + &new_mean * n_new) / n_total;
262
263 for i in 0..n_samples {
265 let x_i = X.row(i);
266 let y_i = y.row(i);
267
268 let pred = self.state.coef.t().dot(&x_i) + &self.state.intercept;
270
271 let error = &y_i - &pred;
273
274 for j in 0..self.state.n_features {
276 for k in 0..self.state.n_outputs {
277 let gradient =
278 -error[k] * x_i[j] + self.state.config.alpha * self.state.coef[[j, k]];
279 self.state.coef[[j, k]] -= self.state.current_learning_rate * gradient;
280 }
281 }
282
283 if self.state.config.fit_intercept {
285 for k in 0..self.state.n_outputs {
286 self.state.intercept[k] += self.state.current_learning_rate * error[k];
287 }
288 }
289 }
290
291 self.state.n_samples_seen += n_samples;
293
294 if self.state.config.adaptive_learning_rate {
296 self.state.current_learning_rate *= self.state.config.learning_rate_decay;
297 }
298
299 Ok(self)
300 }
301
302 pub fn coef(&self) -> &Array2<Float> {
304 &self.state.coef
305 }
306
307 pub fn intercept(&self) -> &Array1<Float> {
309 &self.state.intercept
310 }
311
312 pub fn n_samples_seen(&self) -> usize {
314 self.state.n_samples_seen
315 }
316
317 pub fn current_learning_rate(&self) -> Float {
319 self.state.current_learning_rate
320 }
321}
322
323impl Predict<ArrayView2<'_, Float>, Array2<Float>>
324 for IncrementalMultiOutputRegression<IncrementalMultiOutputRegressionTrained>
325{
326 fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
327 if X.ncols() != self.state.n_features {
328 return Err(SklearsError::InvalidInput(format!(
329 "Expected {} features, got {}",
330 self.state.n_features,
331 X.ncols()
332 )));
333 }
334
335 let n_samples = X.nrows();
336 let mut predictions = Array2::zeros((n_samples, self.state.n_outputs));
337
338 for i in 0..n_samples {
339 let x_i = X.row(i);
340 let pred = self.state.coef.t().dot(&x_i) + &self.state.intercept;
341 predictions.row_mut(i).assign(&pred);
342 }
343
344 Ok(predictions)
345 }
346}
347
348impl Estimator for IncrementalMultiOutputRegression<Untrained> {
349 type Config = IncrementalMultiOutputRegressionConfig;
350 type Error = SklearsError;
351 type Float = Float;
352
353 fn config(&self) -> &Self::Config {
354 &self.config
355 }
356}
357
358impl Estimator for IncrementalMultiOutputRegression<IncrementalMultiOutputRegressionTrained> {
359 type Config = IncrementalMultiOutputRegressionConfig;
360 type Error = SklearsError;
361 type Float = Float;
362
363 fn config(&self) -> &Self::Config {
364 &self.state.config
365 }
366}
367
368#[derive(Debug, Clone)]
374pub struct StreamingMultiOutputConfig {
375 pub batch_size: usize,
377 pub max_buffer_size: usize,
379 pub learning_rate: Float,
381 pub detect_drift: bool,
383 pub drift_window_size: usize,
385 pub drift_threshold: Float,
387}
388
389impl Default for StreamingMultiOutputConfig {
390 fn default() -> Self {
391 Self {
392 batch_size: 32,
393 max_buffer_size: 1000,
394 learning_rate: 0.01,
395 detect_drift: true,
396 drift_window_size: 100,
397 drift_threshold: 0.1,
398 }
399 }
400}
401
402#[derive(Debug, Clone)]
432pub struct StreamingMultiOutput<S = Untrained> {
433 state: S,
434 config: StreamingMultiOutputConfig,
435}
436
437#[derive(Debug, Clone)]
439pub struct StreamingMultiOutputTrained {
440 pub base_model: IncrementalMultiOutputRegressionTrained,
442 pub buffer_X: VecDeque<Array1<Float>>,
444 pub buffer_y: VecDeque<Array1<Float>>,
445 pub error_history: VecDeque<Float>,
447 pub drift_detected: bool,
449 pub n_drift_events: usize,
451 pub config: StreamingMultiOutputConfig,
453}
454
455impl StreamingMultiOutput<Untrained> {
456 pub fn new() -> Self {
458 Self {
459 state: Untrained,
460 config: StreamingMultiOutputConfig::default(),
461 }
462 }
463
464 pub fn config(mut self, config: StreamingMultiOutputConfig) -> Self {
466 self.config = config;
467 self
468 }
469
470 pub fn batch_size(mut self, batch_size: usize) -> Self {
472 self.config.batch_size = batch_size;
473 self
474 }
475
476 pub fn learning_rate(mut self, lr: Float) -> Self {
478 self.config.learning_rate = lr;
479 self
480 }
481
482 pub fn detect_drift(mut self, detect: bool) -> Self {
484 self.config.detect_drift = detect;
485 self
486 }
487}
488
489impl Default for StreamingMultiOutput<Untrained> {
490 fn default() -> Self {
491 Self::new()
492 }
493}
494
495impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for StreamingMultiOutput<Untrained> {
496 type Fitted = StreamingMultiOutput<StreamingMultiOutputTrained>;
497
498 fn fit(self, X: &ArrayView2<Float>, y: &ArrayView2<Float>) -> SklResult<Self::Fitted> {
499 let base_config = IncrementalMultiOutputRegressionConfig {
501 learning_rate: self.config.learning_rate,
502 ..Default::default()
503 };
504
505 let base_model = IncrementalMultiOutputRegression::new()
506 .config(base_config)
507 .fit(X, y)?;
508
509 Ok(StreamingMultiOutput {
510 state: StreamingMultiOutputTrained {
511 base_model: base_model.state,
512 buffer_X: VecDeque::new(),
513 buffer_y: VecDeque::new(),
514 error_history: VecDeque::new(),
515 drift_detected: false,
516 n_drift_events: 0,
517 config: self.config,
518 },
519 config: StreamingMultiOutputConfig::default(),
520 })
521 }
522}
523
524impl StreamingMultiOutput<StreamingMultiOutputTrained> {
525 pub fn update_stream(
527 mut self,
528 X: &ArrayView2<Float>,
529 y: &ArrayView2<Float>,
530 ) -> SklResult<Self> {
531 for i in 0..X.nrows() {
533 self.state.buffer_X.push_back(X.row(i).to_owned());
534 self.state.buffer_y.push_back(y.row(i).to_owned());
535 }
536
537 if self.state.buffer_X.len() >= self.state.config.batch_size {
539 self = self.process_buffer()?;
540 }
541
542 Ok(self)
543 }
544
545 fn process_buffer(mut self) -> SklResult<Self> {
547 let batch_size = self.state.config.batch_size.min(self.state.buffer_X.len());
548
549 if batch_size == 0 {
550 return Ok(self);
551 }
552
553 let mut X_batch = Array2::zeros((batch_size, self.state.base_model.n_features));
555 let mut y_batch = Array2::zeros((batch_size, self.state.base_model.n_outputs));
556
557 for i in 0..batch_size {
558 let x = self
559 .state
560 .buffer_X
561 .pop_front()
562 .expect("operation should succeed");
563 let y = self
564 .state
565 .buffer_y
566 .pop_front()
567 .expect("operation should succeed");
568 X_batch.row_mut(i).assign(&x);
569 y_batch.row_mut(i).assign(&y);
570 }
571
572 if self.state.config.detect_drift {
574 let pred = self.predict(&X_batch.view())?;
575 let error: Float = (&y_batch - &pred)
576 .mapv(|x| x.powi(2))
577 .mean()
578 .expect("array should have elements for mean computation");
579
580 self.state.error_history.push_back(error);
581 if self.state.error_history.len() > self.state.config.drift_window_size {
582 self.state.error_history.pop_front();
583 }
584
585 if self.state.error_history.len() >= self.state.config.drift_window_size {
587 let recent_error: Float = self
588 .state
589 .error_history
590 .iter()
591 .rev()
592 .take(self.state.config.drift_window_size / 2)
593 .sum::<Float>()
594 / (self.state.config.drift_window_size / 2) as Float;
595
596 let old_error: Float = self
597 .state
598 .error_history
599 .iter()
600 .take(self.state.config.drift_window_size / 2)
601 .sum::<Float>()
602 / (self.state.config.drift_window_size / 2) as Float;
603
604 if recent_error > old_error * (1.0 + self.state.config.drift_threshold) {
605 self.state.drift_detected = true;
606 self.state.n_drift_events += 1;
607 }
609 }
610 }
611
612 let base_wrapper = IncrementalMultiOutputRegression {
614 state: self.state.base_model.clone(),
615 config: IncrementalMultiOutputRegressionConfig::default(),
616 };
617
618 let updated = base_wrapper.partial_fit(&X_batch.view(), &y_batch.view())?;
619 self.state.base_model = updated.state;
620
621 Ok(self)
622 }
623
624 pub fn flush_buffer(mut self) -> SklResult<Self> {
626 while !self.state.buffer_X.is_empty() {
627 self = self.process_buffer()?;
628 }
629 Ok(self)
630 }
631
632 pub fn drift_detected(&self) -> bool {
634 self.state.drift_detected
635 }
636
637 pub fn n_drift_events(&self) -> usize {
639 self.state.n_drift_events
640 }
641
642 pub fn buffer_size(&self) -> usize {
644 self.state.buffer_X.len()
645 }
646}
647
648impl Predict<ArrayView2<'_, Float>, Array2<Float>>
649 for StreamingMultiOutput<StreamingMultiOutputTrained>
650{
651 fn predict(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
652 let base_wrapper = IncrementalMultiOutputRegression {
653 state: self.state.base_model.clone(),
654 config: IncrementalMultiOutputRegressionConfig::default(),
655 };
656 base_wrapper.predict(X)
657 }
658}
659
660impl Estimator for StreamingMultiOutput<Untrained> {
661 type Config = StreamingMultiOutputConfig;
662 type Error = SklearsError;
663 type Float = Float;
664
665 fn config(&self) -> &Self::Config {
666 &self.config
667 }
668}
669
670impl Estimator for StreamingMultiOutput<StreamingMultiOutputTrained> {
671 type Config = StreamingMultiOutputConfig;
672 type Error = SklearsError;
673 type Float = Float;
674
675 fn config(&self) -> &Self::Config {
676 &self.state.config
677 }
678}
679
680#[cfg(test)]
685mod tests {
686 use super::*;
687 use scirs2_core::ndarray::array;
689
690 #[test]
691 #[allow(non_snake_case)]
692 fn test_incremental_regression_basic() {
693 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
694 let y = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
695
696 let model = IncrementalMultiOutputRegression::new()
697 .learning_rate(0.1)
698 .alpha(0.0001);
699
700 let trained = model
701 .fit(&X.view(), &y.view())
702 .expect("model fitting should succeed");
703 let predictions = trained
704 .predict(&X.view())
705 .expect("prediction should succeed");
706
707 assert_eq!(predictions.dim(), (3, 2));
708 assert_eq!(trained.n_samples_seen(), 3);
709 }
710
711 #[test]
712 #[allow(non_snake_case)]
713 fn test_incremental_regression_partial_fit() {
714 let X1 = array![[1.0, 2.0], [2.0, 3.0]];
715 let y1 = array![[1.0, 2.0], [2.0, 3.0]];
716
717 let model = IncrementalMultiOutputRegression::new().learning_rate(0.1);
718 let trained = model
719 .fit(&X1.view(), &y1.view())
720 .expect("model fitting should succeed");
721
722 let X2 = array![[3.0, 4.0], [4.0, 5.0]];
724 let y2 = array![[3.0, 4.0], [4.0, 5.0]];
725 let updated = trained
726 .partial_fit(&X2.view(), &y2.view())
727 .expect("operation should succeed");
728
729 assert_eq!(updated.n_samples_seen(), 4);
730
731 let predictions = updated
732 .predict(&X2.view())
733 .expect("prediction should succeed");
734 assert_eq!(predictions.dim(), (2, 2));
735 }
736
737 #[test]
738 #[allow(non_snake_case)]
739 fn test_incremental_regression_learning_rate_decay() {
740 let X = array![[1.0, 2.0], [2.0, 3.0]];
741 let y = array![[1.0, 2.0], [2.0, 3.0]];
742
743 let model = IncrementalMultiOutputRegression::new().learning_rate(0.1);
744 let trained = model
745 .fit(&X.view(), &y.view())
746 .expect("model fitting should succeed");
747
748 let initial_lr = trained.current_learning_rate();
749
750 let X2 = array![[3.0, 4.0]];
752 let y2 = array![[3.0, 4.0]];
753 let updated = trained
754 .partial_fit(&X2.view(), &y2.view())
755 .expect("operation should succeed");
756
757 assert!(updated.current_learning_rate() < initial_lr);
758 }
759
760 #[test]
761 #[allow(non_snake_case)]
762 fn test_streaming_basic() {
763 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
764 let y = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
765
766 let model = StreamingMultiOutput::new().batch_size(2).learning_rate(0.1);
767
768 let trained = model
769 .fit(&X.view(), &y.view())
770 .expect("model fitting should succeed");
771 let predictions = trained
772 .predict(&X.view())
773 .expect("prediction should succeed");
774
775 assert_eq!(predictions.dim(), (3, 2));
776 }
777
778 #[test]
779 #[allow(non_snake_case)]
780 fn test_streaming_update() {
781 let X = array![[1.0, 2.0], [2.0, 3.0]];
782 let y = array![[1.0, 2.0], [2.0, 3.0]];
783
784 let model = StreamingMultiOutput::new().batch_size(2);
785 let trained = model
786 .fit(&X.view(), &y.view())
787 .expect("model fitting should succeed");
788
789 let X_stream = array![[3.0, 4.0], [4.0, 5.0]];
791 let y_stream = array![[3.0, 4.0], [4.0, 5.0]];
792 let updated = trained
793 .update_stream(&X_stream.view(), &y_stream.view())
794 .expect("operation should succeed");
795
796 let predictions = updated
797 .predict(&X_stream.view())
798 .expect("prediction should succeed");
799 assert_eq!(predictions.dim(), (2, 2));
800 }
801
802 #[test]
803 #[allow(non_snake_case)]
804 fn test_streaming_buffer() {
805 let X = array![[1.0, 2.0], [2.0, 3.0]];
806 let y = array![[1.0, 2.0], [2.0, 3.0]];
807
808 let model = StreamingMultiOutput::new().batch_size(5); let trained = model
810 .fit(&X.view(), &y.view())
811 .expect("model fitting should succeed");
812
813 let X_stream = array![[3.0, 4.0]];
815 let y_stream = array![[3.0, 4.0]];
816 let updated = trained
817 .update_stream(&X_stream.view(), &y_stream.view())
818 .expect("operation should succeed");
819
820 assert_eq!(updated.buffer_size(), 1);
821
822 let flushed = updated.flush_buffer().expect("operation should succeed");
824 assert_eq!(flushed.buffer_size(), 0);
825 }
826
827 #[test]
828 #[allow(non_snake_case)]
829 fn test_streaming_drift_detection() {
830 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
831 let y = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
832
833 let model = StreamingMultiOutput::new()
834 .batch_size(2)
835 .detect_drift(true)
836 .learning_rate(0.1);
837
838 let trained = model
839 .fit(&X.view(), &y.view())
840 .expect("model fitting should succeed");
841
842 assert_eq!(trained.n_drift_events(), 0);
844 }
845
846 #[test]
847 #[allow(non_snake_case)]
848 fn test_incremental_regression_error_handling() {
849 let X = array![[1.0, 2.0], [2.0, 3.0]];
850 let y = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]]; let model = IncrementalMultiOutputRegression::new();
853 assert!(model.fit(&X.view(), &y.view()).is_err());
854 }
855
856 #[test]
857 #[allow(non_snake_case)]
858 fn test_incremental_regression_prediction_error() {
859 let X = array![[1.0, 2.0], [2.0, 3.0]];
860 let y = array![[1.0, 2.0], [2.0, 3.0]];
861
862 let model = IncrementalMultiOutputRegression::new();
863 let trained = model
864 .fit(&X.view(), &y.view())
865 .expect("model fitting should succeed");
866
867 let X_test = array![[1.0]];
869 assert!(trained.predict(&X_test.view()).is_err());
870 }
871}