Entendiendo el einsum de NumPy

Resuelto Lance Strait asked hace 10 años • 8 respuestas

¿Cómo np.einsumfunciona?

Dadas las matrices Ay B, su multiplicación de matrices seguida de transposición se calcula usando (A @ B).T, o de manera equivalente, usando:

np.einsum("ij, jk -> ki", A, B)
Lance Strait avatar Sep 29 '14 04:09 Lance Strait
Aceptado

(Nota: esta respuesta se basa en una breve publicación de blog que einsumescribí hace un tiempo).

¿Que es lo que einsumhace?

Imagine que tenemos dos matrices multidimensionales Ay B. Ahora supongamos que queremos...

  • multiplicarse A de Buna manera particular para crear una nueva gama de productos; y luego tal vez
  • sumar esta nueva matriz a lo largo de ejes particulares; y luego tal vez
  • transponer los ejes de la nueva matriz en un orden particular.

Es muy probable que eso einsumnos ayude a hacer esto más rápido y con mayor eficiencia de memoria que las combinaciones de funciones de NumPy como y multiplylo permitirán.sumtranspose

¿Cómo einsumfunciona?

Aquí hay un ejemplo simple (pero no completamente trivial). Tome las siguientes dos matrices:

A = np.array([0, 1, 2])

B = np.array([[ 0,  1,  2,  3],
              [ 4,  5,  6,  7],
              [ 8,  9, 10, 11]])

Multiplicaremos Ay Bpor elementos y luego sumaremos a lo largo de las filas de la nueva matriz. En NumPy "normal" escribiríamos:

>>> (A[:, np.newaxis] * B).sum(axis=1)
array([ 0, 22, 76])

Entonces, aquí, la operación de indexación Aalinea los primeros ejes de las dos matrices para que se pueda transmitir la multiplicación. Luego, las filas de la matriz de productos se suman para obtener la respuesta.

Ahora, si quisiéramos usar einsumen su lugar, podríamos escribir:

>>> np.einsum('i,ij->i', A, B)
array([ 0, 22, 76])

La cadena de firma'i,ij->i' es la clave aquí y necesita un poco de explicación. Puedes pensar en ello en dos mitades. En el lado izquierdo (a la izquierda de ->) hemos etiquetado las dos matrices de entrada. A la derecha de ->, hemos etiquetado la matriz con la que queremos terminar.

Esto es lo que sucede a continuación:

  • Atiene un eje; lo hemos etiquetado i. Y Btiene dos ejes; Hemos etiquetado el eje 0 como iy el eje 1 como j.

  • Al repetir la etiqueta ien ambas matrices de entrada, estamos diciendo einsumque estos dos ejes deben multiplicarse entre sí. En otras palabras, estamos multiplicando la matriz Acon cada columna de la matriz B, tal como A[:, np.newaxis] * Blo hace.

  • Observe que jno aparece como una etiqueta en nuestro resultado deseado; que acabamos de usar i(queremos terminar con una matriz 1D). Al omitir la etiqueta, le decimos einsumque sume a lo largo de este eje. En otras palabras, estamos sumando las filas de los productos, tal como .sum(axis=1)lo hacemos.

Eso es básicamente todo lo que necesitas saber para utilizar einsum. Ayuda jugar un poco; Si dejamos ambas etiquetas en la salida, 'i,ij->ij'obtenemos una matriz 2D de productos (igual que A[:, np.newaxis] * B). Si decimos que no hay etiquetas de salida, 'i,ij->obtenemos un solo número (igual que hacerlo (A[:, np.newaxis] * B).sum()).

Sin embargo, lo mejor de todo einsumes que no crea primero una gama temporal de productos; simplemente suma los productos a medida que avanza. Esto puede generar grandes ahorros en el uso de memoria.

Un ejemplo un poco más grande

Para explicar el producto escalar, aquí hay dos nuevas matrices:

A = array([[1, 1, 1],
           [2, 2, 2],
           [5, 5, 5]])

B = array([[0, 1, 0],
           [1, 1, 0],
           [1, 1, 1]])

Calcularemos el producto escalar usando np.einsum('ij,jk->ik', A, B). Aquí hay una imagen que muestra el etiquetado de Ay Bla matriz de salida que obtenemos de la función:

ingrese la descripción de la imagen aquí

Puedes ver que la etiqueta jestá repetida; esto significa que estamos multiplicando las filas de Apor las columnas de B. Además, la etiqueta jno está incluida en el resultado: estamos sumando estos productos. Las etiquetas iy kse guardan para la salida, por lo que obtenemos una matriz 2D.

Podría ser incluso más claro comparar este resultado con la matriz donde noj se suma la etiqueta. A continuación, a la izquierda, puede ver la matriz 3D que resulta de la escritura (es decir, hemos conservado la etiqueta ):np.einsum('ij,jk->ijk', A, B)j

ingrese la descripción de la imagen aquí

El eje sumador jproporciona el producto escalar esperado, que se muestra a la derecha.

algunos ejercicios

Para tener una idea más clara de einsum, puede resultar útil implementar operaciones familiares de matriz NumPy utilizando la notación de subíndice. Todo lo que involucre combinaciones de ejes de multiplicación y suma se puede escribir usando einsum.

Sean A y B dos matrices 1D con la misma longitud. Por ejemplo, A = np.arange(10)y B = np.arange(5, 15).

  • La suma de Ase puede escribir:

    np.einsum('i->', A)
    
  • La multiplicación por elementos, A * B, se puede escribir:

    np.einsum('i,i->i', A, B)
    
  • El producto interno o producto escalar, np.inner(A, B)o np.dot(A, B), se puede escribir:

    np.einsum('i,i->', A, B) # or just use 'i,i'
    
  • El producto exterior, np.outer(A, B), se puede escribir:

    np.einsum('i,j->ij', A, B)
    

Para matrices 2D Cy D, siempre que los ejes tengan longitudes compatibles (ambos de la misma longitud o uno de ellos tenga longitud 1), aquí hay algunos ejemplos:

  • La traza de C(suma de la diagonal principal), np.trace(C), se puede escribir:

    np.einsum('ii', C)
    
  • La multiplicación por elementos Cy la transposición de D, C * D.T, se pueden escribir:

    np.einsum('ij,ji->ij', C, D)
    
  • Multiplicando cada elemento de Cpor la matriz D(para hacer una matriz 4D), C[:, :, None, None] * Dse puede escribir:

    np.einsum('ij,kl->ijkl', C, D)    
    
Alex Riley avatar Nov 10 '2015 23:11 Alex Riley

Captar la idea de numpy.einsum()es muy fácil si la entiendes intuitivamente. Como ejemplo, comencemos con una descripción simple que involucra la multiplicación de matrices .


Para usar numpy.einsum(), todo lo que tiene que hacer es pasar la llamada cadena de subíndices como argumento, seguida de sus matrices de entrada .

Digamos que tienes dos matrices 2D Ay By quieres realizar una multiplicación de matrices. Tu también:

np.einsum("ij, jk -> ik", A, B)

Aquí la cadena de subíndice ij corresponde a una matriz, Amientras que la cadena de subíndice jk corresponde a una matriz B. Además, lo más importante a tener en cuenta aquí es que la cantidad de caracteres en cada cadena de subíndice debe coincidir con las dimensiones de la matriz (es decir, dos caracteres para matrices 2D, tres caracteres para matrices 3D, etc.). Y si repite los caracteres entre cadenas de subíndices ( jen nuestro caso), eso significa que desea que la einsuma se realice a lo largo de esas dimensiones. Por lo tanto, se reducirán en suma (es decir, esa dimensión desaparecerá ) .

La cadena de subíndice después de este ->símbolo representa las dimensiones de nuestra matriz resultante. Si lo deja vacío, todo se sumará y se devolverá un valor escalar como resultado. De lo contrario, la matriz resultante tendrá dimensiones según la cadena del subíndice . En nuestro ejemplo, será ik. Esto es intuitivo porque sabemos que para que la multiplicación de matrices funcione, el número de columnas en la matriz Atiene que coincidir con el número de filas en la matriz B, que es lo que está sucediendo aquí (es decir, codificamos este conocimiento repitiendo el carácter jen la cadena del subíndice )


Aquí hay algunos ejemplos más que ilustran el uso/poder de np.einsum()implementar algunas operaciones comunes de tensor o matriz nd , de manera sucinta.

Entradas

# a vector
In [197]: vec
Out[197]: array([0, 1, 2, 3])

# an array
In [198]: A
Out[198]: 
array([[11, 12, 13, 14],
       [21, 22, 23, 24],
       [31, 32, 33, 34],
       [41, 42, 43, 44]])

# another array
In [199]: B
Out[199]: 
array([[1, 1, 1, 1],
       [2, 2, 2, 2],
       [3, 3, 3, 3],
       [4, 4, 4, 4]])

1) Multiplicación de matrices (similar a np.matmul(arr1, arr2))

In [200]: np.einsum("ij, jk -> ik", A, B)
Out[200]: 
array([[130, 130, 130, 130],
       [230, 230, 230, 230],
       [330, 330, 330, 330],
       [430, 430, 430, 430]])

2) Extraer elementos a lo largo de la diagonal principal (similar a np.diag(arr))

In [202]: np.einsum("ii -> i", A)
Out[202]: array([11, 22, 33, 44])

3) Producto de Hadamard (es decir, producto por elementos de dos matrices) (similar a arr1 * arr2)

In [203]: np.einsum("ij, ij -> ij", A, B)
Out[203]: 
array([[ 11,  12,  13,  14],
       [ 42,  44,  46,  48],
       [ 93,  96,  99, 102],
       [164, 168, 172, 176]])

4) Elevación al cuadrado por elementos (similar a np.square(arr)o arr ** 2)

In [210]: np.einsum("ij, ij -> ij", B, B)
Out[210]: 
array([[ 1,  1,  1,  1],
       [ 4,  4,  4,  4],
       [ 9,  9,  9,  9],
       [16, 16, 16, 16]])

5) Traza (es decir, suma de elementos de la diagonal principal) (similar a np.trace(arr))

In [217]: np.einsum("ii -> ", A)
Out[217]: 110

6) Transpuesta de matriz (similar a np.transpose(arr))

In [221]: np.einsum("ij -> ji", A)
Out[221]: 
array([[11, 21, 31, 41],
       [12, 22, 32, 42],
       [13, 23, 33, 43],
       [14, 24, 34, 44]])

7) Producto exterior (de vectores) (similar a np.outer(vec1, vec2))

In [255]: np.einsum("i, j -> ij", vec, vec)
Out[255]: 
array([[0, 0, 0, 0],
       [0, 1, 2, 3],
       [0, 2, 4, 6],
       [0, 3, 6, 9]])

8) Producto interno (de vectores) (similar a np.inner(vec1, vec2))

In [256]: np.einsum("i, i -> ", vec, vec)
Out[256]: 14

9) Suma a lo largo del eje 0 (similar a np.sum(arr, axis=0))

In [260]: np.einsum("ij -> j", B)
Out[260]: array([10, 10, 10, 10])

10) Suma a lo largo del eje 1 (similar a np.sum(arr, axis=1))

In [261]: np.einsum("ij -> i", B)
Out[261]: array([ 4,  8, 12, 16])

11) Multiplicación de matrices por lotes

In [287]: BM = np.stack((A, B), axis=0)

In [288]: BM
Out[288]: 
array([[[11, 12, 13, 14],
        [21, 22, 23, 24],
        [31, 32, 33, 34],
        [41, 42, 43, 44]],

       [[ 1,  1,  1,  1],
        [ 2,  2,  2,  2],
        [ 3,  3,  3,  3],
        [ 4,  4,  4,  4]]])

In [289]: BM.shape
Out[289]: (2, 4, 4)

# batch matrix multiply using einsum
In [292]: BMM = np.einsum("bij, bjk -> bik", BM, BM)

In [293]: BMM
Out[293]: 
array([[[1350, 1400, 1450, 1500],
        [2390, 2480, 2570, 2660],
        [3430, 3560, 3690, 3820],
        [4470, 4640, 4810, 4980]],

       [[  10,   10,   10,   10],
        [  20,   20,   20,   20],
        [  30,   30,   30,   30],
        [  40,   40,   40,   40]]])

In [294]: BMM.shape
Out[294]: (2, 4, 4)

12) Suma a lo largo del eje 2 (similar a np.sum(arr, axis=2))

In [330]: np.einsum("ijk -> ij", BM)
Out[330]: 
array([[ 50,  90, 130, 170],
       [  4,   8,  12,  16]])

13) Suma todos los elementos de la matriz (similar a np.sum(arr))

In [335]: np.einsum("ijk -> ", BM)
Out[335]: 480

14) Suma sobre múltiples ejes (es decir, marginación)
(similar a np.sum(arr, axis=(axis0, axis1, axis2, axis3, axis4, axis6, axis7)))

# 8D array
In [354]: R = np.random.standard_normal((3,5,4,6,8,2,7,9))

# marginalize out axis 5 (i.e. "n" here)
In [363]: esum = np.einsum("ijklmnop -> n", R)

# marginalize out axis 5 (i.e. sum over rest of the axes)
In [364]: nsum = np.sum(R, axis=(0,1,2,3,4,6,7))

In [365]: np.allclose(esum, nsum)
Out[365]: True

15) Productos de doble punto (similar a np.sum(hadamard-product) cf. 3 )

In [772]: A
Out[772]: 
array([[1, 2, 3],
       [4, 2, 2],
       [2, 3, 4]])

In [773]: B
Out[773]: 
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])

In [774]: np.einsum("ij, ij -> ", A, B)
Out[774]: 124

16) Multiplicación de matrices 2D y 3D

Esta multiplicación podría resultar muy útil al resolver un sistema lineal de ecuaciones ( Ax = b ) donde desee verificar el resultado.

# inputs
In [115]: A = np.random.rand(3,3)
In [116]: b = np.random.rand(3, 4, 5)

# solve for x
In [117]: x = np.linalg.solve(A, b.reshape(b.shape[0], -1)).reshape(b.shape)

# 2D and 3D array multiplication :)
In [118]: Ax = np.einsum('ij, jkl', A, x)

# indeed the same!
In [119]: np.allclose(Ax, b)
Out[119]: True

Por el contrario, si tenemos que usar np.matmul()para esta verificación, tenemos que hacer un par de reshapeoperaciones para lograr el mismo resultado como:

# reshape 3D array `x` to 2D, perform matmul
# then reshape the resultant array to 3D
In [123]: Ax_matmul = np.matmul(A, x.reshape(x.shape[0], -1)).reshape(x.shape)

# indeed correct!
In [124]: np.allclose(Ax, Ax_matmul)
Out[124]: True

Bonificación : lea más matemáticas aquí: Suma de Einstein y definitivamente aquí: Notación tensorial

kmario23 avatar Dec 25 '2017 07:12 kmario23

Al leer ecuaciones einsum, me ha parecido más útil poder reducirlas mentalmente a sus versiones imperativas.

Comencemos con la siguiente (imponente) declaración:

C = np.einsum('bhwi,bhwj->bij', A, B)

Primero, analizando la puntuación, vemos que tenemos dos manchas de 4 letras separadas por comas: bhwiy bhwj, antes de la flecha, y una sola mancha de 3 letras bijdespués. Por lo tanto, la ecuación produce un resultado de tensor de rango 3 a partir de dos entradas de tensor de rango 4.

Ahora, dejemos que cada letra de cada blob sea el nombre de una variable de rango. La posición en la que aparece la letra en la mancha es el índice del eje que recorre en ese tensor. La suma imperativa que produce cada elemento de C, por lo tanto, tiene que comenzar con tres bucles for anidados, uno para cada índice de C.

for b in range(...):
    for i in range(...):
        for j in range(...):
            # the variables b, i and j index C in the order of their appearance in the equation
            C[b, i, j] = ...

Entonces, esencialmente, tienes un forbucle para cada índice de salida de C. Dejaremos los rangos indeterminados por ahora.

A continuación, miramos el lado izquierdo: ¿hay variables de rango que no aparecen en el lado derecho ? En nuestro caso, sí, hy w. Agregue un forbucle anidado interno para cada una de estas variables:

for b in range(...):
    for i in range(...):
        for j in range(...):
            C[b, i, j] = 0
            for h in range(...):
                for w in range(...):
                    ...

Dentro del bucle más interno ahora tenemos todos los índices definidos, por lo que podemos escribir la suma real y la traducción estará completa:

# three nested for-loops that index the elements of C
for b in range(...):
    for i in range(...):
        for j in range(...):

            # prepare to sum
            C[b, i, j] = 0

            # two nested for-loops for the two indexes that don't appear on the right-hand side
            for h in range(...):
                for w in range(...):
                    # Sum! Compare the statement below with the original einsum formula
                    # 'bhwi,bhwj->bij'

                    C[b, i, j] += A[b, h, w, i] * B[b, h, w, j]

Si has podido seguir el código hasta ahora, ¡felicidades! Esto es todo lo que necesitas para poder leer ecuaciones einsum. Observe en particular cómo la fórmula einsum original se relaciona con la declaración de suma final en el fragmento anterior. Los bucles for y los límites de rango son simplemente tontos y esa declaración final es todo lo que realmente necesitas para entender lo que está pasando.

Para completar, veamos cómo determinar los rangos para cada variable de rango. Bueno, el rango de cada variable es simplemente la longitud de las dimensiones que indexa. Obviamente, si una variable indexa más de una dimensión en uno o más tensores, entonces las longitudes de cada una de esas dimensiones tienen que ser iguales. Aquí está el código anterior con los rangos completos:

# C's shape is determined by the shapes of the inputs
# b indexes both A and B, so its range can come from either A.shape or B.shape
# i indexes only A, so its range can only come from A.shape, the same is true for j and B
assert A.shape[0] == B.shape[0]
assert A.shape[1] == B.shape[1]
assert A.shape[2] == B.shape[2]
C = np.zeros((A.shape[0], A.shape[3], B.shape[3]))
for b in range(A.shape[0]): # b indexes both A and B, or B.shape[0], which must be the same
    for i in range(A.shape[3]):
        for j in range(B.shape[3]):
            # h and w can come from either A or B
            for h in range(A.shape[1]):
                for w in range(A.shape[2]):
                    C[b, i, j] += A[b, h, w, i] * B[b, h, w, j]
Stefan Dragnev avatar Jan 22 '2020 11:01 Stefan Dragnev