najlepszy sposób na zachowanie numpy tablic na dysku

124

Szukam szybkiego sposobu na zachowanie dużych, zdrętwiałych tablic. Chcę je zapisać na dysku w formacie binarnym, a następnie stosunkowo szybko wczytać z powrotem do pamięci. Niestety, cPickle nie jest wystarczająco szybki.

Znalazłem numpy.savez i numpy.load . Ale dziwne jest to, że numpy.load ładuje plik npy ​​do "mapy pamięci". Oznacza to, że regularne manipulowanie tablicami jest naprawdę powolne. Na przykład coś takiego byłoby naprawdę powolne:

#!/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;

dokładniej, pierwsza linia będzie naprawdę szybka, ale pozostałe linie, które przypisują tablice, objsą absurdalnie wolne:

loading time =  0.000220775604248
assining time =  2.72940087318

Czy jest lepszy sposób na zachowanie tablic numpy? W idealnym przypadku chciałbym mieć możliwość przechowywania wielu tablic w jednym pliku.

Wendeta
źródło
3
Domyślnie nienp.load należy mmapować pliku.
Fred Foo
6
A co z pytables ?
dsign
@larsmans, dzięki za odpowiedź. ale dlaczego czas wyszukiwania (z ['a'] w moim przykładzie kodu) jest tak długi?
Vendetta
1
Byłoby miło, gdybyśmy w twoim pytaniu mieli trochę więcej informacji, takich jak rodzaj tablicy, która jest przechowywana w ifile i jej rozmiar, lub jeśli jest to kilka tablic w różnych plikach, lub jak dokładnie je zapisujesz. Przez twoje pytanie mam wrażenie, że pierwsza linijka nic nie robi i że faktyczne ładowanie następuje później, ale to tylko domysły.
dsign
19
@larsmans - Bez względu na to, ile jest to warte, dla pliku „npz” (tj. wielu tablic zapisanych w programie numpy.savez), domyślnie tablice są ładowane leniwie. Nie jest to memmapowanie ich, ale nie ładuje ich, dopóki NpzFileobiekt nie zostanie zindeksowany. (Stąd opóźnienie, do którego odnosi się PO.) Dokumentacja loadpomija to i dlatego jest trochę myląca ...
Joe Kington

Odpowiedzi:

63

Jestem wielkim fanem hdf5 do przechowywania dużych tablic numpy. Istnieją dwie możliwości radzenia sobie z hdf5 w Pythonie:

http://www.pytables.org/

http://www.h5py.org/

Oba są zaprojektowane do wydajnej pracy z tablicami numpy.

JoshAdel
źródło
35
czy zechcesz podać przykładowy kod wykorzystujący te pakiety do zapisania tablicy?
dbliss
1
Z moich doświadczeń wynika, że ​​wydajność hdf5 jest bardzo powolna podczas czytania i pisania z włączoną funkcją przechowywania i kompresji fragmentów. Na przykład mam dwie tablice 2-W z kształtem (2500 000 * 2000) z rozmiarem fragmentu (10 000 * 2000). Pojedyncza operacja zapisu tablicy o kształcie (2000 * 2000) zajmie około 1 ~ 2 s. Czy masz jakieś sugestie dotyczące poprawy wydajności? dzięki.
Simon. Li
206

Porównałem wydajność (przestrzeń i czas) dla wielu sposobów przechowywania tablic numpy. Niewiele z nich obsługuje wiele tablic na plik, ale być może i tak jest to przydatne.

wzorzec dla pamięci masowej numpy

Pliki Npy i binarne są bardzo szybkie i małe dla gęstych danych. Jeśli dane są rzadkie lub bardzo uporządkowane, możesz chcieć użyć npz z kompresją, co pozwoli zaoszczędzić dużo miejsca, ale kosztuje trochę czasu ładowania.

Jeśli problemem jest przenośność, plik binarny jest lepszy niż npy. Jeśli czytelność dla ludzi jest ważna, będziesz musiał poświęcić dużo wydajności, ale można to całkiem dobrze osiągnąć za pomocą csv (który jest oczywiście również bardzo przenośny).

Więcej szczegółów i kod są dostępne w repozytorium github .

znak
źródło
2
Czy możesz wyjaśnić, dlaczego binaryjest lepsze niż w npyprzypadku przenośności? Czy to dotyczy również npz?
daniel451
1
@ daniel451 Ponieważ każdy język może czytać pliki binarne, jeśli zna tylko kształt, typ danych i czy są one oparte na wierszach czy kolumnach. Jeśli używasz tylko Pythona, npy jest w porządku, prawdopodobnie trochę łatwiejsze niż binarne.
Mark
1
Dziękuję Ci! Jeszcze jedno pytanie: czy coś przeoczyłem, czy pominąłeś HDF5? Ponieważ jest to dość powszechne, byłbym zainteresowany porównaniem tego z innymi metodami.
daniel451
1
Próbowałem użyć png i npy, aby zapisać ten sam obraz. png zajmuje tylko 2K miejsca, podczas gdy npy zajmuje 307K. Ten wynik naprawdę różni się od twojej pracy. czy robię coś źle? Ten obraz jest obrazem w skali szarości i tylko 0 i 255 są w środku. Myślę, że to rzadkie dane, prawda? Wtedy też użyłem npz ale rozmiar jest zupełnie taki sam.
York Yang
3
Dlaczego brakuje h5py? A może coś mi brakuje?
daniel451
49

Jest teraz oparty na HDF5 klon picklenazwany 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 )

EDYTOWAĆ:

Istnieje również możliwość "wytrawiania" bezpośrednio do skompresowanego archiwum, wykonując:

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

kompresja


dodatek

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' )
Suuuehgi
źródło
jednym ostrzeżeniem, które może zainteresować niektórych ppl, jest to, że pickle może wykonać dowolny kod, co czyni go mniej bezpiecznym niż inne protokoły do ​​zapisywania danych.
Charlie Parker
To jest świetne! Czy możesz również podać kod do odczytu plików poddanych kompresji bezpośrednio przy użyciu lzma lub bz2?
Ernest S Kirubakaran
14

savez () zapisuje dane w pliku zip, skompresowanie i rozpakowanie pliku może zająć trochę czasu. Możesz użyć funkcji 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()

Aby zapisać wiele tablic w jednym pliku, wystarczy najpierw otworzyć plik, a następnie zapisać lub załadować tablice po kolei.

HYRY
źródło
7

Inną możliwością efektywnego przechowywania tablic numpy jest 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)

i wyjście dla mojego laptopa (stosunkowo stary MacBook Air z procesorem Core2):

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

oznacza to, że może przechowywać naprawdę szybko, tj. wąskim gardłem jest zwykle dysk. Ponieważ jednak współczynniki kompresji są tutaj całkiem dobre, efektywna prędkość jest mnożona przez współczynniki kompresji. Oto rozmiary tych 76 MB macierzy:

$ 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

Należy pamiętać, że użycie kompresora Blosc jest podstawą do osiągnięcia tego. Ten sam skrypt, ale używający „clevel” = 0 (tj. Wyłączając kompresję):

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

jest wyraźnie ograniczony przez wydajność dysku.

Francesc
źródło
2
Do kogo to może dotyczyć: Chociaż Bloscpack i PyTables to różne projekty, pierwszy skupiający się tylko na zrzucie dysku i nie przechowywanych tablicach, przetestowałem oba i dla czystego "projektów zrzutu plików" Bloscpack jest prawie 6x szybszy niż PyTables.
Marcelo Sardelich
4

Czas wyszukiwania jest długi, ponieważ gdy używasz mmapdo, nie ładuje zawartości tablicy do pamięci podczas wywoływania loadmetody. Dane są ładowane z opóźnieniem, gdy potrzebne są określone dane. Dzieje się to podczas wyszukiwania w Twoim przypadku. Ale drugie wyszukiwanie nie będzie tak powolne.

Jest to fajna cecha, mmapgdy masz dużą tablicę, nie musisz ładować całych danych do pamięci.

Aby rozwiązać problem, użyj joblib , możesz zrzucić dowolny obiekt, używając joblib.dumpnawet dwóch lub więcej numpy arrays, patrz przykład

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')
Michał
źródło
Biblioteka nie jest już dostępna.
Andrea Moro,