· ai
Paying attention
Read this sentence. "The bag didn't fit in the car because it was too small." What was too small? The car. Now change one word. "The bag didn't fit in the car because it was too big." Now the bag was too big.
You did that without thinking. When you reached "small", you looked back at the earlier words and picked the one that fit. A model has to do the same, and the way it does it is called attention.
Part 2's best model saw three letters back. Here the word that matters is far behind. Between "car" and "small" sit 20 other letters and spaces. The model needs a larger window and a way to choose which pieces inside it matter. Attention makes that choice. It still cannot read beyond its context window.
The model in this series is too small to solve that sentence. It learned from Shakespeare's plays, one letter at a time. So the sentence explains why attention exists, and the demos show what the tiny model really looks at.
A soft lookup
Think of a library search. You type what you are looking for. That is the query. Each book has a label on its spine, its key, and what is inside, its value. A normal search hands back the one book whose label matches best.
Attention reads a little of every book, and more of the ones whose labels match better. To score a match, multiply two lists of numbers pair by pair and add the results. This calculation is the dot product. For the lists 2, 1 and 3, 0 that is 2 × 3 + 1 × 0 = 6. Draw each list as an arrow, and the total is bigger when the arrows point the same way. Longer arrows count more.
Part 2's softmax turns the match scores into shares that add up to 100%. Those shares are the attention weights. Then the word adds up the values, each scaled by its weight, and that mix is what it takes away.
In the demo, four earlier words from the sentence are arrows, and each arrow is that word's key. The thick arrow is the query for "small". Drag it, or focus it and use the arrow keys. The bars show how much "small" reads from each word, and the chart under them shows the four values and the mix that "small" gets back.
Four earlier words of the sentence, bag, fit, car, and because, drawn as arrows from the middle of a flat chart. A thicker arrow is the query for small. Bars show the softmax of the dot product of the query with each word's arrow, and a second chart shows each word's value and the mix of values that small gets back. At the start small reads car 65.6%, bag 25.1%, because 6.1%, and fit 3.1%.
Move the query toward a word's arrow and watch its share change. Both the direction and the length of each arrow affect the match. Point it between two words and they share. Pull it in toward the middle and the shares even out, because every score shrinks toward 0. At the very middle each word gets 25%.
These arrows are made up, with two numbers each so they fit on a flat chart. In a real model, each word's query, key, and value come from its own list of numbers through three sets of learned knobs, the same knobs at every position. The tiny model starts each letter as a list of 32 numbers. Its code stores the three sets of knobs side by side in one table.
No peeking
When the model writes, it writes one letter at a time, and the next letter does not exist yet. So a letter may read itself and the letters before it, never the ones after. That rule is the causal mask.
The triangle below is the trained model reading the sentence. Each row is a letter doing the reading, and each column is a letter being read. A square's area is how much the row's letter reads the column's letter, and each row adds up to 100%. Point at a row to read it, or focus the triangle and use the up and down arrow keys. Nothing sits above the diagonal, because no letter reads ahead.
| Letter | Reads most | Then | Then |
|---|
A triangle of squares for the 54 letters and spaces of the sentence, one row for each letter doing the reading and one column for each letter being read. Each square's area is how much the row's letter reads the column's letter, in the fourth head of the trained model's second block. No square sits above the diagonal.
The model has eight of these triangles, and the next section shows why. This one often puts its biggest square on a space. Read the row for any letter after the first in a word, and the biggest square is usually the space just before that word.
Attention on its own does not know which letter came first. Shuffle the earlier letters, and each one gets the same score it had before. So each position also gets a learned list of numbers added to its letter's list, its position embedding, like a seat number. The tiny model has 64 seats, so it reads at most the last 64 letters.
Several readers at once
One attention makes one mix of what it reads. A model runs several side by side, each with its own knobs for queries, keys, and values, so each can learn to look for something different. Each one is a head, and together they are multi-head attention. The tiny model repeats attention followed by a small network twice. Each repeat is a block, with four heads, so the model has eight heads in all.
The demo loads the tiny model, trained ahead of time with part 4's trainer. Point at a letter, or focus the demo and use the left and right arrow keys, to see where each head looks from it. Each arc runs from the letter back to a letter it reads, and the darker the arc, the more it reads.
| Head | Reads most |
|---|
Eight strips, one for each head of the trained model, each with the 54 letters and spaces of the sentence. Arcs run from one letter back to the letters it reads, darker for a bigger share. It starts at the last letter of small.
Nobody told the heads what to look for. Training found it. In this sentence, the second head of the first block gives its largest weight to the previous character at 39 of the 53 positions that have one. The fourth head of the second block often reads the space before a word. Choose a character after a word's first letter, with a space before the word. At 20 of those 29 positions, this head gives that space its largest weight.
The heads of the first block read close by. Averaged over the sentence after its first letter, each head puts about 80% to 85% of its weight on the letter itself and the two before it. So the four overlap. The heads of the second block reach further, 3.6 to 6.6 letters back on average over the same positions.
For readers who know JavaScript, here is the attention the tiny model runs. lookups holds each letter's query, key, and value side by side, and each head reads its own 8 of each letter's 32 numbers. For each letter t and each head, it scores every letter from 0 to t with a dot product, turns the scores into weights with softmax, and adds up the values. The loop stops at t, and that is the causal mask. It also divides each score by Math.sqrt(size), about 2.83 here, so that longer lists do not make the scores too big for softmax.
function attend(lookups: Floats, { length, width, heads }: Sizes) {
const size = width / heads;
const scale = 1 / Math.sqrt(size);
const rows = lookups.length / (3 * width * length);
const found = like(lookups, rows * length * width);
const weights = like(lookups, rows * heads * length * length);
for (let r = 0; r < rows; r++) {
for (let t = 0; t < length; t++) {
for (let h = 0; h < heads; h++) {
const query = (r * length + t) * 3 * width + h * size;
const row = ((r * heads + h) * length + t) * length;
let biggest = -Infinity;
for (let s = 0; s <= t; s++) {
const key = (r * length + s) * 3 * width + width + h * size;
let dot = 0;
for (let i = 0; i < size; i++) dot += lookups[query + i] * lookups[key + i];
weights[row + s] = dot * scale;
if (weights[row + s] > biggest) biggest = weights[row + s];
}
let total = 0;
for (let s = 0; s <= t; s++) {
weights[row + s] = Math.exp(weights[row + s] - biggest);
total += weights[row + s];
}
const out = (r * length + t) * width + h * size;
for (let s = 0; s <= t; s++) {
weights[row + s] /= total;
const value = (r * length + s) * 3 * width + 2 * width + h * size;
for (let i = 0; i < size; i++) found[out + i] += weights[row + s] * lookups[value + i];
}
}
}
}
return { found, weights };
}
Keeping what was already there
Each letter carries its list of numbers through the model, like a notebook. Attention reads the notebooks and works out something to add to each one. A small network like part 1's then reads each updated notebook and works out another addition. Attention can mix information between letters. The small network works on each letter separately, with the same learned knobs at every position.
The model adds each change to the numbers already there. If a notebook holds 2 and a part proposes -0.5, the result is 1.5. A change can strengthen or weaken what was there. The old numbers keep a straight path through each addition. This path is called a residual connection. It does not guarantee that the old information survives unchanged. Later additions can cancel it.
Before either part reads a notebook, it takes a copy whose numbers have been brought to a steadier size. Think of a TV's automatic volume control keeping a loud channel and a quiet channel at similar levels. For each letter separately, the model subtracts the average of its numbers and adjusts their spread. Learned knobs can then scale and shift the copy. This is layer normalization. It changes the copy that a part reads, while the original notebook stays on the straight path.
Attention, the small network, the two leveled copies, and the two additions together form a transformer block. The same pattern repeats in each block. The tiny model has two of them.
Press step below to follow one block. The diagram uses three tokens and three made-up numbers per token so every addition is visible. These are teaching numbers, not values from the trained model. Each column is one token's notebook. The first addition comes from attention. The second comes from the small network. The bottom columns show the notebooks now and change at each addition. The darker outline marks the current step.
Three token notebooks travel through one block. Attention reads leveled copies, then adds its changes to the original notebooks. The small network reads leveled copies of those updated notebooks, then adds its own changes. For the first token, 1, 2, 3 becomes 1.5, 1.5, 4 after attention and 0.5, 2, 4 after the small network. The straight path carries the old numbers into each addition.
The notebook metaphor is only about keeping and updating numbers. There are no written facts inside these columns. Training decides what the numbers are useful for, through the same loss and backward walk as in part 1.
For readers who know JavaScript, here is the block the tiny model runs. level produces each leveled copy. The two calls to add are the two residual connections. attention.write mixes the heads' results into one change for each notebook. network.hidden, gelu, and network.write form the small network. gelu is its activation function, a smooth way to reduce negative inputs and pass positive ones through.
The three sets of query, key, and value knobs sit side by side in attention.lookup.weights, a 32 by 96 array. Each letter's 32 input numbers produce 96 output numbers, the first 32 for queries, the next 32 for keys, and the last 32 for values. The layer also adds a learned bias to each output. This is one combined calculation of the three lookups, not a shared set of numbers for all three jobs.
function block(knobs: BlockKnobs, notebooks: Floats, sizes: Sizes): BlockTape {
const { attention, network } = knobs;
const first = level(notebooks, attention.level);
const lookups = multiply(first.out, attention.lookup);
const { found, weights } = attend(lookups, sizes);
const middle = add(notebooks, multiply(found, attention.write));
const second = level(middle, network.level);
const hidden = multiply(second.out, network.hidden);
const fired = gelu(hidden);
const out = add(middle, multiply(fired, network.write));
return { notebooks, first, lookups, weights, found, middle, second, hidden, fired, out };
}
Both code blocks are pieces of the full model, not standalone programs. The block also returns the intermediate numbers so the backward walk can use them during training.
From notebooks to the next letter
The first notebooks start with each letter's embedding plus its position embedding. Each block reads those notebooks and adds its changes twice. Later blocks therefore read numbers that already include information from earlier letters.
After the last block, the model levels one more copy and turns it into a score for every possible next letter. Part 2's softmax turns those scores into chances. To write, the model picks a letter from the last position's chances, adds it to the text, and repeats. That use of the trained model does not change its knobs.
Attention lets a letter choose what to read inside the available context. The small networks process what it found. Training adjusts all their knobs together to make the next-letter guesses less surprising. Part 4 puts those pieces to work on Shakespeare's plays and lets you train the tiny model in your browser.
words from this note
- queryWhat a token is looking for, like the words you type into a library search
- keyWhat a token offers to match against, like a label on a book's spine
- valueThe numbers a token hands over, like the contents of a matching book
- dot productA match score made by multiplying two lists pair by pair and adding the results
- attention weightsThe shares of the values a token reads, adding up to 100%
- causal maskThe rule that a token can read itself and earlier tokens, never later ones
- position embeddingA learned list added to a token's numbers to mark its place, like a seat number
- headOne reader with its own queries, keys, and values, making one mix
- multi-head attentionSeveral heads reading at once, each able to look for different things
- residual connectionAdding a change to the notebook's old numbers, which keep a straight path through
- layer normalizationMaking a leveled copy of each notebook for attention or the small network to read
- transformer blockAttention and a small network, each reading a leveled copy and adding a change to the notebook