🌙 30-Day AI Ramadan Series  ·  Day 23 of 30
Foundation· Mar 13, 2026· 35 min read· NLP · Representations

Word Embeddings:
Word2Vec, GloVe & Sentence Transformers

king − man + woman ≈ queen. This single equation broke people's intuitions about what AI could do with language — not because the machine "understands" anything, but because it learned to place words in a geometric space where meaning becomes arithmetic. This post explains how that space is built, what it means, and why it is the foundation every modern language model stands on.

← Back to Blog ☰ Categories

You shall know a word
by the company it keeps.

— J.R. Firth, 1957

This single sentence, written sixty-six years before ChatGPT,
is the theoretical foundation of every embedding in existence.

01 — The Problem Token IDs Don't Solve

Yesterday we learned that tokenization converts text into numbers. Your message becomes a list of integers before the model ever processes it. "cat" → 9246. "dog" → 10077. "kitten" → 19772.

But here is the problem: those numbers are arbitrary. 9246 and 10077 are just entries in a lookup table — they carry no relationship to each other. The number 9246 is no closer to 10077 than it is to 45,000. Yet "cat" and "dog" are very closely related — both are household pets, both are animals, both appear in similar sentences. "cat" and "kitten" are even more closely related: one is literally the younger form of the other.

Raw token IDs are useless for capturing this. They are the equivalent of naming things with random serial numbers. So we need a different kind of representation — one where similar words produce similar numbers. Not a single number per word, but a list of hundreds of numbers — a vector — arranged so that words with similar meanings cluster together in space.

💡 The core question

How do we represent words as numbers in a way that similar words produce similar numbers? So that the distance between "cat" and "dog" is small, the distance between "cat" and "politics" is large, and the direction from "king" to "queen" is the same direction as from "man" to "woman"? This is the problem word embeddings solve.

Token ID (useless for similarity):
cat → 9246 (just a serial number)
dog → 10077 (no relationship to 9246)
politics → 45231 (equally "different" from cat as dog is)

Embedding vector (captures meaning):
cat → [ 0.82, -0.13, 0.45, 0.71, -0.22, ... ] (300 numbers)
dog → [ 0.79, -0.17, 0.41, 0.68, -0.19, ... ] (300 numbers — nearly identical!)
politics → [-0.12, 0.63, -0.38, -0.04, 0.77, ... ] (very different direction)

The vector for "cat" and the vector for "dog" are nearly parallel — they point in almost the same direction in 300-dimensional space. "Politics" points in a completely different direction. Now similarity is measurable, and arithmetic on concepts becomes possible.

02 — The Distributional Hypothesis: Meaning From Context

Before the algorithms, we need the theoretical foundation. Why should training on text co-occurrence produce vectors that capture meaning? The answer comes from linguistics, not computer science.

In 1957, linguist J.R. Firth wrote: "You shall know a word by the company it keeps." This is the distributional hypothesis — the idea that words with similar meanings tend to appear in similar contexts.

Consider sentences you might find in a large corpus: "The [X] sat on the mat." "The [X] purred quietly." "She stroked the [X] behind its ears." "The [X] chased the mouse across the floor." If X = "cat" → all sentences make perfect sense If X = "kitten" → all sentences still make perfect sense If X = "feline" → all sentences still make perfect sense ∴ "cat", "kitten", "feline" appear in nearly identical contexts ∴ They should have very similar embedding vectors Now try X = "democracy" → none of the sentences make sense ∴ "democracy" appears in completely different contexts ∴ Its vector should be far away from "cat"

This is the magic of large-scale text. You don't need a linguist to manually write that "cat" and "kitten" are related — you just need to observe that they appear in the same contexts millions of times. The mathematical structure of co-occurrence is the structure of meaning, at least to a very good first approximation.

🔍 For Engineers

The distributional hypothesis is the theoretical justification for all count-based and prediction-based embedding methods. It doesn't capture everything — it conflates polysemous words ("bank" the institution vs "bank" the river), is sensitive to corpus domain, and misses logical operators like negation. But it captures an extraordinary amount of semantic structure from raw co-occurrence statistics alone, which is why every major embedding approach since 2013 has been built on some form of this idea.

03 — Why One-Hot Encoding Fails

Before Word2Vec, the dominant approach was one-hot encoding. Understanding why it fails makes the embedding solution much clearer.

One-hot encoding (vocabulary size = 5 for simplicity):

Vocabulary: [cat, dog, kitten, politics, run]

cat → [1, 0, 0, 0, 0]
dog → [0, 1, 0, 0, 0]
kitten → [0, 0, 1, 0, 0]
politics→ [0, 0, 0, 1, 0]

Cosine similarity between any two different words:
cos(cat, dog) = 0.0 ← orthogonal, no relationship captured
cos(cat, kitten) = 0.0 ← same distance as cat↔politics!
cos(cat, politics) = 0.0 ← indistinguishable from all others

Every word is equally far from every other word. "cat" is as different from "dog" as it is from "galaxy." The representation carries zero semantic information.

✗ No similarity signal
All non-identical pairs have exactly the same cosine distance of 1.0. There is no way to ask "which words are similar to cat?" — they're all equally different. The representation is completely uninformative about semantic relationships.
✗ Dimensionality explosion
For a vocabulary of 50,000 words, each vector has 50,000 dimensions — nearly all zeros. This is a sparse representation that wastes enormous memory and computation. Most operations are adding zeros together.
✗ No generalisation
Anything the model learns about "cat" — say, that it's an entity that can be stroked — doesn't transfer at all to "kitten." Every word must be learned from scratch, completely independently. Shared morphology ("run", "running", "runner") is invisible.

The solution is to learn dense, low-dimensional representations: vectors of 100–1000 numbers (not 50,000), where similar words have similar vectors. This is exactly what Word2Vec introduced in 2013.

04 — Word2Vec: Learning Meaning From Prediction

In 2013, Tomas Mikolov and colleagues at Google published Word2Vec — a shockingly simple neural network that produced astonishingly good word representations. The central insight was counterintuitive: don't try to count co-occurrences. Train a network to predict words from their context, and read off the learned weights as the embeddings.

Word2Vec comes in two symmetric flavours:

CBOW — Continuous Bag of Words
Given the surrounding words, predict the centre word. "The ___ sat on the mat" → predict "cat". Context window typically ±5 words. Faster and works better on frequent words.
Skip-Gram
Given the centre word, predict each surrounding word. "cat" → predict "the", "sat", "on", "the". Slower but captures rare words better — it squeezes more signal out of every occurrence by predicting multiple targets per centre word.

The Architecture

The network is almost embarrassingly simple: input → one hidden layer (no activation) → output softmax. What matters is the task, not the architecture.

Skip-Gram network (vocabulary V, embedding dim d):

Input: one-hot vector for centre word → shape [V]
(e.g. "cat" = [0,0,...,1,...,0])

Hidden: W_in ∈ ℝ^(V×d) ← THIS IS THE EMBEDDING MATRIX
x = W_in[word_id] ← just a row lookup, no multiplication
shape: [d] (e.g. d = 300)

Output: W_out ∈ ℝ^(d×V) → softmax over vocab → P(context word)

Loss: Cross-entropy between predicted distribution and actual context words
After training: W_out is discarded. W_in rows are the word embeddings.

By training to predict context words from a centre word, the hidden layer weights are forced to encode what contexts each word appears in. Two words that consistently appear in similar contexts will converge toward similar weight vectors — because similar vectors produce similar predictions, and similar predictions incur similar losses on similar training examples.

The Training Trick: Negative Sampling

Computing softmax over a 50,000-word vocabulary is expensive — you normalise over all 50,000 outputs for every training step. Word2Vec uses negative sampling to avoid this: instead of predicting the full distribution, train a binary classifier. For each (centre, context) pair, ask "is this a real pair or a randomly sampled fake?"

Negative sampling objective (simplified):

Maximise: log σ(v_centre · v_context) ← real pair scores high
+ Σ log σ(−v_centre · v_noise_k) ← k random words score low

σ = sigmoid: σ(x) = 1/(1+e^−x)
k = noise samples per real pair (typically 5–20)

Instead of normalising over 50,000 words → only 1+k comparisons per step
Speedup: 1,000–10,000× for large vocabularies

The Famous Analogy Arithmetic

Once trained on a large corpus, something remarkable emerges. You can do arithmetic on meaning:

v("king") ≈ [ 0.50, 0.69, 0.32, -0.17, 0.81, ... ] v("man") ≈ [ 0.45, 0.39, 0.21, -0.35, 0.52, ... ] v("woman") ≈ [ 0.39, 0.37, 0.19, -0.34, 0.48, ... ] king - man + woman ≈ [ 0.44, 0.67, 0.30, -0.16, 0.77, ... ] Nearest vector in embedding space → v("queen") ✓ Other analogies that emerge naturally: Paris − France + Italy ≈ Rome Doctor − man + woman ≈ Nurse (also reveals training corpus bias!) Tokyo − Japan + China ≈ Beijing Good − better + bad ≈ worse
💡 Why does this work?

The direction from "man" to "woman" in embedding space consistently encodes the concept of gender — because gender-correlated contexts are consistent across the corpus. The direction from "France" to "Paris" consistently encodes "capital city of" — because capital city naming patterns are consistent across thousands of sentences. Arithmetic on vectors captures arithmetic on the underlying conceptual relationships. The same mechanism that reveals the king→queen relationship also reveals the doctor→nurse gender bias, which is a direct mirror of the bias in the training text.

What Word2Vec Gets Wrong

One vector per word — polysemy ignored
"bank" gets a single embedding vector, even though it means a financial institution in one sentence and a river bank in another. The vector ends up as an average of both meanings, which is accurate for neither use. This is the polysemy problem — and it motivates everything after Word2Vec.
No subword understanding
"tokenization" and "tokenize" get completely separate vectors with no built-in connection. Words not seen during training have no embedding at all — they become [UNK], losing all information. FastText (Facebook, 2016) fixed this by learning embeddings for character n-grams and composing word vectors from subword pieces.
Shallow, symmetric context window
"Not good" pushes "not" and "good" together — the model has no understanding of negation or long-range dependencies. The context window is fixed-size and treats all positions equally, missing the syntactic structure of language.
Reflects corpus biases faithfully
"Doctor − man + woman ≈ Nurse" is not a quirk — it is the gender occupational bias of the training corpus rendered in geometry. Embeddings learn whatever the data contains, including social stereotypes encoded in who appears in what roles in news articles and books.

05 — GloVe: Global Vectors for Word Representation

Word2Vec is trained locally — it looks at one (centre, context) pair at a time. Stanford researchers Pennington, Socher, and Manning argued in 2014 that this throws away global statistical information available in the full corpus. Their approach: start from the full co-occurrence matrix and factorize it directly.

The Co-Occurrence Matrix

Before training, GloVe counts how often every word pair appears within a fixed window across the entire corpus, building a matrix X where X[i][j] = times word j appears in the context of word i.

Mini corpus (window = 1): "ice is cold" / "steam is hot" / "ice and steam are both water" Co-occurrence matrix X (partial counts): ice steam solid gas water cold hot ice [ 0, 0, 1, 0, 1, 1, 0 ] steam [ 0, 0, 0, 1, 1, 0, 1 ] water [ 1, 1, 0, 0, 0, 0, 0 ] Key ratio: P(solid|ice) / P(solid|steam) ≈ 8.9 ← ice IS solid, steam is NOT P(gas|ice) / P(gas|steam) ≈ 0.085 ← gas describes steam, not ice P(water|ice) / P(water|steam) ≈ 1.36 ← both are water (ratio near 1) These ratios discriminate context much better than raw counts alone.

The GloVe Objective

GloVe loss function:

J = Σᵢⱼ f(Xᵢⱼ) · (vᵢᵀ vⱼ + bᵢ + bⱼ − log Xᵢⱼ)²

vᵢ, vⱼ = embedding vectors for words i and j
bᵢ, bⱼ = scalar bias terms per word
Xᵢⱼ = co-occurrence count of words i and j
f(Xᵢⱼ) = weighting function — down-weights very frequent pairs

Goal: make the dot product of two word vectors ≈ log(co-occurrence count)

Weighting function (prevents "the", "is", "a" from dominating):
f(x) = (x/x_max)^α if x < x_max, else 1.0
α = 0.75, x_max = 100 (empirically tuned in the paper)

GloVe vs Word2Vec

PropertyWord2VecGloVe
Learning approach Prediction (local windows) Matrix factorisation (global stats)
Statistical scope Local — one window at a time Global — full corpus co-occurrence
Memory during training Low — streams one window Higher — stores V×V matrix (sparse)
Analogy quality ✓ Excellent ✓ Excellent (often slightly better)
Loss interpretability Prediction probability Dot product ≈ log co-occurrence
Polysemy ✗ One vector per word ✗ One vector per word

In practice, both produce embeddings of comparable quality for most downstream tasks. Many practitioners simply try both and pick whichever works better for their specific use case. The shared limitation — one vector per word regardless of context — is what both leave for the next generation to solve.

06 — The Polysemy Problem and Contextual Embeddings

Both Word2Vec and GloVe assign one fixed vector to each word type, regardless of context. But many words have completely different meanings in different sentences.

"bank" — two completely different meanings: "I deposited money at the bank." ← financial institution "We sat on the bank of the river." ← edge of a waterway With static embeddings: v("bank") = some average of both meanings → useful for neither "light" — three distinct meanings: "Turn on the light." ← electromagnetic radiation "The bag is very light." ← low mass "Let me light the candle." ← to ignite "model" — also multiple: "She works as a fashion model." ← person "Train the model on your data." ← machine learning model "This is the 2026 model of the car." ← product version

The solution is context-dependent embeddings: instead of one fixed vector per word type, compute a different vector for each word occurrence based on the surrounding sentence. This is what contextual embeddings do — and it's the bridge from Word2Vec/GloVe to the modern transformer era.

ELMo: The First Contextual Embeddings (2018)

ELMo (Embeddings from Language Models, AllenNLP 2018) was the first widely adopted contextual approach. It runs each sentence through a bidirectional LSTM and uses the resulting hidden states as word representations. The same word "bank" in two different sentences produces two different ELMo vectors — because the hidden states encode the sentence history.

ELMo — contextual embedding for word at position k:

Forward LSTM: h→ₖ = context of words 1 ... k (left context)
Backward LSTM: h←ₖ = context of words k ... N (right context)

ELMo(wₖ) = γ · Σⱼ sⱼ · hⱼₖ
sⱼ = learned weights per LSTM layer (sum to 1)
γ = task-specific scaling factor

"bank" in a financial sentence → different hidden states → different vector
than "bank" in a river sentence ✓ — polysemy finally handled

ELMo was a major advance, but was quickly superseded by BERT (2018), which replaces LSTMs with transformers — attending to the full sentence simultaneously at every layer rather than processing left-to-right and right-to-left in two separate passes. Day 26 covers BERT and GPT in depth.

07 — Sentence Transformers: Embedding Whole Sentences

Word-level embeddings answer "what does this word mean here?" But many real applications need a different answer: "how similar is the meaning of sentence A to sentence B?" — and they need it to be fast enough to search millions of documents.

This is the problem sentence transformers solve, and it's the core technology behind semantic search, RAG retrieval, duplicate detection, and recommendation systems.

The Naive Approach and Why It Fails

Mean pooling of word embeddings (naive):

sentence_vec = (1/N) · Σᵢ word_vec(wᵢ)

Problem 1: "A dog bit a man" ≈ "A man bit a dog"
Same words → same average → identical vector
Completely different meaning — invisible to mean pooling

Problem 2: "Not good" → vector ≈ average(not, good)
Negation is completely lost — the result sits between positive and negative

Problem 3: Stopwords dominate long texts
"the", "a", "is" appear so often they dilute semantically rich words

Why Raw BERT Doesn't Scale to Semantic Search

⚠️ The cross-encoder bottleneck

BERT produces its best sentence representation by reading two texts jointly — the query and the document are concatenated and passed through together, so attention flows between them inside the model. This is called a cross-encoder. But it means you can't pre-compute document embeddings: every time a new query arrives, you must run BERT on every (query, document) pair. For 10,000 documents at ~50ms per pass → 8 minutes per query. Completely unusable in production.

Sentence-BERT: The Siamese Solution (2019)

Nils Reimers and Iryna Gurevych (UKP Lab) published Sentence-BERT (SBERT) in 2019. The solution: fine-tune BERT with a siamese network architecture using sentence pair similarity tasks, so the resulting embeddings are directly comparable by cosine similarity — without joint encoding.

SBERT — Siamese training setup:

Training data: sentence pairs with similarity labels
"A cat sits on a mat." ↔ "A kitten rests on a rug." → similar (1)
"A cat sits on a mat." ↔ "The market crashed today." → different (0)

BERT(sentence_A) → mean_pool(hidden states) → u ∈ ℝ^768
BERT(sentence_B) → mean_pool(hidden states) → v ∈ ℝ^768

Training loss (classification variant):
concat(u, v, |u−v|) → linear → softmax → similarity label

At inference:
similarity(A, B) = cosine(u, v) = (u·v) / (||u|| · ||v||)

Pre-compute all document embeddings once → store in vector DB
Query: embed once → single dot-product search over all documents
Speedup vs raw BERT cross-encoder: ~9,000× for 10,000 documents

How This Powers RAG (Day 29 Preview)

Step 1 — Offline indexing (once)
Every document in your knowledge base is passed through a sentence transformer. The resulting embedding vector (384–1024 dimensions) is stored in a vector database alongside the document text. This is a one-time upfront cost.
Step 2 — Query encoding (per request)
When a user asks a question, it is passed through the same sentence transformer to produce an embedding of identical dimension. Same model, same dimensionality — the query and documents live in the same geometric space.
Step 3 — Semantic similarity search
The query vector is compared against all document vectors using cosine similarity (or approximate nearest-neighbour search via HNSW/IVF for large databases). The top-k most semantically similar documents are retrieved — no keyword matching required.
Step 4 — Grounded generation
The retrieved documents are appended as context to the original question, and the full prompt is sent to the LLM. The model generates an answer grounded in the retrieved text, dramatically reducing hallucination compared to pure parametric memory.
🔍 For Engineers — Popular Models in 2026

The most widely used sentence transformer families: all-MiniLM-L6-v2 (80 MB, 384-dim, fast, great for English), all-mpnet-base-v2 (slower, higher quality), paraphrase-multilingual-mpnet-base-v2 (50 languages). For cutting-edge quality: text-embedding-3-large (OpenAI, 3072-dim) and embed-english-v3.0 (Cohere). All available via the sentence-transformers Python library and HuggingFace Hub. For domain-specific work: BioBERT (medical), LegalBERT (legal), FinBERT (financial).

08 — Cosine Similarity: Measuring Meaning With Angles

Throughout this post we've talked about "similar vectors" without precisely defining similarity. The standard measure in embedding space is cosine similarity.

Cosine similarity between vectors u and v:

cos(u, v) = (u · v) / (||u|| × ||v||)

u · v = Σᵢ uᵢvᵢ ← dot product
||u|| = √(Σᵢ uᵢ²) ← L2 norm of u
||v|| = √(Σᵢ vᵢ²) ← L2 norm of v

Range and interpretation:
cos = +1.0 → identical direction → maximum similarity (e.g. "cat" vs "cat")
cos = 0.0 → perpendicular → no semantic relationship (e.g. "cat" vs "politics")
cos = −1.0 → opposite directions → antonyms (e.g. "love" vs "hate")

Why cosine and not Euclidean distance? Because cosine is magnitude-invariant. A 500-word document will have a larger embedding magnitude than a 5-word query (more vectors were pooled together), but their cosine similarity still correctly captures semantic closeness. Euclidean distance would be dominated by the length difference, not the meaning difference.

Worked example (3D for clarity): u = v("cat") = [0.9, 0.2, 0.1] v = v("dog") = [0.8, 0.3, 0.0] w = v("code") = [-0.1, 0.8, 0.9] cos(cat, dog): u·v = (0.9×0.8) + (0.2×0.3) + (0.1×0.0) = 0.72+0.06+0.0 = 0.78 ||u|| = √(0.81+0.04+0.01) = 0.927 ||v|| = √(0.64+0.09+0.00) = 0.854 cos = 0.78 / (0.927 × 0.854) ≈ 0.985 ← very similar ✓ cos(cat, code): u·w = (0.9×-0.1) + (0.2×0.8) + (0.1×0.9) = -0.09+0.16+0.09 = 0.16 ||w|| = √(0.01+0.64+0.81) = 1.208 cos = 0.16 / (0.927 × 1.208) ≈ 0.143 ← not similar ✓

09 — Code: From Token IDs to Semantic Search

# ─────────────────────────────────────────────────────────────────────
# PART 1: The problem with raw token IDs
# ─────────────────────────────────────────────────────────────────────
import numpy as np

vocab = {"cat": 0, "dog": 1, "kitten": 2, "politics": 3, "democracy": 4}
V = len(vocab)

def one_hot(word_id, vocab_size):
    v = np.zeros(vocab_size)
    v[word_id] = 1.0
    return v

def cosine(u, v):
    return np.dot(u, v) / (np.linalg.norm(u) * np.linalg.norm(v) + 1e-10)

cat_oh    = one_hot(vocab["cat"],    V)
dog_oh    = one_hot(vocab["dog"],    V)
kitten_oh = one_hot(vocab["kitten"], V)

print(f"cos(cat, dog)    = {cosine(cat_oh, dog_oh):.3f}")    # 0.000
print(f"cos(cat, kitten) = {cosine(cat_oh, kitten_oh):.3f}") # 0.000
# All pairs are equally "different" — one-hot encodes nothing about meaning
# ─────────────────────────────────────────────────────────────────────
# PART 2: Loading GloVe embeddings and word analogy arithmetic
# ─────────────────────────────────────────────────────────────────────
# Download: https://nlp.stanford.edu/projects/glove/ (glove.6B.300d.txt)

def load_glove(path, max_words=50_000):
    """Load GloVe vectors into a dict {word: np.array}."""
    embeddings = {}
    with open(path, encoding="utf-8") as f:
        for i, line in enumerate(f):
            if i >= max_words:
                break
            values = line.split()
            word   = values[0]
            vector = np.array(values[1:], dtype=np.float32)
            embeddings[word] = vector
    return embeddings

def analogy(glove, a, b, c, topk=5):
    """Find word d such that a:b :: c:d   (d ≈ b - a + c)."""
    target  = glove[b] - glove[a] + glove[c]
    target  = target / (np.linalg.norm(target) + 1e-10)
    exclude = {a, b, c}
    scores  = [
        (word, np.dot(target, vec / (np.linalg.norm(vec) + 1e-10)))
        for word, vec in glove.items()
        if word not in exclude
    ]
    return sorted(scores, key=lambda x: -x[1])[:topk]

# glove = load_glove("glove.6B.300d.txt")
# print(analogy(glove, "man",    "king",   "woman"))  # → queen  ✓
# print(analogy(glove, "france", "paris",  "italy"))  # → rome   ✓
# print(analogy(glove, "good",   "better", "bad"))    # → worse  ✓

def nearest_neighbours(glove, word, topk=5):
    """Find the k most similar words to a query word."""
    target = glove[word] / (np.linalg.norm(glove[word]) + 1e-10)
    scores = [
        (w, np.dot(target, v / (np.linalg.norm(v) + 1e-10)))
        for w, v in glove.items()
        if w != word
    ]
    return sorted(scores, key=lambda x: -x[1])[:topk]

# nearest_neighbours(glove, "cat")
# → [("cats", 0.924), ("dog", 0.921), ("kitten", 0.908), ("feline", 0.881), ...]
# ─────────────────────────────────────────────────────────────────────
# PART 3: Sentence transformers for semantic search
# ─────────────────────────────────────────────────────────────────────
# pip install sentence-transformers

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("all-MiniLM-L6-v2")  # 80 MB, 384-dim, fast

# Knowledge base
documents = [
    "The Eiffel Tower is located in Paris, France.",
    "Photosynthesis converts sunlight into chemical energy in plants.",
    "Python is a high-level programming language known for readability.",
    "The mitochondria is the powerhouse of the cell.",
    "Gradient descent is an optimisation algorithm used in machine learning.",
    "The Great Wall of China stretches over 21,000 kilometres.",
    "Transformers use self-attention to process sequences in parallel.",
]

# Offline indexing (done once)
doc_embeddings = model.encode(documents, normalize_embeddings=True)
# Shape: (7, 384) — one unit vector per document

# Runtime query
query = "What optimisation algorithm is used in deep learning?"
q_emb = model.encode([query], normalize_embeddings=True)[0]

# Cosine search (dot product of unit vectors = cosine similarity)
scores = doc_embeddings @ q_emb
ranked = np.argsort(scores)[::-1]

print(f"\nQuery: {query}\n")
for rank, idx in enumerate(ranked[:3]):
    print(f"  {rank+1}. [score={scores[idx]:.3f}]  {documents[idx]}")

# Output:
# 1. [score=0.712]  Gradient descent is an optimisation algorithm used in ML.
# 2. [score=0.534]  Transformers use self-attention to process sequences.
# 3. [score=0.298]  Python is a high-level programming language...
# ─────────────────────────────────────────────────────────────────────
# PART 4: FAISS for production-scale vector search
# ─────────────────────────────────────────────────────────────────────
# pip install faiss-cpu
import faiss

dim = 384  # matches all-MiniLM-L6-v2

# Flat index: exact inner product search (cosine on unit vectors)
index = faiss.IndexFlatIP(dim)
index.add(doc_embeddings.astype("float32"))
print(f"Index size: {index.ntotal} vectors")

# Search — returns top-3
D, I = index.search(q_emb.reshape(1, -1).astype("float32"), k=3)

print("\nFAISS results:")
for score, doc_idx in zip(D[0], I[0]):
    print(f"  [score={score:.3f}]  {documents[doc_idx]}")

# For millions of documents: use IndexIVFFlat or IndexHNSWFlat
# for approximate nearest-neighbour search with sub-millisecond latency
# ─────────────────────────────────────────────────────────────────────
# PART 5: Visualising embedding clusters with t-SNE
# ─────────────────────────────────────────────────────────────────────
# pip install scikit-learn matplotlib

from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

words = [
    "cat", "dog", "kitten", "puppy", "feline", "canine",      # animals (green)
    "king", "queen", "prince", "princess", "royal", "throne",  # royalty (yellow)
    "python", "java", "code", "program", "function", "debug",  # programming (blue)
]

word_vecs = model.encode(words, normalize_embeddings=True)  # (18, 384)

# Reduce to 2D for visualisation
tsne   = TSNE(n_components=2, perplexity=5, random_state=42)
coords = tsne.fit_transform(word_vecs)

colors = ["#3fb950"] * 6 + ["#e3b341"] * 6 + ["#58a6ff"] * 6

plt.figure(figsize=(10, 7))
for i, word in enumerate(words):
    plt.scatter(coords[i, 0], coords[i, 1], color=colors[i], s=100, zorder=3)
    plt.annotate(word, (coords[i, 0] + 0.3, coords[i, 1] + 0.3), fontsize=9)

plt.title("Word Embeddings in 2D (t-SNE projection)")
plt.axis("off")
plt.tight_layout()
plt.savefig("embeddings_tsne.png", dpi=150)
# Expected: three clean clusters — animals (green), royalty (yellow),
# programming (blue). Within each cluster: cat↔kitten closest, king↔queen
# adjacent, python↔java near each other.

10 — The Embedding Evolution: 1950s to 2026

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ THE ROAD FROM CO-OCCURRENCE TABLES TO FOUNDATION EMBEDDINGS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1950s–2000s LSA / LSI (Latent Semantic Analysis) ↳ SVD decomposition of term-document co-occurrence matrix ↳ First proof that geometry can capture meaning ↳ Limitation: expensive to compute, no prediction signal 2013 Word2Vec (Mikolov et al., Google) ↳ Prediction-based embeddings via CBOW and skip-gram ↳ king − man + woman ≈ queen — analogy arithmetic emerges ↳ Negative sampling makes training on billions of words practical ↳ Limitation: one static vector per word — polysemy invisible 2014 GloVe (Pennington, Socher, Manning — Stanford) ↳ Matrix factorisation on global co-occurrence counts ↳ Slightly cleaner linear geometry, very similar downstream quality ↳ Same polysemy limitation 2016 FastText (Bojanowski et al., Facebook) ↳ Embeddings built from character n-gram subwords ↳ Handles morphology: "running" shares pieces with "run", "runner" ↳ Works for languages without explicit word boundaries (Thai, Chinese) ↳ Still static — one vector per surface form 2018 ELMo (Peters et al., AllenNLP) ↳ Contextual embeddings from bidirectional LSTM language model ↳ "bank" finally gets different vectors in different sentences ↳ Pre-trained once, fine-tuned by adding task-specific layers 2018 BERT (Devlin et al., Google) ↳ Deep contextual embeddings via transformer self-attention ↳ Full bidirectionality at every layer simultaneously ↳ Pre-train → fine-tune paradigm becomes universal ↳ [CLS] token output usable as sentence embedding (crude but works) 2019 Sentence-BERT (Reimers & Gurevych, UKP Lab) ↳ BERT fine-tuned for sentence-pair similarity via siamese training ↳ Sentence embeddings pre-computable and comparable by cosine ↳ 9,000× faster than BERT cross-encoder for pairwise search ↳ Enables semantic search at production scale → powers modern RAG 2022–2026 Foundation model embeddings ↳ Billion-parameter models trained specifically for embedding ↳ text-embedding-3-large (OpenAI, 3072-dim) ↳ embed-english-v3.0 (Cohere), E5-large, GTE-Qwen2 ↳ Multilingual, multi-modal (text + image in same geometric space) ↳ Trained on massive curated datasets with hard negative mining

11 — What Embeddings Still Can't Do

Negation
"The product is not good" often embeds close to "The product is good." Negation modifies meaning fundamentally, but word-level embeddings average the tokens — "not" doesn't flip the polarity in embedding space. This is why RAG systems sometimes retrieve documents that say the opposite of what was asked.
Domain shift
Embeddings trained on general web text don't transfer perfectly to medical, legal, or financial language. "Discharge" means something very different in cardiology vs. military vs. electronics. Domain-specific models (BioBERT, LegalBERT, FinBERT) often dramatically outperform general embeddings in specialist applications.
Exact keyword matching
Semantic similarity and string matching address different needs. "What is article 7 of the GDPR?" needs the exact article, not semantically similar legal text. Production systems combine dense semantic search with sparse keyword search (BM25/TF-IDF) — hybrid search — to handle both requirements.
Reasoning and compositionality
"If the bank had failed, the depositors would have lost their savings" requires logical reasoning about conditionals. Embedding similarity captures topical closeness, not logical structure. Embeddings are excellent similarity measures — they are not reasoning engines.

12 — A Thought to Sit With

Every word you have ever spoken lives — in some representation — inside these models.

Not as a sound, not as ink, not as memory.
As coordinates in a space of three hundred dimensions.

The question isn't whether that's impressive.
The question is: what gets lost in translation to geometry?

What parts of human language refuse to be a point in space?

📅 Coming up — Day 24

We have the embeddings. Now we need a model that can actually read a sequence of them and generate language. Day 24: Attention & the Transformer Architecture — how "Attention Is All You Need" replaced recurrence entirely with a single elegant mechanism, and why that mechanism became the backbone of every language model from GPT to Gemini to Claude.

📎 Attachments & Further Reading

🔗 TensorFlow Embedding Projector — Interactive 3D visualisation
The single best tool for building embedding intuition. Type any word and watch its nearest neighbours appear in a rotatable 3D word cloud. Run PCA or t-SNE on Word2Vec, GloVe, or your own custom embeddings. Fifteen minutes here makes the "words close in meaning are close in space" concept completely concrete and unforgettable.

📄 Efficient Estimation of Word Representations in Vector Space — Mikolov et al. (2013)
The original Word2Vec paper. Surprisingly readable — the CBOW and skip-gram architectures are described in under four pages. The analogy evaluation results at the end remain striking even knowing the punchline. Essential reading for understanding why prediction-based embeddings work.

📄 GloVe: Global Vectors for Word Representation — Pennington et al. (2014)
Exceptionally well-written. The section explaining why co-occurrence ratios (not raw counts) encode meaning is one of the clearest pieces of mathematical intuition-building in NLP literature. The connection between the objective and the analogy arithmetic is made explicit and elegant.

📄 Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks — Reimers & Gurevych (2019)
The paper that made large-scale semantic search practical. The benchmark comparing BERT cross-encoding vs SBERT sentence encoding (65 hours vs 5 seconds for 10,000 pairs) makes the engineering motivation immediately obvious and the solution feel inevitable.

🔗 SBERT.net — Official sentence-transformers documentation
Model comparisons, benchmark leaderboards, training tutorials, and integration guides for FAISS, Pinecone, Weaviate, and other vector databases. The model selection guide alone is worth bookmarking before any RAG project.

🧠 Check Your Understanding
1. You're told that cos(v("happy"), v("joyful")) = 0.91 and cos(v("happy"), v("miserable")) = −0.74. What does each value mean, and why is the second value negative?
2. A colleague plans to use Word2Vec embeddings for a medical chatbot handling terms like "acute myocardial infarction" and "left ventricular dysfunction." What are the two main problems with this plan?
3. You're building a customer support search system. A user queries "my order hasn't arrived yet." Your database contains: (A) "Package delivery delayed", (B) "Order not received by customer", (C) "Shipping label created", (D) "Refund policy for late deliveries." Rank by expected semantic similarity and explain.
4. What is the fundamental difference between static embeddings and contextual embeddings? Give a concrete scenario where static embeddings fail but contextual embeddings succeed.
⚡ Quick Recap — Word Embeddings
  1. Token IDs encode nothing about meaning. Raw integers are arbitrary serial numbers. We need dense vectors where similar words have similar vectors — that's what embeddings are: lists of ~300 numbers representing a word's position in a learned semantic space.
  2. The distributional hypothesis is the foundation. "You shall know a word by the company it keeps." Words appearing in similar contexts have similar meanings. Measuring co-occurrence at scale extracts semantic structure automatically from raw text — no manual labelling needed.
  3. One-hot encoding fails for three reasons. All non-identical pairs have the same distance (no similarity signal), vectors have V dimensions (sparse and huge), and nothing learned about one word transfers to related words (no generalisation).
  4. Word2Vec learns by predicting context. Train a network to predict surrounding words from a centre word (skip-gram) or the centre from surrounding words (CBOW). The hidden layer weights that emerge encode what contexts each word appears in — similar contexts → similar weights → similar vectors. Negative sampling makes this practical at scale.
  5. GloVe factorises global co-occurrence statistics. Rather than local prediction windows, it minimises the difference between the dot product of two word vectors and the log of their co-occurrence count. Slightly different optimisation, very similar results — both produce analogy arithmetic.
  6. Static embeddings have one vector per word — polysemy invisible. "Bank" gets an averaged vector across all its senses. ELMo (BiLSTM) and BERT (transformer) solve this by computing context-dependent embeddings where the same word gets a different vector in each sentence.
  7. Sentence-BERT makes semantic search practical. Fine-tunes BERT with a siamese architecture so sentence embeddings are directly comparable by cosine similarity — no joint encoding needed. Pre-compute all document embeddings once, search in milliseconds. 9,000× faster than raw BERT cross-encoding.
  8. Cosine similarity measures angles, not lengths. Magnitude-invariant: a 500-word document and a 10-word query can be compared fairly even though they'd have very different vector magnitudes. Values in [−1, +1]: 1.0 = identical direction, 0.0 = orthogonal (unrelated), −1.0 = opposite direction (antonyms).
  9. Embeddings are the foundation of RAG. Every retrieval-augmented generation system relies on sentence embeddings to find relevant documents at query time. Offline index → online search → grounded generation. Understanding embeddings is understanding why RAG works and where it fails.
🎯 Interview Questions
  1. Explain Word2Vec, GloVe, and sentence transformers. When would you use each?
    Word2Vec and GloVe are both static word-level embeddings — one fixed vector per word, trained from corpus co-occurrence. Word2Vec learns by training a prediction task (skip-gram or CBOW); GloVe factorises the global co-occurrence matrix directly. Both produce vectors with analogy-arithmetic properties and similar downstream task quality. Use these for lightweight word-level representations: input features to a classical ML model, visualising word clusters, or quick semantic similarity between individual words where context is not critical. Their core limitation is polysemy: "bank" gets one vector regardless of whether it's financial or geographic. Sentence transformers solve a different problem: they encode entire sentences into single vectors directly comparable by cosine similarity. Use these for semantic search, RAG retrieval, duplicate detection, clustering documents, or any task where you need to compare the meaning of full sentences at scale. They require more compute than static embeddings but are the right tool for any retrieval-based application.
  2. Why is cosine similarity preferred over Euclidean distance for comparing embeddings?
    Cosine similarity measures the angle between two vectors, normalising out their magnitudes entirely. This is critical for embeddings because vector magnitude is not semantically meaningful — it's an artifact of aggregation. A 500-word document embedding will typically be larger in magnitude than a 10-word query embedding simply because more vectors were pooled, not because the document is "more" anything semantically. Euclidean distance is dominated by this magnitude difference and incorrectly ranks the longer text as "farther" from the short query. Cosine similarity removes this confound, comparing only the directional content — which is where the semantic information lives. Additionally, cosine produces normalised values in [−1, 1] that are easy to threshold across different queries, unlike Euclidean distances whose scale grows with embedding dimension. In practice, when using libraries like FAISS with L2-normalised embeddings, the inner product (dot product) equals cosine similarity, which is why normalised embeddings are the standard preprocessing step before vector database indexing.
  3. Your semantic search system returns irrelevant results for the query "not recommended." Why might this happen and how would you fix it?
    This is the negation problem. Most sentence embedding models will embed "not recommended" with a vector close to "recommended" — because the word "recommended" dominates the semantic signal, and "not" is a short function word that gets down-weighted. The model learned from paraphrase pairs where positive sentences are similar to other positive sentences; it was not specifically trained to represent negation as directional opposition. Mitigation strategies, in order of increasing effort: First, add a BM25/TF-IDF sparse retrieval layer in a hybrid search setup — keyword matching correctly distinguishes "recommended" from "not recommended" by exact string presence. Second, add a cross-encoder re-ranker (MS MARCO BERT) on the top-k retrieved candidates — cross-encoders read the query and document jointly and handle negation much better than bi-encoder semantic search. Third, fine-tune the embedding model on contrastive pairs that specifically include negation — ("not good", "bad") as a positive pair and ("not good", "good") as a hard negative. Production recommendation: hybrid search (dense + sparse) plus cross-encoder re-ranking is the standard architecture for precision-sensitive retrieval systems.
#WordEmbeddings#Word2Vec#GloVe#SentenceTransformers#SBERT#SemanticSearch#CosineSimilarity#NLP#Embeddings#RAG#FAISS#DistributionalHypothesis#ELMo#31DaysOfAI#Ramadan2026
← Day 22 Tokenization