1#![allow(dead_code)]
16type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
19
20use crate::parameter::Parameter;
21use crate::{GraphData, GraphLayer};
22use std::collections::{HashMap, HashSet};
23use torsh_tensor::{
24 creation::{from_vec, ones, randn, zeros},
25 Tensor,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum Modality {
31 Text,
32 Image,
33 Audio,
34 Tabular,
35 Graph,
36 Video,
37 TimeSeries,
38}
39
40#[derive(Debug, Clone)]
42pub struct MultiModalNodeData {
43 pub modalities: HashMap<Modality, Tensor>,
44 pub node_id: usize,
45 pub labels: Option<Tensor>,
46}
47
48impl MultiModalNodeData {
49 pub fn new(node_id: usize) -> Self {
51 Self {
52 modalities: HashMap::new(),
53 node_id,
54 labels: None,
55 }
56 }
57
58 pub fn add_modality(mut self, modality: Modality, data: Tensor) -> Self {
60 self.modalities.insert(modality, data);
61 self
62 }
63
64 pub fn with_labels(mut self, labels: Tensor) -> Self {
66 self.labels = Some(labels);
67 self
68 }
69
70 pub fn available_modalities(&self) -> Vec<Modality> {
72 self.modalities.keys().copied().collect()
73 }
74
75 pub fn has_modality(&self, modality: Modality) -> bool {
77 self.modalities.contains_key(&modality)
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct MultiModalGraphData {
84 pub graph: GraphData,
86 pub node_data: HashMap<usize, MultiModalNodeData>,
88 pub available_modalities: HashSet<Modality>,
90 pub modality_dims: HashMap<Modality, usize>,
92}
93
94impl MultiModalGraphData {
95 pub fn new(graph: GraphData) -> Self {
97 Self {
98 graph,
99 node_data: HashMap::new(),
100 available_modalities: HashSet::new(),
101 modality_dims: HashMap::new(),
102 }
103 }
104
105 pub fn add_node_data(&mut self, node_data: MultiModalNodeData) {
107 let node_id = node_data.node_id;
108
109 for modality in node_data.available_modalities() {
111 self.available_modalities.insert(modality);
112
113 if let Some(data) = node_data.modalities.get(&modality) {
115 let dim = data.shape().dims().iter().product::<usize>();
116 self.modality_dims.insert(modality, dim);
117 }
118 }
119
120 self.node_data.insert(node_id, node_data);
121 }
122
123 pub fn get_modality_data(&self, modality: Modality) -> Vec<(usize, &Tensor)> {
125 self.node_data
126 .iter()
127 .filter_map(|(&node_id, data)| {
128 data.modalities
129 .get(&modality)
130 .map(|tensor| (node_id, tensor))
131 })
132 .collect()
133 }
134
135 pub fn get_complete_nodes(&self, modalities: &[Modality]) -> Vec<usize> {
137 self.node_data
138 .iter()
139 .filter(|(_, data)| {
140 modalities
141 .iter()
142 .all(|&modality| data.has_modality(modality))
143 })
144 .map(|(&node_id, _)| node_id)
145 .collect()
146 }
147
148 pub fn modality_statistics(&self) -> HashMap<Modality, f32> {
150 let total_nodes = self.graph.num_nodes;
151 let mut stats = HashMap::new();
152
153 for &modality in &self.available_modalities {
154 let count = self
155 .node_data
156 .values()
157 .filter(|data| data.has_modality(modality))
158 .count();
159
160 let coverage = count as f32 / total_nodes as f32;
161 stats.insert(modality, coverage);
162 }
163
164 stats
165 }
166}
167
168#[derive(Debug)]
170pub struct CrossModalGraphAttention {
171 modalities: Vec<Modality>,
172 feature_dim: usize,
173 attention_dim: usize,
174 num_heads: usize,
175
176 modality_projections: HashMap<Modality, Parameter>,
178
179 query_weights: Parameter,
181 key_weights: Parameter,
182 value_weights: Parameter,
183
184 output_projection: Parameter,
186
187 layer_norm_weight: Parameter,
189 layer_norm_bias: Parameter,
190
191 dropout: f32,
192}
193
194impl CrossModalGraphAttention {
195 pub fn new(
197 modalities: Vec<Modality>,
198 modality_dims: HashMap<Modality, usize>,
199 feature_dim: usize,
200 attention_dim: usize,
201 num_heads: usize,
202 dropout: f32,
203 ) -> Result<Self> {
204 let mut modality_projections = HashMap::new();
205
206 for modality in &modalities {
208 let input_dim = modality_dims.get(modality).copied().unwrap_or(feature_dim);
209 modality_projections
210 .insert(*modality, Parameter::new(randn(&[input_dim, feature_dim])?));
211 }
212
213 let query_weights = Parameter::new(randn(&[feature_dim, attention_dim])?);
214 let key_weights = Parameter::new(randn(&[feature_dim, attention_dim])?);
215 let value_weights = Parameter::new(randn(&[feature_dim, attention_dim])?);
216 let output_projection = Parameter::new(randn(&[attention_dim, feature_dim])?);
217
218 let layer_norm_weight = Parameter::new(ones(&[feature_dim])?);
219 let layer_norm_bias = Parameter::new(zeros::<f32>(&[feature_dim])?);
220
221 Ok(Self {
222 modalities,
223 feature_dim,
224 attention_dim,
225 num_heads,
226 modality_projections,
227 query_weights,
228 key_weights,
229 value_weights,
230 output_projection,
231 layer_norm_weight,
232 layer_norm_bias,
233 dropout,
234 })
235 }
236
237 pub fn forward(&self, mm_graph: &MultiModalGraphData) -> Result<Tensor> {
242 let num_nodes = mm_graph.graph.num_nodes;
243
244 let mut modality_features = HashMap::new();
246
247 for &modality in &self.modalities {
248 let projection = &self.modality_projections[&modality];
249 let modality_data = mm_graph.get_modality_data(modality);
250
251 if !modality_data.is_empty() {
252 let features =
253 self.project_modality_features(&modality_data, projection, num_nodes)?;
254 modality_features.insert(modality, features);
255 }
256 }
257
258 let attended_features = self.apply_cross_modal_attention(&modality_features)?;
260
261 self.layer_norm(&attended_features)
263 }
264
265 fn project_modality_features(
267 &self,
268 modality_data: &[(usize, &Tensor)],
269 projection: &Parameter,
270 num_nodes: usize,
271 ) -> Result<Tensor> {
272 let mut projected_data = vec![0.0f32; num_nodes * self.feature_dim];
273
274 for &(node_id, features) in modality_data {
275 if node_id < num_nodes {
276 let feature_data = features.to_vec()?;
277 let input_tensor = from_vec(
278 feature_data,
279 &[1, features.shape().dims().iter().product::<usize>()],
280 torsh_core::device::DeviceType::Cpu,
281 )?;
282
283 let projected = input_tensor.matmul(&projection.clone_data())?;
284 let projected_data_vec = projected.to_vec()?;
285
286 for (i, &val) in projected_data_vec.iter().enumerate() {
287 if i < self.feature_dim {
288 projected_data[node_id * self.feature_dim + i] = val;
289 }
290 }
291 }
292 }
293
294 Ok(from_vec(
295 projected_data,
296 &[num_nodes, self.feature_dim],
297 torsh_core::device::DeviceType::Cpu,
298 )?)
299 }
300
301 fn apply_cross_modal_attention(
311 &self,
312 modality_features: &HashMap<Modality, Tensor>,
313 ) -> Result<Tensor> {
314 if modality_features.is_empty() {
315 return Ok(zeros::<f32>(&[1, self.feature_dim])?);
318 }
319
320 let first_modality = modality_features.keys().next().ok_or_else(|| {
322 torsh_core::error::TorshError::InvalidArgument(
323 "no modality features supplied".to_string(),
324 )
325 })?;
326 let base_features = &modality_features[first_modality];
327 let _num_nodes = base_features.shape().dims()[0];
328
329 let queries = base_features.matmul(&self.query_weights.clone_data())?;
331 let _keys = base_features.matmul(&self.key_weights.clone_data())?;
332 let values = base_features.matmul(&self.value_weights.clone_data())?;
333
334 let mut attended_values = values.clone();
336
337 for (modality, features) in modality_features {
338 if *modality != *first_modality {
339 let modal_keys = features.matmul(&self.key_weights.clone_data())?;
340 let modal_values = features.matmul(&self.value_weights.clone_data())?;
341
342 let attention_scores = queries.matmul(&modal_keys.t()?)?;
344 let attention_weights = self.softmax(&attention_scores);
345 let attended = attention_weights?.matmul(&modal_values)?;
346
347 attended_values = attended_values.add(&attended)?;
348 }
349 }
350
351 Ok(attended_values.matmul(&self.output_projection.clone_data())?)
353 }
354
355 fn softmax(&self, x: &Tensor) -> Result<Tensor> {
357 let data = x.to_vec()?;
358 let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
359
360 let exp_data: Vec<f32> = data.iter().map(|&val| (val - max_val).exp()).collect();
361 let sum_exp: f32 = exp_data.iter().sum();
362
363 let softmax_data: Vec<f32> = exp_data.iter().map(|&val| val / sum_exp).collect();
364
365 Ok(from_vec(
366 softmax_data,
367 x.shape().dims(),
368 torsh_core::device::DeviceType::Cpu,
369 )?)
370 }
371
372 fn layer_norm(&self, x: &Tensor) -> Result<Tensor> {
374 let data = x.to_vec()?;
375 let num_features = self.feature_dim;
376 let num_samples = data.len() / num_features;
377
378 let mut normalized_data = Vec::new();
379
380 for sample in 0..num_samples {
381 let start_idx = sample * num_features;
382 let end_idx = start_idx + num_features;
383 let sample_data = &data[start_idx..end_idx];
384
385 let mean: f32 = sample_data.iter().sum::<f32>() / num_features as f32;
387 let variance: f32 =
388 sample_data.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / num_features as f32;
389 let std = (variance + 1e-5).sqrt();
390
391 for &val in sample_data {
393 let normalized = (val - mean) / std;
394 normalized_data.push(normalized);
395 }
396 }
397
398 let normalized_tensor = from_vec(
399 normalized_data,
400 x.shape().dims(),
401 torsh_core::device::DeviceType::Cpu,
402 )?;
403
404 Ok(normalized_tensor
406 .mul(&self.layer_norm_weight.clone_data())?
407 .add(&self.layer_norm_bias.clone_data())?)
408 }
409}
410
411impl GraphLayer for CrossModalGraphAttention {
412 fn forward(&self, graph: &GraphData) -> Result<GraphData> {
413 let mut mm_graph = MultiModalGraphData::new(graph.clone());
415
416 for node_id in 0..graph.num_nodes {
417 let node_features = graph.x.slice_tensor(0, node_id, node_id + 1)?;
418 let node_data =
419 MultiModalNodeData::new(node_id).add_modality(Modality::Graph, node_features);
420 mm_graph.add_node_data(node_data);
421 }
422
423 let output_features = CrossModalGraphAttention::forward(self, &mm_graph)?;
424
425 let mut output_graph = graph.clone();
426 output_graph.x = output_features;
427 Ok(output_graph)
428 }
429
430 fn parameters(&self) -> Vec<Tensor> {
431 let mut params = vec![
432 self.query_weights.clone_data(),
433 self.key_weights.clone_data(),
434 self.value_weights.clone_data(),
435 self.output_projection.clone_data(),
436 self.layer_norm_weight.clone_data(),
437 self.layer_norm_bias.clone_data(),
438 ];
439
440 for projection in self.modality_projections.values() {
441 params.push(projection.clone_data());
442 }
443
444 params
445 }
446}
447
448#[derive(Debug)]
450pub struct MultiModalFusion {
451 fusion_strategy: FusionStrategy,
452 modalities: Vec<Modality>,
453 feature_dim: usize,
454 fusion_weights: Option<Parameter>,
455 gating_network: Option<Vec<Parameter>>,
456}
457
458#[derive(Debug, Clone, Copy)]
459pub enum FusionStrategy {
460 Concatenation,
461 ElementwiseSum,
462 WeightedSum,
463 AttentionFusion,
464 GatedFusion,
465}
466
467impl MultiModalFusion {
468 pub fn new(
470 fusion_strategy: FusionStrategy,
471 modalities: Vec<Modality>,
472 feature_dim: usize,
473 ) -> Result<Self> {
474 let fusion_weights = match fusion_strategy {
475 FusionStrategy::WeightedSum => Some(Parameter::new(ones(&[modalities.len()])?)),
476 _ => None,
477 };
478
479 let gating_network = match fusion_strategy {
480 FusionStrategy::GatedFusion => {
481 let mut gates = Vec::new();
482 for _ in 0..modalities.len() {
483 gates.push(Parameter::new(randn(&[feature_dim, 1])?));
484 }
485 Some(gates)
486 }
487 _ => None,
488 };
489
490 Ok(Self {
491 fusion_strategy,
492 modalities,
493 feature_dim,
494 fusion_weights,
495 gating_network,
496 })
497 }
498
499 pub fn fuse_features(&self, modality_features: &HashMap<Modality, Tensor>) -> Result<Tensor> {
501 match self.fusion_strategy {
502 FusionStrategy::Concatenation => self.concatenate_features(modality_features),
503 FusionStrategy::ElementwiseSum => self.elementwise_sum_features(modality_features),
504 FusionStrategy::WeightedSum => self.weighted_sum_features(modality_features),
505 FusionStrategy::AttentionFusion => self.attention_fusion_features(modality_features),
506 FusionStrategy::GatedFusion => self.gated_fusion_features(modality_features),
507 }
508 }
509
510 fn concatenate_features(
512 &self,
513 modality_features: &HashMap<Modality, Tensor>,
514 ) -> Result<Tensor> {
515 let mut concatenated_data = Vec::new();
516
517 for &modality in &self.modalities {
518 if let Some(features) = modality_features.get(&modality) {
519 concatenated_data.extend(features.to_vec()?);
520 } else {
521 concatenated_data.extend(vec![0.0f32; self.feature_dim]);
523 }
524 }
525
526 let num_nodes = modality_features
527 .values()
528 .next()
529 .map(|t| t.shape().dims()[0])
530 .unwrap_or(1);
531
532 Ok(from_vec(
533 concatenated_data,
534 &[num_nodes, self.modalities.len() * self.feature_dim],
535 torsh_core::device::DeviceType::Cpu,
536 )?)
537 }
538
539 fn elementwise_sum_features(
541 &self,
542 modality_features: &HashMap<Modality, Tensor>,
543 ) -> Result<Tensor> {
544 let mut sum_features: Option<Tensor> = None;
545
546 for &modality in &self.modalities {
547 if let Some(features) = modality_features.get(&modality) {
548 if let Some(ref sum) = sum_features {
549 sum_features = Some(sum.add(features)?);
550 } else {
551 sum_features = Some(features.clone());
552 }
553 }
554 }
555
556 match sum_features {
557 Some(features) => Ok(features),
558 None => Ok(zeros::<f32>(&[1, self.feature_dim])?),
559 }
560 }
561
562 fn weighted_sum_features(
564 &self,
565 modality_features: &HashMap<Modality, Tensor>,
566 ) -> Result<Tensor> {
567 let weights = self
568 .fusion_weights
569 .as_ref()
570 .ok_or_else(|| {
571 torsh_core::error::TorshError::InvalidArgument(
572 "weighted-sum fusion requires fusion weights".to_string(),
573 )
574 })?
575 .clone_data()
576 .to_vec()?;
577 let mut weighted_sum: Option<Tensor> = None;
578
579 for (i, &modality) in self.modalities.iter().enumerate() {
580 if let Some(features) = modality_features.get(&modality) {
581 let weight = weights.get(i).copied().unwrap_or(1.0);
582 let weighted_features = features.mul_scalar(weight)?;
583
584 if let Some(ref sum) = weighted_sum {
585 weighted_sum = Some(sum.add(&weighted_features)?);
586 } else {
587 weighted_sum = Some(weighted_features);
588 }
589 }
590 }
591
592 match weighted_sum {
593 Some(features) => Ok(features),
594 None => Ok(zeros::<f32>(&[1, self.feature_dim])?),
595 }
596 }
597
598 fn attention_fusion_features(
600 &self,
601 modality_features: &HashMap<Modality, Tensor>,
602 ) -> Result<Tensor> {
603 let available_features: Vec<&Tensor> = modality_features.values().collect();
605
606 if available_features.is_empty() {
607 return Ok(zeros::<f32>(&[1, self.feature_dim])?);
608 }
609
610 let mut attention_weights = Vec::new();
612 let mut total_norm = 0.0;
613
614 for features in &available_features {
615 let data = features.to_vec()?;
616 let norm: f32 = data.iter().map(|&x| x * x).sum::<f32>().sqrt();
617 attention_weights.push(norm);
618 total_norm += norm;
619 }
620
621 if total_norm > 0.0 {
623 for weight in &mut attention_weights {
624 *weight /= total_norm;
625 }
626 }
627
628 let mut attended_features: Option<Tensor> = None;
630 for (features, &weight) in available_features.iter().zip(attention_weights.iter()) {
631 let weighted = features.mul_scalar(weight)?;
632
633 if let Some(ref sum) = attended_features {
634 attended_features = Some(sum.add(&weighted)?);
635 } else {
636 attended_features = Some(weighted);
637 }
638 }
639
640 match attended_features {
641 Some(features) => Ok(features),
642 None => Ok(zeros::<f32>(&[1, self.feature_dim])?),
643 }
644 }
645
646 fn gated_fusion_features(
648 &self,
649 modality_features: &HashMap<Modality, Tensor>,
650 ) -> Result<Tensor> {
651 let gates = self.gating_network.as_ref().ok_or_else(|| {
652 torsh_core::error::TorshError::InvalidArgument(
653 "gated fusion requires a gating network".to_string(),
654 )
655 })?;
656 let mut gated_sum: Option<Tensor> = None;
657
658 for (i, &modality) in self.modalities.iter().enumerate() {
659 if let Some(features) = modality_features.get(&modality) {
660 let gate = &gates[i];
661 let gate_values = features.matmul(&gate.clone_data())?;
662 let gate_probs = self.sigmoid(&gate_values)?;
663
664 let gated_features = features.mul(&gate_probs)?;
666
667 if let Some(ref sum) = gated_sum {
668 gated_sum = Some(sum.add(&gated_features)?);
669 } else {
670 gated_sum = Some(gated_features);
671 }
672 }
673 }
674
675 match gated_sum {
676 Some(features) => Ok(features),
677 None => Ok(zeros::<f32>(&[1, self.feature_dim])?),
678 }
679 }
680
681 fn sigmoid(&self, x: &Tensor) -> Result<Tensor> {
683 let data = x.to_vec()?;
684 let sigmoid_data: Vec<f32> = data.iter().map(|&val| 1.0 / (1.0 + (-val).exp())).collect();
685
686 Ok(from_vec(
687 sigmoid_data,
688 x.shape().dims(),
689 torsh_core::device::DeviceType::Cpu,
690 )?)
691 }
692}
693
694#[derive(Debug)]
696pub struct MultiModalContrastiveLearning {
697 temperature: f32,
698 projection_dim: usize,
699 modality_projectors: HashMap<Modality, Parameter>,
700}
701
702impl MultiModalContrastiveLearning {
703 pub fn new(
705 modalities: Vec<Modality>,
706 modality_dims: HashMap<Modality, usize>,
707 projection_dim: usize,
708 temperature: f32,
709 ) -> Result<Self> {
710 let mut modality_projectors = HashMap::new();
711
712 for modality in modalities {
713 let input_dim = modality_dims.get(&modality).copied().unwrap_or(128);
714 modality_projectors.insert(
715 modality,
716 Parameter::new(randn(&[input_dim, projection_dim])?),
717 );
718 }
719
720 Ok(Self {
721 temperature,
722 projection_dim,
723 modality_projectors,
724 })
725 }
726
727 pub fn contrastive_loss(
729 &self,
730 modality1: Modality,
731 features1: &Tensor,
732 modality2: Modality,
733 features2: &Tensor,
734 ) -> Result<f32> {
735 let proj1 = features1.matmul(&self.modality_projectors[&modality1].clone_data())?;
737 let proj2 = features2.matmul(&self.modality_projectors[&modality2].clone_data())?;
738
739 let similarity = proj1.matmul(&proj2.t()?)?;
741 let scaled_similarity = similarity.div_scalar(self.temperature)?;
742
743 let sim_data = scaled_similarity.to_vec()?;
745 let max_sim = sim_data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
746 let exp_sims: Vec<f32> = sim_data.iter().map(|&x| (x - max_sim).exp()).collect();
747 let sum_exp: f32 = exp_sims.iter().sum();
748
749 let num_samples = proj1.shape().dims()[0];
751 let mut loss = 0.0;
752
753 for i in 0..num_samples {
754 let positive_sim = exp_sims[i * num_samples + i];
755 loss -= (positive_sim / sum_exp).ln();
756 }
757
758 Ok(loss / num_samples as f32)
759 }
760
761 pub fn generate_contrastive_pairs(
763 &self,
764 mm_graph: &MultiModalGraphData,
765 modality1: Modality,
766 modality2: Modality,
767 ) -> Vec<(Tensor, Tensor, bool)> {
768 let mut pairs = Vec::new();
769
770 let data1 = mm_graph.get_modality_data(modality1);
771 let data2 = mm_graph.get_modality_data(modality2);
772
773 for (node_id, features1) in &data1 {
775 if let Some((_, features2)) = data2.iter().find(|(id, _)| id == node_id) {
776 pairs.push(((*features1).clone(), (*features2).clone(), true));
777 }
778 }
779
780 for (node_id1, features1) in &data1 {
782 for (node_id2, features2) in &data2 {
783 if node_id1 != node_id2 {
784 pairs.push(((*features1).clone(), (*features2).clone(), false));
785
786 if pairs.len() > 1000 {
788 break;
789 }
790 }
791 }
792 if pairs.len() > 1000 {
793 break;
794 }
795 }
796
797 pairs
798 }
799}
800
801pub mod utils {
803 use super::*;
804
805 pub fn create_synthetic_multimodal_graph(
807 num_nodes: usize,
808 base_feature_dim: usize,
809 modalities: Vec<Modality>,
810 ) -> Result<MultiModalGraphData> {
811 let mut rng = scirs2_core::random::thread_rng();
812
813 let base_features = randn(&[num_nodes, base_feature_dim])?;
815 let mut edge_data = Vec::new();
816
817 for _ in 0..(num_nodes * 2) {
818 let src = rng.gen_range(0..num_nodes) as f32;
819 let dst = rng.gen_range(0..num_nodes) as f32;
820 edge_data.push(src);
821 edge_data.push(dst);
822 }
823
824 let edge_index = from_vec(
825 edge_data,
826 &[2, num_nodes * 2],
827 torsh_core::device::DeviceType::Cpu,
828 )?;
829
830 let graph = GraphData::new(base_features, edge_index);
831 let mut mm_graph = MultiModalGraphData::new(graph);
832
833 for node_id in 0..num_nodes {
835 let mut node_data = MultiModalNodeData::new(node_id);
836
837 for &modality in &modalities {
838 let feature_dim = match modality {
840 Modality::Text => 768, Modality::Image => 2048, Modality::Audio => 128, Modality::Tabular => 64, Modality::Graph => base_feature_dim,
845 Modality::Video => 1024, Modality::TimeSeries => 256, };
848
849 if rng.gen_range(0.0..1.0) < 0.8 {
851 let features = randn(&[feature_dim])?;
852 node_data = node_data.add_modality(modality, features);
853 }
854 }
855
856 mm_graph.add_node_data(node_data);
857 }
858
859 Ok(mm_graph)
860 }
861
862 pub fn evaluate_multimodal_quality(
864 mm_graph: &MultiModalGraphData,
865 representations: &HashMap<Modality, Tensor>,
866 ) -> Result<HashMap<String, f32>> {
867 let mut metrics = HashMap::new();
868
869 let modality_stats = mm_graph.modality_statistics();
871 for (modality, coverage) in modality_stats {
872 metrics.insert(format!("{:?}_coverage", modality), coverage);
873 }
874
875 for (modality, tensor) in representations {
877 let data = tensor.to_vec()?;
878 let mean: f32 = data.iter().sum::<f32>() / data.len() as f32;
879 let variance: f32 =
880 data.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / data.len() as f32;
881
882 metrics.insert(format!("{:?}_mean", modality), mean);
883 metrics.insert(format!("{:?}_variance", modality), variance);
884 }
885
886 if representations.len() > 1 {
888 let modalities: Vec<_> = representations.keys().collect();
889 for i in 0..modalities.len() {
890 for j in (i + 1)..modalities.len() {
891 let rep1 = &representations[modalities[i]];
892 let rep2 = &representations[modalities[j]];
893
894 let consistency = compute_tensor_similarity(rep1, rep2);
895 metrics.insert(
896 format!("{:?}_{:?}_consistency", modalities[i], modalities[j]),
897 consistency?,
898 );
899 }
900 }
901 }
902
903 Ok(metrics)
904 }
905
906 fn compute_tensor_similarity(tensor1: &Tensor, tensor2: &Tensor) -> Result<f32> {
908 let data1 = tensor1.to_vec()?;
909 let data2 = tensor2.to_vec()?;
910
911 if data1.len() != data2.len() {
912 return Ok(0.0);
913 }
914
915 let dot_product: f32 = data1.iter().zip(data2.iter()).map(|(&a, &b)| a * b).sum();
917 let norm1: f32 = data1.iter().map(|&x| x * x).sum::<f32>().sqrt();
918 let norm2: f32 = data2.iter().map(|&x| x * x).sum::<f32>().sqrt();
919
920 if norm1 > 0.0 && norm2 > 0.0 {
921 Ok(dot_product / (norm1 * norm2))
922 } else {
923 Ok(0.0)
924 }
925 }
926
927 pub fn generate_alignment_tasks(
929 mm_graph: &MultiModalGraphData,
930 source_modality: Modality,
931 target_modality: Modality,
932 num_tasks: usize,
933 ) -> Result<Vec<(usize, Tensor, Tensor)>> {
934 let source_data = mm_graph.get_modality_data(source_modality);
935 let target_data = mm_graph.get_modality_data(target_modality);
936
937 let mut tasks = Vec::new();
938 let mut rng = scirs2_core::random::thread_rng();
939
940 let common_nodes: Vec<usize> = source_data
942 .iter()
943 .filter_map(|&(node_id, _)| {
944 if target_data.iter().any(|&(id, _)| id == node_id) {
945 Some(node_id)
946 } else {
947 None
948 }
949 })
950 .collect();
951
952 for _ in 0..num_tasks.min(common_nodes.len()) {
953 let &node_id = match common_nodes.choose(&mut rng) {
954 Some(node_id) => node_id,
955 None => break,
956 };
957
958 let source_features = match source_data
959 .iter()
960 .find(|&&(id, _)| id == node_id)
961 .map(|(_, tensor)| (*tensor).clone())
962 {
963 Some(features) => features,
964 None => continue,
965 };
966
967 let target_features = match target_data
968 .iter()
969 .find(|&&(id, _)| id == node_id)
970 .map(|(_, tensor)| (*tensor).clone())
971 {
972 Some(features) => features,
973 None => continue,
974 };
975
976 tasks.push((node_id, source_features, target_features));
977 }
978
979 Ok(tasks)
980 }
981}
982
983trait RandomChoice<T> {
985 fn choose(
986 &self,
987 rng: &mut scirs2_core::random::CoreRandom<scirs2_core::rngs::ThreadRng>,
988 ) -> Option<&T>;
989}
990
991impl<T> RandomChoice<T> for Vec<T> {
992 fn choose(
993 &self,
994 rng: &mut scirs2_core::random::CoreRandom<scirs2_core::rngs::ThreadRng>,
995 ) -> Option<&T> {
996 if self.is_empty() {
997 None
998 } else {
999 let index = rng.gen_range(0..self.len());
1000 self.get(index)
1001 }
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use torsh_core::device::DeviceType;
1009
1010 #[test]
1011 fn test_multimodal_node_data_creation() {
1012 let text_features = randn(&[768]).unwrap();
1013 let image_features = randn(&[2048]).unwrap();
1014
1015 let node_data = MultiModalNodeData::new(0)
1016 .add_modality(Modality::Text, text_features)
1017 .add_modality(Modality::Image, image_features);
1018
1019 assert_eq!(node_data.node_id, 0);
1020 assert!(node_data.has_modality(Modality::Text));
1021 assert!(node_data.has_modality(Modality::Image));
1022 assert!(!node_data.has_modality(Modality::Audio));
1023 assert_eq!(node_data.available_modalities().len(), 2);
1024 }
1025
1026 #[test]
1027 fn test_multimodal_graph_data() {
1028 let features = randn(&[3, 4]).unwrap();
1029 let edges = vec![0.0, 1.0, 1.0, 2.0];
1030 let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
1031 let graph = GraphData::new(features, edge_index);
1032
1033 let mut mm_graph = MultiModalGraphData::new(graph);
1034
1035 for i in 0..3 {
1037 let node_data = MultiModalNodeData::new(i)
1038 .add_modality(Modality::Text, randn(&[768]).unwrap())
1039 .add_modality(Modality::Image, randn(&[2048]).unwrap());
1040 mm_graph.add_node_data(node_data);
1041 }
1042
1043 assert_eq!(mm_graph.available_modalities.len(), 2);
1044 assert_eq!(mm_graph.get_modality_data(Modality::Text).len(), 3);
1045 assert_eq!(
1046 mm_graph
1047 .get_complete_nodes(&[Modality::Text, Modality::Image])
1048 .len(),
1049 3
1050 );
1051
1052 let stats = mm_graph.modality_statistics();
1053 assert_eq!(stats[&Modality::Text], 1.0); assert_eq!(stats[&Modality::Image], 1.0); }
1056
1057 #[test]
1058 fn test_cross_modal_attention() {
1059 let modalities = vec![Modality::Text, Modality::Image];
1060 let mut modality_dims = HashMap::new();
1061 modality_dims.insert(Modality::Text, 768);
1062 modality_dims.insert(Modality::Image, 2048);
1063
1064 let attention = CrossModalGraphAttention::new(
1065 modalities,
1066 modality_dims,
1067 256, 128, 4, 0.1, )
1072 .expect("operation should succeed");
1073
1074 assert_eq!(attention.feature_dim, 256);
1075 assert_eq!(attention.attention_dim, 128);
1076 assert_eq!(attention.num_heads, 4);
1077 }
1078
1079 #[test]
1080 fn test_multimodal_fusion() {
1081 let modalities = vec![Modality::Text, Modality::Image];
1082 let fusion = MultiModalFusion::new(FusionStrategy::WeightedSum, modalities, 128)
1083 .expect("operation should succeed");
1084
1085 let mut modality_features = HashMap::new();
1086 modality_features.insert(Modality::Text, randn(&[3, 128]).unwrap());
1087 modality_features.insert(Modality::Image, randn(&[3, 128]).unwrap());
1088
1089 let fused = fusion.fuse_features(&modality_features);
1090 assert_eq!(
1091 fused.expect("operation should succeed").shape().dims(),
1092 &[3, 128]
1093 );
1094 }
1095
1096 #[test]
1097 fn test_contrastive_learning() {
1098 let modalities = vec![Modality::Text, Modality::Image];
1099 let mut modality_dims = HashMap::new();
1100 modality_dims.insert(Modality::Text, 768);
1101 modality_dims.insert(Modality::Image, 2048);
1102
1103 let contrastive = MultiModalContrastiveLearning::new(
1104 modalities,
1105 modality_dims,
1106 256, 0.07, )
1109 .expect("operation should succeed");
1110
1111 let text_features = randn(&[4, 768]).unwrap();
1112 let image_features = randn(&[4, 2048]).unwrap();
1113
1114 let loss = contrastive
1115 .contrastive_loss(
1116 Modality::Text,
1117 &text_features,
1118 Modality::Image,
1119 &image_features,
1120 )
1121 .expect("operation should succeed");
1122
1123 assert!(loss > 0.0);
1124 }
1125
1126 #[test]
1127 fn test_synthetic_multimodal_graph() {
1128 let modalities = vec![Modality::Text, Modality::Image, Modality::Audio];
1129 let mm_graph = utils::create_synthetic_multimodal_graph(5, 64, modalities)
1130 .expect("operation should succeed");
1131
1132 assert_eq!(mm_graph.graph.num_nodes, 5);
1133 assert!(mm_graph.available_modalities.len() <= 3);
1134
1135 assert!(!mm_graph.node_data.is_empty());
1137
1138 let stats = mm_graph.modality_statistics();
1139 for coverage in stats.values() {
1140 assert!(*coverage >= 0.0 && *coverage <= 1.0);
1141 }
1142 }
1143
1144 #[test]
1145 fn test_multimodal_quality_evaluation() {
1146 let modalities = vec![Modality::Text, Modality::Image];
1147 let mm_graph = utils::create_synthetic_multimodal_graph(4, 32, modalities)
1148 .expect("operation should succeed");
1149
1150 let mut representations = HashMap::new();
1151 representations.insert(Modality::Text, randn(&[4, 128]).unwrap());
1152 representations.insert(Modality::Image, randn(&[4, 128]).unwrap());
1153
1154 let metrics = utils::evaluate_multimodal_quality(&mm_graph, &representations)
1155 .expect("operation should succeed");
1156
1157 assert!(metrics.contains_key("Text_mean"));
1158 assert!(metrics.contains_key("Image_variance"));
1159
1160 let consistency_keys: Vec<_> = metrics
1162 .keys()
1163 .filter(|k| k.contains("consistency"))
1164 .collect();
1165 assert!(!consistency_keys.is_empty());
1166 }
1167
1168 #[test]
1169 fn test_alignment_task_generation() {
1170 let modalities = vec![Modality::Text, Modality::Image];
1171 let mm_graph = utils::create_synthetic_multimodal_graph(3, 32, modalities)
1172 .expect("operation should succeed");
1173
1174 let tasks = utils::generate_alignment_tasks(&mm_graph, Modality::Text, Modality::Image, 5)
1175 .expect("operation should succeed");
1176
1177 assert!(tasks.len() <= 5);
1179
1180 for (node_id, source, target) in &tasks {
1181 assert!(*node_id < 3);
1182 assert!(!source
1183 .to_vec()
1184 .expect("conversion should succeed")
1185 .is_empty());
1186 assert!(!target
1187 .to_vec()
1188 .expect("conversion should succeed")
1189 .is_empty());
1190 }
1191 }
1192}