1#![allow(non_snake_case)] use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
10use scirs2_core::random::thread_rng;
11use scirs2_core::random::RandNormal;
12use sklears_core::{
13 error::{Result as SklResult, SklearsError},
14 traits::{Estimator, Fit, Predict, Untrained},
15 types::Float,
16};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Copy, PartialEq, Default)]
21pub enum ConsistencyEnforcement {
22 #[default]
24 PostProcessing,
25 ConstrainedTraining,
27 BayesianInference,
29}
30
31#[derive(Debug, Clone)]
63pub struct OntologyAwareClassifier<S = Untrained> {
64 state: S,
65 ontology: HashMap<usize, Vec<usize>>,
66 consistency_enforcement: ConsistencyEnforcement,
67 base_classifier_learning_rate: Float,
68 max_iterations: usize,
69}
70
71#[derive(Debug, Clone)]
73pub struct OntologyAwareClassifierTrained {
74 weights: Array2<Float>,
75 biases: Array1<Float>,
76 ontology: HashMap<usize, Vec<usize>>,
77 consistency_enforcement: ConsistencyEnforcement,
78 n_features: usize,
79 n_labels: usize,
80}
81
82impl OntologyAwareClassifier<Untrained> {
83 pub fn new() -> Self {
85 Self {
86 state: Untrained,
87 ontology: HashMap::new(),
88 consistency_enforcement: ConsistencyEnforcement::PostProcessing,
89 base_classifier_learning_rate: 0.01,
90 max_iterations: 100,
91 }
92 }
93
94 pub fn ontology(mut self, ontology: HashMap<usize, Vec<usize>>) -> Self {
96 self.ontology = ontology;
97 self
98 }
99
100 pub fn consistency_enforcement(mut self, enforcement: ConsistencyEnforcement) -> Self {
102 self.consistency_enforcement = enforcement;
103 self
104 }
105
106 pub fn base_classifier_learning_rate(mut self, learning_rate: Float) -> Self {
108 self.base_classifier_learning_rate = learning_rate;
109 self
110 }
111
112 pub fn max_iterations(mut self, max_iterations: usize) -> Self {
114 self.max_iterations = max_iterations;
115 self
116 }
117}
118
119impl Default for OntologyAwareClassifier<Untrained> {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl Estimator for OntologyAwareClassifier<Untrained> {
126 type Config = ();
127 type Error = SklearsError;
128 type Float = Float;
129
130 fn config(&self) -> &Self::Config {
131 &()
132 }
133}
134
135impl Fit<ArrayView2<'_, Float>, Array2<i32>> for OntologyAwareClassifier<Untrained> {
136 type Fitted = OntologyAwareClassifier<OntologyAwareClassifierTrained>;
137
138 fn fit(
139 self,
140 X: &ArrayView2<'_, Float>,
141 y: &Array2<i32>,
142 ) -> SklResult<OntologyAwareClassifier<OntologyAwareClassifierTrained>> {
143 let (n_samples, n_features) = X.dim();
144 let n_labels = y.ncols();
145
146 if n_samples != y.nrows() {
147 return Err(SklearsError::InvalidInput(
148 "X and y must have the same number of samples".to_string(),
149 ));
150 }
151
152 let mut weights = Array2::<Float>::zeros((n_features, n_labels));
154 let mut biases = Array1::<Float>::zeros(n_labels);
155
156 for iteration in 0..self.max_iterations {
158 let mut total_loss = 0.0;
159
160 for sample_idx in 0..n_samples {
161 let x = X.row(sample_idx);
162 let y_true = y.row(sample_idx);
163
164 let logits = x.dot(&weights) + &biases;
166 let probabilities = logits.mapv(|x| 1.0 / (1.0 + (-x).exp()));
167
168 let consistent_probabilities = match self.consistency_enforcement {
170 ConsistencyEnforcement::ConstrainedTraining => {
171 self.enforce_consistency_training(&probabilities)?
172 }
173 _ => probabilities.clone(),
174 };
175
176 for label_idx in 0..n_labels {
178 let y_label = y_true[label_idx] as Float;
179 let prob = consistent_probabilities[label_idx];
180 let error = prob - y_label;
181
182 total_loss += if y_label == 1.0 {
183 -prob.ln()
184 } else {
185 -(1.0 - prob).ln()
186 };
187
188 for feat_idx in 0..n_features {
190 weights[[feat_idx, label_idx]] -=
191 self.base_classifier_learning_rate * error * x[feat_idx];
192 }
193 biases[label_idx] -= self.base_classifier_learning_rate * error;
194 }
195 }
196
197 if iteration > 0 && total_loss < 1e-6 {
198 break;
199 }
200 }
201
202 Ok(OntologyAwareClassifier {
203 state: OntologyAwareClassifierTrained {
204 weights,
205 biases,
206 ontology: self.ontology,
207 consistency_enforcement: self.consistency_enforcement,
208 n_features,
209 n_labels,
210 },
211 ontology: HashMap::new(),
212 consistency_enforcement: self.consistency_enforcement,
213 base_classifier_learning_rate: self.base_classifier_learning_rate,
214 max_iterations: self.max_iterations,
215 })
216 }
217}
218
219impl OntologyAwareClassifier<Untrained> {
220 fn enforce_consistency_training(
222 &self,
223 probabilities: &Array1<Float>,
224 ) -> SklResult<Array1<Float>> {
225 let mut consistent_probs = probabilities.clone();
226
227 for (&child, parents) in &self.ontology {
229 if child < probabilities.len() {
230 for &parent in parents {
231 if parent < probabilities.len() {
232 let child_prob = probabilities[child];
233 if consistent_probs[parent] < child_prob {
234 consistent_probs[parent] = child_prob;
235 }
236 }
237 }
238 }
239 }
240
241 Ok(consistent_probs)
242 }
243}
244
245impl Predict<ArrayView2<'_, Float>, Array2<i32>>
246 for OntologyAwareClassifier<OntologyAwareClassifierTrained>
247{
248 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
249 let (n_samples, n_features) = X.dim();
250
251 if n_features != self.state.n_features {
252 return Err(SklearsError::InvalidInput(
253 "X has different number of features than training data".to_string(),
254 ));
255 }
256
257 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
258
259 for sample_idx in 0..n_samples {
260 let x = X.row(sample_idx);
261
262 let logits = x.dot(&self.state.weights) + &self.state.biases;
264 let probabilities = logits.mapv(|x| 1.0 / (1.0 + (-x).exp()));
265
266 let consistent_probs = match self.state.consistency_enforcement {
268 ConsistencyEnforcement::PostProcessing => {
269 self.enforce_consistency_postprocessing(&probabilities)?
270 }
271 ConsistencyEnforcement::BayesianInference => {
272 self.enforce_consistency_bayesian(&probabilities)?
273 }
274 _ => probabilities,
275 };
276
277 for label_idx in 0..self.state.n_labels {
279 predictions[[sample_idx, label_idx]] = if consistent_probs[label_idx] > 0.5 {
280 1
281 } else {
282 0
283 };
284 }
285 }
286
287 Ok(predictions)
288 }
289}
290
291impl OntologyAwareClassifier<OntologyAwareClassifierTrained> {
292 pub fn weights(&self) -> &Array2<Float> {
294 &self.state.weights
295 }
296
297 pub fn biases(&self) -> &Array1<Float> {
299 &self.state.biases
300 }
301
302 pub fn ontology(&self) -> &HashMap<usize, Vec<usize>> {
304 &self.state.ontology
305 }
306
307 fn enforce_consistency_postprocessing(
309 &self,
310 probabilities: &Array1<Float>,
311 ) -> SklResult<Array1<Float>> {
312 let mut consistent_probs = probabilities.clone();
313
314 for (&child, parents) in &self.state.ontology {
316 if child < probabilities.len() && probabilities[child] > 0.5 {
317 for &parent in parents {
318 if parent < probabilities.len() {
319 consistent_probs[parent] =
320 consistent_probs[parent].max(probabilities[child]);
321 }
322 }
323 }
324 }
325
326 Ok(consistent_probs)
327 }
328
329 fn enforce_consistency_bayesian(
331 &self,
332 probabilities: &Array1<Float>,
333 ) -> SklResult<Array1<Float>> {
334 let mut consistent_probs = probabilities.clone();
335
336 for (&child, parents) in &self.state.ontology {
338 if child < probabilities.len() {
339 let child_prob = probabilities[child];
340 for &parent in parents {
341 if parent < probabilities.len() {
342 consistent_probs[parent] = consistent_probs[parent].max(child_prob * 0.8);
345 }
346 }
347 }
348 }
349
350 Ok(consistent_probs)
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Default)]
356pub enum CostStrategy {
357 #[default]
359 Uniform,
360 DistanceBased,
362 Custom,
364}
365
366#[derive(Debug, Clone)]
398pub struct CostSensitiveHierarchicalClassifier<S = Untrained> {
399 state: S,
400 hierarchy: HashMap<usize, Vec<usize>>,
401 cost_strategy: CostStrategy,
402 cost_matrix: Option<Array2<Float>>,
403 learning_rate: Float,
404 max_iterations: usize,
405 lambda_hierarchy: Float,
406 lambda_cost: Float,
407}
408
409#[derive(Debug, Clone)]
411pub struct CostSensitiveHierarchicalClassifierTrained {
412 weights: Array2<Float>,
413 hierarchy: HashMap<usize, Vec<usize>>,
415 #[allow(dead_code)]
416 cost_strategy: CostStrategy,
417 cost_matrix: Option<Array2<Float>>,
418 n_features: usize,
419 n_labels: usize,
420 #[allow(dead_code)]
421 lambda_hierarchy: Float,
422 #[allow(dead_code)]
423 lambda_cost: Float,
424}
425
426impl CostSensitiveHierarchicalClassifier<Untrained> {
427 pub fn new() -> Self {
429 Self {
430 state: Untrained,
431 hierarchy: HashMap::new(),
432 cost_strategy: CostStrategy::Uniform,
433 cost_matrix: None,
434 learning_rate: 0.01,
435 max_iterations: 100,
436 lambda_hierarchy: 1.0,
437 lambda_cost: 1.0,
438 }
439 }
440
441 pub fn hierarchy(mut self, hierarchy: HashMap<usize, Vec<usize>>) -> Self {
443 self.hierarchy = hierarchy;
444 self
445 }
446
447 pub fn cost_strategy(mut self, strategy: CostStrategy) -> Self {
449 self.cost_strategy = strategy;
450 self
451 }
452
453 pub fn cost_matrix(mut self, cost_matrix: Array2<Float>) -> Self {
455 self.cost_matrix = Some(cost_matrix);
456 self
457 }
458
459 pub fn learning_rate(mut self, learning_rate: Float) -> Self {
461 self.learning_rate = learning_rate;
462 self
463 }
464
465 pub fn max_iterations(mut self, max_iterations: usize) -> Self {
467 self.max_iterations = max_iterations;
468 self
469 }
470
471 pub fn lambda_hierarchy(mut self, lambda: Float) -> Self {
473 self.lambda_hierarchy = lambda;
474 self
475 }
476
477 pub fn lambda_cost(mut self, lambda: Float) -> Self {
479 self.lambda_cost = lambda;
480 self
481 }
482}
483
484impl Default for CostSensitiveHierarchicalClassifier<Untrained> {
485 fn default() -> Self {
486 Self::new()
487 }
488}
489
490impl Estimator for CostSensitiveHierarchicalClassifier<Untrained> {
491 type Config = ();
492 type Error = SklearsError;
493 type Float = Float;
494
495 fn config(&self) -> &Self::Config {
496 &()
497 }
498}
499
500impl Fit<ArrayView2<'_, Float>, Array2<i32>> for CostSensitiveHierarchicalClassifier<Untrained> {
501 type Fitted = CostSensitiveHierarchicalClassifier<CostSensitiveHierarchicalClassifierTrained>;
502
503 fn fit(
504 self,
505 X: &ArrayView2<'_, Float>,
506 y: &Array2<i32>,
507 ) -> SklResult<CostSensitiveHierarchicalClassifier<CostSensitiveHierarchicalClassifierTrained>>
508 {
509 let (n_samples, n_features) = X.dim();
510 let n_labels = y.ncols();
511
512 if n_samples != y.nrows() {
513 return Err(SklearsError::InvalidInput(
514 "X and y must have the same number of samples".to_string(),
515 ));
516 }
517
518 let cost_matrix = match &self.cost_matrix {
520 Some(matrix) => matrix.clone(),
521 None => self.generate_cost_matrix(n_labels)?,
522 };
523
524 let mut weights = Array2::<Float>::zeros((n_features, n_labels));
526
527 for _iteration in 0..self.max_iterations {
529 for sample_idx in 0..n_samples {
530 let x = X.row(sample_idx);
531 let y_true = y.row(sample_idx);
532
533 let scores = x.dot(&weights);
535 let probabilities = scores.mapv(|x| 1.0 / (1.0 + (-x).exp()));
536
537 for label_idx in 0..n_labels {
539 let y_label = y_true[label_idx] as Float;
540 let prob = probabilities[label_idx];
541
542 let mut gradient = prob - y_label;
544
545 let cost_weight = cost_matrix[[label_idx, label_idx]];
547 gradient *= cost_weight * self.lambda_cost;
548
549 gradient += self.lambda_hierarchy
551 * self.hierarchical_gradient(label_idx, &probabilities, &y_true)?;
552
553 for feat_idx in 0..n_features {
555 weights[[feat_idx, label_idx]] -=
556 self.learning_rate * gradient * x[feat_idx];
557 }
558 }
559 }
560 }
561
562 Ok(CostSensitiveHierarchicalClassifier {
563 state: CostSensitiveHierarchicalClassifierTrained {
564 weights,
565 hierarchy: self.hierarchy,
566 cost_strategy: self.cost_strategy,
567 cost_matrix: Some(cost_matrix),
568 n_features,
569 n_labels,
570 lambda_hierarchy: self.lambda_hierarchy,
571 lambda_cost: self.lambda_cost,
572 },
573 hierarchy: HashMap::new(),
574 cost_strategy: self.cost_strategy,
575 cost_matrix: None,
576 learning_rate: self.learning_rate,
577 max_iterations: self.max_iterations,
578 lambda_hierarchy: self.lambda_hierarchy,
579 lambda_cost: self.lambda_cost,
580 })
581 }
582}
583
584impl CostSensitiveHierarchicalClassifier<Untrained> {
585 fn generate_cost_matrix(&self, n_labels: usize) -> SklResult<Array2<Float>> {
587 match self.cost_strategy {
588 CostStrategy::Uniform => Ok(Array2::eye(n_labels)),
589 CostStrategy::DistanceBased => {
590 let mut cost_matrix = Array2::<Float>::zeros((n_labels, n_labels));
591 for i in 0..n_labels {
593 for j in 0..n_labels {
594 cost_matrix[[i, j]] = if i == j { 1.0 } else { 0.5 };
595 }
596 }
597 Ok(cost_matrix)
598 }
599 CostStrategy::Custom => Err(SklearsError::InvalidInput(
600 "Custom cost strategy requires a cost matrix".to_string(),
601 )),
602 }
603 }
604
605 fn hierarchical_gradient(
607 &self,
608 label_idx: usize,
609 probabilities: &Array1<Float>,
610 y_true: &ArrayView1<i32>,
611 ) -> SklResult<Float> {
612 let mut gradient = 0.0;
613
614 if let Some(children) = self.hierarchy.get(&label_idx) {
616 for &child in children {
617 if child < probabilities.len() {
618 let parent_prob = probabilities[label_idx];
619 let child_prob = probabilities[child];
620 let child_true = y_true[child] as Float;
621
622 if child_true > 0.5 && child_prob > parent_prob {
624 gradient += child_prob - parent_prob;
625 }
626 }
627 }
628 }
629
630 for (&parent, children) in &self.hierarchy {
632 if children.contains(&label_idx) && parent < probabilities.len() {
633 let parent_prob = probabilities[parent];
634 let child_prob = probabilities[label_idx];
635 let label_true = y_true[label_idx] as Float;
636
637 if label_true > 0.5 && child_prob > parent_prob {
639 gradient -= child_prob - parent_prob;
640 }
641 }
642 }
643
644 Ok(gradient)
645 }
646}
647
648impl Predict<ArrayView2<'_, Float>, Array2<i32>>
649 for CostSensitiveHierarchicalClassifier<CostSensitiveHierarchicalClassifierTrained>
650{
651 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
652 let (n_samples, n_features) = X.dim();
653
654 if n_features != self.state.n_features {
655 return Err(SklearsError::InvalidInput(
656 "X has different number of features than training data".to_string(),
657 ));
658 }
659
660 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
661
662 for sample_idx in 0..n_samples {
663 let x = X.row(sample_idx);
664
665 let scores = x.dot(&self.state.weights);
667 let probabilities = scores.mapv(|x| 1.0 / (1.0 + (-x).exp()));
668
669 let final_predictions = self.apply_constraints(&probabilities)?;
671
672 for label_idx in 0..self.state.n_labels {
673 predictions[[sample_idx, label_idx]] = final_predictions[label_idx];
674 }
675 }
676
677 Ok(predictions)
678 }
679}
680
681impl CostSensitiveHierarchicalClassifier<CostSensitiveHierarchicalClassifierTrained> {
682 pub fn weights(&self) -> &Array2<Float> {
684 &self.state.weights
685 }
686
687 pub fn cost_matrix(&self) -> Option<&Array2<Float>> {
689 self.state.cost_matrix.as_ref()
690 }
691
692 fn apply_constraints(&self, probabilities: &Array1<Float>) -> SklResult<Array1<i32>> {
694 let mut binary_predictions = Array1::<i32>::zeros(probabilities.len());
695
696 for i in 0..probabilities.len() {
698 let threshold = if let Some(cost_matrix) = &self.state.cost_matrix {
699 let cost = cost_matrix[[i, i]];
701 0.5 / cost.max(0.1) } else {
703 0.5
704 };
705
706 binary_predictions[i] = if probabilities[i] > threshold { 1 } else { 0 };
707 }
708
709 for (&parent, children) in &self.state.hierarchy {
711 if parent < binary_predictions.len() {
712 let mut any_child_predicted = false;
714 for &child in children {
715 if child < binary_predictions.len() && binary_predictions[child] == 1 {
716 any_child_predicted = true;
717 break;
718 }
719 }
720 if any_child_predicted {
721 binary_predictions[parent] = 1;
722 }
723 }
724 }
725
726 Ok(binary_predictions)
727 }
728}
729
730#[derive(Debug, Clone, Copy, PartialEq)]
734pub enum AggregationFunction {
735 Mean,
737 Sum,
739 Max,
741 Attention,
743}
744
745#[derive(Debug, Clone, Copy, PartialEq)]
747pub enum MessagePassingVariant {
748 GCN,
750 GAT,
752 GraphSAGE,
754 GIN,
756}
757
758#[derive(Debug, Clone)]
788pub struct GraphNeuralNetwork<S = Untrained> {
789 state: S,
790 hidden_dim: usize,
791 num_layers: usize,
792 message_passing_variant: MessagePassingVariant,
793 aggregation_function: AggregationFunction,
794 learning_rate: Float,
795 max_iter: usize,
796 dropout_rate: Float,
797 random_state: Option<u64>,
798}
799
800#[derive(Debug, Clone)]
802pub struct GraphNeuralNetworkTrained {
803 layer_weights: Vec<Array2<Float>>,
805 layer_biases: Vec<Array1<Float>>,
807 attention_weights: Option<Vec<Array2<Float>>>,
809 hidden_dim: usize,
811 num_layers: usize,
812 message_passing_variant: MessagePassingVariant,
813 #[allow(dead_code)]
814 aggregation_function: AggregationFunction,
815 n_features: usize,
816 #[allow(dead_code)]
817 n_outputs: usize,
818 #[allow(dead_code)]
819 dropout_rate: Float,
820}
821
822impl GraphNeuralNetwork<Untrained> {
823 pub fn new() -> Self {
825 Self {
826 state: Untrained,
827 hidden_dim: 32,
828 num_layers: 2,
829 message_passing_variant: MessagePassingVariant::GCN,
830 aggregation_function: AggregationFunction::Mean,
831 learning_rate: 0.01,
832 max_iter: 100,
833 dropout_rate: 0.0,
834 random_state: None,
835 }
836 }
837
838 pub fn hidden_dim(mut self, hidden_dim: usize) -> Self {
840 self.hidden_dim = hidden_dim;
841 self
842 }
843
844 pub fn num_layers(mut self, num_layers: usize) -> Self {
846 self.num_layers = num_layers;
847 self
848 }
849
850 pub fn message_passing_variant(mut self, variant: MessagePassingVariant) -> Self {
852 self.message_passing_variant = variant;
853 self
854 }
855
856 pub fn aggregation_function(mut self, function: AggregationFunction) -> Self {
858 self.aggregation_function = function;
859 self
860 }
861
862 pub fn learning_rate(mut self, learning_rate: Float) -> Self {
864 self.learning_rate = learning_rate;
865 self
866 }
867
868 pub fn max_iter(mut self, max_iter: usize) -> Self {
870 self.max_iter = max_iter;
871 self
872 }
873
874 pub fn dropout_rate(mut self, dropout_rate: Float) -> Self {
876 self.dropout_rate = dropout_rate;
877 self
878 }
879
880 pub fn random_state(mut self, random_state: u64) -> Self {
882 self.random_state = Some(random_state);
883 self
884 }
885}
886
887impl Default for GraphNeuralNetwork<Untrained> {
888 fn default() -> Self {
889 Self::new()
890 }
891}
892
893impl Estimator for GraphNeuralNetwork<Untrained> {
894 type Config = ();
895 type Error = SklearsError;
896 type Float = Float;
897
898 fn config(&self) -> &Self::Config {
899 &()
900 }
901}
902
903impl GraphNeuralNetwork<Untrained> {
905 pub fn fit_graph(
907 self,
908 adjacency: &ArrayView2<'_, i32>,
909 node_features: &ArrayView2<'_, Float>,
910 node_labels: &Array2<i32>,
911 ) -> SklResult<GraphNeuralNetwork<GraphNeuralNetworkTrained>> {
912 let (n_nodes, n_features) = node_features.dim();
913 let n_outputs = node_labels.ncols();
914
915 if adjacency.dim() != (n_nodes, n_nodes) {
916 return Err(SklearsError::InvalidInput(
917 "Adjacency matrix must be n_nodes x n_nodes".to_string(),
918 ));
919 }
920
921 if node_labels.nrows() != n_nodes {
922 return Err(SklearsError::InvalidInput(
923 "Node labels must have same number of rows as nodes".to_string(),
924 ));
925 }
926
927 let mut rng_instance = thread_rng();
929 let (layer_weights, layer_biases, attention_weights) =
930 self.initialize_gnn_parameters(n_features, n_outputs, &mut rng_instance)?;
931
932 let mut weights = layer_weights;
934 let biases = layer_biases;
935 let attention_weights = attention_weights;
936
937 for _iteration in 0..self.max_iter {
938 let (node_embeddings, _) = self.forward_pass_graph(
940 adjacency,
941 node_features,
942 &weights,
943 &biases,
944 &attention_weights,
945 )?;
946
947 let _predictions = node_embeddings.mapv(|x| if x > 0.0 { 1 } else { 0 });
949
950 for weight in &mut weights {
952 for i in 0..weight.nrows() {
953 for j in 0..weight.ncols() {
954 weight[[i, j]] *= 0.999; }
956 }
957 }
958 }
959
960 let trained_state = GraphNeuralNetworkTrained {
961 layer_weights: weights,
962 layer_biases: biases,
963 attention_weights,
964 hidden_dim: self.hidden_dim,
965 num_layers: self.num_layers,
966 message_passing_variant: self.message_passing_variant,
967 aggregation_function: self.aggregation_function,
968 n_features,
969 n_outputs,
970 dropout_rate: self.dropout_rate,
971 };
972
973 Ok(GraphNeuralNetwork {
974 state: trained_state,
975 hidden_dim: self.hidden_dim,
976 num_layers: self.num_layers,
977 message_passing_variant: self.message_passing_variant,
978 aggregation_function: self.aggregation_function,
979 learning_rate: self.learning_rate,
980 max_iter: self.max_iter,
981 dropout_rate: self.dropout_rate,
982 random_state: self.random_state,
983 })
984 }
985
986 #[allow(clippy::type_complexity)]
988 fn initialize_gnn_parameters(
989 &self,
990 n_features: usize,
991 n_outputs: usize,
992 rng: &mut scirs2_core::random::CoreRandom,
993 ) -> SklResult<(
994 Vec<Array2<Float>>,
995 Vec<Array1<Float>>,
996 Option<Vec<Array2<Float>>>,
997 )> {
998 let mut layer_weights = Vec::new();
999 let mut layer_biases = Vec::new();
1000 let mut attention_weights = None;
1001
1002 let input_dim = match self.message_passing_variant {
1004 MessagePassingVariant::GraphSAGE => n_features * 2, _ => n_features,
1006 };
1007
1008 let hidden_dim = match self.message_passing_variant {
1010 MessagePassingVariant::GraphSAGE => self.hidden_dim * 2, _ => self.hidden_dim,
1012 };
1013
1014 for layer_idx in 0..self.num_layers {
1016 let (in_dim, out_dim) = if layer_idx == 0 {
1017 (input_dim, self.hidden_dim)
1018 } else if layer_idx == self.num_layers - 1 {
1019 (hidden_dim, n_outputs)
1020 } else {
1021 (hidden_dim, self.hidden_dim)
1022 };
1023
1024 let normal_dist = RandNormal::new(0.0, (2.0 / in_dim as Float).sqrt())
1025 .expect("operation should succeed");
1026 let mut input_weight = Array2::<Float>::zeros((in_dim, out_dim));
1027 for i in 0..in_dim {
1028 for j in 0..out_dim {
1029 input_weight[[i, j]] = rng.sample(normal_dist);
1030 }
1031 }
1032 let bias = Array1::<Float>::zeros(out_dim);
1033
1034 layer_weights.push(input_weight);
1035 layer_biases.push(bias);
1036 }
1037
1038 if self.message_passing_variant == MessagePassingVariant::GAT {
1040 let mut att_weights = Vec::new();
1041 for layer_idx in 0..self.num_layers {
1042 let att_dim = if layer_idx == 0 {
1043 n_features
1044 } else {
1045 self.hidden_dim
1046 };
1047 let att_normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
1048 let mut attention_weight = Array2::<Float>::zeros((att_dim * 2, 1));
1049 for i in 0..(att_dim * 2) {
1050 attention_weight[[i, 0]] = rng.sample(att_normal_dist);
1051 }
1052 att_weights.push(attention_weight);
1053 }
1054 attention_weights = Some(att_weights);
1055 }
1056
1057 Ok((layer_weights, layer_biases, attention_weights))
1058 }
1059
1060 fn forward_pass_graph(
1062 &self,
1063 adjacency: &ArrayView2<'_, i32>,
1064 node_features: &ArrayView2<'_, Float>,
1065 weights: &[Array2<Float>],
1066 biases: &[Array1<Float>],
1067 attention_weights: &Option<Vec<Array2<Float>>>,
1068 ) -> SklResult<(Array2<Float>, Vec<Array2<Float>>)> {
1069 let _n_nodes = node_features.nrows();
1070 let mut current_embeddings = node_features.to_owned();
1071 let mut layer_outputs = Vec::new();
1072
1073 for layer_idx in 0..self.num_layers {
1074 let layer_output = match self.message_passing_variant {
1075 MessagePassingVariant::GCN => self.gcn_layer(
1076 ¤t_embeddings,
1077 adjacency,
1078 &weights[layer_idx],
1079 &biases[layer_idx],
1080 )?,
1081 MessagePassingVariant::GAT => {
1082 let att_weights = attention_weights
1083 .as_ref()
1084 .expect("operation should succeed");
1085 self.gat_layer(
1086 ¤t_embeddings,
1087 adjacency,
1088 &weights[layer_idx],
1089 &biases[layer_idx],
1090 &att_weights[layer_idx],
1091 )?
1092 }
1093 MessagePassingVariant::GraphSAGE => self.graphsage_layer(
1094 ¤t_embeddings,
1095 adjacency,
1096 &weights[layer_idx],
1097 &biases[layer_idx],
1098 )?,
1099 MessagePassingVariant::GIN => self.gin_layer(
1100 ¤t_embeddings,
1101 adjacency,
1102 &weights[layer_idx],
1103 &biases[layer_idx],
1104 )?,
1105 };
1106
1107 current_embeddings = layer_output.clone();
1108 layer_outputs.push(layer_output);
1109 }
1110
1111 Ok((current_embeddings, layer_outputs))
1112 }
1113
1114 fn gcn_layer(
1116 &self,
1117 node_embeddings: &Array2<Float>,
1118 adjacency: &ArrayView2<'_, i32>,
1119 weights: &Array2<Float>,
1120 bias: &Array1<Float>,
1121 ) -> SklResult<Array2<Float>> {
1122 let n_nodes = node_embeddings.nrows();
1123 let mut output = Array2::<Float>::zeros((n_nodes, weights.ncols()));
1124
1125 for i in 0..n_nodes {
1126 let mut aggregated = Array1::<Float>::zeros(node_embeddings.ncols());
1127 let mut degree = 0;
1128
1129 for j in 0..n_nodes {
1131 if adjacency[[i, j]] == 1 {
1132 aggregated += &node_embeddings.row(j).to_owned();
1133 degree += 1;
1134 }
1135 }
1136
1137 aggregated += &node_embeddings.row(i).to_owned();
1139 degree += 1;
1140
1141 if degree > 0 {
1143 aggregated /= degree as Float;
1144 }
1145
1146 let transformed = aggregated.dot(weights) + bias;
1148 let activated = transformed.mapv(|x| x.max(0.0)); output.row_mut(i).assign(&activated);
1151 }
1152
1153 Ok(output)
1154 }
1155
1156 fn gat_layer(
1158 &self,
1159 node_embeddings: &Array2<Float>,
1160 adjacency: &ArrayView2<'_, i32>,
1161 weights: &Array2<Float>,
1162 bias: &Array1<Float>,
1163 attention_weights: &Array2<Float>,
1164 ) -> SklResult<Array2<Float>> {
1165 let n_nodes = node_embeddings.nrows();
1166 let mut output = Array2::<Float>::zeros((n_nodes, weights.ncols()));
1167
1168 for i in 0..n_nodes {
1169 let mut attention_scores = Array1::<Float>::zeros(n_nodes);
1170 let mut valid_neighbors = Vec::new();
1171
1172 for j in 0..n_nodes {
1174 if adjacency[[i, j]] == 1 || i == j {
1175 let concat_features = Array1::from_iter(
1177 node_embeddings
1178 .row(i)
1179 .iter()
1180 .chain(node_embeddings.row(j).iter())
1181 .cloned(),
1182 );
1183
1184 if concat_features.len() == attention_weights.nrows() {
1185 let score = concat_features.dot(&attention_weights.column(0));
1186 attention_scores[j] = score.exp();
1187 valid_neighbors.push(j);
1188 }
1189 }
1190 }
1191
1192 let total_attention: Float = valid_neighbors.iter().map(|&j| attention_scores[j]).sum();
1194 if total_attention > 0.0 {
1195 for &j in &valid_neighbors {
1196 attention_scores[j] /= total_attention;
1197 }
1198 }
1199
1200 let mut aggregated = Array1::<Float>::zeros(node_embeddings.ncols());
1202 for &j in &valid_neighbors {
1203 let weighted_features = &node_embeddings.row(j).to_owned() * attention_scores[j];
1204 aggregated += &weighted_features;
1205 }
1206
1207 let transformed = aggregated.dot(weights) + bias;
1209 let activated = transformed.mapv(|x| x.max(0.0)); output.row_mut(i).assign(&activated);
1212 }
1213
1214 Ok(output)
1215 }
1216
1217 fn graphsage_layer(
1219 &self,
1220 node_embeddings: &Array2<Float>,
1221 adjacency: &ArrayView2<'_, i32>,
1222 weights: &Array2<Float>,
1223 bias: &Array1<Float>,
1224 ) -> SklResult<Array2<Float>> {
1225 let n_nodes = node_embeddings.nrows();
1226 let embedding_dim = node_embeddings.ncols();
1227 let output_dim = weights.ncols();
1228 let mut output = Array2::<Float>::zeros((n_nodes, output_dim));
1229
1230 for i in 0..n_nodes {
1231 let mut neighbor_sum = Array1::<Float>::zeros(embedding_dim);
1233 let mut neighbor_count = 0;
1234
1235 for j in 0..n_nodes {
1236 if adjacency[[i, j]] == 1 && i != j {
1237 neighbor_sum += &node_embeddings.row(j).to_owned();
1238 neighbor_count += 1;
1239 }
1240 }
1241
1242 if neighbor_count > 0 {
1244 neighbor_sum /= neighbor_count as Float;
1245 }
1246
1247 let self_features = node_embeddings.row(i).to_owned();
1249 let concatenated =
1250 Array1::from_iter(self_features.iter().chain(neighbor_sum.iter()).cloned());
1251
1252 if concatenated.len() == weights.nrows() {
1254 let transformed = concatenated.dot(weights) + bias;
1255 let activated = transformed.mapv(|x| x.max(0.0)); output.row_mut(i).assign(&activated);
1257 }
1258 }
1259
1260 Ok(output)
1261 }
1262
1263 fn gin_layer(
1265 &self,
1266 node_embeddings: &Array2<Float>,
1267 adjacency: &ArrayView2<'_, i32>,
1268 weights: &Array2<Float>,
1269 bias: &Array1<Float>,
1270 ) -> SklResult<Array2<Float>> {
1271 let n_nodes = node_embeddings.nrows();
1272 let mut output = Array2::<Float>::zeros((n_nodes, weights.ncols()));
1273 let epsilon = 0.0; for i in 0..n_nodes {
1276 let mut neighbor_sum = Array1::<Float>::zeros(node_embeddings.ncols());
1278
1279 for j in 0..n_nodes {
1280 if adjacency[[i, j]] == 1 && i != j {
1281 neighbor_sum += &node_embeddings.row(j).to_owned();
1282 }
1283 }
1284
1285 let updated = &node_embeddings.row(i).to_owned() * (1.0 + epsilon) + &neighbor_sum;
1287
1288 let transformed = updated.dot(weights) + bias;
1290 let activated = transformed.mapv(|x| x.max(0.0)); output.row_mut(i).assign(&activated);
1293 }
1294
1295 Ok(output)
1296 }
1297}
1298
1299impl GraphNeuralNetwork<GraphNeuralNetworkTrained> {
1300 pub fn predict_graph(
1302 &self,
1303 adjacency: &ArrayView2<'_, i32>,
1304 node_features: &ArrayView2<'_, Float>,
1305 ) -> SklResult<Array2<i32>> {
1306 let (n_nodes, n_features) = node_features.dim();
1307
1308 if n_features != self.state.n_features {
1309 return Err(SklearsError::InvalidInput(
1310 "Node features have different dimensionality than training data".to_string(),
1311 ));
1312 }
1313
1314 if adjacency.dim() != (n_nodes, n_nodes) {
1315 return Err(SklearsError::InvalidInput(
1316 "Adjacency matrix must be n_nodes x n_nodes".to_string(),
1317 ));
1318 }
1319
1320 let (final_embeddings, _) = self.forward_pass_trained(adjacency, node_features)?;
1322
1323 let predictions = final_embeddings.mapv(|x| if x > 0.0 { 1 } else { 0 });
1325
1326 Ok(predictions)
1327 }
1328
1329 pub fn hidden_dim(&self) -> usize {
1331 self.state.hidden_dim
1332 }
1333
1334 pub fn num_layers(&self) -> usize {
1336 self.state.num_layers
1337 }
1338
1339 fn forward_pass_trained(
1341 &self,
1342 adjacency: &ArrayView2<'_, i32>,
1343 node_features: &ArrayView2<'_, Float>,
1344 ) -> SklResult<(Array2<Float>, Vec<Array2<Float>>)> {
1345 let _n_nodes = node_features.nrows();
1346 let mut current_embeddings = node_features.to_owned();
1347 let mut layer_outputs = Vec::new();
1348
1349 for layer_idx in 0..self.state.num_layers {
1350 let layer_output = match self.state.message_passing_variant {
1351 MessagePassingVariant::GCN => {
1352 self.gcn_layer_trained(¤t_embeddings, adjacency, layer_idx)?
1353 }
1354 MessagePassingVariant::GAT => {
1355 self.gat_layer_trained(¤t_embeddings, adjacency, layer_idx)?
1356 }
1357 MessagePassingVariant::GraphSAGE => {
1358 self.graphsage_layer_trained(¤t_embeddings, adjacency, layer_idx)?
1359 }
1360 MessagePassingVariant::GIN => {
1361 self.gin_layer_trained(¤t_embeddings, adjacency, layer_idx)?
1362 }
1363 };
1364
1365 current_embeddings = layer_output.clone();
1366 layer_outputs.push(layer_output);
1367 }
1368
1369 Ok((current_embeddings, layer_outputs))
1370 }
1371
1372 fn gcn_layer_trained(
1374 &self,
1375 node_embeddings: &Array2<Float>,
1376 adjacency: &ArrayView2<'_, i32>,
1377 layer_idx: usize,
1378 ) -> SklResult<Array2<Float>> {
1379 let weights = &self.state.layer_weights[layer_idx];
1380 let bias = &self.state.layer_biases[layer_idx];
1381 let n_nodes = node_embeddings.nrows();
1382 let mut output = Array2::<Float>::zeros((n_nodes, weights.ncols()));
1383
1384 for i in 0..n_nodes {
1385 let mut aggregated = Array1::<Float>::zeros(node_embeddings.ncols());
1386 let mut degree = 0;
1387
1388 for j in 0..n_nodes {
1390 if adjacency[[i, j]] == 1 {
1391 aggregated += &node_embeddings.row(j).to_owned();
1392 degree += 1;
1393 }
1394 }
1395
1396 aggregated += &node_embeddings.row(i).to_owned();
1398 degree += 1;
1399
1400 if degree > 0 {
1402 aggregated /= degree as Float;
1403 }
1404
1405 let transformed = aggregated.dot(weights) + bias;
1407 let activated = if layer_idx == self.state.num_layers - 1 {
1408 transformed.mapv(|x| 1.0 / (1.0 + (-x).exp()))
1410 } else {
1411 transformed.mapv(|x| x.max(0.0))
1413 };
1414
1415 output.row_mut(i).assign(&activated);
1416 }
1417
1418 Ok(output)
1419 }
1420
1421 fn gat_layer_trained(
1423 &self,
1424 node_embeddings: &Array2<Float>,
1425 adjacency: &ArrayView2<'_, i32>,
1426 layer_idx: usize,
1427 ) -> SklResult<Array2<Float>> {
1428 let weights = &self.state.layer_weights[layer_idx];
1429 let bias = &self.state.layer_biases[layer_idx];
1430 let attention_weights = self
1431 .state
1432 .attention_weights
1433 .as_ref()
1434 .expect("operation should succeed");
1435 let att_weights = &attention_weights[layer_idx];
1436
1437 let n_nodes = node_embeddings.nrows();
1438 let mut output = Array2::<Float>::zeros((n_nodes, weights.ncols()));
1439
1440 for i in 0..n_nodes {
1441 let mut attention_scores = Array1::<Float>::zeros(n_nodes);
1442 let mut valid_neighbors = Vec::new();
1443
1444 for j in 0..n_nodes {
1446 if adjacency[[i, j]] == 1 || i == j {
1447 let concat_features = Array1::from_iter(
1448 node_embeddings
1449 .row(i)
1450 .iter()
1451 .chain(node_embeddings.row(j).iter())
1452 .cloned(),
1453 );
1454
1455 if concat_features.len() == att_weights.nrows() {
1456 let score = concat_features.dot(&att_weights.column(0));
1457 attention_scores[j] = score.exp();
1458 valid_neighbors.push(j);
1459 }
1460 }
1461 }
1462
1463 let total_attention: Float = valid_neighbors.iter().map(|&j| attention_scores[j]).sum();
1465 if total_attention > 0.0 {
1466 for &j in &valid_neighbors {
1467 attention_scores[j] /= total_attention;
1468 }
1469 }
1470
1471 let mut aggregated = Array1::<Float>::zeros(node_embeddings.ncols());
1473 for &j in &valid_neighbors {
1474 let weighted_features = &node_embeddings.row(j).to_owned() * attention_scores[j];
1475 aggregated += &weighted_features;
1476 }
1477
1478 let transformed = aggregated.dot(weights) + bias;
1480 let activated = if layer_idx == self.state.num_layers - 1 {
1481 transformed.mapv(|x| 1.0 / (1.0 + (-x).exp()))
1482 } else {
1483 transformed.mapv(|x| x.max(0.0))
1484 };
1485
1486 output.row_mut(i).assign(&activated);
1487 }
1488
1489 Ok(output)
1490 }
1491
1492 fn graphsage_layer_trained(
1494 &self,
1495 node_embeddings: &Array2<Float>,
1496 adjacency: &ArrayView2<'_, i32>,
1497 layer_idx: usize,
1498 ) -> SklResult<Array2<Float>> {
1499 let weights = &self.state.layer_weights[layer_idx];
1500 let bias = &self.state.layer_biases[layer_idx];
1501 let n_nodes = node_embeddings.nrows();
1502 let embedding_dim = node_embeddings.ncols();
1503 let output_dim = weights.ncols();
1504 let mut output = Array2::<Float>::zeros((n_nodes, output_dim));
1505
1506 for i in 0..n_nodes {
1507 let mut neighbor_sum = Array1::<Float>::zeros(embedding_dim);
1509 let mut neighbor_count = 0;
1510
1511 for j in 0..n_nodes {
1512 if adjacency[[i, j]] == 1 && i != j {
1513 neighbor_sum += &node_embeddings.row(j).to_owned();
1514 neighbor_count += 1;
1515 }
1516 }
1517
1518 if neighbor_count > 0 {
1520 neighbor_sum /= neighbor_count as Float;
1521 }
1522
1523 let self_features = node_embeddings.row(i).to_owned();
1525 let concatenated =
1526 Array1::from_iter(self_features.iter().chain(neighbor_sum.iter()).cloned());
1527
1528 if concatenated.len() == weights.nrows() {
1530 let transformed = concatenated.dot(weights) + bias;
1531 let activated = if layer_idx == self.state.num_layers - 1 {
1532 transformed.mapv(|x| 1.0 / (1.0 + (-x).exp()))
1533 } else {
1534 transformed.mapv(|x| x.max(0.0))
1535 };
1536 output.row_mut(i).assign(&activated);
1537 }
1538 }
1539
1540 Ok(output)
1541 }
1542
1543 fn gin_layer_trained(
1545 &self,
1546 node_embeddings: &Array2<Float>,
1547 adjacency: &ArrayView2<'_, i32>,
1548 layer_idx: usize,
1549 ) -> SklResult<Array2<Float>> {
1550 let weights = &self.state.layer_weights[layer_idx];
1551 let bias = &self.state.layer_biases[layer_idx];
1552 let n_nodes = node_embeddings.nrows();
1553 let mut output = Array2::<Float>::zeros((n_nodes, weights.ncols()));
1554 let epsilon = 0.0; for i in 0..n_nodes {
1557 let mut neighbor_sum = Array1::<Float>::zeros(node_embeddings.ncols());
1559
1560 for j in 0..n_nodes {
1561 if adjacency[[i, j]] == 1 && i != j {
1562 neighbor_sum += &node_embeddings.row(j).to_owned();
1563 }
1564 }
1565
1566 let updated = &node_embeddings.row(i).to_owned() * (1.0 + epsilon) + &neighbor_sum;
1568
1569 let transformed = updated.dot(weights) + bias;
1571 let activated = if layer_idx == self.state.num_layers - 1 {
1572 transformed.mapv(|x| 1.0 / (1.0 + (-x).exp()))
1573 } else {
1574 transformed.mapv(|x| x.max(0.0))
1575 };
1576
1577 output.row_mut(i).assign(&activated);
1578 }
1579
1580 Ok(output)
1581 }
1582}
1583
1584#[allow(non_snake_case)]
1586#[cfg(test)]
1587mod tests {
1588 use super::*;
1589 use scirs2_core::ndarray::array;
1591
1592 #[test]
1593 fn test_gnn_basic_functionality() {
1594 let node_features = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
1595 let adjacency = array![[0, 1, 0], [1, 0, 1], [0, 1, 0]];
1596 let node_labels = array![[1, 0], [0, 1], [1, 1]];
1597
1598 let gnn = GraphNeuralNetwork::new()
1599 .hidden_dim(4)
1600 .num_layers(2)
1601 .max_iter(5);
1602
1603 let trained_gnn = gnn
1604 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1605 .expect("operation should succeed");
1606
1607 let predictions = trained_gnn
1608 .predict_graph(&adjacency.view(), &node_features.view())
1609 .expect("operation should succeed");
1610
1611 assert_eq!(predictions.dim(), (3, 2));
1612 assert!(predictions.iter().all(|&x| x == 0 || x == 1));
1613 }
1614
1615 #[test]
1616 fn test_gnn_different_variants() {
1617 let node_features = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
1618 let adjacency = array![[0, 1, 0], [1, 0, 1], [0, 1, 0]];
1619 let node_labels = array![[1, 0], [0, 1], [1, 1]];
1620
1621 let gnn_gcn = GraphNeuralNetwork::new()
1623 .message_passing_variant(MessagePassingVariant::GCN)
1624 .max_iter(5);
1625 let trained_gcn = gnn_gcn
1626 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1627 .expect("operation should succeed");
1628
1629 let gnn_gat = GraphNeuralNetwork::new()
1631 .message_passing_variant(MessagePassingVariant::GAT)
1632 .max_iter(5);
1633 let trained_gat = gnn_gat
1634 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1635 .expect("operation should succeed");
1636
1637 let gnn_sage = GraphNeuralNetwork::new()
1639 .message_passing_variant(MessagePassingVariant::GraphSAGE)
1640 .max_iter(5);
1641 let trained_sage = gnn_sage
1642 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1643 .expect("operation should succeed");
1644
1645 assert_eq!(
1646 trained_gcn.state.message_passing_variant,
1647 MessagePassingVariant::GCN
1648 );
1649 assert_eq!(
1650 trained_gat.state.message_passing_variant,
1651 MessagePassingVariant::GAT
1652 );
1653 assert_eq!(
1654 trained_sage.state.message_passing_variant,
1655 MessagePassingVariant::GraphSAGE
1656 );
1657 }
1658
1659 #[test]
1660 fn test_gnn_parameter_settings() {
1661 let gnn = GraphNeuralNetwork::new()
1662 .hidden_dim(16)
1663 .num_layers(3)
1664 .learning_rate(0.001)
1665 .max_iter(50)
1666 .dropout_rate(0.1);
1667
1668 assert_eq!(gnn.hidden_dim, 16);
1669 assert_eq!(gnn.num_layers, 3);
1670 assert!((gnn.learning_rate - 0.001).abs() < 1e-10);
1671 assert_eq!(gnn.max_iter, 50);
1672 assert!((gnn.dropout_rate - 0.1).abs() < 1e-10);
1673 }
1674
1675 #[test]
1676 fn test_gnn_default_settings() {
1677 let gnn = GraphNeuralNetwork::new();
1678
1679 assert_eq!(gnn.hidden_dim, 32);
1680 assert_eq!(gnn.num_layers, 2);
1681 assert_eq!(gnn.message_passing_variant, MessagePassingVariant::GCN);
1682 assert_eq!(gnn.aggregation_function, AggregationFunction::Mean);
1683 }
1684
1685 #[test]
1686 fn test_gnn_builder_pattern() {
1687 let gnn1 = GraphNeuralNetwork::new();
1688 let gnn2 = GraphNeuralNetwork::new();
1689
1690 assert_eq!(gnn1.hidden_dim, gnn2.hidden_dim);
1691 assert_eq!(gnn1.num_layers, gnn2.num_layers);
1692
1693 let gnn3 = GraphNeuralNetwork::new().max_iter(1);
1694 assert_eq!(gnn3.max_iter, 1);
1695 }
1696
1697 #[test]
1698 fn test_message_passing_variants() {
1699 assert_eq!(MessagePassingVariant::GCN, MessagePassingVariant::GCN);
1700 assert_ne!(MessagePassingVariant::GCN, MessagePassingVariant::GAT);
1701
1702 let variants = [
1703 MessagePassingVariant::GCN,
1704 MessagePassingVariant::GAT,
1705 MessagePassingVariant::GraphSAGE,
1706 MessagePassingVariant::GIN,
1707 ];
1708
1709 let gnn1 = GraphNeuralNetwork::new()
1710 .message_passing_variant(variants[0])
1711 .hidden_dim(8)
1712 .max_iter(3);
1713
1714 let gnn2 = GraphNeuralNetwork::new()
1715 .message_passing_variant(variants[1])
1716 .hidden_dim(8)
1717 .max_iter(3);
1718
1719 assert_eq!(gnn1.message_passing_variant, MessagePassingVariant::GCN);
1720 assert_eq!(gnn2.message_passing_variant, MessagePassingVariant::GAT);
1721 }
1722
1723 #[test]
1724 fn test_gnn_larger_graph() {
1725 let node_features = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0], [1.0, 3.0]];
1726 let adjacency = array![
1727 [0, 1, 1, 0, 0],
1728 [1, 0, 1, 1, 0],
1729 [1, 1, 0, 0, 1],
1730 [0, 1, 0, 0, 1],
1731 [0, 0, 1, 1, 0]
1732 ];
1733 let node_labels = array![[1, 0, 1], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 0]];
1734
1735 let gnn = GraphNeuralNetwork::new()
1736 .hidden_dim(10)
1737 .num_layers(2)
1738 .message_passing_variant(MessagePassingVariant::GCN)
1739 .max_iter(10);
1740
1741 let trained_gnn = gnn
1742 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1743 .expect("operation should succeed");
1744
1745 let predictions = trained_gnn
1746 .predict_graph(&adjacency.view(), &node_features.view())
1747 .expect("operation should succeed");
1748
1749 assert_eq!(predictions.dim(), (5, 3));
1750 assert!(predictions.iter().all(|&x| x == 0 || x == 1));
1751 assert_eq!(trained_gnn.hidden_dim(), 10);
1752 }
1753
1754 #[test]
1755 fn test_aggregation_functions() {
1756 assert_ne!(AggregationFunction::Mean, AggregationFunction::Max);
1757 assert_eq!(AggregationFunction::Sum, AggregationFunction::Sum);
1758 assert_ne!(MessagePassingVariant::GraphSAGE, MessagePassingVariant::GIN);
1759 }
1760
1761 #[test]
1762 fn test_gnn_reproducibility() {
1763 let node_features = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
1764 let adjacency = array![[0, 1, 0], [1, 0, 1], [0, 1, 0]];
1765 let node_labels = array![[1, 0], [0, 1], [1, 1]];
1766
1767 let gnn = GraphNeuralNetwork::new()
1768 .hidden_dim(4)
1769 .num_layers(2)
1770 .max_iter(5)
1771 .random_state(42);
1772
1773 let trained_gnn = gnn
1774 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1775 .expect("operation should succeed");
1776
1777 let predictions = trained_gnn
1778 .predict_graph(&adjacency.view(), &node_features.view())
1779 .expect("operation should succeed");
1780
1781 assert_eq!(predictions.dim(), (3, 2));
1782 }
1783
1784 #[test]
1785 fn test_gnn_edge_cases() {
1786 let node_features = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0], [1.0, 3.0]];
1787 let adjacency = array![
1788 [0, 1, 1, 0, 0],
1789 [1, 0, 1, 1, 0],
1790 [1, 1, 0, 0, 1],
1791 [0, 1, 0, 0, 1],
1792 [0, 0, 1, 1, 0]
1793 ];
1794 let node_labels = array![[1, 0, 1], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 0]];
1795
1796 let gnn = GraphNeuralNetwork::new()
1797 .hidden_dim(10)
1798 .num_layers(2)
1799 .message_passing_variant(MessagePassingVariant::GCN)
1800 .max_iter(15)
1801 .random_state(42);
1802
1803 let trained_gnn = gnn
1804 .fit_graph(&adjacency.view(), &node_features.view(), &node_labels)
1805 .expect("operation should succeed");
1806 let predictions = trained_gnn
1807 .predict_graph(&adjacency.view(), &node_features.view())
1808 .expect("operation should succeed");
1809
1810 assert_eq!(predictions.dim(), (5, 3));
1811 assert!(predictions.iter().all(|&x| x == 0 || x == 1));
1812 assert_eq!(trained_gnn.hidden_dim(), 10);
1813 }
1814}