Phase 3 · Deep learning foundations · Topic 3.14

Embeddings: representing things as vectors

An embedding maps each discrete item — a word, a token, a user, a product — to a learned vector of numbers, stored as one row of a table, so that similar items end up close together.

Not started

Step 1 Understand it

The problem it solves. need numbers, but words and ids are just . are huge, and treat every pair of items as equally different. give each item a small dense vector that to be useful, so "cat" and "dog" end up close. They are the input of every and the of and recommendations.

New words in this topic (tap one to see what it means)

Watch it happen

AnimationEmbeddings: a learned vector for every item
Read the animation as text
  1. An embedding layer is just a table: one row of numbers per item in the vocabulary. Here, 5 words × 6 numbers.
  2. The word "cat" has id 1. Its embedding is simply row 1 of the table: a lookup, no maths.
  3. The rows are ordinary weights. When a sentence uses "cat", backprop only changes row 1, nudging it to be more useful. Over many sentences, similar words drift close together.

The idea

An embedding is a learned vector for each item: a word, a user, a product. Similar items end up with similar vectors. Technically, it is a lookup table with one row per item.

How it works, step by step

  1. Create a table E with one row per item: size × embedding size, for example 50,000 × 768. Start it random.
  2. To embed item id i, take row i: E[i]. It is a lookup, the same as multiplying a one-hot vector by E, but much faster.
  3. Feed the vectors into the rest of the network.
  4. During training, the flows back into only the rows that were used, and nudges them to be more useful for the task.
  5. Items used in similar contexts get similar gradients, so their vectors together.
  6. Measure similarity between items with of their vectors.
  7. Directions can carry meaning: in word2vec, king − man + woman lands near queen.

Picture it

flowchart LR
  T["word id 42"] --> E["embedding table<br/>10,000 rows × 64 columns"] --> R["row 42:<br/>64 learned numbers"]
  R --> S["similar words end up<br/>with similar rows"]
An embedding is a lookup: each id has its own row of learned numbers.

Analogy

A city map where similar shops end up in the same neighbourhood, so distance similarity.

Where the analogy breaks

A map has 2 with names like 'north'. Embeddings have hundreds of dimensions, and they rarely have simple names.

Worked example

10,000 words with dimension 64 gives a 10,000 × 64 . Word id 42 is row 42. A one-hot vector times the matrix gives the same row, but a lookup is much faster.

python
E = np.random.randn(10_000, 64) * 0.01
vecs = E[token_ids]    # shape (len(token_ids), 64)

Must-know points

  • An embedding layer is a learnable lookup table: nn.Embedding(num_items, dim).
  • Lookup = one-hot × E, without building the one-hot.
  • Only the rows used in a get gradients.
  • Similar items → nearby vectors; compare them with cosine similarity.
  • Dimensions rarely have human-readable meanings.
  • embeddings (from language models) power semantic search and (Phase 5).

Common mistakes

  • An id bigger than the table size: an index error.
  • An embedding size so small it can't separate items, or so large it small data.
  • Comparing embeddings from two different as if they shared a space.

Step 2 Go further (optional)

Optional. The lesson above is written to be enough on its own. If you want a second explanation, start with the one marked Worth it.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words, with no notes.

    Explain embeddings with the city-map picture.

    Show answer

    Picture a city map where every shop has a location, and similar shops end up in the same neighbourhood: bakeries near bakeries, phone shops near phone shops. On that map, distance means similarity, and even directions can mean something, like "the cheaper end of town".

    An embedding gives every word, character, product or user a location on such a map, a list of numbers, learned during training. Things that behave alike end up close together, so the model can treat "cat" and "kitten" similarly without being told. Real maps have two coordinates; embeddings use hundreds, which leaves room for many kinds of similarity at once.

  2. DeriveWrite the maths, or the steps, from memory.

    Show that a one-hot vector times an embedding matrix E returns one row of E. Which rows of E get a gradient from one example?

    Show answer

    A one-hot vector for i has a 1 in position i and 0 everywhere else. Multiplying it by E (a V × d matrix):
    (one-hot · E)ⱼ = Σₖ onehotₖ · Eₖⱼ = Eᵢⱼ
    because every term is 0 except k = i. So the result is exactly row i of E. That is why frameworks skip the multiplication and just look the row up.

    Gradient: the output is row i, so the depends only on row i of E (for this token). ∂L/∂E is a matrix that is 0 everywhere except row i, which gets ∂L/∂(output). In matrix form, ∂L/∂E = onehotᵀ · ∂L/∂(output), which puts the gradient into row i only.

    From one example, only the rows of the tokens that appear in it get a gradient. Rare tokens therefore get few updates, which is why their embeddings are learned less well.

    python
    import torch
    E = torch.randn(5, 3, requires_grad=True)
    one_hot = torch.tensor([0., 0., 1., 0., 0.])
    out = one_hot @ E
    print(torch.equal(out, E[2]))           # True: row 2
    out.sum().backward()
    print(E.grad)                           # only row 2 is non-zero
  3. BuildCode it from scratch in NumPy, with no ML library.

    Train makemore-style character embeddings with 2 dimensions and plot them. Which characters end up close together?

    Show answer

    The makemore : take the previous 3 characters, look up a 2-number embedding for each, join them into 6 numbers, pass them through a hidden layer of 100 units, and predict the next character. Train on the 32,000 names, then plot each character at its 2-D embedding.

    The vowels a, e, i, o, u (and often y) usually end up close together, because they play similar roles: they appear in similar positions and are followed by similar letters. "." (start or end of a name) sits apart, and rare letters such as q, x and z drift towards the edges. Nobody told the model what a vowel is; it found the similarity because similar characters help predict the next one in similar ways.

    python
    import urllib.request
    import torch
    import torch.nn.functional as F
    import matplotlib.pyplot as plt
    
    names = urllib.request.urlopen("https://raw.githubusercontent.com/karpathy/makemore/master/names.txt").read().decode().split()
    chars = ["."] + sorted(set("".join(names)))
    stoi = {c: i for i, c in enumerate(chars)}
    X, Y = [], []
    for n in names:
        context = [0, 0, 0]
        for c in n + ".":
            X.append(context); Y.append(stoi[c]); context = context[1:] + [stoi[c]]
    X, Y = torch.tensor(X), torch.tensor(Y)
    
    g = torch.Generator().manual_seed(0)
    C = torch.randn(27, 2, generator=g, requires_grad=True)            # 2-D embeddings
    W1, b1 = (torch.randn(6, 100, generator=g) * 0.4).requires_grad_(), torch.zeros(100, requires_grad=True)
    W2, b2 = (torch.randn(100, 27, generator=g) * 0.1).requires_grad_(), torch.zeros(27, requires_grad=True)
    params = [C, W1, b1, W2, b2]
    for step in range(20_000):
        idx = torch.randint(0, len(X), (64,), generator=g)
        h = torch.tanh(C[X[idx]].view(-1, 6) @ W1 + b1)
        loss = F.cross_entropy(h @ W2 + b2, Y[idx])
        for p in params:
            p.grad = None
        loss.backward()
        for p in params:
            p.data -= (0.1 if step < 15_000 else 0.01) * p.grad
    print("loss:", round(loss.item(), 2))
    
    emb = C.detach()
    plt.scatter(emb[:, 0], emb[:, 1], s=200, c="lightblue")
    for i, ch in enumerate(chars):
        plt.text(emb[i, 0], emb[i, 1], ch, ha="center", va="center")
    plt.show()
    vowels = [stoi[v] for v in "aeiou"]
    print("mean distance between vowels:", round(torch.pdist(emb[vowels]).mean().item(), 2),
          "| between all letters:", round(torch.pdist(emb[1:]).mean().item(), 2))
  4. BreakShow when it fails, and why.

    Pass a token id bigger than the vocabulary and read the error. Then set the embedding size to 1 and see what happens to the loss.

    Show answer

    A token id of 27 or more in a 27-row table: raises "IndexError: index out of range in self". On a the error is often a vague "device-side assert triggered" instead, which is much harder to trace. It usually means the and the model disagree about the vocabulary size, for example after adding new special tokens without resizing the embedding.

    Embedding size 1: every character must sit on a single line, so characters that should be similar in some ways and different in others can't be placed well. The loss ends noticeably higher than with 2 or 10 dimensions (in the run below, the 1-D model plateaus higher than the 10-D one). Too few dimensions ; very many waste memory and can overfit. Real models use hundreds or thousands.

    python
    import urllib.request
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    table = nn.Embedding(27, 2)
    try:
        table(torch.tensor([27]))
    except IndexError as e:
        print(e)
    
    names = urllib.request.urlopen("https://raw.githubusercontent.com/karpathy/makemore/master/names.txt").read().decode().split()
    chars = ["."] + sorted(set("".join(names)))
    stoi = {c: i for i, c in enumerate(chars)}
    X, Y = [], []
    for n in names:
        ctx = [0, 0, 0]
        for c in n + ".":
            X.append(ctx); Y.append(stoi[c]); ctx = ctx[1:] + [stoi[c]]
    X, Y = torch.tensor(X), torch.tensor(Y)
    for dim in [1, 10]:
        torch.manual_seed(0)
        model = nn.Sequential(nn.Embedding(27, dim), nn.Flatten(), nn.Linear(3 * dim, 100), nn.Tanh(), nn.Linear(100, 27))
        opt = torch.optim.Adam(model.parameters(), lr=3e-3)
        for _ in range(5000):
            idx = torch.randint(0, len(X), (128,))
            opt.zero_grad(); F.cross_entropy(model(X[idx]), Y[idx]).backward(); opt.step()
        with torch.no_grad():
            print(f"embedding size {dim}: loss {F.cross_entropy(model(X), Y).item():.3f}")

Step 4 Prove it

Check you understood

What is an embedding table, concretely?

A matrix with one learnable row of numbers per item; embedding an item means reading its row.

Why not just use one-hot vectors?

They are huge and sparse, and every pair of items is equally far apart, so they carry no notion of similarity.

How do you measure how similar two embeddings are?

Usually cosine similarity: the of the two vectors after scaling each to length 1.

How many does nn.Embedding(10000, 64) have?

10,000 × 64 = 640,000.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish reading. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words, with no notes.
  • Derive Write the maths, or the steps, from memory.
  • Build Code it from scratch in NumPy, with no ML library.
  • Break Show when it fails, and why.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.