webentwicklung-frage-antwort-db.com.de

NumPy's einsum verstehen

Ich kämpfe darum, genau zu verstehen, wie einsum funktioniert. Ich habe mir die Dokumentation und einige Beispiele angesehen, aber es scheint nicht zu bleiben.

Hier ist ein Beispiel, das wir in der Klasse durchgesehen haben:

C = np.einsum("ij,jk->ki", A, B)

für zwei Arrays A und B

Ich denke, das würde A^T * B, aber ich bin nicht sicher (es nimmt die Transponierung von einem von ihnen richtig?). Kann mir jemand genau erklären, was hier passiert (und im Allgemeinen, wenn einsum verwendet wird)?

147
Lance Strait

(Hinweis: Diese Antwort basiert auf einem kurzen Blogeintrag über einsum, den ich vor einiger Zeit geschrieben habe.)

Was macht einsum?

Stellen Sie sich vor, wir haben zwei mehrdimensionale Arrays, A und B. Nehmen wir nun an, wir wollen ...

  • multiplizierenA mit B auf eine bestimmte Art und Weise, um ein neues Array von Produkten zu erstellen; und dann vielleicht
  • summe dieses neue Array entlang bestimmter Achsen; und dann vielleicht
  • transponieren die Achsen des neuen Arrays in einer bestimmten Reihenfolge.

Es besteht eine gute Chance, dass einsum uns dabei hilft, schneller und speichereffizienter zu arbeiten als Kombinationen der NumPy-Funktionen wie multiply, sum und transpose ermöglichen.

Wie funktioniert einsum?

Hier ist ein einfaches (aber nicht ganz einfaches) Beispiel. Nehmen Sie die folgenden zwei Arrays:

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

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

Wir werden A und B elementweise multiplizieren und dann entlang der Zeilen des neuen Arrays summieren. In "normalem" NumPy würden wir schreiben:

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

Hier richtet die Indizierungsoperation für A die ersten Achsen der beiden Arrays aus, sodass die Multiplikation rundgesendet werden kann. Die Zeilen des Array von Produkten werden dann summiert, um die Antwort zurückzugeben.

Wenn wir stattdessen einsum verwenden möchten, könnten wir schreiben:

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

Die Signatur Zeichenfolge 'i,ij->i' Ist hier der Schlüssel und muss ein wenig erklärt werden. Sie können sich das in zwei Hälften vorstellen. Auf der linken Seite (links vom ->) Haben wir die beiden Eingabearrays beschriftet. Rechts von -> Haben wir das Array benannt, mit dem wir enden möchten.

Folgendes passiert als nächstes:

  • A hat eine Achse; wir haben es i genannt. Und B hat zwei Achsen; Wir haben Achse 0 als i und Achse 1 als j bezeichnet.

  • Indem wir die Bezeichnung i in beiden Eingabearrays wiederholen , teilen wir einsum mit, dass diese beiden Achsen multipliziert werden sollen zusammen. Mit anderen Worten, wir multiplizieren Array A mit jeder Spalte von Array B, genau wie es A[:, np.newaxis] * B Tut.

  • Beachten Sie, dass j in unserer gewünschten Ausgabe nicht als Bezeichnung erscheint. Wir haben gerade i verwendet (wir wollen ein 1D-Array erhalten). Indem wir das Label weglassen , sagen wir einsum, dass Summe entlang dieser Achse sein soll. Mit anderen Worten, wir summieren die Zeilen der Produkte, genau wie .sum(axis=1).

Das ist im Grunde alles, was Sie wissen müssen, um einsum zu verwenden. Es hilft, ein bisschen herumzuspielen; Wenn wir beide Bezeichnungen in der Ausgabe belassen, 'i,ij->ij', erhalten wir ein 2D-Array von Produkten zurück (dasselbe wie A[:, np.newaxis] * B). Wenn wir keine Ausgabebeschriftung 'i,ij-> Sagen, erhalten wir eine einzelne Zahl zurück (genau wie bei (A[:, np.newaxis] * B).sum()).

Das Tolle an einsum ist jedoch, dass nicht zuerst ein temporäres Array von Produkten erstellt wird. Es summiert nur die Produkte, wie es geht. Dies kann zu erheblichen Einsparungen bei der Speichernutzung führen.

Ein etwas größeres Beispiel

Zur Erläuterung des Skalarprodukts sind hier zwei neue Arrays:

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

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

Wir berechnen das Skalarprodukt mit np.einsum('ij,jk->ik', A, B). Hier ist ein Bild, das die Beschriftung von A und B und das Ausgabearray zeigt, das wir von der Funktion erhalten:

enter image description here

Sie können sehen, dass die Bezeichnung j wiederholt wird. Dies bedeutet, dass wir die Zeilen von A mit den Spalten von B multiplizieren. Außerdem ist das Label j nicht in der Ausgabe enthalten - wir addieren diese Produkte. Die Bezeichnungen i und k werden für die Ausgabe beibehalten, sodass wir ein 2D-Array zurückerhalten.

Es könnte noch deutlicher sein, dieses Ergebnis mit dem Array zu vergleichen, in dem die Bezeichnung j not summiert ist. Unten links sehen Sie das 3D-Array, das sich aus dem Schreiben von np.einsum('ij,jk->ijk', A, B) ergibt (d. H. Wir haben label j beibehalten):

enter image description here

Die Summierachse j ergibt das erwartete Skalarprodukt (siehe rechts).

Einige Übungen

Um ein besseres Gefühl für einsum zu bekommen, kann es nützlich sein, vertraute NumPy-Array-Operationen unter Verwendung der Indexnotation zu implementieren. Alles, was Kombinationen von Multiplikations- und Summationsachsen beinhaltet, kann mit einsum geschrieben werden.

A und B seien zwei 1D-Arrays gleicher Länge. Zum Beispiel A = np.arange(10) und B = np.arange(5, 15).

  • Die Summe von A kann geschrieben werden:

    np.einsum('i->', A)
    
  • Die elementweise Multiplikation A * B Kann wie folgt geschrieben werden:

    np.einsum('i,i->i', A, B)
    
  • Das innere Produkt oder Skalarprodukt np.inner(A, B) oder np.dot(A, B) kann wie folgt geschrieben werden:

    np.einsum('i,i->', A, B) # or just use 'i,i'
    
  • Das äußere Produkt np.outer(A, B) kann wie folgt geschrieben werden:

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

Für 2D-Arrays C und D, sofern die Achsen kompatible Längen haben (beide gleiche Länge oder eine von ihnen hat Länge 1), sind hier einige Beispiele:

  • Die Spur von C (Summe der Hauptdiagonale), np.trace(C), kann geschrieben werden:

    np.einsum('ii', C)
    
  • Elementweise Multiplikation von C und die Transponierung von D, C * D.T, Kann geschrieben werden:

    np.einsum('ij,ji->ij', C, D)
    
  • Das Multiplizieren jedes Elements von C mit dem Array D (um ein 4D Array zu erstellen), C[:, :, None, None] * D, Kann wie folgt geschrieben werden:

    np.einsum('ij,kl->ijkl', C, D)  
    
279
Alex Riley

Die Idee von numpy.einsum() zu verstehen, ist sehr einfach, wenn Sie es intuitiv verstehen. Beginnen wir als Beispiel mit einer einfachen Beschreibung mit Matrixmultiplikation.


Um numpy.einsum() zu verwenden, müssen Sie nur das so übergeben -called subscripts string als Argument, gefolgt von Ihrem input arrays.

Angenommen, Sie haben zwei 2D-Arrays, A und B und Sie möchten eine Matrixmultiplikation durchführen. Also tust du es:

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

Hier entspricht die tiefgestellte Zeichenfolgeij dem Array A während die tiefgestellte Zeichenfolgejk entspricht array B. Das Wichtigste dabei ist auch, dass die Anzahl der Zeichen in jeder tiefgestellten Zeichenfolge muss stimmen mit den Abmessungen des Arrays überein. (dh zwei Zeichen für 2D-Arrays, drei Zeichen für 3D-Arrays usw.) Und wenn Sie die Zeichen zwischen tiefgestellten Zeichenfolgen wiederholen (j in unserem Fall), das heißt, Sie möchten, dass ein summe in diesen Dimensionen vorkommt. Somit werden sie summenreduziert. (d. h. diese Dimension wird weg sein)

Die tiefgestellte Zeichenkette danach -> wird unser resultierendes Array sein. Wenn Sie das Feld leer lassen, wird alles summiert und als Ergebnis ein skalarer Wert zurückgegeben. Andernfalls hat das resultierende Array Abmessungen gemäß der tiefgestellten Zeichenfolge. In unserem Beispiel ist es ik. Dies ist intuitiv, da wir wissen, dass für die Matrixmultiplikation die Anzahl der Spalten im Array A mit der Anzahl der Zeilen im Array übereinstimmen muss B was hier passiert (dh wir kodieren dieses Wissen durch Wiederholung des Zeichens j in der tiefgestellten Zeichenfolge)


Hier einige weitere Beispiele, die die Verwendung/Stärke von np.einsum() bei der Implementierung veranschaulichen einige gebräuchliche Tensor oder nd-Array Operationen, kurz und bündig.

Eingänge

# 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) Matrixmultiplikation (ähnlich wie 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) Extrahieren Sie Elemente entlang der Hauptdiagonale (ähnlich wie np.diag(arr))

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

3) Hadamard-Produkt (dh elementweises Produkt zweier Arrays) (ähnlich wie 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) Elementweises Quadrieren (ähnlich wie np.square(arr) oder 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) Trace (dh Summe der Hauptdiagonalelemente) (ähnlich wie np.trace(arr))

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

6) Matrix transponieren (ähnlich wie 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) Äußeres Produkt (von Vektoren) (ähnlich wie 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) Inneres Produkt (von Vektoren) (ähnlich wie np.inner(vec1, vec2))

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

9) Summe entlang der Achse 0 (ähnlich wie np.sum(arr, axis=0))

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

10) Summe entlang Achse 1 (ähnlich wie np.sum(arr, axis=1))

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

11) Batch-Matrix-Multiplikation

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) Summe entlang Achse 2 (ähnlich wie np.sum(arr, axis=2))

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

13) Summiere alle Elemente im Array (ähnlich wie np.sum(arr))

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

14) Summe über mehrere Achsen (d. H. Marginalisierung)
(ähnlich wie 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) Doppelpunktprodukte (ähnlich wie np.sum (Hadamard-Produkt) vgl. 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) 2D- und 3D-Array-Multiplikation

Eine solche Multiplikation kann sehr nützlich sein, wenn Sie ein lineares Gleichungssystem ( Ax = b ) lösen, bei dem Sie das Ergebnis überprüfen möchten.

# 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

Im Gegenteil, wenn man np.matmul() für diese Überprüfung verwenden muss, Wir müssen einige reshape Operationen ausführen, um das gleiche Ergebnis zu erzielen wie:

# 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

Bonus : Lesen Sie mehr Mathe hier: Einstein-Summation und definitiv hier: Tensornotation

33
kmario23

Erstellen Sie zwei Arrays mit unterschiedlichen, aber kompatiblen Dimensionen, um das Zusammenspiel hervorzuheben

In [43]: A=np.arange(6).reshape(2,3)
Out[43]: 
array([[0, 1, 2],
       [3, 4, 5]])


In [44]: B=np.arange(12).reshape(3,4)
Out[44]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

Für Ihre Berechnung wird ein Punkt (Summe der Produkte) aus (2,3) und (3,4) verwendet, um ein (4,2) -Array zu erstellen. i ist das erste Maß von A, das letzte von C; k der letzte von B, der erste von C. j wird von der Summe 'verbraucht'.

In [45]: C=np.einsum('ij,jk->ki',A,B)
Out[45]: 
array([[20, 56],
       [23, 68],
       [26, 80],
       [29, 92]])

Dies ist dasselbe wie np.dot(A,B).T - es ist die endgültige Ausgabe, die transponiert wird.

Um mehr darüber zu erfahren, was mit j passiert, ändern Sie die C-Indizes in ijk:

In [46]: np.einsum('ij,jk->ijk',A,B)
Out[46]: 
array([[[ 0,  0,  0,  0],
        [ 4,  5,  6,  7],
        [16, 18, 20, 22]],

       [[ 0,  3,  6,  9],
        [16, 20, 24, 28],
        [40, 45, 50, 55]]])

Dies kann auch hergestellt werden mit:

A[:,:,None]*B[None,:,:]

Das heißt, fügen Sie eine k -Dimension an das Ende von A und eine i an die Vorderseite von B, was zu einer (2,3, 4) Array.

0 + 4 + 16 = 20, 9 + 28 + 55 = 92 Usw .; Summiere auf j und transponiere, um das frühere Ergebnis zu erhalten:

np.sum(A[:,:,None] * B[None,:,:], axis=1).T

# C[k,i] = sum(j) A[i,j (,k) ] * B[(i,)  j,k]
7
hpaulj

Ich fand NumPy: Die Tricks des Handels (Teil II) lehrreich

Wir verwenden ->, um die Reihenfolge des Ausgabearrays anzugeben. Stellen Sie sich also 'ij, i-> j' mit der linken Seite (LHS) und der rechten Seite (RHS) vor. Jede Wiederholung von Etiketten auf der LHS berechnet das Produktelement und summiert sich dann auf. Durch Ändern der Beschriftung auf der RHS-Seite (Ausgangsseite) können wir die Achse definieren, in der wir in Bezug auf das Eingangsarray vorgehen möchten, d. H. Die Summierung entlang der Achsen 0, 1 und so weiter.

import numpy as np

>>> a
array([[1, 1, 1],
       [2, 2, 2],
       [3, 3, 3]])
>>> b
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> d = np.einsum('ij, jk->ki', a, b)

Beachten Sie, dass es drei Achsen gibt, i, j, k, und dass j wiederholt wird (auf der linken Seite). i,j stehen für Zeilen und Spalten für a. j,k für b.

Um das Produkt zu berechnen und die Achse j auszurichten, müssen wir a eine Achse hinzufügen. (b wird entlang (?) der ersten Achse gesendet)

a[i, j, k]
   b[j, k]

>>> c = a[:,:,np.newaxis] * b
>>> c
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 0,  2,  4],
        [ 6,  8, 10],
        [12, 14, 16]],

       [[ 0,  3,  6],
        [ 9, 12, 15],
        [18, 21, 24]]])

j fehlt auf der rechten Seite, daher addieren wir j, die zweite Achse des 3x3x3-Arrays

>>> c = c.sum(1)
>>> c
array([[ 9, 12, 15],
       [18, 24, 30],
       [27, 36, 45]])

Schließlich werden die Indizes auf der rechten Seite (alphabetisch) umgekehrt, sodass wir transponieren.

>>> c.T
array([[ 9, 18, 27],
       [12, 24, 36],
       [15, 30, 45]])

>>> np.einsum('ij, jk->ki', a, b)
array([[ 9, 18, 27],
       [12, 24, 36],
       [15, 30, 45]])
>>>
5
wwii