3 NumPy-Tricks für numerische Leistung

# Einführung

Das Python-Ökosystem für wissenschaftliches Rechnen und maschinelles Lernen ist stark darauf angewiesen NumPy. Es fungiert als Leistungsmotor hinter Bibliotheken wie Pandas, Scikit-Be taught, SciPy und PyTorch. Die Geschwindigkeit von NumPy beruht auf der zugrunde liegenden Implementierung in optimiertem C, bei dem zusammenhängende Speicherblöcke ohne den Overhead des Objektmodells und des dynamischen Interpreters von Python manipuliert werden.

Leider schreiben viele Datenwissenschaftler und Entwickler NumPy-Code, der diese Leistungsfähigkeit nicht nutzt. Durch das Übertragen von Commonplace-Python-Schleifen oder das Schreiben naiver Berechnungen, die unnötige Speicherzuweisungen und Array-Kopien erzwingen, kommt es zu Leistungsengpässen. Bei der Arbeit mit großen Datensätzen führen diese Ineffizienzen zu einer überhöhten RAM-Nutzung, Cache-Fehlern und langsamen Ausführungszeiten. Um leistungsstarken numerischen Code zu schreiben, müssen Sie verstehen, wie NumPy Berechnungen, Speicherzuweisung und Datenlayouts unter der Haube verwaltet.

In diesem Artikel behandeln wir drei wesentliche NumPy-Tips zur Optimierung Ihres Codes:

  • Vektorisierung und Rundfunk
  • Vor-Ort-Vorgänge mithilfe der out Parameter
  • Nutzung von Speicheransichten anstelle von Kopien

# 1. Vektorisierung und Übertragung über explizite Schleifen

Explizites Python for Schleifen sind der größte Geschwindigkeitskiller im numerischen Rechnen. Das Component-für-Component-Iterieren über eine Datenstruktur zwingt den Python-Interpreter dazu, bei jedem einzelnen Schritt Typprüfungen und Methodensuchen durchzuführen.

Eine häufige Gefahr ist die Verwendung np.vectorize. Viele Entwickler gehen davon aus, dass das Umschließen einer Commonplace-Python-Funktion mit np.vectorize wandelt es in optimierten C-Code um. In Wirklichkeit, np.vectorize ist lediglich ein praktischer Wrapper, der eine langsame Commonplace-Python-Schleife hinter einer saubereren API ausführt und keinerlei Leistungsvorteile bietet.

Zur Optimierung müssen Sie Code mit nativen Universalfunktionen (ufuncs) und Broadcasting schreiben. Durch Broadcasting kann NumPy Operationen an Arrays unterschiedlicher Kind ausführen, ohne Daten zu kopieren, und Operationen direkt in kompiliertem C verarbeiten.

Dieser naive Ansatz durchläuft ein 2D-Array Zeile für Zeile und Spalte für Spalte, um eine spaltenweise Standardisierung durchzuführen (Subtraktion des Spaltenmittelwerts und Division durch die Spaltenstandardabweichung):

import numpy as np
import time

# Create a pattern matrix (50000 rows, 1000 columns)
matrix = np.random.rand(50000, 1000)

start_time = time.time()

# Naive loop-based column normalization
res = matrix.copy()
for col in vary(matrix.form(1)):
    col_mean = np.imply(matrix(:, col))
    col_std = np.std(matrix(:, col))
    for row in vary(matrix.form(0)):
        res(row, col) = (matrix(row, col) - col_mean) / col_std

duration_loop = time.time() - start_time

print(f"Nested loop processed matrix in: {duration_loop:.4f} seconds")

Ausgabe:

Nested loop processed matrix in: 10.9986 seconds

Anstatt eine Schleife durchzuführen, berechnen wir den Mittelwert und die Standardabweichung entlang der vertikalen Achse (axis=0). NumPy richtet diese 1D-Zusammenfassungsstatistiken mithilfe von Broadcasting automatisch an den 2D-Matrixzeilen aus:

import numpy as np
import time

# Create a pattern matrix (50000 rows, 1000 columns)
matrix = np.random.rand(50000, 1000)

start_time = time.time()

# Compute means and commonplace deviations alongside axis 0 in compiled C
means = np.imply(matrix, axis=0)
stds = np.std(matrix, axis=0)

# Let broadcasting robotically increase the shapes and compute in a single line
res_vectorized = (matrix - means) / stds

duration_vectorized = time.time() - start_time
print(f"Vectorized broadcasting processed matrix in: {duration_vectorized:.4f} seconds")

Ausgabe:

Vectorized broadcasting processed matrix in: 0.1972 seconds

Das ist eine etwa 56-fache Beschleunigung!

In der vektorisierten Implementierung sind die Operationen matrix - means und die anschließende Division durch stds werden mithilfe der Broadcasting-Regeln von NumPy ausgeführt. Weil matrix hat Kind (50000, 1000) Und means hat Kind (1000,)NumPy erweitert konzeptionell die means Array, um es an die Kind der Matrix anzupassen. Unter der Haube erfolgt diese Erweiterung sofort im Speicher, ohne dass Daten dupliziert werden, und die Berechnungen werden auf SIMD-CPU-Anweisungen (Single Instruction, A number of Knowledge) übertragen, was zu einer enormen Geschwindigkeitssteigerung von über 50x führt.

# 2. Vor-Ort-Operationen und die out Parameter

Wenn Sie Ausdrücke schreiben wie y = 2 * x + 3können Sie davon ausgehen, dass es effizient läuft. Unter der Haube wertet NumPy diesen Ausdruck jedoch Schritt für Schritt aus:

  1. Es weist ein temporäres Array im Speicher zu, um das Ergebnis zu speichern 2 * x
  2. Es weist ein weiteres Array zu, um das Ergebnis der Addition zu speichern 3 zum temporären Array
  3. Schließlich wird dieses zweite temporäre Array an den Variablennamen gebunden y

Bei der Arbeit mit sehr großen Arrays (z. B. Millionen von Einträgen) verursacht die Zuweisung und Müllsammlung dieser temporären Zwischenarrays einen erheblichen Mehraufwand. Es belastet die CPU-Caches und überlastet die Speicherbusbandbreite.

Wir können diesen Mehraufwand verhindern, indem wir direkte Berechnungen mit Operatoren wie durchführen *= Und +=oder durch die Nutzung der out Parameter, der in quick alle universellen NumPy-Funktionen integriert ist.

Diese naive Methode führt eine grundlegende lineare Skalierung auf einem riesigen Array durch, was zu mehreren temporären Zuweisungen führt:

import numpy as np
import time

# Create a big 1D array of 10 million parts
x = np.random.rand(10000000)
scale = 2.5
offset = 1.2

start_time = time.time()

# Commonplace chained math creates non permanent intermediate arrays
y_naive = scale * x + offset

duration_naive = time.time() - start_time
print(f"Chained expression executed in: {duration_naive:.4f} seconds")

Ausgabe:

Chained expression executed in: 0.0393 seconds

Hier weisen wir das Zielausgabearray einmal vorab zu und verwenden seinen Puffer für alle nachfolgenden mathematischen Operationen wieder, wobei wir temporäre Zuweisungen umgehen:

import numpy as np
import time

# Create a big 1D array of 10 million parts
x = np.random.rand(10000000)
scale = 2.5
offset = 1.2

start_time = time.time()

# Pre-allocate the ultimate array
y_optimized = np.empty_like(x)

# Carry out math straight into the goal buffer with out intermediate variables
np.multiply(x, scale, out=y_optimized)
np.add(y_optimized, offset, out=y_optimized)

duration_optimized = time.time() - start_time

print(f"Optimized in-place expression executed in: {duration_optimized:.4f} seconds")
print(f"Speedup: {duration_naive / duration_optimized:.2f}x sooner!")

Ausgabe:

Optimized in-place expression executed in: 0.0133 seconds

Im optimierten Beispiel verwenden wir np.multiply(x, scale, out=y_optimized) um das Ergebnis der Multiplikation direkt in unsere Vorabzuordnung zu schreiben y_optimized Array. Dann, np.add(y_optimized, offset, out=y_optimized) fügt den Offset hinzu und schreibt das Ergebnis zurück in denselben Puffer. Dadurch wird die Zuweisung und Müllsammlung temporärer Puffer vollständig vermieden, Systemspeicher gespart, Daten im CPU-Cache gehalten und die Ausführungsgeschwindigkeit erhöht.

# 3. Speicheransichten vs. Speicherkopien (Slicing vs. erweiterte Indizierung)

Verstehen, wann NumPy a zurückgibt Sicht eines Arrays versus a Kopie ist eines der kritischsten Themen in der numerischen Programmierung:

  • Eine Aussicht ist ein neues Array-Objekt, das auf genau denselben zugrunde liegenden Datenpuffer verweist wie das ursprüngliche Array. Das Erstellen einer Ansicht ist ein Vorgang ohne Kopie, der in $O(1)$ konstanter Zeit und Raum ausgeführt wird.
  • Eine Kopie Ordnet einen brandneuen Datenpuffer zu und dupliziert die Daten. Dies läuft in $O(N)$ linearer Zeit und Raum.

Grundlegendes Slicing (mit Begin-, Stopp- und Schrittindizes, z. B arr(0:10:2)) gibt immer eine Ansicht zurück. Im Gegensatz dazu bietet die erweiterte Indizierung (mithilfe von Indexlisten oder booleschen Masken, z. B arr((0, 2, 4))) gibt immer eine Kopie zurück.

Wenn Sie nur Teilsegmente eines Arrays lesen oder aktualisieren müssen, führt die Verwendung der erweiterten Indizierung zu massiven, unnötigen Speicherzuweisungen.

Hier versuchen wir, eine umfangreiche 2D-Matrix (jede zweite Zeile und Spalte) einer Unterabtastung zu unterziehen, indem wir Indexlisten übergeben. Dies zwingt NumPy dazu, ein großes neues Array zuzuweisen und alle Elemente zu kopieren:

import numpy as np
import time

# Create a matrix of 10,000 x 10,000 parts
matrix = np.random.rand(10000, 10000)

start_time = time.time()

# Superior indexing utilizing integer arrays forces a bodily copy of knowledge
rows = np.arange(0, matrix.form(0), 2)
cols = np.arange(0, matrix.form(1), 2)
sub_matrix_copy = matrix(rows(:, None), cols)

duration_copy = time.time() - start_time
print(f"Superior indexing copy accomplished in: {duration_copy:.4f} seconds")

Ausgabe:

Superior indexing copy accomplished in: 0.1575 seconds

Führen wir nun den gleichen Vorgang aus, verwenden jedoch das grundlegende Slicing. Anstatt Daten zu kopieren, passt NumPy die Schrittmetadaten so an, dass sie sofort auf denselben Puffer verweisen:

import numpy as np
import time

# Create a matrix of 10,000 x 10,000 parts
matrix = np.random.rand(10000, 10000)

start_time = time.time()

# Primary slicing returns a zero-copy view immediately
sub_matrix_view = matrix(::2, ::2)

duration_view = time.time() - start_time
print(f"Primary slicing view accomplished in: {duration_view:.8f} seconds")

Ausgabe:

Primary slicing view accomplished in: 0.00001001 seconds

Wenn Sie ein Array mit segmentieren matrix(::2, ::2)NumPy berührt den zugrunde liegenden Datenpuffer nicht. Es wird einfach ein neuer Array-Header mit geänderten Metadaten erstellt: eine andere Kind und neu Schritte (die Anzahl der Bytes, die in jeder Dimension durchlaufen werden müssen, um das nächste Component zu finden). Dieser Vorgang dauert weniger als eine Mikrosekunde, unabhängig davon, wie groß die Matrix ist.

Beachten Sie jedoch den Kompromiss: Da die Ansicht denselben Speicherpuffer verwendet, mutiert sie sub_matrix_view wird das Unique verändern matrix sowie. Wenn Sie eine Änderung des ursprünglichen Arrays vermeiden müssen, müssen Sie es explizit aufrufen .copy().

# Zusammenfassung

Das Schreiben von sauberem, leistungsstarkem NumPy-Code erfordert eine Änderung Ihrer Denkweise über Schleifen, Speicherzuweisungen und Datenstrukturen. Indem Sie Commonplace-Python-Konzepte zugunsten der nativen NumPy-Mechanik vermeiden, können Sie rechnerische Engpässe beseitigen.

Um es noch einmal zusammenzufassen:

  • Vergessen Sie Python-Schleifen und np.vectorize und lassen Sie vektorisiertes Broadcasting die Berechnungen auf optimiertes C herunterschieben
  • Verwenden Sie Vor-Ort-Vorgänge und die out Parameter zum Umgehen des Allokators, um Cache-Thrashing zu verhindern und die RAM-Nutzung zu reduzieren
  • Grasp-Ansichten im Vergleich zu Kopien, um sofortiges Slicing ohne Kopie anstelle teurer Kopien mit erweiterter Indizierung zu nutzen

Durch die Integration dieser drei Leistungsdesignmuster bleiben Ihre Datenverarbeitungspipelines schlank, schnell und für Produktionsworkloads skalierbar.

Matthew Mayo (@mattmayo13) hat einen Grasp-Abschluss in Informatik und ein Diplom in Knowledge Mining. Als geschäftsführender Herausgeber von KDnuggets & Statistikund Mitherausgeber bei Beherrschung des maschinellen LernensZiel von Matthew ist es, komplexe datenwissenschaftliche Konzepte zugänglich zu machen. Zu seinen beruflichen Interessen zählen die Verarbeitung natürlicher Sprache, Sprachmodelle, Algorithmen für maschinelles Lernen und die Erforschung neuer KI. Seine Mission ist es, das Wissen in der Datenwissenschaftsgemeinschaft zu demokratisieren. Matthew programmiert seit seinem sechsten Lebensjahr.



Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert