Majid Al-RaimiLearning skip-gram embeddings with negative sampling

ICS 582Lecture 04Part 08

Learning skip-gram embeddings with negative sampling

The two embedding matrices W and C, how positive and k negative training pairs are built, the cross-entropy loss for SGNS, its gradients, and the SGD updates that pull true neighbors together and push sampled noise apart.

Concepts
6
Slides
75-88
Reading
36 min
Understood
0/6 concepts

Why this part matters

Part 07 built a classifier, P(+ | w, c) = σ(c · w), that scores whether c is a real neighbor of w. It assumed the vectors already existed. This part is where they come from: the Skip-gram with negative sampling training loop that starts from random numbers and, one window at a time, turns them into embeddings in which apricot sits near jam.

Three reasons to know this cold. The Stochastic gradient descent update for skip-gram is a standard exam derivation (it is SLP3 Exercise 5.3). The same pattern of positive and noise pairs, dot-product scores and a cross-entropy loss is the backbone of contrastive learning in modern retrieval and sentence embedding models, so it shows up in research papers well beyond word2vec. And when you train or debug real embeddings, the defaults matter: k = 5 negatives, a 0.75 exponent on the noise distribution, and a learning rate that starts at 0.025 and decays.

By the end you can

  1. Describe θ as W stacked over C, with 2|V| rows of dimension d, and say what each matrix is for.
  2. Build positive pairs from a ±2 window and draw k noise words from the α = 0.75 weighted unigram.
  3. Derive the SGNS cross-entropy loss from the independence assumption and σ(−x) = 1 − σ(x).
  4. Derive the three gradients and apply one SGD update by hand.
  5. Explain why W and C are trained separately and then summed or reduced to W alone.

Take a vocabulary of 10,000 words and embeddings of dimension d = 300. How many numbers does Skip-gram learn? The natural guess is one vector per word, so 3,000,000. The real answer is twice that, 6,000,000, because every word owns two rows.

The parameters θ are two matrices stacked on top of each other. The top block W has one row per vocabulary word, indexed 1 to |V|; these are the target embeddings, used when a word sits at the center of a window. The bottom block C has another row per word, indexed |V| + 1 to 2|V|; these are the context embeddings, used when a word appears as a neighbor or is drawn as a noise word. So apricot has a row w_apricot in W and a separate row c_apricot in C, and together these are the Target and context matrices. SLP3 (Fig. 5.6) calls the W rows the input embeddings and the C rows the output embeddings, which is also the vocabulary of Mikolov et al. and of Rong's derivation notes.

θ=[WC]∈R2∣V∣×d\theta=\begin{bmatrix}W\\C\end{bmatrix}\in\mathbb{R}^{2|V|\times d}
All SGNS parameters: 2|V| dense vectors of dimension d

What each block of θ holds

W, rows 1 to |V|
Target embeddings (also called input embeddings). Row w_i is used when word i sits at the center of a window.
C, rows |V| + 1 to 2|V|
Context embeddings (also called output embeddings). Row c_i is used when word i is a neighbor or a sampled noise word.
Shape of θ
2|V| × d
Size for |V| = 10,000, d = 300
2 × 10,000 × 300 = 6,000,000 parameters
What the classifier reads
Only dot products c · w, with c taken from C and w taken from W. A row of W is never dotted with another row of W.

The reason for two tables becomes clear once you look at what the classifier from part 07 actually computes. It only ever takes a Dot product between a context row and a target row, c · w. A target never meets another target, and a context never meets another context. W and C are therefore two different roles, and the model is free to learn different coordinates for each role. Every row is a Dense vector of length d; nothing here is sparse or counted.

θ as one tall column: W (targets) over C (contexts). apricot owns a row in each band, and after training the two rows are usually added into one vector.

Recall

How many vectors does θ hold, and how many parameters is that for |V| = 10,000 and d = 300?

2|V| vectors of size d, so 2 × 10,000 × 300 = 6,000,000.

Take the sentence fragment "lemon, a tablespoon of apricot jam, a pinch" with apricot as the target and a ±2 window. The four words inside the window give four positive pairs: (apricot, tablespoon), (apricot, of), (apricot, jam) and (apricot, a). No human labeled them; the corpus did, which is the Self-supervision idea from part 07.

A classifier trained only on positives would learn to say "yes" to everything. So each positive pair is matched with k negative pairs, built by keeping the target and replacing the context with a noise word drawn at random from the lexicon. With k = 2 the four positives above get eight negatives, for example aardvark, my, where, coaxial, seven, forever, dear and if. The slide lists these eight as one pool, so the way the table below assigns two to each positive is only illustrative. This is Negative sampling: the model learns to tell real neighbors from random words, rather than to predict the exact neighbor out of all |V| words.

Positive context (label 1)Two noise words drawn for it (label 0, illustrative)
tablespoonaardvark, my
ofwhere, coaxial
jamseven, forever
adear, if
Training pairs for target apricot, window ±2, k = 2

Which random words? The flattened unigram

Noise words are not drawn uniformly, and not quite in proportion to frequency either. They are drawn from the unigram distribution raised to the power α = 0.75 and renormalized, the same Alpha-weighted context probability trick part 06 used to stop Pointwise mutual information from overrating rare contexts. The only constraint SLP3 imposes is that the noise word is not the target itself.

Pα(w)=count(w)α∑w′count(w′)α,α=0.75P_\alpha(w)=\frac{\mathrm{count}(w)^{\alpha}}{\sum_{w'}\mathrm{count}(w')^{\alpha}},\qquad \alpha=0.75
SLP3 eq. 5.19: the weighted unigram used to draw noise words

Raising probabilities to a power below one compresses their range. Frequent words lose a little mass, rare words gain proportionally much more, so the model sees rare words as negatives often enough to learn something about them, while frequent function words still dominate. The two-word example from part 06 (slide 60, SLP3 eq. 5.20) applies unchanged: with P(a) = 0.99 and P(b) = 0.01, the rare word roughly triples its chance of being drawn, to about 0.03, while a drops only to 0.97. The arithmetic is identical; what changes is the role. In part 06 the flattened distribution sat in a PMI denominator, here it decides which words are sampled as noise.

Five words from a Zipf-like unigram. Raising to the power 0.75 and renormalizing shrinks the head (0.60 to 0.51) and lifts the tail (0.01 to 0.024).

Mikolov et al. (2013) report that U(w)^{3/4} / Z "outperformed significantly" both the raw unigram and the uniform distribution. They recommend k between 5 and 20 for small datasets and 2 to 5 for large ones. The reference C code and gensim both default to negative = 5 and an exponent of 0.75 (gensim calls it ns_exponent). Levy, Goldberg and Dagan (2015) named the same move context distribution smoothing and showed it consistently improves count-based PMI vectors too, not just word2vec.

Recall

How are noise words chosen, and what does α = 0.75 do?

They are drawn from count(w)^0.75, normalized, and never the target itself. Rare words gain probability (0.01 becomes about 0.03) and frequent ones lose a little.

Quick check

Why does SGNS draw noise words from the unigram raised to 0.75?

Fix one training instance: target w = apricot, positive context c_pos = jam, and k = 2 noise words matrix and Tolstoy. The loss for this instance is

L=−[log⁡σ(cjam⋅w)+log⁡σ(−cmatrix⋅w)+log⁡σ(−cTolstoy⋅w)]\begin{aligned} L=-\Big[&\log\sigma(\mathbf{c}_{jam}\cdot\mathbf{w}) \\ &+\log\sigma(-\mathbf{c}_{matrix}\cdot\mathbf{w}) \\ &+\log\sigma(-\mathbf{c}_{Tolstoy}\cdot\mathbf{w})\Big] \end{aligned}

The first term is small when the classifier is confident that jam is a neighbor. Each of the other two is small when the classifier is confident that the noise word is not. Where does this shape come from? We want the classifier to give high probability to the correct label of every pair in the instance: label + for the positive and label − for each noise word. Treating the k + 1 decisions as independent, and minimizing the negative log of their joint probability, gives the cross-entropy loss in four short steps.

LCE=−log⁡[P(+∣w,cpos)×∏i=1kP(−∣w,cnegi)]=−[log⁡P(+∣w,cpos)+∑i=1klog⁡P(−∣w,cnegi)]=−[log⁡P(+∣w,cpos)+∑i=1klog⁡(1−P(+∣w,cnegi))]=−[log⁡σ(cpos⋅w)+∑i=1klog⁡σ(−cnegi⋅w)]\begin{aligned} L_{CE}&=-\log\Big[P(+\mid w,c_{pos})\\&\qquad\times\prod_{i=1}^{k}P(-\mid w,c_{neg_i})\Big]\\ &=-\Big[\log P(+\mid w,c_{pos})\\&\qquad+\sum_{i=1}^{k}\log P(-\mid w,c_{neg_i})\Big]\\ &=-\Big[\log P(+\mid w,c_{pos})\\&\qquad+\sum_{i=1}^{k}\log\big(1-P(+\mid w,c_{neg_i})\big)\Big]\\ &=-\Big[\log\sigma(\mathbf{c}_{pos}\cdot\mathbf{w})\\&\qquad+\sum_{i=1}^{k}\log\sigma(-\mathbf{c}_{neg_i}\cdot\mathbf{w})\Big] \end{aligned}
SLP3 eq. 5.21: independence, log of a product, P(−) = 1 − P(+), and 1 − σ(x) = σ(−x)
  1. Independence turns the joint probability of the k + 1 labels into a product, and the minus log turns maximizing probability into minimizing a loss.
  2. The log of a product is a sum of logs.
  3. A pair is either a neighbor or not, so P(− | w, c) = 1 − P(+ | w, c).
  4. Plug in P(+ | w, c) = σ(c · w) from part 07 and use the Sigmoid identity 1 − σ(x) = σ(−x).

The negative terms are not decoration. Goldberg and Levy (2014) point out that with positives alone the objective has a trivial solution: make every vector the same, with a large enough norm that every dot product is huge, and every positive gets probability 1 (they note this happens once the dot product reaches about 40). Such vectors are useless, since every word looks like every other. The noise terms make that collapse expensive, because identical vectors would also give every noise pair probability 1.

Recall

Write the SGNS loss for one target w with one positive and k negatives.

L = −[log σ(c_pos · w) + Σ_{i=1..k} log σ(−c_neg_i · w)], which is minimized.

Take the instance from the previous concept, "...apricot jam..." with k = 2 noise words matrix and Tolstoy. One learning step does three things at once. It moves apricot's target vector w and jam's context vector c_jam toward each other, so c_pos · w rises. It moves w and c_matrix apart, and w and c_Tolstoy apart, so both c_neg · w values fall. Every other row of θ, including aardvark and zebra in both W and C, is left exactly as it was.

One SGD step for apricot with k = 2: w and jam's context vector move toward each other (c·w rises) while matrix and Tolstoy drift away (c·w falls).

The rule behind the picture is gradient descent. The gradient ∇_θ L is the vector of partial derivatives of the loss with respect to every parameter; it points in the direction in which the loss grows fastest. To reduce the loss, step the opposite way, by an amount scaled by the Learning rate η.

θt+1=θt−η ∇θL(θt)\theta^{t+1}=\theta^{t}-\eta\,\nabla_{\theta}L(\theta^{t})
One gradient descent step. SLP3: we minimize the loss using stochastic gradient descent.

"Stochastic" means the gradient is computed from one training instance (or a small batch) at a time, not from the whole corpus. The training loop walks through the corpus, takes each target and each of its window positives, samples k noise words, computes the loss for that small instance, and steps. Because the loss of one instance involves only w, c_pos and the k noise contexts, the gradient is zero for every other row, and the update touches just 2 + k rows out of 2|V|. That sparsity is what makes word2vec fast enough to train on billions of tokens.

Recall

Which rows of θ change after one (w, c_pos) step with k = 2?

Four: w in W, and c_pos, c_neg1 and c_neg2 in C.

Quick check

For one positive pair with k = 2, which vectors does a single SGD step change?

The fastest way to trust the update equations is to run one by hand. Use tiny vectors so every number is visible, then read the general rule off the arithmetic.

Worked example

One SGD step in two dimensions

  1. Setup

    d = 2, η = 0.5, w = (1, 0), c_pos = (0, 1) for jam, c_neg1 = (1, 1) for matrix, c_neg2 = (0.5, −1) for Tolstoy.
  2. Dot products and sigmoids

    c_pos · w = 0, c_neg1 · w = 1, c_neg2 · w = 0.5. So σ(0) = 0.5, σ(1) = 0.731, σ(0.5) = 0.622. The classifier is unsure about jam and wrongly leans toward calling both noise words neighbors.
  3. Loss before the step

    L = −[log 0.5 + log(1 − 0.731) + log(1 − 0.622)] = 0.693 + 1.313 + 0.974 ≈ 2.980.
  4. Gradients (all at time t)

    Each gradient is (σ(c · w) − y) times the partner vector, with y = 1 for jam and y = 0 for noise words; the derivation follows below. ∂L/∂c_pos = (0.5 − 1) w = (−0.5, 0); ∂L/∂c_neg1 = 0.731 w = (0.731, 0); ∂L/∂c_neg2 = 0.622 w = (0.622, 0); ∂L/∂w = −0.5 c_pos + 0.731 c_neg1 + 0.622 c_neg2 = (1.042, −0.391).
  5. Apply θ − η ∇L

    c_pos = (0.25, 1.0), c_neg1 = (0.634, 1.0), c_neg2 = (0.189, −1.0), w = (0.479, 0.196).
  6. Check the effect

    New dot products: c_pos · w = 0.315 (up from 0), c_neg1 · w = 0.500 (down from 1), c_neg2 · w = −0.105 (down from 0.5).
  7. Result

    The loss falls from 2.980 to 2.163 in one step. The positive was pulled up and both negatives were pushed down, exactly the picture in the previous concept.

Where the gradients come from

Two derivative facts do all the work, both about the Sigmoid derivative. From dσ/dz = σ(z)(1 − σ(z)) it follows that d/dz log σ(z) = 1 − σ(z) and d/dz log σ(−z) = −σ(z). Apply them with the chain rule, remembering that z = c · w has derivative w with respect to c and c with respect to w, and the minus sign in front of the loss flips the signs.

∂LCE∂cpos=[σ(cpos⋅w)−1]w∂LCE∂cnegi=[σ(cnegi⋅w)]w∂LCE∂w=[σ(cpos⋅w)−1]cpos+∑i=1k[σ(cnegi⋅w)]cnegi\begin{aligned} \frac{\partial L_{CE}}{\partial \mathbf{c}_{pos}}&=\big[\sigma(\mathbf{c}_{pos}\cdot\mathbf{w})-1\big]\mathbf{w}\\ \frac{\partial L_{CE}}{\partial \mathbf{c}_{neg_i}}&=\big[\sigma(\mathbf{c}_{neg_i}\cdot\mathbf{w})\big]\mathbf{w}\\ \frac{\partial L_{CE}}{\partial \mathbf{w}}&=\big[\sigma(\mathbf{c}_{pos}\cdot\mathbf{w})-1\big]\mathbf{c}_{pos}\\&\quad+\sum_{i=1}^{k}\big[\sigma(\mathbf{c}_{neg_i}\cdot\mathbf{w})\big]\mathbf{c}_{neg_i} \end{aligned}
SLP3 eqs. 5.22 to 5.24

All three have the same shape: (prediction minus label) times the other vector. Write y = 1 for a positive pair and y = 0 for a noise pair; each gradient is (σ(c · w) − y) times the partner vector. The reference word2vec.c computes exactly this as g = (label − sigmoid) * alpha, folding the learning rate and the sign into one number. Plugging the gradients into θ^{t+1} = θ^t − η ∇L gives the three update rules.

cpost+1=cpost−η[σ(cpost⋅wt)−1]wtcnegit+1=cnegit−η[σ(cnegit⋅wt)]wtwt+1=wt−η[[σ(cpost⋅wt)−1]cpost+∑i=1k[σ(cnegit⋅wt)]cnegit]\begin{aligned} \mathbf{c}_{pos}^{t+1}&=\mathbf{c}_{pos}^{t}-\eta\big[\sigma(\mathbf{c}_{pos}^{t}\cdot\mathbf{w}^{t})-1\big]\mathbf{w}^{t}\\ \mathbf{c}_{neg_i}^{t+1}&=\mathbf{c}_{neg_i}^{t}-\eta\big[\sigma(\mathbf{c}_{neg_i}^{t}\cdot\mathbf{w}^{t})\big]\mathbf{w}^{t}\\ \mathbf{w}^{t+1}&=\mathbf{w}^{t}-\eta\Big[\big[\sigma(\mathbf{c}_{pos}^{t}\cdot\mathbf{w}^{t})-1\big]\mathbf{c}_{pos}^{t}\\&\qquad+\sum_{i=1}^{k}\big[\sigma(\mathbf{c}_{neg_i}^{t}\cdot\mathbf{w}^{t})\big]\mathbf{c}_{neg_i}^{t}\Big] \end{aligned}
SLP3 eqs. 5.25 to 5.27, with every right-hand side at time t
ParameterLabel yGradientDirection of the update
c_pos1[σ(c_pos·w) − 1] wToward w (the factor is negative)
c_neg_i0σ(c_neg_i·w) wAway from w
wboth[σ(c_pos·w) − 1] c_pos + Σ σ(c_neg_i·w) c_neg_iToward c_pos, away from each c_neg_i
The three gradients as (σ − y) times the partner vector
Step size equals error. jam (label 1) at σ = 0.50 has an error bar of 0.50 up to 1; matrix (label 0) at σ = 0.73 has a long bar down to 0; a noise word already at σ = 0.02 barely moves.

Recall

Derive ∂L/∂c_pos.

d/dc [−log σ(c · w)] = −(1 − σ(c · w)) w = [σ(c_pos · w) − 1] w.

Quick check

The update for a negative context is η·σ(c_neg·w)·w. When is it nearly zero?

When training ends, apricot still has two rows, w_apricot and c_apricot. SLP3 says the common choice is to add them and represent apricot by w_apricot + c_apricot; the alternative is to keep only w_apricot and throw C away. Either way the result is a Static embedding: one fixed vector per word type, compared with Cosine similarity.

Why keep the tables apart during training at all? Goldberg and Levy (2014) give the argument. Suppose dog had a single vector v used in both roles. Then the score for the pair (dog, dog) would be v · v = |v|², which is large for any vector with a large norm, so the model would believe dog is its own most likely neighbor. Real text rarely says "dog dog", so the model would have to keep word norms small to avoid that, fighting its own objective. Two tables remove the conflict.

Why does adding them afterwards help? Levy, Goldberg and Dagan (2015) show that the cosine of two summed vectors includes terms like w_x · c_y, which measure whether x and y appear in each other's contexts. Adding context vectors therefore adds first-order similarity (co-occurrence) to the second-order similarity (shared neighbors) that W alone captures. The trick comes from GloVe, whose authors summed the two sets of vectors as a cheap way of combining two models, much like an ensemble.

OptionVector for word iWhat it capturesIn practice
Keep W onlyw_iSecond-order similarity: two words are close when they have similar neighborsSmallest vectors; the default in gensim's wv
Sumw_i + c_iAdds first-order similarity terms: words that co-occur also get closerSLP3's common choice; behaves like an ensemble
Concatenate[w_i ; c_i]Keeps both views separately, doubling the dimensionRarely used; costs 2d per word
Turning the two trained tables into one vector per word

The whole recipe

  1. Initialize W and C randomly: 2|V| vectors of dimension d. (word2vec.c draws W uniformly in ±0.5 / d and starts C at zeros.)
  2. Slide a window over the corpus. Each (target, neighbor) pair is a positive; for each, draw k noise words from P_α as negatives.
  3. Train the logistic classifier σ(c · w) to separate positives from negatives, minimizing L_CE with SGD.
  4. Throw the classifier away and keep the learned rows as the embeddings, summed or W only.

Step 4 is the heart of Self-supervision: the yes-or-no prediction task was never the goal. It was a pretext that forced the model to place words with similar contexts close together, and the Embedding is the by-product we wanted.

The same θ column at the end of training: the apricot row in W and the apricot row in C are drawn out and merged into one w + c vector.

Recall

Why keep W and C separate during training, then sum them?

One shared vector would force a high v · v, so a word would look like its own neighbor. Summing afterwards adds first-order similarity and works like an ensemble.

Quick check

Why does SGNS train separate W and C tables instead of one vector per word?

Recap

If you remember nothing else

  • θ = [W; C]: 2|V| vectors of size d. W holds targets and C holds contexts and noise words.
  • Each window positive gets k noise words from P_α(w) ∝ count(w)^0.75, never the target itself.
  • L_CE = −[log σ(c_pos·w) + Σ log σ(−c_neg_i·w)], minimized with SGD.
  • The gradients are (σ − y) times the other vector: [σ(c_pos·w) − 1]w, σ(c_neg·w)w, and the combined sum for w.
  • Updates take the form θ^{t+1} = θ^t − η∇L. Use time-t values throughout. Only 2 + k rows change per pair.
  • After training, discard the classifier and keep w_i + c_i, or w_i alone.

Sources