Word2Vec Skip-gram

Suppose you want a computer to know that "espresso" and "latte" are similar. It should learn this without ever being told about coffee. A dictionary cannot do this cleanly. Hand-built taxonomies are expensive and brittle. Skip-gram solves this problem differently. It turns a purely statistical regularity into a supervised learning problem. The regularity: words that appear in similar contexts tend to be similar. This approach requires no labeled data at all. The trick is deceptively simple. Slide a window across raw text. At each position, ask a tiny neural network to predict the surrounding words. It predicts them from the current word. Training this prediction task forces the network to compress each word. Each word becomes a dense vector capturing its distributional behavior. The predictions themselves are thrown away. The byproduct — the learned vectors — is the actual product. This is the seed of modern representation learning. The same philosophy was later reused by contextual encoders. Pretraining objectives across NLP also share this philosophy. Skip-gram was introduced as part of the word2vec family. It mattered because it made high-quality embeddings cheap to train. This training used huge unlabeled corpora. It replaced the sparse, high-dimensional one-hot and count-based representations. Those representations had dominated earlier NLP. Skip-gram scaled to billions of tokens on ordinary hardware. It produced vectors with strikingly linear semantic structure. This structure gave rise to the famous "king − man + woman ≈ queen" analogies. This article gives a precise statement of the skip-gram objective. It includes a full derivation of its gradient. It also provides working pseudocode and a worked numerical trace. Finally, it gives an honest account of where naive softmax training breaks down computationally. This breakdown motivates the negative-sampling trick. That trick is covered in the companion article.

Key takeaways
  • Skip-gram predicts context words from a single center word using a shallow neural network with no nonlinearity.
  • Two embedding matrices are learned: one for words-as-centers, one for words-as-contexts; only the first is usually kept.
  • The training objective is a softmax over the entire vocabulary at every context position, which is the main computational bottleneck.
  • Correctness follows from maximum-likelihood estimation of a log-bilinear conditional model over co-occurring word pairs.
  • Naive softmax costs O(|V|) per training example, which is why negative sampling and hierarchical softmax exist in practice.
  • The resulting embeddings exhibit linear analogical structure and became the standard input representation before contextual encoders.
The core insight

Skip-gram learns word vectors as a side effect of training a simple classifier to predict a word's neighbors, so that words with similar neighbors end up with similar vectors.

The idea

Skip-gram is a self-supervised objective for learning word embeddings from raw text, with no human-provided labels. The supervision signal is generated automatically from co-occurrence: for every word wtw_t in the corpus, the words within a small window around it — say wt−2,wt−1,wt+1,wt+2w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2} — are treated as the "correct answers" the model should predict given wtw_t as input. This is the mirror image of Continuous Bag-of-Words (CBOW), which instead predicts the center word from its context; skip-gram predicts outward from the center. Formally, each word type ww in the vocabulary VV is assigned two vectors: a target (center) embedding vw∈Rd\mathbf{v}_w \in \mathbb{R}^d, and a context embedding uw∈Rd\mathbf{u}_w \in \mathbb{R}^d. These live in two separate weight matrices, W∈R∣V∣×dW \in \mathbb{R}^{|V|\times d} (rows are target vectors) and W′∈R∣V∣×dW' \in \mathbb{R}^{|V|\times d} (rows are context vectors). The model defines the conditional probability of a context word cc given a center word ww via a softmax over a dot-product score, exactly the same log-bilinear form used in multinomial logistic regression:

P(c∣w)=exp⁡(uc⋅vw)∑c′∈Vexp⁡(uc′⋅vw).P(c \mid w) = \frac{\exp(\mathbf{u}_c \cdot \mathbf{v}_w)}{\sum_{c' \in V} \exp(\mathbf{u}_{c'} \cdot \mathbf{v}_w)}.

There is deliberately no hidden nonlinearity — the architecture is a linear projection into a shared embedding space followed directly by a softmax classifier. This is what makes the model cheap enough (in principle) to train on web-scale corpora, and it is also why the induced geometry is so linear: the score is literally an inner product, so directions in the embedding space correspond to consistent semantic/syntactic shifts.

How it works

Training proceeds by maximum likelihood: for every (center, context) pair observed within a window in the corpus, increase P(c∣w)P(c\mid w). Concretely, given a corpus of TT tokens w1,…,wTw_1, \ldots, w_T and window size mm, the corpus log-likelihood objective is

J(θ)=1T∑t=1T∑−m≤j≤mj≠0log⁡P(wt+j∣wt),J(\theta) = \frac{1}{T}\sum_{t=1}^{T} \sum_{\substack{-m \le j \le m \\ j \neq 0}} \log P(w_{t+j} \mid w_t),

and we perform gradient ascent on JJ (equivalently, descent on −J-J) with respect to all entries of WW and W′W'.

Deriving the gradient. Fix one training pair (w,c)(w, c). Let z=vwz = \mathbf{v}_w be the center vector and write the score of context word c′c' as sc′=uc′⋅zs_{c'} = \mathbf{u}_{c'} \cdot z. The per-example loss is

ℓ=−log⁡P(c∣w)=−sc+log⁡∑c′exp⁡(sc′).\ell = -\log P(c\mid w) = -s_c + \log\sum_{c'} \exp(s_{c'}).

Differentiating with respect to the score vector s=(s1,…,s∣V∣)\mathbf{s} = (s_1, \ldots, s_{|V|}) gives the classic softmax-cross-entropy gradient,

∂ℓ∂sc′=P(c′∣w)−1[c′=c],\frac{\partial \ell}{\partial s_{c'}} = P(c' \mid w) - \mathbb{1}[c' = c],

i.e. the model's predicted probability minus the one-hot true label — the same "error signal" you derive for ordinary softmax regression. Since sc′=uc′⋅vws_{c'} = \mathbf{u}_{c'}\cdot \mathbf{v}_w, the chain rule gives update directions for every context vector and for the center vector:

∂ℓ∂uc′=(P(c′∣w)−1[c′=c]) vw,∂ℓ∂vw=∑c′(P(c′∣w)−1[c′=c]) uc′.\frac{\partial \ell}{\partial \mathbf{u}_{c'}} = \big(P(c'\mid w) - \mathbb{1}[c'=c]\big)\,\mathbf{v}_w, \qquad \frac{\partial \ell}{\partial \mathbf{v}_w} = \sum_{c'} \big(P(c'\mid w) - \mathbb{1}[c'=c]\big)\,\mathbf{u}_{c'}.

Note the second equation sums over the entire vocabulary — this is the crux of the efficiency problem discussed below.

Training loop. At the corpus level, we slide a window over the text, generate all (center, context) pairs inside it, and take a stochastic gradient step per pair (or a mini-batch of pairs). ``` function TRAIN_SKIPGRAM(corpus, V, d, window_size m, learning_rate eta, epochs E): initialize W as |V| x d matrix of small random values # target embeddings initialize W' as |V| x d matrix of small random values # context embeddings for epoch in 1..E: for t in 1..length(corpus): w := corpus[t] # center word context_positions := [t-m, ..., t-1, t+1, ..., t+m] ∩ valid indices for t' in context_positions: c := corpus[t'] # true context word v_w := W[w] # center vector, length d

# forward pass: softmax over full vocabulary scores := [ dot(W'[c'], v_w) for c' in 1..|V| ] probs := softmax(scores) # length |V|

# backward pass error := probs # copy error[c] := error[c] - 1 # probs - one_hot(c) grad_v_w := zeros(d) for c' in 1..|V|: grad_v_w += error[c'] W'[c'] W'[c'] := W'[c'] - eta error[c'] v_w # update context vector c' W[w] := W[w] - eta grad_v_w # update center vector w return W, W' # keep W (or (W+W')/2) as the final word embeddings ``` Two implementation notes matter. First, updates to W′W' touch every row on every single training pair, because the softmax denominator involves all vocabulary words — this is what negative sampling later replaces with a handful of rows. Second, the two matrices are usually not tied; after training, practitioners typically discard W′W' and use WW (or the average 12(W+W′)\tfrac{1}{2}(W+W')) as the final embedding table, since WW is the representation that every word plays the "input" role for.

Figure
Skip-gram architecture: one center word predicts each context word independently
one-hot(w) input word "espresso" W |V|×d v_w (d-dim) W'ᵀ d×|V| P(·|w): "double" P(·|w): "shot" P(·|w): "machine" P(·|w): "cup" shared vector v_w scores against ALL |V| context rows, independently, for each window position

Complexity & correctness

Time complexity. For a single (center, context) training pair, the forward pass computes ∣V∣|V| dot products of length dd to build the softmax scores, costing O(∣V∣ d)O(|V|\,d); the backward pass touches every row of W′W' again for the same cost, O(∣V∣ d)O(|V|\,d). Over a corpus of TT tokens with window size mm, there are O(T m)O(T\,m) training pairs, so one epoch of naive skip-gram costs

O(T m ∣V∣ d).O(T\, m\, |V|\, d).

With realistic settings (∣V∣|V| in the hundreds of thousands, d∼300d \sim 300), this is the dominant cost and is prohibitive at web scale — this is exactly why practical word2vec systems never use the plain softmax described above, and instead use negative sampling (each update touches only k+1≪∣V∣k+1 \ll |V| rows, giving O(T m k d)O(T\,m\,k\,d)) or hierarchical softmax (a binary tree reduces the per-example cost to O(T m dlog⁡∣V∣)O(T\,m\,d\log|V|)). Those are the subjects of the companion article; the pseudocode above is the reference objective they approximate.

Space complexity is O(∣V∣ d)O(|V|\,d) for each of WW and W′W', i.e. O(∣V∣ d)O(|V|\,d) total, independent of corpus size — the parameter count depends only on vocabulary size and embedding dimension, which is what makes embeddings reusable across downstream tasks regardless of how large the training corpus was.

Correctness argument. Skip-gram is exact maximum-likelihood estimation of a well-defined probabilistic model, so "correctness" reduces to two claims: (1) the objective is a valid log-likelihood, and (2) gradient ascent on it is a sound optimization procedure. For (1): P(c∣w)P(c\mid w) as defined is a genuine categorical distribution over VV for each ww (nonnegative, sums to 1 by construction of softmax), so J(θ)J(\theta) is precisely the average log-likelihood of the observed (center, context) pairs under this model — the same object one maximizes in any softmax classifier. Because JJ is a sum of log-likelihoods of i.i.d.-style observations (treating each window pair as a sample), maximizing it is the standard MLE recipe; consistent parameter estimation follows under the usual regularity conditions, modulo the fact that the "labels" here are自generated from co-occurrence rather than ground truth. For (2): the per-pair loss ℓ\ell is convex in the score vector s\mathbf{s} (softmax cross-entropy is a convex function of its input logits), which is why the softmax gradient has the clean, well-behaved form probs−one-hot\text{probs} - \text{one-hot} derived above — this is the same gradient identity used in multinomial logistic regression, and it guarantees that each SGD step moves scores in a direction that provably decreases the per-example loss for a small enough step size. The full objective J(θ)J(\theta) is not jointly convex in (W,W′)(W, W') because the score is a bilinear (product) function of the two embedding matrices, so global convergence is not guaranteed — like most deep learning objectives, we only get convergence to a stationary point of a non-convex landscape. Empirically this stationary point is good: the vectors that come out satisfy analogical linear structure (vking−vman+vwoman≈vqueen\mathbf{v}_{\text{king}} - \mathbf{v}_{\text{man}} + \mathbf{v}_{\text{woman}} \approx \mathbf{v}_{\text{queen}}), which can be explained heuristically because the loss encourages uc⋅vw\mathbf{u}_c \cdot \mathbf{v}_w to track log⁡P(c∣w)Z(w)\log \frac{P(c\mid w)}{Z(w)}, i.e. embeddings end up organized around log co-occurrence ratios — the same statistical quantity that GloVe optimizes for directly and explicitly.

Honest tradeoff. Skip-gram (with negative sampling) tends to do better on rare words than CBOW, because it generates one training example per context word rather than averaging context vectors together, giving rare words more individual gradient signal — at the cost of more training pairs (and thus more compute) per pass over the same corpus.

Worked example

Take the toy sentence, window size m=1m=1, vocabulary V={the, cat, sat, on, mat}V = \{\text{the, cat, sat, on, mat}\}, ∣V∣=5|V|=5, embedding dim d=2d=2: > the cat sat on the mat Tokenized with positions: `[the(1), cat(2), sat(3), on(4), the(5), mat(6)]`.

Step 1 — generate (center, context) pairs with m=1m=1 (each word paired with its immediate left/right neighbor):

centercontext
the(1)cat
cat(2)the, sat
sat(3)cat, on
on(4)sat, the
the(5)on, mat
mat(6)the

This yields 10 training pairs total from a 6-token sentence — illustrating the O(Tm)O(Tm) pair count.

Step 2 — one gradient step in detail. Take the pair (center = "cat", context = "the"). Suppose after random initialization the relevant rows are:

vcat=(0.10,−0.20),uthe=(0.05,0.30),ucat=(0.20,0.10),usat=(−0.10,0.05),uon=(0.00,0.15),umat=(0.25,−0.05).\mathbf{v}_{\text{cat}} = (0.10, -0.20), \quad \mathbf{u}_{\text{the}} = (0.05, 0.30), \quad \mathbf{u}_{\text{cat}} = (0.20, 0.10), \quad \mathbf{u}_{\text{sat}} = (-0.10, 0.05), \quad \mathbf{u}_{\text{on}} = (0.00, 0.15), \quad \mathbf{u}_{\text{mat}} = (0.25, -0.05).

Compute raw scores sc′=uc′⋅vcats_{c'} = \mathbf{u}_{c'} \cdot \mathbf{v}_{\text{cat}}:

c′c'dot productsc′s_{c'}
the0.10×0.05+(−0.20)×0.300.10{\times}0.05 + (-0.20){\times}0.30−0.055-0.055
cat0.10×0.20+(−0.20)×0.100.10{\times}0.20 + (-0.20){\times}0.100.0000.000
sat0.10×(−0.10)+(−0.20)×0.050.10{\times}(-0.10) + (-0.20){\times}0.05−0.020-0.020
on0.10×0.00+(−0.20)×0.150.10{\times}0.00 + (-0.20){\times}0.15−0.030-0.030
mat0.10×0.25+(−0.20)×(−0.05)0.10{\times}0.25 + (-0.20){\times}(-0.05)0.0350.035

Softmax (using exp⁡\exp and normalizing; exponentials rounded to 4 dp): exp⁡(s)\exp(s): the =0.9465=0.9465, cat =1.0000=1.0000, sat =0.9802=0.9802, on =0.9704=0.9704, mat =1.0356=1.0356. Sum =4.8327= 4.8327.

P(the∣cat)=0.1959,  P(cat∣cat)=0.2069,  P(sat∣cat)=0.2028,P(\text{the}\mid\text{cat}) = 0.1959,\ \ P(\text{cat}\mid\text{cat}) = 0.2069,\ \ P(\text{sat}\mid\text{cat}) = 0.2028,
P(on∣cat)=0.2008,  P(mat∣cat)=0.2143.P(\text{on}\mid\text{cat}) = 0.2008,\ \ P(\text{mat}\mid\text{cat}) = 0.2143.

True label is "the", so the error vector (probs minus one-hot) is:

error=(0.1959−1, 0.2069, 0.2028, 0.2008, 0.2143)=(−0.8041, 0.2069, 0.2028, 0.2008, 0.2143).\text{error} = (0.1959 - 1,\ 0.2069,\ 0.2028,\ 0.2008,\ 0.2143) = (-0.8041,\ 0.2069,\ 0.2028,\ 0.2008,\ 0.2143).

The loss ℓ=−log⁡P(the∣cat)=−log⁡(0.1959)≈1.6304\ell = -\log P(\text{the}\mid\text{cat}) = -\log(0.1959) \approx 1.6304 — high, as expected before training, since the model has not yet learned that "the" should follow "cat". Gradient w.r.t. vcat\mathbf{v}_{\text{cat}} is ∑c′errorc′ uc′\sum_{c'} \text{error}_{c'}\, \mathbf{u}_{c'}, which (working through the sum) points mostly in the −uthe-\mathbf{u}_{\text{the}} direction because that error term dominates. Applying the update with η=0.1\eta = 0.1 pushes vcat\mathbf{v}_{\text{cat}} toward uthe\mathbf{u}_{\text{the}}'s direction and pushes uthe\mathbf{u}_{\text{the}} toward vcat\mathbf{v}_{\text{cat}} — exactly the intended effect: repeated exposure to "cat" and "the" co-occurring nudges their vectors closer, while umat\mathbf{u}_{\text{mat}} and the others (which had positive error, i.e. over-predicted) are nudged slightly away. Over thousands of such updates across the whole corpus, words sharing many context partners converge to nearby vectors.

Common pitfalls & traps

PitfallWhy it happensFix
Forgetting to exclude the center word itself from context pairsOff-by-one on window indices lets j=0j=0 slip throughExplicitly skip j=0j=0 when generating context offsets
Using WW and W′W' interchangeably during trainingThey play asymmetric roles (center vs. context) and mixing them breaks the gradient derivationKeep two separate matrices; only merge/average after training finishes
Treating softmax over ∣V∣|V| as "just a detail"Naive implementations silently become O(\V\) per step and never finish on real corporaBudget compute explicitly, or switch immediately to negative sampling / hierarchical softmax
Not subsampling frequent words ("the", "a", "of")Uninformative high-frequency pairs dominate gradients and drown out rare-word signalApply frequency-based subsampling before generating training pairs
Ignoring window edge effects at sentence/document boundariesWindows near position 1 or T reach outside valid indicesClip context indices to [1,T][1, T] and reduce effective window near boundaries
Assuming embeddings are directly interpretable per-dimensionDimensions are entangled linear combinations, not human conceptsOnly trust aggregate geometric properties (cosine similarity, analogy offsets), not single coordinates

Pioneers & history

Origins · 1 minTomas Mikolov and how this was discovered
TM
Tomas Mikolov
1985–present · Google Brain / word2vec, later Facebook AI Research

Tomas Mikolov led the development of word2vec while at Google, publishing the skip-gram and CBOW architectures and, crucially, the negative-sampling training trick that made them practical at scale. His earlier work on recurrent neural network language models set the stage for treating distributed word representations as a first-class object of study, and word2vec's release as a fast, open-source tool made dense embeddings a standard preprocessing step across the NLP industry almost overnight.

History
1954
— Harris and Firth articulate the distributional hypothesis: words in similar contexts have similar meanings.
2003
— Bengio et al. introduce neural probabilistic language models, learning word vectors as a side effect of language modeling.
2013
— Mikolov et al. publish skip-gram and CBOW (word2vec), with negative sampling making training feasible at web scale.
2014
— Pennington, Socher, and Manning release GloVe, an alternative embedding method based on global co-occurrence counts.
2018
— Contextual embeddings (ELMo, BERT) begin superseding static word2vec-style vectors for most downstream tasks.

Where this matters

Skip-gram embeddings (and the broader word2vec toolkit) became a near-universal preprocessing step in NLP pipelines throughout the mid-2010s: search engines used them for query expansion and semantic matching, recommendation systems embedded item and user tokens with the same architecture, and countless text-classification and named-entity-recognition systems bootstrapped their input layer from pretrained word2vec vectors rather than learning embeddings from scratch on small labeled datasets. The core idea — learn a representation by solving an auxiliary prediction task on unlabeled data — is the direct ancestor of pretraining objectives in modern contextual encoders and large language models, even though those models replace the shallow log-bilinear score with deep Transformer-style attention. Understanding skip-gram is the cleanest way to see the distributional hypothesis turned into a concrete, trainable, differentiable objective, which is why it remains the canonical worked example before moving on to Continuous Bag-of-Words (CBOW), Negative Sampling and Training Word2Vec, and GloVe.

Ask the tutor: How exactly does negative sampling replace the full softmax while still approximating the same probabilistic objective?

Is skip-gram supervised or unsupervised learning?

It is best described as self-supervised: no human ever labels the training examples, yet the model is trained with a genuine supervised objective (softmax cross-entropy against a known target). The "labels" — which word is the correct context — are mechanically extracted from raw text co-occurrence patterns using the sliding window. This distinction matters because it explains why skip-gram can exploit arbitrarily large unlabeled corpora, while still enjoying all the clean gradient machinery of ordinary supervised classifiers like softmax and logistic regression.

Why does skip-gram use two separate embedding matrices instead of one?

Separating the center-word matrix W from the context-word matrix W' avoids a degenerate solution where a word is forced to have high dot product with itself, since P(w|w) would otherwise be pushed toward an artificially large value whenever a word appears near itself in overlapping windows. It also gives the model more capacity: a word's role as "the thing being described" and its role as "a clue describing something else" can differ. In practice, many implementations discard W' after training and just use W, or average the two, since both encode overlapping semantic information.

How does skip-gram differ from CBOW?

Skip-gram predicts each context word individually from a single center word, generating one training example per (center, context) pair inside a window. Continuous Bag-of-Words (CBOW) does the reverse: it averages (or sums) the embeddings of all context words and predicts the single center word from that combined representation. Because skip-gram creates more distinct training signals per occurrence of a rare word, it tends to represent infrequent words better, while CBOW trains faster since it processes one combined example per window rather than one per context word.

What replaces the expensive full softmax in real word2vec training?

Two standard tricks exist. Negative sampling reframes each update as several small binary logistic regression problems — distinguish the true context word from a handful of randomly sampled "negative" words — so an update touches only k+1 rows instead of the entire vocabulary. Hierarchical softmax instead arranges the vocabulary as a binary tree (often a Huffman tree) and computes the probability of a word as a product of binary decisions along the tree path, reducing cost to O(log|V|) per example. Negative sampling is covered in detail in the companion article on training tricks.

Why do skip-gram embeddings support analogies like king − man + woman ≈ queen?

The training objective pushes the dot product u_c · v_w to approximate the log of how much more often c and w co-occur than chance would predict. When two words share a systematic relationship (like gender pairs, or singular/plural pairs), the log co-occurrence ratios shift in a consistent direction across many word pairs, and because the score function is linear (a dot product), consistent ratio shifts translate into consistent vector offsets. This is the same underlying statistical quantity that GloVe models explicitly and directly, rather than implicitly through a softmax classifier.

Can skip-gram handle words never seen during training?

No — plain skip-gram, like most word-level embedding methods, has a fixed vocabulary fixed at training time and produces no vector for out-of-vocabulary words. Common mitigations include mapping rare/unseen words to a shared UNK vector, falling back to subword-based models such as fastText that build word vectors from character n-grams, or moving to contextual encoders that tokenize with subword units and handle novel words gracefully. This limitation is one of the main reasons static word embeddings were eventually superseded by contextual, subword-based architectures.

Is skip-gram still used in modern NLP systems?

Direct use of static word2vec embeddings has declined in favor of contextual embeddings from large pretrained models, but skip-gram remains pedagogically central and practically relevant in lower-resource settings, as a fast baseline, and in domains like recommendation systems and graph embeddings (e.g., node2vec, item2vec) where the same skip-gram objective is reused verbatim on non-text sequences. Understanding its objective and gradient is also the cleanest on-ramp to understanding contrastive and self-supervised pretraining objectives used throughout modern deep learning.

Before this

Where this leads