Batched Linear Operations#

Fast alle linearen Algebra-Funktionen von SciPy unterstützen nun N-dimensionale Array-Eingaben. Diese Operationen wurden nicht mathematisch auf höherwertige Tensoren verallgemeinert; vielmehr wird die angegebene Operation auf einem Batch (oder „Stack“) von Eingabeskala-Werten, Vektoren und/oder Matrizen durchgeführt.

Betrachten Sie die Funktion linalg.det, die eine Matrix auf einen Skalar abbildet.

import numpy as np
from scipy import linalg
A = np.eye(3)
linalg.det(A)
np.float64(1.0)

Manchmal benötigen wir die Determinante eines Batches von Matrizen gleicher Dimensionalität.

batch = [i*np.eye(3) for i in range(1, 4)]
batch
[array([[1., 0., 0.],
        [0., 1., 0.],
        [0., 0., 1.]]),
 array([[2., 0., 0.],
        [0., 2., 0.],
        [0., 0., 2.]]),
 array([[3., 0., 0.],
        [0., 3., 0.],
        [0., 0., 3.]])]

Wir könnten die Operation für jedes Element des Batches in einer Schleife oder einer Listenkomprehension durchführen

[linalg.det(A) for A in batch]
[np.float64(1.0), np.float64(8.0), np.float64(27.0)]

Jedoch, genauso wie wir NumPy-Broadcasting- und Vektorisierungsregeln verwenden könnten, um den Batch von Matrizen überhaupt erst zu erstellen

i = np.arange(1, 4).reshape(-1, 1, 1)
batch = i * np.eye(3)
batch
array([[[1., 0., 0.],
        [0., 1., 0.],
        [0., 0., 1.]],

       [[2., 0., 0.],
        [0., 2., 0.],
        [0., 0., 2.]],

       [[3., 0., 0.],
        [0., 3., 0.],
        [0., 0., 3.]]])

könnten wir auch wünschen, die Determinantenoperation auf allen Matrizen in einem einzigen Funktionsaufruf durchzuführen.

linalg.det(batch)
array([ 1.,  8., 27.])

In SciPy bevorzugen wir den Begriff „Batch“ anstelle von „Stack“, da die Idee auf N-dimensionale Batches verallgemeinert wird. Angenommen, die Eingabe ist ein 2 x 4 Batch von 3 x 3 Matrizen.

batch_shape = (2, 4)
i = np.arange(np.prod(batch_shape)).reshape(*batch_shape, 1, 1)
input = i * np.eye(3)

In diesem Fall sagen wir, dass die Batch-Form (2, 4) ist und die Kernform der Eingabe (3, 3) ist. Die Gesamtform der Eingabe ist die Summe (Konkatenation) der Batch-Form und der Kernform.

input.shape
(2, 4, 3, 3)

Da jede 3 x 3 Matrix in einen null-dimensionalen Skalar umgewandelt wird, sagen wir, dass die Kernform der Ausgabe () ist. Die Form der Ausgabe ist die Summe der Batch-Form und der Kernform, sodass das Ergebnis ein 2 x 4 Array ist.

output = linalg.det(input)
output
array([[  0.,   1.,   8.,  27.],
       [ 64., 125., 216., 343.]])
output.shape
(2, 4)

Nicht alle linearen Algebra-Funktionen bilden auf Skalare ab. Zum Beispiel bildet die Funktion scipy.linalg.expm von einer Matrix auf eine Matrix mit derselben Form ab.

A = np.eye(3)
linalg.expm(A)
array([[2.71828183, 0.        , 0.        ],
       [0.        , 2.71828183, 0.        ],
       [0.        , 0.        , 2.71828183]])

In diesem Fall ist die Kernform der Ausgabe (3, 3), sodass wir bei einer Batch-Form von (2, 4) eine Ausgabe der Form (2, 4, 3, 3) erwarten.

output = linalg.expm(input)
output.shape
(2, 4, 3, 3)

Die Verallgemeinerung dieser Regeln auf Funktionen mit mehreren Ein- und Ausgaben ist unkompliziert. Zum Beispiel erzeugt die Funktion scipy.linalg.eig standardmäßig zwei Ausgaben, einen Vektor und eine Matrix.

evals, evecs = linalg.eig(A)
evals.shape, evecs.shape
((3,), (3, 3))

In diesem Fall ist die Kernform des Ausgabevektors (3,) und die Kernform der Ausgabematrix (3, 3). Die Form jeder Ausgabe ist die Batch-Form plus die Kernform, wie zuvor.

evals, evecs = linalg.eig(input)
evals.shape, evecs.shape
((2, 4, 3), (2, 4, 3, 3))

Wenn es mehr als eine Eingabe gibt, gibt es keine Komplikation, wenn die Eingabeformen identisch sind.

evals, evecs = linalg.eig(input, b=input)
evals.shape, evecs.shape
((2, 4, 3), (2, 4, 3, 3))

Die Regeln, wenn die Formen nicht identisch sind, folgen logisch. Jede Eingabe kann ihre eigene Batch-Form haben, solange die Formen gemäß den Broadcasting-Regeln von NumPy broadcastbar sind. Die Gesamt-Batch-Form ist die gebroadcastete Form der einzelnen Batch-Formen, und die Form jeder Ausgabe ist die Gesamt-Batch-Form plus ihre Kernform.

rng = np.random.default_rng(2859239482)

# Define input core shapes
m = 3
core_shape_a = (m, m)
core_shape_b = (m, m)

# Define broadcastable batch shapes
batch_shape_a = (2, 4)
batch_shape_b = (5, 1, 4)

# Define output core shapes
core_shape_evals = (m,)
core_shape_evecs = (m, m)

# Predict shapes of outputs: broadcast batch shapes,
# and append output core shapes
net_batch_shape = np.broadcast_shapes(batch_shape_a, batch_shape_b)
output_shape_evals = net_batch_shape + core_shape_evals
output_shape_evecs = net_batch_shape + core_shape_evecs
output_shape_evals, output_shape_evecs
((5, 2, 4, 3), (5, 2, 4, 3, 3))
# Check predictions
input_a = rng.random(batch_shape_a + core_shape_a)
input_b = rng.random(batch_shape_b + core_shape_b)
evals, evecs = linalg.eig(input_a, b=input_b)
evals.shape, evecs.shape
((5, 2, 4, 3), (5, 2, 4, 3, 3))

Es gibt einige Funktionen, bei denen die Kern-Dimensionalität (d. h. die Länge der Kern-Form) eines Arguments oder einer Ausgabe entweder 1 oder 2 sein kann. In diesen Fällen wird die Kern-Dimensionalität als 1 angenommen, wenn das Array nur eine Dimension hat, und als 2, wenn das Array zwei oder mehr Dimensionen hat. Betrachten Sie zum Beispiel die folgenden Aufrufe von scipy.linalg.solve. Der einfachste Fall ist eine einzelne quadratische Matrix A und ein einzelner Vektor b

A = np.eye(5)
b = np.arange(5)
linalg.solve(A, b)
array([0., 1., 2., 3., 4.])

In diesem Fall ist die Kern-Dimensionalität von A 2 (Form (5, 5)), die Kern-Dimensionalität von b ist 1 (Form (5,)) und die Kern-Dimensionalität der Ausgabe ist 1 (Form (5,)).

Jedoch kann b auch ein zweidimensionales Array sein, bei dem die Spalten als eindimensionale Vektoren betrachtet werden.

b = np.empty((5, 2))
b[:, 0] = np.arange(5)
b[:, 1] = np.arange(5, 10)
linalg.solve(A, b)
array([[0., 5.],
       [1., 6.],
       [2., 7.],
       [3., 8.],
       [4., 9.]])
b.shape
(5, 2)

Auf den ersten Blick könnte man meinen, dass die Kernform von b immer noch (5,) ist, und wir haben einfach die Operation mit einer Batch-Form von (2,) durchgeführt. Wenn dies jedoch der Fall wäre, würde die Batch-Form von b der Kernform vorangestellt, was dazu führen würde, dass b und die Ausgabe die Form (2, 5) hätten. Wenn man genauer darüber nachdenkt, ist es korrekt, die Kern-Dimensionalität sowohl der Eingaben als auch der Ausgabe als 2 zu betrachten; die Batch-Form ist ().

Ebenso wird immer dann, wenn b mehr als zwei Dimensionen hat, die Kern-Dimensionalität von b und der Ausgabe als 2 betrachtet. Um zum Beispiel einen Batch von drei vollständig separaten linearen Systemen zu lösen, die jeweils nur eine rechte Seite haben, muss b als dreidimensionales Array bereitgestellt werden: eine Dimension für die Batch-Form ((3,)) und zwei für die Kernform ((5, 1)).

A = rng.random((3, 5, 5))
b = rng.random((3, 5, 1))  # batch shape (3,), core shape (5, 1)
linalg.solve(A, b).shape
(3, 5, 1)