如何高效地切片memmap?

9

目前我正在处理一个非常巨大的数据集,它几乎无法适应我的内存,所以我使用了np.memmap。但是在某些时候,我需要将数据集分为训练和测试两部分。 我发现当我想要使用一些索引数组切片np.memmap时会出现以下情况: (下面是代码和内存分配)

Line #    Mem usage    Increment   Line Contents
================================================
 7    29.340 MB     0.000 MB   def my_func2():
 8    29.340 MB     0.000 MB       ARR_SIZE = (1221508/4,430)
 9    29.379 MB     0.039 MB       big_mmap = np.memmap('big_mem_test.mmap',shape=ARR_SIZE, dtype=np.float64, mode='r')    
10    38.836 MB     9.457 MB       idx = range(ARR_SIZE[0])
11  2042.605 MB  2003.770 MB       sub = big_mmap[idx,:]
12  3046.766 MB  1004.160 MB       sub2 = big_mmap[idx,:]
13  3046.766 MB     0.000 MB       return  type(sub)

但是,如果我想要连续切片,我更倾向于使用以下代码:

Line #    Mem usage    Increment   Line Contents
================================================
15    29.336 MB     0.000 MB   def my_func3():
16    29.336 MB     0.000 MB       ARR_SIZE = (1221508/4,430)
17    29.375 MB     0.039 MB       big_mmap = np.memmap('big_mem_test.mmap',shape=ARR_SIZE, dtype=np.float64, mode='r')    
18    29.457 MB     0.082 MB       sub = big_mmap[0:1221508/4,:]
19    29.457 MB     0.000 MB       sub2 = big_mmap[0:1221508/4,:]  

请注意,第二个例子中的18、19行没有内存分配和整个操作速度更快。
在第一个例子中,第11行有一个分配,因此整个big_mmap矩阵在切片过程中被读取。但更令人惊讶的是,第12行又有一个分配。如果执行更多类似的操作,很容易耗尽内存。
当我拆分我的数据集时,索引通常是随机的,而不是连续的,因此我无法使用big_mmap[start:end,:]符号。
我的问题是:
是否有其他方法可以允许我在不读取整个数据到内存的情况下对memmap进行切片?
为什么在用索引切片时整个矩阵都读入内存(例如一)?
为什么要重新读取和分配数据(第一个例子的第12行)?

1
我建议使用h5py库通过hdf5文件而不是memmap。它们更加灵活(和强大),并且非常容易使用。 - Bitwise
1
是的,我正在考虑这个问题,实际上我正在考虑使用底层的hdf5来处理。但我仍然对这个memmap行为很好奇。 - Michal
我知道这是走捷径的方法,但你考虑过增加内存条的购买吗?(这只是避免问题而非解决问题,但随着数据的增长,以后你会再次遇到相同的问题) - usethedeathstar
@usethedeathstar 实际上恰恰相反。如果我购买更多的内存,我将再次面临更大的数据集问题。但是,如果我使用一种磁盘分页的方式来解决它,我将能够处理数据,购买内存只会加速整个过程。 - Michal
1个回答

6
你在第一个例子中看到的双重分配并不是由于memmap的行为,而是由于numpy的ndarray类实现__getitem__的方式。当使用列表(如在第一个示例中)对ndarray进行索引时,数据从源数组复制。当使用切片对象进行索引时,创建一个视图到源数组(没有数据被复制)。例如:
In [2]: x = np.arange(16).reshape((4,4))

In [3]: x
Out[3]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])

In [4]: y = x[[0, 2], :]

In [5]: y[:, :] = 100

In [6]: x
Out[6]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])

y 是从 x 复制的数据,因此更改 y 不会影响 x。现在通过切片索引数组:

In [7]: z = x[::2, :]

In [8]: z[:, :] = 100

In [9]: x
Out[9]: 
array([[100, 100, 100, 100],
       [  4,   5,   6,   7],
       [100, 100, 100, 100],
       [ 12,  13,  14,  15]])

关于你的第一个问题,我不知道有什么方法可以让你创建包含整个数组而不读取整个数组到内存中的任意切片。除了像HDF5/PyTables这样的东西,你可能需要考虑以下两个选项:
1. 如果你是按顺序访问训练和测试集元素(而不是将它们作为两个完整的数组进行操作),那么你可以轻松地编写一个小包装类,其__getitem__方法使用你的索引数组从memmap中提取适当的样本(即training[i]返回big_mmap[training_ids[i]])。
2. 将数组拆分成两个单独的文件,其中一个文件仅包含训练值,另一个文件仅包含测试值。然后你可以使用两个单独的memmap对象。

实际上我没有提到,但我使用的是第二种解决方案 - 将测试集和训练集分成两个“memmaps”。但实际上我进行了多次拆分,因此在开始实际处理之前需要一些时间来拆分数据集。 - Michal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接