1type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
8
9use crate::GraphData;
10use std::collections::{HashMap, HashSet};
11use std::fmt;
12use torsh_tensor::{
13 creation::{randn, zeros},
14 Tensor,
15};
16
17#[derive(Debug)]
19pub struct GraphFoundationModel {
20 pub config: FoundationModelConfig,
22 pub encoder_layers: Vec<String>, pub pretraining_head: PretrainingHead,
26 pub task_heads: HashMap<String, String>,
28 pub tokenizer: GraphTokenizer,
30 pub parameters: FoundationModelParameters,
32}
33
34#[derive(Debug, Clone)]
36pub struct FoundationModelConfig {
37 pub model_dim: usize,
39 pub num_layers: usize,
41 pub num_heads: usize,
43 pub ff_dim: usize,
45 pub max_seq_length: usize,
47 pub vocab_size: usize,
49 pub dropout: f32,
51 pub pretraining_objectives: Vec<PretrainingObjective>,
53}
54
55#[derive(Debug, Clone)]
57pub enum PretrainingObjective {
58 MaskedNodeModeling,
60 MaskedEdgeModeling,
62 GraphContrastive,
64 NodeContrastive,
66 StructurePrediction,
68 MotifPrediction,
70 PropertyPrediction,
72 GraphDenoising,
74}
75
76#[derive(Debug, Clone)]
78pub struct PretrainingHead {
79 pub mlm_head: MLMHead,
81 pub contrastive_head: ContrastiveHead,
83 pub structure_head: StructurePredictionHead,
85 pub active_objectives: Vec<PretrainingObjective>,
87}
88
89#[derive(Debug, Clone)]
91pub struct MLMHead {
92 pub output_projection: Tensor,
94 pub bias: Tensor,
96 pub mask_token: Tensor,
98}
99
100#[derive(Debug, Clone)]
102pub struct ContrastiveHead {
103 pub projection: Tensor,
105 pub temperature: f32,
107 pub embed_dim: usize,
109}
110
111#[derive(Debug, Clone)]
113pub struct StructurePredictionHead {
114 pub edge_predictor: Tensor,
116 pub motif_predictor: Tensor,
118 pub property_predictor: Tensor,
120}
121
122#[derive(Debug, Clone)]
124pub struct GraphTokenizer {
125 pub node_vocab: HashMap<String, usize>,
127 pub edge_vocab: HashMap<String, usize>,
129 pub special_tokens: SpecialTokens,
131 pub strategy: TokenizationStrategy,
133}
134
135#[derive(Debug, Clone)]
136pub struct SpecialTokens {
137 pub mask_token: usize,
138 pub cls_token: usize,
139 pub sep_token: usize,
140 pub pad_token: usize,
141 pub unk_token: usize,
142}
143
144#[derive(Debug, Clone)]
145pub enum TokenizationStrategy {
146 NodeCentric,
148 EdgeCentric,
150 WalkBased,
152 SubgraphBased,
154 Hierarchical,
156}
157
158#[derive(Debug, Clone)]
160pub struct FoundationModelParameters {
161 pub pretraining_params: HashMap<String, Tensor>,
163 pub task_params: HashMap<String, HashMap<String, Tensor>>,
165 pub frozen_params: HashSet<String>,
167}
168
169impl GraphFoundationModel {
170 pub fn new(config: FoundationModelConfig) -> Result<Self, FoundationModelError> {
172 let tokenizer = GraphTokenizer::new(config.vocab_size)?;
173 let pretraining_head = PretrainingHead::new(&config)?;
174 let parameters = FoundationModelParameters::new();
175
176 Ok(Self {
177 config,
178 encoder_layers: Vec::new(),
179 pretraining_head,
180 task_heads: HashMap::new(),
181 tokenizer,
182 parameters,
183 })
184 }
185
186 pub fn pretrain(
188 &mut self,
189 graphs: &[GraphData],
190 num_epochs: usize,
191 ) -> Result<PretrainingStats, FoundationModelError> {
192 let mut stats = PretrainingStats::new();
193
194 for epoch in 0..num_epochs {
195 let mut epoch_loss = 0.0;
196 let mut num_batches = 0;
197
198 for graph in graphs {
199 let augmented_graphs = self.apply_augmentation(graph)?;
201
202 for aug_graph in &augmented_graphs {
203 let loss = self.compute_pretraining_loss(aug_graph)?;
205 epoch_loss += loss;
206 num_batches += 1;
207
208 stats.total_samples += 1;
210 }
211 }
212
213 stats.epoch_losses.push(epoch_loss / num_batches as f32);
214 stats.current_epoch = epoch;
215
216 self.update_learning_rate(epoch);
218 }
219
220 stats.pretraining_completed = true;
221 Ok(stats)
222 }
223
224 pub fn finetune(
226 &mut self,
227 task_name: &str,
228 train_data: &[(GraphData, Tensor)],
229 val_data: &[(GraphData, Tensor)],
230 task_config: TaskConfig,
231 ) -> Result<FinetuningStats, FoundationModelError> {
232 self.add_task_head(task_name, task_config.task_type.clone())?;
234
235 if task_config.freeze_pretrained {
237 self.freeze_pretrained_parameters();
238 }
239
240 let mut stats = FinetuningStats::new();
241
242 for _epoch in 0..task_config.num_epochs {
243 let mut train_loss = 0.0;
245 for (graph, target) in train_data {
246 let prediction = self.forward_task(graph, task_name)?;
247 let loss = self.compute_task_loss(&prediction, target, &task_config.task_type)?;
248 train_loss += loss;
249 }
250
251 let mut val_loss = 0.0;
253 let mut val_accuracy = 0.0;
254 for (graph, target) in val_data {
255 let prediction = self.forward_task(graph, task_name)?;
256 let loss = self.compute_task_loss(&prediction, target, &task_config.task_type)?;
257 val_loss += loss;
258
259 let accuracy =
260 self.compute_accuracy(&prediction, target, &task_config.task_type)?;
261 val_accuracy += accuracy;
262 }
263
264 stats
265 .train_losses
266 .push(train_loss / train_data.len() as f32);
267 stats.val_losses.push(val_loss / val_data.len() as f32);
268 stats
269 .val_accuracies
270 .push(val_accuracy / val_data.len() as f32);
271 }
272
273 Ok(stats)
274 }
275
276 fn compute_pretraining_loss(&self, graph: &GraphData) -> Result<f32, FoundationModelError> {
278 let mut total_loss = 0.0;
279
280 for objective in &self.pretraining_head.active_objectives {
281 let loss = match objective {
282 PretrainingObjective::MaskedNodeModeling => self.compute_masked_node_loss(graph)?,
283 PretrainingObjective::MaskedEdgeModeling => self.compute_masked_edge_loss(graph)?,
284 PretrainingObjective::GraphContrastive => {
285 self.compute_graph_contrastive_loss(graph)?
286 }
287 PretrainingObjective::NodeContrastive => {
288 self.compute_node_contrastive_loss(graph)?
289 }
290 PretrainingObjective::StructurePrediction => {
291 self.compute_structure_prediction_loss(graph)?
292 }
293 PretrainingObjective::MotifPrediction => {
294 self.compute_motif_prediction_loss(graph)?
295 }
296 PretrainingObjective::PropertyPrediction => {
297 self.compute_property_prediction_loss(graph)?
298 }
299 PretrainingObjective::GraphDenoising => self.compute_denoising_loss(graph)?,
300 };
301
302 total_loss += loss;
303 }
304
305 Ok(total_loss)
306 }
307
308 fn compute_masked_node_loss(&self, graph: &GraphData) -> Result<f32, FoundationModelError> {
310 let _mask_prob = 0.15;
312 let masked_graph = self.mask_nodes(graph, _mask_prob)?;
313
314 let encoded = self.encode_graph(&masked_graph)?;
316
317 let loss = self.compute_reconstruction_loss(&encoded, &graph.x)?;
320
321 Ok(loss)
322 }
323
324 fn compute_masked_edge_loss(&self, _graph: &GraphData) -> Result<f32, FoundationModelError> {
326 let _mask_prob = 0.15;
328
329 Ok(0.3)
332 }
333
334 fn compute_graph_contrastive_loss(
336 &self,
337 graph: &GraphData,
338 ) -> Result<f32, FoundationModelError> {
339 let positive_graph = self.create_positive_augmentation(graph)?;
341 let negative_graphs = self.create_negative_augmentations(graph, 5)?;
342
343 let anchor_embedding = self.encode_graph_global(graph)?;
345 let positive_embedding = self.encode_graph_global(&positive_graph)?;
346
347 let mut negative_embeddings = Vec::new();
348 for neg_graph in &negative_graphs {
349 let neg_embedding = self.encode_graph_global(neg_graph)?;
350 negative_embeddings.push(neg_embedding);
351 }
352
353 let loss = self.compute_infonce_loss(
355 &anchor_embedding,
356 &positive_embedding,
357 &negative_embeddings,
358 )?;
359
360 Ok(loss)
361 }
362
363 fn apply_augmentation(
365 &self,
366 graph: &GraphData,
367 ) -> Result<Vec<GraphData>, FoundationModelError> {
368 let mut augmented = Vec::new();
369
370 augmented.push(graph.clone());
372
373 let feature_augmented = self.augment_features(graph, 0.1)?;
375 augmented.push(feature_augmented);
376
377 let edge_augmented = self.augment_edges(graph, 0.1)?;
379 augmented.push(edge_augmented);
380
381 let subgraph = self.sample_subgraph(graph, 0.8)?;
383 augmented.push(subgraph);
384
385 Ok(augmented)
386 }
387
388 fn compute_node_contrastive_loss(
390 &self,
391 graph: &GraphData,
392 ) -> Result<f32, FoundationModelError> {
393 let node_embeddings = self.encode_graph(graph)?;
395
396 let positive_pairs = self.create_node_positive_pairs(graph)?;
398 let negative_pairs = self.create_node_negative_pairs(graph, 10)?;
399
400 let loss =
402 self.compute_node_level_infonce(&node_embeddings, &positive_pairs, &negative_pairs)?;
403
404 Ok(loss)
405 }
406
407 fn encode_graph(&self, graph: &GraphData) -> Result<Tensor, FoundationModelError> {
410 Ok(graph.x.clone())
412 }
413
414 fn encode_graph_global(&self, graph: &GraphData) -> Result<Tensor, FoundationModelError> {
415 let node_embeddings = self.encode_graph(graph)?;
417 node_embeddings.mean(Some(&[0]), false).map_err(|e| {
419 FoundationModelError::TensorError(format!("Failed to compute mean: {:?}", e))
420 })
421 }
422
423 fn mask_nodes(
424 &self,
425 graph: &GraphData,
426 _mask_prob: f32,
427 ) -> Result<GraphData, FoundationModelError> {
428 let masked_features = graph.x.clone();
430
431 Ok(GraphData::new(masked_features, graph.edge_index.clone()))
435 }
436
437 fn create_positive_augmentation(
438 &self,
439 graph: &GraphData,
440 ) -> Result<GraphData, FoundationModelError> {
441 self.augment_features(graph, 0.1)
443 }
444
445 fn create_negative_augmentations(
446 &self,
447 graph: &GraphData,
448 num_negatives: usize,
449 ) -> Result<Vec<GraphData>, FoundationModelError> {
450 let mut negatives = Vec::new();
451
452 for _ in 0..num_negatives {
453 let negative = self.create_random_graph(graph.num_nodes, graph.num_edges)?;
455 negatives.push(negative);
456 }
457
458 Ok(negatives)
459 }
460
461 fn augment_features(
462 &self,
463 graph: &GraphData,
464 noise_level: f32,
465 ) -> Result<GraphData, FoundationModelError> {
466 let noise = randn(graph.x.shape().dims()).map_err(|e| {
468 FoundationModelError::TensorError(format!("Failed to create noise tensor: {:?}", e))
469 })?;
470
471 let noisy_features = graph.x.add(&noise.mul_scalar(noise_level)?)?;
472
473 Ok(GraphData::new(noisy_features, graph.edge_index.clone()))
474 }
475
476 fn augment_edges(
477 &self,
478 graph: &GraphData,
479 _drop_prob: f32,
480 ) -> Result<GraphData, FoundationModelError> {
481 Ok(graph.clone())
484 }
485
486 fn sample_subgraph(
487 &self,
488 graph: &GraphData,
489 sample_ratio: f32,
490 ) -> Result<GraphData, FoundationModelError> {
491 let num_nodes_to_keep = (graph.num_nodes as f32 * sample_ratio) as usize;
493
494 if num_nodes_to_keep == 0 {
495 return Ok(graph.clone());
496 }
497
498 Ok(graph.clone())
500 }
501
502 fn create_random_graph(
503 &self,
504 num_nodes: usize,
505 num_edges: usize,
506 ) -> Result<GraphData, FoundationModelError> {
507 let features = randn(&[num_nodes, self.config.model_dim]).map_err(|e| {
509 FoundationModelError::TensorError(format!("Failed to create features: {:?}", e))
510 })?;
511
512 let edge_index = zeros(&[2, num_edges]).map_err(|e| {
513 FoundationModelError::TensorError(format!("Failed to create edge index: {:?}", e))
514 })?;
515
516 Ok(GraphData::new(features, edge_index))
517 }
518
519 fn compute_reconstruction_loss(
520 &self,
521 predictions: &Tensor,
522 targets: &Tensor,
523 ) -> Result<f32, FoundationModelError> {
524 let diff = predictions.sub(targets)?;
526 let squared = diff.mul(&diff)?;
527 let mean_loss = squared.mean(None, false).map_err(|e| {
528 FoundationModelError::TensorError(format!("Failed to compute mean: {:?}", e))
529 })?;
530
531 let loss_data = mean_loss.to_vec().map_err(|e| {
532 FoundationModelError::TensorError(format!("Failed to extract loss: {:?}", e))
533 })?;
534
535 Ok(loss_data[0])
536 }
537
538 fn compute_infonce_loss(
539 &self,
540 anchor: &Tensor,
541 positive: &Tensor,
542 negatives: &[Tensor],
543 ) -> Result<f32, FoundationModelError> {
544 let temperature = self.pretraining_head.contrastive_head.temperature;
546
547 let pos_sim = self.cosine_similarity(anchor, positive)? / temperature;
549
550 let mut neg_sims = Vec::new();
552 for negative in negatives {
553 let neg_sim = self.cosine_similarity(anchor, negative)? / temperature;
554 neg_sims.push(neg_sim);
555 }
556
557 let loss = -pos_sim + (neg_sims.iter().map(|x| x.exp()).sum::<f32>()).ln();
559
560 Ok(loss)
561 }
562
563 fn cosine_similarity(&self, a: &Tensor, b: &Tensor) -> Result<f32, FoundationModelError> {
564 let dot_product = a.dot(b)?;
566 let norm_a = a.norm()?;
567 let norm_b = b.norm()?;
568
569 let dot_data = dot_product.to_vec()?;
570 let norm_a_data = norm_a.to_vec()?;
571 let norm_b_data = norm_b.to_vec()?;
572
573 Ok(dot_data[0] / (norm_a_data[0] * norm_b_data[0]))
574 }
575
576 fn create_node_positive_pairs(
577 &self,
578 graph: &GraphData,
579 ) -> Result<Vec<(usize, usize)>, FoundationModelError> {
580 let edge_data = graph.edge_index.to_vec()?;
582 let num_edges = edge_data.len() / 2;
583
584 let mut pairs = Vec::new();
585 for i in 0..num_edges {
586 let src = edge_data[i] as usize;
587 let dst = edge_data[i + num_edges] as usize;
588 pairs.push((src, dst));
589 }
590
591 Ok(pairs)
592 }
593
594 fn create_node_negative_pairs(
595 &self,
596 graph: &GraphData,
597 num_negatives: usize,
598 ) -> Result<Vec<(usize, usize)>, FoundationModelError> {
599 let mut pairs = Vec::new();
601 let mut rng = scirs2_core::random::thread_rng();
602
603 for _ in 0..num_negatives {
604 let src = rng.gen_range(0..graph.num_nodes);
605 let dst = rng.gen_range(0..graph.num_nodes);
606 if src != dst {
607 pairs.push((src, dst));
608 }
609 }
610
611 Ok(pairs)
612 }
613
614 fn compute_node_level_infonce(
615 &self,
616 _embeddings: &Tensor,
617 positive_pairs: &[(usize, usize)],
618 _negative_pairs: &[(usize, usize)],
619 ) -> Result<f32, FoundationModelError> {
620 let mut total_loss = 0.0;
622
623 for &(_src, _dst) in positive_pairs {
624 total_loss += 1.0; }
628
629 Ok(total_loss / positive_pairs.len() as f32)
630 }
631
632 fn compute_structure_prediction_loss(
633 &self,
634 _graph: &GraphData,
635 ) -> Result<f32, FoundationModelError> {
636 Ok(0.5)
638 }
639
640 fn compute_motif_prediction_loss(
641 &self,
642 _graph: &GraphData,
643 ) -> Result<f32, FoundationModelError> {
644 Ok(0.3)
646 }
647
648 fn compute_property_prediction_loss(
649 &self,
650 _graph: &GraphData,
651 ) -> Result<f32, FoundationModelError> {
652 Ok(0.4)
654 }
655
656 fn compute_denoising_loss(&self, _graph: &GraphData) -> Result<f32, FoundationModelError> {
657 Ok(0.2)
659 }
660
661 fn forward_task(
662 &self,
663 graph: &GraphData,
664 _task_name: &str,
665 ) -> Result<Tensor, FoundationModelError> {
666 self.encode_graph(graph)
670 }
671
672 fn add_task_head(
673 &mut self,
674 task_name: &str,
675 task_type: TaskType,
676 ) -> Result<(), FoundationModelError> {
677 let task_type_name = match task_type {
679 TaskType::NodeClassification { num_classes } => {
680 format!("NodeClassification_{}", num_classes)
681 }
682 TaskType::GraphClassification { num_classes } => {
683 format!("GraphClassification_{}", num_classes)
684 }
685 TaskType::LinkPrediction => "LinkPrediction".to_string(),
686 TaskType::GraphRegression => "GraphRegression".to_string(),
687 };
688
689 self.task_heads
690 .insert(task_name.to_string(), task_type_name);
691 Ok(())
692 }
693
694 fn freeze_pretrained_parameters(&mut self) {
695 for param_name in self.parameters.pretraining_params.keys() {
697 self.parameters.frozen_params.insert(param_name.clone());
698 }
699 }
700
701 fn compute_task_loss(
702 &self,
703 _prediction: &Tensor,
704 _target: &Tensor,
705 task_type: &TaskType,
706 ) -> Result<f32, FoundationModelError> {
707 match task_type {
708 TaskType::NodeClassification { .. } | TaskType::GraphClassification { .. } => {
709 Ok(1.0)
712 }
713 TaskType::LinkPrediction => {
714 Ok(0.7)
716 }
717 TaskType::GraphRegression => {
718 Ok(0.5)
720 }
721 }
722 }
723
724 fn compute_accuracy(
725 &self,
726 _prediction: &Tensor,
727 _target: &Tensor,
728 task_type: &TaskType,
729 ) -> Result<f32, FoundationModelError> {
730 match task_type {
731 TaskType::NodeClassification { .. } | TaskType::GraphClassification { .. } => {
732 Ok(0.85)
735 }
736 TaskType::LinkPrediction => {
737 Ok(0.78)
739 }
740 TaskType::GraphRegression => {
741 Ok(0.65)
743 }
744 }
745 }
746
747 fn update_learning_rate(&mut self, _epoch: usize) {
748 }
751}
752
753#[derive(Debug, Clone)]
755pub struct TaskConfig {
756 pub task_type: TaskType,
758 pub num_epochs: usize,
760 pub learning_rate: f32,
762 pub freeze_pretrained: bool,
764 pub task_params: HashMap<String, f32>,
766}
767
768#[derive(Debug, Clone)]
770pub enum TaskType {
771 NodeClassification { num_classes: usize },
773 GraphClassification { num_classes: usize },
775 LinkPrediction,
777 GraphRegression,
779}
780
781pub trait TaskHead: fmt::Debug {
783 fn forward(&self, embeddings: &Tensor) -> Result<Tensor, FoundationModelError>;
784 fn parameters(&self) -> Vec<Tensor>;
785}
786
787#[derive(Debug)]
789pub struct NodeClassificationHead {
790 pub classifier: Tensor,
791 pub bias: Tensor,
792}
793
794impl NodeClassificationHead {
795 pub fn new(input_dim: usize, num_classes: usize) -> Result<Self, FoundationModelError> {
796 let classifier = randn(&[input_dim, num_classes]).map_err(|e| {
797 FoundationModelError::TensorError(format!("Failed to create classifier: {:?}", e))
798 })?;
799 let bias = zeros(&[num_classes]).map_err(|e| {
800 FoundationModelError::TensorError(format!("Failed to create bias: {:?}", e))
801 })?;
802
803 Ok(Self { classifier, bias })
804 }
805}
806
807impl TaskHead for NodeClassificationHead {
808 fn forward(&self, embeddings: &Tensor) -> Result<Tensor, FoundationModelError> {
809 let logits = embeddings.matmul(&self.classifier).map_err(|e| {
810 FoundationModelError::TensorError(format!("Failed to compute logits: {:?}", e))
811 })?;
812
813 logits
814 .add(&self.bias)
815 .map_err(|e| FoundationModelError::TensorError(format!("Failed to add bias: {:?}", e)))
816 }
817
818 fn parameters(&self) -> Vec<Tensor> {
819 vec![self.classifier.clone(), self.bias.clone()]
820 }
821}
822
823#[derive(Debug)]
825pub struct GraphClassificationHead {
826 pub pooling_layer: Tensor,
827 pub classifier: Tensor,
828 pub bias: Tensor,
829}
830
831impl GraphClassificationHead {
832 pub fn new(input_dim: usize, num_classes: usize) -> Result<Self, FoundationModelError> {
833 let pooling_layer = randn(&[input_dim, input_dim]).map_err(|e| {
834 FoundationModelError::TensorError(format!("Failed to create pooling layer: {:?}", e))
835 })?;
836 let classifier = randn(&[input_dim, num_classes]).map_err(|e| {
837 FoundationModelError::TensorError(format!("Failed to create classifier: {:?}", e))
838 })?;
839 let bias = zeros(&[num_classes]).map_err(|e| {
840 FoundationModelError::TensorError(format!("Failed to create bias: {:?}", e))
841 })?;
842
843 Ok(Self {
844 pooling_layer,
845 classifier,
846 bias,
847 })
848 }
849}
850
851impl TaskHead for GraphClassificationHead {
852 fn forward(&self, embeddings: &Tensor) -> Result<Tensor, FoundationModelError> {
853 let pooled = embeddings.mean(Some(&[0]), true).map_err(|e| {
855 FoundationModelError::TensorError(format!("Failed to compute mean: {:?}", e))
856 })?;
857 let transformed = pooled.matmul(&self.pooling_layer)?;
858 let logits = transformed.matmul(&self.classifier)?;
859 logits
860 .add(&self.bias)
861 .map_err(|e| FoundationModelError::TensorError(format!("Failed to add bias: {:?}", e)))
862 }
863
864 fn parameters(&self) -> Vec<Tensor> {
865 vec![
866 self.pooling_layer.clone(),
867 self.classifier.clone(),
868 self.bias.clone(),
869 ]
870 }
871}
872
873#[derive(Debug)]
875pub struct LinkPredictionHead {
876 pub edge_predictor: Tensor,
877}
878
879impl LinkPredictionHead {
880 pub fn new(input_dim: usize) -> Result<Self, FoundationModelError> {
881 let edge_predictor = randn(&[input_dim * 2, 1]).map_err(|e| {
882 FoundationModelError::TensorError(format!("Failed to create edge predictor: {:?}", e))
883 })?;
884
885 Ok(Self { edge_predictor })
886 }
887}
888
889impl TaskHead for LinkPredictionHead {
890 fn forward(&self, embeddings: &Tensor) -> Result<Tensor, FoundationModelError> {
891 embeddings.matmul(&self.edge_predictor).map_err(|e| {
893 FoundationModelError::TensorError(format!("Failed to predict links: {:?}", e))
894 })
895 }
896
897 fn parameters(&self) -> Vec<Tensor> {
898 vec![self.edge_predictor.clone()]
899 }
900}
901
902#[derive(Debug)]
904pub struct GraphRegressionHead {
905 pub regressor: Tensor,
906 pub bias: Tensor,
907}
908
909impl GraphRegressionHead {
910 pub fn new(input_dim: usize) -> Result<Self, FoundationModelError> {
911 let regressor = randn(&[input_dim, 1]).map_err(|e| {
912 FoundationModelError::TensorError(format!("Failed to create regressor: {:?}", e))
913 })?;
914 let bias = zeros(&[1]).map_err(|e| {
915 FoundationModelError::TensorError(format!("Failed to create bias: {:?}", e))
916 })?;
917
918 Ok(Self { regressor, bias })
919 }
920}
921
922impl TaskHead for GraphRegressionHead {
923 fn forward(&self, embeddings: &Tensor) -> Result<Tensor, FoundationModelError> {
924 let pooled = embeddings.mean(Some(&[0]), true).map_err(|e| {
925 FoundationModelError::TensorError(format!("Failed to compute mean: {:?}", e))
926 })?;
927 let output = pooled.matmul(&self.regressor)?;
928 output
929 .add(&self.bias)
930 .map_err(|e| FoundationModelError::TensorError(format!("Failed to add bias: {:?}", e)))
931 }
932
933 fn parameters(&self) -> Vec<Tensor> {
934 vec![self.regressor.clone(), self.bias.clone()]
935 }
936}
937
938#[derive(Debug, Clone)]
940pub struct PretrainingStats {
941 pub epoch_losses: Vec<f32>,
942 pub total_samples: usize,
943 pub current_epoch: usize,
944 pub pretraining_completed: bool,
945 pub best_loss: f32,
946}
947
948impl PretrainingStats {
949 pub fn new() -> Self {
950 Self {
951 epoch_losses: Vec::new(),
952 total_samples: 0,
953 current_epoch: 0,
954 pretraining_completed: false,
955 best_loss: f32::INFINITY,
956 }
957 }
958}
959
960#[derive(Debug, Clone)]
962pub struct FinetuningStats {
963 pub train_losses: Vec<f32>,
964 pub val_losses: Vec<f32>,
965 pub val_accuracies: Vec<f32>,
966 pub best_val_accuracy: f32,
967 pub converged: bool,
968}
969
970impl FinetuningStats {
971 pub fn new() -> Self {
972 Self {
973 train_losses: Vec::new(),
974 val_losses: Vec::new(),
975 val_accuracies: Vec::new(),
976 best_val_accuracy: 0.0,
977 converged: false,
978 }
979 }
980}
981
982impl GraphTokenizer {
984 pub fn new(vocab_size: usize) -> Result<Self, FoundationModelError> {
985 let mut node_vocab = HashMap::new();
986 let mut edge_vocab = HashMap::new();
987
988 for i in 0..vocab_size / 2 {
990 node_vocab.insert(format!("node_{}", i), i);
991 edge_vocab.insert(format!("edge_{}", i), i);
992 }
993
994 let special_tokens = SpecialTokens {
995 mask_token: vocab_size - 5,
996 cls_token: vocab_size - 4,
997 sep_token: vocab_size - 3,
998 pad_token: vocab_size - 2,
999 unk_token: vocab_size - 1,
1000 };
1001
1002 Ok(Self {
1003 node_vocab,
1004 edge_vocab,
1005 special_tokens,
1006 strategy: TokenizationStrategy::NodeCentric,
1007 })
1008 }
1009
1010 pub fn tokenize(&self, graph: &GraphData) -> Result<Vec<usize>, FoundationModelError> {
1012 match self.strategy {
1013 TokenizationStrategy::NodeCentric => self.tokenize_node_centric(graph),
1014 TokenizationStrategy::EdgeCentric => self.tokenize_edge_centric(graph),
1015 TokenizationStrategy::WalkBased => self.tokenize_walk_based(graph),
1016 TokenizationStrategy::SubgraphBased => self.tokenize_subgraph_based(graph),
1017 TokenizationStrategy::Hierarchical => self.tokenize_hierarchical(graph),
1018 }
1019 }
1020
1021 fn tokenize_node_centric(&self, graph: &GraphData) -> Result<Vec<usize>, FoundationModelError> {
1022 let mut tokens = vec![self.special_tokens.cls_token];
1023
1024 for node in 0..graph.num_nodes {
1026 tokens.push(node % self.node_vocab.len());
1027 }
1028
1029 tokens.push(self.special_tokens.sep_token);
1030 Ok(tokens)
1031 }
1032
1033 fn tokenize_edge_centric(&self, graph: &GraphData) -> Result<Vec<usize>, FoundationModelError> {
1034 let mut tokens = vec![self.special_tokens.cls_token];
1035
1036 let edge_data = graph.edge_index.to_vec()?;
1038 let num_edges = edge_data.len() / 2;
1039
1040 for i in 0..num_edges {
1041 let edge_token = i % self.edge_vocab.len();
1042 tokens.push(edge_token);
1043 }
1044
1045 tokens.push(self.special_tokens.sep_token);
1046 Ok(tokens)
1047 }
1048
1049 fn tokenize_walk_based(&self, graph: &GraphData) -> Result<Vec<usize>, FoundationModelError> {
1050 let mut tokens = vec![self.special_tokens.cls_token];
1052
1053 let walk_length = 20;
1055 let mut current_node = 0;
1056
1057 for _ in 0..walk_length {
1058 tokens.push(current_node % self.node_vocab.len());
1059 current_node = (current_node + 1) % graph.num_nodes;
1061 }
1062
1063 tokens.push(self.special_tokens.sep_token);
1064 Ok(tokens)
1065 }
1066
1067 fn tokenize_subgraph_based(
1068 &self,
1069 graph: &GraphData,
1070 ) -> Result<Vec<usize>, FoundationModelError> {
1071 let mut tokens = vec![self.special_tokens.cls_token];
1073
1074 for i in 0..graph.num_nodes.min(10) {
1076 tokens.push(i % self.node_vocab.len());
1077 }
1078
1079 tokens.push(self.special_tokens.sep_token);
1080 Ok(tokens)
1081 }
1082
1083 fn tokenize_hierarchical(&self, graph: &GraphData) -> Result<Vec<usize>, FoundationModelError> {
1084 let mut tokens = vec![self.special_tokens.cls_token];
1086
1087 for level in 0..3 {
1089 for node in 0..graph.num_nodes.min(5) {
1090 let token = (level * graph.num_nodes + node) % self.node_vocab.len();
1091 tokens.push(token);
1092 }
1093 tokens.push(self.special_tokens.sep_token);
1094 }
1095
1096 Ok(tokens)
1097 }
1098}
1099
1100impl PretrainingHead {
1102 pub fn new(config: &FoundationModelConfig) -> Result<Self, FoundationModelError> {
1103 let mlm_head = MLMHead {
1104 output_projection: randn(&[config.model_dim, config.vocab_size])?,
1105 bias: zeros(&[config.vocab_size])?,
1106 mask_token: randn(&[config.model_dim])?,
1107 };
1108
1109 let contrastive_head = ContrastiveHead {
1110 projection: randn(&[config.model_dim, config.model_dim])?,
1111 temperature: 0.1,
1112 embed_dim: config.model_dim,
1113 };
1114
1115 let structure_head = StructurePredictionHead {
1116 edge_predictor: randn(&[config.model_dim * 2, 1])?,
1117 motif_predictor: randn(&[config.model_dim, 10])?,
1118 property_predictor: randn(&[config.model_dim, 1])?,
1119 };
1120
1121 Ok(Self {
1122 mlm_head,
1123 contrastive_head,
1124 structure_head,
1125 active_objectives: config.pretraining_objectives.clone(),
1126 })
1127 }
1128}
1129
1130impl FoundationModelParameters {
1131 pub fn new() -> Self {
1132 Self {
1133 pretraining_params: HashMap::new(),
1134 task_params: HashMap::new(),
1135 frozen_params: HashSet::new(),
1136 }
1137 }
1138}
1139
1140#[derive(Debug, Clone)]
1142pub enum FoundationModelError {
1143 TensorError(String),
1145 ConfigError(String),
1147 TaskNotFound(String),
1149 PretrainingError(String),
1151 FinetuningError(String),
1153 TokenizationError(String),
1155}
1156
1157impl From<torsh_core::error::TorshError> for FoundationModelError {
1158 fn from(err: torsh_core::error::TorshError) -> Self {
1159 FoundationModelError::TensorError(format!("{:?}", err))
1160 }
1161}
1162
1163impl fmt::Display for FoundationModelError {
1164 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1165 match self {
1166 FoundationModelError::TensorError(msg) => write!(f, "Tensor error: {}", msg),
1167 FoundationModelError::ConfigError(msg) => write!(f, "Configuration error: {}", msg),
1168 FoundationModelError::TaskNotFound(task) => write!(f, "Task not found: {}", task),
1169 FoundationModelError::PretrainingError(msg) => write!(f, "Pre-training error: {}", msg),
1170 FoundationModelError::FinetuningError(msg) => write!(f, "Fine-tuning error: {}", msg),
1171 FoundationModelError::TokenizationError(msg) => {
1172 write!(f, "Tokenization error: {}", msg)
1173 }
1174 }
1175 }
1176}
1177
1178impl std::error::Error for FoundationModelError {}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 #[test]
1185 fn test_foundation_model_config() {
1186 let config = FoundationModelConfig {
1187 model_dim: 256,
1188 num_layers: 6,
1189 num_heads: 8,
1190 ff_dim: 1024,
1191 max_seq_length: 512,
1192 vocab_size: 1000,
1193 dropout: 0.1,
1194 pretraining_objectives: vec![
1195 PretrainingObjective::MaskedNodeModeling,
1196 PretrainingObjective::GraphContrastive,
1197 ],
1198 };
1199
1200 assert_eq!(config.model_dim, 256);
1201 assert_eq!(config.num_layers, 6);
1202 assert_eq!(config.pretraining_objectives.len(), 2);
1203 }
1204
1205 #[test]
1206 fn test_graph_tokenizer() {
1207 let tokenizer = GraphTokenizer::new(1000);
1208 assert!(tokenizer.is_ok());
1209
1210 let tok = tokenizer.unwrap();
1211 assert_eq!(999, tok.special_tokens.unk_token);
1213 }
1214
1215 #[test]
1216 fn test_task_types() {
1217 let node_task = TaskType::NodeClassification { num_classes: 5 };
1218 let _graph_task = TaskType::GraphClassification { num_classes: 3 };
1219 let _link_task = TaskType::LinkPrediction;
1220 let _regression_task = TaskType::GraphRegression;
1221
1222 match node_task {
1223 TaskType::NodeClassification { num_classes } => assert_eq!(num_classes, 5),
1224 _ => panic!("Wrong task type"),
1225 }
1226 }
1227
1228 #[test]
1229 fn test_pretraining_objectives() {
1230 let objectives = vec![
1231 PretrainingObjective::MaskedNodeModeling,
1232 PretrainingObjective::GraphContrastive,
1233 PretrainingObjective::StructurePrediction,
1234 ];
1235
1236 assert_eq!(objectives.len(), 3);
1237 }
1238
1239 #[test]
1240 fn test_task_heads() {
1241 let node_head = NodeClassificationHead::new(128, 5);
1242 assert!(node_head.is_ok());
1243
1244 let graph_head = GraphClassificationHead::new(128, 3);
1245 assert!(graph_head.is_ok());
1246
1247 let link_head = LinkPredictionHead::new(128);
1248 assert!(link_head.is_ok());
1249
1250 let regression_head = GraphRegressionHead::new(128);
1251 assert!(regression_head.is_ok());
1252 }
1253
1254 #[test]
1255 fn test_tokenization_strategies() {
1256 let strategies = vec![
1257 TokenizationStrategy::NodeCentric,
1258 TokenizationStrategy::EdgeCentric,
1259 TokenizationStrategy::WalkBased,
1260 TokenizationStrategy::SubgraphBased,
1261 TokenizationStrategy::Hierarchical,
1262 ];
1263
1264 assert_eq!(strategies.len(), 5);
1265 }
1266
1267 #[test]
1268 fn test_special_tokens() {
1269 let special_tokens = SpecialTokens {
1270 mask_token: 995,
1271 cls_token: 996,
1272 sep_token: 997,
1273 pad_token: 998,
1274 unk_token: 999,
1275 };
1276
1277 assert_eq!(special_tokens.mask_token, 995);
1278 assert_eq!(special_tokens.unk_token, 999);
1279 }
1280}