Thomas Piacentini
← Workshop

Python exercise

Split heads, merge heads, multi-head attention

Original text · English30 min

Multi-head attention runs several small attentions in parallel. Each head sees one slice of the features. The heads are then joined again and mixed by an output projection. The hard part is the reshape: a wrong axis order gives the right shape but the wrong numbers.

Task

Write three functions.

  1. split_heads(x, n_heads) turns x of shape (B, T, D) into an array of shape (B, H, T, d_head), with H = n_heads and d_head = D / H. Head h holds the columns h * d_head to (h + 1) * d_head - 1 of x, in the same order. So split_heads(x, H)[b, h, t, j] equals x[b, t, h * d_head + j].
  2. merge_heads(x) is the inverse. It turns (B, H, T, d_head) into (B, T, H * d_head), so that merge_heads(split_heads(x, H)) equals x.
  3. multi_head_attention(x, w_q, w_k, w_v, w_o, n_heads) returns the output and the attention weights of multi-head self-attention.

The steps of multi_head_attention are:

  1. Q = x W_q, K = x W_k, V = x W_v. There are no bias terms.
  2. Split Q, K and V into heads.
  3. In each head, compute \mathrm{softmax}(Q_h K_h^T / \sqrt{d_{head}})\, V_h. The softmax is over the key axis. There is no mask.
  4. Merge the head outputs and multiply by W_o.

Input and output

Name Type Shape Meaning
x float array (B, T, D) a batch of B sequences of T tokens with D features
n_heads int the number of heads H
w_q, w_k, w_v float array (D, D) the query, key and value projections
w_o float array (D, D) the output projection
  • split_heads returns a float array of shape (B, H, T, D / H).
  • merge_heads takes an array of shape (B, H, T, d_head) and returns a float array of shape (B, T, H * d_head).
  • multi_head_attention returns a tuple (output, weights). output has shape (B, T, D). weights has shape (B, H, T, T), and weights[b, h, i, j] is the weight that query i gives to key j in head h.

Constraints

  • 1 <= B, T <= 16 and 1 <= H <= D <= 64.
  • When D is not divisible by n_heads, split_heads raises ValueError.
  • Activations can be large. The output must stay finite.
  • Do not change the input arrays.
  • Use numpy only.

Examples

python
x = np.arange(8.0).reshape(1, 2, 4)
split_heads(x, 2)
text
[[[[0. 1.]
   [4. 5.]]

  [[2. 3.]
   [6. 7.]]]]

merge_heads of that result gives x again.

python
x = np.array([[[1.0, 0.0, 0.0, 1.0], [0.0, 2.0, 1.0, 0.0], [1.0, 1.0, 1.0, 1.0]]])
eye = np.eye(4)
output, weights = multi_head_attention(x, eye, eye, eye, eye, 2)
text
output
[[[0.80222419 0.79666372 0.59888791 0.80222419]
  [0.23208206 1.72252957 0.80222419 0.59888791]
  [0.59888791 1.20333628 0.75174492 0.75174492]]]
weights
[[[[0.40111209 0.19777581 0.40111209]
   [0.04538836 0.76791794 0.1866937 ]
   [0.19777581 0.40111209 0.40111209]]

  [[0.40111209 0.19777581 0.40111209]
   [0.19777581 0.40111209 0.40111209]
   [0.24825508 0.24825508 0.50348984]]]]
Code and artifacts 14
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓