Code Architecture — Graph Attention Networks (GAT)
Notebook Overview
The notebook implements the Graph Attention Network (GAT) from Veličković et al. (2018) entirely from scratch using NumPy, then validates it on Zachary’s Karate Club graph for semi-supervised node classification:
- Graph construction & preprocessing — build the adjacency matrix, identify neighborhoods (including self-loops), prepare node features
- GAT attention layer from scratch — implement the attention coefficient computation, masked softmax, and weighted neighbor aggregation
- Multi-head attention — run K independent attention heads in parallel, concatenate (hidden) or average (output)
- Two-layer GAT training — train end-to-end with cross-entropy loss on a few labeled nodes using gradient descent and manual backprop
- Evaluation & visualization — classify all nodes, plot the graph with predicted vs true labels, and visualize which neighbors receive the highest attention weights
Section-by-Section Breakdown
1. Setup & Imports
- Installs
networkxandscikit-learn(pip-installable, Colab/Kaggle compatible) - Imports NumPy, NetworkX, scikit-learn (t-SNE), Matplotlib
- Sets random seeds for reproducibility
2. Graph Construction
- Loads Zachary’s Karate Club graph from NetworkX (34 nodes, 78 edges, 2 communities)
- Extracts the adjacency matrix A (34×34), node features (identity matrix for one-hot node IDs), and ground-truth labels (2 communities)
- Builds the neighbor list (including self-loops): for each node, its neighbors + itself
- Deliberate simplification: The paper uses Cora (2,708 nodes, 7 classes), Citeseer, Pubmed, and PPI. We use the Karate Club graph so training completes in seconds and visualizations are interpretable. The math is identical — only the graph size differs.
3. GAT Attention Layer (from scratch)
gat_layer(X, W, a, neighbors, n_heads, activation, concat):- Step 1 — Linear transform: Project all node features:
H = X @ W(shape: N × F’) - Step 2 — Attention coefficients: For each node i and neighbor j, compute:
e_ij = LeakyReLU(a · [W·h_i || W·h_j])Implemented efficiently as: computea_src · H_i + a_dst · H_j(splitting the attention vectorainto source and destination parts), apply LeakyReLU(0.2) - Step 3 — Masked softmax: For each node i, softmax over only its neighbors’ e_ij values (non-neighbors get -inf → 0 weight). This is the “masked” self-attention.
- Step 4 — Aggregation:
h'_i = σ(Σ_j α_ij · W · h_j)— weighted sum of transformed neighbor features, using attention weights α_ij
- Step 1 — Linear transform: Project all node features:
- Self-loops: Each node is included in its own neighbor list, so it attends to its own features alongside neighbors — no explicit A + I needed
- Multi-head attention: K independent sets of (W, a) run in parallel. Hidden layers concatenate outputs (shape: N × K*F’). Output layer averages (shape: N × F’).
4. Two-Layer GAT Architecture
- Layer 1: K=4 heads, F’=8 features per head, ELU activation, output concatenated → (34 × 32)
- Layer 2: K=1 head, F’=2 output classes, softmax activation, output averaged → (34 × 2)
- Input X: (34 × 34) identity matrix
- Deliberate simplification: The paper uses K=8 heads and F’=8 in layer 1. We reduce to K=4 for faster training on the tiny Karate graph.
5. Loss & Backpropagation
cross_entropy(y_pred, y_true): Standard cross-entropy on labeled nodes only (semi-supervised)- Manual gradient computation through attention, softmax, and linear layers:
- Output layer gradient:
dL/dZ2 = (y_pred - y_onehot) / n_labeled(softmax + CE) - Backprop through attention aggregation:
dL/dH_agg = α · dL/dZ2(attention-weighted) - Backprop through linear transform:
dL/dW = H^T · dL/dH_agg - Backprop through attention coefficients: requires differentiating through softmax and LeakyReLU
- Hidden layer gradient through ELU activation
- Output layer gradient:
- Only labeled nodes contribute to the loss gradient, but all nodes participate in forward and gradient propagation
- Deliberate simplification: The paper uses Adam optimizer. We use vanilla gradient descent with a fixed learning rate for transparency and educational clarity.
6. Training Loop
- Semi-supervised setup: Label only a few nodes per class (e.g., 3 per community) — the “training mask”
- Gradient descent with learning rate 0.05, 300 epochs
- Track training accuracy and loss per epoch
- L2 weight decay applied to weight matrices W (λ = 5e-4)
- Deliberate simplification: The paper uses Adam, 200-700 epochs, and sparse dropout on features and attention. We use vanilla GD, no dropout (toy dataset converges without it).
7. Evaluation & Visualization
- Node classification accuracy: On all nodes, compare predicted class (argmax of output) with ground truth
- Graph visualization: Draw the karate club graph with node colors showing:
- Ground-truth labels
- Predicted labels
- Side-by-side comparison highlighting misclassifications
- Attention weight visualization (key deliverable): For selected nodes, draw the graph with edge widths proportional to attention weights — showing which neighbors the model attends to most. This directly addresses the paper’s key feature: interpretable, learned neighbor importance.
- Embedding visualization: Extract the 32-dim hidden layer representations, project to 2D with t-SNE, color by community — shows that GAT learns separable embeddings even with few labels
- Multi-head attention comparison: Show attention patterns from different heads — demonstrating that different heads learn different attention patterns (some focus on local structure, others on specific features)
Data Flow / Shapes
X (34×34) → GAT Layer 1 (4 heads) → H1 (34×32) → GAT Layer 2 (1 head) → Z (34×2) → argmax → predictions (34,)
↓ ↓ ↓
features [head_k: α(34×34) @ (X@W_k)(34×8)] α(34×34) @ (H1@W)(32→2) softmax
× 4 heads, concat × 1 head, average
- Input X: (N × F) = (34 × 34) — identity matrix (one-hot node features)
- Layer 1 per head: H_k = α_k @ (X @ W_k), W_k: (34×8), α_k: (34×34) attention matrix → (34×8)
- Layer 1 output: concat(H_1…H_4) → (34 × 32)
- Layer 2: Z = α @ (H1 @ W_out), W_out: (32×2) → (34×2)
- Attention matrices α_k: (34×34) — dense but effectively masked (non-neighbors = 0)
Deliberate Simplifications vs Full Paper
| Aspect | Paper | This Notebook |
|---|---|---|
| Dataset | Cora (2708 nodes, 7 classes) | Karate Club (34 nodes, 2 classes) |
| Features | 1433-dim bag-of-words | Identity matrix (one-hot node IDs) |
| Attention heads (layer 1) | K=8 | K=4 |
| Hidden features per head | F’=8 | F’=8 |
| Optimizer | Adam (lr=0.01) | Vanilla gradient descent (lr=0.05) |
| Epochs | 200-700 | 300 |
| Weight init | Glorot | Scaled random normal |
| Dropout | Sparse dropout on features + attention | Not applied (toy dataset) |
| L2 regularization | λ=5×10⁻⁴ | λ=5×10⁻⁴ (simplified weight decay) |
| Inductive evaluation | PPI (unseen test graphs) | Transductive only (Karate Club) |
| Implementation | TensorFlow | Pure NumPy (from scratch) |