Python中内存高效的大块numpy数组

时间:2015-07-11 17:40:23

标签: python performance sorting numpy memory

我需要使用numpy对非常大的基因组数据集进行排序。我有一个包含26亿个浮点数的数组,维度= (868940742, 3),一旦加载并且只是坐在那里,它在我的机器上占用大约20GB的内存。我有一台2015年初的13英寸MacBook Pro,配备16GB内存,500GB固态高清和3.1 GHz intel i7处理器。只是加载数组溢出到虚拟内存,但没有到我的机器遭受的程度,或者我必须停止我正在做的其他事情。

我从22个较小的(N, 2)子阵列逐步构建了这个非常大的数组。

函数FUN_1使用我称之为(N, 1)的22个子数组中的每一个生成2个新的sub_arr数组。

FUN_1的第一个输出是通过插入数组sub_arr[:,0]b = array([X, F(X)])的值生成的,第二个输出是通过使用数组{{1将sub_arr[:, 0]放入数据库生成的}}。我分别称这些输出r = array([X, BIN(X)])b_arr。该函数返回3个元组的rate_arr数组:

(N, 1)

我在for循环中调用该函数22次,并使用以下值填充预先分配的零import numpy as np def FUN_1(sub_arr): """interpolate b values and rates based on position in sub_arr""" b = np.load(bfile) r = np.load(rfile) b_arr = np.interp(sub_arr[:,0], b[:,0], b[:,1]) rate_arr = np.searchsorted(r[:,0], sub_arr[:,0]) # HUGE efficiency gain over np.digitize... return r[rate_r, 1], b_arr, sub_arr[:,1] 数组:

full_arr = numpy.zeros([868940742, 3])

在这一步节省内存方面,我认为这是我能做的最好的,但我愿意接受建议。无论哪种方式,我都不会遇到问题,只需要大约2分钟。

这是排序例程(有两个连续的排序)

full_arr[:,0], full_arr[:,1], full_arr[:,2] = FUN_1

现在这种情况一直在起作用,虽然很慢(大约需要10分钟)。但是,我最近开始在for idx in range(2): sort_idx = numpy.argsort(full_arr[:,idx]) full_arr = full_arr[sort_idx] # ... # <additional processing, return small (1000, 3) array of stats> 中使用更大,更精细的[X, F(X)]值的分析表来返回FUN_1并返回b_arr,现在SORT确实变慢了,尽管其他一切仍然存在相同。

有趣的是,我甚至没有在排序现在滞后的步骤中对插值进行排序。以下是不同插值文件的一些片段 - 较小的一个在每种情况下小约30%,在第二列中的值更加均匀;较慢的一个具有更高的分辨率和更多的唯一值,因此插值的结果可能更独特,但我不确定这是否应该有任何影响......?

更大,更慢的文件:

17399307    99.4
17493652    98.8
17570460    98.2
17575180    97.6
17577127    97
17578255    96.4
17580576    95.8
17583028    95.2
17583699    94.6
17584172    94

更小,更统一的常规文件:

1       24  
1001    24  
2001    24  
3001    24  
4001    24  
5001    24
6001    24
7001    24

我不确定是什么导致这个问题,我会对任何建议感兴趣,或者只是关于在这种类型的内存限制情况下进行排序的一般性输入!

2 个答案:

答案 0 :(得分:11)

目前,对np.argsort的每次调用都会生成一个(868940742, 1) int64索引数组,这将自己占用大约7 GB。此外,当您使用这些索引对full_arr的列进行排序时,您生成另一个(868940742, 1)浮点数组,因为fancy indexing always returns a copy rather than a view

一个相当明显的改进是使用.sort() methodfull_arr进行排序。遗憾的是,.sort()不允许您直接指定要排序的行或列。但是,可以指定要对结构化数组进行排序的字段。因此,您可以通过将view作为具有三个浮点字段的结构化数组添加到数组中来强制对三列中的一列进行就地排序,然后按以下字段之一进行排序:

full_arr.view('f8, f8, f8').sort(order=['f0'], axis=0)

在这种情况下,我将full_arr排序到第0个字段,该字段对应于第一列。请注意,我假设有三个float64列('f8') - 如果你的dtype不同,你应该相应地改变它。这还要求您的数组是连续的并且采用行主格式,即full_arr.flags.C_CONTIGUOUS == True

此方法应该归功于Joe Kington的回答here

虽然它需要更少的内存,但与使用np.argsort生成索引数组相比,按字段排序结构化数组要慢得多,正如您在下面的评论中所提到的(参见this previous question)。如果使用np.argsort获取要排序的索引集,则可能会使用np.take而不是直接索引来获取排序数组,从而获得适度的性能提升:

 %%timeit -n 1 -r 100 x = np.random.randn(10000, 2); idx = x[:, 0].argsort()
x[idx]
# 1 loops, best of 100: 148 µs per loop

 %%timeit -n 1 -r 100 x = np.random.randn(10000, 2); idx = x[:, 0].argsort()
np.take(x, idx, axis=0)
# 1 loops, best of 100: 42.9 µs per loop

但是我不希望看到内存使用方面有任何差异,因为这两种方法都会产生副本。

关于为什么对第二个数组进行排序更快的问题 - 是的,当数组中的唯一值较少时,您应该期望任何合理的排序算法更快,因为平均来说它的工作量较少。假设我有一个1到10之间的随机数字序列:

5  1  4  8  10  2  6  9  7  3

有10个! = 3628800这些数字的可能方式,但只有一个数字按升序排列。现在假设只有5个唯一数字:

4  4  3  2  3  1  2  5  1  5

现在有2⁵= 32种方式按升序排列这些数字,因为我可以在排序的向量中交换任何一对相同的数字,而不会破坏排序。

默认情况下,np.ndarray.sort()使用Quicksort。此算法的qsort变体通过递归选择&#39; pivot&#39;来实现。数组中的元素,然后重新排序数组,使得所有小于pivot值的元素都放在它之前,并且所有大于pivot值的元素都放在它之后。已经对已经等于枢轴的值进行了排序。具有较少的唯一值意味着平均而言,在任何给定扫描中,更多值将等于枢轴值,因此完全对阵列进行排序需要更少的扫描。

例如:

%%timeit -n 1 -r 100 x = np.random.random_integers(0, 10, 100000)
x.sort()
# 1 loops, best of 100: 2.3 ms per loop

%%timeit -n 1 -r 100 x = np.random.random_integers(0, 1000, 100000)
x.sort()
# 1 loops, best of 100: 4.62 ms per loop

在这个例子中,两个数组的dtypes是相同的。如果较小的阵列与较大的阵列相比具有较小的项目大小,则由于花式索引而复制它的成本也会较小。

答案 1 :(得分:8)

编辑:如果有任何新手编程和numpy遇到这篇文章,我想指出考虑你正在使用的np.dtype的重要性。在我的情况下,我实际上能够使用半精度浮点数,即np.float16,它将内存中的20GB对象减少到5GB,并使排序更易于管理。 numpy使用的默认值为np.float64,这是您可能不需要的很多精度。查看此处的doc,其中描述了不同数据类型的容量。感谢@ali_m在评论中指出这一点。

我做了一个糟糕的工作来解释这个问题,但我发现了一些有用的解决方法,我认为这对于需要对一个真正庞大的numpy数组进行排序的人来说是有用的。

我正在构建一个非常大的numpy数组来自22&#34;子数组&#34;包含元素[position, value]的人类基因组数据。最终,最终的数组必须按数字排序&#34;到位&#34;基于特定列中的值,而不是对行内的值进行混洗。

子数组维度遵循以下形式:

arr1.shape = (N1, 2)
...
arr22.shape = (N22, 2)

sum([N1..N2]) = 868940742即有近1BN位置需要排序。

首先,我使用函数process_sub_arrs处理22个子数组,它返回与输入长度相同的3元组1D数组。我将1D数组堆叠到一个新的(N, 3)数组中,然后将它们插入到为完整数据集初始化的np.zeros数组中:

    full_arr = np.zeros([868940742, 3])
    i, j = 0, 0

    for arr in list(arr1..arr22):  
        # indices (i, j) incremented at each loop based on sub-array size
        j += len(arr)
        full_arr[i:j, :] = np.column_stack( process_sub_arrs(arr) )
        i = j

    return full_arr

编辑:由于我意识到我的数据集可以用半精度浮点数表示,我现在初始化full_arr如下:full_arr = np.zeros([868940742, 3], dtype=np.float16),它只是大小的1/4并且更容易排序。

结果是一个巨大的20GB阵列:

full_arr.nbytes = 20854577808

正如@ali_m在他的详细帖子中指出的那样,我早期的例行程序效率低下:

sort_idx = np.argsort(full_arr[:,idx])
full_arr = full_arr[sort_idx]

数组sort_idx,其大小为full_arr的33%,在排序full_arr后会挂起并浪费内存。据推测,这种情况会产生full_arr的副本,原因是&#34;幻想&#34;索引,可能会将内存使用量提高到已经用于保存大规模阵列的233%!这是一个缓慢的步骤,持续大约十分钟,并严重依赖虚拟内存。

我不确定&#34;幻想&#34;然而,sort会生成持久性副本。看着我的机器上的内存使用情况,似乎full_arr = full_arr[sort_idx]删除了对未分类原件的引用,因为在大约1秒后剩下的就是分类数组和索引使用的内存,即使有一个瞬态复制。

argsort()用于节省内存的更紧凑的用法是:

    full_arr = full_arr[full_arr[:,idx].argsort()]

这仍然会导致分配时出现峰值,其中瞬态索引数组和瞬态副本都已生成,但内存几乎立即被释放。

@ali_m指出了一个很好的技巧(记入Joe Kington),用view生成full_arr的事实结构化数组。好处是这些可以按顺序排序&#34;保持稳定的行顺序:

full_arr.view('f8, f8, f8').sort(order=['f0'], axis=0)

视图对于执行数学数组操作非常有用,但是对于排序来说,即使是来自我的数据集的单个子数组也是如此。一般来说,即使结构化数组具有非常有用的属性,它们似乎也不会很好地扩展。如果有人知道为什么会这样,我会有兴趣知道。

使用非常大的数组最小化内存消耗和提高性能的一个好方法是构建一个小而简单的函数管道。函数一旦完成就会清除局部变量,所以如果中间数据结构正在构建并且使内存损坏,这可能是一个很好的解决方案。

这是我用来加速大规模排序的管道草图:

def process_sub_arrs(arr):
    """process a sub-array and return a 3-tuple of 1D values arrays"""

    return values1, values2, values3

def build_arr():
    """build the initial array by joining processed sub-arrays"""

    full_arr = np.zeros([868940742, 3])
    i, j = 0, 0

    for arr in list(arr1..arr22):  
        # indices (i, j) incremented at each loop based on sub-array size
        j += len(arr)
        full_arr[i:j, :] = np.column_stack( process_sub_arrs(arr) )
        i = j

    return full_arr

def sort_arr():
    """return full_arr and sort_idx"""

    full_arr = build_arr()
    sort_idx = np.argsort(full_arr[:, index])

    return full_arr[sort_idx]

def get_sorted_arr():
    """call through nested functions to return the sorted array"""

    sorted_arr = sort_arr()
    <process sorted_arr>

    return statistics

call stack:get_sorted_arr - &gt; sort_arr - &gt; build_arr - &gt; process_sub_arrs

每个内部函数完成后get_sorted_arr()最后只保存已排序的数组,然后返回一小组统计信息。

编辑:这里也值得指出,即使您能够使用更紧凑的dtype来表示您的巨大阵列,您也需要使用更高的精度进行汇总计算。例如,从full_arr.dtype = np.float16开始,命令np.mean(full_arr[:,idx])尝试计算半精度浮点的平均值,但是当对一个大型数组进行求和时,这会快速溢出。使用np.mean(full_arr[:,idx], dtype=np.float64)可以防止溢出。

我最初发布这个问题是因为我感到困惑的是,相同大小的数据集突然开始窒息我的系统内存,尽管新的&#34;慢&#中的唯一值的比例有很大差异。 34;组。 @ali_m指出,实际上,具有较少唯一值的更统一的数据更容易排序:

  

Quicksort的qsort变体通过递归选择a来工作   &#39;枢轴&#39;数组中的元素,然后重新排序数组,使所有   小于枢轴值的元素放在它之前,以及所有元素   大于枢轴值的元素放在它之后。   与枢轴相等的值已经排序,因此直观,   数组中的唯一值越少,数字越小   有必要进行互换。

在那个注意事项中,我最终尝试解决此问题的最后一个更改是提前舍入新数据集,因为插值步骤中存在不必要的高级小数精度。这最终比其他节省内存的步骤具有更大的影响,表明排序算法本身就是这种情况下的限制因素。

期待有关此主题的任何人可能提出的其他意见或建议,我几乎肯定会对某些技术问题提出异议,所以我很乐意回复: - )