280

どのように機能するかを正確に理解するのに苦労していeinsumます。ドキュメントといくつかの例を見てきましたが、固執していないようです。

授業で取り上げた例を次に示します。

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

2 つの配列の場合:AおよびB.

これにはA^T * B. ここで何が起こっているのか(そして一般的に を使用しているときeinsum)、誰かが私に教えてくれますか?

4

8 に答える 8

562

(注: この回答は、私が少し前に書いた短いブログ投稿に基づいています。)einsum

何をしeinsumますか?

2 つの多次元配列 と があるAとしBます。今、私たちがしたいとしましょう...

  • 特定の方法で乗算 ABて、新しい製品の配列を作成します。そして多分
  • この新しい配列を特定の軸に沿って合計します。そして多分
  • 新しい配列の軸を特定の順序で転置します。

einsumのような NumPy 関数の組み合わせよりも高速かつメモリ効率的にこれを行うのに役立つ可能性が十分にあります。multiplysumtranspose

どのように機能しeinsumますか?

これは単純な (しかし完全に自明ではない) 例です。次の 2 つの配列を使用します。

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

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

要素ごとに乗算Aしてから、新しい配列の行に沿って合計します。B「通常の」NumPy では、次のように記述します。

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

Aしたがって、ここでは、乗算をブロードキャストできるように、2 つの配列の最初の軸を並べてインデックス操作を行います。次に、製品の配列の行が合計されて、答えが返されます。

代わりに使用したい場合はeinsum、次のように記述できます。

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

ここで鍵となるのは署名文字列'i,ij->i'であり、少し説明が必要です。2つに分けて考えることができます。左側 ( の左側->) では、2 つの入力配列にラベルを付けました。の右側に->、最終的に配置したい配列にラベルを付けました。

次に何が起こるかを次に示します。

  • A1 つの軸があります。ラベルを付けましたiB2 つの軸があります。i軸 0 を、軸 1 を とラベル付けしましjた。

  • 両方の入力配列でラベルを繰り返すことにより、これら 2 つの軸を乗算する必要があることを伝えています。言い換えれば、配列と同じように、配列の各列を掛けています。ieinsumABA[:, np.newaxis] * B

  • j目的の出力にラベルとして表示されないことに注意してください。使用したばかりiです (最終的に 1D 配列にしたい)。ラベルを省略することで、この軸に沿って合計するように指示しますeinsum。つまり、製品の行を合計しています。.sum(axis=1)

基本的に、 を使用するために知っておく必要があるのはそれだけですeinsum。少し遊ぶのに役立ちます。出力に両方のラベルを残すと'i,ij->ij'、製品の 2D 配列が返されます ( と同じA[:, np.newaxis] * B)。出力ラベルがないと言うと'i,ij->、単一の数値が返されます ( を実行するのと同じ(A[:, np.newaxis] * B).sum()です)。

ただし、優れている点einsumは、最初に製品の一時的な配列を構築しないことです。そのまま積を合計するだけです。これにより、メモリ使用量を大幅に節約できます。

少し大きい例

内積を説明するために、2 つの新しい配列を次に示します。

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

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

を使用して内積を計算しnp.einsum('ij,jk->ik', A, B)ます。関数から取得したAandおよび出力配列のラベル付けを示す図を次に示します。B

ここに画像の説明を入力

ラベルjが繰り返されていることがわかります。これは、 の行Aと の列を乗算していることを意味しますB。さらに、ラベルjは出力に含まれません。これらの製品を合計しています。ラベルikは出力用に保持されるため、2D 配列が返されます。

この結果を、ラベルjが合計されていない配列と比較すると、さらに明確になる場合があります。以下の左側には、書き込みの結果の 3D 配列が表示されますnp.einsum('ij,jk->ijk', A, B)(つまり、 label を保持していますj)。

ここに画像の説明を入力

合計軸jは、右側に示すように、期待される内積を示します。

いくつかの演習

の感覚をつかむにはeinsum、添字表記を使用して使い慣れた NumPy 配列操作を実装すると便利です。乗算軸と加算軸の組み合わせを含むものはすべて、 を使用して記述できます einsum

A と B を同じ長さの 2 つの 1D 配列とします。たとえば、A = np.arange(10)B = np.arange(5, 15).

  • の和は次のAように書ける.

    np.einsum('i->', A)
    
  • 要素単位の乗算 はA * B、次のように記述できます。

    np.einsum('i,i->i', A, B)
    
  • 内積または内積、np.inner(A, B)またはnp.dot(A, B)は、次のように記述できます。

    np.einsum('i,i->', A, B) # or just use 'i,i'
    
  • 外積 は次のnp.outer(A, B)ように記述できます。

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

2D 配列CおよびDの場合、軸の長さが互換性がある場合 (両方とも同じ長さであるか、いずれかの長さが 1 である場合)、いくつかの例を次に示します。

  • C(主対角線の合計)のトレースはnp.trace(C)、次のように記述できます。

    np.einsum('ii', C)
    
  • 、 、の要素ごとの乗算Cと転置はDC * D.T次のように記述できます。

    np.einsum('ij,ji->ij', C, D)
    
  • Cの各要素を配列で乗算するDと (4D 配列を作成するため)、C[:, :, None, None] * Dは次のように記述できます。

    np.einsum('ij,kl->ijkl', C, D)    
    
于 2015-11-10T23:10:37.093 に答える
70

numpy.einsum()直感的に理解すれば、その考え方をつかむのはとても簡単です。例として、行列の乗算に関する簡単な説明から始めましょう。


を使用するnumpy.einsum()には、いわゆる添え字文字列を引数として渡し、その後に入力配列を渡すだけです。

2 つの 2D 配列 と がAありB、行列の乗算を行いたいとします。そうしたらいい:

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

ここで、添え字文字列 ijは arrayAに対応し、添え字文字列 jkは array に対応しBます。また、ここで注意すべき最も重要なことは、各添え字文字列の文字数が配列の次元と一致する必要があることです。(つまり、2D 配列の場合は 2 文字、3D 配列の場合は 3 文字などです。) そして、添字文字列の間で文字を繰り返す場合(この場合)、それは、それらの次元に沿って合計が発生することを意味します。したがって、それらは総和になります。(つまり、その次元はなくなります) jein

この の後の添え字文字列->は、結果の配列になります。空のままにすると、すべてが合計され、結果としてスカラー値が返されます。それ以外の場合、結果の配列は添え字 stringに従って次元を持ちます。この例では、 になりますikA行列の乗算では、配列の列数が配列の行数と一致する必要があることがわかっているため、これは直感的Bです (つまりj添字文字列で char を繰り返すことでこの知識をエンコードします) 。


いくつかの一般的なtensorまたはnd-arraynp.einsum()演算を実装する際の の使用/能力を簡潔に示すいくつかの例を次に示します。

入力

# 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) 行列の乗算( に類似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) 主対角線に沿って要素を抽出します(同様にnp.diag(arr))

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

3) アダマール積 (つまり、2 つの配列の要素単位の積) ( と同様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) 要素ごとの 2 乗np.square(arr)(またはと同様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) トレース (つまり、主対角要素の合計) (に類似np.trace(arr))

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

6) 行列転置(類似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) (ベクトルの) 外積( と同様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) (ベクトルの) 内積( と同様np.inner(vec1, vec2))

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

9) 軸 0 に沿った合計( と同様np.sum(arr, axis=0))

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

10) 軸 1 に沿った合計( と同様np.sum(arr, axis=1))

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

11) バッチ行列乗算

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) 軸 2 に沿った合計( と同様np.sum(arr, axis=2))

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

13) 配列内のすべての要素を合計します(同様にnp.sum(arr))

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

14) 複数の軸にわたる合計 (すなわち周辺化)
(類似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) Double Dot Products ( 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) 2D および 3D 配列の乗算

このような乗算は、結果を検証したい線形連立方程式 ( Ax = b ) を解くときに非常に役立ちます。

# 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

逆に、np.matmul()この検証に使用する必要がある場合は、reshape次のような同じ結果を得るためにいくつかの操作を行う必要があります。

# 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

ボーナス: ここで数学の詳細を読む: Einstein-Summationと間違いなくここ: Tensor-Notation

于 2017-12-25T07:04:56.530 に答える
7

相互作用を強調するために、異なるが互換性のある次元を持つ 2 つの配列を作成してみましょう

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]])

あなたの計算は、(2,3)と(3,4)の「ドット」(積の合計)を取り、(4,2)配列を生成します。 iは の最初の次元でありA、 の最後ですCkの最後B、 の 1 番目Cj合計によって「消費」されます。

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

np.dot(A,B).Tこれは、転置された最終出力と同じです。

に何が起こるかをもっと見るには、下付き文字をjに変更します。Cijk

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]]])

これは、次の方法でも作成できます。

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

つまりk、 の末尾に次元を追加し、 の先頭に を追加するAと、(2,3,4) 配列になります。iB

0 + 4 + 16 = 209 + 28 + 55 = 92など; 合計しjて転置して、以前の結果を取得します。

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

# C[k,i] = sum(j) A[i,j (,k) ] * B[(i,)  j,k]
于 2014-09-29T22:06:37.403 に答える
0
于 2021-02-15T10:39:48.267 に答える