どのように機能するかを正確に理解するのに苦労していeinsum
ます。ドキュメントといくつかの例を見てきましたが、固執していないようです。
授業で取り上げた例を次に示します。
C = np.einsum("ij,jk->ki", A, B)
2 つの配列の場合:A
およびB
.
これにはA^T * B
. ここで何が起こっているのか(そして一般的に を使用しているときeinsum
)、誰かが私に教えてくれますか?
どのように機能するかを正確に理解するのに苦労していeinsum
ます。ドキュメントといくつかの例を見てきましたが、固執していないようです。
授業で取り上げた例を次に示します。
C = np.einsum("ij,jk->ki", A, B)
2 つの配列の場合:A
およびB
.
これにはA^T * B
. ここで何が起こっているのか(そして一般的に を使用しているときeinsum
)、誰かが私に教えてくれますか?
(注: この回答は、私が少し前に書いた短いブログ投稿に基づいています。)einsum
einsum
ますか?2 つの多次元配列 と があるA
としB
ます。今、私たちがしたいとしましょう...
A
しB
て、新しい製品の配列を作成します。そして多分、einsum
のような NumPy 関数の組み合わせよりも高速かつメモリ効率的にこれを行うのに役立つ可能性が十分にあります。multiply
sum
transpose
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 つの入力配列にラベルを付けました。の右側に->
、最終的に配置したい配列にラベルを付けました。
次に何が起こるかを次に示します。
A
1 つの軸があります。ラベルを付けましたi
。B
2 つの軸があります。i
軸 0 を、軸 1 を とラベル付けしましj
た。
両方の入力配列でラベルを繰り返すことにより、これら 2 つの軸を乗算する必要があることを伝えています。言い換えれば、配列と同じように、配列の各列を掛けています。i
einsum
A
B
A[:, 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)
ます。関数から取得したA
andおよび出力配列のラベル付けを示す図を次に示します。B
ラベルj
が繰り返されていることがわかります。これは、 の行A
と の列を乗算していることを意味しますB
。さらに、ラベルj
は出力に含まれません。これらの製品を合計しています。ラベルi
とk
は出力用に保持されるため、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
と転置はD
、C * D.T
次のように記述できます。
np.einsum('ij,ji->ij', C, D)
C
の各要素を配列で乗算するD
と (4D 配列を作成するため)、C[:, :, None, None] * D
は次のように記述できます。
np.einsum('ij,kl->ijkl', C, D)
numpy.einsum()
直感的に理解すれば、その考え方をつかむのはとても簡単です。例として、行列の乗算に関する簡単な説明から始めましょう。
を使用するnumpy.einsum()
には、いわゆる添え字文字列を引数として渡し、その後に入力配列を渡すだけです。
2 つの 2D 配列 と がA
ありB
、行列の乗算を行いたいとします。そうしたらいい:
np.einsum("ij, jk -> ik", A, B)
ここで、添え字文字列 ij
は arrayA
に対応し、添え字文字列 jk
は array に対応しB
ます。また、ここで注意すべき最も重要なことは、各添え字文字列の文字数が配列の次元と一致する必要があることです。(つまり、2D 配列の場合は 2 文字、3D 配列の場合は 3 文字などです。) そして、添字文字列の間で文字を繰り返す場合(この場合)、それは、それらの次元に沿って合計が発生することを意味します。したがって、それらは総和になります。(つまり、その次元はなくなります) j
ein
この の後の添え字文字列->
は、結果の配列になります。空のままにすると、すべてが合計され、結果としてスカラー値が返されます。それ以外の場合、結果の配列は添え字 stringに従って次元を持ちます。この例では、 になりますik
。A
行列の乗算では、配列の列数が配列の行数と一致する必要があることがわかっているため、これは直感的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
相互作用を強調するために、異なるが互換性のある次元を持つ 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
、 の最後ですC
。k
の最後B
、 の 1 番目C
。 j
合計によって「消費」されます。
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
に変更します。C
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]]])
これは、次の方法でも作成できます。
A[:,:,None]*B[None,:,:]
つまりk
、 の末尾に次元を追加し、 の先頭に を追加するA
と、(2,3,4) 配列になります。i
B
0 + 4 + 16 = 20
、9 + 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]