Big Data na dysku, czyli jak przetwarzać pliki HDF5 w python

Czy nie macie problemu z przetwarzaniem ogromnego pliku z danymi, albo macie ogrom danych z czujników i nie wiecie jak je zapisać aby łatwo można było później je przetwarzać? Bo ja mam, znaczy miałem odkąd poznałem HDF5.

Biorąc się za jakąkolwiek robotę związaną z analizą danych mamy prosty workflow. Odczytujemy dane, wczytujemy je do pamięci następnie uruchamiamy stosowne algorytmy, dobieramy parametry i zbieramy wyniki. Jednak już na początku tego etapu możemy napotkać problemy. Co zrobić w sytuacji gdy mamy do wczytania ogromny plik, lub gdy mamy miliony małych plików? Jak to zmieścić w RAM’ie naszej maszyny? Odpowiedź jest prosta, wczytywać i przetwarzać dane partiami. Jednak takie podejście wymaga napisania dodatkowego kodu, jeżeli chcemy to zrobić ładnie i łatwo to z pomocą może nam przyjść HDF5.

HDF5 – system plików w pliku

HDF5 Hierarchical Data Format v5 jest standardem opisującym strukturę pliku, do którego w hierarchiczny i ustrukturyzowany sposób możemy zapisywać tablice danych. Jest to nic innego jak jeden wielki plik, któremu wewnętrznie możemy nadać strukturę systemu plików. Taki system plików w jednym pliku. Sam format jest ideologicznie prosty i API także nie jest skomplikowane. W ramach pliku mamy trzy obiekty, które możemy tworzyć:

  • Grupy – odpowiadają folderom w systemie plików
  • Datasety – odpowiadają plikom, z tym że mają postać macierzy o ustalonych wymiarach,
  • Atrybuty – metadane, pozwalające opisać grupę lub dataset (dodatkowa informacja np. autor, data stworzenie, z jakiego urządzenia pochodzą i co nam fantazja podpowie)

Ale nie to jest najistotniejsze, z mojej perspektywy ważny jest sposób dostępu do danych w tym pliku. Do danych zgromadzonych w ’datasecie’ możemy dostać się w poprzez interfejs identyczny z numpy. Czyli działa slicing, broadcasting i wszystkie cuda, które sprawiają, że wybieranie danych z tablic numpy jest tak intuicyjne. Najlepsze jest to, że operacje odbywają się na dysku więc wczytujemy do pamięci tylko to co w danym momencie jest nam potrzebne.

Wyobraźmy sobie, że mamy 80GB plik z danymi, mało kto ma aż tyle ramu aby wczytać go od razu do tablicy numpy i wybrać tylko te wiersze lub kolumny, które są interesujące. Natomiast gdyby ktoś zapisał nasze dane w formacie hdf5, to mamy możliwość otworzenia go a następnie w znany numpy-owy sposób dobrania się do wybranych wierszy. Całość dzieje się z wykorzystaniem dysku, nic nie jest ładowane do pamięci dopóki odpowiednie dane nie będą dostępne.

W czym może ci pomóc HDF5?

  1. Zbieraninie i przechowywanie danych z czujników bądź sensorów w formie jednego pliku, w którym wewnętrznie możemy po katalogować dane. Dzięki możliwości dołączenia metadanych możemy dodatkowo opisać poszczególne partie danych, ułatwia to archiwizację i przyszłe wykorzystanie.
  2. Przetwarzanie wielu małych plików. Zamiast pracować z tysiącami lub milionami małych plików możemy je zapisać w jednym pliku. Dzięki temu łatwiej możemy taki plik  łatwiej udostępnić, operacje kopiowania z dysku na dysk lub przez sieć wykonują się zdecydowanie szybciej.
  3. Interfejs API pozwalający dostać się do zawartości datasetu jest zgodny z numpy, dzięki temu posługujesz się znajomym API w dostępie do danych, twój kod jest czytelny i łatwiejszy w utrzymaniu. Wczytywanie kolejnych partii danych możemy zrealizować z wykorzystaniem znanego z numpy slicing’u (np. data[0:1000])

Tworzenie struktury pliku z wykorzystaniem h5py

Poniżej przedstawiam prosty skrypt, który pozwoli wam poznać podstawowe API tak aby móc rozpocząć pracę z hdf5. W pierwszej części tworzymy plik a w nim strukturę grup i datasetów.

W naszych przykładach skorzystamy z biblioteki pythonowej h5py, którą importujemy w pierwszych liniach skryptu. Następnie przy pomocy klauzuli with otwieramy plik do zapisu (Uwaga! Pliki hdf5 są bardzo wrażliwe na nie poprawne ich zamknięcie więc zalecane jest zawsze wykorzystanie with). Mając stworzony plik file.hdf5 możemy przejść do tworzenia struktury. Tutaj API biblioteki jest bardzo spójne i proste, wystarczy że zapoznamy się z dwoma funkcjami:

  • create_dataset – tworzy dataset, czyli nic innego jak tablicę o z góry określonych wymiarach i typie danych.  Możliwe jest utworzenie datasetu, który można rozszerzać (patrz następny przykład).  Podstawowe argumenty to name – nazwa dataset’u, shape –  wymiary tablicy (wiersze, kolumny), dtype –  typ danych, domyślnie float oraz ewentualnie data – czyli dane/tablicę numpy która ma być przechowywana.
  • create_group – tworzy grupę (folder), można podać całą ścieżkę w celu utworzenie podgrup.

Mam nadzieję że przykład jest na tyle prosty, że nie będę go dokładnie opisywał, zachęcam do przeanalizowania przykładu.

import h5py
import numpy as np
import datetime as dt

def printname(name):
        print name

with h5py.File('file.hdf5','w') as hf:
    
    #cretea new dataset and store numpy array
    dset = hf.create_dataset('my_data',(100,), dtype='i')
    dset[...] = np.arange(100)

    grp = hf.create_group('group1')
    #add some metadata to group
    grp.attrs['name'] = 'main group'
    grp.attrs['author'] = 'ksopyla'
    #create dataset in group1
    train = np.random.random([100,100])
    seg_ds = grp.create_dataset('train',data=train)
    
    
    #we can easily create nested groups
    sub_grp = grp.create_group('subgroup1/subgroup11')
    ones_arr = np.ones((250,5000))
    sub_ds = sub_grp.create_dataset('sensors', data=ones_arr)
    sub_ds.attrs['sensor type'] = 'sensor IO'
    sub_ds.attrs['date_taken'] = dt.datetime.now().isoformat()
    


with  h5py.File('file.hdf5','r') as hf:

    # show all objects in file
    hf.visit(printname)
    
    #get data set
    dset = hf.get('my_data')
    print(dset[0:10])
    
    #get data set, another way
    dset2 = hf['my_data']
    print(dset2[30:50])
    
    
    #get gruoup
    grp = hf['group1']
    grp.items()
    
    #iterate over attributes
    for item in grp.attrs.keys():
        print item + ":", grp.attrs[item]
    
    
    print(hf.keys())

Resize pliku Hdf5, pisanie w blokach

Prócz prostego przykładu podanego wyżej, chciałbym przedstawić wam także sposób zapisu do pliku dużej ilości danych w blokach/częściach (chunks). Mi to przydało się już kilka razy, gdy chciałem przepisać dużą ilość danych z wielu plików do jednego w formacie hdf5. Idea polega na tym, że zapisujemy dane w blokach do datasetu a następnie go rozszerzamy. Dzięki wykorzystaniu slicingu z numpy kod jest prosty i czytelny. Sądzę, że łatwo go będzie wam przerobić aby móc czytać z takiego dużego pliku.

Aby rozszerzanie było możliwe podczas tworzenia datasetu podaję dwa parametry shape=(CHUNK,ARR_SIZE)  oraz maxshape=(None,ARR_SIZE) pierwszy mówi w jakich kawałkach będziemy zapisywać dane, drugi określa maksymalny rozmiar datasetu. W tym przypadku None oznacza że dla tego wymiaru rozmiar nie jest okręślony, czyli możemy dodawać dane w nieskończoność (no prawie 🙂 ).

Kluczową częścią skryptu jest pętla for, w której rozszerzamy dataset dset.resize(row_count+rows, axis=0)  o kolejne wiersze. Zwróćcie uwagę, że podajemy do jakiego rozmiaru chcemy rozszerzyć a nie o ile. Stąd w pętli uaktualniam zmienną row_count, która zlicza ilość wierszy.

import h5py
import numpy as np
import time
import datetime as dt

def printname(name):
        print name

CHUNK = 100
MAX_CHUNKS=5000
ARR_SIZE = 1000

row_count = CHUNK


kbytes_write = CHUNK*ARR_SIZE*4*(MAX_CHUNKS/1024.0)
print('Write {} KB, {} MB'.format(kbytes_write, kbytes_write/1024))

start = dt.datetime.now()
with h5py.File('large.hdf5','w') as hf:
    dset = hf.create_dataset('data',
                        (CHUNK,ARR_SIZE),
                        maxshape=(None,ARR_SIZE), chunks=True)
    
    #write first chunk
    arr = np.ones((CHUNK, ARR_SIZE))
    dset[:]=arr
    
    #expand dataset and write next chunks in to the file
    for i in range(MAX_CHUNKS):
        arr = (i+2)*np.ones((CHUNK, ARR_SIZE))
        rows = arr.shape[0]     
        dset.resize(row_count+rows, axis=0)
        dset[row_count:]=arr
        row_count+= rows
 
duration = dt.datetime.now() - start
throughput = kbytes_write/(1024*duration.total_seconds())
print('Writing takes {} {}MB/s'.format(duration, throughput))
    
with h5py.File('large.hdf5','r') as hf
 hf.visit(printname)
 dset = hf.get('data')
 
 a = np.zeros((100,100))
 dset.read_direct(a, np.s_[0:10,0:10],np.s_[0:10,0:10])
 print(dset[80:110])
 print(hf.keys())
 print(dset.shape)

To czego nie poruszyłem

W tym tutorialu zależało mi na tym aby przybliżyć wam format HDF5, tak abyście mogli wykorzystać go szybko w jakimś z waszych projektów. Biblioteka h5py nie jest rozbudowana, lecz i tak z racji miejsca nie poruszyłem kilku kwestii:

Kod projektu dostępny jest:

7 Comments Big Data na dysku, czyli jak przetwarzać pliki HDF5 w python

  1. artofai.io

    Wygląda nieźle, natomiast obawiam się jednego problemu. Czasem zdarza mi się puścić jakiś model i zbierać informacje odnośnie tego jak się uczy. Potem stwierdzam, że już mu wystarczy bo i tak nic z tego nie będzie, więc go ubijam – ale dane z przebiegu chciałbym zachować. Zakładam, że w takim scenariuszu pik hdf5 może mi się całkiem rozsypać?

    Reply
    1. ksopyla

      @artofio.io plik hdf5 jest raczej formą archiwizacji danych, dzięki której jesteś w stanie logicznie pogrupować dane (w grupy i datasety) oraz później łatwo i przyjemnie się do nich dostać (numpy like interface).
      O ile dobrze cię rozumiem to chcesz wykorzystać plik hdf5 jako miejsce do przechowywanie logów z poszczególnych eksperymentów, bardzo dobry pomysł, wszystko w jednym miejscu. Sam muszę zastosować takie podejście przy dużej ilość eksperymentów wszystko będzie w jedenym pliku a nie kilkunastu, dodatkowo dzięki atrybutom można zapisać wszelkie parametry modelu (do tej pory zapisywałem to w nazwie pliku i wychodziły potworki w stylu model_it_34k_date_01.01.2017_eps_0.1 itd.

      Jeżeli mówisz o ubijaniu (kill -9) procesu to nie wiem jaki to ma skutek na otwarty plik hdf5, trzeba by przetestować.

Ciekawe, wartościowe, podziel się proszę opinią!

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.