[Перевод] Как использовать HDF5-файлы в Python

Всем привет!

Приближается запуск курса «Web-разработчик на Python», соответственно, мы всё так же делимся интересными статьями и приглашаем на наши открытые уроки, где можно посмотреть интересный материал, познакомиться с преподавателями и позадавать им вопросы.

Поехали!

HDF5 позволяет эффективно хранить большие объемы данных

При работе с большими объемами данных, будь то экспериментальные или имитируемые, их хранение в нескольких текстовых файлах не очень эффективно. Иногда вам нужно получить доступ к конкретному подмножеству данных, и вы хотите сделать это быстро. В этих ситуациях формат HDF5 решает обе проблемы благодаря очень оптимизированной надстроенной библиотеке. HDF5 широко используется в научных средах и имеет отличную реализацию в Python, предназначенную для работы с NumPy прямо из коробки.

Формат HDF5 поддерживает файлы любого размера, и каждый файл имеет внутреннюю структуру, которая позволяет вам искать определенный набор данных. Это можно представить как отдельный файл со своей собственной иерархической структурой, так же как набор папок и подпапок. По умолчанию данные хранятся в двоичном формате, и библиотека совместима с разными типами данных. Одним из наиболее важных вариантов формата HDF5 является то, что он позволяет прикреплять метаданные к каждому элементу структуры, что делает его идеальным для создания автономных файлов.

t9aequcpmfwvbjckfxmnwcyevzo.png
В Python интерфейс с форматом HDF5 можно построить с помощью пакета h5py. Одной из наиболее интересных особенностей этого пакета является то, что данные считываются из файла только тогда, когда это необходимо. Представьте, что у вас очень большой массив, который не вмещается в вашу доступную оперативную память. Например, вы могли бы сгенерировать массив на компьютере с другими спецификациями, в отличии от того, который вы используете для анализа данных. Формат HDF5 позволяет вам выбирать, какие элементы массива следует читать с синтаксисом, эквивалентным NumPy. Затем вы можете работать с данными, хранящимися на жестком диске, а не в ОЗУ, без существенных изменений в существующем коде.

В этой статье мы рассмотрим, как вы можете использовать h5py для хранения и извлечения данных с жесткого диска. Мы обсудим различные способы хранения данных и как оптимизировать процесс чтения. Все примеры, которые появляются в этой статье, также доступны в нашем репозитории Github.

Установка

Формат HDF5 поддерживается HDF Group, и он основан на стандартах с открытым исходным кодом, что означает, что ваши данные всегда будут доступны, даже если группа исчезнет. Поддержка Python предоставляется через пакет h5py, который можно установить через pip. Помните, что вы должны использовать виртуальное окружение для проведения тестов:

pip install h5py


эта команда также установит NumPy, если ее нет в вашей среде.

Если вы ищете графический инструмент для изучения содержимого ваших HDF5-файлов, вы можете установить HDF5 Viewer. Он написан на Java, поэтому должен работать практически на любом компьютере.

Базовое сохранение и чтение данных

Давайте перейдем к использованию библиотеки HDF5. Мы создадим новый файл и сохраним в него случайный массив NumPy.

import h5py
import numpy as np

arr = np.random.randn(1000)

with h5py.File('random.hdf5', 'w') as f:
    dset = f.create_dataset("default", data=arr)


Первые несколько строк довольно просты: мы импортируем пакеты h5py и NumPy и создаем массив со случайными значениями. Мы открываем файл random.hdf5 с разрешением на запись w, это значит, что если файл с таким именем уже существует, то он будет перезаписан. Если вы хотите сохранить файл и по-прежнему иметь возможность в него записывать, вы можете открыть его с атрибутом a вместо w. Мы создаем набор данных с именем default и устанавливаем данные как случайный массив, созданный ранее. Наборы данных (dataset) являются хранителями наших данных, в основном строительных блоков формата HDF5.

Заметка

Если вы не знакомы с оператором with, я должен отметить, что это удобный способ открытия и закрытия файлов. Даже если внутри with случится ошибка, файл будет закрыт. Если по какой-то причине вы не используете with, никогда не забывайте добавлять в конец команду f.close(). Оператор with работает с любыми файлами, не только с файлами HDF.

Считать данные мы можем практически таким же образом, как мы читаем файл NumPy:


with h5py.File('random.hdf5', 'r') as f:
   data = f['default']
   print(min(data))
   print(max(data))
   print(data[:15])


Мы открываем файл с атрибутом считывания r и восстанавливаем данные, напрямую обращаясь к набору данных с именем default. Если вы открываете файл и не знаете, какие наборы данных доступны, вы можете получить их:

for key in f.keys():
   print(key)


После того, как вы считали набор данных, который хотели, вы можете использовать его, как если бы вы использовали любой массив NumPy. Например, вы можете узнать максимальные и минимальные значения или выбрать первые 15 значений массива. Эти простые примеры, однако, скрывают много вещей, которые происходят под капотом, и их необходимо обсудить, чтобы понять весь потенциал HDF5.

В приведенном выше примере вы можете использовать данные в виде массива. Например, вы можете обратиться к третьему элементу, введя data[2], или вы можете получить диапазон значений data[1:3]. Обратите внимание: данные не являются массивом, они — набор данных. Вы можете увидеть его, набрав print(type(data)). Наборы данных работают совершенно иначе, чем массивы, потому что их информация хранится на жестком диске, и они не загружают ее в ОЗУ, если мы их не используем. Следующий код, например, не будет работать:

f = h5py.File('random.hdf5', 'r')
data = f['default']
f.close()
print(data[1])


Ошибка, которая появляется, немного громоздка, но последняя строка очень полезна:

ValueError: Not a dataset (not a dataset)


Ошибка означает, что мы пытаемся получить доступ к набору данных, к которому у нас больше нет доступа. Это немного запутанно, но это происходит потому, что мы закрыли файл, и поэтому нам больше не разрешено получать доступ ко второму значению в данных. Когда мы назначили f ['default'] переменным данным, мы фактически не читаем данные из файла, вместо этого мы генерируем указатель на то, где данные находятся на жестком диске. С другой стороны, этот код будет работать:

f = h5py.File('random.hdf5', 'r')
data = f['default'][:]
f.close()
print(data[10])


Обратите внимание, единственное отличие состоит в том, что мы добавили [:] после прочтения набора данных. Многие другие руководства останавливаются на таких примерах, даже не демонстрируя полного потенциала формата HDF5 с пакетом h5py. Из-за примеров, которые мы рассматривали до сих пор, вы могли бы задаться вопросом: зачем использовать HDF5, если сохранение файлов NumPy дает вам такую же функциональность? Давайте погрузимся в особенности формата HDF5.

Выборочное чтение из HDF5-файлов

До сих пор мы видели, что когда мы читаем набор данных, мы еще не читаем данные с диска, вместо этого мы создаем ссылку на конкретное место на жестком диске. Мы можем видеть, что произойдет, если, например, мы явно прочитали первые 10 элементов набора данных:

with h5py.File('random.hdf5', 'r') as f:
   data_set = f['default']
   data = data_set[:10]

print(data[1])
print(data_set[1])


Мы разделяем код на разные строки, чтобы сделать его более явным, но вы можете быть более синтетичными в своих проектах. В строках выше мы сначала читаем файл, а затем читаем набор данных по умолчанию. Мы назначаем первые 10 элементов набора данных переменной data. После закрытия файла (когда он заканчивается), мы можем получить доступ к значениям, хранящимся в data, но data_set выдаст ошибку. Обратите внимание, что мы читаем только с диска, когда мы явно обращаемся к первым 10 элементам набора данных. Если вы посмотрите тип data и data_set, вы увидите, что они действительно разные. Первый — это массив NumPy, а второй — h5py DataSet.

Такое же поведение актуально и в более сложных сценариях. Давайте создадим новый файл, на этот раз с двумя наборами данных, и давайте выберем элементы одного из них на основе элементов другого. Начнем с создания нового файла и хранения данных; эта часть является самой простой:

import h5py
import numpy as np

arr1 = np.random.randn(10000)
arr2 = np.random.randn(10000)

with h5py.File('complex_read.hdf5', 'w') as f:
    f.create_dataset('array_1', data=arr1)
    f.create_dataset('array_2', data=arr2)


У нас есть два набора данных, названные array_1 и array_2, каждый из которых содержит в себе случайный массив NumPy. Мы хотим прочитать значения array_2, которые соответствуют элементам, где значения array_1 являются положительными. Мы можем попытаться сделать что-то вроде этого:

with h5py.File('complex_read.hdf5', 'r') as f:
    d1 = f['array_1']
    d2 = f['array_2']

    data = d2[d1>0]


но это не сработает. d1 — это набор данных и не может сравниться с целым числом. Единственный способ — фактически прочитать данные с диска, а затем сравнить их. Поэтому мы получим что-то вроде этого:

with h5py.File('complex_read.hdf5', 'r') as f:
    d1 = f['array_1']
    d2 = f['array_2']

    data = d2[d1[:]>0]


Первый набор данных d1 полностью загружается в память, когда мы делаем d1[:], но из второго набора данных d2 мы берем только некоторые элементы. Если набор данных d1 был бы слишком велик для загрузки в память целиком, мы могли бы работать внутри цикла.

with h5py.File('complex_read.hdf5', 'r') as f:
    d1 = f['array_1']
    d2 = f['array_2']

    data = []

    for i in range(len(d1)):
        if d1[i] > 0:
            data.append(d2[i])

print('The length of data with a for loop: {}'.format(len(data)))


Конечно, есть проблемы с эффективностью поэлементного чтения и добавления элементов в список, но это очень хороший пример одного из самых больших преимуществ использования HDF5 над текстовыми или NumPy-файлами. Внутри цикла мы загружаем в память только один элемент. В нашем примере каждый элемент представляет собой просто число, но это могло быть что угодно: от текста до изображения или видео.

Как всегда, в зависимости от вашего приложения вам придется решить, хотите ли вы прочитать весь массив в память или нет. Иногда вы запускаете симуляции на конкретном компьютере с большим объемом памяти, но у вас нет таких же характеристик на вашем ноутбуке, и вы вынуждены читать куски ваших данных. Помните, что чтение с жесткого диска происходит относительно медленно, особенно если вы используете HDD вместо SDD-дисков или даже дольше, если читаете с сетевого диска.

Выборочная запись в HDF5-файлы

В приведенных выше примерах мы добавляли данные в набор данных, как только он был создан. Однако для многих приложений вам необходимо сохранять данные во время генерации. HDF5 позволяет сохранять данные практически таким же образом, как вы их считываете. Давайте посмотрим, как создать пустой набор данных и добавить к нему некоторые данные.

arr = np.random.randn(100)

with h5py.File('random.hdf5', 'w') as f:
   dset = f.create_dataset("default", (1000,))
   dset[10:20] = arr[50:60]


Первые две строки такие же, как и раньше, за исключением create_dataset. Мы не добавляем данные при его создании, мы просто создаем пустой набор данных, способный удерживать до 1000 элементов. С той же логикой, что и раньше, когда мы читаем определенные элементы из набора данных, мы на самом деле записываем на диск только тогда, когда присваиваем значения определенным элементам переменной dset. В приведенном выше примере мы присваиваем значения только подмножеству массива, с индексами от 10 до 19.

Предупреждение

Не совсем верно, что вы пишете на диск, когда присваиваете значения набору данных. Точный момент зависит от нескольких факторов, включая состояние операционной системы. Если программа закрывается слишком рано, может случиться так, что не все будет записано. Очень важно всегда использовать метод close(), и в случае, если вы пишете поэтапно, вы также можете использовать flush(), чтобы форсировать запись. Использование with предотвращает множество проблем с записью.

Если вы прочитаете файл и распечатаете первые 20 значений набора данных, вы увидите, что все они нули, за исключением индексов с 10 по 19. Существует распространенная ошибка, которая может вылиться в ощутимую головную боль. Следующий код не сохранит ничего на диске:

arr = np.random.randn(1000)

with h5py.File('random.hdf5', 'w') as f:
   dset = f.create_dataset("default", (1000,))
   dset = arr


Эта ошибка всегда доставляет много проблем, потому что вы не поймете, что ничего не записали, пока не попытаетесь прочитать результат. Проблема здесь в том, что вы не указываете, где вы хотите хранить данные, вы просто перезаписываете переменную dset NumPy-массивом. Поскольку набор данных и массив имеют одинаковую длину, вы должны использовать dset[:] = arr. Эта ошибка случается чаще, чем вы думаете, и поскольку это технически не неверно, вы не увидите никаких ошибок, выведенных на терминал, а ваши данные будут нулями.

До сих пор мы всегда работали с одномерными массивами, но мы не ограничеными ими. Например, предположим, что мы хотим использовать 2D-массив, мы можем просто сделать:

dset = f.create_dataset('default', (500, 1024))


что позволит нам хранить данные в массиве 500×1024. Чтобы использовать набор данных, мы можем использовать тот же синтаксис, что и раньше, но с учетом второго измерения:

dset[1,2] = 1
dset[200:500, 500:1024] = 123


Указывайте типы данных для оптимизации пространства

До сих пор мы рассмотрели только верхушку айсберга того, что может предложить HDF5. Помимо длины данных, которые вы хотите сохранить, вы можете указать тип данных для оптимизации пространства. Документация h5py содержит список всех поддерживаемых типов, здесь мы покажем только пару из них. В то же время мы будем работать с несколькими наборами данных в одном файле.

with h5py.File('several_datasets.hdf5', 'w') as f:
   dset_int_1 = f.create_dataset('integers', (10, ), dtype='i1')
   dset_int_8 = f.create_dataset('integers8', (10, ), dtype='i8')
   dset_complex = f.create_dataset('complex', (10, ), dtype='c16')

   dset_int_1[0] = 1200
   dset_int_8[0] = 1200.1
   dset_complex[0] = 3 + 4j


В приведенном выше примере мы создали три разных набора данных, каждый из которых имеет другой тип. Целые числа из 1 байта, целые 8 байтов и комплексные числа из 16 байтов. Мы сохраняем только одно число, даже если наши наборы данных могут содержать до 10 элементов. Вы можете прочитать значения и посмотреть, что на самом деле было сохранено. Здесь следует отметить, что целое число из 1 байта должно быть округлено до 127 (вместо 1200), а целое число из 8 байтов должно быть округлено до 1200 (вместо 1200.1).

Если вы когда-либо программировали на таких языках, как C или Fortran, вы, вероятно, знаете, что означают разные типы данных. Однако, если вы всегда работали с Python, возможно, вы не сталкивались ни с какими проблемами, не декларируя явно тип данных, с которыми работаете. Важно помнить, что количество байт говорит вам, сколько разных чисел вы можете сохранить. Если вы используете 1 байт, у вас есть 8 бит, и поэтому вы можете хранить 2^8 разных чисел. В приведенном выше примере целые числа являются как положительными, отрицательными, так и 0. Когда вы используете целые числа в 1 байт, вы можете хранить значения от -128 до 127, в общей сложности они составляют 2^8 возможных чисел. Это эквивалентно использованию 8 байтов, но с большим диапазоном чисел.

Тип выбранных данных повлияет на его размер. Во-первых, давайте посмотрим, как это работает с простым примером. Создадим три файла, каждый с одним набором данных для 100000 элементов, но с разными типами данных. Мы сохраним в них те же данные, а затем сравним их размеры. Мы создаем случайный массив для назначения каждому набору данных, чтобы заполнить память. Помните, что данные будут преобразованы в формат, указанный в наборе данных.

arr = np.random.randn(100000)

f = h5py.File('integer_1.hdf5', 'w')
d = f.create_dataset('dataset', (100000,), dtype='i1')
d[:] = arr
f.close()

f = h5py.File('integer_8.hdf5', 'w')
d = f.create_dataset('dataset', (100000,), dtype='i8')
d[:] = arr
f.close()

f = h5py.File('float.hdf5', 'w')
d = f.create_dataset('dataset', (100000,), dtype='f16')
d[:] = arr
f.close()


Когда вы проверите размер каждого файла, вы получите что-то вроде:

Файл Размер (б)
integer_1 102144
integer_9 802144
float 1602144


Связь между размером и типом данных совершенно очевидна. Когда вы переходите от целых чисел от 1 байта до целых 8 байтов, размер файла увеличивается в 8 раз, аналогично, когда вы переходите к 16 байтам, это занимает примерно в 16 раз больше места. Но пространство — это не единственный важный фактор, который нужно учитывать, вы также должны учитывать время, необходимое для записи данных на диск. Чем больше вы должны писать, тем дольше это займет. В зависимости от вашего приложения может быть крайне важно оптимизировать чтение и запись данных.

Обратите внимание: если вы используете неправильный тип данных, вы также можете потерять информацию. Например, если у вас есть целые числа из 8 байт, и вы храните их как целые числа в 1 байт, их значения будут обрезаны. При работе в лаборатории очень часто в наличии находятся устройства, которые создают разные типы данных. Некоторые карты DAQ имеют 16 бит, некоторые камеры работают с 8 битами, но некоторые из них могут работать с 24. Важно обращать внимание на типы данных, но это также то, что разработчики Python могут не принимать во внимание, потому что вам не нужно явно объявлять тип.

Также интересно помнить, что NumPy-массив по умолчанию будет инициализирован float по 8 байтов (64 бит) на элемент. Это может быть проблемой, если, например, вы инициализируете массив с нулями для хранения данных, которые должны быть всего 2 байта. Тип самого массива не изменится, и если вы сохраните данные при создании набора данных (добавив data=my_array), то по умолчанию будет использоваться формат «f8», который является массивом, но не реальными данными,

Размышление о типах данных не является чем-то, что происходит регулярно, если вы работаете с Python в простых приложениях. Однако вы должны знать, что существуют типы данных и какое влияние они могут оказать на ваши результаты. Возможно, у вас большие жесткие диски, и вы особо не заботитесь о хранении файлов, но когда вы заботитесь о скорости, с которой вы сохраняете, нет другого способа, кроме оптимизации каждого аспекта вашего кода, включая типы данных.

Сжатие данных

При сохранении данных вы можете выбрать сжатие с использованием разных алгоритмов. Пакет h5py поддерживает несколько фильтров сжатия, таких как GZIP, LZF и SZIP. При использовании одного из фильтров сжатия данные будут обработаны на своем пути на диск, и при чтении они будут распаковываться. Поэтому особых изменений в работе кода нет. Мы можем повторить тот же эксперимент, сохраняя разные типы данных, но используя фильтр сжатия. Наш код выглядит так:

import h5py
import numpy as np

arr = np.random.randn(100000)

with h5py.File('integer_1_compr.hdf5', 'w') as f:
    d = f.create_dataset('dataset', (100000,), dtype='i1', compression="gzip", compression_opts=9)
    d[:] = arr

with h5py.File('integer_8_compr.hdf5', 'w') as f:
    d = f.create_dataset('dataset', (100000,), dtype='i8', compression="gzip", compression_opts=9)
    d[:] = arr

with h5py.File('float_compr.hdf5', 'w') as f:
    d = f.create_dataset('dataset', (100000,), dtype='f16', compression="gzip", compression_opts=9)
    d[:] = arr


Мы выбрали gzip, потому что он поддерживается на всех платформах. Параметры compression_opts задают уровень сжатия. Чем выше уровень, тем меньше пространства занимают данные, но тем дольше процессор должен работать. Уровень сжатия по умолчанию — 4. Мы можем видеть различия в наших файлах на основе уровня сжатия:

Тип Без сжатия Сжатие 9 Сжатие 4
integer_1 102144 28016 30463
integer_8 802144 43329 57971
float 1602144 1469580 1469868


Воздействие сжатия на целые массивы данных намного заметнее, чем на наборы данных с плавающей точкой. Оставляю вам самостоятельно разобраться, почему сжатие работало так хорошо в первых двух случаях, но не в последнем. В качестве подсказки: вы должны проверить, какие данные вы фактически сохраняете.

Чтение сжатых данных не изменяет никакого кода, описанного выше. Основная библиотека HDF5 позаботится об извлечении данных из сжатых наборов данных с помощью соответствующего алгоритма. Поэтому, если вы реализуете сжатие для сохранения, вам не нужно изменять код, который вы используете для чтения.

Сжатие данных — дополнительный инструмент, который вы должны учитывать вместе со всеми остальными аспектами обработки данных. Вы должны учитывать дополнительное время процессора и эффективную степень сжатия, чтобы оценить выгоду сжатия данных в пределах вашего собственного приложения. Тот факт, что оно прозрачно для нижестоящего кода, делает его невероятно легким для тестирования и поиска оптимального решения.

Изменение размеров наборов данных

Когда вы работаете над экспериментом, иногда невозможно узнать, насколько велики будут ваши данные. Представьте, что вы записываете фильм, возможно, вы остановите его через одну секунду, возможно, через час. К счастью, HDF5 позволяет изменять размеры наборов данных «на лету» с небольшими вычислительными затратами. Длина набора данных может быть превышена вплоть до достижения максимального размера. Этот максимальный размер указывается при создании набора данных с помощью ключевого слова maxshape:

import h5py
import numpy as np

with h5py.File('resize_dataset.hdf5', 'w') as f:
    d = f.create_dataset('dataset', (100, ),  maxshape=(500, ))
    d[:100] = np.random.randn(100)
    d.resize((200,))
    d[100:200] = np.random.randn(100)

with h5py.File('resize_dataset.hdf5', 'r') as f:
    dset = f['dataset']
    print(dset[99])
    print(dset[199])


Сначала вы создаете набор данных для хранения 100 значений и устанавливаете максимальный размер до 500 значений. После того, как вы сохранили первую партию значений, вы можете расширить набор данных, чтобы сохранить следующие 100. Вы можете повторять процедуру, пока не получите набор данных с 500 значениями. То же самое справедливо для массивов различных форм, любой размер N-мерной матрицы может быть изменен. Вы можете проверить, что данные были правильно сохранены, прочитав файл и напечатав два элемента в командной строке.

Вы также можете изменить размер набора данных на более позднем этапе, не нужно делать это в том же сеансе, когда вы создали файл. Например, вы можете сделать что-то подобное (обратите внимание на то, что мы открываем файл с атрибутом, чтобы не уничтожить предыдущий файл):

with h5py.File('resize_dataset.hdf5', 'a') as f:
    dset = f['dataset']
    dset.resize((300,))
    dset[:200] = 0
    dset[200:300] = np.random.randn(100)

with h5py.File('resize_dataset.hdf5', 'r') as f:
    dset = f['dataset']
    print(dset[99])
    print(dset[199])
    print(dset[299])


В приведенном выше примере вы можете увидеть, что мы открываем набор данных, изменяем его первые 200 значений и добавляем новые значения к элементам в позиции от 200 до 299. Чтение файла и печать некоторых значений доказывает, что он работал, как ожидалось.

Представьте, что вы приобретаете фильм, но вы не знаете, как долго он будет идти. Изображение представляет собой 2D-массив, каждый элемент которого является пикселем, а фильм — не что иное, как укладка нескольких 2D-массивов. Для хранения фильмов мы должны определить 3-мерный массив в нашем HDF-файле, но мы не хотим устанавливать ограничение на продолжительность. Чтобы иметь возможность расширять третью ось нашего набора данных без фиксированного максимума, мы можем сделать следующее:

with h5py.File('movie_dataset.hdf5', 'w') as f:
   d = f.create_dataset('dataset', (1024, 1024, 1),  maxshape=(1024, 1024, None ))
   d[:,:,0] = first_frame
   d.resize((1024,1024,2))
   d[:,:,1] = second_frame


Набор данных содержит квадратные изображения размером 1024×1024 пикселя, а третье измерение дает нам укладку по времени. Мы предполагаем, что изображения не изменяются по форме, мы хотели бы складывать одно за другим без установления предела. Вот почему мы установили maxshape третьего измерения в None.

Сохранение данных кусками (Chunks)

Чтобы оптимизировать хранение данных, вы можете хранить их кусками. Каждый кусок (chunk) будет смежным на жестком диске и будет храниться как блок, т.е. весь фрагмент будет записан сразу. Когда вы читаете кусок, произойдет то же самое, кусок данных будут загружен целиком. Чтобы создать кусочный набор данных, используем команду:

dset = f.create_dataset("chunked", (1000, 1000), chunks=(100, 100))


Эта команда означает, что все данные в dset [0: 100,0: 100] будут сохранены вместе. Это также верно для dset [200: 300, 200: 300], dset [100: 200, 400: 500] и т. Д. Согласно h5py, при использовании кусков есть некоторые последствия для производительности:

У разделения на куски (Chunking) есть последствия для производительности. Рекомендуется хранить общий размер ваших кусков между 10 KiB и 1 MiB, или больше для больших наборов данных. Также имейте в виду, что при обращении к любому элементу в куске, весь кусок считывается с диска.

Существует также возможность включения автоматической фрагментации (auto-chunking), которая будет автоматически выбирать оптимальный размер. Автоматическая фрагментация включена по умолчанию, если вы используете сжатие или maxshape. Явно вы это разрешаете так:

dset = f.create_dataset("autochunk", (1000, 1000), chunks=True)


Организация данных группами (Groups)

Мы видели много разных способов хранения и чтения данных. Теперь мы должны рассмотреть одну из последних важных тем HDF5, которая заключается в том, как организовать информацию в файле. Наборы данных могут быть размещены внутри групп (groups), которые ведут себя аналогично тому, как работают каталоги. Сначала мы можем создать группу, а затем добавить в нее набор данных:

import numpy as np
import h5py

arr = np.random.randn(1000)

with h5py.File('groups.hdf5', 'w') as f:
    g = f.create_group('Base_Group')
    gg = g.create_group('Sub_Group')

    d = g.create_dataset('default', data=arr)
    dd = gg.create_dataset('default', data=arr)


Мы создаем группу Base_Group и внутри нее создаем вторую, называемую Sub_Group. В каждой из групп мы создаем набор данных default и сохраняем в них случайный массив. Когда вы считаете файлы, вы заметите, как структурированы данные:

with h5py.File('groups.hdf5', 'r') as f:
   d = f['Base_Group/default']
   dd = f['Base_Group/Sub_Group/default']
   print(d[1])
   print(dd[1])


Как вы можете видеть, для доступа к набору данных мы адресуем его как папку в файле: Base_Group/default или Base_Group/Sub_Group/default. Когда вы читаете файл, возможно, вы не знаете, как были названы группы, и вам нужно их перечислить. Самый простой способ — использовать keys ():

with h5py.File('groups.hdf5', 'r') as f:
    for k in f.keys():
        print(k)


Однако, когда у вас есть вложенные группы, вам также нужно будет инициировать вложенные for-циклы. Существует лучший способ итерации через дерево, но это немного более хитро. Нам нужно использовать метод visit (), например:

def get_all(name):
   print(name)

with h5py.File('groups.hdf5', 'r') as f:
   f.visit(get_all)


Обратите внимание, что мы определяем функцию get_all, которая принимает один аргумент, name. Когда мы используем метод visit, в качестве аргумента он принимает функцию типа get_all. visit будет проходить через каждый элемент, и пока функция не вернет значение, отличное от None, он будет продолжать итерацию. Например, представьте, что мы ищем элемент под названием Sub_Group, мы должны изменить get_all:

def get_all(name):
    if 'Sub_Group' in name:
        return name

with h5py.File('groups.hdf5', 'r') as f:
    g = f.visit(get_all)
    print(g)


Когда visit повторяется через каждый элемент, как только функция возвращает то, что не является None, он остановится и вернет значение, которое генерируется get_all. Поскольку мы ищем Sub_Group, мы заставляем get_all возвращать имя группы, когда она находит Sub_Group как часть анализируемого имени. Имейте в виду, что g является строкой, если вы хотите фактически получить группу, вы должны сделать:

with h5py.File('groups.hdf5', 'r') as f:
   g_name = f.visit(get_all)
   group = f[g_name]


Вы также можете работать с группами как объяснялось ранее. Второй подход — использовать метод, называемый visititems, который принимает функцию с двумя аргументами: name и object. Мы можем сделать:

def get_objects(name, obj):
   if 'Sub_Group' in name:
      return obj

with h5py.File('groups.hdf5', 'r') as f:
   group = f.visititems(get_objects)
   data = group['default']
   print('First data element: {}'.format(data[0]))


Основное отличие при использовании visititems заключается в том, что мы получили доступ не только к имени объекта, который анализируется, но и к самому объекту. Вы можете видеть, что функция возвращает объект, а не имя. Этот шаблон позволяет получить более сложную фильтрацию. Например, вас могут заинтересовать группы, которые являются пустыми или имеют определенный тип набора данных.

Хранение метаданных в HDF5

Одним из аспектов, которые часто игнорируются в HDF5, является возможность хранения метаданных, прикрепленных к любой группе или набору данных. Метаданные имеют решающее значение, чтобы понять, например, откуда взялись данные, каковы параметры, используемые для измерения или моделирования, и т.д. Метаданные делают файл самоописательным. Представьте, что вы открываете старые данные, и находите матрицу размером 200×300x250. Возможно, вы знаете, что это фильм, но вы не знаете, какое измерение — время, а не временная привязка между кадрами.

Сохранение метаданных в файл HDF5 может быть совершено по-разному. Официальным методом является добавление атрибутов к группам и наборам данных.

import time
import numpy as np
import h5py
import os

arr = np.random.randn(1000)

with h5py.File('groups.hdf5', 'w') as f:
    g = f.create_group('Base_Group')
    d = g.create_dataset('default', data=arr)

    g.attrs['Date'] = time.time()
    g.attrs['User'] = 'Me'

    d.attrs['OS'] = os.name

    for k in g.attrs.keys():
        print('{} => {}'.format(k, g.attrs[k]))

    for j in d.attrs.keys():
      print('{} => {}'.format(j, d.attrs[j]))


В приведенном выше коде вы можете видеть, что attrs похож на словарь. В принципе, вы не должны использовать атрибуты для хранения данных, сохраняйте их как можно меньше. Однако вы не ограничены отдельными значениями, вы также можете хранить массивы. Если у вас есть метаданные, хранящиеся в словаре, и вы хотите автоматически добавить их в атрибуты, вы можете использовать update:

with h5py.File('groups.hdf5', 'w') as f:
   g = f.create_group('Base_Group')
   d = g.create_dataset('default', data=arr)

   metadata = {'Date': time.time(),
      'User': 'Me',
      'OS': os.name,}

   f.attrs.update(metadata)

   for m in f.attrs.keys():
      print('{} => {}'.format(m, f.attrs[m]))


Помните, что типы данных, поддерживаемые hdf5, ограничены. Например, словари не поддерживаются. Если вы хотите добавить словарь в файл hdf5, вам необходимо его сериализовать. В Python вы можете сериализовать словарь по-разному. В приведенном ниже примере мы собираемся сделать это с JSON, потому что он очень популярен в разных областях, но вы можете использовать все, что захотите, включая pickle.

import json

with h5py.File('groups_dict.hdf5', 'w') as f:
    g = f.create_group('Base_Group')
    d = g.create_dataset('default', data=arr)

    metadata = {'Date': time.time(),
                'User': 'Me',
                'OS': os.name,}

    m = g.create_dataset('metadata', data=json.dumps(metadata))


Начало такое же, мы создаем группу и набор данных. Для хранения метаданных мы определяем новый набор данных, соответственно называемый метаданными. Когда мы определяем данные, мы используем json.dumps, которые преобразуют словарь в длинную строку. Мы фактически храним строку, а не словарь в HDF5. Чтобы загрузить его обратно, нам нужно прочитать набор данных и преобразовать его обратно в словарь с помощью json.loads:

Python

with h5py.File('groups_dict.hdf5', 'r') as f:
    metadata = json.loads(f['Base_Group/metadata'][()])
    for k in metadata:
        print('{} => {}'.format(k, metadata[k]))


Когда вы используете json для кодирования ваших данных, вы определяете конкретный формат. Вы могли бы использовать YAML, XML и т.д. Поскольку может быть неясно, как загружать метаданные, хранящиеся таким образом, вы можете добавить атрибут attr набора данных, определяющий, какой путь сериализации вы использовали.

Заключительные мысли о HDF5

Во многих приложениях текстовых файлов более чем достаточно, они обеспечивают простой способ хранения данных и обмена ими с другими исследователями. Однако, как только объем информации увеличивается, вам нужно искать инструменты, которые лучше подходят нежели текстовые файлы. Одним из основных преимуществ формата HDF является то, что он самодостаточен, что означает, что сам файл содержит всю необходимую информацию для его чтения, включая информацию метаданных, которая позволяет воспроизводить результаты. Более того, формат HDF поддерживается в разных операционных системах и языках программирования.

Файлы HDF5 сложны и позволяют хранить в них много информации. Основное преимущество перед базами данных состоит в том, что они представляют собой автономные файлы, которые можно легко разделить. Базы данных нуждаются в целой системе для управления ими, их нельзя легко передавать и т. д. Если вы привыкли работать с SQL, вы можете попробовать проект HDFql, который позволяет использовать SQL для анализа данных из файла HDF5.

Хранение большого количества данных в одном файле потенциально повышает шанс порчи данных. Если ваш файл теряет свою целостность, например, из-за неисправного жесткого диска, трудно предсказать, сколько данных будет потеряно. Если вы сохраняете годы измерений в одном файле, вы подвергаете себя ненужным рискам. Более того, резервное копирование будет громоздким, потому что вы не сможете делать инкрементные резервные копии одного двоичного файла.

HDF5 — это формат, имеющий долгую историю и используемую многими исследователями. Вам потребуется немного времени, чтобы привыкнуть, и нужно будет поэкспериментировать некоторое время, пока не найдете способ, которым он может помочь вам сохранить ваши данные. HDF5 — хороший формат, если вам необходимо установить в своей лаборатории правила трансверсальности, касающиеся хранения данных и метаданных.

THE END

Как всегда ждём вопросы, предложения тут или может заглянуть к нам на открытый урок.

© Habrahabr.ru