Back to Graph Theory Series

Graph Neural Networks: GCN & GraphSAGE

September 27, 2026 Wasil Zafar 30 min read

A graph neural network repeatedly asks one question: what should a node learn from its neighbors? GCN fixes normalized weights; GraphSAGE learns an inductive aggregation recipe and samples the neighborhood.

Contents

  1. A Short History
  2. The Message-Passing Lens
  3. Graph Convolutional Networks (GCN)
  4. Why GCN Normalizes
  5. GraphSAGE: Inductive Learning
  6. Sampling & Receptive Fields
  7. GCN vs. GraphSAGE
  8. Worked Examples
  9. Complexity Analysis
  10. PyTorch & NumPy Implementations
  11. Real-World Applications
  12. Exercises
  13. Limitations & Over-Smoothing

A Short History of GCN and GraphSAGE

Images and sequences come with a regular notion of “nearby.” Graphs do not: every node can have a different number of neighbors, and vertex labels carry no spatial meaning. Modern graph neural networks emerged by turning spectral graph filters into localized, reusable neighborhood operations.

2016
Defferrard, Bresson, and Vandergheynst introduced ChebNet, using Chebyshev polynomials to build localized spectral filters without an expensive eigendecomposition at every pass.
2016–17
Thomas Kipf and Max Welling simplified that spectral construction to a first-order propagation rule and demonstrated it for semi-supervised node classification at ICLR 2017.
2017
William Hamilton, Rex Ying, and Jure Leskovec introduced GraphSAGE: learn an aggregation function that can generate embeddings for previously unseen nodes from features and sampled neighborhoods.
2018
PinSage combined neighborhood aggregation with importance sampling and production systems ideas for web-scale recommendation graphs.
Primary papers: ChebNet, GCN, GraphSAGE, and PinSage.

The Message-Passing Lens

Forget matrices for a moment. At layer $\ell$, every node owns a vector $h_v^{(\ell)}$. A message-passing layer performs three conceptual moves:

Collect

Read representations from neighbors $u\in\mathcal N(v)$, optionally including $v$ itself.

Aggregate

Compress a variable-size, unordered multiset of neighbor vectors into one fixed-size summary $m_v^{(\ell)}$.

Update

Combine the summary with the node's current state, apply trainable weights and a nonlinearity, and obtain $h_v^{(\ell+1)}$.

$$m_v^{(\ell)}=\operatorname{AGG}_{\ell}\!\left(\{h_u^{(\ell)}:u\in\mathcal N(v)\}\right),\qquad h_v^{(\ell+1)}=\operatorname{UPDATE}_{\ell}\!\left(h_v^{(\ell)},m_v^{(\ell)}\right).$$

A message-passing layer collects, aggregates, and updates Three neighbor feature vectors flow into a permutation-invariant aggregator. The resulting neighborhood summary combines with the center node's own vector, passes through a learned transformation, and becomes the updated center representation. One layer, viewed from target node v u₁u₂u₃ AGGREGATEmean, sum, pool…order-independent mᵥ hᵥ UPDATEW, σ h′ᵥ Shared parameters let the same local rule run at every node, regardless of vertex label.
The graph supplies connectivity; trainable parameters supply the representation rule. A symmetric aggregator makes the result independent of how an unordered neighbor set happens to be listed.
one layer ≈ one-hop informationshared weightspermutation-aware design

Graph Convolutional Networks (GCN)

Kipf and Welling begin from spectral graph convolution, use a localized first-order approximation, and apply a “renormalization trick.” The resulting layer is compact:

$$H^{(l+1)} = \sigma \left( \tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)} \right)$$

  • $\tilde A=A+I$: self-loops preserve each node's own state while neighbor information arrives.
  • $\tilde D_{ii}=\sum_j\tilde A_{ij}$: degree after self-loops are added.
  • $S=\tilde D^{-1/2}\tilde A\tilde D^{-1/2}$: fixed, symmetric propagation weights determined by graph structure.
  • $H^{(\ell)}\in\mathbb R^{n\times d_\ell}$: node representations, with $H^{(0)}=X$.
  • $W^{(\ell)}\in\mathbb R^{d_\ell\times d_{\ell+1}}$: learned feature transformation shared by every node.

Node-Level Interpretation

GCN forms a degree-normalized weighted sum over the closed neighborhood, then applies the shared projection and activation:

$$h_v^{(l+1)} = \sigma \left( \sum_{u \in \mathcal{N}(v) \cup \{v\}} \frac{1}{\sqrt{\tilde{d}_v \tilde{d}_u}} h_u^{(l)} W^{(l)} \right)$$

Why GCN Adds Self-Loops and Normalizes

A raw sum gives hubs a much larger magnitude simply because they have more neighbors. Symmetric normalization scales an edge contribution using degrees at both endpoints. This keeps the propagation operator balanced across irregular neighborhoods; it is not the same as an unweighted arithmetic mean.

Degree-normalized messages entering one GCN node A target node v with self-loop receives messages from three neighbors. Each edge has weight one divided by the square root of the product of the self-loop-adjusted endpoint degrees. Each message is scaled before summation u₁u₂u₃v d̃(u₁)=2d̃(u₂)=3d̃(u₃)=5d̃(v)=4 1/√(4·2)1/√(4·3)1/√(4·5) self-loop weight 1/4 weighted sumself + neighborsthen W and σ→ h′ᵥ
Adding $I$ makes the current representation one of the messages. The degree terms keep high-degree endpoints from dominating only because they participate in many edges.

The matrix order is associative: implementations may compute $SHW$ or $S(HW)$. On sparse graphs, multiplying by $S$ should use sparse edge operations—materializing a dense $n\times n$ matrix defeats the method's edge-linear scaling.

Transductive nuance: the original citation-network setup trains one full graph with labels hidden on many nodes. That training protocol is transductive. The GCN propagation rule itself can be applied to another compatible graph, but full-batch storage and dependence on its normalized adjacency can make that workflow awkward.

GraphSAGE: Inductive Learning

GraphSAGE learns a function for producing embeddings, not a lookup table containing one learned vector per training node. At inference time, the same parameters can embed an unseen node—or a new compatible graph—from its features and local neighborhood.

For layer $k$:

$$h_{\mathcal{N}(v)}^{(k)} = \text{AGGREGATE}_k \left( \left\{ h_u^{(k-1)}, \forall u \in \mathcal{N}(v) \right\} \right)$$

$$h_v^{(k)} = \sigma \left( W^{(k)} \cdot \text{CONCAT} \left( h_v^{(k-1)}, h_{\mathcal{N}(v)}^{(k)} \right) \right)$$

Mean

Element-wise average. Fast and permutation invariant, but distinct multisets can share the same mean.

Pooling

Apply an MLP to each neighbor, then element-wise max-pool. The final max operation is symmetric.

LSTM

More expressive sequential processing, but not inherently permutation invariant; the original method uses random neighbor permutations.

GCN-style mean

Mean over self and neighbors followed by a shared transform. This variant does not keep self and neighbor summaries separate.

Sampling & Receptive Fields

Without sampling, an $L$-layer mini-batch may need almost the whole graph: every seed requests neighbors, then neighbors of neighbors. GraphSAGE caps this expansion with a fanout $S_k$ at each layer.

Two-layer GraphSAGE sampling with fanouts three and two A seed node samples three first-hop neighbors. Each sampled first-hop node samples two second-hop neighbors. Faded dots represent neighbors not selected for this mini-batch. Fanout [3, 2]: a bounded computation graph seed u₁u₂u₃ w₁w₂w₃w₄w₅w₆ unsampled neighbors1 seed≤ 3 first-hop nodes≤ 3·2 second-hop nodes
Ignoring overlap, one seed touches at most $1+3+3\cdot2=10$ nodes here. Sampling controls memory and latency, but the sampled mean is a stochastic estimate of the full-neighborhood mean.

Sampling Is a Modeling Choice

Small fanouts reduce cost but may miss rare, informative neighbors and add variance. Importance sampling, biased random walks, or domain-aware sampling can outperform uniform selection when neighbor relevance is highly uneven.

GCN vs. GraphSAGE

DimensionGCN layerGraphSAGE
Neighbor weightingfixed symmetric degree normalizationchosen learned aggregator; often a sampled mean or pool
Self informationincluded through self-loopscommonly concatenated or transformed separately
Training unitoften full graph in the original setupseed mini-batches with sampled computation graphs
Unseen nodespossible with compatible features and graph preprocessingan explicit design goal of the architecture
Main tradeoffsimple, stable propagation versus full-graph costbounded batches versus sampling variance

Worked Examples

A Three-Node GCN Pass

Let node 0 connect to nodes 1 and 2. After adding self-loops, adjusted degrees are $(3,2,2)$ and

$$S=\tilde D^{-1/2}\tilde A\tilde D^{-1/2}=\begin{bmatrix}1/3&1/\sqrt6&1/\sqrt6\\1/\sqrt6&1/2&0\\1/\sqrt6&0&1/2\end{bmatrix}.$$

Use $H=\begin{bmatrix}1&0\\0&1\\1&1\end{bmatrix}$ and $W=\begin{bmatrix}0.5&-0.5\\1&0.5\end{bmatrix}$. First transform features, then propagate:

NodeTransformed feature $(HW)_v$Normalized pre-activation $(SHW)_v$After ReLU
0$(0.5,-0.5)$$\approx(1.187,0.037)$$(1.187,0.037)$
1$(1,0.5)$$\approx(0.704,0.046)$$(0.704,0.046)$
2$(1.5,0)$$\approx(0.954,-0.204)$$(0.954,0)$

The values differ even though nodes 1 and 2 have the same degree: their own features differ, and the self-loop preserves that distinction.

A GraphSAGE Mean Input

For target 0, suppose the sampled neighbors are 1 and 2. Their mean is $((0,1)+(1,1))/2=(0.5,1)$. Mean GraphSAGE concatenates the target's own vector $(1,0)$ with that summary, producing $(1,0,0.5,1)$ before the learned linear map. The parameters—not a stored node ID embedding—decide how much self versus neighbor information to keep.

Complexity Analysis

Let $n=|V|$, $m=|E|$, input width $d$, output width $d'$, batch size $B$, depth $L$, and fanout $S_\ell$ at layer $\ell$.

Architecture Approximate work Working memory Scaling lever
Full-Batch GCN $O(md+n dd')$ per layer $O(m+nd)$ excluding parameters sparse adjacency operations
GraphSAGE (Sampled) proportional to sampled nodes/edges and their feature transforms $O\!\left(Bd\sum_{k=0}^{L}\prod_{\ell=1}^{k}S_\ell\right)$ worst-case tree expansion fanouts, depth, deduplication, batch size

Neighborhood overlap often reduces the actual sampled-node count below the tree bound. Conversely, high feature dimensions and storing activations for backpropagation can dominate memory even after sampling controls topology.

PyTorch & NumPy Implementations

The NumPy routine exposes every GCN matrix step. The PyTorch layer consumes an explicit sampled-neighbor list so the implementation matches GraphSAGE's bounded-fanout description. Production libraries replace the teaching loops with sparse kernels and batched index operations.

import numpy as np

def gcn_layer_numpy(A, H, W):
    """One dense teaching layer: ReLU(D^-1/2 (A+I) D^-1/2 H W)."""
    A = np.asarray(A, dtype=float)
    H = np.asarray(H, dtype=float)
    W = np.asarray(W, dtype=float)
    if A.ndim != 2 or A.shape[0] != A.shape[1]:
        raise ValueError("A must be square")
    n = A.shape[0]
    if H.ndim != 2 or H.shape[0] != n:
        raise ValueError("H must have one row per node")
    if W.ndim != 2 or W.shape[0] != H.shape[1]:
        raise ValueError("W has incompatible input width")
    if np.any(A < 0) or not np.allclose(A, A.T):
        raise ValueError("A must be symmetric and nonnegative")
    if not np.allclose(np.diag(A), 0):
        raise ValueError("pass A without self-loops; this layer adds them")

    A_tilde = A + np.eye(n)
    degree = A_tilde.sum(axis=1)
    inv_sqrt = degree ** -0.5
    norm_A = inv_sqrt[:, None] * A_tilde * inv_sqrt[None, :]
    return np.maximum(0, norm_A @ (H @ W))


def sample_neighbors(adj_list, fanout, rng):
    """Uniformly sample at most fanout distinct neighbors per node."""
    if fanout < 1:
        raise ValueError("fanout must be positive")
    sampled = []
    for neighbors in adj_list:
        neighbors = np.asarray(sorted(set(neighbors)), dtype=int)
        size = min(fanout, len(neighbors))
        choice = rng.choice(neighbors, size=size, replace=False)
        sampled.append(choice.tolist())
    return sampled

import torch
import torch.nn as nn

class MeanGraphSAGELayer(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.linear = nn.Linear(2 * in_features, out_features)

    def forward(self, x, sampled_neighbors):
        summaries = []
        for v, neighbors in enumerate(sampled_neighbors):
            if neighbors:
                index = torch.as_tensor(neighbors, device=x.device)
                summaries.append(x.index_select(0, index).mean(dim=0))
            else:
                summaries.append(torch.zeros_like(x[v]))
        neighbor_mean = torch.stack(summaries)
        return torch.relu(self.linear(torch.cat([x, neighbor_mean], dim=1)))


# Three-node example used in the worked calculation
A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 0]], dtype=float)
H = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
W = np.array([[0.5, -0.5], [1.0, 0.5]])
print("GCN output:\n", gcn_layer_numpy(A, H, W))

adj_list = [[1, 2], [0], [0]]
sampled = sample_neighbors(adj_list, fanout=1,
                           rng=np.random.default_rng(7))
sage = MeanGraphSAGELayer(in_features=2, out_features=4)
x_tensor = torch.tensor(H, dtype=torch.float32)
print("Sampled neighbors:", sampled)
print("GraphSAGE output:\n", sage(x_tensor, sampled))

Real-World Applications

GCN and GraphSAGE are architectural templates. The prediction head and loss determine whether the model classifies nodes, scores edges, or summarizes an entire graph.

Recommendation

Users, items, boards, or sessions become nodes. PinSage used importance-based neighborhood sampling to learn item embeddings on Pinterest's web-scale graph.

Molecular prediction

Atoms provide node features and bonds provide topology. A pooling head converts node representations into a molecule-level prediction; edge-aware variants are often needed for bond types.

Fraud and abuse

Accounts, devices, payments, and shared identifiers form heterogeneous interaction graphs. Neighborhood features can expose coordinated patterns missed by isolated records.

Citation and content graphs

Document features combine with link structure for node classification. This is the semi-supervised setting used in the original GCN experiments.

Exercises

1. Recompute the normalization

Add an edge $(1,2)$ to the worked graph. Recalculate $\tilde D$ and $S$, then explain why every existing normalized edge weight changes.

2. Separate self from neighbors

Remove $I$ from the GCN equation and find a graph where a node loses its distinctive feature after one layer. Compare this with GraphSAGE concatenation.

3. Audit permutation invariance

Permute one node's neighbor list. Show that mean and max-pool aggregation are unchanged. Construct an LSTM example whose output changes with order.

4. Measure sampling variance

For a high-degree node, repeatedly estimate the full neighbor mean with fanouts 2, 5, and 20. Plot estimation error against fanout.

5. Build two layers

Implement recursive two-hop sampling for a seed batch. Count requested nodes before and after deduplicating overlapping neighborhoods.

Limitations & Over-Smoothing

The Over-Smoothing Problem

Repeated neighborhood mixing can make node representations increasingly difficult to distinguish. The harmful depth is graph-, task-, and architecture-dependent—there is no universal four-layer threshold. Residual or initial-feature connections, jumping knowledge, normalization, and controlled propagation can help.

Over-squashing

Information from an exponentially growing neighborhood is compressed into a fixed-width vector, so distant signals can become bottlenecked.

Homophily assumptions

Simple averaging works best when connected nodes should be similar. Heterophilous graphs may need signed, directional, relational, or higher-order designs.

Sampling variance

GraphSAGE reduces memory by observing only part of a neighborhood. Predictions can vary with the sample, especially at small fanouts.

Structural expressivity

Basic message passing can map different local structures to the same representation when their aggregated feature multisets are indistinguishable.

Also watch for train–test feature drift, disconnected or featureless nodes, information leakage through graph construction, and evaluation splits that place near-duplicate neighborhoods on both sides of the test boundary.