Luong's attention (multiplicative)

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 dad_a 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 dad_a 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 u⊤v\mathbf{u}^{\top}\mathbf{v}, 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: si−1⊤hˉj\mathbf{s}_{i-1}^{\top}\bar{\mathbf{h}}_j 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 kk of the decoder means the same thing as coordinate kk 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:

eij=a(si−1,hˉj)=si−1⊤Wahˉj∈R,e_{ij} = a\left(\mathbf{s}_{i-1}, \bar{\mathbf{h}}_j\right) = \mathbf{s}_{i-1}^{\top}\mathbf{W}_a\bar{\mathbf{h}}_j \in \mathbb{R},

with Wa∈Rdh×dh\mathbf{W}_a \in \mathbb{R}^{d_h \times d_h}. Read it from right to left: Wahˉj\mathbf{W}_a\bar{\mathbf{h}}_j is the encoder state rewritten in the decoder's coordinates, and the dot product with si−1\mathbf{s}_{i-1} 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 dad_a drops out of the problem.

Count the weights: dh2d_h^2, and as before, none of them depends on the pair or on the lengths. With dh=64d_h = 64 that's 4 0964\,096, against the 4 1284\,128 the additive score needed with da=32d_a = 32. 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, si\mathbf{s}_i, 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 aa, 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:

∇Waeij=si−1hˉj⊤,∇hˉjeij=Wa⊤si−1,∇si−1eij=Wahˉj.\nabla_{\mathbf{W}_a} e_{ij} = \mathbf{s}_{i-1}\bar{\mathbf{h}}_j^{\top}, \qquad \nabla_{\bar{\mathbf{h}}_j} e_{ij} = \mathbf{W}_a^{\top}\mathbf{s}_{i-1}, \qquad \nabla_{\mathbf{s}_{i-1}} e_{ij} = \mathbf{W}_a\bar{\mathbf{h}}_j.

None of them carries a factor 1−tanh⁡21 - \tanh^{2}, because there's no tanh⁡\tanh to differentiate. And everything above eije_{ij} in the chain is the previous lesson's, without a comma changed: the step through the softmax and the derivative

∂ℓ/∂eij=αij(∇ciℓ)⊤(hˉj−ci)\partial\ell/\partial e_{ij} = \alpha_{ij}\left(\nabla_{\mathbf{c}_i}\ell\right)^{\top}\left(\bar{\mathbf{h}}_j - \mathbf{c}_i\right)

never mentioned what form aa had. The attention stays as it is; only the alignment model changes.

And the accumulation over the pairs is a product too

Wa\mathbf{W}_a is one matrix for all Tx⋅TyT_x \cdot T_y pairs, so its gradient adds them all up:

∇Waℓ=∑i=1Ty∑j=1Tx∂ℓ∂eij si−1hˉj⊤,\nabla_{\mathbf{W}_a}\ell = \sum_{i=1}^{T_y}\sum_{j=1}^{T_x} \frac{\partial \ell}{\partial e_{ij}}\,\mathbf{s}_{i-1}\bar{\mathbf{h}}_j^{\top},

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 Wa\mathbf{W}_a equal to the identity, which leaves the score as

eij=si−1⊤hˉj,e_{ij} = \mathbf{s}_{i-1}^{\top}\bar{\mathbf{h}}_j,

and the function aa 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 (dhd_h 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 Hˉ∈RTx×dh\bar{\mathbf{H}} \in \mathbb{R}^{T_x \times d_h} (row jj is hˉj⊤\bar{\mathbf{h}}_j^{\top}, transposed because in this course vectors are columns) and the decoder's in S∈RTy×dh\mathbf{S} \in \mathbb{R}^{T_y \times d_h}, whose row ii is si−1⊤\mathbf{s}_{i-1}^{\top}, the state that scores step ii. Then the Tx⋅TyT_x \cdot T_y scores are a single object:

A=softmax(SWaHˉ⊤)∈RTy×Tx,\mathbf{A} = \text{softmax}\left(\mathbf{S}\mathbf{W}_a\bar{\mathbf{H}}^{\top}\right) \in \mathbb{R}^{T_y \times T_x},

with the softmax applied by rows, that is, over the input positions, as in the lesson on the idea of attention. Check entry (i,j)(i, j): row ii of SWa\mathbf{S}\mathbf{W}_a is si−1⊤Wa\mathbf{s}_{i-1}^{\top}\mathbf{W}_a and column jj of Hˉ⊤\bar{\mathbf{H}}^{\top} is hˉj\bar{\mathbf{h}}_j, so their product is eije_{ij} 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 tanh⁡\tanh sits between the sum and the read-out, so a three-dimensional block (one entry per pair, and dad_a coordinates within each) has to be materialised before it can be reduced to numbers.

On the left, a three-dimensional block of cells labelled T sub y by T sub x by d sub a, with a label saying a sum, a tanh and a read-out in every cell; an arrow reduces it to a flat grid of T sub y by T sub x. On the right, two flat rectangles, a tall one of T sub y by d sub h labelled S times W sub a and a wide one of d sub h by T sub x labelled H bar transposed, joined by a product sign; an arrow takes them to a flat grid identical to the one on the left.
The two scores reach the same grid. The additive one first builds one number per pair and per coordinate of the alignment model; the multiplicative one goes from the states to the grid with two products and nothing in between.

The counts put a price on that difference. Take Tx=Ty=60T_x = T_y = 60 and dh=da=256d_h = d_a = 256, a long sentence and a small model. In products and sums the two are close: about 8.88.8 million for the additive score against about 4.94.9 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 tanh⁡\tanh 921 600921\,600 times and the multiplicative one never does. Above all, the additive score has to store those 921 600921\,600 numbers (7.47.4 MB for each pair of sentences, before multiplying by the batch size), while the multiplicative one needs only the 3 6003\,600 scores and the 6060 states already projected. Between the two, the factor is exactly dad_a: one number per pair against dad_a 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 tanh⁡\tanh 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 S\mathbf{S} means having all TyT_y decoder states, and the decoder is recurrent: si−1\mathbf{s}_{i-1} needs ci−1\mathbf{c}_{i-1}, which needs row i−1i-1 of A\mathbf{A}, which needs si−2\mathbf{s}_{i-2}. 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 WaHˉ⊤\mathbf{W}_a\bar{\mathbf{H}}^{\top}, which depends only on the source and needs computing once per sentence, leaving each decoder step a single matrix-vector product:

(HˉWa⊤)si−1∈RTx,\left(\bar{\mathbf{H}}\mathbf{W}_a^{\top}\right)\mathbf{s}_{i-1} \in \mathbb{R}^{T_x},

row ii 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 Wa\mathbf{W}_a equal to the identity, the score reduces to the dot product of the two states.

import numpy as np

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))))
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 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 0.8510.851 when an even share would be 0.1670.167), 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 tanh⁡\tanh: it bounds each coordinate before va\mathbf{v}_a 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 Wa\mathbf{W}_a 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 numpy as np
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))
numpy

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 tanh⁡\tanh and the 7.47.4 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 dad_a to 6464 and run the cell again: the additive score now does less arithmetic than the multiplicative one (2.22.2 million operations against 4.94.9) 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 Wa\mathbf{W}_a add over scoring with the bare dot product, eij=si−1⊤hˉje_{ij} = \mathbf{s}_{i-1}^{\top}\bar{\mathbf{h}}_j?

An encoder and a decoder with dh=64d_h = 64 coordinates each. How many weights does the multiplicative score have?

weights

A margin of ±0 is accepted.

You score a pair of sentences with Tx=Ty=60T_x = T_y = 60 tokens each, using dh=da=256d_h = d_a = 256. What does the additive score store that the multiplicative one doesn't?

With the multiplicative score, can a recurrent decoder compute the whole grid SWaHˉ⊤\mathbf{S}\mathbf{W}_a\bar{\mathbf{H}}^{\top} in one go while it translates?

Write multiplicative_alignment(S, H, Wa). It takes the decoder states S, of shape (Ty,ds)(T_y, d_s) (row ii is si−1\mathbf{s}_{i-1}), the encoder states H, of shape (Tx,dh)(T_x, d_h), and the score's matrix Wa, of shape (ds,dh)(d_s, d_h). It returns the matrix A\mathbf{A} of shape (Ty,Tx)(T_y, T_x), already normalised by rows.

The function computes the lesson's score, eij=si−1⊤Wahˉje_{ij} = \mathbf{s}_{i-1}^{\top}\mathbf{W}_a\bar{\mathbf{h}}_j, 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 aa, which is where its weight is decided, and once as a term of ci\mathbf{c}_i, 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.

Was this helpful?
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
    paperLuong, Pham and Manning, 2015arXiv:1508.04025EN

    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.