найкращий спосіб зберегти нудні масиви на диску


124

Я шукаю швидкий спосіб зберегти великі масивні масиви. Я хочу зберегти їх на диску у двійковому форматі, а потім відносно швидко прочитати їх у пам'яті. 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

Чи є кращий спосіб збереження нумерованих масивів? В ідеалі я хочу мати можливість зберігати кілька масивів в одному файлі.


3
За замовчуванням неnp.load слід редагувати файл.
Фред Фоо

6
А що з піблетами ?
dsign

@larsmans, дякую за відповідь. але чому час пошуку (z ['a'] в моєму прикладі коду) такий повільний?
Вендетта

1
Було б добре, якби у вас було трохи більше інформації у вашому запитанні, як-от тип масиву, який зберігається в ifile, і його розмір, або якщо вони є декількома масивами в різних файлах, або як саме ви їх зберігаєте. За вашим запитанням у мене склалося враження, що перший рядок нічого не робить і що фактичне завантаження відбувається після, але це лише здогадки.
dsign

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

Відповіді:


63

Я великий фанат hdf5 для зберігання великих масивних масивів. Є два варіанти роботи з hdf5 в python:

http://www.pytables.org/

http://www.h5py.org/

Обидва розроблені для ефективної роботи з нумеровими масивами.


35
Чи готові ви надати якийсь приклад код за допомогою цих пакетів для збереження масиву?
dbliss


1
З мого досвіду, програми hdf5 дуже повільно читають і пишуть з увімкненим зберіганням та стисненням. Наприклад, у мене два двовимірні масиви з формою (2500 000 * 2000) з розміром шматка (10 000 * 2000). Одинична операція запису масиву з формою (2000 * 2000) займе близько 1 ~ 2с. Чи є якісь пропозиції щодо підвищення продуктивності? Дякую.
Саймон. Лі

206

Я порівняв продуктивність (простір та час) для кількох способів зберігання нумерованих масивів. Мало хто з них підтримує кілька масивів у файлі, але, мабуть, це корисно в будь-якому випадку.

орієнтир для зберігання масивного масиву

Npy та бінарні файли є дійсно швидкими та малими для щільних даних. Якщо дані рідкісні або дуже структуровані, можливо, ви хочете використовувати npz зі стисненням, що заощадить багато місця, але коштує певного часу завантаження.

Якщо портативність є проблемою, двійковий файл краще, ніж npy. Якщо читабельність людини важлива, то вам доведеться пожертвувати великою продуктивністю, але це можна досягти досить добре, використовуючи csv (що, звичайно, також дуже портативно).

Більш детальну інформацію та код можна отримати на репозиторії github .


2
Чи можете ви пояснити, чому binaryкраще, ніж npyдля портативності? Це також стосується npz?
daniel451

1
@ daniel451 Оскільки будь-яка мова може читати двійкові файли, якщо вони просто знають форму, тип даних та на основі рядків чи стовпців. Якщо ви просто використовуєте Python, тоді npy добре, мабуть, трохи простіше, ніж двійковий.
Марк

1
Дякую! Ще одне запитання: чи я щось не помічаю чи у вас залишився HDF5? Оскільки це досить поширене явище, мені було б цікаво, як воно порівнюється з іншими методами.
daniel451

1
Я намагався використовувати png і npy для збереження того ж зображення. png займає лише 2K місця, тоді як npy займає 307K. Цей результат справді відрізняється від вашої роботи. Чи я щось роблю не так? Це зображення є зображенням сірого кольору, і лише 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' )

Одне попередження, яке може хвилювати деякі ppl, - це те, що пікел може виконувати довільний код, що робить його менш захищеним, ніж інші протоколи для збереження даних.
Чарлі Паркер

Це чудово! Чи можете ви також надати код для читання файлів, маринованих безпосередньо в стиснення, за допомогою 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

Ще одна можливість ефективно зберігати нумерові масиви - це 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.
Marcelo Sardelich

4

Час пошуку повільний, тому що, коли ви використовуєте 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')

Бібліотека більше не доступна.
Андреа Моро
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.