Próbuję zrozumieć, jak dokładnie einsum
działa. Przejrzałem dokumentację i kilka przykładów, ale wydaje się, że się nie trzyma.
Oto przykład, który przeszliśmy w klasie:
C = np.einsum("ij,jk->ki", A, B)
dla dwóch tablic A
iB
Myślę, że to zajmie A^T * B
, ale nie jestem pewien (czy transponowanie jednego z nich jest prawidłowe?). Czy ktoś może poprowadzić mnie dokładnie przez to, co się tutaj dzieje (i ogólnie podczas korzystania einsum
)?
python
arrays
numpy
multidimensional-array
numpy-einsum
Cieśnina Lance'a
źródło
źródło
(A * B)^T
lub równoważnieB^T * A^T
.einsum
tutaj . (Z przyjemnością przeszczepię najbardziej odpowiednie bity do odpowiedzi na temat przepełnienia stosu, jeśli jest to przydatne).numpy
Dokumentacja jest zdecydowanie niewystarczająca przy wyjaśnianiu szczegółów.*
nie jest to mnożenie macierzy, ale mnożenie elementarne. Uważaj!Odpowiedzi:
(Uwaga: ta odpowiedź jest oparta na krótkim wpisie na blogu, o którym
einsum
napisałem jakiś czas temu).Co ma
einsum
zrobić?Wyobraź sobie, że mamy dwie tablice wielowymiarowe
A
iB
. Załóżmy teraz, że chcemy ...A
zeB
w określony sposób, aby stworzyć nową gamę produktów; a potem możeIstnieje duża szansa, że
einsum
pomoże nam to zrobić szybciej i bardziej wydajnie pod względem pamięci niż kombinacje funkcji NumPymultiply
,sum
itranspose
pozwoli.Jak
einsum
działaOto prosty (ale nie całkowicie trywialny) przykład. Weź następujące dwie tablice:
Pomnożymy
A
iB
elementarnie, a następnie zsumujemy wzdłuż wierszy nowej tablicy. W „normalnym” NumPy pisalibyśmy:Więc tutaj operacja indeksowania włączona
A
liniach pierwszych osi dwóch tablic, aby można było nadawać mnożenie. Rzędy szeregu produktów są następnie sumowane w celu zwrócenia odpowiedzi.Teraz, jeśli chcielibyśmy użyć
einsum
zamiast tego, moglibyśmy napisać:Podpis ciąg
'i,ij->i'
jest kluczem tutaj i potrzebuje trochę wyjaśnień. Możesz myśleć o tym w dwóch połowach. Po lewej stronie (po lewej stronie->
) oznaczyliśmy dwie tablice wejściowe. Po prawej stronie->
oznaczyliśmy tablicę, z którą chcemy się skończyć.Oto, co stanie się potem:
A
ma jedną oś; oznaczyliśmy toi
. IB
ma dwie osie; oznaczyliśmy oś 0 jakoi
i oś 1 jakoj
.Poprzez powtarzanie etykietę
i
w obu tablic wejściowych, mówimyeinsum
, że te dwie osie należy pomnożyć razem. Innymi słowy, mnożymy tablicęA
z każdą kolumną tablicyB
, podobnie jakA[:, np.newaxis] * B
robi.Zauważ, że
j
nie pojawia się jako etykieta w naszym pożądanym wyniku; właśnie użyliśmyi
(chcemy skończyć z tablicą 1D). Przez pomijając etykietę, mówimyeinsum
do podsumowania wzdłuż tej osi. Innymi słowy, sumujemy rzędy produktów, podobnie jak.sum(axis=1)
robi.To właściwie wszystko, co musisz wiedzieć, aby używać
einsum
. Pomaga trochę się bawić; jeśli zostawimy obie etykiety w danych wyjściowych,'i,ij->ij'
otrzymamy tablicę 2D produktów (taką samą jakA[:, np.newaxis] * B
). Jeśli powiemy, że nie ma etykiet wyjściowych,'i,ij->
otrzymamy pojedynczy numer (taki sam jak czynność(A[:, np.newaxis] * B).sum()
).Wspaniałą rzeczą
einsum
jest to, że nie tworzy najpierw tymczasowego zestawu produktów; po prostu sumuje produkty na bieżąco. Może to prowadzić do dużych oszczędności w zużyciu pamięci.Nieco większy przykład
Aby wyjaśnić iloczyn skalarny, oto dwie nowe tablice:
Obliczymy iloczyn skalarny za pomocą
np.einsum('ij,jk->ik', A, B)
. Oto zdjęcie przedstawiające etykietęA
iB
oraz tablicę wyjściową, którą otrzymujemy z funkcji:Widać, że etykieta
j
się powtarza - oznacza to, że mnożymy wierszeA
z kolumnamiB
. Ponadto etykietaj
nie jest uwzględniona w danych wyjściowych - podsumowujemy te produkty. Etykietyi
ik
są przechowywane dla danych wyjściowych, więc otrzymujemy tablicę 2D.Jeszcze bardziej przejrzyste może być porównanie tego wyniku z tablicą, w której etykieta nie
j
jest sumowana. Poniżej po lewej stronie widać tablicę 3D, która powstaje podczas pisania (tzn. Zachowaliśmy etykietę ):np.einsum('ij,jk->ijk', A, B)
j
Oś sumująca
j
daje oczekiwany iloczyn punktowy pokazany po prawej stronie.Niektóre ćwiczenia
Aby poczuć więcej
einsum
, przydatne może być zaimplementowanie znanych operacji tablicowych NumPy za pomocą notacji indeksu dolnego. Wszystko, co obejmuje kombinacje mnożenia i sumowania osi, można zapisać za pomocąeinsum
.Niech A i B będą dwiema tablicami 1D o tej samej długości. Na przykład
A = np.arange(10)
iB = np.arange(5, 15)
.Suma
A
może być zapisana:Mnożenie elementarne
A * B
, można zapisać:Produkt wewnętrzny lub produkt kropkowy
np.inner(A, B)
lubnp.dot(A, B)
można zapisać:Produkt zewnętrzny
np.outer(A, B)
można napisać:Dla tablic 2D
C
iD
pod warunkiem, że osie mają zgodne długości (zarówno ta sama długość, jak i jedna z nich ma długość 1), oto kilka przykładów:Ślad
C
(suma głównej przekątnej)np.trace(C)
można zapisać:Element mnożenie
C
i transpozycjąD
,C * D.T
można zapisać:Mnożąc każdy element
C
przez tablicęD
(aby utworzyć tablicę 4D)C[:, :, None, None] * D
, można zapisać:źródło
ij,jk
może działać samodzielnie (bez strzałek), tworząc mnożenie macierzy. Ale wydaje się, że dla jasności najlepiej jest umieścić strzałki, a następnie wymiary wyjściowe. To jest na blogu.A
ma długość 3, taką samą jak długość kolumnB
(podczas gdy rzędyB
mają długość 4 i nie można ich pomnożyć przez elementA
).->
wpływu ma wpływ na semantykę: „W trybie niejawnym wybrane indeksy dolne są ważne, ponieważ osie danych wyjściowych są uporządkowane alfabetycznie. Oznacza to, żenp.einsum('ij', a)
nie wpływa na tablicę 2D, anp.einsum('ji', a)
dokonuje jej transpozycji”.Uchwycenie idei
numpy.einsum()
jest bardzo łatwe, jeśli zrozumiesz ją intuicyjnie. Na przykład zacznijmy od prostego opisu dotyczącego mnożenia macierzy .Aby użyć
numpy.einsum()
, wystarczy przekazać jako argument tak zwany ciąg indeksu dolnego , a następnie tablice wejściowe .Powiedzmy, że masz dwie tablice 2D,
A
iB
, i chcesz zrobić mnożenia macierzy. Więc robisz:Tutaj łańcuch dolny
ij
odpowiada tablicy,A
podczas gdy łańcuch dolnyjk
odpowiada tablicyB
. Najważniejszą rzeczą, na którą należy tutaj zwrócić uwagę, jest to, że liczba znaków w każdym łańcuchu indeksu dolnego musi pasować do wymiarów tablicy. (tj. dwa znaki dla tablic 2D, trzy znaki dla tablic 3D itd.) A jeśli powtórzysz znaki między łańcuchami indeksu dolnego (j
w naszym przypadku), oznacza to, że chcesz, abyein
suma występowała wzdłuż tych wymiarów. W ten sposób zostaną zmniejszone sumy. (tzn. ten wymiar zniknie )Dolny ciąg po tym
->
, będzie naszym wypadkowa tablicą. Jeśli pozostawisz to puste, wszystko zostanie zsumowane, w wyniku czego zostanie zwrócona wartość skalarna. W przeciwnym razie wynikowa tablica będzie miała wymiary zgodne z ciągiem indeksu dolnego . W naszym przykładzie tak będzieik
. Jest to intuicyjne, ponieważ wiemy, że do mnożenia macierzy liczba kolumn w tablicyA
musi odpowiadać liczbie wierszy w tablicy,B
co się tutaj dzieje (tj. Kodujemy tę wiedzę, powtarzając znakj
w łańcuchu indeksu dolnego )Oto kilka innych przykładów ilustrujących wykorzystanie / moc
np.einsum()
wdrażania niektórych typowych operacji tensorowych lub nd-macierzowych , zwięźle.Wejścia
1) Mnożenie macierzy (podobne do
np.matmul(arr1, arr2)
)2) Wyodrębnij elementy wzdłuż głównej przekątnej (podobnie do
np.diag(arr)
)3) Produkt Hadamarda (tj. Elementarny produkt dwóch tablic) (podobny do
arr1 * arr2
)4) Elementarne kwadraty (podobne do
np.square(arr)
lubarr ** 2
)5) Ślad (tj. Suma elementów głównych przekątnych) (podobny do
np.trace(arr)
)6) Transpozycja macierzy (podobna do
np.transpose(arr)
)7) Produkt zewnętrzny (wektorów) (podobny do
np.outer(vec1, vec2)
)8) Produkt wewnętrzny (wektorów) (podobny do
np.inner(vec1, vec2)
)9) Suma wzdłuż osi 0 (podobnie do
np.sum(arr, axis=0)
)10) Suma wzdłuż osi 1 (podobnie do
np.sum(arr, axis=1)
)11) Mnożenie macierzy partii
12) Suma wzdłuż osi 2 (podobnie do
np.sum(arr, axis=2)
)13) Zsumuj wszystkie elementy w tablicy (podobnie do
np.sum(arr)
)14) Suma na wielu osiach (tj. Marginalizacja)
(podobnie do
np.sum(arr, axis=(axis0, axis1, axis2, axis3, axis4, axis6, axis7))
)15) Produkty z podwójną kropką ( podobne do np. Suma (produkt hadamardowy) porównaj 3 )
16) Mnożenie tablic 2D i 3D
Takie mnożenie może być bardzo przydatne przy rozwiązywaniu liniowego układu równań ( Ax = b ), w którym chcesz zweryfikować wynik.
Wręcz przeciwnie, jeśli trzeba użyć
np.matmul()
tej weryfikacji, musimy wykonać kilkareshape
operacji, aby osiągnąć ten sam wynik, jak:Bonus : Przeczytaj więcej matematyki tutaj: Podsumowanie Einsteina i zdecydowanie tutaj: Notacja Tensor
źródło
Zróbmy 2 tablice o różnych, ale kompatybilnych wymiarach, aby podkreślić ich wzajemne oddziaływanie
Twoje obliczenia wymagają „kropki” (sumy produktów) (2,3) z (3,4), aby utworzyć tablicę (4,2).
i
jest pierwszym przyciemnieniemA
, ostatnim zC
;k
ostatni zB
, 1 zC
.j
jest „pochłonięty” przez sumowanie.To jest to samo co
np.dot(A,B).T
- transponowane jest końcowe wyjście.Aby zobaczyć więcej tego, co się dzieje
j
, zmieńC
indeksy dolne naijk
:Można to również wyprodukować z:
Oznacza to, że dodaj
k
wymiar na końcuA
ii
do przoduB
, uzyskując tablicę (2,3,4).0 + 4 + 16 = 20
,9 + 28 + 55 = 92
itp .; Sumaj
i transpozycja, aby uzyskać wcześniejszy wynik:źródło
Uważam, że NumPy: Sztuczki handlu (część II) są pouczające
Zauważ, że istnieją trzy osie, i, j, k, i że j jest powtarzane (po lewej stronie).
i,j
reprezentują wiersze i kolumny dlaa
.j,k
dlab
.Aby obliczyć iloczyn i wyrównać
j
oś, musimy dodać ośa
. (b
będzie nadawany wzdłuż (?) pierwszej osi)j
jest nieobecny z prawej strony, więc sumujemy, nadj
którą jest druga oś tablicy 3x3x3Wreszcie indeksy są (alfabetycznie) odwrócone po prawej stronie, więc dokonujemy transpozycji.
źródło
Czytając równania einsum, najbardziej pomocne było po prostu sprowadzenie ich mentalnie do ich imperatywnych wersji.
Zacznijmy od następującej (nakładającej) instrukcji:
Najpierw sprawdzając znaki interpunkcyjne, widzimy, że mamy dwa 4-literowe kropki oddzielone przecinkami -
bhwi
abhwj
przed strzałką i jeden 3-literowy kropelkabij
po nim. Dlatego równanie daje wynik tensora rangi 3 z dwóch wejść tensora rangi 4.Teraz niech każda litera w każdym obiekcie blob będzie nazwą zmiennej zakresu. Pozycja, w której litera pojawia się w kropli, jest indeksem osi, nad którą się onaga w tym tensorze. Dlatego sumowanie imperatywne, które wytwarza każdy element C, musi zaczynać się od trzech zagnieżdżonych dla pętli, po jednym dla każdego indeksu C.
Zasadniczo masz
for
pętlę dla każdego indeksu wyjściowego C. Na razie pozostawimy zakresy nieokreślone.Następnie patrzymy na lewą stronę - czy są tam jakieś zmienne zakresu, które nie pojawiają się po prawej stronie? W naszym przypadku - tak
h
iw
. Dodaj wewnętrzną zagnieżdżonąfor
pętlę dla każdej takiej zmiennej:Wewnątrz najbardziej wewnętrznej pętli mamy teraz zdefiniowane wszystkie indeksy, więc możemy napisać faktyczne podsumowanie i tłumaczenie jest kompletne:
Jeśli do tej pory potrafiłeś postępować zgodnie z tym kodem, gratulacje! To wszystko, czego potrzebujesz, aby móc czytać równania einsum. Zwróć uwagę w szczególności na to, w jaki sposób oryginalna formuła einsum mapuje się na końcową instrukcję podsumowania we fragmencie powyżej. Pętle for i zakresy są po prostu puchate, a to ostatnie stwierdzenie jest wszystkim, czego naprawdę potrzebujesz, aby zrozumieć, co się dzieje.
Dla kompletności zobaczmy, jak określić zakresy dla każdej zmiennej zakresu. Cóż, zasięg każdej zmiennej jest po prostu długością wymiaru, który indeksuje. Oczywiście, jeśli zmienna indeksuje więcej niż jeden wymiar w jednym lub więcej tensorach, wówczas długości każdego z tych wymiarów muszą być równe. Oto powyższy kod z pełnymi zakresami:
źródło
Myślę, że najprostszym przykładem są dokumenty tensorflow
Istnieją cztery kroki do przekształcenia równania w notację einsum. Weźmy to równanie jako przykład
C[i,k] = sum_j A[i,j] * B[j,k]
ik = sum_j ij * jk
sum_j
termin, ponieważ jest niejawny. Dostajemyik = ij * jk
*
się,
. Dostajemyik = ij, jk
->
znakiem. Dostajemyij, jk -> ik
Tłumacz einsum po prostu wykonuje te 4 kroki w odwrotnej kolejności. Wszystkie wskaźniki brakujące w wyniku są sumowane.
Oto kilka przykładów z dokumentów
źródło