批量线性运算#

SciPy 的几乎所有线性代数函数现在都支持 N 维数组输入。这些操作尚未从数学上推广到高阶张量;相反,所指示的操作是针对输入标量、向量和/或矩阵的批次(或“堆栈”)执行的。

考虑 linalg.det 函数,它将矩阵映射到标量。

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

有时我们需要相同维度的矩阵批次的行列式。

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

我们可以通过循环或列表推导式对批次中的每个元素执行操作

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

然而,就像我们可能首先使用 NumPy 的广播和向量化规则来创建矩阵批次一样

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

我们也可能希望在一次函数调用中对所有矩阵执行行列式运算。

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

在 SciPy 中,我们更喜欢使用“批次”(batch)而不是“堆栈”(stack),因为这个概念已推广到 N 维批次。假设输入是一个 2 x 4 的 3 x 3 矩阵批次。

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

在这种情况下,我们称批次形状(2, 4),输入的核心形状(3, 3)。输入的总形状是批次形状和核心形状的总和(连接)。

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

由于每个 3 x 3 矩阵都被转换为零维标量,我们称输出的核心形状为 ()。输出的形状是批次形状和核心形状的总和,因此结果是一个 2 x 4 数组。

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

并非所有线性代数函数都映射到标量。例如,scipy.linalg.expm 函数将矩阵映射到具有相同形状的矩阵。

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

在这种情况下,输出的核心形状是 (3, 3),因此当批次形状为 (2, 4) 时,我们期望的输出形状是 (2, 4, 3, 3)

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

将这些规则推广到具有多个输入和输出的函数也很简单。例如,scipy.linalg.eig 函数默认产生两个输出:一个向量和一个矩阵。

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

在这种情况下,输出向量的核心形状是 (3,),输出矩阵的核心形状是 (3, 3)。每个输出的形状仍然是批次形状加上核心形状。

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

当存在多个输入时,如果输入形状相同,则不会出现复杂情况。

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

当形状不相同时,规则也遵循逻辑。只要形状根据 NumPy 的广播规则 可广播,每个输入都可以有自己的批次形状。净批次形状是个别批次形状的广播结果,每个输出的形状是净批次形状加上其核心形状。

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

有一些函数,其参数或输出的核心维度(即核心形状的长度)可以是 1 或 2。在这些情况下,如果数组只有一个维度,则核心维度被视为 1;如果数组有两个或更多维度,则被视为 2。例如,考虑对 scipy.linalg.solve 的以下调用。最简单的情况是单个方阵 A 和单个向量 b

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

在这种情况下,A 的核心维度是 2(形状 (5, 5)),b 的核心维度是 1(形状 (5,)),输出的核心维度是 1(形状 (5,))。

然而,b 也可以是一个二维数组,其中被视为一维向量。

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)

乍一看,b 的核心形状似乎仍然是 (5,),而我们只是以 (2,) 的批次形状执行了操作。然而,如果是这种情况,b 的批次形状将*前置*到核心形状,导致 b 和输出的形状为 (2, 5)。更仔细地思考,将两个输入和输出的核心维度都视为 2 是正确的;此时批次形状是 ()

同样,每当 b 具有超过两个维度时,b 和输出的核心维度都被视为 2。例如,要解决批处理的三个完全独立的线性系统,每个系统只有一个右侧,b 必须作为三维数组提供:一个维度用于批次形状((3,)),两个维度用于核心形状((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)