Code Architecture — node2vec Notebook
Overview
The notebook implements node2vec from scratch (no node2vec library, no gensim for the core algorithm) on a small synthetic graph with known community structure. It demonstrates biased second-order random walks, Skip-gram with negative sampling, and visualization of learned embeddings colored by community membership.
Section-by-Section Breakdown
1. Setup & Imports
- Uses
numpy,networkx,matplotlib,scikit-learn(KMeans, t-SNE) — all pip-installable. - No GPU required; runs entirely on CPU.
2. Graph Construction
- Builds a synthetic graph with 3 communities (Barabasi-Albert clusters connected by bridge edges).
- Each node is assigned a community label (0, 1, or 2).
- The graph is small (~60 nodes) for fast execution and clear visualization.
- Stores adjacency list, edge list, and node-to-community mapping.
3. Biased Random Walk Generation
compute_transition_probs(graph, p, q): Precomputes the 2nd-order transition probabilities. For each pair of (previous_node, current_node), computes α_pq for every neighbor of current_node based on shortest path distance from previous_node.- d(tx) = 0 → α = 1/p (return to previous node)
- d(tx) = 1 → α = 1 (move to a neighbor of current that is also neighbor of previous)
- d(tx) = 2 → α = 1/q (move outward to a node not adjacent to previous)
alias_setup(probs): Constructs the alias sampling tables (prob array and alias array) in O(n) for O(1) sampling.alias_draw(alias, prob): Draws one sample in O(1) using the alias method.biased_random_walk(graph, start_node, walk_length, p, q): Simulates one walk using the precomputed transition probabilities and alias sampling.generate_walks(graph, num_walks, walk_length, p, q): Generates walks for all nodes,num_walkstimes each. Returns list of walk sequences (list of node-id lists).
4. Skip-gram with Negative Sampling
- Vocabulary: All nodes in the graph.
skipgram_train(walks, dim, window_size, neg_samples, epochs, lr): Implements Skip-gram with negative sampling from scratch using numpy.- Initializes embedding matrix W (|V| × dim) and context matrix C (|V| × dim).
- For each walk, for each center node, samples context nodes within ±window_size.
- Positive pair: (center, context) → sigmoid(W[center] · C[context]) → maximize log-likelihood.
- Negative pairs: sample
neg_samplesrandom nodes → minimize sigmoid(W[center] · C[neg]). - Gradient updates with learning rate
lr, using SGD. - Returns the embedding matrix W.
5. Visualization
- t-SNE projection: Reduces d-dimensional embeddings to 2D for visualization.
- K-Means clustering: Clusters embeddings into k=3 clusters to evaluate community recovery.
- Scatter plot: Colors nodes by true community label. Shows that nodes in the same community cluster together in embedding space.
- Graph visualization: Draws the original graph with node colors from learned cluster assignments.
6. Parameter Sensitivity (BFS vs DFS)
- Runs node2vec with two parameter settings:
- p=1, q=0.5 → DFS-like (homophily): nodes in same community should cluster together.
- p=1, q=2.0 → BFS-like (structural equivalence): nodes with similar roles should cluster together.
- Visualizes both to show the complementary embeddings, mirroring the Les Misérables case study from the paper.
7. Evaluation
- Computes clustering accuracy (after optimal label permutation) as a proxy for embedding quality.
- Prints the accuracy for both parameter settings.
Key Functions/Classes
| Function | Purpose |
|---|---|
compute_transition_probs(G, p, q) | Precompute 2nd-order transition probabilities |
alias_setup(probs) | Build alias sampling tables |
alias_draw(alias, prob) | O(1) sampling using alias method |
biased_random_walk(G, node, length, p, q, probs) | Single biased walk |
generate_walks(G, n_walks, walk_len, p, q) | All walks for all nodes |
skipgram_train(walks, dim, window, neg, epochs, lr) | Skip-gram with negative sampling |
visualize_embeddings(emb, labels, title) | t-SNE + scatter |
graph_with_clusters(G, emb, k, title) | Graph viz with KMeans colors |
Data Flow / Shapes
Graph (N nodes, E edges)
→ compute_transition_probs → dict of (prev, curr) → [(neighbor, α_pq), ...]
→ alias_setup → (prob_array, alias_array) per (prev, curr) node pair
→ generate_walks → list of walks: [[n0, n1, ...], [n0, n1, ...], ...]
→ skipgram_train → embedding matrix W: (N × d)
→ t-SNE → (N × 2) for visualization
→ KMeans → cluster assignments: (N,)
Deliberate Simplifications vs Full Paper
- Graph size: ~60 nodes instead of millions. The algorithm is identical; only scale differs.
- No parallelization: Single-threaded. The paper emphasizes parallelism for scalability.
- Skip-gram implementation: Hand-written numpy SGD instead of optimized C/word2vec. Slower but transparent.
- No edge feature learning: We focus on node embeddings. The paper’s edge operators (Hadamard, weighted-L1, etc.) are mentioned but not implemented.
- No semi-supervised parameter learning: p and q are set manually. The paper shows they can be learned from a small labeled set.
- Negative sampling distribution: Uniform random sampling instead of unigram^0.75 (no node degree frequency to weight by in a small graph).