лучший способ сохранить массивы numpy на диске


124

Я ищу быстрый способ сохранить большие массивы 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? В идеале я хочу иметь возможность хранить несколько массивов в одном файле.


3
По умолчанию неnp.load следует отображать файл.
Fred Foo

6
А как насчет pytables ?
dsign 08

@larsmans, спасибо за ответ. но почему время поиска (z ['a'] в моем примере кода) такое медленное?
Vendetta

1
Было бы неплохо, если бы в вашем вопросе было немного больше информации, например, тип массива, который хранится в ifile, и его размер, или если это несколько массивов в разных файлах, или как именно вы их сохраняете. По вашему вопросу у меня сложилось впечатление, что первая строка ничего не делает и что фактическая загрузка происходит после, но это только предположения.
dsign 08

19
@larsmans - Как бы то ни было, для файла "npz" (то есть с несколькими массивами, сохраненными с numpy.savez) по умолчанию массивы загружаются "лениво". Он не запоминает их, но и не загружает, пока NpzFileобъект не проиндексирован. (Таким образом, задержка, о которой идет речь в ОП.) В документации loadэто пропускается, и поэтому она вводит в заблуждение ...
Джо Кингтон

Ответы:


63

Я большой поклонник hdf5 для хранения больших массивов numpy. Есть два варианта работы с hdf5 в python:

http://www.pytables.org/

http://www.h5py.org/

Оба предназначены для эффективной работы с массивами numpy.


35
Не могли бы вы предоставить пример кода с использованием этих пакетов для сохранения массива?
dbliss


1
По моему опыту, hdf5 работает очень медленно при чтении и записи при включенном хранении фрагментов и сжатии. Например, у меня есть два двумерных массива формы (2500000 * 2000) с размером блока (10000 * 2000). Одна операция записи массива с формой (2000 * 2000) займет около 1-2 секунд. Есть ли у вас предложения по повышению производительности? Спасибо.
Саймон. Li

206

Я сравнил производительность (пространство и время) для нескольких способов хранения массивов numpy. Некоторые из них поддерживают несколько массивов на файл, но, возможно, это все равно полезно.

тест для хранения массива numpy

Npy и двоичные файлы очень быстрые и маленькие для плотных данных. Если данные разреженные или очень структурированные, вы можете использовать npz со сжатием, что сэкономит много места, но потребует некоторого времени загрузки.

Если переносимость является проблемой, двоичный файл лучше, чем npy. Если удобочитаемость для человека важна, вам придется пожертвовать большой производительностью, но этого можно довольно хорошо добиться с помощью csv (который, конечно, также очень переносим).

Более подробная информация и код доступны в репозитории github .


2
Не могли бы вы объяснить, почему binaryлучше, чем npyдля портативности? Это также относится к npz?
daniel451 01

1
@ daniel451 Потому что любой язык может читать двоичные файлы, если он просто знает форму, тип данных и то, на основе ли он строк или столбцов. Если вы просто используете Python, тогда npy в порядке, возможно, немного проще, чем двоичный.
Марк

1
Спасибо! Еще один вопрос: я что-то упускаю или вы не учли HDF5? Поскольку это довольно распространено, мне было бы интересно, как он сравнивается с другими методами.
daniel451 02

1
Я пытался использовать png и npy, чтобы сохранить одно и то же изображение. png занимает всего 2 КБ, а npy занимает 307 КБ. Этот результат действительно отличается от вашего. Я делаю что-то неправильно? Это изображение в оттенках серого, внутри которого находятся только 0 и 255. Я думаю, это скудные данные, правильно? Затем я также использовал npz, но размер тот же.
Йорк Ян

3
Почему отсутствует h5py? Или я что-то упускаю?
daniel451

49

Теперь существует клон, основанный на 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' ]
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:

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

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

        else:
            time_start = time.time()
            pickle.dump( data[key], eval(compression).open( 'data.pkl.{}'.format(compression), 'wb' ) )
            time_tot = time.time() - time_start
            sizes[key][ labels[ compressions.index(compression) ] ] = ( os.path.getsize( 'data.pkl.{}'.format(compression) ) * 10**(-6), time_tot )
            os.remove( 'data.pkl.{}'.format(compression) )


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?
Ernest S Kirubakaran,

14

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

Чтобы сохранить несколько массивов в одном файле, вам просто нужно сначала открыть файл, а затем последовательно сохранить или загрузить массивы.


7

Еще одна возможность эффективно хранить массивы 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 МБ:

$ 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 почти в 6 раз быстрее PyTables.
Марсело Сарделич

4

Время поиска медленное, потому что когда вы используете mmapto, не загружает содержимое массива в память при вызове 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')

Библиотека больше недоступна.
Андреа Моро
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.