1mod attention;
7mod callbacks;
8mod conv;
9mod layers;
10mod quantum_layers;
11mod rnn;
12mod schedules;
13
14pub use attention::*;
15pub use callbacks::*;
16pub use conv::*;
17pub use layers::*;
18pub use quantum_layers::*;
19pub use rnn::*;
20pub use schedules::*;
21
22use crate::error::{MLError, Result};
23use scirs2_core::ndarray::{s, ArrayD, Axis, IxDyn};
24use scirs2_core::random::prelude::*;
25use std::collections::HashMap;
26
27pub trait KerasLayer: Send + Sync {
29 fn build(&mut self, input_shape: &[usize]) -> Result<()>;
31
32 fn call(&self, inputs: &ArrayD<f64>) -> Result<ArrayD<f64>>;
34
35 fn compute_output_shape(&self, input_shape: &[usize]) -> Vec<usize>;
37
38 fn name(&self) -> &str;
40
41 fn get_weights(&self) -> Vec<ArrayD<f64>>;
43
44 fn set_weights(&mut self, weights: Vec<ArrayD<f64>>) -> Result<()>;
46
47 fn count_params(&self) -> usize {
49 self.get_weights().iter().map(|w| w.len()).sum()
50 }
51
52 fn built(&self) -> bool;
54
55 fn layer_type(&self) -> &'static str {
66 let full_path = std::any::type_name::<Self>();
67 full_path.rsplit("::").next().unwrap_or(full_path)
68 }
69}
70
71#[derive(Debug, Clone)]
73pub enum ActivationFunction {
74 Linear,
76 ReLU,
78 Sigmoid,
80 Tanh,
82 Softmax,
84 LeakyReLU(f64),
86 ELU(f64),
88}
89
90#[derive(Debug, Clone)]
92pub enum InitializerType {
93 Zeros,
95 Ones,
97 GlorotUniform,
99 GlorotNormal,
101 HeUniform,
103}
104
105pub struct Sequential {
107 layers: Vec<Box<dyn KerasLayer>>,
109 name: String,
111 built: bool,
113 compiled: bool,
115 input_shape: Option<Vec<usize>>,
117 loss: Option<LossFunction>,
119 optimizer: Option<OptimizerType>,
121 metrics: Vec<MetricType>,
123}
124
125impl Sequential {
126 pub fn new() -> Self {
128 Self {
129 layers: Vec::new(),
130 name: format!("sequential_{}", fastrand::u32(..)),
131 built: false,
132 compiled: false,
133 input_shape: None,
134 loss: None,
135 optimizer: None,
136 metrics: Vec::new(),
137 }
138 }
139
140 pub fn name(mut self, name: impl Into<String>) -> Self {
142 self.name = name.into();
143 self
144 }
145
146 pub fn add(&mut self, layer: Box<dyn KerasLayer>) {
148 self.layers.push(layer);
149 self.built = false;
150 }
151
152 pub fn layers(&self) -> &[Box<dyn KerasLayer>] {
159 &self.layers
160 }
161
162 pub fn compute_output_shape(&self, input_shape: &[usize]) -> Vec<usize> {
166 let mut current_shape = input_shape.to_vec();
167 for layer in &self.layers {
168 current_shape = layer.compute_output_shape(¤t_shape);
169 }
170 current_shape
171 }
172
173 pub fn build(&mut self, input_shape: Vec<usize>) -> Result<()> {
175 self.input_shape = Some(input_shape.clone());
176 let mut current_shape = input_shape;
177
178 for layer in &mut self.layers {
179 layer.build(¤t_shape)?;
180 current_shape = layer.compute_output_shape(¤t_shape);
181 }
182
183 self.built = true;
184 Ok(())
185 }
186
187 pub fn compile(
189 mut self,
190 loss: LossFunction,
191 optimizer: OptimizerType,
192 metrics: Vec<MetricType>,
193 ) -> Self {
194 self.loss = Some(loss);
195 self.optimizer = Some(optimizer);
196 self.metrics = metrics;
197 self.compiled = true;
198 self
199 }
200
201 pub fn summary(&self) -> ModelSummary {
203 let mut layers_info = Vec::new();
204 let mut total_params = 0;
205 let mut trainable_params = 0;
206
207 let mut current_shape = self.input_shape.clone().unwrap_or_default();
208
209 for layer in &self.layers {
210 let output_shape = layer.compute_output_shape(¤t_shape);
211 let params = layer.count_params();
212
213 layers_info.push(LayerInfo {
214 name: layer.name().to_string(),
215 layer_type: "Layer".to_string(),
216 output_shape: output_shape.clone(),
217 param_count: params,
218 });
219
220 total_params += params;
221 trainable_params += params;
222 current_shape = output_shape;
223 }
224
225 ModelSummary {
226 layers: layers_info,
227 total_params,
228 trainable_params,
229 non_trainable_params: 0,
230 }
231 }
232
233 pub fn predict(&self, inputs: &ArrayD<f64>) -> Result<ArrayD<f64>> {
235 if !self.built {
236 return Err(MLError::InvalidConfiguration(
237 "Model must be built before prediction".to_string(),
238 ));
239 }
240
241 let mut current = inputs.clone();
242
243 for layer in &self.layers {
244 current = layer.call(¤t)?;
245 }
246
247 Ok(current)
248 }
249
250 #[allow(non_snake_case)]
252 pub fn fit(
253 &mut self,
254 X: &ArrayD<f64>,
255 y: &ArrayD<f64>,
256 epochs: usize,
257 batch_size: Option<usize>,
258 validation_data: Option<(&ArrayD<f64>, &ArrayD<f64>)>,
259 callbacks: Vec<Box<dyn Callback>>,
260 ) -> Result<TrainingHistory> {
261 if !self.compiled {
262 return Err(MLError::InvalidConfiguration(
263 "Model must be compiled before training".to_string(),
264 ));
265 }
266
267 let batch_size = batch_size.unwrap_or(32);
268 let n_samples = X.shape()[0];
269 let n_batches = (n_samples + batch_size - 1) / batch_size;
270
271 let mut history = TrainingHistory::new();
272
273 for epoch in 0..epochs {
274 let mut epoch_loss = 0.0;
275 let mut epoch_metrics: HashMap<String, f64> = HashMap::new();
276
277 for metric in &self.metrics {
278 epoch_metrics.insert(metric.name(), 0.0);
279 }
280
281 for batch_idx in 0..n_batches {
282 let start_idx = batch_idx * batch_size;
283 let end_idx = ((batch_idx + 1) * batch_size).min(n_samples);
284
285 let X_batch = X.slice(s![start_idx..end_idx, ..]);
286 let y_batch = y.slice(s![start_idx..end_idx, ..]);
287
288 let predictions = self.predict(&X_batch.to_owned().into_dyn())?;
289
290 let loss = self.compute_loss(&predictions, &y_batch.to_owned().into_dyn())?;
291 epoch_loss += loss;
292
293 self.backward_pass(
294 &X_batch.to_owned().into_dyn(),
295 &y_batch.to_owned().into_dyn(),
296 )?;
297
298 for metric in &self.metrics {
299 let metric_value =
300 metric.compute(&predictions, &y_batch.to_owned().into_dyn())?;
301 *epoch_metrics.entry(metric.name()).or_insert(0.0) += metric_value;
302 }
303 }
304
305 epoch_loss /= n_batches as f64;
306 for value in epoch_metrics.values_mut() {
307 *value /= n_batches as f64;
308 }
309
310 let (val_loss, val_metrics) = if let Some((X_val, y_val)) = validation_data {
311 let val_predictions = self.predict(X_val)?;
312 let val_loss = self.compute_loss(&val_predictions, y_val)?;
313
314 let mut val_metrics = HashMap::new();
315 for metric in &self.metrics {
316 let metric_value = metric.compute(&val_predictions, y_val)?;
317 val_metrics.insert(format!("val_{}", metric.name()), metric_value);
318 }
319
320 (Some(val_loss), val_metrics)
321 } else {
322 (None, HashMap::new())
323 };
324
325 history.add_epoch(epoch_loss, epoch_metrics, val_loss, val_metrics);
326
327 for callback in &callbacks {
328 callback.on_epoch_end(epoch, &history)?;
329 }
330
331 println!("Epoch {}/{} - loss: {:.4}", epoch + 1, epochs, epoch_loss);
332 }
333
334 Ok(history)
335 }
336
337 #[allow(non_snake_case)]
339 pub fn evaluate(
340 &self,
341 X: &ArrayD<f64>,
342 y: &ArrayD<f64>,
343 _batch_size: Option<usize>,
344 ) -> Result<HashMap<String, f64>> {
345 let predictions = self.predict(X)?;
346 let loss = self.compute_loss(&predictions, y)?;
347
348 let mut results = HashMap::new();
349 results.insert("loss".to_string(), loss);
350
351 for metric in &self.metrics {
352 let metric_value = metric.compute(&predictions, y)?;
353 results.insert(metric.name(), metric_value);
354 }
355
356 Ok(results)
357 }
358
359 fn compute_loss(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
361 if let Some(ref loss_fn) = self.loss {
362 loss_fn.compute(predictions, targets)
363 } else {
364 Err(MLError::InvalidConfiguration(
365 "Loss function not specified".to_string(),
366 ))
367 }
368 }
369
370 fn backward_pass(&mut self, x_batch: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<()> {
380 const PERTURBATION_SCALE: f64 = 1e-3;
381 let learning_rate = self
382 .optimizer
383 .as_ref()
384 .map(optimizer_learning_rate)
385 .unwrap_or(0.01);
386
387 for layer_idx in 0..self.layers.len() {
388 let weights = self.layers[layer_idx].get_weights();
389 if weights.is_empty() {
390 continue;
391 }
392 let shapes: Vec<Vec<usize>> = weights.iter().map(|w| w.shape().to_vec()).collect();
393 let flat: Vec<f64> = weights.iter().flat_map(|w| w.iter().cloned()).collect();
394
395 let mut rng = thread_rng();
396 let direction: Vec<f64> = (0..flat.len())
397 .map(|_| if rng.random::<f64>() < 0.5 { -1.0 } else { 1.0 })
398 .collect();
399
400 let plus_flat: Vec<f64> = flat
401 .iter()
402 .zip(direction.iter())
403 .map(|(w, d)| w + PERTURBATION_SCALE * d)
404 .collect();
405 self.set_layer_weights_from_flat(layer_idx, &shapes, &plus_flat)?;
406 let predictions_plus = self.predict(x_batch)?;
407 let loss_plus = self.compute_loss(&predictions_plus, targets)?;
408
409 let minus_flat: Vec<f64> = flat
410 .iter()
411 .zip(direction.iter())
412 .map(|(w, d)| w - PERTURBATION_SCALE * d)
413 .collect();
414 self.set_layer_weights_from_flat(layer_idx, &shapes, &minus_flat)?;
415 let predictions_minus = self.predict(x_batch)?;
416 let loss_minus = self.compute_loss(&predictions_minus, targets)?;
417
418 let loss_delta = loss_plus - loss_minus;
419 let gradient: Vec<f64> = direction
420 .iter()
421 .map(|d| (loss_delta / (2.0 * PERTURBATION_SCALE)) * d)
422 .collect();
423 let updated_flat: Vec<f64> = flat
424 .iter()
425 .zip(gradient.iter())
426 .map(|(w, g)| w - learning_rate * g)
427 .collect();
428 self.set_layer_weights_from_flat(layer_idx, &shapes, &updated_flat)?;
429 }
430
431 Ok(())
432 }
433
434 fn set_layer_weights_from_flat(
437 &mut self,
438 layer_idx: usize,
439 shapes: &[Vec<usize>],
440 flat: &[f64],
441 ) -> Result<()> {
442 let mut offset = 0;
443 let mut weights = Vec::with_capacity(shapes.len());
444 for shape in shapes {
445 let len: usize = shape.iter().product();
446 let slice = flat[offset..offset + len].to_vec();
447 let array = ArrayD::from_shape_vec(IxDyn(shape), slice).map_err(|e| {
448 MLError::ComputationError(format!("Failed to reshape layer weights: {e}"))
449 })?;
450 weights.push(array);
451 offset += len;
452 }
453 self.layers[layer_idx].set_weights(weights)
454 }
455}
456
457fn optimizer_learning_rate(optimizer: &OptimizerType) -> f64 {
459 match optimizer {
460 OptimizerType::SGD { learning_rate, .. } => *learning_rate,
461 OptimizerType::Adam { learning_rate, .. } => *learning_rate,
462 OptimizerType::RMSprop { learning_rate, .. } => *learning_rate,
463 OptimizerType::AdaGrad { learning_rate, .. } => *learning_rate,
464 }
465}
466
467impl Default for Sequential {
468 fn default() -> Self {
469 Self::new()
470 }
471}
472
473#[derive(Debug, Clone)]
475pub enum LossFunction {
476 MeanSquaredError,
478 BinaryCrossentropy,
480 CategoricalCrossentropy,
482 SparseCategoricalCrossentropy,
484 MeanAbsoluteError,
486 Huber(f64),
488}
489
490impl LossFunction {
491 pub fn compute(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
493 match self {
494 LossFunction::MeanSquaredError => {
495 let diff = predictions - targets;
496 diff.mapv(|x| x * x).mean().ok_or_else(|| {
497 MLError::ComputationError("Failed to compute mean of empty array".to_string())
498 })
499 }
500 LossFunction::BinaryCrossentropy => {
501 let epsilon = 1e-15;
502 let clipped_preds = predictions.mapv(|x| x.max(epsilon).min(1.0 - epsilon));
503 let loss = targets * clipped_preds.mapv(|x| x.ln())
504 + (1.0 - targets) * clipped_preds.mapv(|x| (1.0 - x).ln());
505 loss.mean().map(|m| -m).ok_or_else(|| {
506 MLError::ComputationError("Failed to compute mean of empty array".to_string())
507 })
508 }
509 LossFunction::MeanAbsoluteError => {
510 let diff = predictions - targets;
511 diff.mapv(|x| x.abs()).mean().ok_or_else(|| {
512 MLError::ComputationError("Failed to compute mean of empty array".to_string())
513 })
514 }
515 _ => Err(MLError::InvalidConfiguration(
516 "Loss function not implemented".to_string(),
517 )),
518 }
519 }
520}
521
522#[derive(Debug, Clone)]
524pub enum OptimizerType {
525 SGD { learning_rate: f64, momentum: f64 },
527 Adam {
529 learning_rate: f64,
530 beta1: f64,
531 beta2: f64,
532 epsilon: f64,
533 },
534 RMSprop {
536 learning_rate: f64,
537 rho: f64,
538 epsilon: f64,
539 },
540 AdaGrad { learning_rate: f64, epsilon: f64 },
542}
543
544#[derive(Debug, Clone)]
546pub enum MetricType {
547 Accuracy,
549 Precision,
551 Recall,
553 F1Score,
555 MeanAbsoluteError,
557 MeanSquaredError,
559}
560
561impl MetricType {
562 pub fn name(&self) -> String {
564 match self {
565 MetricType::Accuracy => "accuracy".to_string(),
566 MetricType::Precision => "precision".to_string(),
567 MetricType::Recall => "recall".to_string(),
568 MetricType::F1Score => "f1_score".to_string(),
569 MetricType::MeanAbsoluteError => "mean_absolute_error".to_string(),
570 MetricType::MeanSquaredError => "mean_squared_error".to_string(),
571 }
572 }
573
574 pub fn compute(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
576 match self {
577 MetricType::Accuracy => {
578 let pred_classes = predictions.mapv(|x| if x > 0.5 { 1.0 } else { 0.0 });
579 let correct = pred_classes
580 .iter()
581 .zip(targets.iter())
582 .filter(|(&pred, &target)| (pred - target).abs() < 1e-6)
583 .count();
584 Ok(correct as f64 / targets.len() as f64)
585 }
586 MetricType::MeanAbsoluteError => {
587 let diff = predictions - targets;
588 diff.mapv(|x| x.abs()).mean().ok_or_else(|| {
589 MLError::ComputationError("Failed to compute mean of empty array".to_string())
590 })
591 }
592 MetricType::MeanSquaredError => {
593 let diff = predictions - targets;
594 diff.mapv(|x| x * x).mean().ok_or_else(|| {
595 MLError::ComputationError("Failed to compute mean of empty array".to_string())
596 })
597 }
598 MetricType::Precision => {
599 let true_positives = predictions
600 .iter()
601 .zip(targets.iter())
602 .filter(|(&pred, &target)| pred > 0.5 && target > 0.5)
603 .count() as f64;
604 let predicted_positives =
605 predictions.iter().filter(|&&pred| pred > 0.5).count() as f64;
606 if predicted_positives > 0.0 {
607 Ok(true_positives / predicted_positives)
608 } else {
609 Ok(0.0)
610 }
611 }
612 MetricType::Recall => {
613 let true_positives = predictions
614 .iter()
615 .zip(targets.iter())
616 .filter(|(&pred, &target)| pred > 0.5 && target > 0.5)
617 .count() as f64;
618 let actual_positives =
619 targets.iter().filter(|&&target| target > 0.5).count() as f64;
620 if actual_positives > 0.0 {
621 Ok(true_positives / actual_positives)
622 } else {
623 Ok(0.0)
624 }
625 }
626 MetricType::F1Score => {
627 let precision = MetricType::Precision.compute(predictions, targets)?;
628 let recall = MetricType::Recall.compute(predictions, targets)?;
629 if precision + recall > 0.0 {
630 Ok(2.0 * precision * recall / (precision + recall))
631 } else {
632 Ok(0.0)
633 }
634 }
635 }
636 }
637}
638
639#[derive(Debug, Clone)]
641pub struct TrainingHistory {
642 pub loss: Vec<f64>,
644 pub metrics: Vec<HashMap<String, f64>>,
646 pub val_loss: Vec<f64>,
648 pub val_metrics: Vec<HashMap<String, f64>>,
650}
651
652impl TrainingHistory {
653 pub fn new() -> Self {
655 Self {
656 loss: Vec::new(),
657 metrics: Vec::new(),
658 val_loss: Vec::new(),
659 val_metrics: Vec::new(),
660 }
661 }
662
663 pub fn add_epoch(
665 &mut self,
666 loss: f64,
667 metrics: HashMap<String, f64>,
668 val_loss: Option<f64>,
669 val_metrics: HashMap<String, f64>,
670 ) {
671 self.loss.push(loss);
672 self.metrics.push(metrics);
673
674 if let Some(val_loss) = val_loss {
675 self.val_loss.push(val_loss);
676 }
677 self.val_metrics.push(val_metrics);
678 }
679}
680
681impl Default for TrainingHistory {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687#[derive(Debug)]
689pub struct ModelSummary {
690 pub layers: Vec<LayerInfo>,
692 pub total_params: usize,
694 pub trainable_params: usize,
696 pub non_trainable_params: usize,
698}
699
700#[derive(Debug)]
702pub struct LayerInfo {
703 pub name: String,
705 pub layer_type: String,
707 pub output_shape: Vec<usize>,
709 pub param_count: usize,
711}
712
713pub struct Input {
715 pub shape: Vec<usize>,
717 pub name: Option<String>,
719 pub dtype: DataType,
721}
722
723impl Input {
724 pub fn new(shape: Vec<usize>) -> Self {
726 Self {
727 shape,
728 name: None,
729 dtype: DataType::Float64,
730 }
731 }
732
733 pub fn name(mut self, name: impl Into<String>) -> Self {
735 self.name = Some(name.into());
736 self
737 }
738
739 pub fn dtype(mut self, dtype: DataType) -> Self {
741 self.dtype = dtype;
742 self
743 }
744}
745
746#[derive(Debug, Clone)]
748pub enum DataType {
749 Float32,
751 Float64,
753 Int32,
755 Int64,
757}
758
759pub mod utils {
761 use super::*;
762
763 pub fn create_classification_model(
765 _input_dim: usize,
766 num_classes: usize,
767 hidden_layers: Vec<usize>,
768 ) -> Sequential {
769 let mut model = Sequential::new();
770
771 for (i, &units) in hidden_layers.iter().enumerate() {
772 model.add(Box::new(
773 Dense::new(units)
774 .activation(ActivationFunction::ReLU)
775 .name(format!("dense_{}", i)),
776 ));
777 }
778
779 let output_activation = if num_classes == 2 {
780 ActivationFunction::Sigmoid
781 } else {
782 ActivationFunction::Softmax
783 };
784
785 model.add(Box::new(
786 Dense::new(num_classes)
787 .activation(output_activation)
788 .name("output"),
789 ));
790
791 model
792 }
793
794 pub fn create_quantum_model(
796 num_qubits: usize,
797 num_classes: usize,
798 num_layers: usize,
799 ) -> Sequential {
800 let mut model = Sequential::new();
801
802 model.add(Box::new(
803 QuantumDense::new(num_qubits, num_classes)
804 .num_layers(num_layers)
805 .ansatz_type(QuantumAnsatzType::HardwareEfficient)
806 .name("quantum_layer"),
807 ));
808
809 if num_classes > 1 {
810 model.add(Box::new(
811 Activation::new(ActivationFunction::Softmax).name("softmax"),
812 ));
813 }
814
815 model
816 }
817
818 pub fn create_hybrid_model(
820 _input_dim: usize,
821 num_qubits: usize,
822 num_classes: usize,
823 classical_hidden: Vec<usize>,
824 ) -> Sequential {
825 let mut model = Sequential::new();
826
827 for (i, &units) in classical_hidden.iter().enumerate() {
828 model.add(Box::new(
829 Dense::new(units)
830 .activation(ActivationFunction::ReLU)
831 .name(format!("classical_{}", i)),
832 ));
833 }
834
835 model.add(Box::new(
836 QuantumDense::new(num_qubits, 64)
837 .num_layers(2)
838 .ansatz_type(QuantumAnsatzType::HardwareEfficient)
839 .name("quantum_layer"),
840 ));
841
842 model.add(Box::new(
843 Dense::new(num_classes)
844 .activation(if num_classes == 2 {
845 ActivationFunction::Sigmoid
846 } else {
847 ActivationFunction::Softmax
848 })
849 .name("output"),
850 ));
851
852 model
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use scirs2_core::ndarray::Array;
860
861 #[test]
862 fn test_dense_layer() {
863 let mut dense = Dense::new(10)
864 .activation(ActivationFunction::ReLU)
865 .name("test_dense");
866
867 assert!(!dense.built());
868
869 dense.build(&[5]).expect("Should build successfully");
870
871 assert!(dense.built());
872 assert_eq!(dense.compute_output_shape(&[32, 5]), vec![32, 10]);
873 }
874
875 #[test]
876 fn test_sequential_model() {
877 let mut model = Sequential::new();
878 model.add(Box::new(Dense::new(10)));
879 model.add(Box::new(Activation::new(ActivationFunction::ReLU)));
880 model.add(Box::new(Dense::new(5)));
881
882 model
883 .build(vec![32, 20])
884 .expect("Should build successfully");
885
886 let summary = model.summary();
887 assert_eq!(summary.layers.len(), 3);
888 }
889
890 #[test]
891 fn test_activation_functions() {
892 let relu = ActivationFunction::ReLU;
893 let sigmoid = ActivationFunction::Sigmoid;
894 let _tanh = ActivationFunction::Tanh;
895
896 let mut act_relu = Activation::new(relu);
897 act_relu.build(&[10]).expect("Should build");
898
899 let mut act_sigmoid = Activation::new(sigmoid);
900 act_sigmoid.build(&[10]).expect("Should build");
901 }
902
903 #[test]
907 fn test_fit_updates_weights_and_reduces_loss() {
908 let mut model = Sequential::new();
909 model.add(Box::new(Dense::new(2).name("dense1")));
910 model.build(vec![4, 3]).expect("build should succeed");
911 let mut model = model.compile(
912 LossFunction::MeanSquaredError,
913 OptimizerType::SGD {
914 learning_rate: 0.5,
915 momentum: 0.0,
916 },
917 vec![],
918 );
919
920 let x = Array::from_shape_vec(
921 IxDyn(&[4, 3]),
922 vec![0.1, 0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0.4],
923 )
924 .expect("valid shape");
925 let y = Array::from_shape_vec(IxDyn(&[4, 2]), vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0])
926 .expect("valid shape");
927
928 let weights_before = model.layers[0].get_weights();
929 let initial_predictions = model.predict(&x).expect("predict should succeed");
930 let initial_loss = model
931 .compute_loss(&initial_predictions, &y)
932 .expect("loss should compute");
933
934 let history = model
935 .fit(&x, &y, 150, Some(4), None, vec![])
936 .expect("fit should succeed");
937
938 let weights_after = model.layers[0].get_weights();
939 let weights_changed = weights_before[0]
940 .iter()
941 .zip(weights_after[0].iter())
942 .any(|(a, b)| (a - b).abs() > 1e-9);
943 assert!(
944 weights_changed,
945 "expected Dense layer weights to change after fit"
946 );
947
948 let final_loss = *history.loss.last().expect("history should have losses");
949 assert!(
950 final_loss < initial_loss,
951 "expected training loss to decrease: initial={initial_loss}, final={final_loss}"
952 );
953 }
954
955 #[test]
958 fn test_precision_recall_f1_are_computed() {
959 let predictions =
960 Array::from_shape_vec(IxDyn(&[4]), vec![0.9, 0.1, 0.8, 0.3]).expect("valid shape");
961 let targets =
962 Array::from_shape_vec(IxDyn(&[4]), vec![1.0, 0.0, 0.0, 1.0]).expect("valid shape");
963
964 let precision = MetricType::Precision
965 .compute(&predictions, &targets)
966 .expect("precision should compute");
967 let recall = MetricType::Recall
968 .compute(&predictions, &targets)
969 .expect("recall should compute");
970 let f1 = MetricType::F1Score
971 .compute(&predictions, &targets)
972 .expect("f1 should compute");
973
974 assert!((precision - 0.5).abs() < 1e-9, "precision was {precision}");
977 assert!((recall - 0.5).abs() < 1e-9, "recall was {recall}");
980 assert!((f1 - 0.5).abs() < 1e-9, "f1 was {f1}");
982 }
983}