Batch Operations with Einsum - Einstein Summation (Einsum)
Implement Batch Operations with Einsum in Python with NumPy. medium exercise with examples, hints, and instant feedback. No setup or signup required.
Batch Operations with Einsum
What you'll build
A function einsum_batch_ops(Q, K, V) that computes batched scaled dot-product attention with two einsums: raw scores, softmax weights and the weighted sum of values.
Background
Batched operations add a letter (here b) that appears in every operand and in the output, so it is carried through untouched: every batch element gets its own independent product. Attention is the canonical example. Each query is compared with every key, the scores are turned into weights with a softmax, and the output is the weighted average of the values.
Formula
With d the feature dimension of Q and K (d = Q.shape[-1]):
- Scores (unscaled):
scores[b, q, k] = sum_d Q[b, q, d] * K[b, k, d] einsum 'bqd,bkd->bqk' - Weights: softmax over the keys (last axis) of the scaled scores:
weights[b, q, :] = softmax(scores[b, q, :] / sqrt(d)) softmax(x)_k = exp(x_k - max(x)) / sum_j exp(x_j - max(x)) - Output:
output[b, q, v] = sum_k weights[b, q, k] * V[b, k, v] einsum 'bqk,bkv->bqv'
Inputs
Q: queries, float array of shape(batch, seq_q, d)K: keys, float array of shape(batch, seq_k, d)V: values, float array of shape(batch, seq_k, d_v)
Expected Return Format
Return a dictionary with these keys:
'scores': raw (unscaled) scoresQ @ K^Tper batch, shape(batch, seq_q, seq_k)'attention_weights':softmax(scores / sqrt(d))over the last axis, shape(batch, seq_q, seq_k); each row sums to 1'output': weighted sum of values, shape(batch, seq_q, d_v)
Edge cases and conventions
- Subtract the row maximum before
expso large scores don't overflow; this doesn't change the softmax. 'scores'is before scaling; only the weights use/ sqrt(d).
Constraints
Use np.einsum for both products (no explicit transposes or @) and NumPy for the softmax.
Follow-up
- What is the time and memory cost in terms of
batch,seq_q,seq_k,d? Which term dominates for long sequences? - Why divide by
sqrt(d)at all?
Examples
Input: Q = np.array([[[1., 0.], [0., 1.]]]) # (1, 2, 2)
K = np.array([[[1., 1.], [2., 0.], [0., 3.]]]) # (1, 3, 2)
V = np.array([[[1., 0.], [0., 1.], [1., 1.]]]) # (1, 3, 2)
r = einsum_batch_ops(Q, K, V)
r["scores"]
Output: array([[[1., 2., 0.],
[1., 0., 3.]]])Query 0 = [1, 0] picks feature 0 of each key; query 1 = [0, 1] picks feature 1.
Input: np.round(r["attention_weights"], 4)
Output: array([[[0.284 , 0.576 , 0.14 ],
[0.1784, 0.0879, 0.7337]]])Softmax of each score row divided by sqrt(2); each row sums to 1.
Input: np.round(r["output"], 4)
Output: array([[[0.424 , 0.716 ],
[0.9121, 0.8216]]])Query 0: 0.284*[1, 0] + 0.576*[0, 1] + 0.140*[1, 1] = [0.424, 0.716].
Practice in Python
import numpy as np
def einsum_batch_ops(Q: np.ndarray, K: np.ndarray, V: np.ndarray) -> dict:
"""
Batched scaled dot-product attention with np.einsum.
Args:
Q: Queries of shape (batch, seq_q, d)
K: Keys of shape (batch, seq_k, d)
V: Values of shape (batch, seq_k, d_v)
Returns:
dict with keys:
'scores': Q @ K^T per batch (unscaled), shape (batch, seq_q, seq_k)
'attention_weights': softmax(scores / sqrt(d)) over keys, shape (batch, seq_q, seq_k)
'output': weights @ V per batch, shape (batch, seq_q, d_v)
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 7 tests, or use the progressive hints.