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?
- 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.
- 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.
- 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:
- Kompresja gzip – http://docs.h5py.org/en/latest/high/dataset.html#lossless-compression-filters
- Składowanie w pliku w częściach – http://docs.h5py.org/en/latest/high/dataset.html#chunked-storage
- (Hard, Soft)Linki pomiędzy węzłami z strukturze hdf5 – http://docs.h5py.org/en/latest/high/group.html#dict-interface-and-links
- Równoległy dostęp do pliku – http://docs.h5py.org/en/latest/mpi.html#parallel-hdf5
- hdf5 + pandas
- PyTable – kolejna biblioteka do pracy z plikami hdf5
Kod projektu dostępny jest:
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ć?
@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ć.
Ok, mam sprawdzone. Konkluzja jest taka: czasem jest wszystko OK, czasem się rozsypuje.
Zwięzły, konkretny artykuł. Dzięki!
Dzięki za przystępne wprowadzenie do hdf5, bardzo mi pomogło 🙂
Nie ma sprawy, dzięki za komentarz 🙂