search_ads_recsys / 24 · graph neural networks lesson 24 / 39

Graph neural networks for recommendation

A recommender is not a table that happens to have a graph hiding inside it — it is a graph. The user–item interaction matrix is literally a bipartite graph, and classical collaborative filtering is just the shallowest, 1-hop reading of it. GNNs are what you get when you stop pretending otherwise and let signal flow across the edges you already have.

The recommender was a graph all along

Take the user–item interaction matrix R — rows are users, columns are items, an entry is "this user clicked / watched / bought this item." Rotate your eyes: that matrix is the adjacency matrix of a bipartite graph. Users on one side, items on the other, an edge wherever an interaction happened. Nothing was added; the matrix and the graph are the same object in two notations.

USERS ITEMS ┌─────┐ ┌─────┐ │ u1 │───────────────────────│ i1 │ │ │╲ ╭───────────│ │ │ u2 │ ╲────────╳────────────│ i2 │ │ │ ╱ ╳╲ │ │ │ u3 │╱ ╱ ╲───────────│ i3 │ ← u3 is "cold": one edge │ │ ╱ │ │ │ u4 │───────╱ │ i4 │ ← i4 is "cold": one edge └─────┘ └─────┘ edge = an interaction (click / watch / buy)

Once you see the graph, the classical methods reveal themselves as special cases. Matrix factorization learns a vector e_u per user and e_i per item so that e_u · e_i reconstructs the edge — but it only ever looks at the direct edge between u and i. It is a 1-hop, transductive model: it knows who you clicked, and nothing about who the people who clicked the same things clicked next.

That is the starvation problem. A sparse interaction matrix gives a flat model almost no direct edges per node — long-tail items have a handful of clicks, new users have one. The signal a 1-hop model needs simply is not in its receptive field. A GNN widens that receptive field by propagating embeddings over multi-hop neighborhoods, so a node's representation is built from its neighbors, its neighbors' neighbors, and so on. That multi-hop reach is precisely the signal collaborative filtering throws away.

The one-line summary you should be able to give
The user–item matrix is a bipartite graph; matrix factorization is the 1-hop special case that only sees direct edges. A GNN generalizes it by message-passing embeddings across multi-hop neighborhoods, so sparse and long-tail nodes borrow evidence from nodes several hops away — exactly the high-order connectivity a flat model is blind to.

Message passing — the one primitive that runs everything

Every GNN, under the branding, is the same loop: each node builds a new representation by aggregating messages from its neighbors, then updating itself with that aggregate. Repeat K times and information has flowed K hops. The general layer equation:

h_v(k) = UPDATE( h_v(k−1), AGG( { h_u(k−1) : u ∈ N(v) } ) )

Read it slowly. h_v(k) is node v's embedding after k layers. N(v) is its neighbors. AGG is a permutation-invariant pool (mean, sum, max, or attention) — it must not care about neighbor ordering, because a set of neighbors has none. UPDATE combines the old self-embedding with the pooled neighbor message (a concatenation through a weight matrix, a gated sum, or — as we will see — nothing at all).

The depth k is the hop count, and the receptive field grows combinatorially with it: layer 1 sees direct neighbors, layer 2 sees neighbors-of-neighbors, and on a bipartite graph that alternation is what lets a user embedding absorb information from other users who share its items:

k=0 u3 ← its own ID embedding only (what MF sees: just u3) k=1 u3 ← AGG{ items u3 touched } (1 hop: u3's items) k=2 u3 ← AGG{ users who touched those items } (2 hops: u3 ↔ i ↔ u') k=3 u3 ← AGG{ items THOSE users touched } (3 hops: candidate items!) the path u → i → u' → i' is the collaborative signal: "people who liked what you liked also liked i'"

That u → i → u' → i' path is the entire intuition of collaborative filtering, made explicit as graph propagation. A 1-hop model can never traverse it; a 3-hop GNN bakes it directly into the embedding.

GraphSAGE — sample, aggregate, and stay inductive

The first thing that breaks at scale: you cannot aggregate over a node's entire neighborhood when a popular item has ten million neighbors. GraphSAGE's answer is to sample a fixed-size neighbor set at each hop (say 25 at hop 1, 10 at hop 2) and aggregate over the sample. The layer, concretely with a mean aggregator:

h_v(k) = σ( W · CONCAT( h_v(k−1), meanu ∈ S(v) h_u(k−1) ) )

where S(v) is a fixed-size sample of N(v). The aggregator can be mean, an LSTM (order-sensitive, so neighbors are shuffled), or max/mean pool (each neighbor passed through a small MLP, then pooled). The deeper point is in the word inductive.

Matrix factorization is transductive: it learns one fixed embedding per node ID. A node that did not exist at training time has no row in the embedding table — you must retrain to give it one. GraphSAGE instead learns the aggregator weights W, not the per-node vectors. To embed a brand-new node you feed its features and its sampled neighbors through the learned aggregators — no retraining, no new parameters. That is the bridge straight to 15 · Cold start: an inductive GNN can embed an item the instant it is uploaded, as long as it has features and at least a few edges.

PropertyTransductive (matrix factorization)Inductive (GraphSAGE / PinSage)
What is learnedOne embedding vector per node IDAggregator weights shared across all nodes
New node at servingNo embedding until retrainEmbed on the fly from features + neighbors
Cold startStructurally impossible (no row)Natural — the whole point
Generalizes to unseen graphNoYes (e.g. train on subgraph, serve on full)
MemoryO(#nodes × dim) — huge tablesO(model weights) — tiny
Why "inductive" is the word the interviewer wants
If you can articulate transductive vs inductive cleanly, you have shown you understand why a GNN is not just "MF with extra steps." Transductive models memorize a vector per ID and die on unseen nodes; inductive models learn a function that produces embeddings from features and local structure, so cold items and cold users get a real vector on arrival. This is the single most reused idea in the lesson.

PinSage — GraphSAGE at three billion nodes

PinSage is GraphSAGE taken to Pinterest production: a graph of pins and boards with roughly 3 billion nodes and 18 billion edges. At that scale even sampling a fixed neighbor count uniformly is wrong, because uniform sampling on a power-law graph mostly returns noise. Three engineering moves make it work:

And the training detail interviewers love: curriculum with hard negatives. Easy negatives (a random pin) teach the model almost nothing once it is competent. PinSage starts with random negatives, then progressively mixes in hard negatives — items that are somewhat related to the query (high PageRank-ish proximity but not the true positive) — so the model is forced to learn fine distinctions. Increase negative difficulty by epoch; that is the curriculum.

The neighbor-explosion trap
Naive K-hop aggregation is a combinatorial bomb. With average degree d, a node's K-hop receptive field is O(dK) nodes — on a graph where one popular item has millions of neighbors, hop 2 already pulls in essentially the whole graph for every training example. This is why every production GNN samples (GraphSAGE's fixed fan-out, PinSage's random walks). If an interviewer asks "what stops a GNN from being O(everything) per node?", the answer is neighbor sampling, and the second-order answer is that sampling also acts as a regularizer.

LightGCN — the radical subtraction

Here is the result that surprises people. GNNs imported a lot of deep-learning machinery — feature transformation matrices W, nonlinearities σ, self-connections. LightGCN's authors ablated all of it for the collaborative filtering setting and found that the feature transform and the nonlinearity actively hurt. In pure CF the only input is a one-hot node ID — there are no rich node features for a W to transform — so the transform just adds parameters that overfit, and the nonlinearity discards useful linear structure.

So LightGCN keeps only neighborhood aggregation, and makes it purely linear. The propagation rule, symmetrically normalized:

e_u(k+1) = Σi ∈ N(u) ( 1 / √( |N(u)| · |N(i)| ) ) · e_i(k)

No W, no σ, no self-loop inside the layer — just a normalized sum of neighbor embeddings. The 1/√(|N(u)||N(i)|) term is the standard symmetric normalization: it down-weights edges to high-degree (popular) nodes so a hub does not drown out the signal. Items aggregate from users by the mirror-image rule.

Then the second idea: instead of using only the last layer, the final embedding is a weighted sum of every layer's output, including layer 0:

e_u = Σk=0..K αk · e_u(k) (typically αk = 1/(K+1))

This layer combination is what tames over-smoothing (next section): keeping e_u(0) in the mix preserves the node's own identity, while the deeper layers add high-order signal. The recommendation score is the same old dot product e_u · e_i — so at serving, LightGCN is a two-tower / embedding model, and everything you know from 03 · Embeddings & ANN applies directly. The GNN is only how the embeddings were trained.

GraphSAGEPinSageLightGCN
SettingGeneral graphs, node featuresWeb-scale CF + contentPure CF (IDs only)
Neighbor selectionUniform fixed-size sampleRandom-walk importanceFull normalized neighborhood
Feature transform WYes (per layer)YesNone — removed
NonlinearityYes (ReLU)YesNone — linear
Inductive?YesYesNo (transductive, ID-based)
Final embeddingLast layerLast layerWeighted sum of all layers
Killer use caseCold-start, evolving graphsBillion-node visual feedsStrong CF baseline, dense graphs
The senior framing of LightGCN
The lesson is not "simpler is always better" — it is "match model complexity to the information in your inputs." With only node IDs, there is nothing for a feature transform to do but overfit, so you strip it. The instant you have real node features (content, text, image), the transform earns its keep again and you are back toward GraphSAGE/PinSage. Knowing when the subtraction applies is the signal.

Why GNNs beat plain CF — and where they break

The win is high-order connectivity. A 1-hop model scores a cold node from almost nothing — one edge, maybe none. A multi-hop GNN lets that node borrow signal from its multi-hop neighbors: a long-tail item with three clicks inherits structure from the users who made those clicks and the other items those users engaged with. The sparser the node, the more it gains, because the multi-hop reach is doing the work the missing direct edges cannot. This is the same cold-start logic as lesson 15, expressed structurally instead of through content.

The failure mode is over-smoothing, and it is the flip side of the same mechanism. Each aggregation hop mixes a node's representation with its neighbors'. Do it too many times and every node's embedding converges toward the same average — the graph's stationary distribution. After enough hops you cannot tell two nodes apart, and the embeddings become useless for ranking.

as K → ∞ : h_v(K) → c · (degree-weighted global mean) ∀ v (everything collapses to one point)

This is why GNN depth is capped low in practice — typically 2 to 4 hops. Past that, over-smoothing erases more identity than the extra hops add. LightGCN's layer-combination (keeping e(0)) and residual/jumping-knowledge connections are the standard defenses, but the blunt truth is: depth is a hyperparameter you tune down, not up. A GNN is not a CNN; more layers is not more capacity, it is more blur.

Interactive · how far does a cold node's evidence reach?

A tiny bipartite graph: users u1…u4, items i1…i4. We focus on cold user u3, who has touched only one item. Slide the hop count and watch how many nodes enter u3's receptive field — its "evidence set" — and how the embedding behaves. At 0 hops u3 is just its own (near-empty) ID. A couple of hops pull in collaborative signal. Crank it too high and the over-smoothing readout fires: every node's embedding has converged to the same blur.

Message passing on a cold node
The reachable-node counts are computed by real breadth-first propagation on the graph below. The "embedding distinctiveness" models smoothing: each hop mixes a node with its neighbors, so distinctiveness decays — gently at the first useful hop, then collapsing toward zero as every node converges to the same blur. That decay is over-smoothing made visible.
nodes in u3's reach
candidate items reached
other users reached
embedding distinctiveness
Reading
Slide the hop count to grow u3's neighborhood.
Where the GNN lives in the funnel
A production GNN almost never runs live multi-hop traversal at request time — the latency budget forbids it. Instead you precompute every node embedding offline (the PinSage batched pass), push item vectors into an ANN index, and at retrieval do a single approximate nearest-neighbor lookup against the user vector — exactly the 02 · Candidate generation + 03 · Embeddings & ANN pipeline. The graph structure is amortized into the offline embeddings; serving sees only vectors. The cost you pay is a heavier, graph-aware training/embedding pipeline and staler embeddings between refreshes.

When is a GNN actually worth it?

GNNs are not free — they add a graph-construction step, a sampling pipeline, an offline embedding job, and a refresh cadence to your stack. The honest answer to "should we build one?":

Reach for a GNN when…Stick with a two-tower model when…
Graph structure carries signal a flat model can't see (multi-hop co-engagement, social graph, knowledge graph)Direct features (user profile, item content) already explain most of the variance
The catalog is sparse / long-tailed and nodes need to borrow neighbor signalMost nodes are dense; 1-hop signal is plenty
You face constant cold start and can use an inductive modelThe node set is stable and content features already handle cold start
You can afford the offline pipeline and a refresh SLAYou need real-time embedding updates the GNN refresh can't meet

The principle in one sentence: a GNN is worth it exactly when the edges encode information the node features alone do not, and you can afford to amortize the graph computation offline. If a two-tower model with good content features already captures the signal, the GNN buys you complexity, not accuracy. The interviewer is testing whether you reach for the heavy tool reflexively or because the structure demands it.

Interview prompts you should be ready for

  1. "Why is a recommender naturally a graph, and how does that relate to collaborative filtering?" (The user–item interaction matrix is the adjacency matrix of a bipartite graph. Matrix factorization is the 1-hop, transductive special case — it only sees direct edges. A GNN generalizes it by propagating embeddings over multi-hop neighborhoods, recovering the u→i→u'→i' collaborative path explicitly.)
  2. "Write the general message-passing layer and explain each piece." (h_v^(k) = UPDATE(h_v^(k−1), AGG({h_u^(k−1): u∈N(v)})). AGG is permutation-invariant — mean/sum/max/attention — because neighbors are a set; UPDATE fuses the old self-embedding with the pooled message. K layers = K hops of reach.)
  3. "What does 'inductive' mean and why does it matter for cold start?" (Transductive = learn one vector per node ID, so unseen nodes have no embedding and need a retrain. Inductive = learn aggregator weights, a function from features + neighbors to an embedding, so a brand-new node gets a real vector on arrival. GraphSAGE/PinSage are inductive; that's the cold-start win.)
  4. "LightGCN removes the feature transform and nonlinearity. Why does that help?" (In pure CF the input is a one-hot ID — there's nothing for W to transform but noise, so it overfits, and ReLU discards useful linear structure. Keep only normalized neighbor aggregation e_u^(k+1)=Σ 1/√(|N(u)||N(i)|) e_i^(k), and take the final embedding as a weighted sum across layers. Caveat: once you have real node features, the transform earns its keep again.)
  5. "What is over-smoothing and how does it cap GNN depth?" (Each hop averages a node with its neighbors; too many hops drive every embedding toward one global mean, so nodes become indistinguishable. It caps practical depth at ~2–4 hops. Defenses: layer combination keeping e^(0), residual / jumping-knowledge connections. More layers ≠ more capacity — it's more blur.)
  6. "What stops a GNN from being O(whole-graph) per node, and how does PinSage scale to 3B nodes?" (K-hop reach is O(d^K) — neighbor explosion. Fix: neighbor sampling — GraphSAGE's fixed fan-out, PinSage's random-walk importance neighborhoods + importance pooling. PinSage also computes all embeddings in one batched offline MapReduce pass and serves via ANN, plus a hard-negative curriculum for training.)
  7. "When would you NOT build a GNN and use a two-tower model instead?" (When direct node features already explain the variance, nodes are mostly dense, content already handles cold start, or you need real-time embedding freshness the offline GNN refresh can't meet. A GNN pays off only when edges carry signal the features don't and you can amortize the graph computation offline.)
Takeaway
The user–item matrix is a bipartite graph and CF is its 1-hop reading; a GNN generalizes that by message-passing embeddings over multi-hop neighborhoods so sparse and cold nodes borrow signal from their neighbors. GraphSAGE samples neighbors and is inductive (the cold-start win); PinSage scales it to billions of nodes with random-walk neighborhoods, importance pooling, offline embedding + ANN serving, and a hard-negative curriculum; LightGCN shows that for pure-ID CF you should strip the feature transform and nonlinearity and keep only normalized aggregation. The mechanism that gives you high-order connectivity is the same one that causes over-smoothing, so depth is tuned down to 2–4 hops. Build one only when the edges carry signal a two-tower model can't see — and amortize the graph into offline embeddings so serving is just an ANN lookup.