Easyword2vec

Skip-gram Pair Generation

Word2Vec

Easy

Problem

Generate deterministic Skip-gram training pairs from a token sequence. For each center position, pair its token with every token inside a fixed context window, excluding the center itself and positions outside the sequence.

\left\{(w_i,w_j):\max(0,i-c)\le j\le\min(n-1,i+c),\ j\ne i\right\}

Here, w_i is the token at center position i, w_j is a context token, c is window, and n is the sequence length. Emit pairs by increasing center position and then increasing context position. Return an int64 PyTorch tensor with shape (P,2), including shape (0,2) when no pair exists.

Theory

Skip-gram is one of the two model architectures introduced by Mikolov et al. (2013) for learning distributed word representations. Its training data is not raw text but a stream of (center, context) pairs extracted from a sliding window over each sentence. This problem implements that extraction step: given a sequence of token ids and a window size, produce every center-context pair the model would be trained on.


What Skip-gram Learns

The Skip-gram objective is to predict surrounding context words from a given center word. For each position in the corpus, the center word is fed into the model and the model is asked to assign high probability to each of the words that actually appear nearby. Over millions of such pairs, the embedding of each word is pulled toward the embeddings of the words it co-occurs with, so words that share contexts end up close together in vector space.

Formally, given a training corpus of tokens w_1, w_2, \ldots, w_T and a window size c, the model maximizes the average log probability:

\frac{1}{T} \sum_{t=1}^{T} \sum_{-c \le j \le c,\ j \ne 0} \log p(w_{t+j} \mid w_t)

The inner sum ranges over every offset j in the window except j = 0 (the center itself). Each term \log p(w_{t+j} \mid w_t) corresponds to exactly one (center, context) training pair (w_t, w_{t+j}). This problem generates the set of pairs (w_t, w_{t+j}) that the objective sums over.


The Window Concept

The window defines locality. With window size c, the context of a center token at position i is every token at positions i-c through i+c, excluding i itself. The window captures the intuition behind the distributional hypothesis: words appearing in similar surrounding positions tend to have similar meaning.

A larger window pulls in more distant words and tends to capture topical or domain similarity (words that appear in the same kind of document). A smaller window captures more syntactic or functional similarity (words that are interchangeable in a sentence). Mikolov et al. report good results with windows around 5 to 10 for Skip-gram.

For a center at position i in a sequence of length n, the context indices are:

\{ j : \max(0, i-c) \le j \le \min(n-1, i+c),\ j \ne i \}

The \max and \min clamp the window to the sequence boundaries so it never reads past the first or last token.


Why Pairs Instead of Full Context Vectors

Skip-gram decomposes the multi-word prediction into independent single-word predictions. Instead of predicting the whole context \{w_{i-c}, \ldots, w_{i+c}\} jointly, it emits one pair per context word and treats each as a separate training example. This has two practical benefits:

The pair generation step is therefore the bridge between raw token sequences and the example stream that the optimizer consumes. Getting the pairs right (correct centers, correct contexts, correct boundaries) directly determines what the embeddings learn.


Skip-gram vs CBOW

Word2Vec ships two architectures. They use the same window but flip the direction of prediction:

In pair terms, Skip-gram and CBOW use the same window membership; the difference is which element is the input and which is the target. This problem implements the Skip-gram direction: column 0 of each row is the center, column 1 is a context word.


Fixed vs Dynamic Window

The original word2vec implementation uses a dynamic window: for each center word it samples a random integer r uniformly from 1 to c and uses r as the effective window for that position. This weights nearby words more heavily (they fall inside the window for more sampled values of r) and acts as a cheap form of distance-based weighting.

This problem deliberately uses a fixed window of exactly c on both sides. The dynamic variant depends on a random draw per position, which would make the output nondeterministic and impossible to check exactly. Fixing the window keeps the mapping from input to output deterministic while preserving the core mechanic: enumerate the symmetric neighborhood and emit a pair for each neighbor.

If you wanted the dynamic behavior, you would replace the constant c inside the loop with a per-center sampled value. Everything else (boundary clamping, self-exclusion, emission order) stays the same.


Boundary Handling

Tokens near the start or end of a sequence have a truncated window because there are no words beyond the boundary. The first token has no left context; the last token has no right context. Correct handling clamps the window with \max(0, i-c) on the left and \min(n-1, i+c) on the right.

Two common mistakes here:

A single-token sequence has no context at any window size, so it produces zero pairs. The function returns an empty tensor of shape (0, 2) in that case, which keeps the output shape consistent for downstream batching.


Paper Context and Design Decisions

Mikolov et al. introduced Skip-gram in "Efficient Estimation of Word Representations in Vector Space" (2013) and refined the training in "Distributed Representations of Words and Phrases and their Compositionality" (2013). The first paper proposed the architecture; the second made it practical on billion-word corpora through negative sampling and subsampling of frequent words.

The pair stream sits upstream of those tricks. The paper describes the training objective as a sum over context offsets within a window, which is exactly the set of pairs this problem produces. Two design choices in the original work are worth tracing:

The authors found Skip-gram with negative sampling to be the best speed-quality trade-off, and it became the default for downstream word2vec usage. Understanding pair generation makes the rest of the pipeline (subsampling, negative sampling, the embedding update) easy to reason about, because everything downstream consumes this pair stream.


Complexity

Each center position contributes at most 2c pairs (the full window minus the center). For a sequence of length n the total number of pairs is bounded by 2cn, and is exactly 2cn once away from the boundaries. The time complexity to generate all pairs is therefore O(n \cdot c), linear in both the sequence length and the window size.

Memory is O(n \cdot c) as well, since every pair is materialized into the output tensor. In a real training pipeline pairs are usually streamed rather than stored, but for this problem the full (N, 2) tensor is returned so it can be checked exactly.

The exact count of pairs is useful as a sanity check. For an interior position (one where the full window fits), the position contributes 2c pairs. Boundary positions contribute fewer: position 0 contributes \min(c, n-1) pairs, position 1 contributes \min(c, 1) + \min(c, n-2), and so on. Summing across all positions gives the total N. When c \ge n - 1, the window covers the whole sequence from every position, so each of the n centers pairs with all n - 1 other tokens, yielding exactly n(n-1) pairs. This is why a window larger than the sequence behaves like a dense all-pairs enumeration.


Worked Example

Take token ids [5, 6, 7] with window c = 2. The sequence length is n = 3.

  1. Center at position 0 (token 5): window is \max(0, -2) = 0 to \min(2, 2) = 2, so positions 0, 1, 2. Exclude 0. Emit (5, 6) from position 1 and (5, 7) from position 2.

  2. Center at position 1 (token 6): window is positions 0 to 2. Exclude 1. Emit (6, 5) then (6, 7).

  3. Center at position 2 (token 7): window is positions 0 to 2. Exclude 2. Emit (7, 5) then (7, 6).

The final tensor, in center-then-context order, is:

\begin{pmatrix} 5 & 6 \ 5 & 7 \ 6 & 5 \ 6 & 7 \ 7 & 5 \ 7 & 6 \end{pmatrix}

Six pairs total, matching the bound 2cn clamped by the short sequence. Note that with c = 2 \ge n - 1 the window covers the whole sequence, so every ordered pair of distinct positions appears.


Modern Context

Skip-gram pairs are the conceptual ancestor of the training signal used by many later representation learners. The core idea, turning co-occurrence into a stream of positive pairs and contrasting them against negatives, reappears across modern machine learning:

Transformer language models replaced static word2vec embeddings with contextual representations, but the windowed co-occurrence intuition still underlies how those models learn: nearby tokens shape each other's representations. The pair generation step here is the minimal, transparent version of that signal.


Emission Order

Pairs are emitted in a fixed deterministic order: center position ascending, then context position ascending. The outer loop walks center positions 0, 1, \ldots, n-1; the inner loop walks the clamped window left to right, skipping the center. This ordering matters because the output is compared exactly. Any other order (for example contexts emitted right-to-left, or grouping all left contexts before right contexts) produces the same set of pairs but a different tensor, which would fail an exact equality check.


Pitfalls


Examples

Example 1

Input
token_ids = [0,1,2,3], window = 1
Output
[[0,1],[1,0],[1,2],[2,1],[2,3],[3,2]]
Explanation
Each token is paired with its immediate neighbors while the sequence boundaries are respected.

Example 2

Input
token_ids = [5,6,7], window = 2
Output
[[5,6],[5,7],[6,5],[6,7],[7,5],[7,6]]

Example 3

Input
token_ids = [9], window = 1
Output
[]

Hints

  1. Clamp the context loop to the sequence boundaries for each center index.
  2. Reshape an empty result to (0, 2) before returning it.

Requirements

Constraints

Starter Code

import torch

def skipgram_pairs(token_ids: torch.Tensor, window: int) -> torch.Tensor:
    """
    Returns the ordered center-context pairs as an int64 tensor.
    """
    pass

Test Cases

CaseMatches
Window onepublic
Wide windowpublic
Single tokenpublic