ero*_*3pi 6 python numpy vectorization python-3.x numba
我有两组大的 2D 点,我需要计算一个距离矩阵。
我需要它在 python 中运行得很快,所以很明显我使用了 numpy。我最近了解了 numpy 广播并使用了它,而不是在 python 中循环,numpy 将在 C 中进行。
我真的认为广播就是我所需要的,直到我看到其他方法比普通广播更好,我有两种计算距离矩阵的方法,但我不明白为什么一种比另一种更好。
我在这里查找https://github.com/numpy/numpy/issues/14761并且我得到了相互矛盾的结果。
下面是两种计算距离矩阵的方法
单元格 [3, 4, 6] 和 [8, 9] 都计算距离矩阵,但 3+4 使用减法。outer 比使用 vanilla 广播的 8 快,使用 hypot 的 6 比 9 快,这很简单道路。我没有尝试在 python 循环中假设它永远不会完成。
我想知道
1. 有没有更快的方法来计算距离矩阵(可能是 scikit-learn 或 scipy)?
2.为什么hypot和subtract.outer这么快?
为了方便起见,我还附上了代码段 tp run 整个事情,并更改了种子以防止缓存恢复
### Cell 1
import numpy as np
np.random.seed(858442)
### Cell 2
%%time
obs = np.random.random((50000, 2))
interp = np.random.random((30000, 2))
CPU times: user 2.02 ms, sys: 1.4 ms, total: 3.42 ms
Wall time: 1.84 ms
### Cell 3
%%time
d0 = np.subtract.outer(obs[:,0], interp[:,0])
CPU times: user 2.46 s, sys: 1.97 s, total: 4.42 s
Wall time: 4.42 s
### Cell 4
%%time
d1 = np.subtract.outer(obs[:,1], interp[:,1])
CPU times: user 3.1 s, sys: 2.7 s, total: 5.8 s
Wall time: 8.34 s
### Cell 5
%%time
h = np.hypot(d0, d1)
CPU times: user 12.7 s, sys: 24.6 s, total: 37.3 s
Wall time: 1min 6s
### Cell 6
np.random.seed(773228)
### Cell 7
%%time
obs = np.random.random((50000, 2))
interp = np.random.random((30000, 2))
CPU times: user 1.84 ms, sys: 1.56 ms, total: 3.4 ms
Wall time: 2.03 ms
### Cell 8
%%time
d = obs[:, np.newaxis, :] - interp
d0, d1 = d[:, :, 0], d[:, :, 1]
CPU times: user 22.7 s, sys: 8.24 s, total: 30.9 s
Wall time: 33.2 s
### Cell 9
%%time
h = np.sqrt(d0**2 + d1**2)
CPU times: user 29.1 s, sys: 2min 12s, total: 2min 41s
Wall time: 6min 10s
Run Code Online (Sandbox Code Playgroud)
import sys
import time
import numba as nb
import numpy as np
np.random.seed(int(sys.argv[1]))
d0 = np.random.random((49000, 2))
d1 = np.random.random((12000, 2))
def f1(d0, d1):
print('Numba without parallel')
res = np.empty((d0.shape[0], d1.shape[0]), dtype=d0.dtype)
for i in nb.prange(d0.shape[0]):
for j in range(d1.shape[0]):
res[i, j] = np.sqrt((d0[i, 0] - d1[j, 0])**2 + (d0[i, 1] - d1[j, 1])**2)
return res
# Add eager compilation, compiles before hand
@nb.njit((nb.float64[:, :], nb.float64[:, :]), parallel=True)
def f2(d0, d1):
print('Numba with parallel')
res = np.empty((d0.shape[0], d1.shape[0]), dtype=d0.dtype)
for i in nb.prange(d0.shape[0]):
for j in range(d1.shape[0]):
res[i, j] = np.sqrt((d0[i, 0] - d1[j, 0])**2 + (d0[i, 1] - d1[j, 1])**2)
return res
def f3(d0, d1):
print('hypot + subtract.outer')
np.hypot(
np.subtract.outer(d0[:,0], d1[:,0]),
np.subtract.outer(d0[:,1], d1[:,1])
)
if __name__ == '__main__':
s1 = time.time()
eval(f'{sys.argv[2]}(d0, d1)')
print(time.time() - s1)
Run Code Online (Sandbox Code Playgroud)
(base) ~/xx@xx:~/xx$ python3 test.py 523432 f3
hypot + subtract.outer
9.79756784439087
(base) xx@xx:~/xx$ python3 test.py 213622 f2
Numba with parallel
0.3393140316009521
Run Code Online (Sandbox Code Playgroud)
首先,d0每d1一个都50000 x 30000 x 8 = 12 GB相当大。确保您有超过 100 GB 的内存,因为这是整个脚本所需要的!这是一个巨大的内存量。如果没有足够的内存,操作系统将使用存储设备(例如交换)来存储多余的数据,速度要慢得多。实际上,Cell-4 没有理由比 Cell-3 慢,而且我猜想您已经没有足够的内存来(完全)存储d1在 RAM 中,而d0似乎(大部分)适合内存中。当两者都可以放入 RAM 时,在我的机器上没有区别(也可以颠倒操作顺序来检查这一点)。这也解释了为什么进一步的操作往往会变得更慢。
话虽这么说,单元 8+9 也较慢,因为它们创建临时数组,并且需要比单元 3+4+5更多的内存通道来计算结果。事实上,该表达式np.sqrt(d0**2 + d1**2)首先d0**2在内存中计算,生成一个新的 12 GB 临时数组,然后计算d1**2生成另一个 12 GB 临时数组,然后对两个临时数组求和,生成另一个新的 12 GB 临时数组,最后计算平方-root 产生另一个 12 GB 临时阵列。这可能需要高达 48 GB 的内存,并且需要 4 次读写内存限制传递。这效率不高,并且不能有效地使用 CPU/RAM(例如 CPU 缓存)。
有一种更快的实现方式,即使用Numba 的 JIT一次性完成整个计算并并行进行。这是一个例子:
import numba as nb
@nb.njit(parallel=True)
def distanceMatrix(a, b):
res = np.empty((a.shape[0], b.shape[0]), dtype=a.dtype)
for i in nb.prange(a.shape[0]):
for j in range(b.shape[0]):
res[i, j] = np.sqrt((a[i, 0] - b[j, 0])**2 + (a[i, 1] - b[j, 1])**2)
return res
Run Code Online (Sandbox Code Playgroud)
此实现使用的内存少了 3 倍(仅 12 GB),并且比使用subtract.outer. 事实上,由于交换,Cell 3+4+5 需要几分钟,而这个需要 1.3 秒!
结论是内存访问和临时数组一样昂贵。在处理巨大缓冲区时需要避免在内存中使用多次传递,并在执行的计算不简单时(例如通过使用数组块)利用 CPU 缓存。
| 归档时间: |
|
| 查看次数: |
207 次 |
| 最近记录: |