numpy.array形状(R,1)和(R,)的区别

428

numpy 中,一些操作返回的形状为 (R, 1),但有些返回为 (R,)。这将使矩阵乘法更加繁琐,因为需要显式地进行 reshape。例如,给定一个矩阵 M,如果我们想要执行 numpy.dot(M[:,0], numpy.ones((1, R))),其中 R 是行数 (当然,列也会遇到同样的问题)。我们会得到一个错误信息“matrices are not aligned”,因为 M[:,0] 的形状是 (R,),而 numpy.ones((1, R)) 的形状是 (1, R)

所以我的问题是:

  1. 形状 (R, 1)(R,) 之间有什么区别?我知道从字面上讲,它们分别是由数字列表和只包含一个数字的列表组成的列表。只是在想,为什么不把 numpy 设计成偏向于形状 (R, 1),以便更容易进行矩阵乘法呢?

  2. 有没有更好的方法来处理上面的例子?不需要像这样显式地进行 reshapenumpy.dot(M[:,0].reshape(R, 1), numpy.ones((1, R)))


3
这可能会有所帮助。不过并不能帮助找到实际的解决方案。 - keyser
2
正确的解决方案:numpy.ravel( M[ : , 0] ) -- 将形状从 (R, 1) 转换为 (R,) - Andi R
1
元组不是由括号确定的,它们不是元组的一部分,而是由逗号确定的。x=4, 分配一个元组,x=(4) 分配一个整数,这常常会引起混淆。形状 n, 表示具有 n 个项目的一维数组的形状,而 n,1 表示具有 n 行 x 1 列的数组的形状。(R,)(R,1) 只是添加了(无用的)括号,但仍然分别表示 1D 和 2D 数组的形状。在元组周围加上括号可以强制求值顺序并防止其被读取为值列表(例如在函数调用中)。记住这种元组的奇特性,事情就变得更清晰了,NumPy 返回有意义的形状。 - mins
8个回答

711

1. NumPy中形状的含义

你这样写道:“我知道它实际上是一个数字列表和一个包含仅数字的列表的列表”,但这种方式并不是很有帮助。

最好的方法是将NumPy数组看作由两个部分组成:一个数据缓冲区,它只是一块原始元素的块,以及一个视图,描述了如何解释数据缓冲区。

例如,如果我们创建了一个包含12个整数的数组:

>>> a = numpy.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

然后a由一个数据缓冲区组成,排列方式类似于:

┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│  01234567891011 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

并且有一个视图,描述了如何解释这些数据:

>>> a.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : True
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False
>>> a.dtype
dtype('int64')
>>> a.itemsize
8
>>> a.strides
(8,)
>>> a.shape
(12,)

在这里,形状 (12,) 表示该数组由单个索引进行索引,该索引从0到11运行。就概念上而言,如果我们将这个单一的索引标记为i,那么数组a看起来像这样:

i= 0    1    2    3    4    5    6    7    8    9   10   11
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│  0 │  1 │  2 │  3 │  4 │  5 │  6 │  7 │  8 │  9 │ 10 │ 11 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

如果我们重塑一个数组,这并不会改变数据缓冲区。相反,它会创建一个新的视图来描述不同的数据解释方式。因此,在进行以下操作后:

>>> b = a.reshape((3, 4))

数组ba拥有相同的数据缓冲区,但现在它由两个索引索引,分别从0到2和0到3。如果我们标记这两个索引为ij,那么数组b看起来像这样:

i= 0    0    0    0    1    1    1    1    2    2    2    2
j= 0    1    2    3    0    1    2    3    0    1    2    3
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│  0 │  1 │  2 │  3 │  4 │  5 │  6 │  7 │  8 │  9 │ 10 │ 11 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

这意味着:

>>> b[2,1]
9

您可以看到第二个索引的变化速度很快,而第一个索引的变化速度较慢。如果您希望反过来,可以指定order参数:

>>> c = a.reshape((3, 4), order='F')

这将导致一个像这样索引的数组:

i= 0    1    2    0    1    2    0    1    2    0    1    2
j= 0    0    0    1    1    1    2    2    2    3    3    3
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│  0 │  1 │  2 │  3 │  4 │  5 │  6 │  7 │  8 │  9 │ 10 │ 11 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

这意味着:

>>> c[2,1]
5

现在应该很清楚,一个数组具有一个或多个尺寸为1的维度的形状是什么意思了。之后:

>>> d = a.reshape((12, 1))

数组d由两个索引引用,第一个索引从0到11,第二个索引始终为0:

i= 0    1    2    3    4    5    6    7    8    9   10   11
j= 0    0    0    0    0    0    0    0    0    0    0    0
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│  0 │  1 │  2 │  3 │  4 │  5 │  6 │  7 │  8 │  9 │ 10 │ 11 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

因此:

>>> d[10,0]
10

长度为1的维度在某种程度上是“自由”的,因此没有任何阻止你进行操作:

>>> e = a.reshape((1, 2, 1, 6, 1))

给出一个像这样索引的数组:

i= 0    0    0    0    0    0    0    0    0    0    0    0
j= 0    0    0    0    0    0    1    1    1    1    1    1
k= 0    0    0    0    0    0    0    0    0    0    0    0
l= 0    1    2    3    4    5    0    1    2    3    4    5
m= 0    0    0    0    0    0    0    0    0    0    0    0
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│  0 │  1 │  2 │  3 │  4 │  5 │  6 │  7 │  8 │  9 │ 10 │ 11 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

因此:

>>> e[0,1,0,0,0]
6

查看NumPy内部文档了解有关如何实现数组的更多细节。

2. 怎么做?

由于numpy.reshape只是创建一个新视图,所以在必要时使用它不必担心。当您想以不同的方式对数组进行索引时,这是正确的工具。

然而,在长时间计算中,通常可以安排在第一次构造具有“正确”形状的数组,从而最小化重塑和转置的数量。但是,如果没有看到导致需要重塑的实际上下文,则很难说应该更改什么。

您问题中的示例为:

numpy.dot(M[:,0], numpy.ones((1, R)))

但这不是现实。首先,这个表达式:

M[:,0].sum()

计算结果更简单。其次,第0列是否真的有特殊之处?也许您实际需要的是:

M.sum(axis=0)

50
这篇文章帮助我更好地理解了数组的存储方式,非常有帮助,谢谢!不过,要进行进一步矩阵计算时,访问(2维)矩阵的列(或行)比较麻烦,因为我必须将列适当地重新塑形。每次都需要将其从 (n,) 改变为 (n,1) 的形式。 - OfLettersAndNumbers
4
如果您需要添加另一个轴,可以使用newaxis。例如,a[:, j, np.newaxis]a的第j列,“a[np.newaxis, i]”是a的第i行。请参考newaxis - Gareth Rees
1
我正在尝试绘制指数以更好地理解这个模型,但似乎无法理解。如果我有一个形状为2 x 2 x 4的数组,我可以将前两个理解为0000000011111111,最后四个理解为0123012301230123,那么中间的会发生什么? - PirateApp
5
这里一个简单的想法是,numpy 在这里的工作方式与预期完全一致,但 Python 对元组的打印可能会导致误解。在 (R,) 的情况下,ndarray 的形状是一个具有单个元素的元组,因此 Python 打印时会带上逗号。去掉额外的逗号,它将 与括号中的表达式模糊不清。具有单个维度的 ndarray 可以被视为长度为 R 的列向量。在 (R, 1) 的情况下,元组有两个元素,因此可以视为行向量(或者长度为R的1行矩阵)。 - Michael
1
@Alex-droidAD:请查看这个问题及其答案。 - Gareth Rees
显示剩余3条评论

30

“(R,)”和“(1,R)”之间的区别实际上在于你需要使用的索引数量。"ones((1,R))" 是一个只有一行的二维数组。 "ones(R)"是一个向量。通常,如果变量不应该具有多于一行/列,那么你应该使用一个向量而不是一个具有单例维度的矩阵。

对于您特定的情况,有几个选项:

1) 只需将第二个参数设置为向量即可。以下方法可以正常工作:

    np.dot(M[:,0], np.ones(R))

2)如果您想要类似Matlab的矩阵操作,请使用matrix类而不是ndarray类。所有矩阵都被强制转换为2-D数组,并且运算符*执行矩阵乘法,而不是逐元素相乘(因此您不需要使用点乘法)。根据我的经验,这可能带来更多麻烦,但如果您习惯于Matlab,它可能很有用。


1
是的,我原本期望更像Matlab的行为。我会看一下“矩阵”类。顺便问一下,“矩阵”类有什么问题吗? - clwen
4
“matrix”的问题在于它只是2D的,而且因为它重载了运算符“*”,所以为“ndarray”编写的函数如果用于“matrix”上可能会失败。 - Evan

23

形状是一个元组。如果只有一个维度,形状将会是一个数字,在逗号后面留空。对于2个或更多维度,所有逗号后面都会有一个数字。

# 1 dimension with 2 elements, shape = (2,). 
# Note there's nothing after the comma.
z=np.array([  # start dimension
    10,       # not a dimension
    20        # not a dimension
])            # end dimension
print(z.shape)

(2,)

# 2 dimensions, each with 1 element, shape = (2,1)
w=np.array([  # start outer dimension 
    [10],     # element is in an inner dimension
    [20]      # element is in an inner dimension
])            # end outer dimension
print(w.shape)

(2,1)

注:以上内容已经是中文了,无需翻译。

1
经典。有很多复杂的答案,然后我找到了这个完全解释它的方式。谢谢! - Michael

5

对于其基础数组类而言,二维数组并没有比一维或三维数组更加特殊。有些操作可以保留维度,有些可以减少维度,其他的则是合并或扩展它们。

M=np.arange(9).reshape(3,3)
M[:,0].shape # (3,) selects one column, returns a 1d array
M[0,:].shape # same, one row, 1d array
M[:,[0]].shape # (3,1), index with a list (or array), returns 2d
M[:,[0,1]].shape # (3,2)

In [20]: np.dot(M[:,0].reshape(3,1),np.ones((1,3)))

Out[20]: 
array([[ 0.,  0.,  0.],
       [ 3.,  3.,  3.],
       [ 6.,  6.,  6.]])

In [21]: np.dot(M[:,[0]],np.ones((1,3)))
Out[21]: 
array([[ 0.,  0.,  0.],
       [ 3.,  3.,  3.],
       [ 6.,  6.,  6.]])

其他等价的表示给出相同的数组

np.dot(M[:,0][:,np.newaxis],np.ones((1,3)))
np.dot(np.atleast_2d(M[:,0]).T,np.ones((1,3)))
np.einsum('i,j',M[:,0],np.ones((3)))
M1=M[:,0]; R=np.ones((3)); np.dot(M1[:,None], R[None,:])

MATLAB最初只支持2D数组。新版本允许更多维度,但保留了下限为2的限制。但你仍然需要注意行向量和列向量之间的区别,一个形状为(1,3),另一个形状为(3,1)。你有多少次写过[1,2,3].'?我本来想写行向量列向量,但由于这个2D约束,在MATLAB中并没有向量-至少不是数学上的1D向量。
你看过np.atleast_2d(还有_1d和_3d版本)吗?
在较新的Python/numpy版本中有一个matmul操作符。
In [358]: M[:,0,np.newaxis]@np.ones((1,3))
Out[358]: 
array([[0., 0., 0.],
       [3., 3., 3.],
       [6., 6., 6.]])

numpy 中,逐元素乘法在某种意义上比矩阵乘法更基础。通过对大小为1的尺寸进行积和求和,不需要使用 dot/matmul
In [360]: M[:,0,np.newaxis]*np.ones((1,3))
Out[360]: 
array([[0., 0., 0.],
       [3., 3., 3.],
       [6., 6., 6.]])

这里使用了broadcasting,这是numpy一直拥有的强大功能。MATLAB最近才添加了它。

3

形状为(n,)的数据结构称为一维数组。它不像行向量或列向量那样具有一致的行为,这使得其中一些操作和效果非常不直观。如果你对这个(n,)数据结构取转置,它看起来仍然一样,并且点积将给出一个数字而不是一个矩阵。 形状为(n,1)或(1,n)的向量,即行向量或列向量,更加直观和一致。


1
你的直觉已经被线性代数和/或类似MATLAB的语言所塑造,这些语言主要使用2D数组、矩阵。在MATLAB中,即使是“标量”也是2D的。我们使用Python和numpy不仅仅是为了进行点积运算 :) - hpaulj
我同意。点积帮助我更好地理解了结构,出于同样的原因,我已经提到它 :) - Palak Bansal

3

这里已经有很多好的答案了,但对我来说,很难找到一些示例,其中形状或数组可能会破坏整个程序。

所以这是一个例子:

import numpy as np
a = np.array([1,2,3,4])
b = np.array([10,20,30,40])


from sklearn.linear_model import LinearRegression
regr = LinearRegression()
regr.fit(a,b)

如果不使用reshape,将会出现以下错误:

ValueError: 预计接收二维数组,但只接收到了一维数组

但是如果我们在a中添加reshape

a = np.array([1,2,3,4]).reshape(-1,1)

这个工程正常运作!


1
另外,TensorFlow 2.4 可以参考 https://stackoverflow.com/questions/67662727/tfp-linear-regression-yhat-modelx-tst-doesnt-work-for-other-data。 - Julian Moore

1

1) 之所以不喜欢使用形状为(R, 1)而选择(R,)的原因是,它会使事情变得不必要地复杂。此外,对于一个长度为R的向量,默认使用形状(R, 1)而不是(1, R)是否更可取?保持简单并在需要额外维度时明确表示更好。

2) 对于您的示例,您正在计算外积,因此可以使用np.outer而无需调用reshape

np.outer(M[:,0], numpy.ones((1, R)))

感谢您的回答。1) M[:,0] 实际上是获取所有第一个元素的行,所以使用 (R, 1)(1, R) 更合理。2) 它并不总是可以被 np.outer 替代,例如,对于形状为 (1, R) 和 (R, 1) 的矩阵进行点乘。 - clwen
  1. 是的,那可能是惯例,但在其他情况下这会变得不太方便。惯例也可以是让 M[1, 1] 返回一个形状为 (1, 1) 的数组,但通常比标量更不方便。如果你真的想要类似矩阵的行为,那么最好使用 matrix 对象。
  2. 实际上,np.outer 不管形状是 (1, R)(R, 1) 还是两者的组合都可以工作。
- bogatron

1
清楚起见,我们正在讨论:
- 一个NumPy数组,也称为`numpy.ndarray` - 数组的形状,由`numpy.ndarray.shape`指定 - 问题假设有一个未知的`numpy.ndarray`,其形状为`(R,)`,其中`R`应该被理解为其相应维度的长度
NumPy数组有一个形状。该`.shape`由元组表示,在元组中的每个元素告诉我们该维度的长度。为了保持简单,让我们专注于行和列。虽然在以下示例中我们的`numpy.ndarray`的值不会改变,但形状会发生变化。
让我们考虑一个具有值1、2、3和4的数组。
我们的示例将包括以下`.shape`表示形式:
(4,)  # 1-dimensional array with length 4
(1,4) # 2-dimensional array with row length 1, column length 4
(4,1) # 2-dimensional array with row length 4, column length 1

我们可以用变量ab来更抽象地思考这个问题。
(a,)  # 1-dimensional array with length a
(b,a) # 2-dimensional array with row length b, column length a
(a,b) # 2-dimensional array with row length a, column length b

对我来说,手动构建它们可以更好地理解它们的维度意义。

>> # (4,)
>> one_dimensional_vector = np.array(
    [1, 2, 3, 4]
)

>> # (1,4)
>> row_vector = np.array(
    [
        [1, 2, 3, 4]
    ]
)

>> # (4,1)
>> column_vector = np.array(
    [
        [1], 
        [2], 
        [3], 
        [4]
    ]
)

所以,第一个问题的答案是:

  1. 形状为(R,1)和(R,)之间有什么区别?

答案:它们具有不同的维度。 a 是一个维度的长度,b 是另一个维度的长度,.shape 分别为 (a, b)(a,)。其中,b 恰好为 1。一种思考方式是,如果 a = 1,则该行的长度为 1,因此它是一个行向量。如果 b = 1,则该列的长度为 1,因此它表示的 numpy.ndarray 是一个列向量。

  1. 上述示例是否有更好的方法?

答案:假设我们有上面用作示例的数组,其值为 1、2、3 和 4。将 (R,) 方便地变成 (R, 1) 的方法如下:

>> one_dimensional_array = np.array([1,2,3,4])
>> one_dimensional_array.shape
(4,)
>> row_vector = one_dimensional_array[:, None]
>> row_vector.shape
(4, 1)

资源

  1. NumPy - ndarrays - https://numpy.org/doc/stable/reference/arrays.ndarray.html
  2. Cross Validated @unutbu - dimension trick - https://stats.stackexchange.com/a/285005

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接