如何将一个二维数组复制到第三维度,重复N次?

203
我想将一个numpy的二维数组复制到第三维度中。例如,给定一个二维numpy数组:
import numpy as np

arr = np.array([[1, 2], [1, 2]])
# arr.shape = (2, 2)

将其转换为一个具有N个副本的新维度中的3D矩阵。对于N=3,对arr进行操作,输出应为:
new_arr[:,:,0]
# array([[1, 2], [1, 2]])

new_arr[:,:,1]
# array([[1, 2], [1, 2]])

new_arr[:,:,2]
# array([[1, 2], [1, 2]])

# new_arr.shape = (2, 2, 3)

2
仅需使用 new_arr = np.array([a]*3) 即可,其中 a = [[1,2],[1,2]] 适用于我。 - JStrahl
1
如果a的形状为(r,c),那么np.array([a]*3)会产生一个形状为(3,r,c)的数组,而我认为OP想要的是(r,c,3)。 - mins
2
@mins,从 new_arr.shape = (3,2,2) 可以看出 OP 想要的是 (3, r, c)。所以这样就可以了。 - JStrahl
1
@rayryeng 啊,真正的问题是问题文本与示例代码不匹配。在第3个版本(以及之前的版本)中,输出的arr已经具有形状(3,2,2) - undefined
1
@rayryeng 一眼看去,大多数顶级答案都是针对问题文本而不是预期的代码进行的。感谢你的发现,我会相应地进行修订。 - undefined
显示剩余6条评论
7个回答

259

最干净的方法可能是使用np.repeat函数:

a = np.array([[1, 2], [1, 2]])
print(a.shape)
# (2,  2)

# indexing with np.newaxis inserts a new 3rd dimension, which we then repeat the
# array along, (you can achieve the same effect by indexing with None, see below)
b = np.repeat(a[:, :, np.newaxis], 3, axis=2)

print(b.shape)
# (2, 2, 3)

print(b[:, :, 0])
# [[1 2]
#  [1 2]]

print(b[:, :, 1])
# [[1 2]
#  [1 2]]

print(b[:, :, 2])
# [[1 2]
#  [1 2]]

话虽如此,你通常可以通过使用广播来避免重复数组。例如,假设我想要添加一个(3,)向量:

c = np.array([1, 2, 3])

a数组的内容在第三个维度上复制3次,再将c数组的内容在前两个维度上分别复制2次,从而得到两个形状为(2, 2, 3)的数组,然后计算它们的和。但是,更简单快捷的方法是这样的:

d = a[..., None] + c[None, None, :]

这里,a[..., None] 的形状为 (2, 2, 1),而 c[None, None, :] 的形状为 (1, 1, 3)。当我计算它们的和时,结果会在大小为1的维度上进行'广播',得到的结果形状为 (2, 2, 3)

print(d.shape)
# (2,  2, 3)

print(d[..., 0])    # a + c[0]
# [[2 3]
#  [2 3]]

print(d[..., 1])    # a + c[1]
# [[3 4]
#  [3 4]]

print(d[..., 2])    # a + c[2]
# [[4 5]
#  [4 5]]

广播是一种非常强大的技术,因为它避免了在内存中创建重复副本的额外开销。
*尽管我为清晰起见包含了它们,但对于c中的None索引实际上并不必要 - 你也可以做a[..., None] + c,即将一个(2,2,1)数组广播到一个(3,)数组。这是因为如果两个数组中有一个数组的维数少于另一个数组,则只需要使两个数组的后续维度兼容即可。为了举一个更复杂的例子:
a = np.ones((6, 1, 4, 3, 1))  # 6 x 1 x 4 x 3 x 1
b = np.ones((5, 1, 3, 2))     #     5 x 1 x 3 x 2
result = a + b                # 6 x 5 x 4 x 3 x 2

为了验证这确实给出了正确的结果,您还可以打印 b[:,:,0]b[:,:,1]b[:,:,2]。每个第三维切片都是原始二维数组的副本。仅从 print(b) 看是不太明显的。 - ely
None和np.newaxis有什么区别?当我测试时,它给出了相同的结果。 - monolith
1
@wedran,它们完全相同 - np.newaxis只是None的别名。 - ali_m
你可以执行 "b = np.repeat(a[np.newaxis, :, :], 3, axis=0)" 来重复三次并添加到第一维。 - alextc

48

另一种方法是使用numpy.dstack。假设您想要将矩阵a重复num_repeats次:

import numpy as np
b = np.dstack([a]*num_repeats)

关键在于将矩阵 a 包装进一个单元素的列表,然后使用 * 运算符将该列表中的元素重复 num_repeats 次。

例如,如果:

a = np.array([[1, 2], [1, 2]])
num_repeats = 5

这将[1 2; 1 2]数组在第三维中重复5次。要验证(在IPython中):

In [110]: import numpy as np

In [111]: num_repeats = 5

In [112]: a = np.array([[1, 2], [1, 2]])

In [113]: b = np.dstack([a]*num_repeats)

In [114]: b[:,:,0]
Out[114]: 
array([[1, 2],
       [1, 2]])

In [115]: b[:,:,1]
Out[115]: 
array([[1, 2],
       [1, 2]])

In [116]: b[:,:,2]
Out[116]: 
array([[1, 2],
       [1, 2]])

In [117]: b[:,:,3]
Out[117]: 
array([[1, 2],
       [1, 2]])

In [118]: b[:,:,4]
Out[118]: 
array([[1, 2],
       [1, 2]])

In [119]: b.shape
Out[119]: (2, 2, 5)

最终我们可以看到矩阵的形状是2 x 2,第三维有5个切片。


这与 reshape 相比如何?更快吗?提供相同的结构吗?它绝对更整洁。 - Ander Biguri
@AnderBiguri 我从未进行过基准测试...我主要是为了完整性而将其放在这里。计时并观察差异会很有趣。 - rayryeng
1
我刚刚执行了img = np.dstack([arr] * 3)并且运行良好!谢谢。 - thanos.a
1
我觉得我可以为效率提出一个视图输出。由于这是一篇旧帖子,可能有人错过了它。在这个问答中添加了一个解决方案。 - Divakar

35

使用视图并获得免费运行时!将通用的n-dim数组扩展到n+1-dim

NumPy 1.10.0中引入了numpy.broadcast_to,我们可以利用它简单地生成一个2D输入数组的3D视图。好处是没有额外的内存开销,几乎免费的运行时间。这在数组很大且我们可以使用视图时是必不可少的。此外,这也适用于通用的n-dim情况。

我会使用stack代替copy,因为读者可能会将其与创建内存副本的数组复制混淆。

沿第一个轴堆叠

如果我们想要沿着第一个轴堆叠输入的arr,则使用np.broadcast_to创建3D视图的解决方案为 -

np.broadcast_to(arr,(3,)+arr.shape) # N = 3 here

沿第三/最后一个轴堆叠

要沿第三个轴堆叠输入的arr,创建3D视图的解决方案是 -

np.broadcast_to(arr[...,None],arr.shape+(3,))

如果我们确实需要进行内存复制,我们可以在此处添加 .copy()。因此,解决方案为 -
np.broadcast_to(arr,(3,)+arr.shape).copy()
np.broadcast_to(arr[...,None],arr.shape+(3,)).copy()

这是两种情况下的堆叠方式,以一个示例案例的形状信息展示 -
# Create a sample input array of shape (4,5)
In [55]: arr = np.random.rand(4,5)

# Stack along first axis
In [56]: np.broadcast_to(arr,(3,)+arr.shape).shape
Out[56]: (3, 4, 5)

# Stack along third axis
In [57]: np.broadcast_to(arr[...,None],arr.shape+(3,)).shape
Out[57]: (4, 5, 3)

相同的解决方案可用于将n-dim输入扩展到沿第一个和最后一个轴的n+1-dim视图输出。让我们探索一些更高维度的情况 -

3D输入情况:

In [58]: arr = np.random.rand(4,5,6)

# Stack along first axis
In [59]: np.broadcast_to(arr,(3,)+arr.shape).shape
Out[59]: (3, 4, 5, 6)

# Stack along last axis
In [60]: np.broadcast_to(arr[...,None],arr.shape+(3,)).shape
Out[60]: (4, 5, 6, 3)

4D输入案例:

In [61]: arr = np.random.rand(4,5,6,7)

# Stack along first axis
In [62]: np.broadcast_to(arr,(3,)+arr.shape).shape
Out[62]: (3, 4, 5, 6, 7)

# Stack along last axis
In [63]: np.broadcast_to(arr[...,None],arr.shape+(3,)).shape
Out[63]: (4, 5, 6, 7, 3)

等等。

时间

让我们使用一个大样本的2D案例,获取时序并验证输出是否为view

# Sample input array
In [19]: arr = np.random.rand(1000,1000)

让我们证明所提出的解决方案确实是一个视图。我们将沿着第一个轴进行堆叠(如果沿着第三个轴进行堆叠,结果会非常相似)-

In [22]: np.shares_memory(arr, np.broadcast_to(arr,(3,)+arr.shape))
Out[22]: True

让我们来展示一下它几乎是免费的时间 -

In [20]: %timeit np.broadcast_to(arr,(3,)+arr.shape)
100000 loops, best of 3: 3.56 µs per loop

In [21]: %timeit np.broadcast_to(arr,(3000,)+arr.shape)
100000 loops, best of 3: 3.51 µs per loop

作为一个视图,在将N从3增加到3000时,对时间没有任何影响,并且两者在时间单位上都可以忽略不计。因此,既高效又节省内存和性能!

这是一个很好的选择。然而,我认为操作创建的视图所需的时间可能很重要。例如,对于a = np.zeros((1000,1000)),简单乘法的计时如下:使用np.repeat --> %timeit np.repeat(a[:,:,None], 3, axis=-1)*2.0 得到 26.4 ms ± 456 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)。使用np.broadcast_to --> %timeit np.broadcast_to(a[:,:,None], a.shape+(3,))*2.0 得到 29.8 ms ± 394 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) - Javier TG

13

现在可以使用np.tile来实现:

import numpy as np

a = np.array([[1,2],[1,2]])
b = np.tile(a,(3, 1,1))

b.shape
(3,2,2)

b
array([[[1, 2],
        [1, 2]],

       [[1, 2],
        [1, 2]],

       [[1, 2],
        [1, 2]]])

4
A=np.array([[1,2],[3,4]])
B=np.asarray([A]*N)

编辑@Mr.F,以保留维度顺序:

B=B.T

这对我来说会产生一个 N x 2 x 2 的数组,例如 B.shape 打印出 (N, 2, 2),无论 N 的值是多少。如果你用 B.TB 进行转置,那么它将与预期的输出相匹配。 - ely
@Mr.F - 您是正确的。这将沿着第一维进行广播,因此执行B[0],B[1],...将为您提供正确的切片,我会争辩并说这比使用B[:,:,0],B[:,:,1]等更容易键入。 - rayryeng
可能打字更容易,但例如如果您正在处理图像数据,则大部分情况下都会出现错误,因为几乎所有算法都期望使用线性代数的约定来处理像素通道的2D切片。很难想象一个应用程序,您从一个2D数组开始,按照某种约定处理行和列,然后您希望将该相同的东西的多个副本延伸到新轴中,但突然间您希望第一个轴改变含义成为新轴... - ely
@Mr.F - 当然可以。我无法猜测您将来想要在哪些应用程序中使用3D矩阵。话虽如此,这完全取决于应用程序。顺便说一句,我也更喜欢B[:,:,i],因为我习惯了这种方式。 - rayryeng

2
这里有一个广播示例,完全符合要求。
a = np.array([[1, 2], [1, 2]])
a=a[:,:,None]
b=np.array([1]*5)[None,None,:]

那么b*a就是期望的结果,(b*a)[:,:,0]产生了array([[1, 2],[1, 2]]),这是原始的a(b*a)[:,:,1]也是如此。


0

总结以上解决方案:

a = np.arange(9).reshape(3,-1)
b = np.repeat(a[:, :, np.newaxis], 5, axis=2)
c = np.dstack([a]*5)
d = np.tile(a, [5,1,1])
e = np.array([a]*5)
f = np.repeat(a[np.newaxis, :, :], 5, axis=0) # np.repeat again
print('b='+ str(b.shape), b[:,:,-1].tolist())
print('c='+ str(c.shape),c[:,:,-1].tolist())
print('d='+ str(d.shape),d[-1,:,:].tolist())
print('e='+ str(e.shape),e[-1,:,:].tolist())
print('f='+ str(f.shape),f[-1,:,:].tolist())

b=(3, 3, 5) [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
c=(3, 3, 5) [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
d=(5, 3, 3) [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
e=(5, 3, 3) [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
f=(5, 3, 3) [[0, 1, 2], [3, 4, 5], [6, 7, 8]]

祝你好运


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