use candle_core::{Result, Tensor};
use candle_nn::VarBuilder;
pub struct SparseEdgeBatch {
pub dst_flat: Tensor,
pub src_flat: Tensor,
pub weight: Tensor,
}
pub struct GcnBlock {
d_model: usize,
gamma: Tensor,
}
impl GcnBlock {
pub fn new(d_model: usize, vb: VarBuilder) -> Result<Self> {
let gamma = vb.get_with_hints((d_model,), "gamma", candle_nn::init::ZERO)?;
Ok(Self { d_model, gamma })
}
pub fn d_model(&self) -> usize {
self.d_model
}
pub fn gamma_vec(&self) -> Result<Vec<f32>> {
self.gamma.to_vec1::<f32>()
}
pub fn forward(&self, v: &Tensor, edges: &SparseEdgeBatch) -> Result<Tensor> {
let (n, k, h) = v.dims3()?;
debug_assert_eq!(h, self.d_model);
let v_flat = v.reshape((n * k, h))?;
let v_at_src = v_flat.index_select(&edges.src_flat, 0)?; let v_weighted = v_at_src.broadcast_mul(&edges.weight.unsqueeze(1)?)?; let smoothed_flat = Tensor::zeros((n * k, h), v.dtype(), v.device())?.index_add(
&edges.dst_flat,
&v_weighted,
0,
)?;
let smoothed = smoothed_flat.reshape((n, k, h))?;
let gamma = self.gamma.reshape((1, 1, h))?;
let delta = smoothed.broadcast_mul(&gamma)?;
v + delta
}
}
#[cfg(test)]
mod tests {
use super::*;
use candle_core::{DType, Device};
use candle_nn::VarMap;
fn small_edges(device: &Device) -> SparseEdgeBatch {
let dst =
Tensor::from_vec(vec![0u32, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4], (11,), device).unwrap();
let src =
Tensor::from_vec(vec![0u32, 1, 0, 1, 2, 1, 2, 3, 4, 3, 4], (11,), device).unwrap();
let w = Tensor::from_vec(
vec![
0.5f32,
0.5,
1.0 / 3.0,
1.0 / 3.0,
1.0 / 3.0,
0.5,
0.5,
0.5,
0.5,
0.5,
0.5,
],
(11,),
device,
)
.unwrap();
SparseEdgeBatch {
dst_flat: dst,
src_flat: src,
weight: w,
}
}
#[test]
fn gcn_block_forward_shape_and_finite() {
let device = Device::Cpu;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, DType::F32, &device);
let d = 8usize;
let block = GcnBlock::new(d, vb.pp("gcn")).unwrap();
let n = 2usize;
let k = 3usize;
let v = Tensor::randn(0.0f32, 1.0, (n, k, d), &device).unwrap();
let edges = small_edges(&device);
let out = block.forward(&v, &edges).unwrap();
assert_eq!(out.dims(), &[n, k, d]);
for row in out.flatten_all().unwrap().to_vec1::<f32>().unwrap() {
assert!(row.is_finite(), "non-finite output {row}");
}
}
#[test]
fn gcn_identity_at_init() {
let device = Device::Cpu;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, DType::F32, &device);
let d = 4usize;
let block = GcnBlock::new(d, vb.pp("gcn")).unwrap();
let v = Tensor::randn(0.0f32, 1.0, (2, 3, d), &device).unwrap();
let edges = small_edges(&device);
let out = block.forward(&v, &edges).unwrap();
let v_flat = v.flatten_all().unwrap().to_vec1::<f32>().unwrap();
let out_flat = out.flatten_all().unwrap().to_vec1::<f32>().unwrap();
for (a, b) in v_flat.iter().zip(out_flat.iter()) {
assert!((a - b).abs() < 1e-6, "γ=0 should give identity: {a} vs {b}");
}
}
}