具有Numpy和性能的矩阵的选定行的平均值

ovfsdjhp  于 2022-12-04  发布在  其他
关注(0)|答案(3)|浏览(86)

我需要计算一个二维的平均值。这里我保留了所有的行:

import numpy as np, time
x = np.random.random((100000, 500))

t0 = time.time()
y = x.mean(axis=0)       # y.shape is (500,) as expected
print(time.time() - t0)  # 36 milliseconds

当我筛选并选择一些行时,我注意到速度慢了8倍。所以我尝试了一个简单的测试,其中selected_rows实际上是 * 所有行 *。但仍然慢了8倍:

selected_rows = np.arange(100000)
t0 = time.time()
y = x[selected_rows, :].mean(axis=0)        # selecting all rows!
print(time.time() - t0) # 280 milliseconds! (for the same result as above!)

是否有办法加快选择某些行(selected_rows)和计算.mean(axis=0)的过程?

selected_rows =所有行的特定情况下,* 不 * 将执行速度降低8倍会很有趣。

ohtdti5x

ohtdti5x1#

当你执行x[selected_rows, :]时,其中selected_rows是一个数组,它会执行高级索引来创建一个 new 数组。
相反,如果您执行了一个 slice 操作,则会创建原始数组的一个 view,这样花费的时间更少。例如:

import timeit
import numpy as np

selected_rows = np.arange(0, 100000, 2)
array = np.random.random((100000, 500))

t1 = timeit.timeit("array[selected_rows, :].mean(axis=0)", globals=globals(), number=10)
t2 = timeit.timeit("array[::2, :].mean(axis=0)", globals=globals(), number=10)

print(t1, t2, t1 / t2) # 1.3985465039731935 0.18735826201736927 7.464557414839488

不幸的是,没有一种好的方法可以将所有可能的selected_rows表示为切片,因此,如果您有一个不能表示为切片的selected_rows,您没有任何其他选择,只能承受性能上的损失。

这里的dankal444's answer对您的情况没有帮助,因为mean调用的轴是您首先要过滤的轴。然而,如果过滤轴和mean轴不同,这是最好的方法--将新数组的创建保存到压缩一个轴之后。与基本切片相比,您仍然会受到性能影响。但是它没有在mean调用之前进行索引时那么大。
例如,如果您需要.mean(axis=1)

t1 = timeit.timeit("array[selected_rows, :].mean(axis=1)", globals=globals(), number=10)
t2 = timeit.timeit("array.mean(axis=1)[selected_rows]", globals=globals(), number=10)
t3 = timeit.timeit("array[::2, :].mean(axis=1)", globals=globals(), number=10)
t4 = timeit.timeit("array.mean(axis=1)[::2]", globals=globals(), number=10)

print(t1, t2, t3, t4)
# 1.4732236850004483 0.3643951010008095 0.21357544500006043 0.32832237200000236

由此可见

  • mean之前的索引是迄今为止最差的(t1
  • 最好在mean之前切片,因为您不必花费额外的时间计算不必要行的平均值(t3
  • mean之后的索引(t2)和切片(t4)都优于在mean之前的索引,但不优于在mean之前的切片
k7fdbhmy

k7fdbhmy2#

AFAIK,这是不可能的,只有在Numpy有效地做到这一点。第二个代码是缓慢的,因为**x[selected_rows, :]创建一个新的数组**(如PranavHosangadi所解释的,在一般情况下无法创建视图)。这意味着需要分配新的缓冲区,数据填充(由于写入分配高速缓存策略,导致在x86-64平台上读取它,并导致与系统分配器有关的慢速页面错误)从x中读取,这些操作也必须从内存中读取。所有这些操作都非常昂贵,更不用说mean函数然后从内存中读回新创建的缓冲区。Numpy还在C代码内部以次优方式执行这种奇特的索引操作(由于高级迭代器的开销、最后一个轴的小尺寸以及Numpy代码中需要优化的可能情况的数量)。(它们是为这样的使用情况设计的),但是所得到的性能将是相当令人失望的。

Numba和Cython可以帮助加快速度。@dankal444使用Numba提供了一个非常快的串行实现。下面是一个更快的基于块的并行实现(也有点复杂):

import numba as nb

# Use '(float64[:,:], int64[:])' if indices is a 64-bit input.
# Use a list with both signature to be able to use both types at the expense of a slower compilation time.
@nb.jit('(float64[:,:], int32[:])', fastmath=True, parallel=True)
def indexedParallelMean(arr, indices):
    splitCount = 4
    l, m, n = arr.shape[0], arr.shape[1], indices.size
    res = np.zeros((splitCount, m))

    # Parallel reduction of each chunk
    for i in nb.prange(splitCount):
        start = n * i // splitCount
        end = n * (i + 1) // splitCount
        for j in range(start, end):
            res[i, :] += arr[indices[j], :]

    # Final sequential reduction
    for i in range(1, splitCount):
        res[0] += res[i]

    return res[0] / n

以下是在我的计算机上使用i5- 9600 KF处理器(6核)的性能结果:

Numpy initial version:         100 ms
Numba serial (of dankal444):    22 ms
Numba parallel (this answer):   11 ms

计算受内存限制:它使RAM带宽的吞吐量的大约80%饱和,这对于高级Numba/Cython代码几乎是最佳的。

zrfyljdw

zrfyljdw3#

(很抱歉此答案为第一个版本)
问题在于创建新数组,与计算平均值相比,这需要花费大量时间。
我尝试使用numba优化整个过程:

import numba
@numba.jit('float64[:](float64[:, :], int32[:])')
def selective_mean(array, indices):
    sum = np.zeros(array.shape[1], dtype=np.float64)
    for idx in indices:
        sum += array[idx]
    return sum / array.shape[0]

t0 = time.time()
y2 = selective_mean(x, selected_rows)
print(time.time() - t0)

与numpy相比,它的运行速度几乎没有变慢,但要慢得多(慢20%?)。编译后(第一次调用此函数),我得到了大致相同的时间。对于索引较少的数组,您应该会看到一些增益。

相关问题