在磁盘上保存NumPy数组的最佳方法

173

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

我发现了numpy.saveznumpy.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数组?理想情况下,我希望能够在一个文件中存储多个数组。


4
默认情况下,np.load 不应该使用内存映射方式加载文件。 - Fred Foo
6
关于 pytables 怎么样? - dsign
1
如果您的问题中能够提供更多信息,比如ifile中存储的数组类型及其大小,或者不同文件中是否有多个数组,以及保存方式等,那就更好了。从您的问题中,我得到的印象是第一行代码没有实际作用,而真正的加载操作发生在之后,但这只是我的猜测。 - dsign
23
值得一提的是,对于“npz”文件(即使用numpy.savez保存的多个数组),默认情况下是“懒加载”这些数组。它并没有将它们映射到内存,但在索引NpzFile对象之前不会加载它们。(这就是OP所指的延迟时间)。 load方法的文档跳过了这一点,因此有点误导性... - Joe Kington
1
@JoeKington 谢谢Joe。但是我如何“不懒惰地加载”npz文件? - CuriousMind
显示剩余7条评论
7个回答

303

我比较了一些存储numpy数组的方法的性能(空间和时间)。其中很少有支持将多个数组存储在一个文件中,但也许这仍然有用。

numpy数组存储基准测试

对于密集数据,npz和二进制文件都非常快速且占用空间小。如果数据是稀疏的或者非常结构化的,你可能需要使用带有压缩功能的npz,虽然会花费一些加载时间,但可以节省大量空间。

如果可移植性是一个问题,那么二进制文件比npy更好。如果人类可读性很重要,那么你需要牺牲很多性能,但可以使用csv(当然也非常可移植)来实现相对良好的效果。

更多细节和代码请参阅github repo


3
你能解释一下为什么在可移植性方面 binarynpy 更好吗?这是否也适用于 npz - daniel451
4
因为任何语言只要知道数据形状、数据类型以及是行或列,就可以读取二进制文件。如果你只是使用 Python,那么 npy 格式就可以了,可能比二进制文件更容易些。 - Mark
1
谢谢!还有一个问题:我是不是忽略了什么,或者你遗漏了HDF5?由于这是非常常见的,我很想知道它与其他方法的比较。 - daniel451
1
我尝试使用png和npy来保存同一张图片。png只占用2K的空间,而npy占用307K的空间。这个结果与你的工作真的很不同。我做错了什么吗?这张图片是灰度图像,只有0和255在内部。我认为这是稀疏数据,对吗?然后我也使用了npz,但大小完全相同。 - York Yang
6
为什么缺少 h5py?或者是我漏掉了什么? - daniel451
显示剩余9条评论

77

51
您愿意提供一些使用这些包保存数组的示例代码吗? - abcd
18
好的,以下是翻译内容:h5py example:原文:import numpy as np import h5py arr = np.zeros((100, 100)) with h5py.File('data.h5', 'w') as hf: hf.create_dataset("dataset1", data=arr)翻译:import numpy as np import h5py # 创建一个100x100的零数组 arr = np.zeros((100, 100)) # 打开HDF5文件并创建一个数据集 with h5py.File('data.h5', 'w') as hf: hf.create_dataset("dataset1", data=arr)pytables example:原文:import numpy as np import tables hdf5_file = tables.open_file('data.h5', mode='w', title="Test Array") array_c = tables.Float32Col(shape=(2,)) filters = tables.Filters(complib='blosc', complevel=9) table = hdf5_file.create_table('/', 'data', array_c, "Data storage table", filters=filters) dw = table.row dw['f0'] = np.array([1.3, 2.5], dtype=np.float32) dw.append() dw['f0'] = np.array([1.4, 2.6], dtype=np.float32) dw.append() table.flush() hdf5_file.close()翻译:import numpy as np import tables # 打开HDF5文件并创建一个数据表 hdf5_file = tables.open_file('data.h5', mode='w', title="Test Array") # 定义一个包含两个元素的浮点数列 array_c = tables.Float32Col(shape=(2,)) # 设置压缩过滤器和数据表参数 filters = tables.Filters(complib='blosc', complevel=9) table = hdf5_file.create_table('/', 'data', array_c, "Data storage table", filters=filters) # 向数据表中添加两行数据并刷新数据表 dw = table.row dw['f0'] = np.array([1.3, 2.5], dtype=np.float32) dw.append() dw['f0'] = np.array([1.4, 2.6], dtype=np.float32) dw.append() table.flush() # 关闭HDF5文件 hdf5_file.close() - Kamil Slowikowski
1
根据我的经验,在启用块存储和压缩的情况下,hdf5 的读写性能非常慢。例如,我有两个形状为(2500,000 * 2000)、块大小为(10,000 * 2000)的二维数组。一个形状为(2000 * 2000)的单一写操作需要约1 ~ 2秒才能完成。您有任何改善性能的建议吗?谢谢。 - Simon. Li
1
1到2秒对于这么大的数组来说并不算太长。与.npy格式相比,性能如何? - Alexandre Huat
HDF5在CPU内存消耗方面是否存在问题?当HDF5文件较大时,我在多工作器训练中遇到了一些问题。而NPZ可以使用内存映射来避免这种情况。 - ToughMind

54

现在有一个基于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'))

compression


附录

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可以执行任意代码,这使其比其他用于保存数据的协议不太安全。 - Charlie Parker
太好了!您能否提供读取使用lzma或bz2直接压缩的文件pickle的代码? - Ernest S Kirubakaran
3
基本上是一样的:如果你是使用 pickle.dump( obj, gzip.open( 'filename.pkl.gz', 'wb' ) ) 来保存数据,那么你可以使用 pickle.load( gzip.open( 'filename.pkl.gz', 'r' ) ) 来加载数据。 - Suuuehgi

18

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()

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


8

另一种有效存储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)

我的笔记本电脑是相对较旧的MacBook Air,配备了Core2处理器,以下是输出结果:

$ 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)

很明显,磁盘性能是瓶颈。


2
敬启者:虽然Bloscpack和PyTables是不同的项目,前者仅关注于磁盘转储而不是存储数组切片,但我测试了两个项目,对于纯“文件转储项目”,Bloscpack几乎比PyTables快6倍。 - Marcelo Sardelich

5

查询时间较慢是因为使用 mmap 时,在调用 load 方法时不会将数组的内容加载到内存中。只有在需要特定数据时才会进行延迟加载,而这正发生在您的查找中。但第二次查询将不会那么慢。

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

要解决这个问题,您可以使用 joblib。使用 joblib.dump,您可以转储任何想要的对象,甚至包括两个或多个 numpy 数组,请参见示例。

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')

该库不再可用。 - Andrea Moro

0
“最好”的选择取决于你的目标。正如其他人所说,二进制文件具有最大的可移植性,但问题在于你需要了解数据的存储方式。 Darr以基于平面二进制和文本文件的自记录方式保存你的numpy数组。这最大化了广泛的可读性。它还自动包含了如何在各种数据科学语言中读取你的数组的代码,例如numpy本身,以及R、Matlab、Julia等。
声明:我编写了这个库。

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