Velickovic, Cucurull, Casanova, Romero, Lio & Bengio — ICLR 2018 • arXiv:1710.10903

Graph Attention Networks

What if, instead of averaging all neighbors equally, the graph network could learn which neighbors to pay attention to — and how much? This paper answers that question by injecting self-attention into message passing.

Prerequisites: GNN basics (message passing, GCN) + Softmax + Basic neural networks. We derive the attention mechanism from scratch.
10
Chapters
2
Simulations
10
Quizzes

Chapter 0: The Problem with Uniform Weights

You're a paper in a citation network. Your neighbors are three other papers: one from Nature, one from a student blog, one from a related workshop. When computing your embedding, GCN weights all three equally — each contributes 1/3 to the average. But the Nature paper and the workshop paper are probably much more relevant to your topic than the blog post.

This is the problem GAT was designed to solve. GCN uses a fixed weighting function: 1 divided by the geometric mean of degrees. This is a heuristic based on graph structure alone. It ignores the content of the nodes. Two papers about quantum computing should probably pay more attention to each other than to a paper about cooking — but GCN can't express this preference.

The core question GAT asks: Can we replace the fixed, hand-crafted aggregation weight (1/√degu · degv) with a learned weight that depends on the actual content of the nodes? The answer is yes — and the mechanism is attention.

What GCN Does (The Baseline)

Recall GCN: for each node v, aggregate all neighbor embeddings with uniform weights, transform, and activate:

hv(l) = σ( ∑u ∈ N(v) ∪ {v} 1|N(u)||N(v)| · W hu(l-1) )

The weight 1/√(|N(u)||N(v)|) is fixed, computable from graph structure alone, requires no learning. This is a strength (no parameters) and a weakness (ignores content).

What GAT Does (The Fix)

Replace the fixed weight with a learned attention coefficient αvu:

hv(l) = σ( ∑u ∈ N(v) αvu · W hu(l-1) )

The key question is: how do we compute αvu? It should: (1) depend on the features of both v and u, (2) be differentiable (so we can learn it with gradient descent), (3) be normalized across neighbors (so attention weights sum to 1). The next three chapters build this up piece by piece.

A Concrete Failure Mode of GCN

Consider a paper v about machine learning. Its neighbors in a citation graph are: 5 papers about ML (highly relevant), and 15 papers about something tangentially related (irrelevant but historically cited). GCN computes:

hvnew = σ(W · (1/20) ∑u ∈ N(v) hu)

The 15 irrelevant papers contribute 75% of the signal. The 5 relevant papers contribute only 25%. If the irrelevant papers have features that average to something far from the ML centroid, they drag v's embedding away from its true class. This is why GCN underperforms on heterophilic graphs.

GAT computes (approximately, if it learns well):

hvnew = σ(W · (0.8 · meanrelevant + 0.2 · meanirrelevant))

By up-weighting the 5 relevant neighbors and down-weighting the 15 irrelevant ones, v's embedding stays close to its true class representation. The attention weights are not specified by the programmer — they are learned by gradient descent on the node classification loss.

Uniform vs. Learned Weights

A central node v with 4 neighbors, each with a different feature value. Uniform weighting (GCN) treats all neighbors equally. Attention weighting (GAT) focuses on similar neighbors. Drag the slider to change v's feature and watch how attention weights shift.

Node v feature 0.7
GAT focuses attention on neighbors with similar features. GCN treats all equally.
What is the fundamental limitation of GCN's aggregation that GAT addresses?

Chapter 1: Attention Coefficients

How do we measure how "compatible" two nodes v and u are? We need a function that takes both nodes' features and outputs a scalar score. Velickovic et al. choose a specific form: first project both into a shared space, concatenate, then apply a small learned linear layer.

Step 1: Project with a Shared Weight Matrix

Both hv and hu are projected by the same weight matrix W ∈ Rd' × d:

W hv ∈ Rd',    W hu ∈ Rd'

The shared matrix W serves dual purpose: (1) it transforms embeddings into a "comparison space" where compatibility makes sense, and (2) it's the same W used for the final weighted aggregation. This means attention and aggregation are jointly optimized through the same projection — an efficient coupling that forces the projection to be simultaneously good for comparison and for embedding computation.

Step 2: Concatenate and Score

Concatenate the two projected vectors and score them with a learnable vector a ∈ R2d':

evu = LeakyReLU( aT · [W hv ‖ W hu] )

The symbol ∥ means concatenation. So [W hv ∥ W hu] ∈ R2d' is the concatenated pair. aT is the transpose of the attention vector — a single linear layer with no bias that maps R2d' → R1. The output evu is a raw, unnormalized score: how much node v should pay attention to node u.

Why concatenate rather than dot product? Dot product attention (as in Transformers) computes qTk — the inner product of query and key vectors. This is symmetric: sim(v, u) = sim(u, v). GAT's additive attention (LeakyReLU(aT[Whv ∥ Whu])) is asymmetric: avu ≠ auv in general. For directed graphs, this asymmetry is crucial. For undirected graphs, the paper adds self-loops and treats the graph as directed — asymmetry still emerges because softmax normalizes over different neighborhood sizes.

Why LeakyReLU?

LeakyReLU(x) = x if x > 0, else 0.01x. Why not ReLU? ReLU kills all negative inputs — any pair of nodes where the raw compatibility is negative would get score 0, meaning "no information" rather than "low compatibility." LeakyReLU preserves the ordering even for negative scores (very incompatible pairs get a small negative score, which after softmax becomes a small positive weight — still distinguishable from zero). In practice, the slope 0.01 is a hyperparameter; the paper uses 0.2.

python
import torch
import torch.nn as nn

class GATAttention(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        self.W = nn.Linear(d_in, d_out, bias=False)  # shared projection
        self.a = nn.Linear(2 * d_out, 1, bias=False)  # attention vector
        self.leaky = nn.LeakyReLU(negative_slope=0.2)

    def raw_score(self, h_v, h_u):
        # h_v, h_u: [d_in]
        Wh_v = self.W(h_v)            # [d_out]
        Wh_u = self.W(h_u)            # [d_out]
        concat = torch.cat([Wh_v, Wh_u])  # [2*d_out]
        e = self.leaky(self.a(concat))    # scalar
        return e

Computational Graph of One GAT Layer

Let's trace data flow precisely. Input: N nodes, each with d-dimensional embedding. One GAT layer with output dimension d', K=1 head:

Input H
Shape [N, d] — node embeddings from previous layer (or raw features at layer 0)
↓ W ∈ Rd × d' (shared linear projection)
Wh for all nodes
Shape [N, d']. Broadcasted: for each edge (v,u), we need Wh_v and Wh_u.
↓ Index by edge list, concatenate
Concat [Wh_v ∥ Wh_u] per edge
Shape [|E|, 2d']. Each row = one edge's feature pair.
a ∈ R2d', then LeakyReLU
Raw scores e
Shape [|E|]. One scalar per edge — the raw attention coefficient.
↓ Softmax per source node
Normalized weights α
Shape [|E|]. Still one per edge, but now ∑u∈N(v) αvu = 1 for each v.
↓ Weighted sum per destination node
Output H'
Shape [N, d']. Each row = σ(∑u∈N(v) αvu Whu). The new embedding for node v.

Total parameter count for one layer with K=1: W has d×d' parameters, a has 2d' parameters. Total: d×d' + 2d'. Compare to GCN: d×d' parameters. GAT adds only 2d' extra — the attention vector. For d=d'=64: GCN has 4096 params, GAT has 4224. The attention mechanism costs only 128 extra numbers.

In GAT's attention coefficient e_vu = LeakyReLU(aT[Wh_v ∥ Wh_u]), why is the same weight matrix W used for BOTH nodes v and u?

Chapter 2: Softmax Normalization

The raw attention scores evu are on an arbitrary scale. Node v might have 2 neighbors and node w might have 20. Comparing "the maximum e for v" vs "the maximum e for w" is meaningless if the scales differ. We need to normalize the scores into a proper probability distribution over each node's neighborhood.

The Normalization

Softmax normalizes the scores for node v across ALL its neighbors:

αvu = softmaxu(evu) = exp(evu)k ∈ N(v) exp(evk)

This guarantees: (1) αvu ≥ 0 for all u, (2) ∑u ∈ N(v) αvu = 1. Now αvu is interpretable as "the fraction of attention node v allocates to neighbor u." The neighbor with the highest raw score gets the largest α weight, but ALL neighbors get a positive (if tiny) weight.

Softmax is computed independently per node. Node v's softmax is over {evk : k ∈ N(v)} — only its neighbors. Node w's softmax is over its own neighborhood. The normalization is LOCAL, not global. This is important: a very active node (many neighbors) does not dilute attention away from a quiet node.

Numerical Stability

Computing exp(e) can overflow for large e values. Standard trick: subtract the max before exponentiating.

αvu = exp(evu - maxk evk) / ∑j exp(evj - maxk evk)

Mathematically identical (the max cancels), but numerically stable. PyTorch's F.softmax handles this automatically.

A Concrete Example

Say node v has 3 neighbors A, B, C with raw scores evA = 2.1, evB = 0.3, evC = -0.8.

NeighborRaw score eexp(e)α (normalized)
A2.18.1660.723
B0.31.3500.119
C-0.80.4490.040
self (self-loop)1.23.3200.118
Sum13.2851.000

Node v allocates 72.3% of its attention to A (most similar), 11.9% to B, 11.8% to its own embedding (self-loop), and only 4.0% to C (least similar). The final embedding hv = σ(0.723 W hA + 0.119 W hB + 0.040 W hC + 0.118 W hv).

Implementation: Efficient Softmax over Neighborhoods

Naively, computing softmax per node requires a loop over nodes. In PyTorch, the efficient way uses scatter operations — aggregate by source node index:

python
import torch
import torch.nn.functional as F
from torch_scatter import scatter_softmax

def gat_softmax(e, src_idx):
    # e: [|E|] raw attention scores (one per edge)
    # src_idx: [|E|] which node is the SOURCE (v) for each edge
    # Returns alpha: [|E|] normalized per-node attention weights

    # Naive approach (loop) — DO NOT USE in practice
    alpha = torch.zeros_like(e)
    for v in src_idx.unique():
        mask = (src_idx == v)
        alpha[mask] = F.softmax(e[mask], dim=0)
    return alpha

    # Efficient approach using scatter_softmax — groups by src_idx in parallel
    return scatter_softmax(e, src_idx, dim=0)

# Example: 5 edges from node 0, 3 edges from node 1
e = torch.tensor([2.1, 0.3, -0.8, 1.2, 0.5,   # node 0's 5 edges
                  1.8, 0.1, 2.3])              # node 1's 3 edges
src = torch.tensor([0, 0, 0, 0, 0, 1, 1, 1])
alpha = scatter_softmax(e, src, dim=0)
# alpha[0:5] sums to 1 (node 0's distribution)
# alpha[5:8] sums to 1 (node 1's distribution)
print(alpha[:5].sum())  # tensor(1.0)
print(alpha[5:].sum())  # tensor(1.0)
Why is the softmax in GAT computed locally per node (over each node's neighbors separately) rather than globally across all edges?

Chapter 3: Multi-Head Attention (Showcase)

A single attention head asks one question: "which neighbors are most compatible with me?" But compatibility is multidimensional. In a molecular graph, atom v might attend to neighbors based on bond strength (head 1) or electronegativity similarity (head 2) or spatial proximity (head 3). A single head can only capture one criterion at a time.

Multi-head attention runs K independent attention mechanisms in parallel, then concatenates (or averages) the results. Each head has its own weight matrix Wk and attention vector ak. They are trained jointly but initialized independently, so they specialize to different aspects of compatibility.

The Multi-Head Formula

For intermediate layers, concatenate K head outputs:

hv(l) = ‖k=1K σ( ∑u ∈ N(v) αvuk · Wk hu(l-1) )

For the final (prediction) layer, average K head outputs (to avoid K-fold dimension expansion):

hvfinal = σ( 1Kk=1Ku ∈ N(v) αvuk · Wk hu(l-1) )
The GAT paper uses K=8 heads for all hidden layers, K=1 with averaging for the output layer. Each hidden layer maps d→d/K per head, then concatenates K heads back to d. So the total hidden dimension stays constant across layers. The output layer averages K heads instead of concatenating, so the final embedding is d-dimensional regardless of K.
Multi-Head GAT Visualizer (Showcase)

A 7-node graph. Each attention head has independently learned to emphasize different neighbors. Switch between heads to see how different heads focus on different neighborhoods. Edge thickness = α weight for the selected head.

Focus node 0
Head 1 active. Slide to select focus node. Different heads prioritize different neighborhood aspects.

Why K=8?

This is an empirical finding. The paper reports ablations: K=1 gives 81.8% on Cora, K=8 gives 83.0%. The improvement is consistent but not dramatic. More heads give more capacity, but also more parameters and more training instability. The paper found K=8 to be a good tradeoff for citation networks. For PPI (protein-protein interaction, inductive), K=4 heads for intermediate layers, K=6 for final.

Dimensionality Accounting

Let's be precise about shapes for a 2-layer GAT with K=8 heads on Cora:

LayerInput shapeHeadsDim per headOutput shapeConcat/Avg
Input features[2708, 1433][2708, 1433]
GAT Layer 1[2708, 1433]K=88[2708, 64]Concat 8×8=64
GAT Layer 2[2708, 64]K=17 (classes)[2708, 7]Average 1×7=7
Softmax[2708, 7][2708, 7] probabilities

Parameters: Wk per head: 1433×8 = 11,464. Eight heads: 91,712. Attention vectors: 8×(2×8) = 128. Layer 1 total: ~91,840. Layer 2: 64×7 + 2×7 = 462. Grand total: ~92,302 parameters. For comparison, GCN (2 layers): 1433×64 + 64×7 = 92,160 — almost identical despite very different architectures.

In multi-head GAT for intermediate (hidden) layers, why are head outputs CONCATENATED rather than averaged?

Chapter 4: The 'a' Function in Detail

The attention function a: R2d' → R seems simple — it's just a linear layer. But the paper made specific choices that are worth understanding: why additive? why LeakyReLU? why this particular concatenation order?

Additive Attention vs. Dot-Product Attention

There are two classic attention formulations:

Additive attention (Bahdanau 2014, GAT):

evu = vT tanh(W1hv + W2hu)

Or the GAT variant: LeakyReLU(aT[Whv ∥ Whu]). Asymmetric (score depends on ORDER of v, u in concatenation). Has O(d) extra parameters (a).

Dot-product attention (Vaswani 2017, Transformer):

evu = (WQhv)T(WKhu) / √d'

Symmetric in Q and K (but Q and K can be different matrices). O(d2) extra parameters (WQ, WK). Scales better to large d because of the 1/√d' term.

GAT uses additive attention because it was the dominant paradigm in 2017 (pre-Transformer). The LeakyReLU is used instead of tanh because it's faster, avoids saturation for large inputs, and empirically performs comparably. GATv2 (Brody et al., 2021) later showed that GAT's attention is actually "static" (the ranking of neighbors is fixed regardless of the query node's state) — GATv2 fixes this by changing the concatenation order.

The Asymmetry Property

When v attends to u, it concatenates [Whv ∥ Whu]. When u attends to v, it concatenates [Whu ∥ Whv] — different order. The learned vector a = [a1a2] (first half for "source," second half for "target") sees these differently. So αvu ≠ αuv in general — even on an undirected graph, the attention is directional.

This asymmetry is a feature, not a bug. In a directed citation graph (papers cite papers), the attention from the paper being cited to the citing paper should be different from the reverse direction. GAT naturally handles directed graphs because the attention coefficient evu depends on the order [hv ∥ hu], not just their sum or inner product.

A Worked Example

Let d = 2 (2D embeddings for simplicity), d' = 2, K = 1.

hv = [1.0, 0.5], hu = [0.8, 0.6], W = I (identity for clarity).

a = [0.5, -0.3, 0.2, 0.4] (2d' = 4 parameters).

Concat: [Whv ∥ Whu] = [1.0, 0.5, 0.8, 0.6].

aT · concat = 0.5(1.0) + (-0.3)(0.5) + 0.2(0.8) + 0.4(0.6) = 0.5 - 0.15 + 0.16 + 0.24 = 0.75.

evu = LeakyReLU(0.75) = 0.75 (positive, so LeakyReLU is identity here).

python
# Full GAT layer, step by step
import torch, torch.nn.functional as F

def gat_layer(h, adj, W, a, K=1):
    # h: [N, d_in]  adj: list of (src, dst) edges
    # W: [d_in, d_out//K]  a: [2*d_out//K]
    N = h.shape[0]
    Wh = h @ W  # [N, d_out//K]

    # Compute e_vu for all edges
    src, dst = zip(*adj)
    src = torch.tensor(src); dst = torch.tensor(dst)
    concat = torch.cat([Wh[src], Wh[dst]], dim=-1)  # [|E|, 2*d_out//K]
    e = F.leaky_relu(concat @ a, negative_slope=0.2)   # [|E|]

    # Softmax per node (src-side)
    alpha = torch.zeros(len(adj))
    for v in range(N):
        mask = (src == v)
        alpha[mask] = F.softmax(e[mask], dim=0)

    # Weighted aggregation
    h_out = torch.zeros_like(Wh)
    for i, (v, u) in enumerate(adj):
        h_out[v] += alpha[i] * Wh[u]

    return F.elu(h_out)  # ELU used in original paper

The Role of the Shared W

A subtle but important design question: why use the SAME W for both the attention computation and the value computation? Wouldn't it be better to have separate matrices Wattn (for computing scores) and Wval (for computing the values that get summed)?

The paper uses the same W for both: the attention score uses W hv and W hu, and the weighted sum also uses W hu. This coupling means the projection must simultaneously be good for (1) computing compatibility scores and (2) encoding information to be aggregated. These are two potentially conflicting objectives.

In practice this works fine — W is expressive enough to serve both roles. And coupling them reduces parameters (one W instead of two). GATv2 separates them: W1 hv + W2 hu for scoring (different W1, W2), plus a separate WV for the value aggregation. This gives more flexibility at the cost of more parameters.

Gradient Flow Through Attention

During backpropagation, the gradient flows through the softmax and back to the raw scores evu, then back to W and a. The gradient of the softmax output αvu with respect to the raw score evu is αvu(1 - αvu). This means:

This is the same gradient property as logistic regression. The attention mechanism learns by comparing neighbors' contributions against what the final prediction needs — if attending to neighbor A was wrong (caused high loss), the gradient pushes αvA down and αvB up for the "better" neighbor B.

The LeakyReLU in evu has derivative 1 (for positive inputs) or 0.2 (for negative inputs). This ensures gradients flow even when the raw score is negative — unlike ReLU which would create a zero-gradient "dead zone" for incompatible node pairs.

What the Attention Mechanism Actually Learns

We can inspect what GAT learns by analyzing the attention vector a. Write a = [a1a2] where a1, a2 ∈ Rd'. The raw score is:

evu = LeakyReLU( a1T W hv + a2T W hu )

The first term a1T W hv is a function of v only — it's the same scalar added to ALL neighbor scores for v. This is what makes attention "static" (Brody et al.): the relative ranking of neighbors u1 vs u2 is determined solely by a2T W hu1 vs a2T W hu2, which doesn't depend on hv.

What a2T W learns is: a direction in feature space such that nodes that score high in this direction are considered "important." This direction is learned from the task loss — if nodes with high f1 feature tend to have the right labels, the network learns to prefer neighbors with high f1. This is a weak form of feature selection: the attention mechanism learns which feature dimensions of neighbors matter most.

Why does GAT use LeakyReLU instead of ReLU in the attention mechanism e_vu = LeakyReLU(aT[Wh_v ∥ Wh_u])?

Chapter 5: GAT vs. Transformers

The attention mechanism in GAT was published in October 2017 — one month AFTER the Transformer paper (June 2017). The GAT authors reference Bahdanau attention (2014) but not the Transformer. They arrived at a similar idea independently, but for graphs instead of sequences. Understanding the connection reveals what makes graph attention genuinely different.

The Transformer Is GAT on a Complete Graph

In a Transformer processing a sequence of n tokens, every token attends to every other token. The "graph" is a complete graph where every node is connected to every other node. The attention mechanism is: Q = XWQ, K = XWK, V = XWV, output = softmax(QKT/√dk)V.

In GAT, the attention is restricted to the actual edges of the graph. Instead of attending to ALL tokens (O(n2) attention), node v attends only to its neighbors (O(|N(v)|) attention). This makes GAT O(|E|) — linear in the number of edges, not quadratic in nodes.

PropertyGAT (2017)Transformer (2017)
Graph structureSparse: only edges in the input graphComplete: every token attends to every other
Attention typeAdditive: LeakyReLU(aT[Whv ∥ Whu])Dot-product: QKT/√dk
Complexity per layerO(|V|d2 + |E|d) — linear in edgesO(n2d) — quadratic in sequence length
Position encodingGraph topology IS the position (via edge structure)Explicit sinusoidal or learned position encoding
Asymmetryevu ≠ euv (concat order matters)Symmetric in Q,K basis (but Q≠K possible)
Self-loopAdd self-loop manually (include v in N(v))Implicit: every token attends to itself
The unification view: Both GAT and Transformers are instances of the same computation: "for each node/token, compute a weighted combination of all neighbors/tokens, where weights depend on pairwise compatibility." The key difference is which pairs are allowed to interact (graph edges vs. all pairs) and how compatibility is measured (additive vs. dot-product).

Implementing Dot-Product Attention on a Graph

If you wanted to replace GAT's additive attention with Transformer-style dot-product attention, here's exactly what changes:

python
# GAT: additive attention
class GATStyleAttn(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        self.W = nn.Linear(d_in, d_out, bias=False)
        self.a = nn.Linear(2 * d_out, 1, bias=False)

    def score(self, h_v, h_u):
        Wh_v, Wh_u = self.W(h_v), self.W(h_u)
        return F.leaky_relu(self.a(torch.cat([Wh_v, Wh_u], dim=-1)))

# Transformer-style: dot-product attention on graph edges
class GraphDotProductAttn(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        self.W_Q = nn.Linear(d_in, d_out, bias=False)  # separate Q
        self.W_K = nn.Linear(d_in, d_out, bias=False)  # separate K
        self.W_V = nn.Linear(d_in, d_out, bias=False)  # value projection
        self.scale = d_out ** 0.5

    def score(self, h_v, h_u):
        q = self.W_Q(h_v)  # query from center node v
        k = self.W_K(h_u)  # key from neighbor u
        return (q * k).sum() / self.scale  # scaled dot product

# The graph structure is the only difference in WHERE we compute scores:
# GAT: only for edges (v,u) in E — O(|E|) pairs
# Transformer: for ALL pairs (i,j) — O(n²) pairs
# Both then apply softmax (local per node in GAT, global per row in Transformer)

Graph Transformers (2022+)

Recent work fuses both ideas: use Transformer-style dot-product attention, but restrict it to graph edges (like GAT) or use graph structure to bias the attention scores. Examples: Graphormer (Ying et al., 2021) adds degree and spatial encoding to Transformer attention; SAN (Kreuzer et al., 2021) uses Laplacian eigenvectors as positional encodings. These are direct descendants of both GAT and the Transformer.

What is the key computational advantage of GAT over a Transformer when applied to a sparse graph?

Chapter 6: Inductive Learning on PPI

One of GAT's most impressive results is on PPI — the Protein-Protein Interaction dataset, an inductive benchmark. "Inductive" means: train on one set of graphs, test on an entirely different set of graphs. The test proteins were NEVER SEEN during training. This is the hardest regime for GNNs, because you can't learn node-specific parameters.

Why PPI Is Hard

PPI consists of 24 human tissue graphs. Each node is a protein, each edge is a known interaction, and the task is multi-label classification (each protein may have multiple biological functions from 121 possible labels). Train on 20 graphs, validate on 2, test on 2 — completely different graphs. Test-time proteins are entirely new. Any model that stores per-node parameters fails.

GAT is inductive by design. The attention weights αvu are computed from node features — they don't depend on node identity or position. To embed a new protein, you just run the same attention mechanism over its neighborhood. No retraining needed. This is identical to how a CNN classifies a new image it's never seen: the same filters apply everywhere.

Why GAT Excels on PPI

PPI proteins have very heterogeneous neighborhoods. A hub protein may interact with 20+ other proteins, some highly relevant to the target label and some not. The ability to focus attention on relevant neighbors is crucial. GCN's uniform averaging dilutes signal from key interaction partners with signal from incidental ones. GAT's learned attention preserves the signal from biologically relevant partners.

GAT configuration for PPI: 3 layers, K=4 attention heads for layers 1 and 2, K=6 heads (averaged) for layer 3. 256 features per head per layer (total hidden dim = 1024). This is substantially larger than the citation network configurations — PPI is more complex.

Inductive vs. Transductive Results

DatasetSettingMetricGATGCNGraphSAGE
CoraTransductiveAccuracy83.0%81.5%
CiteseerTransductiveAccuracy72.5%70.3%
PubmedTransductiveAccuracy79.0%79.0%
PPIInductivemicro-F197.3%61.2%

On PPI, GAT achieves 97.3% micro-F1 — a dramatic improvement over GraphSAGE's 61.2%. The gap shows the importance of learned attention for complex, heterogeneous biological networks where not all interactions are equally relevant to the prediction task.

GAT Configuration for PPI (From the Paper)

The inductive PPI configuration is substantially different from the transductive citation network one — more layers, more heads, larger hidden dimension:

python
from torch_geometric.nn import GATConv
import torch.nn as nn
import torch.nn.functional as F

class GATForPPI(nn.Module):
    """3-layer GAT configured as in Velickovic et al. for PPI.
    Input: 50 protein features. Output: 121 binary labels."""
    def __init__(self):
        super().__init__()
        # Layer 1: K=4 heads, 256 out per head → concat = 1024
        self.gat1 = GATConv(50, 256, heads=4, concat=True)
        # Layer 2: K=4 heads, 256 out per head → concat = 1024
        self.gat2 = GATConv(1024, 256, heads=4, concat=True)
        # Layer 3: K=6 heads, averaged → 121 (one per label)
        self.gat3 = GATConv(1024, 121, heads=6, concat=False)

    def forward(self, x, edge_index):
        x = F.elu(self.gat1(x, edge_index))   # [N, 1024]
        x = F.elu(self.gat2(x, edge_index))   # [N, 1024]
        x = self.gat3(x, edge_index)           # [N, 121]
        return torch.sigmoid(x)  # Multi-label: sigmoid, not softmax

# Key difference from citation network config:
# - 3 layers (vs 2), K=4 hidden (vs 8), K=6 output (vs 1)
# - Hidden dim: 1024 (vs 64 on Cora)
# - Multi-label: BCELoss, not cross-entropy
# - NO dropout (PPI has enough data unlike 20 labels/class on Cora)

The training protocol also differs. For PPI, the paper uses mini-batch training with full neighborhoods (no sampling) — each graph is a mini-batch. For Cora/Citeseer, it's full-batch (the entire graph fits in memory). The loss is binary cross-entropy (multi-label) instead of categorical cross-entropy.

What makes GAT "inductive" — i.e., able to generalize to graphs and nodes not seen during training?

Chapter 7: Experimental Results

The paper evaluates on four benchmarks: Cora, Citeseer, Pubmed (transductive citation networks) and PPI (inductive protein interaction). Let's look at what the results tell us beyond just the numbers.

Citation Networks: Transductive Benchmarks

These three datasets (Cora: 2,708 nodes, Citeseer: 3,327, Pubmed: 19,717) have been the standard GNN testbeds since Kipf & Welling 2017. The protocol: 20 labeled nodes per class for training (very few!), 500 for validation, 1000 for test.

The improvements over GCN are modest: 1.5% on Cora, 2.2% on Citeseer, 0% on Pubmed. This is expected — these datasets are small, homophilic (connected nodes tend to have the same label), and nearly all GNNs perform similarly on them. The uniform-weight heuristic of GCN is already near-optimal when the graph is homophilic, because the most relevant neighbors ARE the most similar ones, and similar nodes ARE typically neighbors.

When GAT helps most: GAT's attention matters most when the graph is heterophilic — when neighbors are often different from the central node. In that case, you need to selectively attend to the few neighbors that ARE relevant and down-weight the noisy majority. Citation networks are mostly homophilic, so the gain is small. PPI is more heterophilic, so the gain is large (97.3% vs 61.2%).

Benchmark Context: Why These Numbers Matter

The Cora, Citeseer, and Pubmed benchmarks use a very specific and arguably unrealistic split: 20 labeled nodes PER CLASS for training. On Cora (7 classes), that's 140 labeled nodes out of 2,708 total — a 5% supervision rate. This protocol was designed to test semi-supervised learning, where the model must leverage graph structure to propagate labels to the 95% of unlabeled nodes.

Under this protocol, over-regularization (heavy dropout like p=0.6) is essential — there's simply not enough labeled data to prevent overfitting without it. This explains why the citation network GAT configs are so heavily regularized compared to PPI. The benchmark is a STRESS TEST for label propagation via graph structure, not a test of raw representation quality.

More recent benchmarks (OGB — Open Graph Benchmark, 2020) use larger training sets and more realistic splits. GAT's improvements on OGB benchmarks are typically smaller, suggesting that the original Cora-scale improvements partly reflected the small-training-set regime where attention-based neighbor selection is most critical.

Sensitivity to K (Number of Heads)

The paper's ablation on Cora: K=1 (81.8%), K=2 (82.0%), K=4 (82.6%), K=8 (83.0%). The improvement saturates around K=8. Each additional head adds O(d²/K) parameters — the total parameter count stays roughly constant as K increases (fewer parameters per head). So K=8 is "free" in terms of model size but costs K× more compute (K independent attention computations).

State-of-the-Art Context (What Came After)

How does 83.0% on Cora look in 2025? Here's the progression on the standard 140-training-node Cora benchmark:

ModelYearCora (%)Key Idea
GCN (Kipf & Welling)201781.5Spectral conv, uniform weights
GAT (Velickovic et al.)201883.0Learned attention weights
GATv2 (Brody et al.)202283.7Dynamic attention
GCNII (Chen et al.)202085.5Initial residual + identity mapping (64 layers)
APPNP (Gasteiger et al.)201983.3Personalized PageRank propagation
RevGNN (Li et al.)202184.2Reversible GNN layers (1000 layers!)
NodeFormer (Wu et al.)202283.7All-pair attention approximation

The accuracy numbers on this benchmark have converged to the 83-86% range. The law of diminishing returns has set in — the remaining error is largely irreducible (noisy labels, ambiguous papers). More recent benchmarks (OGB-Arxiv, OGB-Products) offer more headroom for improvement and better reflect real-world scale.

Visualization: What Does GAT Attend To?

The paper includes a striking visualization: on the Cora dataset, GAT's attention weights align with class membership. Nodes tend to attend more to neighbors of the SAME class. This wasn't specified during training — the network learned on its own that class membership is a relevant criterion for attention, using only the node features (paper abstracts as bag-of-words).

Training Details and Optimization

The paper uses the Adam optimizer (Kingma & Ba, 2015) for all experiments. Key hyperparameters:

SettingCora / CiteseerPPI
Learning rate0.0050.005
Weight decay (L2)5e-40
Dropout (input + attention)0.60
Hidden units per head8 (×8 heads = 64)256 (×4 heads = 1024)
Epochs1000 (early stop)100,000
LossCross-entropyBinary cross-entropy

Dropout is applied to both the INPUT features and the ATTENTION COEFFICIENTS (before softmax). Dropping attention coefficients is unusual — it acts like randomly removing edges during training, similar to DropEdge. This was found to be crucial for citation network generalization (small datasets, high overfitting risk). For PPI (large dataset), dropout hurts rather than helps.

GAT shows a large improvement over GraphSAGE on PPI (97.3% vs 61.2%) but only a small improvement on Cora (83.0% vs no reported baseline). What does this suggest about when learned attention helps?

Chapter 8: Limitations

GAT was a breakthrough in 2018, but the field kept probing its weaknesses. Several important limitations have been identified since publication — and each limitation spawned a follow-up paper.

Static Attention (GATv2, 2021)

Brody, Alon, and Yahav (ICLR 2022) proved that GAT computes static attention: the ranking of neighbors for a given node v is FIXED regardless of what query v is asked. Formally, there exists a node v such that the argmax of αvu over u is the same for ALL possible feature vectors of v.

This happens because W is applied to hv and hu BEFORE concatenation and scoring. The function is: LeakyReLU(a1T W hv + a2T W hu). The v-dependent part (a1T W hv) is a scalar that shifts all scores uniformly — it doesn't change the RANKING of neighbors, only the scale.

GATv2 fixes this by moving the nonlinearity: evu = aT LeakyReLU(W1 hv + W2 hu). Now the v-dependent and u-dependent parts interact nonlinearly, enabling dynamic attention where the ranking changes with the query. GATv2 consistently outperforms GAT on benchmarks where dynamic attention matters.

The lesson: Beware of claims that a model is "expressive" without formal analysis. GAT looks expressive (learned attention!) but a simple structural argument shows the attention ranking is static. This is a common pattern: a component looks powerful but has a hidden rigidity that limits what it can learn.

No Edge Features

GAT computes attention from NODE features only. In graphs where edge features carry important information (molecular graphs: bond type, bond length; road networks: road type, speed limit), GAT ignores them. You can hack around this by concatenating edge features to node features before attention, but this is not principled.

Fixed by: GAT with edge features (add edge features to the concatenation [Whv ∥ Whu ∥ Wevu]), message-passing neural networks (MPNN), or graph Transformers that treat edge features as bias terms in the attention score.

Quadratic Memory on Dense Graphs

For sparse graphs (|E| << n²), GAT is efficient. But on dense graphs or when attention is applied across ALL pairs (complete graph), memory and compute scale as O(n²), same as Transformers. Sparse Transformers (Beltagy et al., 2020) and linear attention approximations (Choromanski et al., 2021) address this — and have been adapted to GNNs.

Oversmoothing and Over-Squashing

GAT shares GCN's fundamental problem: attention doesn't prevent over-smoothing with many layers. And deep message-passing suffers from over-squashing — information from distant nodes is exponentially compressed into fixed-size vectors. Attention can PRIORITIZE which information to pass, but it can't expand the bandwidth of the message. This is a fundamental limitation of all local message-passing GNNs.

To see why: with K=3 GNN layers, a node v at the center of a star with 100 leaves must aggregate from 100 nodes at depth 1, 100×100 = 10,000 nodes at depth 2, and 10,000×100 = 1 million nodes at depth 3 — all compressed into a single d-dimensional vector at each step. The information from depth-3 nodes is exponentially diluted. GAT's attention helps by down-weighting irrelevant nodes, but even if you zero out 99% of them, the 1% of relevant ones at depth 3 still go through 3 rounds of fixed-size compression.

The theoretical characterization of over-squashing (Alon & Yahav, 2021) shows that the bottleneck is the graph's Cheeger constant — a measure of how many bottleneck edges separate parts of the graph. Nodes connected through narrow bottlenecks (few edges between communities) suffer from over-squashing regardless of attention weights. Solutions include graph rewiring (adding edges to reduce bottlenecks) and positional encodings that give nodes "global awareness" without requiring information to travel through many local hops.

LimitationRoot CauseFix
Static attention rankingLinear v before nonlinearityGATv2: move nonlinearity inside
No edge featuresAttention only on node pairsInclude evu in concat
Oversmoothing (deep)Repeated averagingSkip connections, JK-Net
Over-squashingFixed message capacityRewiring (add virtual nodes/edges)
Dense graphs: O(|E|)One attention per edgeSparse attention, linear approximations

GATv2: The Fix for Static Attention

The fix is surgical — one line changes. Move the nonlinearity from outside the concat to inside:

python
# GAT (original): static attention
# e_vu = a^T * LeakyReLU([Wh_v || Wh_u])
# = a1^T * LeakyReLU(Wh_v) + a2^T * LeakyReLU(Wh_u)  — separable!
# The v-part is just a SCALAR SHIFT on all neighbor scores — doesn't change ranking.

class GATv2Attention(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        # GATv2 uses SEPARATE W1, W2 (not shared W like GAT)
        self.W1 = nn.Linear(d_in, d_out, bias=False)  # for h_v
        self.W2 = nn.Linear(d_in, d_out, bias=False)  # for h_u
        self.a = nn.Linear(d_out, 1, bias=False)
        self.leaky = nn.LeakyReLU(0.2)

    def raw_score(self, h_v, h_u):
        # GATv2: nonlinearity INSIDE, applied to SUM not concat
        # e_vu = a^T * LeakyReLU(W1*h_v + W2*h_u)
        # Now v and u interact INSIDE the nonlinearity — NOT separable
        # → attention ranking CAN change with h_v
        z = self.leaky(self.W1(h_v) + self.W2(h_u))  # [d_out]
        e = self.a(z)  # scalar
        return e

# In practice: use GATv2Conv from PyG (drop-in replacement for GATConv)
from torch_geometric.nn import GATv2Conv
gat2_layer = GATv2Conv(in_channels=64, out_channels=64, heads=8)
# Same interface, strictly more expressive than GATConv

Training Instability and How to Fix It

GAT can exhibit training instability, especially with many heads and high dropout. Common failure modes and fixes:

SymptomLikely CauseFix
Loss NaN after a few epochsAttention scores overflow (very large e_vu)Reduce learning rate, add gradient clipping (torch.nn.utils.clip_grad_norm_)
Validation loss oscillates, never convergesDropout too high + LR too high togetherReduce both; dropout 0.5 with LR 0.001 is a safe start
Model memorizes training nodes but val accuracy lowToo few layers / small receptive fieldAdd one GNN layer + skip connection, verify dropout is on during training
All α_vu equal (~1/|N(v)|)Attention vector a initialized too smallUse larger initializer for a (0.1 std), or increase attention dropout slightly
Training fast but test accuracy lowLabel leakage (using test nodes in training)Verify masking: model(data.x, data.edge_index) returns [N, C], loss only on train_mask
What is the "static attention" limitation of GAT that GATv2 fixes?

Chapter 9: Connections

GAT sits at a fascinating intersection: it brought self-attention to graph learning at roughly the same time the Transformer brought it to sequences. Understanding where GAT fits in the historical arc helps you read the follow-up work with the right framing.

Precursors

Bahdanau et al. (2014) — additive attention for RNN encoder-decoder translation. This is the direct parent of GAT's attention mechanism. GAT replaces "source hidden state" with "source node embedding" and restricts attention to graph neighbors instead of all source tokens.

GCN (Kipf & Welling, 2017) — the model GAT extends. GAT replaces GCN's fixed normalization weights with learned ones. The rest of the architecture (projection, nonlinearity, multi-layer stacking) follows GCN directly.

Direct Descendants

GATv2 (Brody, Alon, Yahav — ICLR 2022): Fixes static attention by moving nonlinearity inside the attention function. Same structure as GAT, strictly more expressive. Drop-in replacement in most codebases.

Graph Transformer (Shi et al., 2020; Kreuzer et al., 2021): Combine Transformer self-attention with graph inductive biases (degree encoding, Laplacian eigenvectors as position encoding). More powerful but more complex.

Graphormer (Ying et al., NeurIPS 2021): Transformer applied to molecular graphs for property prediction (competed in OGB-LSC). Adds degree centrality, spatial encoding, edge encoding as bias terms to standard Transformer attention. Achieved SOTA on several molecular benchmarks.

Related Micro-Lessons

The GNN design framework that puts GAT in context: CS224W Lecture 4: A General Perspective on GNNs.
The GCN paper that GAT extends: CS224W Lecture 3: Graph Neural Networks 1.
The Transformer attention mechanism GAT parallels: Attention & Transformers lesson.

Application Domains

GAT has been applied far beyond the citation network benchmarks in the paper. A selection of impactful applications:

DomainGraph TypeWhy GAT Helps
Drug discoveryMolecular graphs (atoms = nodes, bonds = edges)Different bond types and atomic environments have different relevance for property prediction. Attention learns to focus on reactive sites.
Knowledge graphsEntity-relation graphsA given entity may be connected by dozens of relation types. Attention selects relevant relation types for the prediction task.
Traffic forecastingRoad network (intersections = nodes)During rush hour, nearby roads matter more. Attention can dynamically upweight congested neighbors.
Scene graph understandingObject relation graphsRecognizing "person riding bike" requires attending to the spatial relationship edge, not the color or size edges.
Code analysisProgram dependence graphsData-flow dependencies matter more than control-flow for detecting certain bugs. Attention learns this distinction.

A Complete Picture: The Graph ML Family Tree

PaperYearKey InnovationLimitation Addressed
DeepWalk / node2vec2014–16Random walk + SkipGram for graph embeddingsNo deep features on graphs
GCN (Kipf & Welling)2017Spectral convolution → localized aggregationSparse matrix computation on graphs
GraphSAGE (Hamilton et al.)2017Inductive GNN via neighbor samplingTransductive GCN can't scale to new nodes
GAT (Velickovic et al.)2018Learned attention weightsUniform weights ignore content
GIN (Xu et al.)2019Sum aggregation + MLP = 1-WL expressiveMean/max lose count information
GATv2 (Brody et al.)2022Dynamic attention (nonlinearity inside)GAT's static attention ranking
Graphormer (Ying et al.)2021Transformer + degree/spatial encodingGNNs can't capture global structure
GPS (Rampasek et al.)2022Local MPNN + global Transformer combinedNeither alone captures all scales

Comparison: Training Protocols Across Datasets

The different benchmark protocols are worth spelling out explicitly, because they change what "good" looks like:

ProtocolCora / Citeseer / PubmedPPI
Training nodes20 per class (140 for Cora)~44,000 nodes across 20 graphs
Labeled fraction~5%~85% (semi-supervised within each graph)
Test dataSame graph, different nodesCompletely new graphs (2 held out)
Mini-batchFull batch (all nodes)One graph per mini-batch
MetricAccuracy (single label)Micro-F1 (121 binary labels)
Epochs1000 (early stop at patience=100)100,000
Key regularizerDropout p=0.6 + L2 5e-4No dropout, no L2

Reproducing the Paper: A Recipe

To reproduce the Cora result (83.0%) from scratch:

python
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GATConv
import torch, torch.nn as nn, torch.nn.functional as F

dataset = Planetoid(root='/tmp/Cora', name='Cora')
data = dataset[0]  # 2708 nodes, 1433 features, 7 classes

class GAT(nn.Module):
    def __init__(self):
        super().__init__()
        # Layer 1: K=8 heads, 8-dim each → 64 total
        self.gat1 = GATConv(1433, 8, heads=8, dropout=0.6)
        # Layer 2: K=1 head, 7-dim (classes), averaged
        self.gat2 = GATConv(64, 7, heads=1, concat=False, dropout=0.6)

    def forward(self, x, edge_index):
        x = F.dropout(x, p=0.6, training=self.training)
        x = F.elu(self.gat1(x, edge_index))
        x = F.dropout(x, p=0.6, training=self.training)
        x = self.gat2(x, edge_index)
        return F.log_softmax(x, dim=-1)

model = GAT()
optimizer = torch.optim.Adam(model.parameters(), lr=0.005, weight_decay=5e-4)

def train():
    model.train()
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)
    loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
    loss.backward()
    optimizer.step()
    return loss.item()

# Train 1000 epochs with early stopping on val accuracy
# Expected: ~83% test accuracy, ~0.01-0.02 variance across seeds

The entire trainable parameter count is: GATConv1: 1433×8×8 + 2×8×8 = 92,032 params. GATConv2: 64×7 + 2×7×1 = 462 params. Total: ~92,500 params. This is comparable to a tiny MLP — the graph structure (not model capacity) does the heavy lifting.

Key reproducibility notes: (1) results vary by ±0.5% across random seeds due to the stochastic dropout; (2) the paper reports the mean over 10 runs; (3) the specific data split (which 20 nodes per class are labeled) also matters — always use the standard Planetoid split from Yang et al. 2016, not a random split; (4) the ELU activation in the original code differs from the ReLU used in some re-implementations. These small details compound to explain why many "reproductions" report slightly different numbers.

What GAT Still Gets Right (in 2025)

Despite GATv2 being strictly more expressive, the original GAT remains widely used because: (1) static attention is not a problem on homophilic graphs where neighbor ranking doesn't need to be dynamic; (2) GATConv is extremely well-optimized in PyTorch Geometric — faster in practice than GATv2Conv on many GPUs; (3) the original 83.0% Cora accuracy set a reference point that has guided benchmark design for years. Many later papers report Cora results specifically to be comparable to GAT.

Citation Impact

GAT (arXiv: 1710.10903) has over 20,000 citations as of 2025 — one of the most cited papers in graph machine learning. It is implemented natively in PyTorch Geometric (GATConv, GATv2Conv), DGL, and virtually every graph ML library. The core insight — that not all neighbors deserve equal attention — has become a design principle applied far beyond GNNs, influencing work on hypergraph attention, molecular attention, and scene graph understanding.

The arXiv Paper vs. the Published Version

GAT has an interesting publication history. The arXiv version (1710.10903, October 2017) preceded the Transformer paper by 4 months but was submitted to ICLR 2018. The ICLR reviews initially expressed skepticism about the novelty relative to existing attention mechanisms — one reviewer gave a 3/10 score. The paper was accepted after revision, partly because the inductive PPI results were particularly strong and distinguished it from prior graph attention work. This is worth knowing because the early arXiv version circulated widely before publication; if you see citations to "GAT 2017," they're citing the arXiv preprint of the same paper.

The Enduring Lesson

GAT's lasting contribution is not the specific architecture — GATv2 supersedes it on expressiveness, Graphormer supersedes it on scale, and countless follow-ups refine individual components. The lasting contribution is the QUESTION: should neighbor importance be fixed or learned? Before GAT, the field accepted fixed structural weights as natural. After GAT, learned content-aware weights became the default aspiration.

This mirrors a broader pattern in deep learning: the key papers are often not the ones that achieve the highest numbers but the ones that reframe the question. GCN asked "can spectral graph convolution be made local?" GAT asked "should all neighbors be equal?" The best papers identify the right question; the community then spends years optimizing the answer.

"The key difference from prior work on attention on graphs is that we do not require any kind of expensive matrix operation (such as inversion) or make any assumptions about the graph structure. All of these properties make GAT readily applicable to inductive problems as well."
— Velickovic et al., ICLR 2018

Debugging GAT in Practice

When GAT doesn't work as expected, here's a systematic debugging process:

Check attention entropy
If all attention weights are near-uniform (entropy ≈ log(|N(v)|)), attention isn't learning to differentiate. Increase the scale of the attention vector initialization or reduce dropout on attention weights.
Check attention collapse
If all attention goes to one neighbor (entropy ≈ 0), the model has found a degenerate solution. Happens with high learning rate — reduce it, or add attention entropy regularization.
Verify layer norms
If training loss oscillates, add LayerNorm after each GAT layer. Especially important for K≥4 heads and d_hid≥256. BatchNorm also works but can fail with very small mini-batches.
Ablate attention
Replace GAT with GCN (set all α = 1/|N(v)|). If GCN matches or beats GAT, the task is homophilic and attention isn't helping — save parameters and use GCN.

Attention Visualization as Model Interpretability

One practical advantage of GAT over GCN is interpretability: you can visualize which neighbors each node attends to. For citation graphs, this reveals topic clusters. For molecular graphs, this can highlight reactive functional groups. For recommendation systems, it shows which historical interactions inform the current recommendation.

However, Jain & Wallace (2019) warned that attention weights are not always faithful explanations — high attention does not necessarily mean causal importance. In GAT, the attention weights interact with the value transformation W hu in complex ways. A neighbor might receive low attention but have a highly informative transformed embedding that strongly influences the output. For safety-critical applications (medical diagnosis, drug discovery), use post-hoc explainability tools (GNNExplainer, SubgraphX) rather than relying solely on attention weights.

Open Research Questions (as of 2025)

GAT raised as many questions as it answered. Here are the open problems that the community is still actively working on:

Dynamic attention (partially solved): GATv2 is more expressive but the gap on real benchmarks is small. When does dynamic attention actually matter? Which tasks require it? Current answer: heterophilic graphs and tasks requiring long-range reasoning — but the community lacks good benchmarks for this.

Scalable attention: GAT's O(|E|) per layer is great for sparse graphs. But for dense graphs or when we want ALL-PAIR attention, we need approximations (linear attention, sparse Transformers). These are active research areas.

Interpretability of attention: When GAT attends to a specific neighbor, does it mean that neighbor is causally important? Jain & Wallace (2019) showed for NLP that high attention ≠ important token. Similar questions apply to GAT — attention weights are not always faithful explanations of model behavior.

Pre-training GAT: How to pre-train a GAT on unlabeled graphs and fine-tune on small labeled datasets? Self-supervised GNN pre-training (Hu et al., 2020) showed this is possible for molecular graphs. GAT's attention mechanism adds complexity to the pre-training design.

Which architectural change would transform GAT into something resembling a Transformer?