Index Ai Academy
Phase 5 · Transformers · The transformer

Encoders and embedding models

BERT-style models and where they fit.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

GPT-style decoders get the headlines. Encoders do much of the quiet work in applied AI. They make the embeddings behind the RAG systems from Phase 1. They power rerankers that sort search results. They run classifiers that tag millions of support tickets cheaply. An encoder is built from the same transformer blocks as a decoder, with one change: its attention looks both ways. It also learns through a different training game.

What you'll be able to do

  • Contrast encoders (bidirectional attention) with decoders (causal attention)
  • Explain masked language modeling, the training objective behind BERT
  • Describe how encoders produce embeddings, classifications and reranking scores
  • Choose between encoder, decoder and encoder-decoder models for a task

What it covers

  1. Reading in both directions
  2. A new training game: fill in the blanks
  3. Turning a whole text into one vector
  4. Worked example: averaging without the padding
  5. Classifiers and rerankers
  6. Encoder-decoder models

Reading in both directions

Try to fill the gap in this sentence: "The ___ of the river was muddy after the storm." You can't guess the word from "The" alone. The clue, "of the river", comes after the gap. To understand a word, you often need the words on both sides of it.

GPT-style models can't do that. A decoder, such as GPT, uses a causal mask. Each token may only look at the tokens before it. That rule is necessary for writing text from left to right, one token at a time.

An encoder is a transformer that drops the mask. Every token may look at every other token, before it and after it. This is called bidirectional attention, meaning "both directions". Everything else is the same transformer block you already know.

The code below tests the difference. It makes six random token vectors and runs attention on them twice: once with the causal mask, once without. Then it changes only the last token and checks which outputs moved.

import math
import torch

def attention(x, W_q, W_k, W_v, causal):
    q, k, v = x @ W_q, x @ W_k, x @ W_v
    s = q @ k.T / math.sqrt(q.shape[-1])
    if causal:
        T = x.shape[0]
        s = s.masked_fill(~torch.tril(torch.ones(T, T)).bool(), float("-inf"))
    return torch.softmax(s, dim=-1) @ v

torch.manual_seed(0)
x = torch.randn(6, 8)
W = [torch.randn(8, 8) for _ in range(3)]
x2 = x.clone()
x2[5] = torch.randn(8)                                   # change only the LAST token
for name, causal in [("decoder (causal)", True), ("encoder (bidirectional)", False)]:
    a, b = attention(x, *W, causal), attention(x2, *W, causal)
    changed = [i for i in range(6) if (a[i] - b[i]).abs().max().item() > 1e-6]
    print(f"{name:<24} outputs that changed: {changed}")
It runs right here in your browser, with nothing to install.

Here's what each piece does.

  • attention is the scaled dot-product attention from earlier lessons. The causal switch turns the mask on or off.
  • x2 is a copy of the six tokens. Only token 5, the last one, gets new random numbers.
  • The loop runs each version on the old tokens and the new ones. It lists every position whose output moved by more than one millionth.

The decoder prints [5]. Only the last output changed, because no earlier token can see the last one. The encoder prints all six positions. Changing the last word changed every output, the first one included.

That's what you want for understanding. In "the bank of the river", the meaning of "bank" depends on words that come after it. It's also why an encoder can't write text left to right. If it tried to guess the next word, it would already see the answer. (torch here is the course's in-browser PyTorch subset.)

Quick check
Six tokens go through an encoder. You change token 2 only. Which outputs can change?
  1. Positions 0, 1 and 2
  2. All six positions
  3. Only position 2
  4. Positions 2, 3, 4 and 5
Show the answer

All six positions

With no causal mask, every token looks at token 2, so every output can move. In a decoder, only positions 2 to 5 could change.

A new training game: fill in the blanks

A decoder learns by guessing the next token. That game doesn't work for an encoder. It already sees the whole sentence, so the next token is sitting right there in its input. It could copy the answer and learn nothing.

So encoders play a different game. Think of a fill-in-the-blanks quiz at school. Some words are blanked out, and you use the words around each gap to fill it in. BERT, an encoder released by Google in 2018, introduced this for transformers. The method is called masked language modeling. About 15% of the tokens are chosen and hidden, usually behind a special [MASK] token. The model must guess each hidden word from the context on both sides.

The code below picks the blanks for one product sentence.

import re
import torch

sentence = "The Cedar rain jacket is made from recycled nylon . It is light and fully waterproof ."
tokens = sentence.split()
torch.manual_seed(3)
masked = torch.rand(len(tokens)) < 0.15
inputs = [("[MASK]" if m else t) for t, m in zip(tokens, masked.tolist())]
targets = [t for t, m in zip(tokens, masked.tolist()) if m]
print("model sees:   ", " ".join(inputs))
print("must predict: ", targets)
print(f"training signals from this sentence: {len(targets)} of {len(tokens)} tokens")

Here's what each piece does.

  • sentence.split() cuts the sentence at every space, giving 17 tokens. Real models use a tokenizer, as in the tokenization lesson. Splitting at spaces keeps the example easy to read.
  • torch.rand(len(tokens)) draws a random number between 0 and 1 for each token. A token is hidden when its number is below 0.15. So each token has a 15% chance.
  • inputs is what the model sees, with [MASK] in place of each hidden token. targets lists the hidden words it must guess.

Each token is picked by chance, so one short sentence can get more or fewer than 15% hidden. Over millions of sentences, the share settles very close to 15%.

Here's the catch. Only the hidden positions give a training signal: a guess that gets graded. That's about 15% of the tokens. A GPT-style decoder makes a graded guess at every position. So masked training learns less from each sentence. That's one reason decoder-style pretraining won out for the largest models. (Pretraining is the first, longest stage of training, on huge amounts of text.)

Go deeper Why BERT doesn't always use [MASK]

BERT doesn't always swap a chosen token for [MASK]. Sometimes it puts in a random word. Sometimes it leaves the word as it was. In the original BERT paper, the split was 80% [MASK], 10% random word and 10% unchanged.

The reason: [MASK] never shows up in the real text the model reads later. If every chosen spot said [MASK], the model could learn to pay close attention only there. Mixing things up means any token might be one it gets graded on. So it has to build a good picture of every token.

Quick check
Why can't a BERT-style encoder be used directly to generate text left to right?
  1. It has no vocabulary
  2. Its attention looks at the whole input, including the positions it would be predicting
  3. It doesn't use attention
  4. It can only process numbers
Show the answer

Its attention looks at the whole input, including the positions it would be predicting

Attention in both directions is great for understanding. But when guessing the next word, it would let the model peek at the future.

Turning a whole text into one vector

In Phase 1 you used embeddings. An embedding is a list of numbers that captures what a text means, so you can compare texts by meaning. Most embedding models are encoders underneath.

An encoder gives back one vector per token. A sentence of 12 tokens gives 12 vectors. Search needs one vector per text, so the model squashes the token vectors into one. This step is called pooling. The most common way is mean pooling: take the average. Add up the vectors number by number, then divide by how many there are.

Think of a team sheet. Each player has a score for speed and a score for strength. The team average gives one speed number and one strength number for the whole team. Mean pooling does the same with token vectors.

A plain encoder doesn't make a good embedding model straight away. It's trained further with contrastive learning. The model sees pairs that belong together, such as a question and its answer. Training pulls their vectors closer. It pushes the vectors of unrelated texts apart. The result is an embedding model like the ones used for semantic search and RAG.

In plain words: an embedding model is an encoder plus an averaging step. It's trained so that texts which mean the same thing end up with similar vectors.

Quick check
Predict what this prints. It mean-pools two token vectors, each with two numbers.
vecs = [[2.0, 4.0], [4.0, 0.0]]
print([(a + b) / 2 for a, b in zip(*vecs)])
  1. [6.0, 4.0]
  2. [3.0, 2.0]
  3. [3.0, 3.0]
  4. [2.5, 2.5]
Show the answer

[3.0, 2.0]

Average the first numbers: (2 + 4) / 2 = 3.0. Average the second numbers: (4 + 0) / 2 = 2.0. Pooling works number by number, not token by token.

Worked example

Worked example: averaging without the padding

Texts in a batch have different lengths. Shorter ones get filler tokens, called padding, so they all line up. Padding carries no meaning, so pooling must skip it. Here a text of three tokens has one padding token at the end. Each token vector has two numbers. Work out the mean of the real tokens. Then see what happens if the padding sneaks in.

tokens = [[1.0, 2.0], [3.0, 4.0], [5.0, 0.0], [0.0, 0.0]]   # the last row is padding
keep = [True, True, True, False]
kept = [vec for vec, k in zip(tokens, keep) if k]
mean = [sum(vec[d] for vec in kept) / len(kept) for d in range(2)]
wrong = [sum(vec[d] for vec in tokens) / len(tokens) for d in range(2)]
print("mean of the real tokens:", mean)
print("mean with padding:      ", wrong)
In the lesson you can change this and run it.

The real tokens are [1, 2], [3, 4] and [5, 0]. The first numbers add up to 1 + 3 + 5 = 9, and 9 / 3 = 3. The second numbers add up to 2 + 4 + 0 = 6, and 6 / 3 = 2. So the text's vector is [3.0, 2.0].

Counting the padding adds a row of zeros and divides by 4 instead of 3. That gives [2.25, 1.5], a vector dragged toward zero. The same text would get a different vector depending on how much padding its batch needed. The keep list is the fix. It marks real tokens with True and padding with False. You'll build the same thing with tensors in the exercise.

Classifiers and rerankers

Classifiers. A classifier sorts texts into labels you choose. Is a review positive or negative? Which topic is this email about? Is this comment abusive? To build one, add a small layer on top of the pooled vector that picks a label. Then fine-tune: train the whole model a little more on your own labeled examples. Small encoders do this fast and cheaply. That matters when you have millions of texts.

Rerankers. You met reranking in Phase 1. A search first fetches candidates by comparing embeddings. Then a reranker re-scores the best few. Most rerankers are cross-encoders. A cross-encoder reads the query and one document together, as a single input. It outputs one relevance score.

Why is that more accurate? With separate embeddings, the query and the document never meet. Each is squashed into a vector on its own, and then the two vectors are compared. In a cross-encoder, every query token can attend to every document token. It can notice that "waterproof" in the query matches "fully waterproof" in a product page.

The cost is speed. Document embeddings can be worked out once, ahead of time, and stored. A cross-encoder has to run again for every pair of query and document. So pipelines fetch candidates with embeddings and rerank only the top few with a cross-encoder.

Quick check
Why do search pipelines run a cross-encoder on only the top few candidates?
  1. It's less accurate than comparing embeddings
  2. It can only read short documents
  3. It needs the documents to be translated first
  4. It must run once for every query and document pair, so it's too slow for everything
Show the answer

It must run once for every query and document pair, so it's too slow for everything

Embeddings for documents can be stored ahead of time. A cross-encoder reads each query and document together, so it has to run fresh for every pair. That's accurate but slow, so it gets a short list.

Encoder-decoder models

The original transformer, from 2017, had both halves. Picture an interpreter. First they hear the whole sentence. Then they speak the translation, glancing back at what they heard.

The encoder reads the source sentence, with attention in both directions. The decoder writes the translation one token at a time, with a causal mask. To look back at the source, the decoder uses cross-attention. The queries come from the decoder. The keys and values come from the encoder's outputs. So each word being written can look up the parts of the source it needs.

T5 and BART are well-known encoder-decoder models. Today most general assistants are decoder-only. Encoder-decoders are still common in translation and speech recognition.

Choosing a model

TaskTypical choice
Generate text, chat, write codedecoder (GPT, Claude, Llama)
Embeddings for search / RAGencoder-based embedding model
High-volume classificationsmall fine-tuned encoder (or an LLM prompt if volume is low)
Reranking retrieved passagescross-encoder
Translation, speech-to-textencoder-decoder

For marketers: say you tag a million product reviews by theme every night. That's a job for a small fine-tuned encoder, or for embeddings plus clustering (grouping similar vectors). It's not a job for a chat model at a fraction of a cent per call, a million times a night.

Try it now

The difference comes down to who can see what.

Quick check
A support team must sort 2 million tickets into 10 topics every night. Which choice fits best?
  1. An encoder-decoder translation model
  2. A cross-encoder reranker
  3. A small fine-tuned encoder
  4. A large chat model, prompted once per ticket
Show the answer

A small fine-tuned encoder

High-volume classification is a classic encoder job: fast, cheap and consistent. A prompted chat model would work, but at 2 million calls a night it's slow and expensive.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What does an encoder's attention look like compared with a decoder's?

  1. Every token attends to every token (no causal mask)
  2. Every token attends only to earlier tokens
  3. Tokens don't attend to each other
  4. Only the first token attends

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson