AI-340 · Lesson 2 of 12 · 38 min

Word embeddings

Lesson 1 represented words as separate symbols: "cat" and "kitten" had nothing in common, so a model that learned something about one learned nothing about the other. Word embeddings fix this by representing each word as a dense vector of a few hundred real numbers, arranged so that words with similar meanings have similar vectors. Embeddings learned from raw text were one of the breakthroughs that made deep learning succeed in NLP, and every modern language model begins by mapping each token to an embedding vector. This lesson explains the idea behind embeddings, how count-based and predictive methods learn them, what they capture, how they encode social biases, and how static embeddings gave way to contextual ones.

The distributional hypothesis

The linguist J. R. Firth summarized the key idea in 1957: "You shall know a word by the company it keeps." Words that occur in similar contexts tend to have similar meanings. If we have never seen the word "tezgüino" but read that "a bottle of tezgüino is on the table", "everybody likes tezgüino" and "tezgüino makes you drunk", we can infer that it is an alcoholic drink, because those contexts are shared with words like "beer" and "wine". Embedding methods turn this intuition into computation: they represent each word by statistics of its contexts, compressed into a short vector.

A one-hot vector represents word \(i\) of a vocabulary of size \(V\) as a vector of \(V\) zeros with a single 1 in position \(i\). All one-hot vectors are orthogonal, so every pair of words has similarity zero. A dense embedding instead uses \(d\) dimensions, with \(d\) typically 50 to 1000, far smaller than \(V\), and every dimension is used by every word. An embedding matrix \(E\) of shape \(V \times d\) stores one row per word; multiplying a one-hot vector by \(E\) simply selects that word's row, which is why embedding layers are implemented as table lookups.

Comparison of one-hot and dense word representations. On the left, cat, kitten and truck are each a long vector of zeros with a single one in a different position, and all pairwise similarities are zero. On the right, each word is a short vector of real numbers shown as colored cells; cat and kitten have similar patterns and a similarity of 0.92, while cat and truck have a similarity of 0.05.
One-hot vectors make all words equally different; dense embeddings place similar words close together.

Count-based embeddings

The direct way to apply the distributional hypothesis is to count. A word-context co-occurrence matrix \(C\) has one row per word and one column per context word, with \(C_{wc}\) the number of times \(c\) appears within a window (say two words either side) of \(w\). Raw counts are dominated by frequent words such as "the", so they are reweighted by pointwise mutual information (PMI), which compares how often two words occur together with how often they would if they were independent:

\[\text{PMI}(w, c) = \log \frac{P(w, c)}{P(w)\,P(c)}\]

PMI is positive when the words co-occur more than chance, zero when independent, and negative when they avoid each other. Negative values are unreliable for rare pairs, so positive PMI (PPMI) replaces them with zero. The resulting matrix is still \(V \times V\) and sparse; a truncated singular value decomposition (AI-210) keeps the top \(k\) singular vectors, giving \(k\)-dimensional dense vectors. This approach, known as latent semantic analysis when applied to word-document matrices, captures the main patterns of co-occurrence in a few dimensions.

Worked problem

PMI by hand

In a corpus of 1,000,000 word-context pairs, "coffee" appears in 2,000 pairs, "cup" in 5,000 pairs, and they co-occur in 150 pairs. Compute PMI (natural log). How would it change if they co-occurred only 10 times?

Show solution
  1. \(P(w,c) = 150/10^6 = 1.5 \times 10^{-4}\); \(P(w) = 0.002\); \(P(c) = 0.005\).
  2. Independent expectation: \(0.002 \times 0.005 = 10^{-5}\).
  3. PMI \(= \ln(1.5 \times 10^{-4}/10^{-5}) = \ln 15 = 2.71\).
  4. With 10 co-occurrences: \(\ln(10^{-5}/10^{-5}) = \ln 1 = 0\).

Answer: "coffee" and "cup" co-occur 15 times more often than chance (PMI 2.71). With only 10 co-occurrences they would be exactly independent (PMI 0), and below that PMI would be negative and set to zero in PPMI.

PPMI and SVD embeddings from a synthetic corpus: nearest neighbors by cosine similarity.
import numpy as np

rng = np.random.default_rng(0)
animals = ["cat", "dog", "horse", "cow"]
vehicles = ["car", "truck", "bus", "van"]
foods = ["bread", "rice", "apple", "cheese"]
templates = [("the", "A", "ate", "the", "F"), ("the", "A", "slept", "in", "the", "barn"),
             ("she", "drove", "the", "V", "to", "town"), ("the", "V", "needs", "fuel"),
             ("we", "cooked", "the", "F", "for", "dinner"), ("the", "F", "was", "fresh")]
fill = {"A": animals, "V": vehicles, "F": foods}
corpus = []
for _ in range(3000):
    t = templates[rng.integers(len(templates))]
    corpus.append([str(rng.choice(fill[w])) if w in fill else w for w in t])

vocab = sorted({w for s in corpus for w in s})
ix = {w: i for i, w in enumerate(vocab)}
C = np.zeros((len(vocab), len(vocab)))
for s in corpus:                               # window of 2 words each side
    for i, w in enumerate(s):
        for j in range(max(0, i - 2), min(len(s), i + 3)):
            if j != i:
                C[ix[w], ix[s[j]]] += 1

total = C.sum()
pw = C.sum(axis=1, keepdims=True) / total
pc = C.sum(axis=0, keepdims=True) / total
with np.errstate(divide="ignore"):
    pmi = np.log((C / total) / (pw * pc))
ppmi = np.maximum(pmi, 0)

U, S, Vt = np.linalg.svd(ppmi)
k = 5
E = U[:, :k] * S[:k]                           # 5-dimensional word vectors
E /= np.linalg.norm(E, axis=1, keepdims=True)

print("vocabulary:", len(vocab), " co-occurrence matrix:", C.shape, " vectors:", E.shape)
for w in ["cat", "truck", "rice", "ate"]:
    sims = E @ E[ix[w]]
    best = [vocab[i] for i in np.argsort(-sims) if vocab[i] != w][:3]
    print(f"{w:6s} nearest: {best}")
print("cos(cat, dog) =", round(float(E[ix["cat"]] @ E[ix["dog"]]), 2),
      "  cos(cat, truck) =", round(float(E[ix["cat"]] @ E[ix["truck"]]), 2))
Output
vocabulary: 29  co-occurrence matrix: (29, 29)  vectors: (29, 5)
cat    nearest: ['cow', 'horse', 'dog']
truck  nearest: ['bus', 'van', 'car']
rice   nearest: ['bread', 'cheese', 'apple']
ate    nearest: ['the', 'cooked', 'in']
cos(cat, dog) = 1.0   cos(cat, truck) = 0.02

The corpus was generated from templates in which animals eat and sleep, vehicles are driven and need fuel, and food is cooked and eaten. Nothing told the method about these categories, yet with only five dimensions each word's nearest neighbors are the other members of its category: the animals share contexts like "ate" and "barn", the vehicles share "drove" and "fuel". The cosine similarity of "cat" and "dog" rounds to 1.0 because in this artificial corpus they appear in exactly the same kinds of contexts, while "cat" and "truck" are nearly orthogonal. Verbs such as "ate" have less clear neighbors because each occupies its own slot in the templates.

Scatter plot of the word vectors from the synthetic corpus projected to two dimensions. Animal words cat, dog, horse and cow form one tight cluster, vehicle words car, truck, bus and van another, and food words bread, rice, apple and cheese a third, with function words and verbs scattered between the clusters.
Word vectors projected to two dimensions cluster by meaning.

Predictive embeddings: word2vec

In 2013, Mikolov and colleagues at Google introduced word2vec, which learns embeddings by training a simple model to predict context, rather than counting. The skip-gram variant predicts each context word from the center word; the continuous bag-of-words (CBOW) variant predicts the center word from the average of its context. Skip-gram with negative sampling (SGNS) keeps two vectors per word, a target vector \(\mathbf{w}\) and a context vector \(\mathbf{c}\), and trains a logistic classifier to tell real (word, context) pairs from random "negative" pairs:

\[\mathcal{L} = -\log \sigma(\mathbf{c}_{pos} \cdot \mathbf{w}) - \sum_{k=1}^{K} \log \sigma(-\mathbf{c}_{neg,k} \cdot \mathbf{w})\]

where \(\sigma(x) = 1/(1 + e^{-x})\) and the \(K\) negative words (typically 5 to 20) are sampled from the unigram distribution raised to the 3/4 power, which samples rare words a little more often than their frequency. Minimizing this loss pushes the target vector toward the vectors of words that really appear near it and away from random words. Because each update touches only \(K + 1\) context vectors instead of the whole vocabulary, training is fast enough to process billions of words. Other refinements include subsampling very frequent words and dynamic window sizes.

Diagram of skip-gram with negative sampling. In the sentence the dog slept in the barn, the center word slept is highlighted and the two words either side form the context window. Positive pairs link slept with dog and in; negative pairs link slept with randomly sampled words such as fuel and town. A logistic classifier scores each pair with the sigmoid of the dot product of the target and context vectors, with targets 1 for positive pairs and 0 for negative ones.
Skip-gram with negative sampling learns vectors by classifying real and random word pairs.
Skip-gram with negative sampling trained from scratch with stochastic gradient descent.
import numpy as np

rng = np.random.default_rng(1)
animals = ["cat", "dog", "horse", "cow"]
vehicles = ["car", "truck", "bus", "van"]
foods = ["bread", "rice", "apple", "cheese"]
templates = [("the", "A", "ate", "the", "F"), ("the", "A", "slept", "in", "the", "barn"),
             ("she", "drove", "the", "V", "to", "town"), ("the", "V", "needs", "fuel"),
             ("we", "cooked", "the", "F", "for", "dinner"), ("the", "F", "was", "fresh")]
fill = {"A": animals, "V": vehicles, "F": foods}
corpus = [[str(rng.choice(fill[w])) if w in fill else w for w in templates[rng.integers(6)]]
          for _ in range(3000)]
vocab = sorted({w for s in corpus for w in s})
ix = {w: i for i, w in enumerate(vocab)}
pairs = [(ix[s[i]], ix[s[j]]) for s in corpus for i in range(len(s))
         for j in range(max(0, i - 2), min(len(s), i + 3)) if j != i]
counts = np.bincount([c for _, c in pairs], minlength=len(vocab)).astype(float)
noise = counts ** 0.75; noise /= noise.sum()      # negative-sampling distribution

d, K, lr = 10, 5, 0.025
W = rng.normal(0, 0.1, (len(vocab), d))           # target (input) vectors
Cv = np.zeros((len(vocab), d))                    # context (output) vectors
sig = lambda x: 1 / (1 + np.exp(-x))

order = np.concatenate([rng.permutation(len(pairs)) for _ in range(2)])
block = len(order) // 6
loss = 0.0
for step, n in enumerate(order, 1):
    t, c = pairs[n]
    negs = rng.choice(len(vocab), K, p=noise)
    ctx = np.concatenate(([c], negs))
    label = np.zeros(K + 1); label[0] = 1
    s = sig(Cv[ctx] @ W[t])
    g = s - label                                  # gradient of logistic loss
    loss -= np.log(s[0] + 1e-9) + np.log(1 - s[1:] + 1e-9).sum()
    gw = g @ Cv[ctx]
    Cv[ctx] -= lr * np.outer(g, W[t])
    W[t] -= lr * gw
    if step % block == 0:
        print(f"after {step:6d} pairs: mean loss {loss / block:.3f}")
        loss = 0.0

E = W / np.linalg.norm(W, axis=1, keepdims=True)
for w in ["dog", "bus", "cheese"]:
    sims = E @ E[ix[w]]
    print(f"{w:7s}", [vocab[i] for i in np.argsort(-sims)[1:4]])
Output
after  14752 pairs: mean loss 2.595
after  29504 pairs: mean loss 1.994
after  44256 pairs: mean loss 1.953
after  59008 pairs: mean loss 1.974
after  73760 pairs: mean loss 1.985
after  88512 pairs: mean loss 2.008
dog     ['cow', 'cat', 'horse']
bus     ['van', 'car', 'truck']
cheese  ['rice', 'bread', 'apple']

The loss falls quickly in the first block of training pairs and then levels off around 2.0; it cannot fall to zero, because many words share contexts and the negative samples sometimes include true context words. The learned target vectors again place each word near the other members of its category. Levy and Goldberg showed in 2014 that SGNS implicitly factorizes a shifted PMI matrix, so count-based and predictive methods are closer than they appear; in practice, careful tuning matters as much as the choice of method.

Worked problem

One SGNS step

A target vector is \(\mathbf{w} = (0.5, -0.2)\), a true context vector \(\mathbf{c}_+ = (0.4, 0.1)\) and a negative context vector \(\mathbf{c}_- = (0.3, -0.6)\). Compute the loss and the gradient with respect to \(\mathbf{w}\).

Show solution
  1. \(\mathbf{c}_+ \cdot \mathbf{w} = 0.20 - 0.02 = 0.18\); \(\sigma(0.18) = 0.545\).
  2. \(\mathbf{c}_- \cdot \mathbf{w} = 0.15 + 0.12 = 0.27\); \(\sigma(0.27) = 0.567\).
  3. Loss \(= -\ln 0.545 - \ln(1 - 0.567) = 0.607 + 0.837 = 1.44\).
  4. Gradient: \((\sigma_+ - 1)\mathbf{c}_+ + \sigma_- \mathbf{c}_- = -0.455(0.4, 0.1) + 0.567(0.3, -0.6) = (-0.012, -0.386)\).

Answer: Loss 1.44. A gradient descent step moves \(\mathbf{w}\) in the direction \((+0.012, +0.386)\) times the learning rate, which raises its dot product with the true context vector (by 0.043 per unit step) and lowers it with the negative one (by 0.228).

Two other influential methods followed. GloVe (Stanford, 2014) fits word vectors so that their dot products match the logarithms of global co-occurrence counts, combining count statistics with an efficient learned model. fastText (Facebook, 2016) represents each word as the sum of vectors for its character n-grams (for example "<wh", "whe", "her", "ere", "re>" for "where"), so it can build vectors for unseen and misspelled words and works well for languages with rich morphology.

What embeddings capture

Well-trained embeddings encode surprisingly regular relationships. Famously, the vector \(\mathbf{v}_{king} - \mathbf{v}_{man} + \mathbf{v}_{woman}\) is closer to \(\mathbf{v}_{queen}\) than to any other word, and similar offsets capture country-capital, verb tense and plural relationships. These analogies work because a consistent difference in contexts (such as the contexts that distinguish male and female words) corresponds to a consistent direction in the vector space.

\[\hat{b}^{*} = \arg\max_{x \notin \{a, b, a^*\}} \cos(\mathbf{v}_x, \mathbf{v}_b - \mathbf{v}_a + \mathbf{v}_{a^*})\]

Worked problem

An analogy with small vectors

Suppose \(\mathbf{v}_{man} = (1, 0, 2)\), \(\mathbf{v}_{woman} = (1, 2, 2)\), \(\mathbf{v}_{king} = (3, 0, 5)\), and the candidates are \(\mathbf{v}_{queen} = (3, 2, 5)\) and \(\mathbf{v}_{prince} = (2.5, 0, 4)\). Which completes "man is to king as woman is to ?"

Show solution
  1. Target: \(\mathbf{v}_{king} - \mathbf{v}_{man} + \mathbf{v}_{woman} = (3, 2, 5)\).
  2. queen: identical to the target, cosine 1.
  3. prince: \(\cos = (7.5 + 0 + 20)/(\sqrt{38} \times \sqrt{22.25}) = 27.5/(6.16 \times 4.72) = 0.946\).

Answer: queen (cosine 1.0 against 0.946). In real embeddings the match is never exact; the input words are excluded from the search because \(\mathbf{v}_{king}\) itself is often the nearest vector to the target.

Diagram of a parallelogram in embedding space. Vectors for man and woman are joined by an arrow labeled gender direction, and vectors for king and queen are joined by a parallel arrow of the same length. A second pair of parallel arrows joins man to king and woman to queen, labeled royalty direction.
Consistent relationships appear as parallel offsets between word vectors.

Analogy results have been overstated: they depend on excluding the input words, work for some relation types far better than others, and partly reflect simple frequency effects. Embeddings are better evaluated on downstream tasks and on word-similarity benchmarks that compare cosine similarity with human ratings. Static embeddings also have a fundamental limitation: each word type has one vector, so "bank" (river) and "bank" (money) share a single vector that blends both meanings.

Bias in embeddings

Because embeddings learn from human-written text, they absorb the associations in that text, including social stereotypes. In 2016, Bolukbasi and colleagues showed that word2vec vectors trained on news text completed "man is to computer programmer as woman is to ?" with "homemaker", and that occupation words lay along a gender direction in proportion to stereotypes. The Word Embedding Association Test (WEAT) measures such associations statistically and has found biases related to gender, race, age and other attributes that mirror findings from human implicit association tests.

These biases matter when embeddings feed real decisions, such as ranking job applicants, moderating content or retrieving documents. Proposed mitigations include identifying a bias direction (for example the average of \(\mathbf{v}_{she} - \mathbf{v}_{he}\) over several word pairs) and projecting it out of words that should be neutral, rebalancing training data, and testing downstream systems for disparate outcomes. Later research showed that projection methods mostly hide bias rather than remove it, since the associations remain recoverable from other directions. The more reliable lesson is to measure bias in the final application, on the decisions that matter, rather than assuming a debiased representation makes a system fair.

Worked problem

Projection onto a bias direction

A gender direction is the unit vector \(\mathbf{g} = (0.6, 0.8, 0)\). The vector for "engineer" is \(\mathbf{v} = (0.5, 1.0, 2.0)\). Compute its component along \(\mathbf{g}\) and the "neutralized" vector with that component removed.

Show solution
  1. Component: \(\mathbf{v} \cdot \mathbf{g} = 0.3 + 0.8 + 0 = 1.1\).
  2. Neutralized: \(\mathbf{v} - 1.1\,\mathbf{g} = (0.5 - 0.66, 1.0 - 0.88, 2.0) = (-0.16, 0.12, 2.0)\).
  3. Check: \((-0.16)(0.6) + (0.12)(0.8) = -0.096 + 0.096 = 0\).

Answer: The component along the bias direction is 1.1; after removal it is 0. Whether this removes the bias in a downstream system must still be tested, because related information may be encoded elsewhere in the vector.

From static to contextual embeddings

Static embeddings give each word one vector regardless of context. Contextual embeddings instead compute a different vector for each occurrence of a word, depending on the surrounding sentence. ELMo (2018) produced them from a bidirectional LSTM language model (Lesson 4); BERT (2018) from a transformer encoder (Lesson 6). In "she sat by the river bank" and "the bank approved the loan", a contextual model gives "bank" different vectors that cluster with other river words and other financial words respectively.

Today, embeddings are used in two main ways. Within language models, the input embedding layer maps tokens to vectors that the transformer layers then contextualize. Separately, sentence and document embedding models (such as Sentence-BERT and the embedding models offered by AI providers) map a whole passage to a single vector, so that semantic search can find passages similar in meaning to a query even without shared words. These vectors are stored in vector databases and searched with approximate nearest-neighbor algorithms, forming the retrieval half of retrieval-augmented generation (Lesson 9 and AI-410).

Worked problem

Storage for a vector index

A company embeds 20 million document chunks with a 1024-dimensional model, storing each value as a 32-bit float. How much memory does the raw index need? What if values are stored as 8-bit integers?

Show solution
  1. Values: \(20 \times 10^6 \times 1024 = 2.05 \times 10^{10}\).
  2. 32-bit: \(\times 4\) bytes \(= 8.2 \times 10^{10}\) bytes \(\approx 82\) GB.
  3. 8-bit: \(\approx 20.5\) GB.

Answer: About 82 GB at 32 bits, or 20 GB with 8-bit quantization (plus index overhead). Quantization and lower-dimensional embeddings trade a little retrieval accuracy for large savings in memory and cost.

Lesson 3 puts these representations to work in classifiers and taggers, and Lesson 9 returns to sentence embeddings for retrieval.

Common mistakes

  • MistakeTreating cosine similarity of embeddings as a measure of meaning equivalence.

    InsteadSimilar vectors mean similar contexts; antonyms such as "hot" and "cold" are often very close.

  • MistakeIncluding the query words when solving analogies and then overstating the results.

    InsteadReport how the search was done; analogies work for some relations far better than others.

  • MistakeAssuming debiasing an embedding makes downstream systems fair.

    InsteadMeasure outcomes in the final application; bias can remain in other directions.

  • MistakeUsing one static vector for words with several senses.

    InsteadUse contextual embeddings when meaning depends on context.

  • MistakeComparing embeddings from different models or training runs directly.

    InsteadEach model has its own coordinate system; compare only vectors from the same model.

Summary and key terms

  • The distributional hypothesis: words in similar contexts have similar meanings.
  • Dense embeddings represent words as short real vectors; an embedding layer is a table lookup.
  • PMI measures co-occurrence above chance; PPMI plus truncated SVD gives count-based embeddings.
  • Skip-gram with negative sampling learns embeddings by classifying real and random word-context pairs.
  • GloVe fits log co-occurrence counts; fastText adds character n-grams for unseen words.
  • Vector offsets capture some relations, such as analogies, but results are often overstated.
  • Embeddings absorb social biases from text; measure bias in downstream outcomes.
  • Contextual embeddings vary with context; sentence embeddings power semantic search.
Distributional hypothesis
The idea that words occurring in similar contexts have similar meanings.
Word embedding
A dense real-valued vector representing a word.
One-hot vector
A vector with a single 1 identifying a word and 0 elsewhere.
Pointwise mutual information
The log ratio of a joint probability to the product of marginals.
Truncated SVD
A low-rank approximation keeping the largest singular values.
Skip-gram
A word2vec model predicting context words from a center word.
Negative sampling
Training against randomly sampled non-context words instead of the full vocabulary.
fastText
Embeddings built from character n-gram vectors.
Contextual embedding
A vector for a word occurrence that depends on its sentence.
Sentence embedding
A single vector representing a sentence or passage.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. What does an embedding layer compute when given a one-hot vector?

    Show answer

    It selects the corresponding row of the embedding matrix.

  2. P(w,c) = 0.001, P(w) = 0.01, P(c) = 0.02. PMI (natural log)?

    Show answer

    ln(0.001/0.0002) = ln 5 = 1.61.

  3. Why is PMI replaced by PPMI?

    Show answer

    Negative PMI values are unreliable, especially for rare pairs, so they are set to zero.

  4. How many vectors does SGNS learn per word, and which are usually kept?

    Show answer

    Two (target and context); usually the target vectors, sometimes their sum.

  5. A word has unigram probability 0.01 and another 0.0001. What is the ratio of their negative-sampling probabilities with exponent 0.75?

    Show answer

    100^0.75 = 31.6, compared with 100 by raw frequency.

  6. σ(c·w) = 0.8 for a true pair. What is the loss term?

    Show answer

    −ln 0.8 = 0.223.

  7. How does fastText build a vector for an unseen word?

    Show answer

    By summing the vectors of its character n-grams.

  8. Why are "hot" and "cold" often close in embedding space?

    Show answer

    They occur in very similar contexts (weather, temperature, food).

  9. Remove the component of v = (2, 1) along the unit vector g = (1, 0).

    Show answer

    (0, 1).

  10. Ten million 768-dimensional float32 vectors need how much memory?

    Show answer

    10⁷ × 768 × 4 = 30.7 GB.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz