Почему может возникнуть необходимость в изучение различных способов хранения и доступа к изображениям в Python? Так например если вам будет нужно классифицировать несколько изображений по цветам или найти лицо человека в изображении с помощью OpenCV вам не потребуется специальные способы работы с изображениями. Даже если вы используете Python Imaging Library (PIL) для рисования нескольких сотен фотографий, вам все равно это будет не нужно. В этих случаях достаточно обычного хранение изображений на диске в виде файлов .png или .jpg.
Однако число изображений, необходимых для выполнения определенных задач, может быть значительно большим. Алгоритмы, такие как сверточные нейронные сети, также известные как Connet или CNN, могут обрабатывать огромные наборы данных изображений и даже учиться на них. Если вам интересно, вы можете прочитать больше о том, как использовать сети Connet для ранжирования селфи или для анализа настроений.
ImageNet — это общедоступная база данных изображений, созданная для обучения моделей таким задачам, как классификация объектов, обнаружение и сегментация, и состоит из более чем 14 миллионов изображений.
Подумайте, сколько времени потребуется, чтобы загрузить их все в память для тренировок, партиями, возможно, сотни или тысячи раз. Позже вы убедитесь, что это займет довольно много времени — по крайней мере, достаточно, чтобы отойти от компьютера и заняться другими делами.
В этой статье вы узнаете о:
Вы также изучите следующее:
Давайте начнем!
Вам понадобится набор данных изображений для экспериментов, а также несколько пакетов Python.
Мы будем использовать набор данных изображений Канадского института перспективных исследований, более известный как CIFAR-10, который состоит из 60000 цветных изображений размером 32×32 пикселя, принадлежащих к различным классам объектов, таким как собаки, кошки и самолеты. Относительно CIFAR — не очень большой набор данных, но если бы мы использовали больший набор данных например TinyImages dataset, то в этом случае вам потребовалось бы около 400 ГБ свободного дискового пространства, что, вероятно, будет ограничивающим фактором.
Если вы хотите следовать примерам кода в этой статье, вы можете скачать CIFAR-10 здесь, выбрав нужную вам версию Python. Этот набор займет около 163 МБ дискового пространства:
Когда вы загрузите и разархивируете папку, вы обнаружите, что эти файлы не являются удобочитаемыми файлами изображений. Они будут фактически сериализованы и сохранены в пакетах с использованием cPickle.
Хотя в этой статье мы не будем рассматривать pickle или cPickle, кроме как для извлечения набора данных CIFAR, стоит отметить, что модуль pickle имеет ключевое преимущество, заключающееся в возможности сериализации любого объекта Python без какого-либо дополнительного кода. У него также есть потенциально серьезный недостаток: он создает угрозу безопасности и плохо справляется с очень большими объемами данных.
Следующий код распаковывает каждый из пяти пакетных файлов и загружает все изображения в массив NumPy:
import numpy as np import pickle from pathlib import Path # Path to the unzipped CIFAR data data_dir = Path("data/cifar-10-batches-py/") # Unpickle function provided by the CIFAR hosts def unpickle(file): with open(file, "rb") as fo: dict = pickle.load(fo, encoding="bytes") return dict images, labels = [], [] for batch in data_dir.glob("data_batch_*"): batch_data = unpickle(batch) for i, flat_im in enumerate(batch_data[b"data"]): im_channels = [] # Each image is flattened, with channels in order of R, G, B for j in range(3): im_channels.append( flat_im[j * 1024 : (j + 1) * 1024].reshape((32, 32)) ) # Reconstruct the original image images.append(np.dstack((im_channels))) # Save the label labels.append(batch_data[b"labels"][i]) print("Loaded CIFAR-10 training set:") print(f" - np.shape(images) {np.shape(images)}") print(f" - np.shape(labels) {np.shape(labels)}")
Все изображения теперь находятся в ОЗУ в переменной images с соответствующими метаданными в labels и готовы для манипулирования. Затем вы можете установить пакеты Python, которые вы будете использовать для трех методов.
Примечание: последний блок кода использовал f-строки. Вы можете прочитать больше о них в f-Strings в Python 3: An Improved String Formatting Syntax (Guide).
Вам нужно будет настроить свою среду на метод сохранения и доступа к этим изображениям с диска по умолчанию. В этой статье предполагается, что у вас установлен Python 3.x, и вы будете использовать Pillow для манипулирования изображениями:
$ pip install Pillow
Кроме того, если вы предпочитаете, вы можете установить его с помощью Anaconda:
$ conda install -c conda-forge pillow
Примечание: PIL — это оригинальная версия библиотеки изображений Python, которая больше не поддерживается и не совместима с Python 3.x. Если вы ранее установили PIL, обязательно удалите его перед установкой Pillow, поскольку они не могут существовать вместе.
Теперь вы готовы к хранению и чтению изображений с диска.
LMDB, иногда называют «Lightning Database», или «Lightning Memory-Mapped Database», потому что она быстрая и использует файлы с отображением в памяти. По сути представляет собой хранилище ключ-значение.
С точки зрения реализации, LMDB — это дерево B+, что в основном означает, что это древовидная структура графа, хранящаяся в памяти, где каждый элемент значения ключа является узлом, а узлы могут иметь много дочерних элементов. Узлы одного уровня связаны друг с другом для быстрого обхода.
Важно отметить, что ключевые компоненты дерева B+ соответствуют размеру страницы операционной системы хоста, что обеспечивает максимальную эффективность при доступе к любой паре ключ-значение в базе данных. Поскольку высокая производительность LMDB в значительной степени зависит от этого конкретного момента, эффективность LMDB, как было показано, зависит от базовой файловой системы и ее реализации.
Другой ключевой причиной эффективности LMDB является то, что она отображена в памяти. Это означает, что она возвращает прямые указатели на адреса памяти как ключей, так и значений, без необходимости что-либо копировать в памяти, как это делается в большинстве других баз данных.
Те, кто хочет углубиться в некоторые внутренние детали реализации деревьев B+, могут ознакомиться с этой статьей о деревьях B+, а затем поиграть с этой визуализацией вставки узлов.
Если B+ деревья вас не интересуют, не волнуйтесь. Вам не нужно много знать об их внутренней реализации, чтобы использовать LMDB. Мы будем использовать привязку Python для библиотеки LMDB, которую можно установить через pip:
$ pip install lmdb
У вас также есть возможность установки через Anaconda:
$ conda install -c conda-forge python-lmdb
HDF5 обозначает Иерархический формат данных, формат файла, называемый HDF4 или HDF5. Нам не нужно беспокоиться о HDF4, так как HDF5 является текущей поддерживаемой версией.
Интересно, что HDF берет свое начало в Национальном центре суперкомпьютерных приложений, как портативный, компактный формат научных данных. Если вам интересно, широко ли он используется, посмотрите на рекламный ролик НАСА о HDF5 из их проекта Earth Data.
Файлы HDF состоят из двух типов объектов:
Наборы данных (Datasets) являются многомерными массивами, а группы (Groups) состоят из наборов данных или других групп. Многомерные массивы любого размера и типа могут быть сохранены как набор данных, но размеры и тип должны быть одинаковыми в наборе данных. Каждый набор данных должен содержать однородный N-мерный массив. Тем не менее, поскольку группы и наборы данных могут быть вложенными, вы все равно можете получить гетерогенность, которая может вам понадобиться:
$ pip install h5py
Как и в случае с другими библиотеками, вы можете поочередно установить через Anaconda:
$ conda install -c conda-forge python-lmdb
Теперь, когда у вас есть общий обзор методов, давайте подробно рассмотрим количественное сравнение основных задач, которые нас интересуют: сколько времени занимает чтение и запись файлов и сколько дисковой памяти будет использоваться. Это также послужит базовым введением в работу методов и примерами их использования.
Когда я ссылаюсь на «файлы», я обычно имею в виду большую группу файлов. Однако важно учитывать различие, поскольку некоторые методы могут быть оптимизированы для различных операций и количества файлов.
В целях эксперимента мы можем сравнить производительность между различными количествами файлов, от 10 изображения до 100 000 изображений. Поскольку наши пять групп CIFAR-10 содержат до 50 000 изображений, мы можем использовать каждое изображение дважды, чтобы получить 100 000 изображений.
Чтобы подготовиться к экспериментам, вам нужно будет создать папку для каждого метода, которая будет содержать все файлы базы данных или изображения, и сохранить пути к этим каталогам в переменных:
from pathlib import Path disk_dir = Path("data/disk/") lmdb_dir = Path("data/lmdb/") hdf5_dir = Path("data/hdf5/")
Path не создает автоматически папки, если вы специально не укажите это:
disk_dir.mkdir(parents=True, exist_ok=True) lmdb_dir.mkdir(parents=True, exist_ok=True) hdf5_dir.mkdir(parents=True, exist_ok=True)
Теперь мы можем приступить к проведению реальных экспериментов с примерами кода того, как выполнять основные задачи с помощью трех различных методов. Мы можем использовать модуль timeit, который входит в стандартную библиотеку Python, чтобы помочь рассчитать время экспериментов.
Хотя основная цель этой статьи не состоит в изучении API различных пакетов Python, полезно иметь представление о том, как их можно реализовать. Мы пройдемся по общим принципам вместе со всем кодом, используемым для проведения экспериментов по хранению.
Входными данными для этого эксперимента является одно изображение image, которое в данный момент находится в памяти в виде массива NumPy. Сначала мы сохраним его на диск как изображение в формате .png и назвать его, используя уникальный идентификатор изображения image_id. Это можно сделать с помощью пакета Pillow, который вы установили ранее:
from PIL import Image import csv def store_single_disk(image, image_id, label): """ Stores a single image as a .png file on disk. Parameters: --------------- image image array, (32, 32, 3) to be stored image_id integer unique ID for image label image label """ Image.fromarray(image).save(disk_dir / f"{image_id}.png") with open(disk_dir / f"{image_id}.csv", "wt") as csvfile: writer = csv.writer( csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL ) writer.writerow([label])
Это функция просто сохраняет изображение. Во всех реалистичных приложениях вам также нужно позаботиться о метаданных, прикрепленных к изображению, которое в нашем примере набора данных является меткой изображения label. Когда вы сохраняете изображения на диск, есть несколько вариантов сохранения метаданных.
Одним из решений является кодирование меток в название изображения. Преимущество этого варианта в том, что он не требует никаких дополнительных файлов.
Тем не менее, он также имеет большой недостаток, заставляя вас иметь дело со всеми файлами всякий раз, когда вы делаете что-либо с метками. Хранение меток в отдельном файле позволяет вам работать только с метками, не загружая изображения. Выше я сохранил метки в отдельных файлах .csv для этого эксперимента.
Теперь давайте перейдем к выполнению той же задачи с LMDB.
Во-первых, LMDB — это система хранения значений ключей, в которой каждая запись сохраняется в виде байтового массива, поэтому в нашем случае ключи будут уникальным идентификатором для каждого изображения, а значением будет само изображение. Предполагается, что и ключи, и значения являются строками, поэтому обычно используется для сериализации значения в виде строки, а затем десериализации его при чтении.
Вы можете использовать pickle для сериализации. Любой объект Python может быть сериализован, поэтому вы также можете включить метаданные изображения в базу данных. Это избавит вас от необходимости прикреплять метаданные обратно к данным изображения, когда мы загружаем набор данных с диска.
Вы можете создать базовый класс Python для изображения и его метаданных:
class CIFAR_Image: def __init__(self, image, label): # Dimensions of image for reconstruction - not really necessary # for this dataset, but some datasets may include images of # varying sizes self.channels = image.shape[2] self.size = image.shape[:2] self.image = image.tobytes() self.label = label def get_image(self): """ Returns the image as a numpy array. """ image = np.frombuffer(self.image, dtype=np.uint8) return image.reshape(*self.size, self.channels)
Во-вторых, поскольку LMDB отображается в памяти, новые базы данных должны знать, сколько памяти они будут использовать. В нашем случае это относительно просто, но в других случаях это может быть серьезной болью, которую мы рассмотрим более подробно в следующем разделе. LMDB вызывает переменную map_size.
Наконец, операции чтения и записи с LMDB выполняются в транзакциях. Вы можете думать о них, как о аналогах традиционной базы данных, состоящей из группы операций над базой данных.
Помня об этих трех моментах, давайте рассмотрим код для сохранения одного изображения в LMDB:
import lmdb import pickle def store_single_lmdb(image, image_id, label): """ Stores a single image to a LMDB. Parameters: --------------- image image array, (32, 32, 3) to be stored image_id integer unique ID for image label image label """ map_size = image.nbytes * 10 # Create a new LMDB environment env = lmdb.open(str(lmdb_dir / f"single_lmdb"), map_size=map_size) # Start a new write transaction with env.begin(write=True) as txn: # All key-value pairs need to be strings value = CIFAR_Image(image, label) key = f"{image_id:08}" txn.put(key.encode("ascii"), pickle.dumps(value)) env.close()
Примечание. Рекомендуется рассчитать точное число байтов, которое займет каждая пара ключ-значение.
С набором данных изображений различного размера это будет приближенно, но вы можете использовать sys.getsizeof() для получения разумного приближения. Помните, что sys.getsizeof(CIFAR_Image) будет возвращать только размер определения класса, равный 1056, а не размер экземпляра объекта.
Функция также не сможет полностью рассчитать вложенные элементы, списки или объекты, содержащие ссылки на другие объекты.
Кроме того, вы можете использовать pympler, чтобы сэкономить некоторые расчеты, определив точный размер объекта.
Наконец, давайте посмотрим на последний метод, HDF5.
Помните, что файл HDF5 может содержать более одного набора данных. В нашем довольно тривиальном случае вы можете создать два набора данных, один для изображения и один для его метаданных:
import h5py def store_single_hdf5(image, image_id, label): """ Stores a single image to an HDF5 file. Parameters: --------------- image image array, (32, 32, 3) to be stored image_id integer unique ID for image label image label """ # Create a new HDF5 file file = h5py.File(hdf5_dir / f"{image_id}.h5", "w") # Create a dataset in the file dataset = file.create_dataset( "image", np.shape(image), h5py.h5t.STD_U8BE, data=image ) meta_set = file.create_dataset( "meta", np.shape(label), h5py.h5t.STD_U8BE, data=label ) file.close()
h5py.h5t.STD_U8BE указывает тип данных, которые будут храниться в наборе данных, в нашем случае представляет собой 8-разрядные целые числа без знака. Вы можете увидеть полный список предопределенных типов данных HDF здесь.
Примечание. Выбор типа данных сильно повлияет на требования к времени выполнения и хранилищу HDF5, поэтому лучше выбрать минимальные требования.
Теперь, когда мы рассмотрели три метода сохранения одного изображения, давайте перейдем к следующему шагу.
Теперь вы можете поместить все три функции для сохранения одного изображения в словарь, который можно вызвать позже во время экспериментов по синхронизации:
_store_single_funcs = dict( disk=store_single_disk, lmdb=store_single_lmdb, hdf5=store_single_hdf5 )
Наконец, все готово для проведения запланированного эксперимента. Давайте попробуем сохранить первое изображение из CIFAR и соответствующий ему label и сохранить его тремя различными способами:
from timeit import timeit store_single_timings = dict() for method in ("disk", "lmdb", "hdf5"): t = timeit( "_store_single_funcs[method](image, 0, label)", setup="image=images[0]; label=labels[0]", number=1, globals=globals(), ) store_single_timings[method] = t print(f"Method: {method}, Time usage: {t}")
Примечание. Во время работы с LMDB вы можете увидеть ошибку MapFullError: mdb_txn_commit: MDB_MAP_FULL: Environment mapsize limit reached
error. Важно отметить, что LMDB не перезаписывает существующие значения, даже если они имеют одинаковый ключ.
Это способствует быстрому времени записи, но также означает, что если вы храните изображение более одного раза в одном и том же файле LMDB, то вы будете использовать размер карты. Если вы запускаете функцию хранилища, сначала обязательно удалите все существующие файлы LMDB.
Помните, что нас интересует время выполнения, отображаемое здесь в секундах, а также использование памяти:
Method | Save Single Image + Meta | Memory |
---|---|---|
Disk | 1.915 ms | 8 K |
LMDB | 1.203 ms | 32 K |
HDF5 | 8.243 ms | 8 K |
Здесь есть два момента:
Очевидно, что, несмотря на незначительное снижение производительности LMDB, мы никого не убедили, почему бы не просто хранить образы на диске. В конце концов, это удобочитаемый формат, и вы можете открывать и просматривать их из любого браузера файловой системы! Ну что ж, пришло время посмотреть на еще больше количество изображений …
Вы видели код для использования различных методов хранения для сохранения одного изображения, поэтому теперь нам нужно создать код, чтобы сохранить множество изображений, а затем запустить эксперимент.
Сохранение нескольких изображений в виде файлов .png так же просто, как многократный вызов store_single_method(). Но это не так для LMDB или HDF5, поскольку вам не нужен отдельный файл базы данных для каждого изображения. Скорее всего, вы хотите поместить все изображения в один или несколько файлов.
Вам нужно будет немного изменить код и создать три новые функции, которые принимают несколько изображений: store_many_disk(), store_many_lmdb() и store_many_hdf5():
store_many_disk(images, labels): """ Stores an array of images to disk Parameters: --------------- images images array, (N, 32, 32, 3) to be stored labels labels array, (N, 1) to be stored """ num_images = len(images) # Save all the images one by one for i, image in enumerate(images): Image.fromarray(image).save(disk_dir / f"{i}.png") # Save all the labels to the csv file with open(disk_dir / f"{num_images}.csv", "w") as csvfile: writer = csv.writer( csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL ) for label in labels: # This typically would be more than just one value per row writer.writerow([label]) def store_many_lmdb(images, labels): """ Stores an array of images to LMDB. Parameters: --------------- images images array, (N, 32, 32, 3) to be stored labels labels array, (N, 1) to be stored """ num_images = len(images) map_size = num_images * images[0].nbytes * 10 # Create a new LMDB DB for all the images env = lmdb.open(str(lmdb_dir / f"{num_images}_lmdb"), map_size=map_size) # Same as before — but let's write all the images in a single transaction with env.begin(write=True) as txn: for i in range(num_images): # All key-value pairs need to be Strings value = CIFAR_Image(images[i], labels[i]) key = f"{i:08}" txn.put(key.encode("ascii"), pickle.dumps(value)) env.close() def store_many_hdf5(images, labels): """ Stores an array of images to HDF5. Parameters: --------------- images images array, (N, 32, 32, 3) to be stored labels labels array, (N, 1) to be stored """ num_images = len(images) # Create a new HDF5 file file = h5py.File(hdf5_dir / f"{num_images}_many.h5", "w") # Create a dataset in the file dataset = file.create_dataset( "images", np.shape(images), h5py.h5t.STD_U8BE, data=images ) meta_set = file.create_dataset( "meta", np.shape(labels), h5py.h5t.STD_U8BE, data=labels ) file.close()
Таким образом, вы можете хранить более одного файла на диске, метод файлов изображений был изменен для циклического перебора каждого изображения в списке. Для LMDB также необходим цикл, так как мы создаем объект CIFAR_Image для каждого изображения и его метаданных.
Наименьшие изменения в методе HDF5. На самом деле, изменений почти нет! Файлы HFD5 не имеют ограничений по размеру файла, кроме внешних ограничений или размера набора данных, поэтому все изображения были помещены в один набор данных, как и раньше.
Далее вам нужно будет подготовить набор данных для экспериментов, увеличив его размер.
Прежде чем снова проводить эксперименты, давайте сначала удвоим размер нашего набора данных, чтобы мы могли протестировать до 100 000 изображений:
cutoffs = [10, 100, 1000, 10000, 100000] # Let's double our images so that we have 100,000 images = np.concatenate((images, images), axis=0) labels = np.concatenate((labels, labels), axis=0) # Make sure you actually have 100,000 images and labels print(np.shape(images)) print(np.shape(labels))
Теперь, когда изображений достаточно, пришло время для эксперимента.
Как и в случае с чтением множества изображений, вы можете создать словарь для обработки всех функций с помощью store_many_ и запустить тесты:
_store_many_funcs = dict( disk=store_many_disk, lmdb=store_many_lmdb, hdf5=store_many_hdf5 ) from timeit import timeit store_many_timings = {"disk": [], "lmdb": [], "hdf5": []} for cutoff in cutoffs: for method in ("disk", "lmdb", "hdf5"): t = timeit( "_store_many_funcs[method](images_, labels_)", setup="images_=images[:cutoff]; labels_=labels[:cutoff]", number=1, globals=globals(), ) store_many_timings[method].append(t) # Print out the method, cutoff, and elapsed time print(f"Method: {method}, Time usage: {t}")
Если вы следите за выполнением кода и запускаете его самостоятельно, вам придется немного подождать, пока 111 110 изображений сохранятся по три раза на ваш диск в трех разных форматах. Вам также придется попрощаться примерно с 2 ГБ дискового пространства.
Теперь момент истины! Сколько времени заняло все это хранение? Одна картинка стоит тысячи слов:
На первом графике показано нормальное нескорректированное время хранения, подчеркивающее существенную разницу между хранением в файлах .png и LMDB или HDF5.
Второй график показывает логарифм временных интервалов, подчеркивая, что HDF5 запускается медленнее, чем LMDB, но с большим количеством изображений выходит немного впереди.
Хотя точные результаты могут отличаться в зависимости от вашей машины, именно поэтому стоит подумать о LMDB и HDF5. Вот код, который сгенерировал приведенный выше график:
import matplotlib.pyplot as plt def plot_with_legend( x_range, y_data, legend_labels, x_label, y_label, title, log=False ): """ Displays a single plot with multiple datasets and matching legends. Parameters: -------------- x_range list of lists containing x data y_data list of lists containing y values legend_labels list of string legend labels x_label x axis label y_label y axis label """ plt.style.use("seaborn-whitegrid") plt.figure(figsize=(10, 7)) if len(y_data) != len(legend_labels): raise TypeError( "Error: number of data sets does not match number of labels." ) all_plots = [] for data, label in zip(y_data, legend_labels): if log: temp, = plt.loglog(x_range, data, label=label) else: temp, = plt.plot(x_range, data, label=label) all_plots.append(temp) plt.title(title) plt.xlabel(x_label) plt.ylabel(y_label) plt.legend(handles=all_plots) plt.show() # Getting the store timings data to display disk_x = store_many_timings["disk"] lmdb_x = store_many_timings["lmdb"] hdf5_x = store_many_timings["hdf5"] plot_with_legend( cutoffs, [disk_x, lmdb_x, hdf5_x], ["PNG files", "LMDB", "HDF5"], "Number of images", "Seconds to store", "Storage time", log=False, ) plot_with_legend( cutoffs, [disk_x, lmdb_x, hdf5_x], ["PNG files", "LMDB", "HDF5"], "Number of images", "Seconds to store", "Log storage time", log=True, )
Теперь давайте перейдем к чтению изображений.
Во-первых, давайте рассмотрим случай чтения одного изображения обратно в массив для каждого из трех методов.
Из трех методов LMDB требует больше усилий при чтении файлов изображений из-за шага сериализации. Давайте рассмотрим эти функции, которые считывают одно изображение для каждого из трех форматов хранения.
Сначала загрузим одно изображение и его метаданные из файлов .png и .csv:
def read_single_disk(image_id): """ Stores a single image to disk. Parameters: --------------- image_id integer unique ID for image Returns: ---------- image image array, (32, 32, 3) to be stored label associated meta data, int label """ image = np.array(Image.open(disk_dir / f"{image_id}.png")) with open(disk_dir / f"{image_id}.csv", "r") as csvfile: reader = csv.reader( csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL ) label = int(next(reader)[0]) return image, label
Затем загрузим то же изображение и мета из LMDB, открыв среду и запустив транзакцию чтения:
1 def read_single_lmdb(image_id): 2 """ Stores a single image to LMDB. 3 Parameters: 4 --------------- 5 image_id integer unique ID for image 6 7 Returns: 8 ---------- 9 image image array, (32, 32, 3) to be stored 10 label associated meta data, int label 11 """ 12 # Open the LMDB environment 13 env = lmdb.open(str(lmdb_dir / f"single_lmdb"), readonly=True) 14 15 # Start a new read transaction 16 with env.begin() as txn: 17 # Encode the key the same way as we stored it 18 data = txn.get(f"{image_id:08}".encode("ascii")) 19 # Remember it's a CIFAR_Image object that is loaded 20 cifar_image = pickle.loads(data) 21 # Retrieve the relevant bits 22 image = cifar_image.get_image() 23 label = cifar_image.label 24 env.close() 25 26 return image, label
Вот пара моментов, которые не касаются фрагмента кода выше:
Это завершает чтение изображения обратно из LMDB. Наконец, вы захотите сделать то же самое с HDF5.
Чтение из HDF5 выглядит очень похоже на процесс записи. Вот код для открытия и чтения файла HDF5 и анализа того же изображения и мета данных:
def read_single_hdf5(image_id): """ Stores a single image to HDF5. Parameters: --------------- image_id integer unique ID for image Returns: ---------- image image array, (32, 32, 3) to be stored label associated meta data, int label """ # Open the HDF5 file file = h5py.File(hdf5_dir / f"{image_id}.h5", "r+") image = np.array(file["/image"]).astype("uint8") label = int(np.array(file["/meta"]).astype("uint8")) return image, label
Обратите внимание, что вы обращаетесь к различным наборам данных в файле, индексируя объект file, используя имя набора данных, которому предшествует косая черта /. Как и раньше, вы можете создать словарь, содержащий все функции чтения:
_read_single_funcs = dict( disk=read_single_disk, lmdb=read_single_lmdb, hdf5=read_single_hdf5 )
Подготовив этот словарь, вы готовы к проведению эксперимента.
Вы можете ожидать, что эксперимент для чтения одного изображения будет иметь несколько предсказуемые результаты, но вот код эксперимента:
from timeit import timeit read_single_timings = dict() for method in ("disk", "lmdb", "hdf5"): t = timeit( "_read_single_funcs[method](0)", setup="image=images[0]; label=labels[0]", number=1, globals=globals(), ) read_single_timings[method] = t print(f"Method: {method}, Time usage: {t}")
Вот результаты эксперимента по чтению одного изображения:
Method | Read Single Image + Meta |
---|---|
Disk | 1.61970 ms |
LMDB | 4.52063 ms |
HDF5 | 1.98036 ms |
Чтение файлов .png и .csv непосредственно с диска происходит немного быстрее, но все три метода работают достаточно быстро. Эксперименты, которые мы сделаем дальше, намного интереснее.
Теперь вы можете настроить код для чтения сразу нескольких изображений. Скорее всего, это действие, которое вы будете выполнять чаще всего, поэтому производительность среды выполнения очень важна.
Расширяя приведенные выше функции, вы можете создавать функции с read_many_, которые можно использовать для следующих экспериментов. Как и прежде, интересно сравнить производительность при чтении разных количеств изображений, которые повторяются в приведенном ниже коде:
def read_many_disk(num_images): """ Reads image from disk. Parameters: --------------- num_images number of images to read Returns: ---------- images images array, (N, 32, 32, 3) to be stored labels associated meta data, int label (N, 1) """ images, labels = [], [] # Loop over all IDs and read each image in one by one for image_id in range(num_images): images.append(np.array(Image.open(disk_dir / f"{image_id}.png"))) with open(disk_dir / f"{num_images}.csv", "r") as csvfile: reader = csv.reader( csvfile, delimiter=" ", quotechar="|", quoting=csv.QUOTE_MINIMAL ) for row in reader: labels.append(int(row[0])) return images, labels def read_many_lmdb(num_images): """ Reads image from LMDB. Parameters: --------------- num_images number of images to read Returns: ---------- images images array, (N, 32, 32, 3) to be stored labels associated meta data, int label (N, 1) """ images, labels = [], [] env = lmdb.open(str(lmdb_dir / f"{num_images}_lmdb"), readonly=True) # Start a new read transaction with env.begin() as txn: # Read all images in one single transaction, with one lock # We could split this up into multiple transactions if needed for image_id in range(num_images): data = txn.get(f"{image_id:08}".encode("ascii")) # Remember that it's a CIFAR_Image object # that is stored as the value cifar_image = pickle.loads(data) # Retrieve the relevant bits images.append(cifar_image.get_image()) labels.append(cifar_image.label) env.close() return images, labels def read_many_hdf5(num_images): """ Reads image from HDF5. Parameters: --------------- num_images number of images to read Returns: ---------- images images array, (N, 32, 32, 3) to be stored labels associated meta data, int label (N, 1) """ images, labels = [], [] # Open the HDF5 file file = h5py.File(hdf5_dir / f"{num_images}_many.h5", "r+") images = np.array(file["/images"]).astype("uint8") labels = np.array(file["/meta"]).astype("uint8") return images, labels _read_many_funcs = dict( disk=read_many_disk, lmdb=read_many_lmdb, hdf5=read_many_hdf5 )
С функциями чтения, хранящимися в словаре, как и с функциями записи, вы готовы к эксперименту.
Теперь вы можете запустить эксперимент для чтения множества изображений:
from timeit import timeit read_many_timings = {"disk": [], "lmdb": [], "hdf5": []} for cutoff in cutoffs: for method in ("disk", "lmdb", "hdf5"): t = timeit( "_read_many_funcs[method](num_images)", setup="num_images=cutoff", number=1, globals=globals(), ) read_many_timings[method].append(t) # Print out the method, cutoff, and elapsed time print(f"Method: {method}, No. images: {cutoff}, Time usage: {t}")
Как и раньше, вы можете построить график результатов эксперимента:
Верхний график показывает нормальное, нескорректированное время чтения, показывая резкое различие между чтением из файлов .png и LMDB или HDF5.
Напротив, график внизу показывает логарифм времени, выделяя относительные различия с меньшим количеством изображений. А именно, мы можем видеть, как HDF5 начинает отставать, но с большим количеством изображений становится стабильно быстрее, чем LMDB, с небольшим отрывом.
Используя ту же функцию построения графиков, что и для времени записи, мы имеем следующее:
disk_x_r = read_many_timings["disk"] lmdb_x_r = read_many_timings["lmdb"] hdf5_x_r = read_many_timings["hdf5"] plot_with_legend( cutoffs, [disk_x_r, lmdb_x_r, hdf5_x_r], ["PNG files", "LMDB", "HDF5"], "Number of images", "Seconds to read", "Read time", log=False, ) plot_with_legend( cutoffs, [disk_x_r, lmdb_x_r, hdf5_x_r], ["PNG files", "LMDB", "HDF5"], "Number of images", "Seconds to read", "Log read time", log=True, )
На практике время записи часто менее критично, чем время чтения. Представьте, что вы тренируете глубокую нейронную сеть на изображениях, и только половина всего вашего набора данных изображений помещается в ОЗУ одновременно. Каждая эпоха обучения сети требует всего набора данных, а модели требуется несколько сотен эпох, чтобы сходиться. По сути, вы будете читать половину набора данных в память каждую эпоху.
Есть несколько уловок, которые делают люди, такие как обучение псевдо-эпох, чтобы сделать это немного лучше, но вы поняли идею.
Теперь, посмотрите еще раз на прочитанный график выше. Разница между 40-секундным и 4-секундным временем чтения внезапно становится разницей между ожиданием тренировки модели на шесть часов или на сорок минут!
Если мы посмотрим время чтения и записи на одном графике, у нас будет следующее:
Вы можете построить все данные чтения и записи на одном графике, используя одну и ту же функцию построения графика:
plot_with_legend( cutoffs, [disk_x_r, lmdb_x_r, hdf5_x_r, disk_x, lmdb_x, hdf5_x], [ "Read PNG", "Read LMDB", "Read HDF5", "Write PNG", "Write LMDB", "Write HDF5", ], "Number of images", "Seconds", "Log Store and Read Times", log=False, )
Когда вы храните изображения в виде файлов .png, существует большая разница между временем записи и чтения. Однако, с LMDB и HDF5, разница гораздо менее заметна. В целом, даже если время чтения более критично, чем время записи, существует веский аргумент в пользу хранения изображений с использованием LMDB или HDF5.
Теперь, когда вы увидели преимущества LMDB и HDF5 в производительности, давайте рассмотрим еще один важный показатель: использование диска.
Скорость — не единственная метрика производительности, которая вас может заинтересовать. Мы уже имеем дело с очень большими наборами данных, поэтому дисковое пространство также очень актуально.
Предположим, у вас есть набор данных изображения 3 ТБ. Предположительно, они уже есть где-то на диске, в отличие от нашего примера CIFAR, поэтому, используя альтернативный метод хранения, вы, по сути, делаете их копию, которая также должна храниться. Это даст вам огромный выигрыш в производительности при использовании образов, но вам нужно убедиться, что у вас достаточно места на диске.
Сколько дискового пространства используют различные методы хранения? Вот дисковое пространство, используемое для каждого метода для каждого количества изображений:
Я использовал команду Linux du -h -c folder_name / * для вычисления использования дискового пространства в моей системе. Существует некоторое приближение, присущее этому методу из-за округления, но вот общее сравнение:
# Memory used in KB disk_mem = [24, 204, 2004, 20032, 200296] lmdb_mem = [60, 420, 4000, 39000, 393000] hdf5_mem = [36, 304, 2900, 29000, 293000] X = [disk_mem, lmdb_mem, hdf5_mem] ind = np.arange(3) width = 0.35 plt.subplots(figsize=(8, 10)) plots = [plt.bar(ind, [row[0] for row in X], width)] for i in range(1, len(cutoffs)): plots.append( plt.bar( ind, [row[i] for row in X], width, bottom=[row[i - 1] for row in X] ) ) plt.ylabel("Memory in KB") plt.title("Disk memory used by method") plt.xticks(ind, ("PNG", "LMDB", "HDF5")) plt.yticks(np.arange(0, 400000, 100000)) plt.legend( [plot[0] for plot in plots], ("10", "100", "1,000", "10,000", "100,000") ) plt.show()
И HDF5, и LMDB занимают больше места на диске, чем при хранении с использованием обычных изображений .png. Важно отметить, что использование и производительность дисков LMDB и HDF5 сильно зависят от различных факторов, включая операционную систему и, что более важно, размер хранимых данных.
LMDB получает свою эффективность от кэширования и использования размеров страниц ОС. Вам не нужно понимать его внутреннюю работу, но имейте в виду, что с большими изображениями у вас будет значительно больше использования диска с LMDB, потому что изображения не поместятся на листовых страницах LMDB, обычном месте хранения в дереве, и вместо этого у вас будет много переполненных страниц.
Наши изображения размером 32x32x3 пикселя относительно малы по сравнению со средними изображениями, которые вы можете использовать, и они обеспечивают оптимальную производительность LMDB.
Хотя мы не будем исследовать это здесь экспериментально, по моему опыту с изображениями 256x256x3 или 512x512x3 пикселей HDF5 обычно немного более эффективен с точки зрения использования диска, чем LMDB.
Есть и другие отличительные особенности LMDB и HDF5, о которых стоит знать, и также важно кратко обсудить некоторые критические замечания обоих методов.
Ключевое сравнение, которое мы не тестировали в приведенных выше экспериментах, — это одновременное чтение и запись. Часто с такими большими наборами данных вам может потребоваться ускорить работу за счет распараллеливания.
В большинстве случаев вы не будете заинтересованы в чтении частей одного и того же изображения одновременно, но вам захочется прочитать несколько изображений одновременно. При таком определении параллелизма сохранение на диск в виде файлов .png фактически обеспечивает полный параллелизм. Ничто не мешает вам читать несколько изображений одновременно из разных потоков или записывать несколько файлов одновременно, если имена изображений различны.
Как насчет LMDB? В среде LMDB может быть несколько читателей одновременно, но только один писатель, и писатели не блокируют читателей. Вы можете узнать больше об этом на веб-сайте технологии LMDB.
Несколько приложений могут одновременно обращаться к одной и той же базе данных LMDB, а несколько потоков одного и того же процесса могут одновременно обращаться к LMDB для чтения. Это обеспечивает еще более быстрое время чтения: если вы разделите весь CIFAR на десять наборов, то вы можете настроить десять процессов для каждого чтения в одном наборе, и это уменьшит время загрузки в десять раз.
HDF5 также предлагает параллельный ввод / вывод, позволяющий одновременное чтение и запись. Однако в реализации блокировка записи удерживается, и доступ является последовательным, если у вас нет параллельной файловой системы.
Если вы работаете с такой системой, есть два основных варианта, которые более подробно обсуждаются в этой статье группой HDF по параллельному вводу-выводу. Это может быть довольно сложно, и самый простой вариант — это разбить ваш набор данных на несколько файлов HDF5, чтобы каждый процесс мог работать с одним файлом .h5 независимо от других.
Существует один основной источник документации для привязки Python к LMDB, который размещен в Read the Docs LMDB. Хотя пакет даже не достиг версии> 0.94, он довольно широко используется и считается стабильным.
Что касается самой технологии LMDB, на веб-сайте технологии LMDB есть более подробная документация, которая может немного походить на изучение исчисления во втором классе, если только вы не начнете со страницы «Начало работы».
Что касается HDF5, то на сайте h5py есть документация, а также полезная запись в блоге Кристофера Ловелла, которая представляет собой отличный обзор того, как использовать пакет h5py. Книга O’Reilly, Python и HDF5 также является хорошим источником для начала.
Хотя это и не так задокументировано, как, возможно, необходимо начинающему, и LMDB, и HDF5 имеют большие сообщества пользователей, поэтому поиск в Google обычно дает полезные результаты.
Описанные системы хранения не идеальны, и у LMDB и HDF5 есть свои подводные камни.
Ключевым моментом для понимания LMDB является то, что новые данные записываются без перезаписи или перемещения существующих данных. Это конструктивное решение, которое позволяет чрезвычайно быстро считывать данные, свидетелями которых вы были в наших экспериментах, а также гарантирует целостность и надежность данных без дополнительной необходимости ведения журналов транзакций.
Помните, однако, что вам нужно было определить параметр map_size для распределения памяти перед записью в новую базу данных? Это где LMDB может создать много проблем. Предположим, вы создали базу данных LMDB, и все замечательно. Вы терпеливо ждали, пока ваш огромный набор данных будет упакован в LMDB.
Затем, позже, вы вспомните, что вам нужно добавить новые данные. Даже с буфером, который вы указали в map_size, вы легко можете ожидать появления ошибки lmdb.MapFullError. Если вы не хотите переписывать всю свою базу данных с обновленным map_size, вам придется хранить эти новые данные в отдельном файле LMDB. Несмотря на то, что одна транзакция может охватывать несколько файлов LMDB, наличие нескольких файлов может быть проблемой.
Кроме того, некоторые системы имеют ограничения на объем памяти, который может быть запрошен одновременно. По моему собственному опыту, работая с системами высокопроизводительных вычислений (HPC), это оказалось крайне неприятным и часто заставляло меня выбирать HDF5, а не LMDB.
Как для LMDB, так и для HDF5, только запрошенный элемент считывается в память одновременно. В LMDB пары ключ-блок считываются в память по одному, в то время как в HDF5 доступ к объекту dataset возможен как массив Python, с индексным набором dataset[i], диапазоном, набором dataset[i:j] и другим вариантом dataset[i:j:interval].
Из-за того, как системы оптимизированы, и в зависимости от вашей операционной системы, порядок доступа к элементам может влиять на производительность.
По моему опыту, в целом верно, что для LMDB вы можете получить лучшую производительность при последовательном доступе к элементам по ключу (пары ключ-значение хранятся в памяти, упорядоченные буквенно-цифрово по ключу), и что для HDF5 доступ к большим диапазонам будет работать лучше, чем чтение каждого элемента набора данных один за другим, используя следующее:
# Slightly slower for i in range(len(dataset)): # Read the ith value in the dataset, one at a time do_something_with(dataset[i]) # This is better data = dataset[:] for d in data: do_something_with(d)
Если вы рассматриваете выбор формата хранения файлов для написания своего программного обеспечения, было бы упущением не упомянуть Moving away from HDF5 Сирила Россанта с подводные камни HDF5, а также ответ Конрада Хинсена о On HDF5 and the future of data management, которое рассказывает, как можно избежать некоторых ловушек в некоторых случаях использования с большим количеством небольших наборов данных. Обратите внимание, что размер относительно небольшого набора данных по-прежнему составляет несколько ГБ.
Если вы имеете дело с действительно большими наборами данных, весьма вероятно, что вы будете делать с ними что-то важное. Стоит подумать о библиотеках глубокого обучения и о том, какая у них интеграция с LMDB и HDF5.
Прежде всего, все библиотеки поддерживают чтение изображений с диска в виде файлов .png, если вы преобразуете их в массивы NumPy ожидаемого формата. Это справедливо для всех методов, и мы уже видели выше, что считывать изображения в виде массивов относительно просто.
Вот несколько самых популярных библиотек глубокого обучения и их интеграция с LMDB и HDF5:
Хотя это далеко не исчерпывающее описание, мы надеемся, что это даст вам представление об интеграции LMDB / HDF5 с некоторыми ключевыми библиотеками глубокого обучения.
В своей ежедневной работе по анализу терабайтов медицинских изображений я использую как LMDB, так и HDF5, и узнал, что при любом способе хранения предусмотрительность крайне важна.
Часто модели необходимо обучать с использованием k-кратной перекрестной проверки, которая включает в себя разбиение всего набора данных на k наборов (k обычно равняется 10) и обучение k моделей, каждая с различным k-набором, используемым в качестве тестового набора. Это гарантирует, что модель не переопределяет набор данных или, другими словами, не может делать хорошие прогнозы по невидимым данным.
Стандартный способ создания k-набора состоит в размещении равного представления каждого типа данных, представленных в наборе данных, в каждом k-множестве. Таким образом, сохранение каждого набора k в отдельный набор данных HDF5 максимизирует эффективность. Иногда один набор k не может быть загружен в память одновременно, поэтому даже порядок данных в наборе данных требует некоторой предусмотрительности.
С LMDB я также тщательно планирую заранее, прежде чем создавать базы данных. Перед сохранением изображений стоит задать несколько хороших вопросов:
Независимо от способа хранения, когда вы работаете с большими наборами данных изображений, небольшое планирование имеет большое значение.
В этой статье вы познакомились с тремя способами хранения и доступа к большому количеству изображений в Python и, возможно, имели возможность поиграть с некоторыми из них. Весь код этой статьи находится в notebook Jupyter здесь или ввиде скрипта Python здесь.
Мы рассмотрели доказательства того, как различные методы хранения могут существенно повлиять на время чтения и записи, а также некоторые плюсы и минусы трех методов, рассмотренных в этой статье. Хотя хранение изображений в виде файлов .png может быть наиболее интуитивно понятным, при рассмотрении таких методов, как HDF5 или LMDB, есть большие преимущества в производительности.
Не стесняйтесь обсуждать в разделе комментариев отличные методы хранения, не описанные в этой статье, такие как LevelDB, Feather, TileDB, Badger, BoltDB или что-либо еще. Не существует идеального метода хранения, и наилучший метод зависит от вашего конкретного набора данных и вариантов использования.
Оригинальная статья: Rebecca Stone Three Ways of Storing and Accessing Lots of Images in Python
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Спасибо за полезную статью, мне особенно интересовал Хранение изображений в формате HDF5
Катя, "мне интересовал" или "меня интересовал"? "Интересовал хранение" или "интересовало хранение"? И Катя ли ты вообще?