#![allow(dead_code)]
use crate::{GraphData, GraphLayer};
use std::collections::HashMap;
use torsh_tensor::{
creation::{ones, zeros},
Tensor,
};
#[derive(Debug, Clone)]
pub struct GraphLRP {
pub alpha: f32,
pub beta: f32,
pub epsilon: f32,
pub activations: HashMap<String, Tensor>,
pub relevances: HashMap<String, Tensor>,
}
impl GraphLRP {
pub fn new() -> Self {
Self {
alpha: 1.0,
beta: 0.0,
epsilon: 1e-6,
activations: HashMap::new(),
relevances: HashMap::new(),
}
}
pub fn with_alpha_beta(alpha: f32, beta: f32) -> Self {
Self {
alpha,
beta,
epsilon: 1e-6,
activations: HashMap::new(),
relevances: HashMap::new(),
}
}
pub fn store_activation(&mut self, layer_name: String, activation: Tensor) {
self.activations.insert(layer_name, activation);
}
pub fn compute_relevance_epsilon(
&self,
input: &Tensor,
_output: &Tensor,
weight: &Tensor,
output_relevance: &Tensor,
) -> Result<Tensor, Box<dyn std::error::Error>> {
let z = input.matmul(weight)?;
let z_with_eps = self.add_epsilon_with_sign(&z)?;
let weighted_input = input.matmul(weight)?;
let relevance_factors = weighted_input.div(&z_with_eps)?;
let input_relevance = relevance_factors.mul(output_relevance)?;
Ok(input_relevance)
}
pub fn compute_relevance_alpha_beta(
&self,
input: &Tensor,
weight: &Tensor,
output_relevance: &Tensor,
) -> Result<Tensor, Box<dyn std::error::Error>> {
let weight_pos = self.relu_tensor(weight)?;
let weight_neg = weight.sub(&weight_pos)?;
let z_pos = input.matmul(&weight_pos)?;
let z_neg = input.matmul(&weight_neg)?;
let alpha_contrib = z_pos.mul_scalar(self.alpha)?;
let beta_contrib = z_neg.mul_scalar(self.beta)?;
let z_combined = alpha_contrib.sub(&beta_contrib)?;
let z_with_eps = self.add_epsilon_with_sign(&z_combined)?;
let relevance_factors = z_combined.div(&z_with_eps)?;
let input_relevance = relevance_factors.mul(output_relevance)?;
Ok(input_relevance)
}
pub fn compute_graph_relevance(
&self,
graph: &GraphData,
node_relevance: &Tensor,
layer_name: &str,
) -> Result<GraphRelevanceResult, Box<dyn std::error::Error>> {
let _num_nodes = graph.num_nodes;
let num_edges = graph.num_edges;
let mut edge_relevance = zeros(&[num_edges])?;
let node_relevance_propagated = node_relevance.clone();
let edge_data = graph.edge_index.to_vec()?;
for edge_idx in 0..num_edges {
let src_idx = edge_data[edge_idx] as usize;
let dst_idx = edge_data[edge_idx + num_edges] as usize;
let src_relevance = self.get_node_relevance(node_relevance, src_idx)?;
let dst_relevance = self.get_node_relevance(node_relevance, dst_idx)?;
let edge_rel = (src_relevance + dst_relevance) / 2.0;
edge_relevance = self.set_edge_relevance(edge_relevance, edge_idx, edge_rel)?;
}
Ok(GraphRelevanceResult {
node_relevance: node_relevance_propagated,
edge_relevance,
layer_name: layer_name.to_string(),
})
}
pub fn analyze_relevance_patterns(
&self,
_graph: &GraphData,
relevance_result: &GraphRelevanceResult,
) -> RelevanceAnalysis {
let node_stats = self.compute_node_relevance_stats(&relevance_result.node_relevance);
let edge_stats = self.compute_edge_relevance_stats(&relevance_result.edge_relevance);
let important_nodes = self.find_important_nodes(&relevance_result.node_relevance, 0.8);
let important_edges = self.find_important_edges(&relevance_result.edge_relevance, 0.8);
RelevanceAnalysis {
node_stats: node_stats.clone(),
edge_stats: edge_stats.clone(),
important_nodes,
important_edges,
total_relevance: node_stats.sum + edge_stats.sum,
}
}
fn add_epsilon_with_sign(&self, tensor: &Tensor) -> Result<Tensor, Box<dyn std::error::Error>> {
let sign_tensor = self.sign_tensor(tensor)?;
let epsilon_term = sign_tensor.mul_scalar(self.epsilon)?;
Ok(tensor.add(&epsilon_term)?)
}
fn relu_tensor(&self, tensor: &Tensor) -> Result<Tensor, Box<dyn std::error::Error>> {
let zeros_tensor = zeros(tensor.shape().dims())?;
Ok(tensor.maximum(&zeros_tensor)?)
}
fn sign_tensor(&self, tensor: &Tensor) -> Result<Tensor, Box<dyn std::error::Error>> {
let zeros_tensor = zeros(tensor.shape().dims())?;
let ones_tensor = ones(tensor.shape().dims())?;
let neg_ones = ones_tensor.mul_scalar(-1.0)?;
let positive_mask = tensor.gt(&zeros_tensor)?;
let negative_mask = tensor.lt(&zeros_tensor)?;
let pos_mask_data = positive_mask.to_vec()?;
let neg_mask_data = negative_mask.to_vec()?;
let pos_float: Vec<f32> = pos_mask_data
.iter()
.map(|&b| if b { 1.0 } else { 0.0 })
.collect();
let neg_float: Vec<f32> = neg_mask_data
.iter()
.map(|&b| if b { 1.0 } else { 0.0 })
.collect();
let pos_tensor =
torsh_tensor::creation::from_vec(pos_float, tensor.shape().dims(), tensor.device())?;
let neg_tensor =
torsh_tensor::creation::from_vec(neg_float, tensor.shape().dims(), tensor.device())?;
let mut result = zeros(tensor.shape().dims())?;
result = result.add(&pos_tensor.mul(&ones_tensor)?)?;
result = result.add(&neg_tensor.mul(&neg_ones)?)?;
Ok(result)
}
fn get_node_relevance(
&self,
relevance: &Tensor,
node_idx: usize,
) -> Result<f32, Box<dyn std::error::Error>> {
let node_rel = relevance.slice_tensor(0, node_idx, node_idx + 1)?;
let rel_vec = node_rel.to_vec()?;
Ok(rel_vec[0])
}
fn set_edge_relevance(
&self,
edge_relevance: Tensor,
_edge_idx: usize,
_value: f32,
) -> Result<Tensor, Box<dyn std::error::Error>> {
Ok(edge_relevance)
}
fn compute_node_relevance_stats(&self, _relevance: &Tensor) -> RelevanceStats {
RelevanceStats {
mean: 0.0,
std: 0.0,
min: 0.0,
max: 0.0,
sum: 0.0,
}
}
fn compute_edge_relevance_stats(&self, _relevance: &Tensor) -> RelevanceStats {
RelevanceStats {
mean: 0.0,
std: 0.0,
min: 0.0,
max: 0.0,
sum: 0.0,
}
}
fn find_important_nodes(&self, _relevance: &Tensor, _threshold: f32) -> Vec<usize> {
Vec::new()
}
fn find_important_edges(&self, _relevance: &Tensor, _threshold: f32) -> Vec<usize> {
Vec::new()
}
}
impl Default for GraphLRP {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct GraphRelevanceResult {
pub node_relevance: Tensor,
pub edge_relevance: Tensor,
pub layer_name: String,
}
#[derive(Debug, Clone)]
pub struct RelevanceStats {
pub mean: f32,
pub std: f32,
pub min: f32,
pub max: f32,
pub sum: f32,
}
#[derive(Debug, Clone)]
pub struct RelevanceAnalysis {
pub node_stats: RelevanceStats,
pub edge_stats: RelevanceStats,
pub important_nodes: Vec<usize>,
pub important_edges: Vec<usize>,
pub total_relevance: f32,
}
#[derive(Debug, Clone)]
pub struct GraphGradientAttribution {
pub gradients: HashMap<String, Tensor>,
pub smooth_steps: usize,
}
impl GraphGradientAttribution {
pub fn new() -> Self {
Self {
gradients: HashMap::new(),
smooth_steps: 50,
}
}
pub fn integrated_gradients(
&self,
graph: &GraphData,
_baseline_graph: &GraphData,
_target_class: usize,
) -> Result<GraphData, Box<dyn std::error::Error>> {
let integrated_features = graph.x.clone();
let integrated_edges = graph.edge_index.clone();
Ok(GraphData::new(integrated_features, integrated_edges))
}
pub fn gradient_saliency(
&self,
graph: &GraphData,
_target_output: &Tensor,
) -> Result<Tensor, Box<dyn std::error::Error>> {
Ok(zeros(&[graph.num_nodes])?)
}
}
impl Default for GraphGradientAttribution {
fn default() -> Self {
Self::new()
}
}
pub struct GraphExplainer {
pub lrp: GraphLRP,
pub gradient_attribution: GraphGradientAttribution,
}
impl GraphExplainer {
pub fn new() -> Self {
Self {
lrp: GraphLRP::new(),
gradient_attribution: GraphGradientAttribution::new(),
}
}
pub fn explain_prediction(
&mut self,
graph: &GraphData,
model_layers: &[Box<dyn GraphLayer>],
target_class: usize,
) -> Result<GraphExplanation, Box<dyn std::error::Error>> {
let mut current_graph = graph.clone();
for (i, layer) in model_layers.iter().enumerate() {
current_graph = layer.forward(¤t_graph);
self.lrp
.store_activation(format!("layer_{}", i), current_graph.x.clone());
}
let final_relevance = ones(&[graph.num_nodes])?; let lrp_result = self
.lrp
.compute_graph_relevance(graph, &final_relevance, "final")?;
let gradient_saliency = self
.gradient_attribution
.gradient_saliency(graph, &final_relevance)?;
let relevance_analysis = self.lrp.analyze_relevance_patterns(graph, &lrp_result);
Ok(GraphExplanation {
lrp_result,
gradient_saliency,
relevance_analysis,
target_class,
})
}
}
impl Default for GraphExplainer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct GraphExplanation {
pub lrp_result: GraphRelevanceResult,
pub gradient_saliency: Tensor,
pub relevance_analysis: RelevanceAnalysis,
pub target_class: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use torsh_core::device::DeviceType;
use torsh_tensor::creation::{from_vec, randn};
#[test]
fn test_graph_lrp_creation() {
let lrp = GraphLRP::new();
assert_eq!(lrp.alpha, 1.0);
assert_eq!(lrp.beta, 0.0);
assert_eq!(lrp.epsilon, 1e-6);
}
#[test]
fn test_graph_lrp_alpha_beta() {
let lrp = GraphLRP::with_alpha_beta(2.0, -1.0);
assert_eq!(lrp.alpha, 2.0);
assert_eq!(lrp.beta, -1.0);
}
#[test]
fn test_graph_explainer_creation() {
let explainer = GraphExplainer::new();
assert_eq!(explainer.lrp.alpha, 1.0);
assert_eq!(explainer.gradient_attribution.smooth_steps, 50);
}
#[test]
fn test_relevance_epsilon_computation() {
let lrp = GraphLRP::new();
let input = randn(&[3, 4]).unwrap();
let weight = randn(&[4, 2]).unwrap();
let output = input.matmul(&weight).unwrap();
let output_relevance = ones(&[3, 2]).unwrap();
let _result = lrp.compute_relevance_epsilon(&input, &output, &weight, &output_relevance);
}
#[test]
fn test_graph_relevance_structure() {
let lrp = GraphLRP::new();
let x = randn(&[4, 3]).unwrap();
let edge_index = from_vec(
vec![0.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0],
&[2, 4],
DeviceType::Cpu,
)
.unwrap();
let graph = GraphData::new(x, edge_index);
let node_relevance = ones(&[4]).unwrap();
let _result = lrp.compute_graph_relevance(&graph, &node_relevance, "test_layer");
}
}