Katkat

Implement Multi-Head Attention from Scratch in NumPy

AmazonApplied Scientist · Onsite

Last updated

This problem tests your understanding of Transformer components by requiring an implementation of Multi-Head Scaled Dot-Product Attention using only NumPy, emphasizing matrix manipulations and numerical stability.

Multi-Head Attention Implementation

Implement the MultiHeadAttention class using NumPy. Your implementation should perform the core attention mechanism used in Transformer architectures.

Requirements

  1. Linear Projections: Given input X of shape (batch_size, seq_len, d_model), project into Queries, Keys, and Values using weight matrices Wq, Wk, Wv of shape (d_model, d_model).
  2. Split Heads: Reshape the projected matrices to (batch_size, num_heads, seq_len, d_k) where d_k = d_model / num_heads.
  3. Scaled Dot-Product Attention: Compute Attention(Q, K, V) = softmax((Q @ K.T) / sqrt(d_k)) @ V.
  4. Causal Masking: Optionally apply a mask to prevent tokens from attending to future positions (i.e., setting values in the upper triangle of the attention matrix to negative infinity before softmax).
  5. Output: Concatenate the results from heads and apply a final linear projection Wo.

Function Signature

import numpy as np

class MultiHeadAttention:
    def __init__(self, d_model, num_heads):
        # Initialize weight matrices here
        pass

    def forward(self, X, mask=None):
        # Perform the multi-head attention operation
        # Returns the output tensor of shape (batch_size, seq_len, d_model)
        pass

Constraints

  • d_model will always be divisible by num_heads.
  • Use a numerically stable softmax implementation: exp(x - max(x)).

Example

Input: X (1, 3, 8), num_heads = 2. Internal d_k = 4. The attention scores for each head will be (1, 2, 3, 3) shaped tensors before being concatenated back to (1, 3, 8).

Edge Cases

  • mask=None: Standard self-attention.
  • mask=True: Apply a causal (look-ahead) mask.
  • Single-head vs. Multi-head scenarios.

Hints

Hint 1

To reshape the tensors for heads, use np.transpose() carefully to ensure the dimensions are (batch, heads, seq_len, d_k) before performing dot products.

Hint 2

For the causal mask, create a matrix of ones where the lower triangle is 1 and the rest 0; multiply positions with 0 by a very large negative number before applying softmax.

Hint 3

Remember that the softmax function must be applied across the last dimension (the key-sequence dimension).

Follow-up questions

  • How would you modify this implementation to handle variable-length sequences using padding masks?
  • Explain the memory complexity of the attention mechanism. At what sequence length does this approach become the primary bottleneck?
  • If you were deploying this to a production inference service, what specific NumPy optimizations or BLAS libraries would you look into to improve performance?