Estou lutando para entender exatamente como einsum
funciona. Eu olhei para a documentação e alguns exemplos, mas parece que não fica.
Aqui está um exemplo que abordamos na aula:
C = np.einsum("ij,jk->ki", A, B)
para duas matrizes A
eB
Eu acho que isso levaria A^T * B
, mas não tenho certeza (está aceitando a transposição de um deles, certo?). Alguém pode me explicar exatamente o que está acontecendo aqui (e em geral ao usar einsum
)?
python
arrays
numpy
multidimensional-array
numpy-einsum
Estreito de Lance
fonte
fonte
(A * B)^T
, ou equivalenteB^T * A^T
.einsum
aqui . (Estou feliz em transplantar os bits mais relevantes para uma resposta no Stack Overflow, se útil).numpy
documentação é lamentavelmente inadequada ao explicar os detalhes.*
não é multiplicação de matrizes, mas multiplicação por elementos. Cuidado!Respostas:
(Nota: esta resposta é baseada em uma pequena postagem no blog sobre
einsum
que escrevi há um tempo.)O que
einsum
faz?Imagine que temos duas matrizes multidimensionais
A
eB
. Agora vamos supor que queremos ...A
comB
de um modo particular para criar uma nova gama de produtos; e então talvezHá uma boa chance de que
einsum
isso nos ajude a fazer isso mais rapidamente e com mais eficiência de memória que as combinações do NumPy funcionammultiply
,sum
etranspose
permitirão.Como
einsum
funciona?Aqui está um exemplo simples (mas não completamente trivial). Pegue as duas matrizes a seguir:
Vamos multiplicar
A
eB
elemento-wise e depois somar ao longo das linhas da nova matriz. Em NumPy "normal", escreveríamos:Então aqui, a operação de indexação em
A
alinha os primeiros eixos das duas matrizes para que a multiplicação possa ser transmitida. As linhas da matriz de produtos são somadas para retornar a resposta.Agora, se quiséssemos usar
einsum
, poderíamos escrever:A string de assinatura
'i,ij->i'
é a chave aqui e precisa de um pouco de explicação. Você pode pensar nisso em duas partes. No lado esquerdo (à esquerda da->
), rotulamos as duas matrizes de entrada. À direita->
, rotulamos a matriz com a qual queremos terminar.Aqui está o que acontece a seguir:
A
tem um eixo; nós o rotulamosi
. EB
tem dois eixos; rotulamos o eixo 0 comoi
e o eixo 1 comoj
.Ao repetir o rótulo
i
em ambas as matrizes de entrada, estamos dizendoeinsum
que estes dois eixos devem ser multiplicados juntos. Em outras palavras, estamos multiplicando a matrizA
com cada coluna da matrizB
, assim comoA[:, np.newaxis] * B
faz.Observe que
j
não aparece como um rótulo em nossa saída desejada; acabamos de usari
(queremos terminar com uma matriz 1D). Ao omitir o rótulo, estamos dizendoeinsum
para somar ao longo deste eixo. Em outras palavras, estamos somando as linhas dos produtos, assim como.sum(axis=1)
faz.Isso é basicamente tudo que você precisa saber para usar
einsum
. Ajuda a brincar um pouco; se deixarmos os dois rótulos na saída,'i,ij->ij'
obteremos uma matriz de produtos 2D (igual aA[:, np.newaxis] * B
). Se dissermos que não há etiquetas de saída,'i,ij->
retornamos um único número (o mesmo que fazer(A[:, np.newaxis] * B).sum()
).O melhor de tudo
einsum
, porém, é que isso não cria primeiro uma matriz temporária de produtos; apenas soma os produtos como vai. Isso pode levar a grandes economias no uso da memória.Um exemplo um pouco maior
Para explicar o produto escalar, aqui estão duas novas matrizes:
Vamos calcular o produto escalar usando
np.einsum('ij,jk->ik', A, B)
. Aqui está uma foto mostrando a rotulagem doA
eB
e a matriz de saída que começa a partir da função:Você pode ver que o rótulo
j
é repetido. Isso significa que multiplicamos as linhasA
com as colunas deB
. Além disso, o rótuloj
não está incluído na saída - estamos somando esses produtos. Etiquetasi
ek
são mantidos para a saída, por isso recuperamos uma matriz 2D.Pode ser ainda mais claro comparar esse resultado com a matriz em que o rótulo não
j
é somado. Abaixo, à esquerda, você pode ver a matriz 3D resultante da escrita (ou seja, mantemos o rótulo ):np.einsum('ij,jk->ijk', A, B)
j
O eixo somador
j
fornece o produto de ponto esperado, mostrado à direita.Alguns exercícios
Para entender melhor
einsum
, pode ser útil implementar operações familiares de array NumPy usando a notação subscrita. Qualquer coisa que envolva combinações de eixos multiplicadores e somatórios pode ser escrita usandoeinsum
.Sejam A e B duas matrizes 1D com o mesmo comprimento. Por exemplo,
A = np.arange(10)
eB = np.arange(5, 15)
.A soma de
A
pode ser escrita:A multiplicação por elementos
A * B
, pode ser escrita:O produto interno ou produto de ponto,
np.inner(A, B)
ounp.dot(A, B)
, pode ser escrito:O produto externo,,
np.outer(A, B)
pode ser escrito:Para matrizes 2D
C
eD
, desde que os eixos tenham comprimentos compatíveis (ambos com o mesmo comprimento ou um deles com comprimento 1), aqui estão alguns exemplos:O traço de
C
(soma da diagonal principal)np.trace(C)
, pode ser escrito:Multiplicação elemento-wise de
C
e a transposta deD
,C * D.T
, pode ser escrito:Multiplicando cada elemento
C
pela matrizD
(para criar uma matriz 4D)C[:, :, None, None] * D
, pode ser escrito:fonte
ij,jk
poderia funcionar por si só (sem as setas) para formar a multiplicação da matriz. Mas, para maior clareza, é melhor colocar as setas e as dimensões da saída. Está no post do blog.A
tem comprimento 3, o mesmo que o comprimento das colunasB
(enquanto as linhasB
têm comprimento 4 e não podem ser multiplicadas por elementosA
).->
afeta a semântica: "No modo implícito, os subscritos escolhidos são importantes, pois os eixos da saída são reordenados em ordem alfabética. Isso significa quenp.einsum('ij', a)
isso não afeta uma matriz 2D, enquantonp.einsum('ji', a)
faz sua transposição".Compreender a idéia de
numpy.einsum()
é muito fácil se você a entender intuitivamente. Como exemplo, vamos começar com uma descrição simples envolvendo multiplicação de matrizes .Para usar
numpy.einsum()
, tudo o que você precisa fazer é passar a chamada sequência de subscritos como argumento, seguida pelas matrizes de entrada .Digamos que você tenha duas matrizes 2D
A
eB
, e deseja fazer a multiplicação de matrizes. Então você faz:Aqui, a sequência de subscritos
ij
corresponde à matriz,A
enquanto a sequência de subscritosjk
corresponde à matrizB
. Além disso, o mais importante a ser observado aqui é que o número de caracteres em cada sequência de caracteres subscrito deve corresponder às dimensões da matriz. (ou seja, dois caracteres para matrizes 2D, três caracteres para matrizes 3D e assim por diante.) E se você repetir os caracteres entre as seqüências de caracteres subscritas (j
no nosso caso), isso significa que você deseja que aein
soma aconteça nessas dimensões. Assim, eles serão reduzidos pela soma. (ou seja, essa dimensão desaparecerá )A cadeia de caracteres subscrita após isso
->
será nossa matriz resultante. Se você deixar em branco, tudo será somado e um valor escalar será retornado como resultado. Caso contrário, a matriz resultante terá dimensões de acordo com a cadeia de caracteres subscrita . No nosso exemplo, seráik
. Isso é intuitivo porque sabemos que, para multiplicação de matrizes, o número de colunas na matrizA
deve corresponder ao número de linhas na matriz, oB
que está acontecendo aqui (ou seja, codificamos esse conhecimento repetindo o caracterej
na sequência de caracteres subscrito )Aqui estão mais alguns exemplos que ilustram o uso / potência
np.einsum()
na implementação de algumas operações comuns de tensor ou nd-array , de forma sucinta.Entradas
1) Multiplicação de matrizes (semelhante a
np.matmul(arr1, arr2)
)2) Extraia elementos ao longo da diagonal principal (semelhante a
np.diag(arr)
)3) Produto Hadamard (isto é, produto de duas matrizes) (semelhante a
arr1 * arr2
)4) Quadratura elemento a elemento (semelhante a
np.square(arr)
ouarr ** 2
)5) Rastreio (ou seja, soma dos elementos da diagonal principal) (semelhante a
np.trace(arr)
)6) Transposição da matriz (semelhante a
np.transpose(arr)
)7) Produto externo (de vetores) (semelhante a
np.outer(vec1, vec2)
)8) Produto interno (de vetores) (semelhante a
np.inner(vec1, vec2)
)9) Soma ao longo do eixo 0 (semelhante a
np.sum(arr, axis=0)
)10) Soma ao longo do eixo 1 (semelhante a
np.sum(arr, axis=1)
)11) Multiplicação de matrizes em lote
12) Soma ao longo do eixo 2 (semelhante a
np.sum(arr, axis=2)
)13) Soma todos os elementos da matriz (semelhante a
np.sum(arr)
)14) Soma sobre eixos múltiplos (ou seja, marginalização)
(semelhante a
np.sum(arr, axis=(axis0, axis1, axis2, axis3, axis4, axis6, axis7))
)15) Produtos de ponto duplo (semelhante ao np.sum (produto hadamard), cf. 3 )
16) multiplicação de matrizes 2D e 3D
Essa multiplicação pode ser muito útil para resolver o sistema linear de equações ( Ax = b ) onde você deseja verificar o resultado.
Pelo contrário, se for necessário usar
np.matmul()
essa verificação, precisamos executar algumasreshape
operações para obter o mesmo resultado, como:Bônus : Leia mais matemática aqui: Einstein-Summation e definitivamente aqui: Tensor-Notation
fonte
Vamos criar 2 matrizes, com dimensões diferentes, mas compatíveis, para destacar sua interação
Seu cálculo utiliza um 'ponto' (soma dos produtos) de a (2,3) com a (3,4) para produzir uma matriz (4,2).
i
é a 1ª dim deA
, a última deC
;k
o último deB
, primeiro deC
.j
é "consumido" pelo somatório.É o mesmo que
np.dot(A,B).T
- é a saída final que é transposta.Para ver mais do que acontece
j
, altere osC
subscritos paraijk
:Isso também pode ser produzido com:
Ou seja, adicione uma
k
dimensão ao finalA
ei
à frente deB
, resultando em uma matriz (2,3,4).0 + 4 + 16 = 20
,9 + 28 + 55 = 92
etc; Somaj
e transpõe para obter o resultado anterior:fonte
Achei NumPy: Os truques do comércio (Parte II) instrutivos
Observe que existem três eixos, i, j, k, e que j é repetido (no lado esquerdo).
i,j
representa linhas e colunas paraa
.j,k
parab
.Para calcular o produto e alinhar o
j
eixo, precisamos adicionar um eixo aa
. (b
será transmitido ao longo (?) do primeiro eixo)j
está ausente do lado direito, então somamosj
qual é o segundo eixo da matriz 3x3x3Finalmente, os índices são (em ordem alfabética) invertidos no lado direito, para que possamos transpor.
fonte
Ao ler as equações de einsum, achei mais útil apenas reduzi-las mentalmente às suas versões imperativas.
Vamos começar com a seguinte declaração (imponente):
Trabalhando primeiro com a pontuação, vemos que temos dois blobs separados por vírgula de quatro letras -
bhwi
ebhwj
, antes da seta, e um único blob de três letrasbij
depois dela. Portanto, a equação produz um resultado de tensor de classificação 3 a partir de duas entradas de tensor de classificação 4.Agora, permita que cada letra em cada blob seja o nome de uma variável de intervalo. A posição em que a letra aparece no blob é o índice do eixo no qual ele varia nesse tensor. A soma imperativa que produz cada elemento de C, portanto, deve começar com três aninhados para loops, um para cada índice de C.
Então, essencialmente, você tem um
for
loop para cada índice de saída C. Vamos deixar os intervalos indeterminados por enquanto.A seguir, examinamos o lado esquerdo - existem variáveis de intervalo que não aparecem no lado direito? No nosso caso - sim
h
ew
. Adicione umfor
loop aninhado interno para cada variável:Dentro do loop mais interno, agora temos todos os índices definidos, para que possamos escrever o somatório real e a tradução concluída:
Se você conseguiu seguir o código até agora, parabéns! Isso é tudo o que você precisa para poder ler as equações de einsum. Observe, em particular, como a fórmula original do einsum é mapeada para a instrução final de soma no snippet acima. Os loops de for e os limites do intervalo são apenas leves e essa afirmação final é tudo o que você realmente precisa para entender o que está acontecendo.
Por uma questão de integridade, vamos ver como determinar os intervalos para cada variável de intervalo. Bem, o intervalo de cada variável é simplesmente o comprimento da (s) dimensão (s) que ela indexa. Obviamente, se uma variável indexar mais de uma dimensão em um ou mais tensores, os comprimentos de cada uma dessas dimensões deverão ser iguais. Aqui está o código acima com os intervalos completos:
fonte