The idea of attention: looking back

The idea of attention: looking back

24 min read

Letting the decoder read the whole input sounds like a fix, but it isn't an architecture yet. The previous lesson, on the context-vector bottleneck, measured what summarising costs and left the unused material within reach: the TxT_x states the encoder computes and nothing touches again. Everything else is still missing. What exactly does the decoder receive at step ii, who builds it, and out of what?

Before any of that, there's an obstacle of shape, and it decides the answer. There are as many encoder states as the sentence has tokens: six for one sentence, thirty for another. The decoder, on the other hand, has a size fixed before it sees any sentence (its weight matrices have a set number of columns), so laying the states out in a row and multiplying them by a matrix runs into the same wall as the lesson on why the multilayer perceptron fails on sequences: an input whose length changes doesn't fit a matrix whose shape doesn't. Reading all the states will have to mean something else: combining them into a single vector of the size the decoder already expects. Then the only thing left to decide is how much of each to take.

Take the translation of ayer leí un libro muy bueno as yesterday i read a very good book, whose words line up: ayer is yesterday, leí is i read, un libro is a book and muy bueno is very good. What each output step decides is how much to take from each encoder state, not which single one to keep. That difference is what makes the idea work. If step ii had to point at one state, choosing would be a discrete decision, and further down that turns out to carry a price nobody can pay. Sharing out, on the other hand, is done with numbers. The step that writes book puts nearly all its weight on libro and divides the little that's left among the other tokens; the step that writes very concentrates on muy. Each step gets its own mixture, and that sharing out (one amount per input position, different at every output step) is what we call attention.

The explorable brings back that pair from the lesson on the encoder-decoder architecture, with one row per output step. Go through the rows and look at two places. i and read both pull from the same token, leí, because Spanish carries the pronoun in the verb's ending while English has to write it separately. And book, at step 7, goes back to libro at position 4, after steps 5 and 6 have sat on muy and bueno: the lines cross, which is the reordering the fixed summary had to hold in memory.

I set the weights by hand for the course; what matters is their shape. The right-hand column gives each row's sum, because by eye a row that sums to 0.97 looks the same as one that sums to 1.

A different context at every step

Start with what step ii receives: TxT_x numbers, one per input position, which we call αi1,…,αiTx\alpha_{i1}, \dots, \alpha_{iT_x}. With them, the context for that step is

ci=∑j=1Txαij hˉj∈Rdh,αij≥0,∑j=1Txαij=1.\mathbf{c}_i = \sum_{j=1}^{T_x} \alpha_{ij}\,\bar{\mathbf{h}}_j \in \mathbb{R}^{d_h}, \qquad \alpha_{ij} \geq 0, \qquad \sum_{j=1}^{T_x} \alpha_{ij} = 1.

Look at the two shapes, because the whole point is that they don't match. The list of weights has length TxT_x: it grows with the input, one more weight for every new token, and that's where the information that didn't fit before gets in. The result always has size dhd_h, which is exactly what lets it take the place of the usual context vector without touching the rest of the decoder. The input grew; the slot didn't.

The two conditions on the weights turn that sum into a mixture, and it's worth seeing what they rule out. Since no weight is negative and they all sum to one, each coordinate of ci\mathbf{c}_i falls between the smallest and largest value of that same coordinate across the hˉj\bar{\mathbf{h}}_j: the mixture blends what's already there, and doesn't invent new values or amplify any. The two extremes are easy to write down. If one weight is 11 and the rest are 00, then ci=hˉj\mathbf{c}_i = \bar{\mathbf{h}}_{j} and the mixture has become a hard choice of one state. If they're all 1/Tx1/T_x, then ci\mathbf{c}_i is the mean of the whole sentence, which looks at everything and tells nothing apart. The mechanism lives between those two cases.

The fixed summary was a frozen alignment

There's a third set of weights you already know, though it was never written as one. Take αij=1\alpha_{ij} = 1 when j=Txj = T_x and αij=0\alpha_{ij} = 0 everywhere else, for every step alike. In the explorable's map, that would switch everything off except the column for bueno, lit in all eight rows:

ci=∑j=1Txαij hˉj=hˉTx=c.\mathbf{c}_i = \sum_{j=1}^{T_x} \alpha_{ij}\,\bar{\mathbf{h}}_j = \bar{\mathbf{h}}_{T_x} = \mathbf{c}.

That's the encoder-decoder of the previous two lessons, whole, with nothing taken away and nothing added. And it's a perfectly legal alignment: the weights are non-negative and they sum to one. So attention doesn't replace that architecture or compete with it: it contains it. What attention does is unfreeze weights that had been fixed by hand on the last token, the same for every ii. The bottleneck didn't come from dhd_h being small. It came from that row being unable to move.

The context enters the decoder as one more input

The figure puts the two halves together at an arbitrary step, step ii. At the top is the encoder, with its TxT_x states. A thread runs from each one to ci\mathbf{c}_i, and each thread's thickness is its weight αij\alpha_{ij}: here the thickest is the one from hˉ2\bar{\mathbf{h}}_2, so ci\mathbf{c}_i mostly resembles that state, with smaller contributions from the other two. On the right is the decoder cell, which receives three things: its previous state, si−1\mathbf{s}_{i-1}, from above; the token written at the previous step, yi−1y_{i-1}, from below; and ci\mathbf{c}_i from the left, which is the only new one.

At the top, three encoder cells labelled h bar sub one, sub two and sub T x, linked from left to right, each receiving an input token from above: x sub one, x sub two, x sub T x. From the bottom of the three cells come three green threads of different thickness, labelled alpha sub i one, alpha sub i two and alpha sub i T x; the middle one is the thickest. The three converge on a green box labelled c sub i, with the note "d sub h numbers" beneath it. From that box a horizontal green arrow runs right into a decoder cell labelled s sub i. That cell also receives the previous state, s sub i minus one, from above, and the previous token, y sub i minus one, from below; on the right it puts out y hat sub i.
The threads leave every encoder cell, not only the last, and each one's thickness is its weight. The previous architecture is this same drawing with a single thread, always the one on the right.

The context enters the decoder's recurrence as one more term, with its own weight matrix Wch∈Rdh×dh\mathbf{W}_{ch} \in \mathbb{R}^{d_h \times d_h}:

si=tanh⁡(Wxhdecoyi−1+Whhdecsi−1+Wchci+bhdec),\mathbf{s}_i = \tanh\left(\mathbf{W}^{\text{dec}}_{xh}\mathbf{o}_{y_{i-1}} + \mathbf{W}^{\text{dec}}_{hh}\mathbf{s}_{i-1} + \mathbf{W}_{ch}\mathbf{c}_i + \mathbf{b}^{\text{dec}}_h\right),

where oyi−1\mathbf{o}_{y_{i-1}} is the one-hot vector of yi−1y_{i-1}, which in training is the correct token (teacher forcing) and when generating is the one the model itself wrote at the previous step. What comes out on the right of the figure is the same as in the encoder-decoder architecture:

y^i=softmax(Whysi+by)∈R∣Vy∣,\hat{\mathbf{y}}_i = \text{softmax}\left(\mathbf{W}_{hy}\mathbf{s}_i + \mathbf{b}_y\right) \in \mathbb{R}^{\lvert V_y \rvert},

one probability for each entry of the output vocabulary. When generating, the step writes the most probable entry, and that token becomes the yiy_i that enters from below at the next step. The context doesn't reach y^i\hat{\mathbf{y}}_i directly: it goes through si\mathbf{s}_i first.

The starting state, on the other hand, does change. In the encoder-decoder lesson the decoder started at s0=c\mathbf{s}_0 = \mathbf{c}, because there was one c\mathbf{c} and only one. Now there isn't, so we take s0=0\mathbf{s}_0 = \mathbf{0}, like the recurrence of the previous block. That's a choice, not a necessity (the usual alternative is to start from a learned projection of hˉTx\bar{\mathbf{h}}_{T_x}), and nothing that follows depends on it.

With that, we can rewrite the uncomfortable line from the lesson on the encoder-decoder architecture:

P(yi∣y<i, c)⟶P(yi∣y<i, ci).P\left(y_i \mid y_{<i},\, \mathbf{c}\right) \quad\longrightarrow\quad P\left(y_i \mid y_{<i},\, \mathbf{c}_i\right).

The change looks like a subscript, but it changes what each step conditions on. The old equality claimed that everything the input had to say about any position of the output fitted in dhd_h numbers fixed once and for all; this one claims something much weaker: that for each position, some mixture of the states will do. And since ci\mathbf{c}_i is built from all TxT_x states, step ii is conditioning on the complete source again. The assumption hasn't gone away. It has become cheap.

Where the weights come from

That leaves the question everything rests on: who decides the αij\alpha_{ij}? They can't be fixed in advance (that's exactly what was unfrozen above). Nor can they depend only on jj, the input position (and with it the encoder state hˉj\bar{\mathbf{h}}_j being weighted), because then every step would look at the same place. They have to depend on the pair: on the step ii the decoder has reached and on what sits at position jj. So at step ii the mechanism scores TxT_x pairs, the decoder's state against each of the encoder's states:

eij=a(si−1, hˉj),j=1,…,Tx,e_{ij} = a\left(\mathbf{s}_{i-1},\, \bar{\mathbf{h}}_j\right), \qquad j = 1, \dots, T_x,

where aa is a function that returns a number (how compatible that decoder state and that encoder state are), and it uses the previous state si−1\mathbf{s}_{i-1} for a reason of order, not taste: computing si\mathbf{s}_i requires already having ci\mathbf{c}_i, which requires the weights, which require the scores. The latest state that exists when it's time to score is si−1\mathbf{s}_{i-1}. That's why the scores come out row by row: those of step ii can't be computed until step i−1i-1 has finished.

The scores are raw numbers that can be negative, so they have to be turned into a mixture. A softmax does that, and it's worth noticing what it runs over:

αij=exp⁡(eij)∑k=1Txexp⁡(eik).\alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T_x} \exp(e_{ik})}.

The weights come out positive because of the exponential and sum to one because of the denominator, which are the mixture's two conditions. As a bonus, the sharing is smooth: a slightly larger score takes a bit more of the weight, not all of it.

Two consequences follow, and the first is a bill. There's one score for every pair of step and position, so translating a sentence costs Tx⋅TyT_x \cdot T_y evaluations of aa: the price grows with the product of the two lengths. The fixed summary paid nothing, and that's exactly what was choking it. Still, it's worth knowing from the start that looking back doesn't come free.

The second explains why the softmax is used instead of something more direct. The natural move would be to keep the best-scored state, arg⁡max⁡jeij\arg\max_j e_{ij}, and save the whole sum. But that returns a position, and a position is a whole number: nudge the parameters of aa by a millionth and the index doesn't budge, until one day it jumps. The derivative of ci\mathbf{c}_i with respect to those parameters is zero wherever it exists, so the loss would have no way of teaching aa to score better. With the softmax, ci\mathbf{c}_i depends continuously on the scores, and the chain rule passes through the weights the same way it passes through anything else. Choosing smoothly is what makes choosing learnable.

The function aa is still missing, and this lesson doesn't define it. All it needs from aa is that it returns a number and is differentiable; what form it takes, how many parameters it carries and how those parameters are trained are the next lesson's business.

The per-step context, in NumPy

The cell takes the explorable's alignment (my numbers again) and one state per input token, and does three things. It checks the one property you can't see by looking at the map: that every row sums to one. It builds all eight contexts in one go, because mixing every row at once is a matrix product. And it repeats the calculation with the frozen alignment, so you can compare the two.

import numpy as np

# The explorable's alignment: row = output step, column = input position.
SOURCE = ["ayer", "leí", "un", "libro", "muy", "bueno"]
TARGET = ["yesterday", "i", "read", "a", "very", "good", "book", "<EOS>"]
alpha = np.array([[0.86, 0.06, 0.03, 0.02, 0.02, 0.01], # yesterday
[0.09, 0.74, 0.05, 0.07, 0.03, 0.02], # i <- from "leí"
[0.04, 0.81, 0.04, 0.06, 0.03, 0.02], # read <- from "leí" again
[0.02, 0.05, 0.72, 0.15, 0.03, 0.03], # a
[0.02, 0.03, 0.04, 0.06, 0.79, 0.06], # very
[0.02, 0.02, 0.03, 0.05, 0.10, 0.78], # good
[0.02, 0.03, 0.06, 0.76, 0.05, 0.08], # book <- back to "libro"
[0.02, 0.03, 0.03, 0.12, 0.07, 0.73]]) # <EOS>
# One state per input token: d_h = 5 coordinates in (-1, 1), like a tanh.
H = np.array([[ 0.82, -0.31, 0.10, 0.45, -0.62], [-0.55, 0.71, 0.28, -0.14, 0.36],
[ 0.12, 0.44, -0.77, 0.21, 0.08], [ 0.39, -0.68, 0.52, 0.77, -0.11],
[-0.24, 0.15, 0.63, -0.58, 0.49], [ 0.66, 0.29, -0.35, 0.18, 0.74]])
T_y, T_x = alpha.shape

# What the map doesn't let you see, and without which this wouldn't be a mixture:
assert np.allclose(alpha.sum(axis=1), 1.0), "some row doesn't sum to 1"

C = alpha @ H # c_i = sum_j alpha_ij * h_j, all T_y at once
fixed = np.zeros_like(alpha); fixed[:, -1] = 1.0 # the fixed summary, written as an alignment
C_fixed = fixed @ H

print("step writes pulls mostly from weight distance to the fixed summary")
for i in range(T_y):
j = int(alpha[i].argmax())
print(" %d %-11s %-18s %.2f %.2f"
% (i + 1, TARGET[i], SOURCE[j], alpha[i, j], np.linalg.norm(C[i] - C_fixed[i])))

print()
print("distinct contexts: with attention %d of %d, with the fixed summary %d of %d"
% (len(np.unique(C.round(9), axis=0)), T_y,
len(np.unique(C_fixed.round(9), axis=0)), T_y))
print("and that one is the state of '%s':" % SOURCE[-1], np.allclose(C_fixed[0], H[-1]))
numpy

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

The middle column is the map read out loud, and the last two lines are the finding: with the alignment above, the eight steps receive eight different vectors, and with the frozen one they receive the same vector eight times, the state of the input's last token. The last column shows how much each step was missing, and it deserves a slow look, because not every step misses the same amount. The two small values, 0.250.25 and 0.320.32, belong to good and to <EOS>, the end-of-sequence token, which are the only two steps that pull from bueno, the very token the fixed summary carried inside it. The values for yesterday and book are above 1.351.35. The fixed summary wasn't equally bad everywhere: it got things right where the sentence ends and wrong everywhere else, which is where it broke down in the previous lesson. Change one weight in the alpha matrix and run the cell again: the assert complains before you can look at anything, which is exactly its job.

Test your intuition

Four questions: what the softmax normalises over, what model you get by freezing the alignment, why mix instead of choosing, and how much it costs.

In αij=exp⁡(eij)/∑kexp⁡(eik)\alpha_{ij} = \exp(e_{ij}) / \sum_k \exp(e_{ik}), what does kk run over in the denominator?

You set αij=1\alpha_{ij} = 1 when j=Txj = T_x and αij=0\alpha_{ij} = 0 everywhere else, the same at every step ii. What model are you left with?

Instead of mixing, step ii could keep the best-scored state: ci=hˉj⋆\mathbf{c}_i = \bar{\mathbf{h}}_{j^{\star}} with j⋆=arg⁡max⁡jeijj^{\star} = \arg\max_j e_{ij}. What breaks?

You translate ayer leí un libro muy bueno (six tokens), and the decoder writes seven words plus the end-of-sequence token <EOS>: eight steps. How many scores eije_{ij} does the mechanism compute for that one sentence?

scores

A margin of ±0 is accepted.


The mechanism is complete except for one piece, and it's the piece that decides everything else. Nobody computed this lesson's weights: I wrote them into a matrix, choosing them so that the map would show a reordering and a shared token. A real model has no one to write them for it. It has to score each pair (si−1,hˉj)(\mathbf{s}_{i-1}, \bar{\mathbf{h}}_j) on its own, with a function that starts out knowing as little as the rest of the network and learns alongside it.

That function is aa, and the next lesson, on Bahdanau's attention, is the one that writes it. It gives aa its own parameters (a small layer, with its matrix and its vector), puts it into the same gradient descent that already trains the encoder and the decoder, and derives how the gradient reaches it, which turns out to be along two routes at once. The rest of the block then shows that there's more than one reasonable way to score a pair, and that the choice between them ends up deciding what Block 5 looks like.

Was this helpful?
Further reading2 sources · 1 paper, 1 article

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.