อะไรคือความแตกต่างระหว่างอาร์เรย์ที่ต่อเนื่องกันและไม่ต่อเนื่องกัน?


108

ในคู่มือ numpyเกี่ยวกับฟังก์ชัน reshape () ระบุว่า

>>> a = np.zeros((10, 2))
# A transpose make the array non-contiguous
>>> b = a.T
# Taking a view makes it possible to modify the shape without modifying the
# initial object.
>>> c = b.view()
>>> c.shape = (20)
AttributeError: incompatible shape for a non-contiguous array

คำถามของฉันคือ:

  1. อาร์เรย์แบบต่อเนื่องและไม่ติดกันคืออะไร? คล้ายกับบล็อกหน่วยความจำที่ต่อเนื่องกันใน C เช่นบล็อกหน่วยความจำต่อเนื่องคืออะไร?
  2. มีความแตกต่างด้านประสิทธิภาพหรือไม่? เราควรใช้อย่างใดอย่างหนึ่งเมื่อใด
  3. เหตุใดทรานสโพสจึงทำให้อาร์เรย์ไม่ติดกัน
  4. ทำไมc.shape = (20)โยนข้อผิดพลาดincompatible shape for a non-contiguous array?

ขอบคุณสำหรับคำตอบ!

คำตอบ:


230

อาร์เรย์ที่ต่อเนื่องกันเป็นเพียงอาร์เรย์ที่เก็บไว้ในบล็อกหน่วยความจำที่ไม่ถูกแบ่ง: ในการเข้าถึงค่าถัดไปในอาร์เรย์เราเพียงแค่ย้ายไปยังที่อยู่หน่วยความจำถัดไป

arr = np.arange(12).reshape(3,4)พิจารณาอาร์เรย์ 2 มิติ ดูเหมือนว่า:

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

ในหน่วยความจำของคอมพิวเตอร์ค่าของarrจะถูกจัดเก็บดังนี้:

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

วิธีนี้arrเป็นC ที่ต่อเนื่องกันอาร์เรย์เพราะแถวจะถูกเก็บไว้เป็นบล็อกที่ต่อเนื่องกันของหน่วยความจำ ที่อยู่หน่วยความจำถัดไปเก็บค่าแถวถัดไปในแถวนั้น หากเราต้องการเลื่อนคอลัมน์ลงเราก็ต้องกระโดดข้ามสามช่วงตึก (เช่นกระโดดจาก 0 เป็น 4 หมายความว่าเราข้ามไปที่ 1,2 และ 3)

การย้ายอาร์เรย์ด้วยarr.Tวิธีการที่ความต่อเนื่องของ C หายไปเนื่องจากรายการแถวที่อยู่ติดกันไม่ได้อยู่ในที่อยู่หน่วยความจำที่อยู่ติดกันอีกต่อไป อย่างไรก็ตามarr.Tเป็นที่ต่อเนื่องกัน Fortranตั้งแต่คอลัมน์อยู่ในตึกที่อยู่ติดกันของหน่วยความจำ:

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


ประสิทธิภาพที่ชาญฉลาดการเข้าถึงที่อยู่หน่วยความจำซึ่งอยู่ติดกันมักจะเร็วกว่าการเข้าถึงที่อยู่ที่ "กระจาย" มากกว่า (การดึงค่าจาก RAM อาจทำให้เกิดที่อยู่ใกล้เคียงจำนวนมากที่ถูกดึงและแคชไว้สำหรับ CPU) หมายความว่าการดำเนินการกับอาร์เรย์ที่ต่อเนื่องกันมักจะเร็วกว่า

อันเป็นผลมาจากรูปแบบหน่วยความจำที่ต่อเนื่องกันการดำเนินการแบบแถวมักจะเร็วกว่าการดำเนินการแบบคอลัมน์ ตัวอย่างเช่นโดยทั่วไปคุณจะพบว่า

np.sum(arr, axis=1) # sum the rows

เร็วกว่าเล็กน้อย:

np.sum(arr, axis=0) # sum the columns

ในทำนองเดียวกันการดำเนินการกับคอลัมน์จะเร็วขึ้นเล็กน้อยสำหรับอาร์เรย์ที่ต่อเนื่องกันของ Fortran


สุดท้ายทำไมเราไม่สามารถแบนอาร์เรย์ที่อยู่ติดกันของ Fortran โดยกำหนดรูปร่างใหม่ได้?

>>> arr2 = arr.T
>>> arr2.shape = 12
AttributeError: incompatible shape for a non-contiguous array

เพื่อให้สิ่งนี้เป็นไปได้ NumPy จะต้องรวมแถวarr.Tเข้าด้วยกันดังนี้:

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

(การตั้งค่าshapeแอตทริบิวต์จะถือว่าลำดับ C โดยตรงนั่นคือ NumPy พยายามดำเนินการตามแถวการดำเนินการ)

สิ่งนี้เป็นไปไม่ได้ที่จะทำ สำหรับแกนใด ๆ NumPy จำเป็นต้องมีความยาวก้าวคงที่ (จำนวนไบต์ที่จะย้าย) เพื่อไปยังองค์ประกอบถัดไปของอาร์เรย์ การแบนarr.Tด้วยวิธีนี้จะต้องมีการข้ามไปข้างหน้าและข้างหลังในหน่วยความจำเพื่อดึงค่าที่ต่อเนื่องกันของอาร์เรย์

หากเราเขียนarr2.reshape(12)แทน NumPy จะคัดลอกค่าของ arr2 ไปยังบล็อกหน่วยความจำใหม่ (เนื่องจากไม่สามารถคืนมุมมองไปยังข้อมูลดั้งเดิมสำหรับรูปร่างนี้ได้)


ฉันมีปัญหาในการทำความเข้าใจโปรดอธิบายให้ละเอียดหน่อยได้ไหม ในการแสดงกราฟิกล่าสุดของการจัดลำดับที่เป็นไปไม่ได้ในหน่วยความจำความก้าวนั้นคงที่ในความคิดของฉัน ตัวอย่างเช่นในการเปลี่ยนจาก 0 ถึง 1 ก้าวคือ 1 ไบต์ (สมมติว่าแต่ละองค์ประกอบเป็นไบต์) และจะเหมือนกันสำหรับแต่ละคอลัมน์ ในทำนองเดียวกันการก้าวย่างคือ 4 ไบต์เพื่อไปจากองค์ประกอบหนึ่งในแถวไปยังองค์ประกอบถัดไปและมันก็คงที่เช่นกัน
Vesnog

3
@Vesnog การปรับรูปร่าง 2D arr2ให้เป็นรูปร่าง 1D ที่ล้มเหลว(12,)ใช้คำสั่ง C ซึ่งหมายความว่าแกน 1 จะคลายออกก่อนแกน 0 (กล่าวคือแต่ละแถวทั้งสี่จะต้องวางติดกันเพื่อสร้างอาร์เรย์ 1D ที่ต้องการ) เป็นไปไม่ได้ที่จะอ่านลำดับของจำนวนเต็ม (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) จากบัฟเฟอร์โดยใช้ความยาวก้าวคงที่ (ไบต์ที่จะข้ามไปเยี่ยมชม องค์ประกอบเหล่านี้ตามลำดับจะเป็น 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4) NumPy ต้องการความยาวก้าวต่อแกน
Alex Riley

ขอบคุณตอนแรกฉันคิดว่ามันจะสร้างอาร์เรย์ใหม่ แต่ใช้หน่วยความจำของอันเก่า
Vesnog

@AlexRiley จะเกิดอะไรขึ้นเบื้องหลังเมื่ออาร์เรย์ถูกทำเครื่องหมายเพื่อนบ้าน C หรือ F สั่ง? ตัวอย่างเช่นใช้อาร์เรย์อาร์เรย์ NxD ทุกตัวและพิมพ์ (arr [:, :: - 1] .flags) เกิดอะไรขึ้นในสถานการณ์นี้? ฉันเดาว่าอาร์เรย์เป็น C หรือ F ที่สั่ง แต่อันไหน? และการเพิ่มประสิทธิภาพของ numpy ใดที่เราสูญเสียไปหากทั้งสองแฟล็กเป็นเท็จ
Jjang

@Jjang: ไม่ว่า NumPy จะพิจารณาว่าอาร์เรย์เป็น C หรือ F จะขึ้นอยู่กับรูปร่างและขั้นตอนทั้งหมด (เกณฑ์อยู่ที่นี่ ) ดังนั้นในขณะที่arr[:, ::-1]เป็นมุมมองของบัฟเฟอร์หน่วยความจำเช่นเดียวกับarrNumPy ไม่ถือว่าเป็นคำสั่ง C หรือ F เนื่องจากมีการข้ามค่าในบัฟเฟอร์ในลำดับที่ "ไม่ได้มาตรฐาน" ...
Alex Riley

13

บางทีตัวอย่างที่มีค่าอาร์เรย์ 12 ค่าจะช่วยได้:

In [207]: x=np.arange(12).reshape(3,4).copy()

In [208]: x.flags
Out[208]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  ...
In [209]: x.T.flags
Out[209]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  ...

C orderค่าอยู่ในลำดับที่พวกเขาได้รับการสร้างขึ้นใน. คนย้ายไม่ได้

In [212]: x.reshape(12,)   # same as x.ravel()
Out[212]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

In [213]: x.T.reshape(12,)
Out[213]: array([ 0,  4,  8,  1,  5,  9,  2,  6, 10,  3,  7, 11])

คุณจะได้รับมุมมอง 1d ของทั้งสองอย่าง

In [214]: x1=x.T

In [217]: x.shape=(12,)

รูปร่างxยังสามารถเปลี่ยนแปลงได้

In [220]: x1.shape=(12,)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-220-cf2b1a308253> in <module>()
----> 1 x1.shape=(12,)

AttributeError: incompatible shape for a non-contiguous array

แต่รูปร่างของทรานสโพสไม่สามารถเปลี่ยนแปลงได้ dataยังคงอยู่ใน0,1,2,3,4...การสั่งซื้อที่ไม่สามารถเข้าถึงเข้าถึงได้เป็น0,4,8...ในอาร์เรย์ 1D

แต่x1สามารถเปลี่ยนสำเนาได้:

In [227]: x2=x1.copy()

In [228]: x2.flags
Out[228]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  ...
In [229]: x2.shape=(12,)

การมองไปที่stridesอาจช่วยได้ ความก้าวคือระยะทาง (เป็นไบต์) ที่ต้องก้าวเพื่อไปยังค่าถัดไป สำหรับอาร์เรย์ 2d จะมีค่า stride 2 ค่า:

In [233]: x=np.arange(12).reshape(3,4).copy()

In [234]: x.strides
Out[234]: (16, 4)

ในการไปยังแถวถัดไปขั้นตอนที่ 16 ไบต์คอลัมน์ถัดไปเท่านั้น 4

In [235]: x1.strides
Out[235]: (4, 16)

เปลี่ยนเพียงแค่เปลี่ยนลำดับของการก้าว แถวถัดไปมีเพียง 4 ไบต์นั่นคือตัวเลขถัดไป

In [236]: x.shape=(12,)

In [237]: x.strides
Out[237]: (4,)

การเปลี่ยนรูปร่างยังเปลี่ยนก้าว - เพียงแค่ก้าวผ่านบัฟเฟอร์ครั้งละ 4 ไบต์

In [238]: x2=x1.copy()

In [239]: x2.strides
Out[239]: (12, 4)

แม้ว่าจะx2ดูเหมือน แต่x1ก็มีบัฟเฟอร์ข้อมูลของตัวเองโดยมีค่าในลำดับที่แตกต่างกัน ตอนนี้คอลัมน์ถัดไปมีขนาด 4 ไบต์ในขณะที่แถวถัดไปคือ 12 (3 * 4)

In [240]: x2.shape=(12,)

In [241]: x2.strides
Out[241]: (4,)

และเช่นเดียวกับxการเปลี่ยนรูปร่างไป 1 (4,)วันจะช่วยลดความก้าวหน้าในการ

สำหรับการx1ที่มีข้อมูลใน0,1,2,...การสั่งซื้อที่มีอยู่ไม่ก้าว 1D 0,4,8...ที่จะให้

__array_interface__ เป็นอีกวิธีที่มีประโยชน์ในการแสดงข้อมูลอาร์เรย์:

In [242]: x1.__array_interface__
Out[242]: 
{'strides': (4, 16),
 'typestr': '<i4',
 'shape': (4, 3),
 'version': 3,
 'data': (163336056, False),
 'descr': [('', '<i4')]}

x1อยู่บัฟเฟอร์ข้อมูลจะเป็นเช่นเดียวกับxกับที่หุ้นข้อมูล x2มีที่อยู่บัฟเฟอร์ที่แตกต่างกัน

คุณยังสามารถทดลองเพิ่มorder='F'พารามิเตอร์ให้กับคำสั่งcopyandreshape

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.