1#![allow(dead_code)]
7type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
10
11use crate::conv::{GATConv, GCNConv, GINConv, SAGEConv};
12use crate::parameter::Parameter;
13use crate::pool::global::{global_max_pool, global_mean_pool, GlobalAttentionPool};
14use crate::{GraphData, GraphLayer};
15use torsh_tensor::{
16 creation::{randn, zeros},
17 Tensor,
18};
19
20pub trait GraphClassifier {
22 fn forward(&self, graph: &GraphData) -> Result<Tensor>;
28
29 fn parameters(&self) -> Vec<Tensor>;
31
32 fn num_classes(&self) -> usize;
34}
35
36pub struct GraphClassificationGCN {
38 layers: Vec<GCNConv>,
40 classifier: Parameter,
42 bias: Option<Parameter>,
44 pooling_type: PoolingType,
46 attention_pool: Option<GlobalAttentionPool>,
48 num_classes: usize,
50 dropout: f32,
52}
53
54#[derive(Debug, Clone)]
56pub enum PoolingType {
57 Mean,
58 Max,
59 Sum,
60 Attention,
61}
62
63impl GraphClassificationGCN {
64 pub fn new(
66 layer_dims: Vec<usize>,
67 num_classes: usize,
68 pooling_type: PoolingType,
69 dropout: f32,
70 ) -> Result<Self> {
71 if layer_dims.len() < 2 {
72 return Err(torsh_core::error::TorshError::InvalidArgument(
73 "layer_dims needs at least input and output dimensions".to_string(),
74 ));
75 }
76
77 let mut layers = Vec::new();
78 for i in 0..layer_dims.len() - 1 {
79 layers.push(GCNConv::new(layer_dims[i], layer_dims[i + 1], true)?);
80 }
81
82 let final_dim = layer_dims.last().ok_or_else(|| {
83 torsh_core::error::TorshError::InvalidArgument(
84 "layer_dims must not be empty".to_string(),
85 )
86 })?;
87 let classifier = Parameter::new(randn(&[*final_dim, num_classes])?);
88 let bias = Some(Parameter::new(zeros(&[num_classes])?));
89
90 let attention_pool = if matches!(pooling_type, PoolingType::Attention) {
91 Some(GlobalAttentionPool::new(*final_dim, *final_dim)?)
92 } else {
93 None
94 };
95
96 Ok(Self {
97 layers,
98 classifier,
99 bias,
100 pooling_type,
101 attention_pool,
102 num_classes,
103 dropout,
104 })
105 }
106
107 fn pool_graph(&self, graph: &GraphData) -> Result<Tensor> {
109 match &self.pooling_type {
110 PoolingType::Mean => global_mean_pool(graph),
111 PoolingType::Max => global_max_pool(graph),
112 PoolingType::Sum => {
113 let num_features = graph.x.shape().dims()[1];
115 let mut sum_features = zeros(&[num_features])?;
116
117 for node in 0..graph.num_nodes {
118 let node_feat = graph.x.slice_tensor(0, node, node + 1)?.squeeze_tensor(0)?;
119 sum_features = sum_features.add(&node_feat)?;
120 }
121
122 Ok(sum_features)
123 }
124 PoolingType::Attention => {
125 if let Some(ref pool) = self.attention_pool {
126 pool.forward(graph)
127 } else {
128 global_mean_pool(graph) }
130 }
131 }
132 }
133}
134
135impl GraphClassifier for GraphClassificationGCN {
136 fn forward(&self, graph: &GraphData) -> Result<Tensor> {
137 let mut current_graph = graph.clone();
138
139 for (i, layer) in self.layers.iter().enumerate() {
141 current_graph = layer.forward(¤t_graph)?;
142
143 if i < self.layers.len() - 1 {
145 let zero_tensor = zeros(current_graph.x.shape().dims())?;
146 current_graph.x = current_graph.x.maximum(&zero_tensor)?;
147
148 if self.dropout > 0.0 {
150 }
152 }
153 }
154
155 let graph_embedding = self.pool_graph(¤t_graph)?;
157
158 let graph_embedding_2d = if graph_embedding.shape().dims().len() == 1 {
161 graph_embedding.unsqueeze_tensor(0)?
162 } else if graph_embedding.shape().dims().len() == 2 {
163 graph_embedding
164 } else {
165 let total_features = graph_embedding.shape().dims().iter().product::<usize>();
167 graph_embedding.view(&[1, total_features as i32])?
168 };
169
170 let mut logits = graph_embedding_2d.matmul(&self.classifier.clone_data())?;
171
172 if logits.shape().dims().len() == 2 && logits.shape().dims()[0] == 1 {
174 logits = logits.squeeze_tensor(0)?;
175 }
176
177 if let Some(ref bias) = self.bias {
179 logits = logits.add(&bias.clone_data())?;
180 }
181
182 Ok(logits)
183 }
184
185 fn parameters(&self) -> Vec<Tensor> {
186 let mut params = Vec::new();
187
188 for layer in &self.layers {
190 params.extend(layer.parameters());
191 }
192
193 params.push(self.classifier.clone_data());
195 if let Some(ref bias) = self.bias {
196 params.push(bias.clone_data());
197 }
198
199 if let Some(ref pool) = self.attention_pool {
201 params.extend(pool.parameters());
202 }
203
204 params
205 }
206
207 fn num_classes(&self) -> usize {
208 self.num_classes
209 }
210}
211
212pub struct GraphClassificationGAT {
214 gat_layers: Vec<GATConv>,
216 classifier: Vec<Parameter>,
218 pooling_type: PoolingType,
220 attention_pool: Option<GlobalAttentionPool>,
222 num_classes: usize,
224}
225
226impl GraphClassificationGAT {
227 pub fn new(
228 input_dim: usize,
229 hidden_dim: usize,
230 num_heads: usize,
231 num_layers: usize,
232 num_classes: usize,
233 dropout: f32,
234 ) -> Result<Self> {
235 let mut gat_layers = Vec::new();
236
237 gat_layers.push(GATConv::new(
239 input_dim, hidden_dim, num_heads, dropout, true,
240 )?);
241
242 for _ in 1..num_layers {
244 gat_layers.push(GATConv::new(
245 hidden_dim * num_heads,
246 hidden_dim,
247 num_heads,
248 dropout,
249 true,
250 )?);
251 }
252
253 let final_dim = hidden_dim * num_heads;
255 let classifier = vec![
256 Parameter::new(randn(&[final_dim, final_dim / 2])?),
257 Parameter::new(randn(&[final_dim / 2, num_classes])?),
258 ];
259
260 Ok(Self {
261 gat_layers,
262 classifier,
263 pooling_type: PoolingType::Attention,
264 attention_pool: Some(GlobalAttentionPool::new(final_dim, final_dim)?),
265 num_classes,
266 })
267 }
268}
269
270impl GraphClassifier for GraphClassificationGAT {
271 fn forward(&self, graph: &GraphData) -> Result<Tensor> {
272 let mut current_graph = graph.clone();
273
274 for (i, layer) in self.gat_layers.iter().enumerate() {
276 current_graph = layer.forward(¤t_graph)?;
277
278 if i < self.gat_layers.len() - 1 {
280 let zero_tensor = zeros(current_graph.x.shape().dims())?;
281 current_graph.x = current_graph.x.maximum(&zero_tensor)?;
282 }
283 }
284
285 let graph_embedding = if let Some(ref pool) = self.attention_pool {
287 pool.forward(¤t_graph)?
288 } else {
289 global_mean_pool(¤t_graph)?
290 };
291
292 let graph_embedding_2d = if graph_embedding.shape().dims().len() == 1 {
295 graph_embedding.unsqueeze_tensor(0)?
296 } else if graph_embedding.shape().dims().len() == 2 {
297 graph_embedding
298 } else {
299 let total_features = graph_embedding.shape().dims().iter().product::<usize>();
301 graph_embedding.view(&[1, total_features as i32])?
302 };
303
304 let mut hidden = graph_embedding_2d.matmul(&self.classifier[0].clone_data())?;
305
306 if hidden.shape().dims().len() == 2 && hidden.shape().dims()[0] == 1 {
308 hidden = hidden.squeeze_tensor(0)?;
309 }
310
311 let zero_tensor = zeros(hidden.shape().dims())?;
313 hidden = hidden.maximum(&zero_tensor)?;
314
315 let hidden_2d = if hidden.shape().dims().len() == 1 {
317 hidden.unsqueeze_tensor(0)?
318 } else {
319 hidden
320 };
321
322 let mut logits = hidden_2d.matmul(&self.classifier[1].clone_data())?;
323
324 if logits.shape().dims().len() == 2 && logits.shape().dims()[0] == 1 {
326 logits = logits.squeeze_tensor(0)?;
327 }
328
329 Ok(logits)
330 }
331
332 fn parameters(&self) -> Vec<Tensor> {
333 let mut params = Vec::new();
334
335 for layer in &self.gat_layers {
337 params.extend(layer.parameters());
338 }
339
340 for layer in &self.classifier {
342 params.push(layer.clone_data());
343 }
344
345 if let Some(ref pool) = self.attention_pool {
347 params.extend(pool.parameters());
348 }
349
350 params
351 }
352
353 fn num_classes(&self) -> usize {
354 self.num_classes
355 }
356}
357
358pub struct HierarchicalGraphClassifier {
361 local_layers: Vec<GCNConv>,
363 global_layers: Vec<SAGEConv>,
365 attention_weights: Parameter,
367 classifier: Parameter,
369 num_classes: usize,
371}
372
373impl HierarchicalGraphClassifier {
374 pub fn new(
375 input_dim: usize,
376 local_hidden: usize,
377 global_hidden: usize,
378 num_classes: usize,
379 ) -> Result<Self> {
380 let local_layers = vec![
382 GCNConv::new(input_dim, local_hidden, true)?,
383 GCNConv::new(local_hidden, local_hidden, true)?,
384 ];
385
386 let global_layers = vec![
388 SAGEConv::new(input_dim, global_hidden, true)?,
389 SAGEConv::new(global_hidden, global_hidden, true)?,
390 ];
391
392 let combined_dim = local_hidden + global_hidden;
394 let attention_weights = Parameter::new(randn(&[combined_dim, 1])?);
395
396 let classifier = Parameter::new(randn(&[combined_dim, num_classes])?);
398
399 Ok(Self {
400 local_layers,
401 global_layers,
402 attention_weights,
403 classifier,
404 num_classes,
405 })
406 }
407}
408
409impl GraphClassifier for HierarchicalGraphClassifier {
410 fn forward(&self, graph: &GraphData) -> Result<Tensor> {
411 let mut local_graph = graph.clone();
413 for layer in &self.local_layers {
414 local_graph = layer.forward(&local_graph)?;
415 let zero_tensor = zeros(local_graph.x.shape().dims())?;
416 local_graph.x = local_graph.x.maximum(&zero_tensor)?;
417 }
418 let local_repr = global_mean_pool(&local_graph)?;
419
420 let mut global_graph = graph.clone();
422 for layer in &self.global_layers {
423 global_graph = layer.forward(&global_graph)?;
424 let zero_tensor = zeros(global_graph.x.shape().dims())?;
425 global_graph.x = global_graph.x.maximum(&zero_tensor)?;
426 }
427 let global_repr = global_mean_pool(&global_graph)?;
428
429 let local_1d = if local_repr.shape().dims().len() == 1 {
432 local_repr
433 } else {
434 let flat = local_repr.shape().dims().iter().product::<usize>() as i32;
435 local_repr.view(&[flat])?
436 };
437 let global_1d = if global_repr.shape().dims().len() == 1 {
438 global_repr
439 } else {
440 let flat = global_repr.shape().dims().iter().product::<usize>() as i32;
441 global_repr.view(&[flat])?
442 };
443
444 let combined_1d = Tensor::cat(&[&local_1d, &global_1d], 0)?;
446 let combined = combined_1d.unsqueeze_tensor(0)?;
447
448 let combined_2d = if combined.shape().dims().len() == 1 {
451 combined.unsqueeze_tensor(0)?
452 } else {
453 combined.clone()
454 };
455
456 let mut attention_scores = combined_2d.matmul(&self.attention_weights.clone_data())?;
457
458 if attention_scores.shape().dims().len() == 2 && attention_scores.shape().dims()[1] == 1 {
460 attention_scores = attention_scores.squeeze_tensor(1)?;
461 }
462
463 let exp_scores = attention_scores.exp()?;
465 let sum_exp = exp_scores.sum()?;
466 let normalized_scores = exp_scores.div_scalar(sum_exp.to_vec()?[0])?;
467
468 let scores_expanded = normalized_scores.unsqueeze_tensor(1)?;
470 let combined_shape = combined_2d.shape();
471 let scores_broadcasted = scores_expanded.expand(combined_shape.dims())?;
472 let attended = combined_2d.mul(&scores_broadcasted)?.sum_dim(&[0], false)?; let attended_2d = if attended.shape().dims().len() == 1 {
476 attended.unsqueeze_tensor(0)?
477 } else {
478 attended
479 };
480
481 let mut logits = attended_2d.matmul(&self.classifier.clone_data())?;
482
483 if logits.shape().dims().len() == 2 && logits.shape().dims()[0] == 1 {
485 logits = logits.squeeze_tensor(0)?;
486 }
487
488 Ok(logits)
489 }
490
491 fn parameters(&self) -> Vec<Tensor> {
492 let mut params = Vec::new();
493
494 for layer in &self.local_layers {
496 params.extend(layer.parameters());
497 }
498
499 for layer in &self.global_layers {
501 params.extend(layer.parameters());
502 }
503
504 params.push(self.attention_weights.clone_data());
506 params.push(self.classifier.clone_data());
507
508 params
509 }
510
511 fn num_classes(&self) -> usize {
512 self.num_classes
513 }
514}
515
516pub struct GraphRegressor {
518 feature_layers: Vec<GINConv>,
520 regressor: Parameter,
522 bias: Option<Parameter>,
524 output_dim: usize,
526}
527
528impl GraphRegressor {
529 pub fn new(layer_dims: Vec<usize>, output_dim: usize, bias: bool) -> Result<Self> {
530 let mut feature_layers = Vec::new();
531 for i in 0..layer_dims.len() - 1 {
532 feature_layers.push(GINConv::new(
533 layer_dims[i],
534 layer_dims[i + 1],
535 0.0,
536 false,
537 true,
538 )?);
539 }
540
541 let final_dim = layer_dims.last().ok_or_else(|| {
542 torsh_core::error::TorshError::InvalidArgument(
543 "layer_dims must not be empty".to_string(),
544 )
545 })?;
546 let regressor = Parameter::new(randn(&[*final_dim, output_dim])?);
547 let bias_param = if bias {
548 Some(Parameter::new(zeros(&[output_dim])?))
549 } else {
550 None
551 };
552
553 Ok(Self {
554 feature_layers,
555 regressor,
556 bias: bias_param,
557 output_dim,
558 })
559 }
560
561 pub fn forward(&self, graph: &GraphData) -> Result<Tensor> {
562 let mut current_graph = graph.clone();
563
564 for layer in &self.feature_layers {
566 current_graph = layer.forward(¤t_graph)?;
567
568 let zero_tensor = zeros(current_graph.x.shape().dims())?;
570 current_graph.x = current_graph.x.maximum(&zero_tensor)?;
571 }
572
573 let graph_embedding = global_mean_pool(¤t_graph)?;
575
576 let graph_embedding_2d = if graph_embedding.shape().dims().len() == 1 {
578 graph_embedding.unsqueeze_tensor(0)?
579 } else {
580 graph_embedding
581 };
582
583 let mut output = graph_embedding_2d.matmul(&self.regressor.clone_data())?;
584
585 if output.shape().dims().len() == 2 && output.shape().dims()[0] == 1 {
587 output = output.squeeze_tensor(0)?;
588 }
589
590 if let Some(ref bias) = self.bias {
592 output = output.add(&bias.clone_data())?;
593 }
594
595 Ok(output)
596 }
597
598 pub fn parameters(&self) -> Vec<Tensor> {
599 let mut params = Vec::new();
600
601 for layer in &self.feature_layers {
603 params.extend(layer.parameters());
604 }
605
606 params.push(self.regressor.clone_data());
608 if let Some(ref bias) = self.bias {
609 params.push(bias.clone_data());
610 }
611
612 params
613 }
614}
615
616pub struct MultiTaskGraphNetwork {
618 shared_layers: Vec<GCNConv>,
620 classifier: Parameter,
622 regressor: Parameter,
624 classification_bias: Parameter,
626 regression_bias: Parameter,
627 num_classes: usize,
629 regression_dim: usize,
630}
631
632impl MultiTaskGraphNetwork {
633 pub fn new(layer_dims: Vec<usize>, num_classes: usize, regression_dim: usize) -> Result<Self> {
634 let mut shared_layers = Vec::new();
635 for i in 0..layer_dims.len() - 1 {
636 shared_layers.push(GCNConv::new(layer_dims[i], layer_dims[i + 1], true)?);
637 }
638
639 let final_dim = layer_dims.last().ok_or_else(|| {
640 torsh_core::error::TorshError::InvalidArgument(
641 "layer_dims must not be empty".to_string(),
642 )
643 })?;
644
645 let classifier = Parameter::new(randn(&[*final_dim, num_classes])?);
646 let regressor = Parameter::new(randn(&[*final_dim, regression_dim])?);
647
648 let classification_bias = Parameter::new(zeros(&[num_classes])?);
649 let regression_bias = Parameter::new(zeros(&[regression_dim])?);
650
651 Ok(Self {
652 shared_layers,
653 classifier,
654 regressor,
655 classification_bias,
656 regression_bias,
657 num_classes,
658 regression_dim,
659 })
660 }
661
662 pub fn forward(&self, graph: &GraphData) -> Result<(Tensor, Tensor)> {
663 let mut current_graph = graph.clone();
664
665 for layer in &self.shared_layers {
667 current_graph = layer.forward(¤t_graph)?;
668
669 let zero_tensor = zeros(current_graph.x.shape().dims())?;
670 current_graph.x = current_graph.x.maximum(&zero_tensor)?;
671 }
672
673 let graph_embedding = global_mean_pool(¤t_graph)?;
675
676 let graph_embedding_2d = if graph_embedding.shape().dims().len() == 1 {
678 graph_embedding.unsqueeze_tensor(0)?
679 } else {
680 graph_embedding.clone()
681 };
682
683 let mut classification_logits = graph_embedding_2d.matmul(&self.classifier.clone_data())?;
684
685 if classification_logits.shape().dims().len() == 2
687 && classification_logits.shape().dims()[0] == 1
688 {
689 classification_logits = classification_logits.squeeze_tensor(0)?;
690 }
691
692 let classification_logits =
693 classification_logits.add(&self.classification_bias.clone_data())?;
694
695 let graph_embedding_2d_reg = if graph_embedding.shape().dims().len() == 1 {
697 graph_embedding.unsqueeze_tensor(0)?
698 } else {
699 graph_embedding
700 };
701
702 let mut regression_output = graph_embedding_2d_reg.matmul(&self.regressor.clone_data())?;
703
704 if regression_output.shape().dims().len() == 2 && regression_output.shape().dims()[0] == 1 {
706 regression_output = regression_output.squeeze_tensor(0)?;
707 }
708
709 let regression_output = regression_output.add(&self.regression_bias.clone_data())?;
710
711 Ok((classification_logits, regression_output))
712 }
713
714 pub fn parameters(&self) -> Vec<Tensor> {
715 let mut params = Vec::new();
716
717 for layer in &self.shared_layers {
719 params.extend(layer.parameters());
720 }
721
722 params.push(self.classifier.clone_data());
724 params.push(self.regressor.clone_data());
725 params.push(self.classification_bias.clone_data());
726 params.push(self.regression_bias.clone_data());
727
728 params
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::scirs2_integration::generation;
736
737 #[test]
738 fn test_graph_classification_gcn() {
739 let graph = generation::erdos_renyi(10, 0.3).expect("operation should succeed");
740 let classifier = GraphClassificationGCN::new(
741 vec![16, 32, 16], 3, PoolingType::Mean,
744 0.1, )
746 .expect("operation should succeed");
747
748 let logits = classifier
749 .forward(&graph)
750 .expect("operation should succeed");
751 assert_eq!(logits.shape().dims(), &[3]);
752
753 let params = classifier.parameters();
754 assert!(params.len() > 0);
755 assert_eq!(classifier.num_classes(), 3);
756 }
757
758 #[test]
759 fn test_different_pooling_strategies() {
760 let graph = generation::complete(5).expect("operation should succeed");
761 let pooling_types = vec![
762 PoolingType::Mean,
763 PoolingType::Max,
764 PoolingType::Sum,
765 PoolingType::Attention,
766 ];
767
768 for pooling in pooling_types {
769 let classifier = GraphClassificationGCN::new(vec![16, 8], 2, pooling, 0.0)
770 .expect("operation should succeed");
771
772 let logits = classifier
773 .forward(&graph)
774 .expect("operation should succeed");
775 assert_eq!(logits.shape().dims(), &[2]);
776
777 let logit_vals = logits.to_vec().expect("conversion should succeed");
778 assert!(logit_vals.iter().all(|&x| x.is_finite()));
779 }
780 }
781
782 #[test]
783 fn test_graph_regressor() {
784 let graph = generation::barabasi_albert(8, 2).expect("operation should succeed");
785 let regressor = GraphRegressor::new(
786 vec![16, 12, 8],
787 2, true,
789 )
790 .expect("operation should succeed");
791
792 let output = regressor.forward(&graph).expect("operation should succeed");
793 assert_eq!(output.shape().dims(), &[2]);
794
795 let output_vals = output.to_vec().expect("conversion should succeed");
796 assert!(output_vals.iter().all(|&x| x.is_finite()));
797 }
798
799 #[test]
800 fn test_hierarchical_classifier() {
801 let graph = generation::watts_strogatz(12, 4, 0.3).expect("operation should succeed");
802 let classifier = HierarchicalGraphClassifier::new(
803 16, 8, 8, 4, )
808 .expect("operation should succeed");
809
810 let logits = classifier
811 .forward(&graph)
812 .expect("operation should succeed");
813 assert_eq!(logits.shape().dims(), &[4]);
814
815 let params = classifier.parameters();
816 assert!(params.len() > 0);
817 }
818
819 #[test]
820 fn test_multi_task_network() {
821 let graph = generation::erdos_renyi(6, 0.5).expect("operation should succeed");
822 let multi_task = MultiTaskGraphNetwork::new(
823 vec![16, 12],
824 3, 2, )
827 .expect("operation should succeed");
828
829 let (class_logits, reg_output) = multi_task
830 .forward(&graph)
831 .expect("operation should succeed");
832
833 assert_eq!(class_logits.shape().dims(), &[3]);
834 assert_eq!(reg_output.shape().dims(), &[2]);
835
836 let class_vals = class_logits.to_vec().expect("conversion should succeed");
837 let reg_vals = reg_output.to_vec().expect("conversion should succeed");
838
839 assert!(class_vals.iter().all(|&x| x.is_finite()));
840 assert!(reg_vals.iter().all(|&x| x.is_finite()));
841 }
842
843 #[test]
844 fn test_parameter_count_consistency() {
845 let graph = generation::complete(4).expect("operation should succeed");
846
847 let classifiers: Vec<Box<dyn GraphClassifier>> = vec![Box::new(
849 GraphClassificationGCN::new(vec![16, 8], 2, PoolingType::Mean, 0.0)
850 .expect("operation should succeed"),
851 )];
852
853 for classifier in classifiers {
854 let params1 = classifier.parameters();
855 let params2 = classifier.parameters();
856
857 assert_eq!(params1.len(), params2.len());
858
859 let logits1 = classifier
861 .forward(&graph)
862 .expect("operation should succeed");
863 let logits2 = classifier
864 .forward(&graph)
865 .expect("operation should succeed");
866
867 let vals1 = logits1.to_vec().expect("conversion should succeed");
868 let vals2 = logits2.to_vec().expect("conversion should succeed");
869
870 for (v1, v2) in vals1.iter().zip(vals2.iter()) {
871 assert!((v1 - v2).abs() < 1e-6);
872 }
873 }
874 }
875}