在磁盘上保留 numpy 数组的最佳方法

Posted

技术标签:

【中文标题】在磁盘上保留 numpy 数组的最佳方法【英文标题】:best way to preserve numpy arrays on disk 【发布时间】:2012-03-26 00:38:43 【问题描述】:

我正在寻找一种快速保存大型 numpy 数组的方法。我想以二进制格式将它们保存到磁盘,然后相对快速地将它们读回内存。不幸的是,cPickle 不够快。

我找到了numpy.savez 和numpy.load。但奇怪的是,numpy.load 将一个 npy 文件加载到“内存映射”中。这意味着对数组的常规操作真的很慢。例如,这样的事情会很慢:

#!/usr/bin/python
import numpy as np;
import time; 
from tempfile import TemporaryFile

n = 10000000;

a = np.arange(n)
b = np.arange(n) * 10
c = np.arange(n) * -0.5

file = TemporaryFile()
np.savez(file,a = a, b = b, c = c);

file.seek(0)
t = time.time()
z = np.load(file)
print "loading time = ", time.time() - t

t = time.time()
aa = z['a']
bb = z['b']
cc = z['c']
print "assigning time = ", time.time() - t;

更准确地说,第一行会非常快,但是将数组分配给obj 的其余行慢得离谱:

loading time =  0.000220775604248
assining time =  2.72940087318

有没有更好的方法来保存 numpy 数组?理想情况下,我希望能够在一个文件中存储多个数组。

【问题讨论】:

默认情况下,np.load 应该映射文件。 pytables 呢? 如果我们在您的问题中提供更多信息会很好,例如存储在 ifile 中的数组类型及其大小,或者它们是不同文件中的多个数组,或者如何你到底是不是救了他们。根据您的问题,我的印象是第一行什么都不做,实际加载发生在之后,但这些只是猜测。 @larsmans - 对于“npz”文件(即使用numpy.savez 保存的多个数组)来说,默认是“延迟加载”数组。它不是对它们进行映射,但在索引NpzFile 对象之前不会加载它们。 (因此 OP 所指的延迟。)load 的文档跳过了这一点,因此有点误导...... @JoeKington 谢谢乔。但是我如何“不延迟加载”一个 npz 文件? 【参考方案1】:

“最佳”取决于您的目标。正如其他人所说,二进制文件具有最大的可移植性,但问题是您需要了解数据的存储方式。

Darr 以基于平面二进制和文本文件的自记录方式保存您的 numpy 数组。这最大限度地提高了广泛的可读性。它还自动包含有关如何以各种数据科学语言(例如 numpy 本身,以及 R、Matlab、Julia 等)读取数组的代码。

披露:我编写了库。

【讨论】:

【参考方案2】:

现在有一个基于 HDF5 的 pickle 克隆,称为 hickle

https://github.com/telegraphic/hickle

import hickle as hkl 

data = 'name': 'test', 'data_arr': [1, 2, 3, 4]

# Dump data to file
hkl.dump(data, 'new_data_file.hkl')

# Load data from file
data2 = hkl.load('new_data_file.hkl')

print(data == data2)

编辑:

也可以通过以下方式直接“腌制”到压缩存档中:

import pickle, gzip, lzma, bz2

pickle.dump(data, gzip.open('data.pkl.gz', 'wb'))
pickle.dump(data, lzma.open('data.pkl.lzma', 'wb'))
pickle.dump(data, bz2.open('data.pkl.bz2', 'wb'))


附录

import numpy as np
import matplotlib.pyplot as plt
import pickle, os, time
import gzip, lzma, bz2, h5py

compressions = ['pickle', 'h5py', 'gzip', 'lzma', 'bz2']
modules = dict(
    pickle=pickle, h5py=h5py, gzip=gzip, lzma=lzma, bz2=bz2
)

labels = ['pickle', 'h5py', 'pickle+gzip', 'pickle+lzma', 'pickle+bz2']
size = 1000

data = 

# Random data
data['random'] = np.random.random((size, size))

# Not that random data
data['semi-random'] = np.zeros((size, size))
for i in range(size):
    for j in range(size):
        data['semi-random'][i, j] = np.sum(
            data['random'][i, :]) + np.sum(data['random'][:, j]
        )

# Not random data
data['not-random'] = np.arange(
    size * size, dtype=np.float64
).reshape((size, size))

sizes = 

for key in data:

    sizes[key] = 

    for compression in compressions:
        path = 'data.pkl.'.format(compression)

        if compression == 'pickle':
            time_start = time.time()
            pickle.dump(data[key], open(path, 'wb'))
            time_tot = time.time() - time_start
            sizes[key]['pickle'] = (
                os.path.getsize(path) * 10**-6, 
                time_tot.
            )
            os.remove(path)

        elif compression == 'h5py':
            time_start = time.time()
            with h5py.File(path, 'w') as h5f:
                h5f.create_dataset('data', data=data[key])
            time_tot = time.time() - time_start
            sizes[key][compression] = (os.path.getsize(path) * 10**-6, time_tot)
            os.remove(path)

        else:
            time_start = time.time()
            with modules[compression].open(path, 'wb') as fout:
                pickle.dump(data[key], fout)
            time_tot = time.time() - time_start
            sizes[key][labels[compressions.index(compression)]] = (
                os.path.getsize(path) * 10**-6, 
                time_tot,
            )
            os.remove(path)


f, ax_size = plt.subplots()
ax_time = ax_size.twinx()

x_ticks = labels
x = np.arange(len(x_ticks))

y_size = 
y_time = 
for key in data:
    y_size[key] = [sizes[key][x_ticks[i]][0] for i in x]
    y_time[key] = [sizes[key][x_ticks[i]][1] for i in x]

width = .2
viridis = plt.cm.viridis

p1 = ax_size.bar(x - width, y_size['random'], width, color = viridis(0))
p2 = ax_size.bar(x, y_size['semi-random'], width, color = viridis(.45))
p3 = ax_size.bar(x + width, y_size['not-random'], width, color = viridis(.9))
p4 = ax_time.bar(x - width, y_time['random'], .02, color='red')

ax_time.bar(x, y_time['semi-random'], .02, color='red')
ax_time.bar(x + width, y_time['not-random'], .02, color='red')

ax_size.legend(
    (p1, p2, p3, p4), 
    ('random', 'semi-random', 'not-random', 'saving time'),
    loc='upper center', 
    bbox_to_anchor=(.5, -.1), 
    ncol=4,
)
ax_size.set_xticks(x)
ax_size.set_xticklabels(x_ticks)

f.suptitle('Pickle Compression Comparison')
ax_size.set_ylabel('Size [MB]')
ax_time.set_ylabel('Time [s]')

f.savefig('sizes.pdf', bbox_inches='tight')

【讨论】:

有些人可能关心的一个警告是,pickle 可以执行任意代码,这使得它比其他用于保存数据的协议更不安全。 这太棒了!能否提供读取使用 lzma 或 bz2 直接压缩的文件的代码? @ErnestSKirubakaran 基本一样:如果你使用pickle.dump( obj, gzip.open( 'filename.pkl.gz', 'wb' ) )保存,你可以使用pickle.load( gzip.open( 'filename.pkl.gz', 'r' ) )加载它【参考方案3】:

查找时间很慢,因为当您使用mmap 时,调用load 方法时不会将数组的内容加载到内存中。当需要特定数据时,数据是延迟加载的。 在您的情况下,这发生在查找中。但是第二次查找不会那么慢。

这是mmap 的一个不错的功能,当您拥有一个大数组时,您不必将整个数据加载到内存中。

要解决您可以使用joblib 的问题,您可以使用joblib.dump 转储您想要的任何对象,甚至两个或更多numpy arrays,请参阅示例

firstArray = np.arange(100)
secondArray = np.arange(50)
# I will put two arrays in dictionary and save to one file
my_dict = 'first' : firstArray, 'second' : secondArray
joblib.dump(my_dict, 'file_name.dat')

【讨论】:

图书馆不再可用。【参考方案4】:

我比较了存储 numpy 数组的多种方式的性能(空间和时间)。它们中很少有人支持每个文件多个数组,但无论如何它也许很有用。

对于密集数据,Npy 和二进制文件都非常快而且很小。如果数据稀疏或非常结构化,您可能希望使用带有压缩功能的 npz,这将节省大量空间但会花费一些加载时间。

如果可移植性是一个问题,二进制比 npy 更好。如果人类可读性很重要,那么您将不得不牺牲很多性能,但使用 csv 可以很好地实现这一点(当然这也是非常便携的)。

更多详情和代码可在the github repo获取。

【讨论】:

您能解释一下为什么binary 在便携性方面比npy 更好吗?这是否也适用于npz @daniel451 因为任何语言只要知道形状、数据类型以及是基于行还是基于列,都可以读取二进制文件。如果你只是使用 Python,那么 npy 就可以了,可能比二进制更容易。 谢谢!还有一个问题:我是否忽略了某些内容,还是您遗漏了 HDF5?由于这很常见,因此我很想将它与其他方法进行比较。 我尝试使用 png 和 npy 来保存相同的图像。 png 只占用 2K 空间,而 npy 占用 307K。这个结果和你的工作真的不一样。难道我做错了什么?此图像是灰度图像,里面只有 0 和 255。我认为这是一个稀疏数据对吗?然后我也用了 npz 但大小完全一样。 为什么没有 h5py?还是我错过了什么?【参考方案5】:

我非常喜欢使用 hdf5 存储大型 numpy 数组。 python中处理hdf5有两种选择:

http://www.pytables.org/

http://www.h5py.org/

两者都旨在有效地处理 numpy 数组。

【讨论】:

您愿意提供一些使用这些包保存数组的示例代码吗? h5py example 和 pytables example 根据我的经验,启用块存储和压缩后,hdf5 的读写速度非常慢。例如,我有两个形状为 (2500,000 * 2000) 且块大小为 (10,000 * 2000) 的二维数组。对形状为 (2000 * 2000) 的数组的单次写入操作大约需要 1~2s 才能完成。您对提高性能有什么建议吗?谢谢。 1 到 2 秒对于这么大的数组来说看起来并不长。与 .npy 格式相比,性能如何?【参考方案6】:

另一种有效存储 numpy 数组的可能性是Bloscpack:

#!/usr/bin/python
import numpy as np
import bloscpack as bp
import time

n = 10000000

a = np.arange(n)
b = np.arange(n) * 10
c = np.arange(n) * -0.5
tsizeMB = sum(i.size*i.itemsize for i in (a,b,c)) / 2**20.

blosc_args = bp.DEFAULT_BLOSC_ARGS
blosc_args['clevel'] = 6
t = time.time()
bp.pack_ndarray_file(a, 'a.blp', blosc_args=blosc_args)
bp.pack_ndarray_file(b, 'b.blp', blosc_args=blosc_args)
bp.pack_ndarray_file(c, 'c.blp', blosc_args=blosc_args)
t1 = time.time() - t
print "store time = %.2f (%.2f MB/s)" % (t1, tsizeMB / t1)

t = time.time()
a1 = bp.unpack_ndarray_file('a.blp')
b1 = bp.unpack_ndarray_file('b.blp')
c1 = bp.unpack_ndarray_file('c.blp')
t1 = time.time() - t
print "loading time = %.2f (%.2f MB/s)" % (t1, tsizeMB / t1)

以及我的笔记本电脑(配备 Core2 处理器的相对较旧的 MacBook Air)的输出:

$ python store-blpk.py
store time = 0.19 (1216.45 MB/s)
loading time = 0.25 (898.08 MB/s)

这意味着它可以非常快速地存储,即瓶颈通常是磁盘。但是,由于这里的压缩比非常好,因此有效速度乘以压缩比。以下是这些 76 MB 数组的大小:

$ ll -h *.blp
-rw-r--r--  1 faltet  staff   921K Mar  6 13:50 a.blp
-rw-r--r--  1 faltet  staff   2.2M Mar  6 13:50 b.blp
-rw-r--r--  1 faltet  staff   1.4M Mar  6 13:50 c.blp

请注意,使用Blosc 压缩器是实现这一目标的基础。相同的脚本,但使用 'clevel' = 0(即禁用压缩):

$ python bench/store-blpk.py
store time = 3.36 (68.04 MB/s)
loading time = 2.61 (87.80 MB/s)

显然是磁盘性能的瓶颈。

【讨论】:

它可能关注的对象:虽然 Bloscpack 和 PyTables 是不同的项目,但前者只关注磁盘转储而不是存储数组切片,我测试了两者,对于纯“文件转储项目”Bloscpack 几乎是 6 倍比 PyTables 更快。【参考方案7】:

savez() 将数据保存在 zip 文件中,压缩和解压缩文件可能需要一些时间。你可以使用 save() & load() 函数:

f = file("tmp.bin","wb")
np.save(f,a)
np.save(f,b)
np.save(f,c)
f.close()

f = file("tmp.bin","rb")
aa = np.load(f)
bb = np.load(f)
cc = np.load(f)
f.close()

要将多个数组保存在一个文件中,只需先打开文件,然后依次保存或加载数组即可。

【讨论】:

以上是关于在磁盘上保留 numpy 数组的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

在 ascii 文件中存储 numpy 数组的最佳方法

保存在磁盘上的 numpy 数组中的随机访问

Numpy - 从一维数组中删除最后一个元素的最佳方法?

将 SymPy 矩阵转换为 numpy 数组/矩阵的最佳方法是啥

将不同长度的numpy数组保存到同一个csv文件的最佳方法是啥?

这是在一行代码中向 numpy 数组添加额外维度的最佳方法吗?