Python et le format HDF5

Introduction au format HDF5 avec Python

GALODE Alexandre

Lors de toute création de logiciel, se pose à un moment donné, la sauvegarde des données manipulées.

Dans un précédent article, je vous avais exposé un comparatif INI / JSON. Outre ce format, nous avions évoqué également le XML. Des formats différents, standards ou maisons, il en existe un nombre incalculable.

Concernant le traitement et la manipulation de données numériques massives, l'un d'entre eux se trouve être très utilisé en informatique scientifique : le format HDF5.

C'est ce format que je vous invite ici à découvrir.

Commentez

- Titre : Python et le format HDF5

- Auteur : GALODE Alexandre

- Parution : 22 mai 2017

- Public visé :

- Licence : Le contenu de cet article est rédigé par GALODE Alexandre et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Le format HDF5 : la théorie

- II-A. Les objets

- II-A-1. Groupe

- II-A-2. Dataset

- II-A-3. Attribut

- II-B. Les types possibles

- III. Manipulations

- III-A. Ouvrir un fichier HDF5

- III-B. Fichier

- III-B-1. Ouverture

- III-B-2. Forcer l'enregistrement

- III-B-3. Fermeture

- III-C. Groupe

- III-C-1. Ajout

- III-C-2. Accès

- III-C-3. Suppression

- III-C-4. Connaître le chemin d'accès

- III-D. Dataset

- III-D-1. Ajout / modification

- III-D-2. Accès

- III-D-3. Suppression

- III-E. Attribut

- III-E-1. Ajout / modification

- III-E-2. Accès

- III-E-3. Suppression

- III-E-4. Connaître la liste des attributs disponibles

- III-F. Exemple de l'utilité du format

- IV. Conclusion

- V. Remerciements

I - Introduction

Le format HDF5 (pour Hierarchical Data Format, V5) est un format de type conteneur de fichier. Ce format est donc assimilable à une arborescence de dossiers/fichiers, le tout contenu dans un fichier.

Son histoire commence en 1987, et il s'appelait alors AEHOO (All Encompassing Hierarchical Object Oriented Format). La première version réellement exploitable a vu le jour par l'intervention de la Nasa qui le sélectionne pour son projet EOS (Earth Observing System).

Le projet est alors renommé et le format HDF4 fait son apparition. Cependant, sa structure complexe et certaines limitations de fonctionnement (taille de fichier maximale à 2 Go) vont entraîner la création du format HDF5, simplifiant sa structure et passant outre les limitations bloquantes.

Les possibilités qu'offre le format HDF pour effectuer des calculs complexes sur de grandes masses de données, grâce à une indexation efficace, sont à la base de son succès. De plus, en Python, l'utilisation de ce format est très simple grâce au package h5py.

Pour cet article, seuls seront considérés HDF5 et Python3.x. Le format HDF4 et HDF5 sont non compatibles.

II - Le format HDF5 : la théorie

II-A - Les objets

Le format HDF5 supporte principalement trois types d'objets. Il est assez limité à ce niveau. Cependant, cela suffit pour la plupart des traitements numériques.

II-A-1 - Groupe

Les groupes, comme leurs noms l'indiquent, permettent de regrouper des datasets et des attributs, ainsi que des sous-groupes. Il sont assimilables à des dossiers.

Un groupe est constitué :

- d'une liste d'attributs ;

- d'une liste d'objets HDF5 (les groupes étant eux-mêmes de objets HDF5).

II-A-2 - Dataset

Les datasets sont des tableaux multidimensionnels contenant des données d'un même type. Il sont assimilables à des fichiers.

Un dataset est composé :

- d'un datatype (le type de données stockées) ;

- d'un dataspace (nombre de dimensions du dataset) ;

- d'un format de stockage (linéaire, ou fractionné en morceaux de même dimension).

II-A-3 - Attribut

Les attributs, enfin, sont assimilables à des métadonnées pour les fichiers ou les dossiers. Fonctionnant sur le principe d'une valeur associée à un nom, ils permettent d'ajouter des descriptions complémentaires ou de stocker de très petites valeurs.

Ils sont constitués :

- d'une valeur ;

- d'un datatype.

II-B - Les types possibles

Si la quantité d'objets supportée par HDF5 est limitée, il n'en est pas de même pour les types de données possibles, lesquels permettent de couvrir l'ensemble des besoins scientifiques.

NumPy permet une meilleure intégration de HDF5 dans Python. Cela impose néanmoins que l'on convertisse nos données Python en format NumPy. Afin d'y voir plus clair, voici quelques petits tableaux récapitulatifs :

Groupe et dataset

Type Python

Type NumPy

Objet

bool

np.bool_

dataset

None

np.float_

int

np.int_

long

np.int_

float

np.float_

complex

no.complex_

str

np.str_

bytes

np.bytes_

bytearray

np.object_

list

np.object_

tuple

np.object_

set

np.object_

dict

groupe

Dans le cas des dictionnaires, toutes les clés doivent être de type « str », sans caractères « / » ou « \x00 ».

Attributs

Type Python

Type NumPy

bool

np.bool_

None

np.float_

int

np.int_

long

np.int_

float

np.float_

complex

no.complex_

str

np.uint_ ou np.str_

bytes

np.bytes_

bytearray

np.object_

list

np.object_

tuple

np.object_

set

np.object_

III - Manipulations

Comme nous allons le voir, l'accès à un niveau désiré de données doit passer par la transmission d'un pseudo-XPATH. La racine du fichier est alors, comme sous Linux, représentée par « / ».

III-A - Ouvrir un fichier HDF5

Pour ouvrir un fichier HDF5, afin d'y lire/modifier des données, le plus simple reste de passer par le logiciel VITABLES.

Il s'agit d'un logiciel écrit en Python et PyQt, multiplateforme, créé en 2008. Il vous permettra de visualiser, sous forme de tableau, l'ensemble des données disponibles dans le fichier et de les éditer de façon conviviale.

Ajoutons enfin que VITABLES est directement disponible sur Pypi.

III-B - Fichier

III-B-1 - Ouverture

Lorsque l'on charge un fichier HDF5, nous devons lui passer deux paramètres :

- le chemin d'accès au fichier ;

- le mode d'accès.

Les mode d'accès disponibles sont les suivants :

Mode d'accès

Description

r

Lecture seule, le fichier doit exister

r+

Lecture et écriture, le fichier doit exister

w

Crée le fichier, écrase si le fichier existe

x

Crée le fichier, échoue si le fichier existe

a

Lecture et écriture, crée le fichier s'il n'existe pas

Ainsi, par exemple, pour ouvrir un fichier en lecture/écriture avec création si inexistant, nous ferons :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

III-B-2 - Forcer l'enregistrement

Un des défauts de cette librairie, ou peut-être est-ce du format, est que l'enregistrement des manipulations n'est pas systématiquement effectué dans le fichier.

De fait, vous pourrez constater l'enregistrement :

- directement après la manipulation ;

- à la fermeture du fichier ;

- si vous le demandez expressément.

Pour cette dernière solution, rien de plus simple, il suffit d'utiliser la méthode « flush ».

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. mon_fichier.flush()

Cette commande permet de demander l'exécution des actions encore non effectives.

III-B-3 - Fermeture

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. mon_fichier.flush()

5. mon_fichier.close()

III-C - Groupe

L'ajout ou la suppression d'un groupe est soumis à la condition que le fichier HDF5 soit manipulé dans un mode d'accès autorisant l'écriture.

Si vous essayez de créer un groupe déjà existant, selon le mode d'accès sélectionné, une erreur pourrait survenir

III-C-1 - Ajout

Pour créer un groupe, il suffit d'appeler la fonction « create_group », puis de lui passer le chemin d'accès :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. mon_groupe_01 = mon_fichier.create_group('groupe_01')

5. mon_fichier.close()

Il est également ensuite possible de créer un ou plusieurs sous-groupes, selon vos besoins :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. mon_groupe_01 = mon_fichier.create_group('groupe_01')

5. mon_sous_groupe_01 = mon_fichier.create_group('/groupe_01/sous_groupe_01')

6. mon_fichier.close()

Notons enfin qu'il est possible de tout réaliser en une fois, en utilisant un chemin. Ainsi, les deux lignes précédentes auraient pu être remplacées par la ligne suivante :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. mes_groupes_01 = mon_fichier.create_group('/groupe_01/sous_groupe_01')

5. mon_fichier.close()

III-C-2 - Accès

Une liaison vers un groupe se fait très simplement de la façon suivante :

1. tmp_var = mon_fichier['/groupe_01/sous_groupe_01']

Bien entendu, vous pouvez également accéder à un attribut du groupe, de la même façon que vu précédemment avec le chemin d'accès.

De même, vous pouvez avoir aisément accès aux éléments disponibles dans le groupe :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. for element in mon_fichier['/groupe_01'].items():

5. print(element[0])

6. print(element[1])

7. print(element[1].name)

8. mon_fichier.close()

III-C-3 - Suppression

La suppression, elle aussi est très simple et se réalise de la façon suivante :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. del mon_fichier['/groupe_01/sous_groupe_01']

5. mon_fichier.close()

III-C-4 - Connaître le chemin d'accès

Afin de connaître le chemin d'accès associé à un objet, il suffit d'accéder à l'attribut par défaut « name » :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. mes_groupes_01 = mon_fichier.create_group('/groupe_01/sous_groupe_01')

5. print(mes_groupes_01.name)

6. mon_fichier.close()

III-D - Dataset

L'ajout ou la suppression d'un dataset est soumis à la condition que le fichier HDF5 soit manipulé dans un mode d'accès autorisant l'écriture.

III-D-1 - Ajout / modification

Un dataset est assimilable à une matrice NumPy. Pour en créer un, il suffit de passer par la méthode « create_dataset » d'un groupe.

1. import h5py

2. import numpy as np

4. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

5. groupe_01 = mon_fichier['/groupe_01']

6. ma_matrice = np.ones((100, 100))

7. mon_dataset = groupe_01.create_dataset(name='demo_dataset', data=ma_matrice, dtype="i8")

8. mon_fichier.close()

Par la suite, la syntaxe NumPy est de mise afin de faciliter la manipulation des données.

1. import h5py

2. import numpy as np

4. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

5. groupe_01 = mon_fichier['/groupe_01']

6. ma_matrice = np.ones((100, 100))

7. mon_dataset = groupe_01['demo_dataset']

8. print(mon_dataset[0,0])

9. print(mon_dataset[0, 1:10])

10. print(mon_dataset[:,::2])

11. mon_fichier.close()

Et bien entendu, une mise à jour est extrêmement simple également :

1. import h5py

2. import numpy as np

4. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

5. groupe_01 = mon_fichier['/groupe_01']

6. ma_matrice = np.ones((100, 100))

7. mon_dataset = groupe_01['demo_dataset']

8. mon_dataset[0, :] = 2

9. print(mon_dataset[0,0])

10. print(mon_dataset[0, 1:10])

11. print(mon_dataset[:,::2])

12. mon_fichier.close()

III-D-2 - Accès

Accéder à un dataset est relativement simple. Il suffit tout d'abord d'avoir accès au fichier ou au groupe dans lequel se trouve le dataset. Ensuite on y accède comme avec un dictionnaire, où le nom du dataset est en réalité la clé.

1. mon_dataset = mon_fichier['demo_dataset']

Mais comment connaître la liste des datasets disponibles ? Cela est possible en deux étapes. Tout d'abord, établir la liste des éléments (groupes et datasets) disponibles, puis distinguer les deux types (l'exemple ci-après cherche les éléments à la racine du fichier).

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

5. list_elmts = [key for key in mon_fichier['/'].keys()]

6. for key in list_elmts:

7. print(key)

8. print(type(mon_fichier['/'][key]))

9. print(mon_fichier['/'][key])

10. print([key for key in mon_fichier['/'][key].keys()])

11. mon_fichier.close()

On peut voir ici que l'on retrouve un groupe et un dataset.

III-D-3 - Suppression

La suppression s'effectue comme avec un dictionnaire.

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

5. print([key for key in mon_fichier['/']['groupe_01'].keys()])

6. del mon_fichier['/groupe_01/sous_groupe_01']

7. print([key for key in mon_fichier['/']['groupe_01'].keys()])

8. mon_fichier.close()

III-E - Attribut

L'ajout ou la suppression d'un attribut est soumis à la condition que le fichier HDF5 soit manipulé dans un mode d'accès autorisant l'écriture.

Dans h5py, les attributs sont disponibles sous la forme d'un dictionnaires « attrs », rattaché à un groupe ou à un dataset.

III-E-1 - Ajout / modification

L'ajout d'attributs utilise la méthode « create() ». Cependant attention, car si cette méthode permet de créer des attributs, dans le cas où l'attribut existe déjà, il sera écrasé. À utiliser avec précaution donc.

Cette méthode attend quatre paramètres :

Paramètres

Valeur par défaut

Description

name

Chaîne de caractères correspondant au nom de l'attribut

data

Il s'agit de la valeur de l'attribut

shape

None

De type data.shape, il s'agit d'un « tuple » correspondant aux dimensions de l'attribut

dtype

None

Il s'agit du type de l'attribut. Dans les faits il s'agira d'un « dtype » de NumPy (np.int32, np.float64, np.dtype(float)…)

Le paramètre « data » pourra être directement une valeur simple (12, 42.3, 'Hello'…) ou alors un tableau numpy de données (numpy.array(data)). Dans le premier cas, il vous faudra laisser le paramètre « shape » à « None ». Dans le second cas, il vous faudra renseigner correctement ce paramètre. La plupart du temps vous n'aurez même pas besoin de renseigner « shape » et « dtype ».

1. import h5py

2. import numpy as np

4. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

6. mon_fichier['/groupe_01'].attrs.create("test_dvp1", 12)

7. mon_fichier['/groupe_01'].attrs.create("test_dvp2",np.array([[12], [12]]))

Veuillez noter qu'il est également tout à fait possible de passer par le dictionnaire « attrs » afin de se simplifier la vie. Il ne vous sera cependant possible que de créer des attributs avec des valeurs basiques, h5py se chargeant de définir automatiquement les paramètres « shape » et « dtype ».

1. mon_fichier['/groupe_01'].attrs['demo_dvp_01'] = 'Hello World !!!'

S'agissant d'attributs, cette dernière méthode est celle à privilégier, et que vous utiliserez le plus souvent, sauf cas spécifiques.

Côté modifications, elles sont quelque peu limitées. En effet, seule la valeur peut être modifiée, via la méthode « modify », ou via l'utilisation du dictionnaire « attrs ».

1. mon_fichier['/groupe_01'].modify('demo_dvp_01', 'Hello Breizh !!!')

1. mon_fichier['/groupe_01'].attrs['demo_dvp_01'] = 'Hello Breizh !!!'

III-E-2 - Accès

Il existe deux principales façons d'avoir accès à un attribut.

La première méthode consiste à utiliser le dictionnaires « attrs » :

1. mon_fichier['/groupe_01'].attrs['demo_dvp_01']

La seconde, consiste à utiliser la méthode « get ». En effet, la première procédure, sous-entend que vous sachiez que l'attribut existe bien. Mais ce pourrait bien ne pas être le cas. La méthode « get » prendra une valeur par défaut qui vous sera renvoyée si l'attribut demandé n'existe pas.

1. Value = mon_fichier['/groupe_01'].get('demo_dvp', 'Cet attribut est inexistant.')

III-E-3 - Suppression

La suppression se fait de même que pour l'élément d'un dictionnaire standard :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. del mon_fichier['/groupe_01'].attrs['demo_dvp_01']

III-E-4 - Connaître la liste des attributs disponibles

En tant que dictionnaire, « attrs » vous autorise à connaître les clés disponibles, et donc à avoir accès à la liste d'attributs du groupe ou du dataset. Ainsi, il est aisé de faire :

1. import h5py

3. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

4. liste_attributs = [key for key in mon_fichier['/groupe_01'].attrs.keys()]

5. print(liste_attributs)

Des modification ayant eu lieu récemment, en 2016, dans le stockage des données, votre version de h5py peut temporairement être incapable de lire les attributs systèmes. Plusieurs bogues sont ouverts à ce sujet depuis le printemps (exemple). Afin de ne pas récupérer ces attributs systèmes, il vous suffit d'utiliser les lignes de code suivantes :

1. liste_attributs = [key for key in mon_fichier['/groupe_01'].keys()]

III-F - Exemple de l'utilité du format

En quelque sorte « base de données » orientée purement numérique, ce format autorise le traitement massif des informations qui y sont stockées, permettant ainsi un gain de temps énorme, et en même temps, un accès aisé aux données.

La manipulation de matrice de grande taille demandant beaucoup de ressources, il se peut donc que l'exemple ne fonctionne pas chez vous. Dans ce cas je vous invite à diminuer la taille de la matrice.

Pour cet exemple, la première chose que nous allons faire est de créer une matrice numpy de « 1 » de 10 000 par 10 000 (soit 100 000 000 de données potentielles à traiter). On enregistrera alors cette matrice au format hdf5. On effectuera ensuite un traitement de masse, dans ce fichier sur une partie des données.

1. import h5py

2. import time

3. import numpy as np

5. # Création d'une matrice

6. mon_tableau = np.ones((10000, 10000))

8. # création du fichier hdf5

9. mon_fichier = h5py.File('./mon_fichier.hdf5', 'a')

10. mon_dataset = mon_fichier.create_dataset("fichier de demo2", data=mon_tableau)

11. mon_fichier.flush()

13. # Maintenant les données numériques sont aisément accessible

14. start = time.time()

15. mon_dataset[:, 2] = 56 * mon_dataset[:, 3]

16. end = time.time()

17. print("10 000 données modifiées en {} secondes".format(end - start))

19. mon_fichier.close()

Comme on peux le constater, les données sont traitées très rapidement, et néanmoins disponibles dans un fichier unique aisément transmissible.

Un rapide coup d'œil aux caractéristiques du fichier vous apprendra que malgré la rapidité constatée sur la manipulation du fichier, il n'en pèse pas moins plus de 750 Mo.

IV - Conclusion

Comme nous venons de le voir, le format HDF5 est extrêmement adapté au traitement massif de données numériques.

Sous Python, son intégration est excellente, et la manipulation de données aisée grâce à NumPy.

Bien que ce tutoriel n'en soit qu'une introduction, j'espère néanmoins que la puissance de ce format vous permettra à l'avenir d'optimiser certains de vos développements logiciels.

V - Remerciements

- Malick SECK

- jacques_jean