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.
split_heads(x, n_heads)turnsxof shape(B, T, D)into an array of shape(B, H, T, d_head), withH = n_headsandd_head = D / H. Headhholds the columnsh * d_headto(h + 1) * d_head - 1ofx, in the same order. Sosplit_heads(x, H)[b, h, t, j]equalsx[b, t, h * d_head + j].merge_heads(x)is the inverse. It turns(B, H, T, d_head)into(B, T, H * d_head), so thatmerge_heads(split_heads(x, H))equalsx.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:
- Q = x W_q, K = x W_k, V = x W_v. There are no bias terms.
- Split Q, K and V into heads.
- 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.
- 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_headsreturns a float array of shape(B, H, T, D / H).merge_headstakes an array of shape(B, H, T, d_head)and returns a float array of shape(B, T, H * d_head).multi_head_attentionreturns a tuple(output, weights).outputhas shape(B, T, D).weightshas shape(B, H, T, T), andweights[b, h, i, j]is the weight that queryigives to keyjin headh.
Constraints
1 <= B, T <= 16and1 <= H <= D <= 64.- When
Dis not divisible byn_heads,split_headsraisesValueError. - 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]]]]