ทำความเข้าใจกับ einsum ของ NumPy


190

ฉันพยายามที่จะเข้าใจอย่างถ่องแท้ว่าeinsumทำงานอย่างไร ฉันได้ดูเอกสารประกอบและตัวอย่างบางส่วนแล้ว แต่ดูเหมือนจะไม่ติด

นี่คือตัวอย่างที่เราไปเรียนในชั้นเรียน:

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

สำหรับสองอาร์เรย์AและB

ฉันคิดว่ามันน่าจะเกิดA^T * Bขึ้น แต่ฉันไม่แน่ใจ (มันเป็นการเปลี่ยนใจคนหนึ่งใช่ไหม) มีใครบ้างที่จะพาฉันไปพบสิ่งที่เกิดขึ้นที่นี่ (และโดยทั่วไปเมื่อใช้einsum)


7
จริงๆแล้วมันจะเป็นหรือเท่ากัน(A * B)^T B^T * A^T
Tigran Saluev

23
ผมเขียนบล็อกโพสต์สั้น ๆ เกี่ยวกับพื้นฐานของที่นี่einsum (ฉันยินดีที่จะย้ายบิตที่เกี่ยวข้องมากที่สุดไปยังคำตอบของ Stack Overflow หากมีประโยชน์)
Alex Riley

1
@ajcr - ลิงค์ที่สวยงาม ขอบคุณ numpyเอกสารไม่เพียงพอละห้อยเมื่ออธิบายรายละเอียด
rayryeng

ขอบคุณสำหรับการโหวตความมั่นใจ! ถึงจะฉันได้มีส่วนคำตอบด้านล่าง
Alex Riley

โปรดทราบว่าในไพ ธ อน*นั้นไม่ใช่การคูณเมทริกซ์ แต่เป็นการคูณแบบตามองค์ประกอบ ระวัง!
วิทยาศาสตร์คอมพิวเตอร์

คำตอบ:


371

(หมายเหตุ: คำตอบนี้ขึ้นอยู่กับการโพสต์บล็อกสั้น ๆเกี่ยวกับeinsumฉันเขียนในขณะที่ผ่านมา)

อะไรeinsumทำอย่างไร

ลองนึกภาพว่าเรามีสองอาร์เรย์หลายมิติและA Bทีนี้สมมติว่าเราต้องการ ...

  • ทวีคูณ Aด้วยBวิธีการเฉพาะในการสร้างผลิตภัณฑ์ใหม่ แล้วบางที
  • หาผลรวมอาร์เรย์ใหม่นี้ตามแกนเฉพาะ แล้วบางที
  • สลับแกนของอาร์เรย์ใหม่ตามลำดับที่ต้องการ

มีโอกาสที่ดีที่einsumจะช่วยให้เราทำสิ่งนี้ได้เร็วขึ้นและมีประสิทธิภาพมากขึ้นโดยการรวมฟังก์ชั่นของ NumPy เข้าmultiplyด้วยกันsumและtransposeจะช่วยให้

วิธีการeinsumทำงานหรือไม่

ต่อไปนี้เป็นตัวอย่างที่เรียบง่าย (แต่ไม่สำคัญมาก) ใช้สองอาร์เรย์ต่อไปนี้:

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บรรทัดขึ้นแกนแรกของสองอาร์เรย์เพื่อให้การคูณสามารถออกอากาศได้ แถวของอาเรย์ของผลิตภัณฑ์จะถูกนำมารวมกันเพื่อคืนคำตอบ

ตอนนี้ถ้าเราต้องการใช้einsumแทนเราสามารถเขียน:

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

ลายเซ็นสตริง'i,ij->i'เป็นกุญแจสำคัญที่นี่และต้องมีนิด ๆ หน่อย ๆ ของการอธิบาย คุณสามารถนึกถึงมันในสองส่วน ทางด้านซ้าย (ด้านซ้ายของ->) เราได้ระบุทั้งสองอาร์เรย์อินพุต ทางด้านขวาของ->เราได้ติดป้ายกำกับอาร์เรย์ที่เราต้องการ

นี่คือสิ่งที่เกิดขึ้นต่อไป:

  • Aมีแกนเดียว เราติดป้ายกำกับiแล้ว และBมีสองแกน เราได้ติดป้ายชื่อแกน 0 เป็นiแกน j1

  • โดยการทำซ้ำป้ายiทั้งในอาร์เรย์อินพุตเรากำลังบอกeinsumว่าควรจะคูณแกนทั้งสองเข้าด้วยกัน กล่าวอีกนัยหนึ่งเราจะคูณอาเรย์Aกับแต่ละคอลัมน์ของอาเรBย์เหมือนที่A[:, np.newaxis] * Bทำ

  • ขอให้สังเกตว่าjไม่ปรากฏเป็นป้ายกำกับในผลลัพธ์ที่เราต้องการ; เราเพิ่งใช้i(เราต้องการที่จะจบลงด้วยอาร์เรย์ 1D) เราจะบอกให้รวมผลรวมตามแกนนี้ด้วยการละเว้นป้ายกำกับ กล่าวอีกนัยหนึ่งเรากำลังรวมแถวของผลิตภัณฑ์เหมือนกันeinsum.sum(axis=1)

einsumที่เป็นพื้นทั้งหมดที่คุณจำเป็นต้องรู้เพื่อการใช้งาน มันช่วยให้เล่นได้เล็กน้อย ถ้าเราปล่อยให้ป้ายทั้งสองอยู่ในผลลัพธ์'i,ij->ij'เราจะได้รับผลิตภัณฑ์อาร์เรย์ 2 มิติ (เช่นเดียวกับA[:, np.newaxis] * B) ถ้าเราบอกว่าไม่มีป้ายกำกับเอาท์พุท'i,ij->เราจะได้รับหมายเลขเดียว (เช่นเดียวกับการทำ(A[:, np.newaxis] * B).sum())

อย่างไรก็ตามสิ่งที่ดีเกี่ยวกับeinsumคือไม่ได้สร้างผลิตภัณฑ์ชั่วคราวขึ้นมาก่อน มันเป็นเพียงผลรวมของผลิตภัณฑ์ตามที่ไป สิ่งนี้สามารถนำไปสู่การประหยัดอย่างมากในการใช้หน่วยความจำ

ตัวอย่างที่ใหญ่กว่าเล็กน้อย

เพื่ออธิบายผลิตภัณฑ์ดอทนี่คือสองอาร์เรย์ใหม่:

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และBและอาร์เรย์เอาต์พุตที่เราได้รับจากฟังก์ชั่น:

ป้อนคำอธิบายรูปภาพที่นี่

คุณสามารถดูป้ายชื่อที่jซ้ำแล้วซ้ำอีก - ที่นี้หมายถึงเรากำลังคูณแถวของมีคอลัมน์ของA Bนอกจากนี้ฉลากjจะไม่รวมอยู่ในผลลัพธ์ - เรากำลังรวมผลิตภัณฑ์เหล่านี้ เลเบลiและkถูกเก็บไว้สำหรับเอาต์พุตดังนั้นเราจึงกลับอาร์เรย์ 2 มิติ

มันอาจจะเป็นที่ชัดเจนมากขึ้นเพื่อเปรียบเทียบผลกับอาร์เรย์ที่ฉลากjจะไม่ได้สรุป ด้านล่างทางซ้ายคุณจะเห็นอาร์เรย์ 3 มิติที่เป็นผลมาจากการเขียนnp.einsum('ij,jk->ijk', A, B)(เช่นเราได้ติดป้ายกำกับไว้j):

ป้อนคำอธิบายรูปภาพที่นี่

แกนรวมjทำให้ผลิตภัณฑ์จุดที่คาดหวังแสดงทางด้านขวา

ออกกำลังกายบ้าง

หากต้องการรับความรู้สึกeinsumมากกว่านี้จะเป็นประโยชน์ในการใช้การดำเนินการอาร์เรย์ NumPy ที่คุ้นเคยโดยใช้รูปแบบตัวห้อย einsumสิ่งใดที่เกี่ยวข้องกับการรวมกันของการขยายตัวและแกนข้อสรุปสามารถเขียนได้ใช้

ให้ A และ B เป็นสองอาร์เรย์ 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)

สำหรับอาร์เรย์ 2 มิติCและDหากว่าแกนเป็นความยาวที่เข้ากันได้ (ทั้งความยาวเท่ากันหรือหนึ่งในนั้นมีความยาว 1) นี่คือตัวอย่างบางส่วน:

  • ร่องรอยของC(ผลรวมของเส้นทแยงมุมหลัก) np.trace(C), สามารถเขียนได้:

    np.einsum('ii', C)
  • คูณองค์ประกอบที่ชาญฉลาดของCและ transpose ของ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)  

1
คำอธิบายที่ดีมากขอบคุณ "สังเกตว่าฉันไม่ปรากฏเป็นป้ายกำกับในผลลัพธ์ที่เราต้องการ" - ใช่ไหม?
Ian Hincks

ขอบคุณ @IanHincks! มันดูเหมือนคำที่สะกดผิด ฉันแก้ไขมันแล้ว
Alex Riley

1
คำตอบที่ดีมาก นอกจากนี้ยังเป็นที่น่าสังเกตว่าij,jkสามารถทำงานได้ด้วยตัวเอง (โดยไม่มีลูกศร) เพื่อสร้างการคูณเมทริกซ์ แต่ดูเหมือนว่าความชัดเจนจะดีที่สุดที่จะวางลูกศรและขนาดเอาท์พุท มันอยู่ในบล็อกโพสต์
วิทยาศาสตร์คอมพิวเตอร์

1
@Peaceful: นี่เป็นหนึ่งในโอกาสที่ยากต่อการเลือกคำที่ถูกต้อง! ฉันรู้สึกว่า "คอลัมน์" เหมาะสมกว่านี้เล็กน้อยเนื่องจากAมีความยาว 3 เท่ากับความยาวของคอลัมน์ในB(ในขณะที่แถวที่Bมีความยาว 4 และไม่สามารถคูณองค์ประกอบที่ฉลาดได้A)
Alex Riley

1
โปรดทราบว่าถนัด->ส่งผลกระทบต่อความหมาย: "ในโหมดนัยห้อยเลือกที่มีความสำคัญเนื่องจากแกนของการส่งออกที่มีการจัดลำดับใหม่ตามลำดับตัวอักษรซึ่งหมายความว่า. np.einsum('ij', a)ไม่ส่งผลกระทบต่ออาร์เรย์ 2 มิติในขณะที่np.einsum('ji', a)ใช้เวลา transpose ของมัน."
BallpointBen

40

การเข้าใจความคิดของnumpy.einsum()เรื่องง่ายมากถ้าคุณเข้าใจมันอย่างสังหรณ์ใจ เป็นตัวอย่างที่ขอเริ่มต้นด้วยคำอธิบายง่ายๆที่เกี่ยวข้องกับการคูณเมทริกซ์


ในการใช้งานnumpy.einsum()สิ่งที่คุณต้องทำคือส่งสตริงตัวห้อยที่เรียกว่าเป็นอาร์กิวเมนต์ตามด้วยอินพุตอาร์เรย์ของคุณ

สมมติว่าคุณมีสอง 2D อาร์เรย์AและBและคุณต้องการที่จะทำคูณเมทริกซ์ ดังนั้นคุณทำ:

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

นี่สตริงห้อย ijสอดคล้องกับอาร์เรย์Aขณะที่สตริงห้อย สอดคล้องกับอาร์เรย์jk Bสิ่งที่สำคัญที่สุดที่ควรทราบคือจำนวนอักขระในสตริงตัวห้อย แต่ละตัวต้องตรงกับขนาดของอาเรย์ (นั่นคือสองตัวอักษรสำหรับ 2D อาร์เรย์สามตัวอักษรสำหรับแบบสามมิติและอื่น ๆ ) และถ้าคุณทำซ้ำตัวอักษรระหว่างสตริงตัวห้อย ( jในกรณีของเรา) นั่นหมายความว่าคุณต้องการeinผลรวมที่เกิดขึ้นตามมิติเหล่านั้น ดังนั้นพวกเขาจะได้รับผลรวมลดลง (เช่นขนาดนั้นจะหายไป )

สตริงห้อยหลังจากนี้->จะเป็นอาร์เรย์ผลลัพธ์ของเรา หากคุณปล่อยว่างไว้ทุกอย่างจะถูกรวมและส่งคืนค่าสเกลาร์เป็นผลลัพธ์ อื่นอาร์เรย์ผลจะมีขนาดตามที่สตริงห้อย ikในตัวอย่างของเรามันจะเป็น นี่คือสัญชาตญาณเพราะเรารู้ว่าสำหรับการคูณเมทริกซ์จำนวนคอลัมน์ในอาร์เรย์Aต้องตรงกับจำนวนแถวในอาเรย์Bซึ่งเป็นสิ่งที่เกิดขึ้นที่นี่ (เช่นเราเข้ารหัสความรู้นี้โดยการทำซ้ำอักขระjในสตริงตัวห้อย )


ต่อไปนี้เป็นตัวอย่างเพิ่มเติมที่แสดงให้เห็นถึงการใช้งาน / กำลังของnp.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) ผลิตภัณฑ์ Hadamard (เช่นผลิตภัณฑ์องค์ประกอบที่ชาญฉลาดของสองอาร์เรย์) (คล้ายกับ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) การยกกำลังสององค์ประกอบ (คล้ายกับ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) ผลรวมมากกว่าหลายแกน (เช่น marginalization)
(คล้ายกับ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 (คล้ายกับ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) การคูณอาร์เรย์ 2 มิติและ 3 มิติ

การคูณนั้นมีประโยชน์มากเมื่อแก้ระบบสมการเชิงเส้น ( 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


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เป็นสลัวที่ 1 ของAสุดท้ายC; kสุดท้ายB, C1 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และiส่วนหน้าBทำให้เกิดอาร์เรย์ (2,3,4)

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]

7

ผมพบว่าNumPy: เทคนิคของการค้า (Part II)ให้คำแนะนำ

เราใช้ -> เพื่อระบุลำดับของอาร์เรย์ผลลัพธ์ ดังนั้นคิดว่า 'ij, i-> j' เหมือนมีด้านซ้ายมือ (LHS) และด้านขวามือ (RHS) การทำซ้ำฉลากบน LHS ใด ๆ จะคำนวณองค์ประกอบของผลิตภัณฑ์อย่างชาญฉลาด โดยการเปลี่ยนฉลากที่ด้าน RHS (output) เราสามารถกำหนดแกนที่เราต้องการดำเนินการกับอาร์เรย์อินพุตเช่นการรวมตามแกน 0, 1 และอื่น ๆ

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)

โปรดสังเกตว่ามีสามแกนคือ i, j, k และ j นั้นซ้ำ (บนด้านซ้ายมือ) แทนแถวและคอลัมน์i,j สำหรับaj,kb

ในการคำนวณผลิตภัณฑ์และจัดเรียงjแกนเราจำเป็นต้องเพิ่มแกนเข้าaด้วยกัน ( bจะออกอากาศตาม (?) แกนแรก)

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หายไปจากด้านขวามือดังนั้นเราจึงรวมกันjซึ่งเป็นแกนที่สองของอาร์เรย์ 3x3x3

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

ในที่สุดดัชนีจะกลับด้านขวามือเราจึงทำการไขว้กัน

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

NumPy: เทคนิคการซื้อขาย (ตอนที่ 2) ดูเหมือนจะต้องได้รับคำเชิญจากเจ้าของเว็บไซต์รวมถึงบัญชี Wordpress
Tejas Shetty

... ลิงค์อัปเดตโชคดีที่ฉันพบว่ามีการค้นหา - ขอบคุณ
สงครามโลกครั้งที่

@TejasShetty คำตอบที่ดีกว่ามากที่นี่ตอนนี้ - บางทีฉันควรลบอันนี้
สงครามโลกครั้งที่

2
โปรดอย่าลบคำตอบของคุณ
Tejas Shetty

5

เมื่ออ่านสมการ einsum ฉันพบว่ามีประโยชน์มากที่สุดที่จะสามารถทำให้พวกเขาเดือดร้อนลงในใจได้

เริ่มต้นด้วยคำสั่ง (โอฬาร) ต่อไปนี้:

C = np.einsum('bhwi,bhwj->bij', A, B)

เมื่อทำงานผ่านเครื่องหมายวรรคตอนก่อนเราจะเห็นว่าเรามี blobs ที่คั่นด้วยเครื่องหมายจุลภาค 4 ตัวอักษรสองตัวbhwiและbhwjก่อนหน้าลูกศรและหยด 3 ตัวอักษรเดียวbijหลังจากนั้น ดังนั้นสมการที่สร้างผลลัพธ์เทนเซอร์อันดับ 3 จากอินพุตเทนเซอร์อันดับ 4 ทั้งสอง

ตอนนี้ให้ตัวอักษรแต่ละตัวในแต่ละหยดเป็นชื่อของตัวแปรช่วง ตำแหน่งที่ตัวอักษรปรากฏใน blob คือดัชนีของแกนที่อยู่ในเทนเซอร์ การสรุปที่จำเป็นที่ทำให้แต่ละองค์ประกอบของ C ต้องเริ่มต้นด้วยสามซ้อนกันสำหรับลูปหนึ่งรายการสำหรับแต่ละดัชนีของ C

for b in range(...):
    for i in range(...):
        for j in range(...):
            # the variables b, i and j index C in the order of their appearance in the equation
            C[b, i, j] = ...

โดยพื้นฐานแล้วคุณมีforลูปสำหรับดัชนีเอาท์พุทของซีทุกตัวเราจะปล่อยช่วงที่ไม่ได้กำหนดไว้ตอนนี้

ต่อไปเราจะดูที่ด้านซ้าย - มีตัวแปรช่วงที่ไม่ปรากฏทางด้านขวาหรือไม่? ในกรณีของเรา - ใช่และh wเพิ่มforลูปซ้อนภายในสำหรับทุกตัวแปรดังกล่าว:

for b in range(...):
    for i in range(...):
        for j in range(...):
            C[b, i, j] = 0
            for h in range(...):
                for w in range(...):
                    ...

ภายในวงด้านในสุดตอนนี้เราได้กำหนดดัชนีทั้งหมดแล้วดังนั้นเราจึงสามารถเขียนผลรวมจริงและการแปลเสร็จสมบูรณ์:

# three nested for-loops that index the elements of C
for b in range(...):
    for i in range(...):
        for j in range(...):

            # prepare to sum
            C[b, i, j] = 0

            # two nested for-loops for the two indexes that don't appear on the right-hand side
            for h in range(...):
                for w in range(...):
                    # Sum! Compare the statement below with the original einsum formula
                    # 'bhwi,bhwj->bij'

                    C[b, i, j] += A[b, h, w, i] * B[b, h, w, j]

หากคุณสามารถติดตามรหัสได้แล้วขอแสดงความยินดีด้วย! นี่คือทั้งหมดที่คุณต้องสามารถอ่านสมการ einsum โปรดสังเกตว่าสูตร einsum ดั้งเดิมจับคู่กับคำสั่งการสรุปสุดท้ายในตัวอย่างด้านบน ขอบเขตของลูปและขอบเขตเป็นเพียงแค่ฟูและคำสั่งสุดท้ายคือทั้งหมดที่คุณต้องเข้าใจสิ่งที่เกิดขึ้น

เพื่อความสมบูรณ์เรามาดูวิธีกำหนดช่วงสำหรับตัวแปรแต่ละช่วง ช่วงของตัวแปรแต่ละตัวนั้นก็แค่ความยาวของมิติที่มันจัดทำดัชนี เห็นได้ชัดว่าหากตัวแปรมีดัชนีมากกว่าหนึ่งมิติในหนึ่งเมตริกซ์หรือมากกว่านั้นความยาวของแต่ละมิตินั้นจะต้องเท่ากัน นี่คือโค้ดด้านบนพร้อมช่วงที่สมบูรณ์:

# C's shape is determined by the shapes of the inputs
# b indexes both A and B, so its range can come from either A.shape or B.shape
# i indexes only A, so its range can only come from A.shape, the same is true for j and B
assert A.shape[0] == B.shape[0]
assert A.shape[1] == B.shape[1]
assert A.shape[2] == B.shape[2]
C = np.zeros((A.shape[0], A.shape[3], B.shape[3]))
for b in range(A.shape[0]): # b indexes both A and B, or B.shape[0], which must be the same
    for i in range(A.shape[3]):
        for j in range(B.shape[3]):
            # h and w can come from either A or B
            for h in range(A.shape[1]):
                for w in range(A.shape[2]):
                    C[b, i, j] += A[b, h, w, i] * B[b, h, w, j]

0

ฉันคิดว่าตัวอย่างที่ง่ายที่สุดคือในเทนเซอร์ไหลของเอกสาร

มีสี่ขั้นตอนในการแปลงสมการของคุณเป็นสัญกรณ์ einsum ให้ใช้สมการนี้เป็นตัวอย่างC[i,k] = sum_j A[i,j] * B[j,k]

  1. ครั้งแรกที่เราวางชื่อตัวแปร เราได้รับik = sum_j ij * jk
  2. เราวางsum_jคำศัพท์ตามที่เป็นนัย เราได้รับik = ij * jk
  3. เราแทนที่ด้วย* ,เราได้รับik = ij, jk
  4. เอาต์พุตอยู่บน RHS และคั่นด้วย->เครื่องหมาย เราได้รับij, jk -> ik

ล่าม einsum เพียงเรียกใช้ 4 ขั้นตอนเหล่านี้ในสิ่งที่ตรงกันข้าม ดัชนีทั้งหมดที่หายไปในผลลัพธ์จะถูกนำมารวมกัน

นี่คือตัวอย่างเพิ่มเติมจากเอกสาร

# Matrix multiplication
einsum('ij,jk->ik', m0, m1)  # output[i,k] = sum_j m0[i,j] * m1[j, k]

# Dot product
einsum('i,i->', u, v)  # output = sum_i u[i]*v[i]

# Outer product
einsum('i,j->ij', u, v)  # output[i,j] = u[i]*v[j]

# Transpose
einsum('ij->ji', m)  # output[j,i] = m[i,j]

# Trace
einsum('ii', m)  # output[j,i] = trace(m) = sum_i m[i, i]

# Batch matrix multiplication
einsum('aij,ajk->aik', s, t)  # out[a,i,k] = sum_j s[a,i,j] * t[a, j, k]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.