加快对 numpy 中数组的分析

Speeding up analysis on arrays in numpy

我有一个 python 代码,它导入包含数字的 4 列 txt 文件 前三列是 x、y、z 坐标,第四列是该坐标处的密度。

下面是读取的代码,转换为 ndarray,对该字段进行傅里叶变换,计算距原点的距离 (k=(0,0,0)) 和变换后的坐标,取平均值,然后绘制它们。 多亏了 pandas(python 数据分析库)和 python FFT,加载 256^3 行并对它们进行傅立叶变换非常快,只需几秒钟即可完成。

然而,将加载的txt转换为numpy ndarray,计算平均密度(每个坐标的平均值),计算与原点的距离(k=(0,0,0))需要很长时间。

我认为问题出在最后的 np.around 部分,但我想不出优化它的方法。

我有32核机器的资源

有人可以教我如何加快速度,使其成为多进程代码,或类似的东西以便可以非常快速地完成这些吗?谢谢

(如果你是宇宙学家并且需要这个代码,你可以使用它,但如果可以的话请联系我。谢谢)

from __future__ import division
import numpy as np

ngridx = 128
ngridy = 128    
ngridz = 128

maxK = max(ngridx,ngridy,ngridz)

#making input file
f = np.zeros((ngridx*ngridy*ngridz,4))

i = 0
for i in np.arange(len(f)):
    f[i][0] = int(i/(ngridy*ngridz))
    f[i][1] = int((i/ngridz))%ngridy
    f[i][2] = int(i%ngridz)
    f[i][3] = np.random.rand(1)
    if i%1000000 ==0:
        print i
#This takes forever
#end making input file

#Thanks to Mike,
a = f[:,3].reshape(ngridx,ngridy,ngridz)

avg =np.sum(f[:,3])/len(f)
a /= avg
p = np.fft.fftn(a)
#This part is much much faster than before (Original Post).

#Keeping track of corresponding wavenumbers (k_x, k_y,k_z) for each element in p
#This is just a convension on fourier transformation so you can ignore this part
kValx = np.fft.fftfreq( ngridx , (1.0 / ngridx ) )
kValy = np.fft.fftfreq( ngridy , (1.0 / ngridy ) )
kValz = np.fft.fftfreq( ngridz , (1.0 / ngridz ) )
kx = np.zeros((ngridx,ngridy,ngridz))
ky = np.zeros((ngridx,ngridy,ngridz))
kz = np.zeros((ngridx,ngridy,ngridz))
rangecolx = np.arange(ngridx)
rangecoly = np.arange(ngridy)
rangecolz = np.arange(ngridz)
for row in np.arange(ngridx):
    for column in np.arange(ngridy):
        for height in np.arange(ngridz):
            kx[row][column][height] = (kValx[row])
            ky[row][column][height] = (kValy[column])
            kz[row][column][height] = (kValz[height])
    if row%10 == 0:
        print row
print 'wavenumber generate complete!'

#Calculating the average powerspectrum in terms of fixed K (Distance from origin to a point in fourier space)
#by taking the spherical shell of thickness 1 and averaging out the values inside it.
#I am sure that this process can be optimised somehow, but I gave up.

qlen = maxK/2 #Nyquist frequency
q = np.zeros(((qlen),4),dtype=complex)
#q is a four column array with length maxK/2.
#q[:,0] is integer wavenumber (K, which is the distance from the origin = sqrt(kx^2+ky^2+kz^2))
#q[:,1] is the sum of square of the fourier transformed value 
#q[:,2] is the sum of the fourier transformed value, 
#and q[:,3] is the total number of samples with K=q[:,0]

for i in  np.arange(len(q)):
    q[i][0] = i
i = 0
for i in np.arange(len(p)):
    for r in np.arange(len(p[0])):
        for s in np.arange(len(p[0,0])):
            K = np.around(np.sqrt(kx[i,r,s]**2+ky[i,r,s]**2+kz[i,r,s]**2))
            if K < qlen:
                q[K][1]=q[K][1]+np.abs(p[i,r,s])**2
                q[K][2]=q[K][2]+p[i,r,s]
                q[K][3]=q[K][3]+1   
    if i%10 ==0:
        print 'i = ',i,' !'
print q

Numpy 通常可以比普通 python 快数百倍的速度,只需很少的额外努力(有时甚至会自动使用多个内核,而无需您的工作)。您只需要知道编写代码的正确方法。仅举出我想到的第一件事:

  • 考虑一下您的方法 — 简化它并确保它在编码之前是正确的
  • 学习使用 numpy 的索引
  • 尽可能不要复制数据
  • 不惜一切代价尽量避免循环
  • 使用 numpy 的内置函数
  • 处理整个数组(或大块数组),而不是一次处理单个元素
  • 分析您的代码以找出慢点
  • 使用numba,尤其是对于您无法避免的循环或 numpy 无法帮助解决的循环

你的方法

我认为我们都会犯的一个错误是试图在代码中只写,即使我们仍在试图弄清楚代码应该做什么——基本上,说一种我们不太了解的语言,它不是真正的描述性语言,而且单词数量非常有限。我发现最好首先用简单的英语将我的方法概括为一系列步骤。这让我清楚地看到,用我自己的话来说,正是我想要代码做什么。因此,很容易获得概览并思考正在发生的事情,但如果我意识到某些东西需要返工,也很容易重写。

事实上,我将这些步骤作为注释写在代码文件的不同行上。一旦我对这种方法感到满意,我就开始在这些评论之间编写代码本身。这给我留下了结构良好且注释正确的代码。

在 OP 中原始代码的特定示例中,整个 f 数组可能不需要存在。如果您只是用简单的英语编写程序,我想您会看到这一点,并且能够在编写代码之前轻松地 in plain English 更改您的方法。就目前而言,如此多的代码依赖于 f,需要完全重写才能更改它。

索引

Plain python 通常在计算机应该擅长的事情上非常慢。一个例子是索引,所以一行

a[f[i,0]][f[i,1]][f[i,2]]=f[i,3]

让我很怀疑。当您说 "transforming the loaded txt to numpy ndarray" 需要很长时间时,您指的是这个吗?这不会让我感到惊讶,因为每次 python 看到 a[f[i,0]],它必须首先索引 f,确保 i 是一个整数,而你没有运行掉边f;然后它必须确保 f[i,0] 是一个整数,并且你没有 运行 超出 a 的边缘。然后它必须再重复两次才能知道您要设置哪个元素。

一个改进是使用 a[f[i,0],f[i,1],f[i,2]],因为 numpy 使用这种索引速度更快。

但我假设您的数据实际上是按某种顺序排列的。例如,f[i,2] 是否从 0 循环到 256,然后 f[i,1] 递增 1,然后 f[i,2] 从 0 重新开始?如果是这样,您实际上需要做的就是说

a = f[:,3].reshape(ngridx,ngridy,ngridz)

这是一个非常快的操作,只需几分之一毫秒。形状可能是错误的,所以你可能不得不改变参数的顺序,用转置做一些事情,但基本的想法肯定是存在的。您可以在 the documentation.

中阅读相关信息

更具体地说,如果我正确理解代码,前三部分代码可以完全替换为

a = np.random.rand(ngridx,ngridy,ngridz)

也就是说,据我所知,f根本不需要存在。

复制数据错误

你不需要复制所有东西,当你确实需要复制一个数组(或数组的一部分)时,你应该让 numpy 为你做。例如,不用 Firstdel 函数,只需使用 a[1:]:

def Firstdel(a):
    return a[1:]

这根本不会复制数据;它 returns 只是忽略了另一个数字存储在它之前的 RAM 中的事实。或者,如果你真的需要复制数据(你不只是为了绘图)使用这个:

def Firstdel(a):
    return numpy.copy(a[1:])

但通常情况下,您可以只使用 "slices" 个 numpy 数组,而不是复制它们。了解它 here

但是,还要注意,如果您只获取一个切片(并且没有复制发生),这意味着如果您写入切片,原始数组也会被写入。

循环

循环也是出了名的浪费时间——特别是当这些循环在 python 代码中,而不是某些 C 代码中时。事实上,我认为 numpy 的全部意义在于用他们用 C 语言为你编写的循环替换你将在 python 中编写的循环。

首先,对于简单循环,while在python中并不常见。所以而不是

while i < len(f):
    # do stuff
    i = i+1

你可能应该使用

for i in range(len(f)):
    # do stuff

但这只是风格和可读性的问题。为了提高速度,您需要尽可能多地摆脱循环——尤其是嵌套循环。要设置 kxkykz,此代码比 OP 中参数的嵌套循环快大约 10 倍,但缩放为 N 而不是 N^3(其中N=ngridx*ngridy*ngridz):

for row in range(ngridx):
    kx[row,:,:] = kValx[row]
for column in range(ngridy):
    ky[:,column,:] = kValy[column]
for height in range(ngridz):
    kz[:,:,height] = kValz[height]

切片对于设置值也很有用,因为循环在 numpy 内部。而不是这个代码

i = 0
while i < len(q):
    q[i][0] = i
    i = i + 1

只需使用

q[:,0] = np.arange(len(q))

在这里,我只是将 q 的 "slice" 设置为另一个数组。

该循环之后的嵌套循环也可以加速,但它们会稍微复杂一些。

但您还希望尽可能避免循环。这将我们带到...

使用内置的 numpy 函数

同样,numpy 存在的主要原因是将这些慢速 python 循环转换为快速 C 代码(我们不需要知道它的存在)。所以有很多函数可以做你想做的事情已经内置到 numpy 中。

而不是

while i < len(f):
    masstot = masstot + f[i,3]
    i = i+1

你应该使用像

这样的东西
masstot = np.sum(f[:,3])

阅读起来更简单,但也可能 方式 更快,因为 numpy 可以直接访问计算机内存中的数据,并且可以使用快速 C求和的函数,而不是使用缓慢的 python 函数。 (同样,您无需了解 C 函数;它们会完成工作。)

处理整个数组(或切片)

一个常见的错误是使用 numpy 函数,但仍然自己执行循环。这段代码中有一个很好的例子:

for i in np.arange(len(p)):
    for r in np.arange(len(p[0])):
        for s in np.arange(len(p[0,0])):
            K = np.around(np.sqrt(kx[i,r,s]**2+ky[i,r,s]**2+kz[i,r,s]**2))

与其每次通过循环计算 K 的大嵌套循环不同,只需将 K 设为具有适当值的数组:

K = np.around(np.sqrt(kx**2+ky**2+kz**2))

K 将与 kxkykz 的大小和形状相同。然后,您可以使用 advanced indexing 来设置 q 的值。这就是最后一部分的方法:

# Again, we get rid of nested loops, to get a large improvement in speed and scaling
K = np.around(np.sqrt(kx**2+ky**2+kz**2)).astype(int)
for i in range(qlen):
    p_i = p[K==i]  # This will be just the elements of p where K==i
    q[i,0] = i
    q[i,1] = np.sum(np.abs(p_i)**2)
    q[i,2] = np.sum(p_i)
    q[i,3] = len(p_i)
print q

将这些技巧放在一起,我发现您的问题中的代码比当前代码提高了大约 70 倍。如果它仍然太慢,您可以使用分析器找到慢的部分。如果您仍然无法改进它们,可能值得尝试 numba,它会根据需要编译代码,运行 几乎与等效的 C 代码一样快。

还可以加快 输入文件 的创建速度:

size = ngridx*ngridy*ngridz
f = np.zeros((size,4))
a = np.arange(size)
f[:, 0] = np.floor_divide(a, ngridy*ngridz)
f[:, 1] = np.fmod(np.floor_divide(a, ngridz), ngridy)
f[:, 2] = np.fmod(a, ngridz)
f[:, 3] = np.random.rand(size)

要制作 kxkykz,您可以使用 broadcasting:

摆脱循环
kx += kValx[:, np.newaxis, np.newaxis]
ky += kValy[np.newaxis, :, np.newaxis]
kz += kValz[np.newaxis, np.newaxis, :]