Skip to main content

torsh_graph/conv/
gat.rs

1//! Graph Attention Network (GAT) layer implementation
2// Framework infrastructure - components designed for future use
3#![allow(dead_code)]
4/// Crate-local result alias: the error type defaults to [`TorshError`],
5/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
6type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
7
8use crate::parameter::Parameter;
9use crate::{GraphData, GraphLayer};
10use torsh_tensor::{
11    creation::{randn, zeros},
12    Tensor,
13};
14
15/// Graph Attention Network (GAT) layer
16#[derive(Debug)]
17pub struct GATConv {
18    in_features: usize,
19    out_features: usize,
20    heads: usize,
21    weight: Parameter,
22    attention: Parameter,
23    bias: Option<Parameter>,
24    dropout: f32,
25}
26
27impl GATConv {
28    /// Create a new GAT convolution layer
29    pub fn new(
30        in_features: usize,
31        out_features: usize,
32        heads: usize,
33        dropout: f32,
34        bias: bool,
35    ) -> Result<Self> {
36        let weight = Parameter::new(randn(&[in_features, heads * out_features])?);
37        let attention = Parameter::new(randn(&[heads, 2 * out_features])?);
38        let bias = if bias {
39            Some(Parameter::new(zeros(&[heads * out_features])?))
40        } else {
41            None
42        };
43
44        Ok(Self {
45            in_features,
46            out_features,
47            heads,
48            weight,
49            attention,
50            bias,
51            dropout,
52        })
53    }
54
55    /// Apply graph attention convolution
56    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
57        let num_nodes = graph.num_nodes;
58
59        // Transform node features: X @ W
60        let x_transformed = graph.x.matmul(&self.weight.clone_data())?;
61
62        // Reshape to separate heads: [num_nodes, heads, out_features]
63        let x_reshaped = x_transformed.view(&[
64            num_nodes as i32,
65            self.heads as i32,
66            self.out_features as i32,
67        ])?;
68
69        // Get edge indices - flatten and interpret as pairs
70        let edge_flat = graph.edge_index.to_vec()?;
71        let num_edges = graph.num_edges;
72
73        // Extract source and destination nodes (edge_index is [2, num_edges] stored row-major)
74        let src_nodes: Vec<usize> = (0..num_edges).map(|i| edge_flat[i] as usize).collect();
75        let dst_nodes: Vec<usize> = (0..num_edges)
76            .map(|i| edge_flat[i + num_edges] as usize)
77            .collect();
78
79        // Initialize output
80        let mut output = zeros(&[num_nodes, self.heads * self.out_features])?;
81
82        // Process each head independently
83        for head in 0..self.heads {
84            // Extract attention parameters for this head
85            let attention_head = self
86                .attention
87                .clone_data()
88                .slice_tensor(0, head, head + 1)?
89                .squeeze_tensor(0)?;
90
91            // Compute attention scores for all edges
92            let mut attention_scores = Vec::with_capacity(num_edges);
93
94            for edge_idx in 0..num_edges {
95                let src = src_nodes[edge_idx];
96                let dst = dst_nodes[edge_idx];
97
98                // Get source and destination node features for this head
99                let src_feat = x_reshaped
100                    .slice_tensor(0, src, src + 1)?
101                    .slice_tensor(1, head, head + 1)?
102                    .squeeze_tensor(0)?
103                    .squeeze_tensor(0)?;
104
105                let dst_feat = x_reshaped
106                    .slice_tensor(0, dst, dst + 1)?
107                    .slice_tensor(1, head, head + 1)?
108                    .squeeze_tensor(0)?
109                    .squeeze_tensor(0)?;
110
111                // Concatenate source and destination features
112                let concat_feat = Tensor::cat(&[&src_feat, &dst_feat], 0)?;
113
114                // Compute attention coefficient: a^T [h_i || h_j]
115                // Element-wise multiplication and sum to get scalar
116                let attention_coeff = attention_head.mul(&concat_feat)?.sum()?;
117
118                // Apply LeakyReLU activation
119                let coeff_val = attention_coeff.to_vec()?[0] as f64;
120                let activated_val = if coeff_val > 0.0 {
121                    coeff_val
122                } else {
123                    0.2 * coeff_val // LeakyReLU with alpha=0.2
124                };
125
126                attention_scores.push((src, dst, activated_val));
127            }
128
129            // Apply softmax normalization for each destination node
130            let mut normalized_scores = vec![0.0; num_edges];
131            for node in 0..num_nodes {
132                // Find edges pointing to this node
133                let mut node_edge_indices = Vec::new();
134                let mut node_scores = Vec::new();
135
136                for (edge_idx, (_, dst, score)) in attention_scores.iter().enumerate() {
137                    if *dst == node {
138                        node_edge_indices.push(edge_idx);
139                        node_scores.push(*score);
140                    }
141                }
142
143                if !node_scores.is_empty() {
144                    // Apply softmax
145                    let max_score = node_scores.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
146                    let exp_scores: Vec<f64> =
147                        node_scores.iter().map(|s| (*s - max_score).exp()).collect();
148                    let sum_exp: f64 = exp_scores.iter().sum();
149
150                    for (i, &edge_idx) in node_edge_indices.iter().enumerate() {
151                        normalized_scores[edge_idx] = exp_scores[i] / sum_exp;
152                    }
153                }
154            }
155
156            // Aggregate features using attention weights
157            let head_output = zeros(&[num_nodes, self.out_features])?;
158
159            for node in 0..num_nodes {
160                let mut node_output = zeros(&[self.out_features])?;
161
162                for (edge_idx, (src, dst, _)) in attention_scores.iter().enumerate() {
163                    if *dst == node {
164                        let weight = normalized_scores[edge_idx];
165                        if weight > 0.0 {
166                            let src_feat = x_reshaped
167                                .slice_tensor(0, *src, *src + 1)?
168                                .slice_tensor(1, head, head + 1)?
169                                .squeeze_tensor(0)?
170                                .squeeze_tensor(0)?;
171
172                            let weighted_feat = src_feat.mul_scalar(weight as f32)?;
173                            node_output = node_output.add(&weighted_feat)?;
174                        }
175                    }
176                }
177
178                // Set the aggregated features for this node
179                let mut node_slice = head_output.slice_tensor(0, node, node + 1)?;
180                let _ = node_slice.copy_(&node_output.unsqueeze_tensor(0)?);
181            }
182
183            // Place head output into the appropriate slice of the final output
184            let start_feat = head * self.out_features;
185            let end_feat = (head + 1) * self.out_features;
186            let mut output_slice = output.slice_tensor(1, start_feat, end_feat)?;
187            let _ = output_slice.copy_(&head_output);
188        }
189
190        // Add bias if present
191        if let Some(ref bias) = self.bias {
192            output = output.add(&bias.clone_data())?;
193        }
194
195        // Apply dropout if in training mode (placeholder for now)
196        if self.dropout > 0.0 {
197            // Note: For now, we'll skip dropout implementation to focus on core functionality
198            // In a complete implementation, this would apply dropout during training
199        }
200
201        Ok(GraphData {
202            x: output,
203            edge_index: graph.edge_index.clone(),
204            edge_attr: graph.edge_attr.clone(),
205            batch: graph.batch.clone(),
206            num_nodes: graph.num_nodes,
207            num_edges: graph.num_edges,
208        })
209    }
210}
211
212impl GraphLayer for GATConv {
213    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
214        self.forward(graph)
215    }
216
217    fn parameters(&self) -> Vec<Tensor> {
218        let mut params = vec![self.weight.clone_data(), self.attention.clone_data()];
219        if let Some(ref bias) = self.bias {
220            params.push(bias.clone_data());
221        }
222        params
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use torsh_core::device::DeviceType;
230    use torsh_tensor::creation::from_vec;
231
232    #[test]
233    fn test_gat_creation() {
234        let gat = GATConv::new(16, 8, 4, 0.1, true).expect("operation should succeed");
235        let params = gat.parameters();
236        assert_eq!(params.len(), 3); // weight + attention + bias
237        assert_eq!(gat.heads, 4);
238    }
239
240    #[test]
241    fn test_gat_forward() {
242        let gat = GATConv::new(3, 4, 2, 0.0, false);
243
244        // Create simple test graph
245        let x = from_vec(
246            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
247            &[3, 3],
248            DeviceType::Cpu,
249        )
250        .expect("operation should succeed");
251        let edge_index = from_vec(vec![0.0, 1.0, 2.0, 1.0, 2.0, 0.0], &[2, 3], DeviceType::Cpu)
252            .expect("from vec should succeed");
253        let graph = GraphData::new(x, edge_index);
254
255        let output = gat
256            .expect("operation should succeed")
257            .forward(&graph)
258            .expect("operation should succeed");
259        assert_eq!(output.x.shape().dims(), &[3, 8]); // 2 heads * 4 features
260        assert_eq!(output.num_nodes, 3);
261    }
262}