· 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 it's the bag.
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 thing, and the way it does it is called attention.
Part 2's best model could only see three letters back. Here the word that matters is way behind: between "car" and "small" sit 20 other letters and spaces. So the model needs two things, a bigger window and a way to pick what inside it matters. Attention does the picking. (It still can't see past the 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 is here to show why attention exists, and the demos show what the tiny model really looks at. From here on it works on letters, not words, but the idea is the same.
A soft lookup
Think of a library search. What you type is the query. Each book has a label on its spine, its key, and what's inside, its value. A normal search hands back the one book whose label matches best.
Attention is softer. It 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 up the results. That's the dot product. For the lists (2, 1) and (3, 0), it's 2 × 3 + 1 × 0 = 6. Draw each list as an arrow, and the score is bigger when the arrows point the same way. Longer arrows count more.
Part 2's softmax turns those scores into shares that add up to 100%. Those shares are the attention weights. The word then takes a mix of all the values, each scaled by its weight, and that mix is what it walks away with.
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 the thick arrow. 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. Drag it, or focus it and use the arrow keys. 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.
No peeking
When the model writes, it writes one letter at a time, and the next letter doesn't 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. 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. Point at a row to read it, or focus the triangle and use the up and down arrow keys. 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 doesn't 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 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. Point at a letter, or focus the demo and use the left and right arrow keys, to see where each head looks from it. 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. The second head of the first block mostly watches the character just before. In this sentence, that's where it looks hardest at 39 of the 53 positions that have one. The fourth head of the second block has learned to find the start of the current 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 a short version of one attention head. The example has two positions, each with two numbers in its query, key, and value. It uses the queries as keys too. For each position t, it scores the keys from 0 to t with a dot product, turns those scores into weights with part 2's softmax, and mixes the values. The loop stops at j <= t, so it never reads ahead. Dividing the dot product by the square root of the list's length keeps longer lists from making the scores too big for softmax. The tiny model's code stores the three sets of knobs side by side in one table. Press Run to see the last position's weights and mix, or Edit to try other vectors.
function dot(a, b) {
let sum = 0;
for (let i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
function softmax(scores) {
const biggest = Math.max(...scores);
const sides = scores.map((s) => Math.exp(s - biggest));
let total = 0;
for (const side of sides) total += side;
return sides.map((side) => side / total);
}
function attention(queries, keys, values) {
const weights = [];
const found = [];
for (let t = 0; t < queries.length; t++) {
const size = Math.sqrt(queries[t].length);
const scores = [];
for (let j = 0; j <= t; j++) {
scores.push(dot(queries[t], keys[j]) / size);
}
const chances = softmax(scores);
const mix = Array(values[0].length).fill(0);
for (let j = 0; j <= t; j++) {
for (let i = 0; i < mix.length; i++) {
mix[i] += chances[j] * values[j][i];
}
}
weights.push(chances);
found.push(mix);
}
return { weights, found };
}
const queries = [
[1, 0],
[1, 1],
];
const values = [
[2, 0],
[1, 1],
];
const keys = queries;
const { weights, found } = attention(queries, keys, values);
const shares = weights[1].map((share) => share.toFixed(2));
const mix = found[1].map((number) => number.toFixed(2));
console.log('Last weights:', shares.join(', '));
console.log('Last mix:', mix.join(', '));Attention is the part that chooses what to read. The rest of the block keeps that working when you stack more than one.
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, with a smoother squash called GELU, 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 always have a straight path through each addition. That path is the residual connection. It doesn't lock anything in, since a later addition can still cancel what was there.
Before either part reads a notebook, it makes a copy and evens out the numbers, the way a TV's automatic volume keeps a loud channel and a quiet one at about the same level. 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.
And that's it. Attention, the small network, the two leveled copies, and the two additions together make a transformer block, the thing part 1 said ChatGPT is built from. The tiny model has two of them, stacked.
Press step to follow three notebooks through one block. Watch them change twice, once when attention adds its part and once when the small network adds its own. These are made-up teaching numbers, not values from the trained model.
Three token notebooks travel through one block. 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. 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 notebooks hold numbers, not notes. Training decides what those numbers end up being useful for, through the same loss and backward walk as in part 1.
Here is a short version of a block for its first token, with two numbers in its notebook and one head. This token can read only itself, so its attention weight is 1 whatever its query and key are. level makes a leveled copy, and a quarter of that copy is the value attention reads. These are fixed example knobs. The tiny model learns separate tables for its queries, keys, and values.
middle adds attention's change to the original notebooks. The small network reads a leveled copy of that result, scales it, applies gelu, and scales again. gelu is that smoother squash, a smooth way to reduce negative inputs and pass positive ones through. out adds the network's change. Press Run to see the first notebook after each addition, or Edit to change its starting numbers, [1, 2]. The demo's wider block learns all these knobs and keeps the intermediate numbers for training.
function level(notebook) {
let total = 0;
for (const number of notebook) total += number;
const mean = total / notebook.length;
let spread = 0;
for (const number of notebook) {
spread += (number - mean) ** 2;
}
const variance = spread / notebook.length;
const scale = Math.sqrt(variance + 1e-5);
return notebook.map((number) => (number - mean) / scale);
}
function gelu(number) {
const bend = number + 0.044715 * number ** 3;
const squash = Math.tanh(Math.sqrt(2 / Math.PI) * bend);
return 0.5 * number * (1 + squash);
}
function block(notebook) {
const copy = level(notebook);
// The first token reads only itself, with weight 1.
const value = copy.map((number) => number * 0.25);
const middle = [];
for (let i = 0; i < notebook.length; i++) {
const change = value[i] * 0.5;
middle.push(notebook[i] + change);
}
const leveled = level(middle);
const out = [];
for (let i = 0; i < middle.length; i++) {
const weighted = leveled[i] * 0.7;
const hidden = gelu(weighted);
const change = hidden * 0.5;
out.push(middle[i] + change);
}
return { middle, out };
}
const result = block([1, 2]);
const middle = result.middle.map((number) =>
number.toFixed(2),
);
const out = result.out.map((number) => number.toFixed(2));
console.log('After attention:', middle.join(', '));
console.log('After network:', out.join(', '));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 doesn't change its knobs.
That's every piece. Attention picks what to read, the small networks work on what it found, and training tunes all their knobs together so the next-letter guesses get less surprising. In part 4 you get to train the whole thing yourself, on Shakespeare, in this browser tab.
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