6.5. Form und Strides

Die Daten innerhalb eines ndarray bilden einen gepackten Block von Zahlen. Der Deskriptor vor diesem Block entscheidet, wie dieser flache Block als Tensor ausgelesen wird.

6.5.1. Was der Deskriptor festhält

Fünf Werte beschreiben, wie der Datenblock als Tensor zu lesen ist:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)

a.ndim       # 2     - number of dimensions
a.shape      # (2, 3)- length along each dimension
a.itemsize   # 1     - bytes per element (from dtype)
a.size       # 6     - total number of elements
a.strides    # (3, 1)- step pattern through the buffer

Der Helfer ndinfo() gibt sie alle plus die Position des zugrunde liegenden Puffers in einem Aufruf aus. Zwei Arrays, deren Pufferpositionen übereinstimmen, teilen sich den Speicher:

np.ndinfo(a)
# class: ndarray
# shape: (2, 3)
# strides: (3, 1)
# itemsize: 1
# data pointer: 0x...
# type: uint8

6.5.2. Strides erklärt

Ein Stride gibt an, wie viele Bytes im Datenblock zu überspringen sind, um sich entlang einer gegebenen Achse um ein Element zu bewegen. Für das obige 2x3-uint8-Array sind die Strides (3, 1): eine Zeile nach unten zu gehen springt 3 Bytes, eine Spalte nach rechts zu gehen springt 1 Byte. Das ist gleichbedeutend damit zu sagen, dass die Zeilen Rücken an Rücken, von links nach rechts gespeichert sind:

memory: [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ]
          ^ row 0          ^ row 1
          <------- 3 bytes ---->

Um a[i, j] zu lesen, berechnet numpy i * strides[0] + j * strides[1] ab dem Anfang des Datenblocks und liest von dort itemsize Bytes. Dieselbe Formel erweitert sich auf eine beliebige Anzahl von Dimensionen.

Dieses Layout – Zeilen Ende an Ende gespeichert, wobei die letzte Achse im Speicher am schnellsten variiert – wird als row-major-Reihenfolge bezeichnet. Jedes Array, das numpy auf der Kamera alloziert, verwendet dieses Layout.

6.5.3. Row-major hat Konsequenzen

Aus „Zeilen Rücken an Rücken gespeichert“ ergeben sich zwei Dinge, die beim Formen eines Puffers auf der Kamera von Bedeutung sind.

Die letzte Achse ist zusammenhängend. Von a[0, 0] zu a[0, 1] zu gehen berührt das nächste Byte. Von a[0, 0] zu a[1, 0] zu gehen springt über eine ganze Zeile.

Die letzte Achse ist die schnelle Achse für Berechnungen über das gesamte Array. numpy auf der Kamera durchläuft immer die letzte Achse innerste, unabhängig davon, welche Achse zufällig länger ist. Die Desktop-Bibliothek numpy ordnet ihre Schleifen stillschweigend um, um die längste Achse innerste zu legen; die Kamera tut das nicht, sodass eine Layout-Wahl, die das Desktop-numpy kaschiert hätte, hier weiterhin Zeit kostet. np.sum(m, axis=1) kollabiert die letzte Achse und läuft in der zusammenhängenden Richtung; np.sum(m, axis=0) nicht. Wenn die Anwendung die Wahl hat, wie ein Puffer angelegt wird, legen Sie die lange Achse zuletzt, damit Operationen entlang dieser Achse in der inneren Schleife bleiben.

Wenn das Layout von Anfang an falsch ist, behebt transpose() (oder die Abkürzung .T) es, ohne die Daten zu kopieren – es vertauscht lediglich die Strides:

a = b.T            # now iterates fast

Leistung enthält die vollständige Leistungsdiskussion.

6.5.4. Reshape, Transpose, Slicing – Deskriptorbearbeitungen

Jede Operation, die nur den Deskriptor neu schreibt, ist kostenlos. reshape tauscht eine neue shape und strides über denselben Datenblock aus. transpose kehrt die Strides um. a[::2] verdoppelt einen Stride. Jede gibt einen View desselben zugrunde liegenden Puffers zurück.

Alles, was die Daten durchlaufen und einen neuen Puffer schreiben muss, ist eine Kopie. Die Regel für den Moment lautet, dass Deskriptorbearbeitungen kostenlos sind und Datendurchläufe nicht.

6.5.5. Eine Anmerkung zu ndim

numpy auf der Kamera ist mit einem maximal unterstützten ndim von 4 gebaut. Operationen, die ein Array mit höherem Rang erzeugen würden, lösen ValueError aus. Die überwiegende Mehrheit der kameraseitigen Arbeit ist 1-D oder 2-D, sodass die Grenze selten ein Problem darstellt.