Luong's attention (multiplicative)
27 min read
A model with attention scores pairs: one number for each output step and each input position, and those numbers become the weights that mix the encoder states. The previous lesson, on Bahdanau's additive attention, filled the slot that computes them with a small network (a hidden layer of units and a linear read-out, trained along with everything else), and it left one cost standing: scoring a pair means building a whole vector of coordinates only to reduce it to the single number that was wanted.
This lesson asks whether it takes a hidden layer to say that two vectors correspond. Linear algebra has been producing that number since Block 1 with no network in between, and in 2015 Luong, Pham and Manning proposed using it with a single adjustment. What comes out doesn't look like a network; it looks like a product. And since the two forms are comparable in quality (the experiments in their papers measure that; no derivation does), what decides between them is everything else: what they cost, how much memory they move, and what shape their computation takes.
Go back to what the score has to do. A number that goes up when two vectors point the same way has been in the course since the lesson on the bag of words: the dot product , whose normalised version is cosine similarity. It would be a free score (zero parameters) and a function of the pair, which is the condition the previous lesson declared non-negotiable: can't be split into one opinion about a state plus another about the other.
What fails is a hidden assumption. The dot product multiplies coordinate against coordinate, so it takes for granted that coordinate of the decoder means the same thing as coordinate of the encoder, on the same scale. But these are two different recurrences, with two sets of weights and two jobs (one reads Spanish, the other writes English), and nothing forces them to have chosen the same directions for the same things, or even to have the same number of coordinates. So the fix is to change basis first and measure afterwards.
Multiplying instead of adding
The multiplicative score puts a matrix between the two states and does nothing else:
with . Read it from right to left: is the encoder state rewritten in the decoder's coordinates, and the dot product with then measures what a dot product always measures. There's no hidden layer, no saturation and no space of the function's own, so the previous lesson's drops out of the problem.
Count the weights: , and as before, none of them depends on the pair or on the lengths. With that's , against the the additive score needed with . Practically the same, which is useful to know early: what separates the two scores isn't how many weights they have, but what they do with them.
This is the place to say what the course leaves out. Luong, Pham and Manning also change where the context comes in (they score with the current step's state, , and mix in the context after the recurrence instead of inside it), and this course doesn't follow them. It keeps the placement from the lesson on the idea of attention, so that across that lesson, the previous one and this one, only one thing changes. The comparison is of the function , with everything else held fixed.
The gradient is shorter than the additive score's, for the same reason the computation is. The three derivatives of a score come from differentiating a product of three factors:
None of them carries a factor , because there's no to differentiate. And everything above in the chain is the previous lesson's, without a comma changed: the step through the softmax and the derivative
never mentioned what form had. The attention stays as it is; only the alignment model changes.
And the accumulation over the pairs is a product too
is one matrix for all pairs, so its gradient adds them all up:
which is the grid of derivatives sandwiched between the decoder states and the encoder states (two matrix products, with the grid in the middle). The backward pass has the same shape as the forward pass, which is why the next section holds for both.
Without a matrix: the dot product
The extreme case is to take equal to the identity, which leaves the score as
and the function without a single parameter to train. What it asks in return is exactly what the matrix was there to avoid asking: that the two sides be the same width ( coordinates in both) and that they have also learned to use those coordinates in the same way. That's a lot to demand when the encoder and the decoder are two different networks, each trained for its own language, which is why the matrix is the form Luong's paper takes as its default. The parameter-free variant stays in the course (it's the cheapest way to score that exists), but it only holds up where the two sides share a basis.
The whole grid in one matrix product
So far the lesson has written the scores one at a time. Now stack them. Store the encoder states by rows in (row is , transposed because in this course vectors are columns) and the decoder's in , whose row is , the state that scores step . Then the scores are a single object:
with the softmax applied by rows, that is, over the input positions, as in the lesson on the idea of attention. Check entry : row of is and column of is , so their product is and nothing else.
The additive score can't be written this way, and no amount of cleverness changes that. Its two projections do come out, but the sits between the sum and the read-out, so a three-dimensional block (one entry per pair, and coordinates within each) has to be materialised before it can be reduced to numbers.
The counts put a price on that difference. Take and , a long sentence and a small model. In products and sums the two are close: about million for the additive score against about million for the multiplicative one, a factor of less than two, and in both most of the work goes into the projections. Where they part ways is everything else. The additive score evaluates the times and the multiplicative one never does. Above all, the additive score has to store those numbers ( MB for each pair of sentences, before multiplying by the batch size), while the multiplicative one needs only the scores and the states already projected. Between the two, the factor is exactly : one number per pair against numbers per pair.
There's also a difference of shape that doesn't show up in any of those numbers. The multiplicative score's work is two matrix products; the additive score's is sums and over a three-dimensional block, coordinate by coordinate. The same amount of arithmetic, organised in those two ways, doesn't cost the same on the hardware that trains these networks, which is built precisely for the first (that's what the lesson on the forward pass measured, with a loop against a product).
What's left is the honest part, and it's what opens the next block. Writing means having all decoder states, and the decoder is recurrent: needs , which needs row of , which needs . While the model translates, the grid fills up row by row and the big product never gets to happen. What can come out of the loop is , which depends only on the source and needs computing once per sentence, leaving each decoder step a single matrix-vector product:
row of unnormalised scores. The score is no longer the computation's bottleneck. What strangles the parallelism now is the recurrence that produces the states.
The two scores, in NumPy
The first cell computes the multiplicative alignment the short way and checks that the product says nothing different from scoring the pairs one by one. Look at the last two lines too: with equal to the identity, the score reduces to the dot product of the two states.
T_y, T_x, d_h, d_a = 8, 6, 5, 4
rng = np.random.default_rng(4)
S = rng.normal(size=(T_y, d_h)) * 0.6 # row i: the state s_{i-1} that scores step i
H = rng.normal(size=(T_x, d_h)) * 0.6 # row j: the encoder state at position j
Wa = rng.normal(size=(d_h, d_h)) * 0.4 # untrained
def softmax_rows(E):
Z = np.exp(E - E.max(axis=1, keepdims=True)) # shift invariance
return Z / Z.sum(axis=1, keepdims=True)
E = (S @ Wa) @ H.T # (T_y, T_x): the whole grid, two products
A = softmax_rows(E)
E_loop = np.array([[S[i] @ Wa @ H[j] for j in range(T_x)] for i in range(T_y)])
print("shapes: S", S.shape, " H", H.shape, " Wa", Wa.shape, " -> A", A.shape)
print("every row sums to 1:", bool(np.allclose(A.sum(axis=1), 1.0)))
print("the product and the loop agree:", bool(np.allclose(E, E_loop)))
print("weights of a: multiplicative", d_h * d_h, " additive", 2 * d_a * d_h + d_a)
print("largest weight in each row:", np.round(A.max(axis=1), 3), " uniform:", round(1 / T_x, 3))
print()
# Wa = I: no change of basis, and the score is the dot product of the two states.
identity = softmax_rows((S @ np.eye(d_h)) @ H.T)
print("with Wa = I, row 1:", np.round(identity[0], 3))
print("same as scoring with S @ H.T:", bool(np.allclose(identity, softmax_rows(S @ H.T))))
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
The line that matters is the loop's: scoring pair by pair gives the same numbers as the product, which is all there is to check about the short form. The largest-weight line holds a surprise. With freshly initialised weights the map already shares out unevenly (one row takes when an even share would be ), which didn't happen in the previous lesson at a similar size, where the largest weight in each row hovered around the even share. The difference is the : it bounds each coordinate before reads it, so the additive score starts out small. The multiplicative score has nothing to bound it, and its size grows with the size of the states. That peak isn't an alignment, it's the direction happened to take at random; what the formula guarantees isn't that it starts out saying something, but that there's a route along which to train it. Scores that grow without bound as the model gets larger are a real problem, and the next block brings a one-line fix.
import time
T_y = T_x = 60
d_h, d_a = 256, 256
rng = np.random.default_rng(0)
S, H = rng.normal(size=(T_y, d_h)) * 0.5, rng.normal(size=(T_x, d_h)) * 0.5
Wa = rng.normal(size=(d_h, d_h)) * 0.05
Wsa, Wha = rng.normal(size=(d_a, d_h)) * 0.05, rng.normal(size=(d_a, d_h)) * 0.05
va = rng.normal(size=d_a)
def additive(S, H):
P = (S @ Wsa.T)[:, None, :] + (H @ Wha.T)[None, :, :] # (T_y, T_x, d_a): it has to be built
return np.tanh(P) @ va # (T_y, T_x)
def multiplicative(S, H):
return S @ (Wa @ H.T) # (T_y, T_x): two products and nothing else
t0 = time.perf_counter(); Ea = additive(S, H)
t1 = time.perf_counter(); Em = multiplicative(S, H)
t2 = time.perf_counter()
print("same output shape:", Ea.shape, Em.shape)
print("products and sums: additive %9d multiplicative %9d"
% ((T_x + T_y) * d_a * d_h + T_x * T_y * d_a, T_x * d_h * d_h + T_x * T_y * d_h))
print("tanh evaluations: additive %9d multiplicative %9d" % (T_x * T_y * d_a, 0))
print("intermediate numbers: additive %9d multiplicative %9d"
% (T_x * T_y * d_a, T_x * d_h + T_x * T_y))
print(" %.1f MB against %.2f MB (8 bytes per number)"
% (T_x * T_y * d_a * 8 / 1e6, (T_x * d_h + T_x * T_y) * 8 / 1e6))
print("time: additive %7.1f ms multiplicative %7.1f ms"
% ((t1 - t0) * 1e3, (t2 - t1) * 1e3))
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
Everything above the last line is the previous section told in code: the arithmetic is similar, but the and the MB of intermediates aren't. The last line depends on the machine and the browser, and it still tells you something, because the difference it measures isn't small: the multiplicative score takes around an order of magnitude less time, and the exact factor changes from run to run without ever getting close to one. There's an experiment that settles the question. Change to and run the cell again: the additive score now does less arithmetic than the multiplicative one ( million operations against ) and still takes longer, about twice as long in the runs behind this lesson. What the clock is measuring isn't how many operations there are.
Test your intuition
Four questions: what the matrix adds, how many weights the score carries, what the additive score stores that the other doesn't, and whether the grid can be computed in one go.
What does add over scoring with the bare dot product, ?
An encoder and a decoder with coordinates each. How many weights does the multiplicative score have?
A margin of ±0 is accepted.
You score a pair of sentences with tokens each, using . What does the additive score store that the multiplicative one doesn't?
With the multiplicative score, can a recurrent decoder compute the whole grid in one go while it translates?
Write multiplicative_alignment(S, H, Wa). It takes the decoder states S, of shape
(row is ), the encoder states H, of shape ,
and the score's matrix Wa, of shape . It returns the matrix of shape
, already normalised by rows.
The function computes the lesson's score, , followed by a softmax over the input positions. Write it with matrix products, without looping over the pairs, and don't write into the arrays you receive.
The first run downloads the Python interpreter (~15 MB); after that it stays in the browser cache. This challenge is much easier to solve on a physical keyboard: on a phone, read it and come back later.
With the two scores side by side, something shows that neither could reveal alone. In both, the encoder state enters the mechanism twice, through different doors: once as an argument of , which is where its weight is decided, and once as a term of , which is how its content reaches the decoder. The decoder state, by contrast, enters only once, always from the same side: it's the one asking. That makes three roles, and neither formula requires the first two to be played by the same vector. One vector plays both only because nobody has yet proposed separating them.
Separating them and giving them names (query, key and value) is the job of the next lesson, the last in the block, and it's the most important one in this course. It doesn't change a single equation you've already seen: it rewrites the same ones in the words and symbols Block 5 writes everything in, including the choice of score this lesson has now justified. Nor does the change of names fix what was left pending here (the states still arriving one at a time); that's work for the next block, and it pays to arrive there with the names already in place.
Further reading1 source · 1 paper
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- Effective Approaches to Attention-based Neural Machine Translation
Its §3.1 has the multiplicative score (their "general": s times W_a times h) and the parameter-free variant. They also move the context after the RNN and add input feeding (§3.3); the course keeps only the score.