The problem
Inside a transformer, every position of the text looks at the other positions and decides how much each one matters. To do that, each position has two vectors of numbers: a query (what this position is looking for) and a key (what this position offers). The score between a query and a key is their dot product: large when the two vectors point the same way.
The real arrays have four axes, because many scores are computed at once:
| letter | axis | meaning |
|---|---|---|
| batch | several texts processed together | |
| heads | attention runs times side by side, each with its own queries and keys (explained in the Transformers section) | |
| or | positions | the tokens of the text: query positions, key positions |
| head size | the numbers in one query or key vector |
This exercise is the shape work you will reuse when you build attention, without the attention itself.
Part 1: scaled dot products
For every text and head , the score between query position and key position is:
where:
- is number of the query vector at position (text , head );
- is number of the key vector at position ;
- the sum multiplies the two vectors number by number and adds up: their dot product;
- dividing by keeps the scores around the same size whatever the head size (more on this in the Transformers section).
Example. One text and one head (), , two query positions and one key position.
- Queries: , . Key: .
- Dot products: and .
- Divided by : the scores are and .
The result has shape : one score for each of the 2 query positions and the 1 key position.
np.einsum writes this in one line. You give each axis a letter, and letters that appear in the inputs but not in the output are summed over. Here the output keeps , and is summed.
Part 2: merging the heads
After attention, each head has produced a vector of numbers for every position. The next layer wants, for every position, one long vector with all the heads side by side: head 0's numbers, then head 1's, and so on. So shape must become .
Example. One text, heads, positions, :
- head 0 gives
[0, 1]at position 0 and[2, 3]at position 1;- head 1 gives
[4, 5]at position 0 and[6, 7]at position 1.The right result puts each position's heads together: position 0 is
[0, 1, 4, 5]and position 1 is[2, 3, 6, 7].A plain
x.reshape(1, 2, 4)has the right shape but returns[0, 1, 2, 3]and[4, 5, 6, 7]: it glues together the two positions of head 0, then the two positions of head 1. That is the wrong grouping.
reshape reads the numbers in memory order, last axis fastest, and never moves them. To group by position, first swap the head and position axes with transpose, so the order is , and then reshape.
What to implement
attention_scores(q, k):qhas shape andkhas shape . Return the array of scaled dot products.merge_heads(x):xhas shape . Return shape , whereout[b, t]isx[b, 0, t], thenx[b, 1, t], and so on, side by side.
Requirements
- No Python loops over batch or heads.
merge_headsmust keep each position's heads together, as in the example. The tests catch a plainreshape.
Hints
- For
attention_scores, the einsum string has the form"....,....->....": four letters forq, four fork, four for the output. The two inputs share the letters of , and . - For
merge_heads,transposetakes the new order of the axes: on a table,m.transpose(1, 0)swaps rows and columns.