Skip to main content

torsh_graph/conv/
transformer.rs

1//! Graph Transformer Networks layer implementation
2/// Crate-local result alias: the error type defaults to [`TorshError`],
3/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
4type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
5
6use crate::parameter::Parameter;
7use crate::{GraphData, GraphLayer};
8use torsh_tensor::{
9    creation::{randn, zeros},
10    Tensor,
11};
12
13/// Graph Transformer Networks layer
14#[derive(Debug)]
15pub struct GraphTransformer {
16    in_features: usize,
17    out_features: usize,
18    heads: usize,
19    edge_dim: usize,
20    query_weight: Parameter,
21    key_weight: Parameter,
22    value_weight: Parameter,
23    edge_weight: Parameter,
24    output_weight: Parameter,
25    bias: Option<Parameter>,
26    dropout: f32,
27}
28
29impl GraphTransformer {
30    /// Create a new Graph Transformer layer
31    ///
32    /// # Errors
33    /// Returns [`torsh_core::error::TorshError::InvalidArgument`] when `heads`
34    /// is zero or does not divide `out_features` (the per-head dimension would
35    /// not be well defined), and propagates tensor-allocation failures.
36    pub fn new(
37        in_features: usize,
38        out_features: usize,
39        heads: usize,
40        edge_dim: usize,
41        dropout: f32,
42        bias: bool,
43    ) -> Result<Self> {
44        // Validate the head split once, so the forward path stays cheap.
45        if heads == 0 || out_features % heads != 0 {
46            return Err(torsh_core::error::TorshError::InvalidArgument(format!(
47                "out_features ({out_features}) must be a positive multiple of heads ({heads})"
48            )));
49        }
50
51        let query_weight = Parameter::new(randn(&[in_features, out_features])?);
52        let key_weight = Parameter::new(randn(&[in_features, out_features])?);
53        let value_weight = Parameter::new(randn(&[in_features, out_features])?);
54        let edge_weight = Parameter::new(randn(&[edge_dim, heads])?);
55        let output_weight = Parameter::new(randn(&[out_features, out_features])?);
56
57        let bias = if bias {
58            Some(Parameter::new(zeros(&[out_features])?))
59        } else {
60            None
61        };
62
63        Ok(Self {
64            in_features,
65            out_features,
66            heads,
67            edge_dim,
68            query_weight,
69            key_weight,
70            value_weight,
71            edge_weight,
72            output_weight,
73            bias,
74            dropout,
75        })
76    }
77
78    /// Get input feature dimension
79    pub fn in_features(&self) -> usize {
80        self.in_features
81    }
82
83    /// Get output feature dimension
84    pub fn out_features(&self) -> usize {
85        self.out_features
86    }
87
88    /// Get number of attention heads
89    pub fn heads(&self) -> usize {
90        self.heads
91    }
92
93    /// Get edge feature dimension
94    pub fn edge_dim(&self) -> usize {
95        self.edge_dim
96    }
97
98    /// Get dropout rate
99    pub fn dropout(&self) -> f32 {
100        self.dropout
101    }
102
103    /// Apply graph transformer convolution
104    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
105        let num_nodes = graph.num_nodes;
106        let head_dim = self.out_features / self.heads;
107
108        // Linear transformations for Q, K, V
109        let queries = graph.x.matmul(&self.query_weight.clone_data())?;
110        let keys = graph.x.matmul(&self.key_weight.clone_data())?;
111        let values = graph.x.matmul(&self.value_weight.clone_data())?;
112
113        // Reshape for multi-head attention
114        let q = queries.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
115        let k = keys.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
116        let v = values.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
117
118        // Initialize output
119        let mut output_features = zeros(&[num_nodes, self.out_features])?;
120
121        // For simplicity, use a basic attention mechanism
122        for head in 0..self.heads {
123            let head_dim_start = head * head_dim;
124            let head_dim_end = (head + 1) * head_dim;
125
126            let q_head = q.slice(1, head, head + 1)?;
127            let k_head = k.slice(1, head, head + 1)?;
128            let v_head = v.slice(1, head, head + 1)?;
129
130            // Basic self-attention computation
131            let scale = 1.0 / (head_dim as f64).sqrt();
132            let k_head_tensor = k_head.to_tensor()?.squeeze_tensor(1)?;
133            let q_head_tensor = q_head.to_tensor()?.squeeze_tensor(1)?;
134            let v_head_tensor = v_head.to_tensor()?.squeeze_tensor(1)?;
135
136            let k_transposed = k_head_tensor.transpose(0, 1)?;
137            let attention_scores = q_head_tensor
138                .matmul(&k_transposed)?
139                .mul_scalar(scale as f32)?;
140            let attention_weights = attention_scores.softmax(-1)?;
141            let head_output = attention_weights.matmul(&v_head_tensor)?;
142
143            // Copy to output
144            let output_slice = output_features.slice(1, head_dim_start, head_dim_end)?;
145            // head_output is already [num_nodes, head_dim] - no need to squeeze
146            let mut output_slice_tensor = output_slice.to_tensor()?;
147            output_slice_tensor.copy_(&head_output)?;
148        }
149
150        // Apply output projection
151        output_features = output_features.matmul(&self.output_weight.clone_data())?;
152
153        // Add bias if present
154        if let Some(ref bias) = self.bias {
155            output_features = output_features.add(&bias.clone_data())?;
156        }
157
158        Ok(GraphData {
159            x: output_features,
160            edge_index: graph.edge_index.clone(),
161            edge_attr: graph.edge_attr.clone(),
162            batch: graph.batch.clone(),
163            num_nodes: graph.num_nodes,
164            num_edges: graph.num_edges,
165        })
166    }
167}
168
169impl GraphLayer for GraphTransformer {
170    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
171        self.forward(graph)
172    }
173
174    fn parameters(&self) -> Vec<Tensor> {
175        let mut params = vec![
176            self.query_weight.clone_data(),
177            self.key_weight.clone_data(),
178            self.value_weight.clone_data(),
179            self.edge_weight.clone_data(),
180            self.output_weight.clone_data(),
181        ];
182        if let Some(ref bias) = self.bias {
183            params.push(bias.clone_data());
184        }
185        params
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use torsh_core::device::DeviceType;
193    use torsh_tensor::creation::from_vec;
194
195    #[test]
196    fn test_transformer_creation() {
197        let transformer =
198            GraphTransformer::new(16, 32, 8, 4, 0.1, true).expect("operation should succeed");
199        let params = transformer.parameters();
200        assert_eq!(params.len(), 6); // Q, K, V, edge, output weights + bias
201        assert_eq!(transformer.heads, 8);
202    }
203
204    #[test]
205    fn test_transformer_forward() {
206        let transformer = GraphTransformer::new(6, 12, 3, 2, 0.0, false);
207
208        // Create test graph
209        let x = from_vec(
210            vec![
211                1.0, 2.0, 3.0, 4.0, 5.0, 6.0, // node 0
212                7.0, 8.0, 9.0, 10.0, 11.0, 12.0, // node 1
213                13.0, 14.0, 15.0, 16.0, 17.0, 18.0, // node 2
214            ],
215            &[3, 6],
216            DeviceType::Cpu,
217        )
218        .expect("operation should succeed");
219        let edge_index = from_vec(vec![0.0, 1.0, 2.0, 1.0, 2.0, 0.0], &[2, 3], DeviceType::Cpu)
220            .expect("from vec should succeed");
221        let graph = GraphData::new(x, edge_index);
222
223        let output = transformer
224            .expect("operation should succeed")
225            .forward(&graph)
226            .expect("operation should succeed");
227        assert_eq!(output.x.shape().dims(), &[3, 12]);
228        assert_eq!(output.num_nodes, 3);
229    }
230}