Skip to content

    Chapter 30, Advanced

    einsum and tensordot

    The `einsum` describes an operation between arrays with a small formula using letters, and replaces dozens of combinations of `transpose`, `reshape`, and `sum` with a readable line.

    Code for this chapter: avancado/cap30_einsum.py

    The formula

    You name each axis with a letter and specify what you want in the result. The rule is short: repeated letters between the inputs are multiplied , and letters thatdo not appear in the output are summed . Matrix multiplication is the classic example:multiplies by the "ij,jk->ik" (repeated) and sums it (because the j is not in the output): j With the same rule, the formula expresses dozens of operations:

    avancado/cap30_einsum.pylinhas 10 a 15
    import numpy as np
    
    A = np.arange(6).reshape(2, 3)
    B = np.arange(12).reshape(3, 4)
    print(np.einsum("ij,jk->ik", A, B))
    print(np.array_equal(np.einsum("ij,jk->ik", A, B), A @ B))
    
    Output
    [[20 23 26 29]
     [56 68 80 92]]
    True
    

    A dictionary of patterns

    Formula

    avancado/cap30_einsum.pylinhas 20 a 25
    M = np.arange(9).reshape(3, 3)
    print(np.einsum("ii->", M), np.einsum("ii->i", M), np.einsum("ij->i", M), np.einsum("ij->j", M))
    
    v = np.array([1, 2, 3])
    w = np.array([4, 5, 6])
    print(np.einsum("i,i->", v, w), np.einsum("i,j->ij", v, w))
    
    Output
    12 [0 4 8] [ 3 12 21] [ 9 12 15]
    32 [[ 4  5  6]
     [ 8 10 12]
     [12 15 18]]
    
    FormulaOperation
    "ij->ji"Transpose
    "ii->"Trace (sum of the diagonal)
    "ii->i"Diagonal
    "ij->i"Sum of each row
    "ij->j"Sum of each column
    "i,i->"Dot product
    "i,j->ij"Outer product (all combinations)
    "bij,bjk->bik"Batch matrix multiplication

    The real use case: attention in neural networks

    The central calculation of language models compares each "query" with each "key", in a batch. In formula, "bqd,bkd->bqk": for each example b, it multiplies the vector of dimension d of each query q by each key k. Without the einsum, you would need to transpose the last dimension manually:

    avancado/cap30_einsum.pylinhas 30 a 34
    rng = np.random.default_rng(0)
    Q = rng.normal(size=(2, 5, 8))
    K = rng.normal(size=(2, 7, 8))
    scores = np.einsum("bqd,bkd->bqk", Q, K)
    print(scores.shape, np.allclose(scores, Q @ K.transpose(0, 2, 1)))
    
    Output
    (2, 5, 7) True
    

    The formula documents the axes: each letter has a meaning, and the reader understands the operation without deciphering a sequence of transpose.

    `tensordot` and the order of multiplications

    The np.tensordot sum over the axes you indicate (the parameter axes), and for two matrices with axes=1 it is equivalent to @. The same operation can be much cheaper depending on the order in which a chain of matrices is multiplied, because multiplication is associative, but the cost is not:

    avancado/cap30_einsum.pylinhas 39 a 51
    import timeit
    
    print(np.allclose(np.tensordot(A, B, axes=1), A @ B))
    
    C1 = rng.normal(size=(2000, 3))
    C2 = rng.normal(size=(3, 2000))
    C3 = rng.normal(size=(2000, 3))
    esquerda = lambda: (C1 @ C2) @ C3
    direita = lambda: C1 @ (C2 @ C3)
    print(np.allclose(esquerda(), direita()))
    t_esq = min(timeit.repeat(esquerda, number=1, repeat=3))
    t_dir = min(timeit.repeat(direita, number=1, repeat=3))
    print("a ordem importa:", t_esq > 3 * t_dir)
    
    Output
    True
    True
    a ordem importa: True
    

    The first way creates a 2000 by 2000 matrix in the middle of the process, and the second reduces everything to a 3 by 3 matrix first. The result is the same, and the cost, very different. The np.linalg.multi_dot chooses the best order for you, and the einsum with optimize=True does the same.

    When I don't use einsum

    For a simple matrix multiplication, the @ is clearer and uses the optimized libraries directly. The einsum pays off when there are more than two inputs, batch axes, or unusual sums, and when the formula documents better than the equivalent code.

    Exercise 1

    Cosine similarity between all pairs

    Write similaridade_cosseno(X) that receives a matrix with one vector per row and returns the matrix n × n of cosines between all pairs. Use np.einsum for the dot products.