【问题标题】:Performance improvement in numpy array transformationnumpy 数组转换的性能改进
【发布时间】:2021-02-13 16:33:12
【问题描述】:

给定三个numpy 一维数组,我想将它们转换如下:

import numpy as np

Xd = np.asarray([0, 0,   1,   1,   0.5])
Yd = np.asarray([0, 0,   0,   2.5, 2.5])
Zd = np.asarray([0, 1.5, 1.5, 1.5, 1.5])

points = np.stack([Xd, Yd, Zd], axis=1).reshape(-1, 1, 3)
segments = np.concatenate([points[:-1], points[1:]], axis = 1)    

print(segments.shape)
print(segments)

输出:

(4, 2, 3)
[[[0.  0.  0. ]
  [0.  0.  1.5]]

 [[0.  0.  1.5]
  [1.  0.  1.5]]

 [[1.  0.  1.5]
  [1.  2.5 1.5]]

 [[1.  2.5 1.5]
  [0.5 2.5 1.5]]]

有没有办法提高这种转换的性能?

背景

此转换对于将matplotlib 中的XYZ 坐标与Line3DCollection 一起使用是必要的。到目前为止,我只看到了上述代码的变体,但使用thousands of coordinates 或插值数据以获得更好的分辨率,需要优化方法。

总结

感谢@Mercury,可以得出结论,对于较短的数组(长度answer by @Miguel 的性能更好,但当数组变长时approach by @mathfux 的扩展性更好。

【问题讨论】:

    标签: python performance numpy matplotlib


    【解决方案1】:

    作为一般建议,当您想要速度时,通常应尽量避免堆栈和连接,因为这通常意味着多次复制相同的数据。

    无论如何,这就是我的做法,代码稍长,但不会做比需要更多的工作

    n = len(Xd)
    segments = np.empty((n-1, 2, 3))
    
    segments[:,0,0] = Xd[:-1]
    segments[:,1,0] = Xd[1:]
    
    segments[:,0,1] = Yd[:-1]
    segments[:,1,1] = Yd[1:]
    
    segments[:,0,2] = Zd[:-1]
    segments[:,1,2] = Zd[1:]
    

    [编辑] - 以下是为了科学/娱乐而制作的,请勿复制e

    所以我试着看看我是否可以从@mathfux 的回答中获得更多的性能,结果我得到了这个丑陋的代码:

    a = np.empty(3*n)
    a[:n]    = Xd
    a[n:n+n] = Yd
    a[n+n:]  = Zd
    
    interface = dict(a.__array_interface__)
    interface['shape'] = (n-1, 2, 3)
    interface['strides'] = (a.itemsize, a.itemsize, n*a.itemsize)
    segments= np.array(np.lib.stride_tricks.DummyArray(interface, base=a), copy=False)
    

    在我的机器上,它明显更快(根据输入的大小,最高可达 ~30%)。收益部分是由于a的构建和跳过as_strided的检查

    【讨论】:

    • 哦,那是相当大的,所以我想这种方法不是那么有趣。
    • 如果您想看一下,我在 mathfux 的答案上添加了一个变体,尽管我不建议您使用它。话虽如此,我认为他的答案中的转置结构可以用更快的东西代替
    • 我更喜欢你早期的作品。这变得不可读,至少对我来说是这样。
    • 非常抱歉,我将不得不接受另一个答案而不是您的答案。我喜欢您的初始解决方案的可读性方面,并且它对于较小的阵列表现得非常好,但问题是关于大型阵列的性能。
    • 别担心,我完全同意你的看法
    【解决方案2】:

    您似乎正在尝试在二维数组中滚动形状为 (2, 3) 的窗口。这类似于convolution of image,可以通过np.lib.stride_tricks 以非常有效的方式完成。

    a = np.transpose([Xd, Yd, Zd])
    window = (2, 3)
    view_shape = (len(a) - window[0] + 1,) + window # (4,2,3) if len(a) == 5
    sub_matrix = np.lib.stride_tricks.as_strided(a, shape = view_shape, strides = (a.itemsize,) + a.strides)
    >>> sub_matrix
    array([[[0. , 0. , 0. ],
            [0. , 0. , 1.5]],
    
           [[0. , 0. , 1.5],
            [1. , 0. , 1.5]],
    
           [[1. , 0. , 1.5],
            [1. , 2.5, 1.5]],
    
           [[1. , 2.5, 1.5],
            [0.5, 2.5, 1.5]]])
    

    请注意,np.lib.stride_tricks 对任何替代方式都非常有效。

    【讨论】:

    • 这真是令人印象深刻!
    • 确实如此。与原始方法和 Miguel 的方法相比,这种方法变得更好,数组 Xd, Yd, Zd 越长。
    • 好的,T先生。因此,我对任意长度的数组进行了小扩展,并对其进行了计时。长度为 1000 的样本需要 25µs,长度为 1M 的样本需要 15ms。
    • 有没有人提到这种性能改进令人印象深刻?
    • @Mr.T 我想每个人都应该从@Divakar那里学到很多东西。他提到,跨步技巧是他个人资料中的超级工具。
    【解决方案3】:

    这里有一些时序测试,在更大的阵列上,这使得差异更加明显。

    import numpy as np
    from timeit import timeit
    
    # original
    def f1(x, y, z):
        points = np.stack([x, y, z], axis=1).reshape(-1, 1, 3)
        return np.concatenate([points[:-1], points[1:]], axis = 1)
    
    # preallocating and then assigning
    def f2(x, y, z):
        segments = np.empty((len(x)-1, 2, 3))
    
        segments[:,0,0] = x[:-1]
        segments[:,1,0] = x[1:]
    
        segments[:,0,1] = y[:-1]
        segments[:,1,1] = y[1:]
    
        segments[:,0,2] = z[:-1]
        segments[:,1,2] = z[1:]
        return segments
    
    # stacking, but in one go
    def f3(x, y, z):
        segments = np.stack([x[:-1], y[:-1], z[:-1], x[1:], y[1:],z[1:]], axis=1)
        return segments.reshape(-1, 2, 3)
    
    # list comparison
    def f4(x, y, z):
        z_ = [i for i in zip(x,y,z)]
        return [[[z_[i]],[z_[i+1]]] for i in range(len(z_)-1)]
    
    #np.lib.stride_tricks approach
    def f5(x, y, z):
        a = np.transpose([x, y, z])
        window = (2, 3)
        view_shape = (len(a) - window[0] + 1,) + window # (4,2,3) if len(a) == 5
        return np.lib.stride_tricks.as_strided(a, shape = view_shape, strides = (a.itemsize,) + a.strides)
        
    
    ntime = 5000 #number of test runs
    nxd = 500    #array length
    
    Xd = np.random.randn(nxd)
    Yd = np.random.randn(nxd)
    Zd = np.random.randn(nxd)
    
    print(timeit(lambda: f1(Xd, Yd, Zd), number=ntime))
    #0.11369249999999999
    
    print(timeit(lambda: f2(Xd, Yd, Zd), number=ntime))
    #0.0480651
    
    print(timeit(lambda: f3(Xd, Yd, Zd), number=ntime))
    #0.10202380000000003
    
    print(timeit(lambda: f4(Xd, Yd, Zd), number=ntime))
    #1.8407391
    
    print(timeit(lambda: f5(Xd, Yd, Zd), number=ntime))
    #0.09132560000000023
        
    ntime = 50     #number of test runs
    nxd = 500000   #array length
    
    Xd = np.random.randn(nxd)
    Yd = np.random.randn(nxd)
    Zd = np.random.randn(nxd)
    
    print(timeit(lambda: f1(Xd, Yd, Zd), number=ntime))
    #1.7519548999999999
    
    print(timeit(lambda: f2(Xd, Yd, Zd), number=ntime))
    #1.504727
    
    print(timeit(lambda: f3(Xd, Yd, Zd), number=ntime))
    #1.5010566
    
    print(timeit(lambda: f4(Xd, Yd, Zd), number=ntime))
    #22.6208157
    
    print(timeit(lambda: f5(Xd, Yd, Zd), number=ntime))
    #0.46465339999999955
    

    如您所见,@Miguel 的方式是可行的方式:预先分配数组然后分配是最有效的方式。即使您以像 f3() 那样更智能的方式堆叠它们,它仍然比 f2() 慢。但是当数组长度大幅增加时,没有什么比 f5() 更好了。

    【讨论】:

    • 您介意我在您的帖子中包含np.lib.stride_tricks 方法以及不同的数组长度吗?
    • 当然,随意!
    【解决方案4】:

    我发现这比@Miguel 的代码要快。

    z = [i for i in zip(Xd,Yd,Zd)]
    segments = [[[z[i]],[z[i+1]]] for i in range(len(z)-1)]
    

    【讨论】:

    • 极不可能。乍一看,我可以看出这应该很差,实际上比 OP 的原始代码更糟糕。尝试一些 %timeit 测试,将 Xd、Yd、Zd 作为更长的一维数组,可能长度为 500。
    • @Mercury Ah.. 由于某种原因,我以错误的方式进行了测试,我认为 x、y、z 的长度会保持不变。我的错。添加了我的测试作为参考。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-16
    • 1970-01-01
    • 1970-01-01
    • 2019-07-20
    • 2016-05-27
    • 2016-11-16
    • 2021-12-29
    相关资源
    最近更新 更多