Skip to content

GloVe, FastText, and the Embedding Zoo

12 min read

Why are there so many word embedding methods?

I remember the first time I tried to actually use pretrained word vectors for a project. I went to download them and immediately hit a fork in the road: GloVe? word2vec? FastText? They all produce the same kind of artifact (a big table mapping words to dense vectors), and they all seem to capture similar semantic relationships. So what's the difference? And does it matter?

I think the cleanest way to understand this is to look at where these methods came from intellectually. Before word2vec showed up in 2013, NLP researchers had been building word representations from co-occurrence statistics for decades. Latent Semantic Analysis, pointwise mutual information matrices, all that stuff. These were "count-based" methods. Then Mikolov's word2vec came along and said: forget counting, just train a neural network to predict context words. That's a "prediction-based" method.

For a while, the field treated these as fundamentally different philosophies. Count-based versus prediction-based, old school versus new school. They turn out to be closer than anyone expected.

GloVe: Making Co-occurrence Counts Learnable

GloVe (Global Vectors for Word Representation) came out of Stanford in 2014. Pennington, Socher, and Manning noticed that raw co-occurrence counts are noisy and hard to use directly. But ratios of co-occurrence probabilities are stable and meaningful in a way raw counts aren't.

Here's the classic example from the paper. Let P(kw)P(k \mid w) be the probability that word kk appears in the context of word ww, estimated from a big corpus. Consider the words "ice" and "steam," and look at how various probe words kk relate to them:

probe k
P(k | ice) ×10⁻⁴
P(k | steam) ×10⁻⁴
P(k | ice) / P(k | steam)
solid
19.0
2.2
ice8.6
cold
13.0
3.8
ice3.4
gas
2.2
18.0
steam0.12
boiling
3.8
16.0
steam0.24
water
30.0
22.0
either1.4
fashion
1.7
1.8
either0.94
Raw probabilities are noisy and hard to use. The ratio column is what actually discriminates: large ratios mark ice-specific probes, small ratios mark steam-specific ones, and ratios near 1 mark probes that are unrelated to the distinction. The ratios are what GloVe is motivated by; the objective itself fits dot products plus bias terms to log co-occurrence counts.

Co-occurrence probabilities with 'ice' and 'steam', plus their ratios. The 'solid', 'gas', 'water', and 'fashion' rows are scaled from Pennington et al. (2014) Table 1; 'cold' and 'boiling' are illustrative additions to show the same pattern across more probes.

"Solid" co-occurs much more with "ice" than "steam," so the ratio is large. "Gas" goes the other way. "Water" and "fashion" are either related to both or neither, so their ratios sit near 1. The ratio P(kice)/P(ksteam)P(k \mid \text{ice}) / P(k \mid \text{steam}) cleanly separates words that discriminate between the two concepts from words that don't.

GloVe is motivated by these ratios, but the implemented objective is something simpler: a weighted least-squares regression that fits dot products (plus bias terms) to log co-occurrence counts. The actual training equation is:

wiTw~j+bi+b~jlogXijw_i^T \tilde{w}_j + b_i + \tilde{b}_j \approx \log X_{ij}

where XijX_{ij} is the raw co-occurrence count, wiw_i and w~j\tilde{w}_j are the center and context vectors, and bib_i, b~j\tilde{b}_j are scalar biases per word. The biases aren't cosmetic. They absorb word- and context-frequency effects, which lets the dot product focus on relational structure rather than just raw popularity. A weighting function on the loss caps the influence of extremely frequent co-occurrences (so "the" doesn't dominate). The model learns two vector tables (ww and w~\tilde{w}), and the final embedding for a word is typically their sum.

The connection back to ratios is what makes the simplification work: log(P(ki)/P(kj))=logP(ki)logP(kj)\log(P(k|i) / P(k|j)) = \log P(k|i) - \log P(k|j), so a model that fits log-counts well also encodes the discriminative structure the ratio table exposes, as differences between learned vectors.

So GloVe is not a context-prediction model in the word2vec sense. There is no softmax or negative-sampling classifier. It first constructs sparse global co-occurrence statistics — only the non-zero XijX_{ij} entries, which is far smaller than the full V×VV \times V matrix — then trains a shallow log-bilinear model on those entries with a weighted regression objective. The training loop is just gradient descent.

The Unification: Closer Than the Framing Suggested

In 2014, Levy and Goldberg published a paper proving that word2vec's skip-gram model with negative sampling (SGNS) is implicitly factorizing a matrix. When SGNS converges, the dot product of the word and context vectors equals the pointwise mutual information (PMI) of the word pair, shifted by a constant:

wiTcj=PMI(i,j)logkw_i^T c_j = \text{PMI}(i, j) - \log k

where kk is the number of negative samples. PMI is a classical co-occurrence statistic that NLP researchers had been computing from count matrices for years. So skip-gram, the supposedly "prediction-based" method, is implicitly factorizing a shifted PMI matrix. GloVe explicitly fits a weighted log-count matrix with bias terms. Those are different objectives over closely related co-occurrence statistics, not the same matrix.

The methods aren't as philosophically different as the original count-vs-predict framing suggested. They're both recovering low-rank structure from word-context co-occurrence signal. They get there by different objectives, with different weighting functions, different treatment of frequent words, and different optimization paths — and those differences matter for practical trade-offs (which we'll get to). Baroni et al. found that prediction-based methods tended to win benchmarks; Levy et al. (2015) later showed that hyperparameter choices and design details explain much of that gap, not any deep architectural advantage of one family over the other.

wi · ck ≈ log Xik
dot = 0.00 target = 1.38
word vectors Wcontext vectors Cco-occurrence matrix Xicesteamcloudsolidgascoldboiling1.4-1.31.0-1.4-1.11.5-0.81.40.00.60.20.4w(ice) · c(solid) =0.9×1.2+(-0.6)×(-0.4)+0.2×0.3= 0.00≈ X[ice, solid] = 1.38skip-gram and GloVe both recover W and C such that W Cᵀ ≈ shifted log-count matrix

Both methods recover a word table W and a context table C such that w_i · c_k reconstructs the corresponding co-occurrence entry. SGNS does this implicitly via its loss on a shifted PMI matrix; GloVe does it explicitly via weighted least squares on log counts. Watch one cell at a time rebuild from the dot product.

I find this result satisfying in a way that's hard to articulate. Two communities thought they were doing different things. Turns out they were learning low-rank structure from closely related co-occurrence matrices, using different objectives and optimization paths. Goldberg's textbook on neural methods for NLP walks through the equivalence more carefully if the algebra is clicking.

FastText: Words Aren't Atomic

Word2vec and GloVe both treat words as atomic units. The word "unforgettable" gets one vector, learned from its contexts. If "unforgettable" doesn't appear in your training corpus, you get nothing. It's out-of-vocabulary (OOV), and your only option is a generic unknown token.

Bojanowski et al. at Facebook addressed this in 2017 with FastText. The core idea is simple: represent each word as a bag of character n-grams, and learn vectors for the n-grams instead of (or in addition to) whole words.

Take the word "unforgetting." FastText would decompose it into character n-grams (using nn from 3 to 6 by default), plus special boundary markers:

FastText subword decomposition
n = 3
<unforgetting><unmorphological piece — likely has a pretrained vector from other wordsword vector = Σ vectors(n-grams) — OOV words become composable

FastText slides a width-n window across the padded word, producing a bag of n-grams for n = 3 through 6. Green chips are pieces that almost certainly have good pretrained vectors from other words.

The vector for "unforgetting" is the sum of vectors for all these n-gram fragments. And here's the payoff: even if "unforgetting" never appeared in training, many of its n-grams (like "for", "get", "ing") appeared in thousands of other words. So FastText can construct a reasonable vector for it by composing the pieces.

This matters a lot for morphologically rich languages. In Turkish or Finnish, a single word can carry what English spreads across an entire phrase. GloVe and word2vec need to see every inflected form independently. FastText shares information across forms that share substrings, which is a much better inductive bias for these languages.

The skip-gram training procedure is basically the same as word2vec. The only difference is that instead of looking up a single vector per word, you sum up the n-gram vectors. Gradients flow back to the n-grams, and the model learns that "un-" tends to negate, "-ing" tends to mark progressive aspect, and so on.

OOV handling: GloVe vs FastText
input: kingdom
GloVe / word2vecFastTextkingdomtable lookuphitdecompose & sum n-gram vectors<kikingngdodom>+++composed vectorcommon word — both methods give a vector

Same four inputs, two lookup strategies. GloVe either hits its table or returns the unknown token. FastText can usually compose a vector from character n-grams it has seen before — though the result is only as good as the substring evidence in the training corpus.

The Embedding Zoo: A Comparison

So which one should you actually use? Here's where they differ in practice:

Propertyword2vec (SGNS)GloVeFastText
Training signalLocal context windowsGlobal co-occurrence matrixLocal context windows
OOV handlingNoneNoneYes (subword composition)
MorphologyNot capturedNot capturedShared across forms
Training speedFast (online, streaming)Fast (needs sparse co-occurrence construction up front)Slower (more parameters per word)
Memory at train timeLowMedium (sparse non-zero entries)Medium
Best forGeneral purpose, large corporaWhen you want to exploit global statisticsMorphologically rich languages, noisy text, OOV-heavy domains

In terms of raw quality on English benchmarks, the three methods are close enough that hyperparameter tuning and corpus choice matter more than the algorithm. The real differentiator is the OOV story. If you're working with social media text, medical terminology, or agglutinative languages, FastText's subword approach is a clear win.

Zoom in on the geometry for a second. One of the things that made word embeddings so compelling in 2013 was the analogy trick: take the vector for "king," subtract "man," add "woman," and the nearest neighbor is "queen." What's interesting is that this works across all three methods, even though they're fitting different objectives. The relational structure (gender axis, royalty axis) shows up in every recovered space.

king − man + woman ≈ queen — three objectives, same geometry
word2vec (SGNS)predicts context from center wordmankingwomanqueen≈ queenGloVefactors log co-occurrence matrixmankingwomanqueen≈ queenFastTextSGNS + character n-gram bagmankingwomanqueen≈ queeneach panel: start at woman, add the (king − man) vector, land near queenthree different losses recover the same relational geometry
The canonical analogy, run through three different training objectives. Each panel is a projection of pretrained vectors. The arrow from 'man' to 'king' and the arrow from 'woman' to 'queen' are parallel in every panel, even though the losses are different. That's the underlying signal Levy and Goldberg pointed at: what matters is the co-occurrence statistics, not the specific optimizer.

Worked Example: Loading GloVe and Computing Similarities

Let's make this concrete. GloVe vectors are distributed as plain text files (one word per line, followed by its vector components). Loading them is almost embarrassingly simple.

import numpy as np
 
def load_glove(path):
    """Load GloVe vectors from a text file."""
    embeddings = {}
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            parts = line.strip().split()
            word = parts[0]
            vec = np.array(parts[1:], dtype=np.float32)
            embeddings[word] = vec
    return embeddings
 
def cosine_sim(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
 
# Load pretrained GloVe (glove.6B.100d.txt)
glove = load_glove("glove.6B.100d.txt")
 
# Classic analogy: king - man + woman ≈ queen
# Exclude the input words from the search — otherwise one of them
# usually comes back as the nearest neighbor of the query vector.
query = glove["king"] - glove["man"] + glove["woman"]
exclude = {"king", "man", "woman"}
sims = {
    w: cosine_sim(query, v)
    for w, v in glove.items()
    if w not in exclude
}
top5 = sorted(sims, key=sims.get, reverse=True)[:5]
print("king - man + woman =", top5)
# ['queen', 'monarch', 'princess', 'throne', 'prince']
 
# Semantic similarity
pairs = [("cat", "dog"), ("cat", "car"), ("good", "great")]
for w1, w2 in pairs:
    sim = cosine_sim(glove[w1], glove[w2])
    print(f"  sim({w1}, {w2}) = {sim:.3f}")
# sim(cat, dog)   = 0.922
# sim(cat, car)   = 0.342
# sim(good, great) = 0.816

And for FastText's subword decomposition, here's what happens under the hood when it encounters a word:

The key thing to notice: GloVe gives you nothing for a word that's not in its vocabulary. FastText can usually construct a vector from subword parts, which is often much better than a generic unknown-token fallback — but the vector is only as good as the substring evidence learned during training. That difference matters a lot once you're processing real-world text full of misspellings, slang, and domain jargon.

Misconceptions

"GloVe is count-based, word2vec is neural, so they're fundamentally different." They're not. The Levy and Goldberg result is the whole point of the middle section above. Skip-gram with negative sampling is implicitly factoring a shifted PMI matrix. GloVe explicitly factors a weighted log-count matrix. Different objectives, very similar low-rank factorizations of a similar underlying signal. The "neural vs count" framing was a useful narrative for a few years, then the math caught up with it.

"FastText is always better because it handles OOV." The n-gram bag gives FastText its robustness, but it also means a FastText vector mixes in pieces that might have nothing to do with the word's actual meaning. For morphologically simple English, FastText's subword trick sometimes hurts a little on semantic similarity benchmarks compared to word2vec trained on the same corpus. So if your pipeline is mostly clean English, the OOV advantage stops mattering and you might actually prefer word2vec. On messy text, the OOV story dominates and FastText wins comfortably.

"These methods capture meaning." They capture distributional statistics. "Meaning," in the GloVe/word2vec/FastText sense, is the pattern of words a word tends to appear near. This is enough to reproduce striking semantic relationships, and it's also why these models cheerfully encode every stereotype in the training corpus. Bolukbasi et al. showed back in 2016 that "man : programmer :: woman : homemaker" falls out cleanly from pretrained word vectors because that's the distributional pattern in the text. Static embeddings reflect the data, not some platonic semantics.

What's next

Static vectors give every word a single fixed point in space, no matter how it's being used. The next step is sequences: models that read one word at a time and update a running state as they go. That's Recurrent Neural Networks and Sequence Modeling, where the hidden state becomes the thing doing the work.


Additional reading (and watching)