翻译自:stackoverflow 回答 By Gareth Rees
原问题
在 numpy 中,有些运算返回 shape 为 (R, 1)
而有些返回 (R,)
。由于需要显式调用 reshape
,这会让矩阵乘法变得更加繁琐。举例来说,假设有一个矩阵 M
,如果我们想执行 numpy.dot(M[:,0], numpy.ones((1, R)))
,其中 R
是行数(当然,换成列会有同样的问题)。我们会得到 matrices are not aligned
异常,因为 M[:,0]
的 shape 是 (R,)
但是 numpy.ones((1, R))
的 shape 是 (1, R)
。
所以我的问题是:
-
shape
(R, 1)
and(R,)
有什么区别。我知道从字面意思上一个是数的 list,另一个是只包含一个数的 list 组成的 list。只是好奇为什么不把numpy
设计成更倾向于 shape(R, 1)
而不是(R,)
以让矩阵乘法更简单。 -
上面的例子有更好的方法吗?而不用显式地像这样调用
reshape
:numpy.dot(M[:,0].reshape(R, 1), numpy.ones((1, R)))
。
回答
1. NumPy 中 shape 的含义
你写到,“我知道从字面意思上一个是数的 list,另一个是只包含一个数的 list 组成的 list”,但是用这种方式去想有点没啥用。
考虑 NumPy 数组的最好方式是它们包含两个部分,一个 数据缓冲区(data buffer),它只是一个原始数据块(a block of raw elements), 以及一个描述如何解释数据缓冲区的 视图(view)。
比如,如果我们创建一个包含 12 个整数的数组:
>>> a = numpy.arange(12)
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
这时 a
包含一个数据缓冲区,排列如下:
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
以及描述如何解释数据的视图:
>>> 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,)
这里的 shape (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 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
如果我们 reshape 一个数组,它不会改变数据缓冲区。相反,它创建了一个新视图,描述了解释数据的不同方式。所以当运行下面之后:
>>> b = a.reshape((3, 4))
数组 b
和 a
有相同的数据缓冲区,但是现在被 两个 索引索引,其中分别从 0 到 2 和 0 到 3。如果我们标记两个索引为 i
和 j
,数组 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
现在应该清楚了数组具有一个或多个维度(dimensions)为 1 的 shape 意味着什么。经过执行:
>>> 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 只是创建了一个新视图,那么在有必要去使用它的时候不要感到害怕。当你想要以不同的方式索引数组时,这是正确的工具。
然而,在长时间的计算中,通常可以首先安排去构造具有“正确”形状的数组,从而最大限度地减少 reshape 和 transpose 的次数。但在没有看到导致有 reshape 需求的实际背景之前,很难说应该改变什么。
你的问题中的例子是:
numpy.dot(M[:,0], numpy.ones((1, R)))
但这是不现实的。首先,这个表达式:
M[:,0].sum()
可以更简单地计算结果。其次,第 0 列真的有什么特别的吗?也许你真正需要的是:
M.sum(axis=0)