【问题标题】:Best approach to use numpy 1d arrays for linear algebra将 numpy 一维数组用于线性代数的最佳方法
【发布时间】:2023-03-05 22:50:01
【问题描述】:

在使用 numpy 进行线性代数运算时,我遇到了很多“小”问题,因为 numpy 处理“向量”或一维数组的方式在我看来会导致不一致的行为。

我的问题是,如果我在如何将 numpy 数组用于线性代数时犯了一些明显的错误,或者这就是它的工作原理并且没有其他明显的方法可以做到这一点?

例如,假设我想对两个向量执行单变量 OLS。

import numpy as np
from numpy import linalg as la

y = np.arange(10)
x = np.arange(10)
print(x.shape)

ols = la.inv(x.T@x)@(x.T@y)

LinAlgError: 0-dimensional array given. Array must be at least two-dimensional

所以一种解决方案是强制数组具有额外的维度:

import numpy as np
from numpy import linalg as la

y = np.arange(10).reshape(-1, 1)
x = np.arange(10).reshape(-1, 1)

ols = la.inv(x.T@x)@(x.T@y)
print(ols)
>>> [[1.]]

然后可以认为问题解决了!但不完全是。如果我在 X 轴上有更多维度,计算 t 值就会成为问题。

y = np.arange(10).reshape(-1, 1)
X = np.arange(20).reshape(10, 2)
b_hat = la.inv((X.T@X))@(X.T@y)

# Calculate standard errors
residual = y - X@b_hat
sigma_hat = residual.T@residual/(y.size - b_hat.size)
b_var = sigma_hat*la.inv(X.T@X)
b_std = np.sqrt(b_var.diagonal())  # The diagonal method returns 1d array.

# Calculate t-values
t_values = b_hat/b_std
print(t_values)

>>> [[2.47854011e+13 2.67712930e+13]
 [1.40888694e+00 1.52177182e+00]]

这当然不是故意的。为什么会这样?这是因为 np.sqrt(b_var.diagonal()) 返回 b_std 的 (2,) 形状。因此,当我划分b_hat/b_std numpy 时,会检查它们是否具有相同的形状,它们不是(b_hat 具有(2, 1) 形状),并且 numpy 不会进行“真正的”划分,而是进行其他类型的划分。

解决这个问题的方法当然是再次使用.reshape(-1, 1),但是我的计算会越来越复杂,所以总是检查一个向量是作为一维数组还是二维数组返回,然后重新整形是很麻烦的。如果我不小心将矩阵重塑为向量,这也容易出错。

那么,我在如何将 numpy 数组用于线性代数方面是否犯了一些明显的错误,或者这就是它的工作原理并且没有其他明显的方法可以做到这一点? p>

【问题讨论】:

  • 我不确定您认为其中哪一部分不一致。此外,您似乎隐含地假设 NumPy 会在您对一维向量感到惊讶的所有地方使用列向量。没有特别的理由会使用列向量而不是行向量。
  • 我不会称这种“不一致”行为。 numpy 有你需要遵循的明确规则。例如。在第二个示例中,这就是广播的工作方式。请注意,您可以使用 None 快速添加维度,例如b_std[:, None] 会给你一个 (2, 1 向量)。我也不认为期望知道一些基本操作的返回形状,比如diag
  • 我可以理解,对于大多数人来说,这并不矛盾,只是“在我看来”。你当然是对的,我确实假设它返回一个列向量。但是写np.sqrt(b_var.diagonal()).T 并不能解决问题,因为一维数组没有行/列的概念。
  • NumPy 几乎只会在您明确要求时插入额外的长度为 1 的维度。您担心的繁琐的一维或二维检查几乎不会出现。你只需要摆脱面向矩阵的思维方式,因为 NumPy 不是面向矩阵的库。
  • @user2357112supportsMonica 是的,我有点想知道。如果我误解了一些基本概念,或者 numpy 1d 数组没有面向矩阵的功能。这很好。感谢所有 cmets!

标签: python arrays numpy linear-algebra


【解决方案1】:

也许记住这些规则可能有助于在使用numpy 时提供您所需要的谨慎(术语轴和尺寸在这里的含义相同):

  1. 在数学中,当我们写下一系列数字[1 2 3 4] 时,我们选择与该符号相关联的语义会随着上下文的不同而略有不同。有时我们将其视为单轴数组(这是正确的语义),但有时我们将其视为“1行4列”。您如何证明数学家声称列向量在转置时会给出行向量,反之亦然?术语“转置”意味着行和列的交换,这本身意味着是两个轴,而不仅仅是一个。在numpy 的情况下,同一事物的语义一致且严格是“长度为 4 的单轴”,而不是“长度为 1 的第一轴和长度为 4 的第二轴”。
  2. numpy 中,就像在数学中一样,只有当您至少有两个轴时,转置的想法才有意义。如上所述,在数学中,我们没有一致的符号来区分单轴阵列和双轴阵列,因此这个规则实际上没有实际意义。在numpy 执行arr.T 时,如果arr 恰好是一个单轴数组,则只返回arr
  3. numpy 中,我们可以在我们选择的任何位置添加一个单位长度的额外轴。对此的一种表示法是arr.reshape(n1,n2,...1,...,nk)(即通过在现有的逗号分隔轴长度中间插入1)。另一种方法是使用索引符号arr[:,:,...,None,...,:](即,使用与轴一样多的逗号分隔冒号,并在其中插入None)。代替None,也可以用np.newaxis,不过有点啰嗦。
  4. 基于上述情况,如果arr 具有单轴(例如,形状(3,)),我们可能会期望numpy 矩阵乘法运算符@arr @ arr.T 中引发错误。 (如何为单轴数组定义矩阵乘法?)相反,该表达式返回 arrarr.T 的元素的和积,并将其作为标量返回(甚至不作为单元素数组)。
  5. numpy 中,算术和比较运算符在两个具有相同形状的数组之间使用时,将按“元素方式”应用(这意味着在属于两个数组的每对对应元素之间)。这将产生一个新数组,其形状与操作数数组的形状相同。
  6. 使用算术和比较运算符时,如果两个操作数是形状不同但可广播为一个共同形状的数组,则运算符在广播后按元素应用,结果将再次成为具有广播的数组-生成的形状。
  7. 对于算术和比较运算符,如果其中一个操作数是标量,则标量将被视为形状为 (1,) 的数组,然后将应用之前的(广播)规则。
  8. 虽然上述第 5、6、7 点实际上增加了numpy 的表达能力,但它们经常让新学习者感到惊讶。例如,1.0 / arr 其中arr[1 2 3 4] 将生成一个由值[1.0/1 1.0/2 1.0/3 1.0/4] 组成的新数组。 (我认为这是您在进行分区时遇到的惊喜之一)
  9. 如果arr 的形状为(3,4,1,5,2,1,1),则arr.squeeze() 将摆脱单位长度轴,从而返回形状为(3,4,5,2) 的数组
  10. 当我们索引一个多维数组时,我们通常期望结果具有更低的维度(更少的轴,和/或更小的相同轴的长度)或相同的维度,因为被索引的数组。在numpy 中,像arr[my_index_arr] 这样的索引表达式可以产生比原始数组arr 更复杂和更高维度的形状。同样,这是一个强大的表达功能,有时会使新学习者感到惊讶/困惑。在numpy 中,这称为Advanced Indexing with Integer Arrays

为了强调上述一点,当您的数组有一个单轴(形状类似于(L,))时,请特别注意您的期望。

【讨论】:

    【解决方案2】:

    即使在面向矩阵的 MATLAB(最初只有 2d 矩阵)中,我发现跟踪维度是调试工作的 80%。在numpy 中,没有捷径可以密切关注尺寸。不要假设。

    在您的第一种情况下,数组是 1d:

    In [305]: x
    Out[305]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    In [306]: x.T                   # same, only one axis to 'switch'
    Out[306]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    

    一维数组的矩阵乘积是dot/inner 乘积,结果是一个标量

    In [307]: x.T@x
    Out[307]: 285
    

    @/matmul 不喜欢使用标量。 np.dot 可以接受,但 matmul 是为处理“批量”数组而创建的:

    In [308]: (x@x)@(x@y)
    Traceback (most recent call last):
      File "<ipython-input-308-2d486b202d47>", line 1, in <module>
        (x@x)@(x@y)
    TypeError: unsupported operand type(s) for @: 'numpy.int64' and 'numpy.int64'
    

    然后创建二维数组,(10,1) 形状

    In [309]: y = y[:,None]
    In [310]: y
    Out[310]: 
    array([[0],
           [1],
           ...
           [9]])
    In [311]: y.T             # (1,10) shape
    Out[311]: array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
    

    现在matmul (1,10) 与 (10,1) 对 10 求和以生成 (1,1) 数组:

    In [312]: y.T@y
    Out[312]: array([[285]])
    In [313]: _.shape
    Out[313]: (1, 1)
    

    两个 (1,1) 数组与 @ 一起工作以再次产生 (1,1),inv 可以这样做。

    X

    In [314]: X.shape
    Out[314]: (10, 2)
    In [315]: X.T@X         #  (2,10) with (10,2) producing (2,2)
    Out[315]: 
    array([[1140, 1230],
           [1230, 1330]])
    In [316]: X.T@y         # (2,10) with (10,1) producing (2,1)  
    Out[316]: 
    array([[570],
           [615]])
    In [317]: (X.T@X)@(X.T@y)         # (2,2) with (2,1) producing (2,1)
    Out[317]: 
    array([[1406250],
           [1519050]])
    

    所有这些的关键是 (K,n) 和 (n,M) 产生 (K,M) 与共享 n 上的产品总和 - 第一个的最后一个,第二个到第二个参数的最后一个。那就是高中跨行和下列做矩阵乘积的方法。

    matmul 文档谈论将 1d 数组提升为 2d 以执行相同的操作 - 但结果中删除了额外的维度。

    下一个 @ 将 (10,2) 与 (2,1) 连接以产生 (10,1)(对 2 求和):

    In [319]: X@Out[317]
    Out[319]: 
    array([[ 1519050],
           [ 7369650],
           [13220250],
            ...
           [54174450]])
    

    可以用 (10,1) y 减去。

    residual.T@residual 是 (1,10) 与 (10,1) 乘积 (1,1)(大小)。

    sigma_hat*la.inv(X.T@X) 是 (1,1) 乘以 (2,2) 得到 (2,2) (如果 sigma_hat 是标量,则同样有效。inv(A) 在 (1, 1) 只是1/A

    b_var.diagonal() throws away 具有b_var 的值。因为它是一个标量时间X.T@X,我们可以检查:

    In [323]: X.T@X
    Out[323]: 
    array([[1140, 1230],
           [1230, 1330]])
    In [324]: (X.T@X).diagonal()
    Out[324]: array([1140, 1330])
    

    这与在大小 10 维度上求和相同

    In [325]: (X*X).sum(0)
    Out[325]: array([1140, 1330])
    In [326]: np.einsum('ij,ij->j',X,X)
    Out[326]: array([1140, 1330])
    

    它将二维视为一个“批次”:

    In [328]: [X[:,i]@X[:,i] for i in range(2)]
    Out[328]: [1140, 1330]
    

    matmul 设计用于“批处理” - 适用于 3d(及更高)数组:

    In [329]: Xt = X.T
    In [330]: Xt.shape
    Out[330]: (2, 10)
    In [331]: Xt[:,None,:]@Xt[:,:,None]    # (2,1,10) with (2,10,1)=>(2,1,1)
    Out[331]: 
    array([[[1140]],
    
           [[1330]]])
    In [332]: (Xt[:,None,:]@Xt[:,:,None]).squeeze()
    Out[332]: array([1140, 1330])
    

    这表明X 从一开始就应该是 (2,10) 甚至 (2,1,10)。 (和y (1,10)?)。

    这个答案有点啰嗦,但希望这些细节能让您了解如何跟踪尺寸。它旨在补充其他答案的一般原则。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-03-17
      • 1970-01-01
      • 2016-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多