il modo migliore per preservare gli array numpy su disco


124

Sto cercando un modo veloce per preservare grandi array numpy. Voglio salvarli sul disco in un formato binario, quindi leggerli di nuovo in memoria in modo relativamente veloce. cPickle non è abbastanza veloce, sfortunatamente.

Ho trovato numpy.savez e numpy.load . Ma la cosa strana è che numpy.load carica un file npy in "memory-map". Ciò significa che la manipolazione regolare degli array è molto lenta. Ad esempio, qualcosa del genere sarebbe molto lento:

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

più precisamente, la prima riga sarà molto veloce, ma le restanti righe che assegnano gli array objsono ridicolmente lente:

loading time =  0.000220775604248
assining time =  2.72940087318

C'è un modo migliore per preservare gli array numpy? Idealmente, voglio essere in grado di memorizzare più array in un file.


3
Per impostazione predefinita, nonnp.load dovrebbe mmap il file.
Fred Foo

6
E i pirati ?
design

@larsmans, grazie per la risposta. ma perché il tempo di ricerca (z ['a'] nel mio esempio di codice) è così lento?
Vendetta

1
Sarebbe bello se ci fossero un po 'più di informazioni nella tua domanda, come il tipo di array che è memorizzato in ifile e la sua dimensione, o se sono diversi array in file diversi, o come esattamente li salvi. Dalla tua domanda, ho l'impressione che la prima riga non faccia nulla e che il caricamento effettivo avvenga dopo, ma queste sono solo supposizioni.
design

19
@larsmans - Per quel che vale, per un file "npz" (cioè più array salvati con numpy.savez), l'impostazione predefinita è "caricare pigramente" gli array. Non li memorizza, ma non li carica finché l' NpzFileoggetto non viene indicizzato. (Così il ritardo a cui si riferisce l'OP.) La documentazione per loadsaltare questo, ed è quindi un po 'fuorviante ...
Joe Kington

Risposte:


63

Sono un grande fan di hdf5 per la memorizzazione di grandi array numpy. Ci sono due opzioni per gestire hdf5 in Python:

http://www.pytables.org/

http://www.h5py.org/

Entrambi sono progettati per funzionare in modo efficiente con gli array numpy.


35
saresti disposto a fornire un codice di esempio utilizzando questi pacchetti per salvare un array?
dbliss


1
Dalle mie esperienze, hdf5 offre prestazioni di lettura e scrittura molto lente con archiviazione e compressione dei blocchi abilitate. Ad esempio, ho due array 2-D con forma (2500.000 * 2000) con dimensione del blocco (10.000 * 2000). Una singola operazione di scrittura di un array con forma (2000 * 2000) richiederà circa 1 ~ 2 secondi per essere completata. Hai qualche suggerimento per migliorare le prestazioni? grazie.
Simon. Li

206

Ho confrontato le prestazioni (spazio e tempo) per diversi modi per memorizzare array numpy. Pochi di loro supportano più array per file, ma forse è comunque utile.

punto di riferimento per l'archiviazione di array numpy

I file npy e binari sono entrambi molto veloci e piccoli per dati densi. Se i dati sono scarsi o molto strutturati, potresti voler utilizzare npz con compressione, che risparmierà molto spazio ma costi un po 'di tempo di caricamento.

Se la portabilità è un problema, binary è meglio di npy. Se la leggibilità umana è importante, dovrai sacrificare molte prestazioni, ma può essere ottenuto abbastanza bene usando csv (che è anche molto portabile ovviamente).

Maggiori dettagli e il codice sono disponibili nel repository github .


2
Potresti spiegare perché binaryè meglio che npyper la portabilità? Questo vale anche per npz?
daniel451

1
@ daniel451 Perché qualsiasi lingua può leggere file binari se conosce solo la forma, il tipo di dati e se è basato su righe o colonne. Se stai usando solo Python, npy va bene, probabilmente un po 'più facile del binario.
Marco

1
Grazie! Ancora una domanda: trascuro qualcosa o hai tralasciato HDF5? Poiché questo è abbastanza comune, sarei interessato a come si confronta con gli altri metodi.
daniel451

1
Ho provato a usare png e npy per salvare la stessa immagine. png occupa solo 2K di spazio mentre npy occupa 307K. Questo risultato è molto diverso dal tuo lavoro. Sto facendo qualcosa di sbagliato? Questa immagine è in scala di grigi e solo 0 e 255 sono all'interno. Penso che questo sia un dato scarso corretto? Poi ho usato anche npz ma la dimensione è totalmente la stessa.
York Yang

3
Perché manca h5py? Oppure mi sfugge qualcosa?
daniel451

49

Ora c'è un clone basato su HDF5 di picklechiamato 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 )

MODIFICARE:

C'è anche la possibilità di "decapare" direttamente in un archivio compresso facendo:

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

compressione


Appendice

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

un avvertimento che potrebbe interessare ad alcuni ppl è che pickle può eseguire codice arbitrario che lo rende meno sicuro di altri protocolli per il salvataggio dei dati.
Charlie Parker

È fantastico! Potete anche fornire il codice per leggere i file decapati direttamente in compressione usando lzma o bz2?
Ernest S Kirubakaran

14

savez () salva i dati in un file zip, potrebbe essere necessario del tempo per comprimere e decomprimere il file. Puoi usare la funzione 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()

Per salvare più array in un file, è sufficiente aprire prima il file, quindi salvare o caricare gli array in sequenza.


7

Un'altra possibilità per memorizzare gli array numpy in modo efficiente è 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)

e l'output per il mio laptop (un MacBook Air relativamente vecchio con un processore Core2):

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

ciò significa che può memorizzare molto velocemente, cioè il collo di bottiglia è tipicamente il disco. Tuttavia, poiché i rapporti di compressione sono abbastanza buoni qui, la velocità effettiva viene moltiplicata per i rapporti di compressione. Ecco le dimensioni per questi array da 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

Si noti che l'utilizzo del compressore Blosc è fondamentale per ottenere questo risultato. Lo stesso script ma usando 'clevel' = 0 (cioè disabilitando la compressione):

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

è chiaramente ostacolato dalle prestazioni del disco.


2
A chi può interessare: sebbene Bloscpack e PyTables siano progetti diversi, il primo incentrato solo sul dump del disco e non sull'affettatura di array archiviati, ho testato entrambi e per i puri "progetti di file dump" Bloscpack è quasi 6 volte più veloce di PyTables.
Marcelo Sardelich

4

Il tempo di ricerca è lento perché quando si utilizza mmapper non carica il contenuto dell'array in memoria quando si richiama il loadmetodo. I dati vengono caricati in modo pigro quando sono necessari dati particolari. E questo accade nella ricerca nel tuo caso. Ma la seconda ricerca non sarà così lenta.

Questa è una caratteristica interessante di mmapquando si dispone di un array di grandi dimensioni non è necessario caricare interi dati in memoria.

Per risolvere il tuo problema puoi usare joblib puoi scaricare qualsiasi oggetto tu voglia usando joblib.dumpanche due o più numpy arrays, vedi l'esempio

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

La libreria non è più disponibile.
Andrea Moro
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.