meilleure façon de préserver les tableaux numpy sur le disque

124

Je recherche un moyen rapide de conserver de grands tableaux numpy. Je veux les enregistrer sur le disque au format binaire, puis les relire en mémoire relativement rapidement. cPickle n'est malheureusement pas assez rapide.

J'ai trouvé numpy.savez et numpy.load . Mais la chose étrange est que numpy.load charge un fichier npy dans "memory-map". Cela signifie que la manipulation régulière des tableaux est vraiment lente. Par exemple, quelque chose comme ça serait vraiment lent:

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

plus précisément, la première ligne sera vraiment rapide, mais les lignes restantes qui assignent les tableaux objsont ridiculement lentes:

loading time =  0.000220775604248
assining time =  2.72940087318

Existe-t-il un meilleur moyen de préserver les tableaux numpy? Idéalement, je souhaite pouvoir stocker plusieurs tableaux dans un seul fichier.

Vengeance
la source
3
Par défaut, nenp.load doit pas mmapper le fichier.
Fred Foo
6
Et les pytables ?
dsign
@larsmans, merci pour la réponse. mais pourquoi le temps de recherche (z ['a'] dans mon exemple de code) est-il si lent?
Vendetta
1
Ce serait bien s'il y avait un peu plus d'informations dans votre question, comme le type de tableau stocké dans ifile et sa taille, ou s'il s'agit de plusieurs tableaux dans des fichiers différents, ou comment les enregistrez-vous exactement. Par votre question, j'ai l'impression que la première ligne ne fait rien et que le chargement se fait après, mais ce ne sont que des suppositions.
dsign
19
@larsmans - Pour ce que ça vaut, pour un fichier "npz" (c'est-à-dire plusieurs tableaux enregistrés avec numpy.savez), la valeur par défaut est de "charger paresseusement" les tableaux. Il ne les mappe pas, mais il ne les charge pas tant que l' NpzFileobjet n'est pas indexé. (Ainsi, le délai auquel l'OP fait référence.) La documentation pour loadignorer cela, et est donc un peu trompeuse ...
Joe Kington

Réponses:

63

Je suis un grand fan de hdf5 pour stocker de grands tableaux numpy. Il existe deux options pour gérer hdf5 en python:

http://www.pytables.org/

http://www.h5py.org/

Les deux sont conçus pour fonctionner efficacement avec des tableaux numpy.

JoshAdel
la source
35
seriez-vous prêt à fournir un exemple de code utilisant ces packages pour enregistrer un tableau?
dbliss
12
Exemple h5py et exemple pytables
Kamil Slowikowski
1
D'après mes expériences, hdf5 exécute une lecture et une écriture très lentes avec le stockage de blocs et la compression activés. Par exemple, j'ai deux tableaux 2D avec une forme (2500 000 * 2000) avec une taille de bloc (10 000 * 2000). Une seule opération d'écriture d'un tableau avec une forme (2000 * 2000) prendra environ 1 à 2 s. Avez-vous des suggestions pour améliorer les performances? THX.
Simon. Li
206

J'ai comparé les performances (espace et temps) pour un certain nombre de façons de stocker des tableaux numpy. Peu d'entre eux prennent en charge plusieurs tableaux par fichier, mais c'est peut-être utile quand même.

référence pour le stockage de baies numpy

Les fichiers Npy et binaires sont à la fois très rapides et petits pour les données denses. Si les données sont rares ou très structurées, vous pouvez utiliser npz avec compression, ce qui économisera beaucoup d'espace mais coûtera du temps de chargement.

Si la portabilité est un problème, le binaire est meilleur que npy. Si la lisibilité humaine est importante, vous devrez sacrifier beaucoup de performances, mais cela peut être assez bien réalisé en utilisant csv (qui est également très portable bien sûr).

Plus de détails et le code sont disponibles dans le dépôt github .

marque
la source
2
Pourriez-vous expliquer pourquoi binaryc'est mieux que npypour la portabilité? Cela vaut-il également pour npz?
daniel451
1
@ daniel451 Parce que n'importe quel langage peut lire des fichiers binaires s'ils connaissent juste la forme, le type de données et s'il s'agit d'une ligne ou d'une colonne. Si vous utilisez simplement Python, npy est très bien, probablement un peu plus facile que binaire.
Mark
1
Je vous remercie! Une dernière question: est-ce que j'oublie quelque chose ou avez-vous oublié HDF5? Comme c'est assez courant, je serais intéressé de savoir comment cela se compare aux autres méthodes.
daniel451
1
J'ai essayé d'utiliser png et npy pour enregistrer une même image. png ne prend que 2K d'espace tandis que le npy prend 307K. Ce résultat est vraiment différent de votre travail. Est-ce que je fais quelque chose de mal? Cette image est une image en niveaux de gris et seuls 0 et 255 sont à l'intérieur. Je pense que ce sont des données rares correctes? Ensuite, j'ai également utilisé npz mais la taille est totalement la même.
York Yang
3
Pourquoi h5py manque-t-il? Ou est-ce que je manque quelque chose?
daniel451
49

Il existe maintenant un clone basé sur HDF5 de picklecalled 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 )

ÉDITER:

Il y a aussi la possibilité de "pickle" directement dans une archive compressée en faisant:

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


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' )
Suuuehgi
la source
un avertissement qui pourrait intéresser certains utilisateurs est que pickle peut exécuter du code arbitraire, ce qui le rend moins sûr que d'autres protocoles de sauvegarde de données.
Charlie Parker le
C'est bien! Pouvez-vous également fournir le code de lecture des fichiers directement en compression avec lzma ou bz2?
Ernest S Kirubakaran
14

savez () enregistrer les données dans un fichier zip, cela peut prendre un certain temps pour compresser et décompresser le fichier. Vous pouvez utiliser la fonction 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()

Pour enregistrer plusieurs tableaux dans un fichier, il vous suffit d'ouvrir d'abord le fichier, puis d'enregistrer ou de charger les tableaux dans l'ordre.

HYRY
la source
7

Une autre possibilité de stocker efficacement des tableaux numpy est 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)

et la sortie de mon ordinateur portable (un MacBook Air relativement ancien avec un processeur Core2):

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

cela signifie qu'il peut stocker très rapidement, c'est-à-dire que le goulot d'étranglement est généralement le disque. Cependant, comme les taux de compression sont assez bons ici, la vitesse effective est multipliée par les taux de compression. Voici les tailles de ces baies de 76 Mo:

$ 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

Veuillez noter que l'utilisation du compresseur Blosc est fondamentale pour y parvenir. Le même script mais en utilisant 'clevel' = 0 (c'est-à-dire en désactivant la compression):

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

est clairement goulot d'étranglement par les performances du disque.

Francesc
la source
2
Pour qui cela peut concerner: Bien que Bloscpack et PyTables soient des projets différents, le premier se concentrant uniquement sur le vidage de disque et non le découpage des tableaux stockés, j'ai testé à la fois et pour de purs "projets de vidage de fichiers" Bloscpack est presque 6 fois plus rapide que PyTables.
Marcelo Sardelich
4

Le temps de recherche est lent car lorsque vous utilisez mmappour ne charge pas le contenu du tableau en mémoire lorsque vous appelez la loadméthode. Les données sont chargées paresseusement lorsque des données particulières sont nécessaires. Et cela se produit lors de la recherche dans votre cas. Mais la deuxième recherche ne sera pas si lente.

C'est une fonctionnalité intéressante mmaplorsque vous avez un grand tableau, vous n'avez pas à charger des données entières en mémoire.

Pour résoudre votre problème d'utilisation de joblib, vous pouvez vider n'importe quel objet de votre choix en utilisant joblib.dumpmême deux ou plus numpy arrays, voir l'exemple

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')
Michal
la source
La bibliothèque n'est plus disponible.
Andrea Moro