理解对4D ndarray上的高级多维索引行为

huangapple go评论73阅读模式
英文:

Understanding the behaviour of advanced multi-dimensional indexing on a 4D ndarray

问题

Scenario

我有一个由多个三维图像/体素组成的四维ndarray,其维度为(体素,dim1,dim2,dim3),假设为(12个体素,96个像素,96个像素,96个像素)。我的目标是从m个体素的体积中采样n个切片的范围

我已经查阅了Numpy关于(高级)索引的文档,以及解释广播的这个答案,以及解释Numpy插入newaxis这个答案,但我仍然无法理解我的情况下的底层行为。

Problem

最初,我尝试通过一次性索引数组来实现上述目标,使用以下代码:

import numpy as np

array = np.random.rand(12, 96, 96, 96)

n = 4
m_voxels = 6
samples_range = np.arange(0, m_voxels)

middle_slices = array.shape[1] // 2
middle_slices_range = np.arange(middle_slices - n // 2, middle_slices + n // 2)

samples_from_the_middle = array[samples_range, middle_slices_range, :, :]

但我遇到了以下的IndexError,而不是获得形状为(6, 4, 96, 96)的数组:

IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (6,) (4,)

当我尝试显式或分两步索引数组时,它按预期工作:

explicit_indexing = array[0:6, 46:50, :, :]
temp = array[samples_range]
samples_from_the_middle = temp[:, middle_slices_range, :, :]
explicit_indexing.shape # 输出:(6, 4, 96, 96)
samples_from_the_middle.shape  # 输出:(6, 4, 96, 96)

另外,如在这个答案中提到的,另一种方法是:

samples_from_the_middle = array[samples_range[:, np.newaxis], middle_slices_range, :, :]  
samples_from_the_middle.shape # 输出:(6, 4, 96, 96)

我有以下问题:

  1. 为什么np.arange的方法无法产生预期的结果,而带有冒号的显式索引(with a colon)却能正常工作,尽管实际上我们是用相同的整数范围进行索引?
  2. 为什么在第一个索引的1D数组中添加newaxis似乎解决了问题?

非常感谢任何见解。

英文:

Scenario

I have a 4D ndarray consisting of multiple 3D images/voxels with dimensions (voxels, dim1, dim2, dim3), let's say (12 voxels, 96 pixels, 96 pixels, 96 pixels). My goal is to sample a range of n slices from the middle of the volume of m voxels.

I have reviewed the Numpy documentation on (advanced) indexing, as well as this answer that explains broadcasting, and this answer that explains the insertion of a newaxis by numpy, but I am still unable to understand the underlying behaviour in my scenario.

Problem

Initially, I attempted to achieve the above by indexing the array in one go using the following code:

import numpy as np

array = np.random.rand(12, 96, 96, 96)

n = 4
m_voxels = 6
samples_range = np.arange(0, m_voxels)

middle_slices = array.shape[1] // 2
middle_slices_range = np.arange(middle_slices - n // 2, middle_slices + n // 2)

samples_from_the_middle = array[samples_range, middle_slices_range, :, :]

Instead of obtaining an array of shape (6, 4, 96, 96), I encountered the following IndexError:

IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (6,) (4,)

when I attempted to index the array explicitly or in two steps, it worked as expected:

explicit_indexing = array[0:6, 46:50, :, :]
temp = array[samples_range]
samples_from_the_middle = temp[:, middle_slices_range, :, :]
explicit_indexing.shape # output: (6, 4, 96, 96)
samples_from_the_middle.shape  # output: (6, 4, 96, 96)

Alternatively, as mentioned in this answer, another approach would be:

samples_from_the_middle = array[samples_range[:, np.newaxis], middle_slices_range, :, :]  
samples_from_the_middle.shape # output: (6, 4, 96, 96)

I have the following questions:

  1. Why does the np.arange approach fail to produce the expected result while the explicit indexing (with a colon) works correctly, even though we're practically indexing with the same range of integers?
  2. Why does the addition of a newaxis to the first indexing 1D-array seem to resolve the issue?

Any insights would be greatly appreciated.

答案1

得分: 3

numpy根据您使用的方式处理索引,是否使用slices或者numpy数组而异。当您执行my_array[a:b]时,生成的是切片,而这些切片会有不同的处理方式。一个有用的思考方式是将其看作笛卡尔积。看一下这个演示:

In [1]: import numpy as np

In [2]: x = np.array([[1,2,3],[4,5,6],[7,8,9]])

In [3]: x
Out[3]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [4]: x[0:3, 0:3]
Out[4]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [5]: x[np.arange(3), np.arange(3)]
Out[5]: array([1, 5, 9])

请注意,当我们使用切片时,会得到所需的输出。但是,当我们使用numpy数组时,得到的是一个只有3个元素而不是9个的一维数组。为什么?这是因为切片会自动用于创建笛卡尔积。Python会自动生成形如[0, 0],[0, 1],[0, 2],[1, 0],...的所有可能的值对的索引。

而在使用numpy数组进行索引时,情况不同。相反,数组会逐个元素进行匹配。这意味着只有配对[0, 0],[1, 1],[2, 2]会被创建,我们只得到了3个对角线元素。这与numpy不将一维数组视为合适的行或列向量有关,除非我们显式说明数组有多少行和列。当我们这样做时,我们启用了numpy的广播,在本质上,数组在其长度为1的轴上“重复”,这使我们能够执行如下操作:

In [10]: x = np.array([1,2,3,4,5])

In [11]: y = np.array([6,7,8])

In [12]: from numpy import newaxis as nax

In [13]: x = x[:, nax]

In [14]: y = y[nax, :]

In [15]: x + y
Out[15]:
array([[ 7,  8,  9],
       [ 8,  9, 10],
       [ 9, 10, 11],
       [10, 11, 12],
       [11, 12, 13]])

在这里,您可以看到,当索引时,我们获得了与您要求的行为完全相同的结果!x数组的每个元素都与y数组的每个元素配对。

现在,我们可以按如下方式使用这些知识:

In [16]: x = np.array([[1,2,3],[4,5,6],[7,8,9]])

In [17]: x
Out[17]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [18]: x[0:3, 0:3]
Out[18]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [19]: x[np.arange(3), np.arange(3)]
Out[19]: array([1, 5, 9])

In [20]: x[np.arange(3)[:, nax], np.arange(3)[nax, :]]
Out[20]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

然后,我们完成了!

只是为了完整起见,需要注意numpy.ix_函数恰好是为您处理此问题而存在的。这里是一个示例:

In [21]: x = np.array([1,2,3,4,5])

In [22]: y = np.array([6,7,8])

In [23]: x, y = np.ix_(x,y)

In [24]: x
Out[24]:
array([[1],
       [2],
       [3],
       [4],
       [5]])

In [25]: y
Out[25]: array([[6, 7, 8]])

最后,所有这些都等同于使用numpy.meshgrid函数,该函数显式创建了与xy的每个可能的配对元素。但是,对于索引,您不应该使用这个函数,因为明确地在内存中同时创建这些配对元素非常浪费内存。最好让numpy为您执行其魔法。

英文:

So, numpy handles indexing differently depending on whether you use slices, which are what is created when you do my_array[a:b], or numpy arrays instead. A helpful way to think about it is as cartesian products. Look at this demo:

In [1]: import numpy as np

In [2]: x = np.array([[1,2,3],[4,5,6],[7,8,9]])

In [3]: x
Out[3]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [4]: x[0:3, 0:3]
Out[4]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [5]: x[np.arange(3), np.arange(3)]
Out[5]: array([1, 5, 9])

Notice that when we use slices, we get the output you wanted. When we use numpy arrays, we instead get a 1D array with only 3 elements instead of 9. Why? This is because slices are automatically used to create cartesian products. Python is automatically generating indices of the form [0, 0], [0, 1], [0, 2], [1, 0], ... for all possible pairs of values from the two slices.

When using numpy arrays for indexing, this is not the case. Instead, the arrays are matched elementwise. That means only the pairs [0, 0], [1, 1], [2, 2] are created, and we get only the 3 diagonal elements. This has to do with numpy not treating 1D arrays as proper row or column vectors unless we explicitly state how many rows and columns an array has. When we do this, we enable numpy to do broadcasting, where, in essence, arrays are "repeated" along axes where their length is 1. This lets us do things like

In [10]: x = np.array([1,2,3,4,5])

In [11]: y = np.array([6,7,8])

In [12]: from numpy import newaxis as nax

In [13]: x = x[:, nax]

In [14]: y = y[nax, :]

In [15]: x + y
Out[15]:
array([[ 7,  8,  9],
       [ 8,  9, 10],
       [ 9, 10, 11],
       [10, 11, 12],
       [11, 12, 13]])

where you can see we obtained exactly the behavior you were looking for when indexing! Each element from the x array was paired with each element from the y array.

Now we can use this knowledge as follows:

In [16]: x = np.array([[1,2,3],[4,5,6],[7,8,9]])

In [17]: x
Out[17]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [18]: x[0:3, 0:3]
Out[18]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [19]: x[np.arange(3), np.arange(3)]
Out[19]: array([1, 5, 9])

In [20]: x[np.arange(3)[:, nax], np.arange(3)[nax, :]]
Out[20]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

And we're done!

Just for completeness, note that the numpy.ix_ function exists precisely to take care of this for you. Here's an example:

In [21]: x = np.array([1,2,3,4,5])

In [22]: y = np.array([6,7,8])

In [23]: x, y = np.ix_(x,y)

In [24]: x
Out[24]:
array([[1],
       [2],
       [3],
       [4],
       [5]])

In [25]: y
Out[25]: array([[6, 7, 8]])

And finally, all of these are equivalent to using the numpy.meshgrid function, which explicitly creates arrays with every possible pairing of elements from x and y. You don't want to use this for indexing, however, since it's very wasteful regarding memory to explicitly create these pairings all at the same time and keep them in your RAM. Better let numpy work its magic for you.

huangapple
  • 本文由 发表于 2023年7月6日 18:23:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76627832.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定