Вычисление предварительно обученных вкраплений изображений в Python

Фотография Matthew Ansley on Unsplash

В этом уроке мы создадим пакет Python для вычисления вкраплений изображений с помощью предварительно обученной сверточной нейронной сети (CNN) из командной строки.

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

После завершения этого урока вы будете знать, как:

  • Загружать данные изображений с помощью numpy и scikit-image.
  • Вычислять линейные вложения изображений с помощью PCA из scikit-learn
  • Вычисление глубоких вложений изображений с помощью предварительно обученной модели ResNet из Keras
  • Удаление любого количества слоев из предварительно обученной модели в Keras
  • Визуализация глубоких вкраплений изображений с помощью UMAP

Мы собираемся создать инструмент командной строки под названием embedding для выполнения всего этого. В качестве последней демонстрации мы сравним PCA и ResNet вкрапления изображений рукописных цифр MNIST.

Настройка пакета embedding

Мы пропустим описание пакета embedding Python и пройдемся только по разработке. Во-первых, вам нужно будет клонировать этот репозиторий:

git clone https://github.com/jmswaney/blog.git
cd blog/07_pretrained_image_embeddings
Войти в полноэкранный режим Выйти из полноэкранного режима

В этой папке вы увидите файл pyproject.toml, а также файл environment.yml для poetry и conda соответственно. Вам потребуется установить conda в вашей системе, и вы можете либо следовать моему предыдущему руководству для глобальной установки поэзии, либо просто использовать тот, который включен в виртуальную среду embedding. Чтобы установить виртуальную среду, вы можете выполнить:

conda env create -f environment.yml
conda activate embedding
Войти в полноэкранный режим Выйти из полноэкранного режима

Примечание: Я включил cudatoolkit и cudnn в зависимости environment.yml для поддержки GPU. Вы можете удалить эти зависимости, если вам не нужно GPU-ускорение.

После установки и активации нам остается установить зависимости Python, включая сам embedding CLI:

# from 07_pretrained_image_embeddings/
poetry install
Войти в полноэкранный режим Выйти из полноэкранного режима

Это установит наши зависимости машинного обучения, такие как scikit-learn и tensorflow. Также будет создан исполняемый консольный скрипт с точкой входа, подключенной к embedding:cli.

Разработка embedding CLI

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

Создание простой точки входа

Первое, что нам нужно реализовать, это простую функцию точки входа под названием cli и открыть ее из пакета __init__.py. Мы можем создать cli.py и добавить простую команду click, как в моем учебнике по CLI.

# in cli.py
import click

@click.command()
def cli():
    print("Hello, embedding!")
Вход в полноэкранный режим Выйти из полноэкранного режима

Чтобы представить это как embedding:cli, нам просто нужно импортировать его в __init__.py нашего пакета.

# in __init__.py
__version__ = '0.1.0'

from .cli import *
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем проверить, что наша точка входа работает из терминала,

embedding
Hello, embedding!
Войти в полноэкранный режим Выйти из полноэкранного режима

По умолчанию поэзия устанавливает текущий пакет в режиме «редактируемый», что означает, что наши изменения в исходниках немедленно вступят в силу в нашем CLI.

Загрузка данных изображения

Далее нам нужно иметь возможность загружать изображение из файла. Лучший способ сделать это — использовать функцию imread из scikit-image, которая обеспечивает высокую скорость и поддержку многих форматов изображений без потерь и с потерями. Мы можем обновить функцию cli, чтобы открыть входной файл изображения.

import click
from skimage.io import imread

@click.command()
@click.argument("input")
def cli(input):
    print(main(input))

def main(input):
    return imread(input).shape
Вход в полноэкранный режим Выход из полноэкранного режима

При тестировании с изображением собаки печатается форма изображения.

embedding data/dog.jpg
(333, 516, 3)
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем захотеть загрузить данные из массивов Numpy, поэтому давайте вынесем это в модуль, специально предназначенный для операций ввода-вывода, под названием io.py.

# in io.py
import os
import numpy as np
from skimage.io import imread

NUMPY_EXT = ".npy"

def load(filename: str) -> np.ndarray:
    ext = os.path.splitext(filename)[-1]
    if ext == NUMPY_EXT: return np.load(filename)
    return imread(filename)
Вход в полноэкранный режим Выход из полноэкранного режима

Тогда загрузка файлов .npy и .npz также должна работать,

embedding data/dog.npz
(333, 516, 3)

embedding data/dog.npy
(333, 516, 3)
Войти в полноэкранный режим Выйти из полноэкранного режима

Линейные вложения с помощью PCA

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

Для этого нам понадобится несколько изображений, поэтому давайте произвольно выберем квадратные участки из нашего примера. Затем, в новом модуле embed.py, мы можем использовать sklearn для расчета встраивания PCA.

# in cli.py
from embedding.io import load
from embedding.embed import pca
import numpy as np
from random import random
from math import floor

def main(input):
    data = load(input)

    size = 64
    start = lambda: (floor(random()*(data.shape[0] - size)), floor(random()*(data.shape[1] - size)))
    x = np.asarray([data[y:y+size, x:x+size] for (y, x) in [start() for _ in range(32)]])
    print("Patches shape", x.shape)

    x_pca = pca(x.reshape(len(x), -1), 4)
    print("PCA embedded shape", x_pca.shape)
Вход в полноэкранный режим Выход из полноэкранного режима

Поскольку PCA применяется к векторным входам, нам нужно перестроить наши изображения в векторы. Запустив эту процедуру, мы видим, что размерность данных уменьшилась, как и ожидалось.

embedding data/dog.jpg

Patches shape (32, 64, 64, 3)
PCA embedded shape (32, 4)
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь мы отобрали 32 патча изображений, каждый (64, 64, 3), и решили использовать 4 главных компонента.

Глубокие вкрапления с использованием ResNet50V2

Теперь, когда у нас есть работающие вкрапления изображений PCA, мы можем перейти к предварительно обученной нейронной сети. В Keras есть несколько CNN с весами, полученными в результате предварительного обучения в ImageNet. Чтобы не усложнять задачу, для демонстрации мы будем использовать ахитектуру ResNet50V2.

В нашем модуле embed.py мы можем импортировать модель и инстанцировать ее с весами ImageNet. В первый раз, когда мы это делаем, Keras загрузит веса и сохранит их в локальном кэше. Нам нужно убедиться, что мы инстанцируем модель с include_top=False, чтобы мы получили последнее скрытое представление в сети (в отличие от вероятностей меток классов из последнего слоя softmax).

import numpy as np
from sklearn.decomposition import PCA
import tensorflow as tf
from tensorflow.keras.applications.resnet_v2 import preprocess_input

def pca(x: np.ndarray, n: int) -> np.ndarray:
    return PCA(n_components=n).fit_transform(x)

def resnet(x: np.ndarray) -> np.ndarray:
    model = tf.keras.applications.ResNet50V2(include_top=False)
    maps = model.predict(preprocess_input(x))
    if np.prod(maps.shape) == maps.shape[-1] * len(x):
        return np.squeeze(maps)
    else:
        return maps.mean(axis=1).mean(axis=1)
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что мы также использовали служебную функцию Keras preprocess_input для семейства моделей resnet_v2. Это важно для того, чтобы убедиться, что мы масштабируем наши изображения до значений интенсивности между -1 и 1, так как модель обучалась на изображениях, которые были масштабированы аналогичным образом. Мы также выполняем объединение глобальных средних, чтобы вернуть векторное представление, эффективно удаляя любую оставшуюся пространственную информацию в картах активации.

Когда мы вызываем resnet с нашими патчами изображений в cli.py, мы получаем следующее,

embedding data/dog.jpg

Patches shape (32, 64, 64, 3)
PCA embedded shape (32, 4)
ResNet embedded shape (32, 2048)
Вход в полноэкранный режим Выход из полноэкранного режима

Это показывает, что архитектура ResNet50V2 обеспечивает 2048-мерное встраивание с последнего слоя модели.

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

Сохранение вкраплений изображений

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

# in io.py
def save(filename: str, x: np.ndarray, compress: bool) -> None:
    np.savez_compressed(filename, x) if compress else np.save(filename, x)
Вход в полноэкранный режим Выход из полноэкранного режима

На данном этапе было бы полезно рефакторить нашу функцию main в более гибкую функцию embed, которая использует заданную модель для встраивания изображений в виде массивов numpy.

# in embed.py
def embed(x: np.ndarray, model: str, n: int):
    match model:
        case "pca":
            return pca(x.reshape(len(x), -1), n)
        case "resnet":
            if x.ndim == 3:
                return resnet(np.repeat(x[..., np.newaxis], 3, axis=-1))
            return resnet(x)
        case _:
            raise Exception("Invalid model provided")
Вход в полноэкранный режим Выход из полноэкранного режима

Мы повторили черно-белые изображения в последнем измерении, чтобы соответствовать ожидаемой размерности входных данных для ResNet50V2.

Мы можем использовать это в cli.py, который использует io.py для выполнения операций с побочными эффектами.

# in cli.py
MODEL_TYPES = ['pca', 'resnet']

@click.command()
@click.argument("input")
@click.argument("output")
@click.option("--model", "-m", required=True, type=click.Choice(MODEL_TYPES))
@click.option("--dimensions", "-d", "n_dim", default=32, type=int)
@click.option("--compress", "-c", is_flag=True)
def cli(input, output, model, n_dim, compress):
    x = sample_patches(load(input), 32, 64)
    x_embed = embed(x, model, n_dim)
    save(output, x_embed, compress)
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы добавили аргументы для записи выходного файла с опциональным сжатием, а также выбор модели для использования и размерность встраивания (для PCA). Мы также переработали код выборки патчей изображений в функцию sample_patches.

Загрузка данных MNIST

Поскольку встраивание одного изображения является редкостью, мы можем удалить нашу функцию sample_patches. Если входными данными является массив numpy, мы предполагаем, что он имеет нужную размерность для последующих моделей. Если входными данными является папка, мы должны считать, что это плоская папка, содержащая только изображения, которые должны быть расположены в алфавитно-цифровом порядке. Мы также хотели бы иметь возможность загружать учебные изображения MNIST. Чтобы выполнить все эти операции загрузки, мы можем отредактировать функцию load в io.py,

from typing import Optional
import os
import numpy as np
from skimage.io import imread
from tensorflow.keras.datasets.mnist import load_data

MNIST_NAME = "mnist"
MNIST_CACHE = "mnist.npz"
NUMPY_EXT = ".npy"

def load(name: str) -> tuple[np.ndarray, Optional[np.ndarray]]:
    if name == MNIST_NAME: 
        return load_data(MNIST_CACHE)[0] # Only training set
    ext = os.path.splitext(name)[-1]
    if ext == NUMPY_EXT: return np.load(name)
    return np.asarray([imread(os.path.join(name, x)) for x in os.listdir(name)])
Вход в полноэкранный режим Выйти из полноэкранного режима

Итак, если мы передадим «mnist» в качестве первого аргумента в embedding, мы получим данные изображения и метки из набора данных MNIST в Keras. В противном случае мы загрузим заданную матрицу или папку с изображениями.

Генерирование визуализаций UMAP

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

Вернемся в наш модуль embed.py и добавим следующее:

from umap import UMAP

def umap(x, seed = None):
    if seed: np.seed(seed)
    return UMAP().fit_transform(x)
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы можем вызвать эту команду в cli.py, чтобы сохранить результаты в файл.

from embedding.io import load, save
from embedding.embed import embed, umap
from embedding.plot import plot_umap, plot_pca
import os
import click

MODEL_TYPES = ['pca', 'resnet']

@click.command()
@click.argument("input")
@click.argument("output")
@click.option("--model", "-m", required=True, type=click.Choice(MODEL_TYPES))
@click.option("--dimensions", "-d", "n_dim", default=32, type=int)
@click.option("--compress", "-c", is_flag=True)
@click.option("--plot", "-p", is_flag=True)
@click.option("--seed", "-s", type=int, default=None)
def cli(input, output, model, n_dim, compress, plot, seed):
    x, y = load(input)
    print("Input shape", x.shape)
    x_embed = embed(x, model, n_dim)
    print("Embedded shape", x_embed.shape)
    save(output, x_embed, compress)
    if plot:
        root, ext = os.path.splitext(output)
        name = os.path.basename(root)
        if x_embed.shape[-1] == 2:
            plot_pca(x_embed, y, f"{name}_pca.png")
        else:
            x_umap = umap(x_embed, seed)
            print("UMAP shape", x_umap.shape)
            save(f"{name}_umap{ext}", x_umap, compress)
            plot_umap(x_umap, y, f"{name}_umap.png")
Вход в полноэкранный режим Выход из полноэкранного режима

Мы добавили суффикс _umap к именам выходных файлов графиков и матриц для результатов UMAP (если размерность встраивания еще не равна 2). Если размерность вложения равна 2, то мы просто считаем, что это результат PCA, и строим график без выполнения UMAP.

Мы можем написать функции plot_pca и plot_umap в plot.py.

from typing import Optional
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np

sns.set(style="white", context="poster", rc={ "figure.figsize": (14, 10), "lines.markersize": 1 })

def make_scatter(x: np.ndarray, y: Optional[np.ndarray]):
    if y is None:
        plt.scatter(x[idx, 0], x[idx, 1])
    else:
        for label in np.unique(y):
            idx = np.where(y == label)[0]
            plt.scatter(x[idx, 0], x[idx, 1], label=label)
            plt.legend(bbox_to_anchor=(0, 1), loc="upper left", markerscale=6)

def plot_umap(x: np.ndarray, y: Optional[np.ndarray], filename: str):
    make_scatter(x, y)
    plt.xlabel("UMAP 1")
    plt.ylabel("UMAP 2")
    plt.title(filename)
    plt.savefig(filename)

def plot_pca(x: np.ndarray, y: Optional[np.ndarray], filename: str):
    make_scatter(x, y)
    plt.xlabel("PC 1")
    plt.ylabel("PC 2")
    plt.title(filename)
    plt.savefig(filename)
Вход в полноэкранный режим Выход из полноэкранного режима

В make_scatter мы используем seaborn и matplotlib для построения диаграммы рассеяния всех 2D представлений изображений вместе с метками классов, если они у нас есть.

Удаление любого количества слоев

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

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

# Add option to cli.py
...
@click.option("--layers", "-l", type=int, default=None)
def cli(input, output, model, n_dim, layers, compress, plot, seed, batch_size):
    ...
    x_embed = embed(x, model, n_dim, batch_size, layers)
    ...

# Remove layers in embed.py
def resnet(x: np.ndarray, batch_size = None, layers = None) -> np.ndarray:
    full = tf.keras.applications.ResNet50V2(include_top=False)
    model = full if layers is None else tf.keras.models.Model(full.input, full.layers[layers].output)
    ...
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, если мы передадим --layers 100, мы получим первые 100 слоев модели ResNet50V2.

Результаты

Теперь, когда мы завершили начальную разработку нашего инструмента embedding, пришло время использовать его. Сначала рассмотрим простое встраивание 2D PCA-изображения.

# Embed MNIST using 2D PCA
embedding mnist mnist_pca_2d.npy --model pca -d 2 -p
Вход в полноэкранный режим Выход из полноэкранного режима

Первый главный компонент выглядит так, как будто он примерно соответствует наличию дырки в центре цифры (нулевой).

Теперь давайте сравним это с нашим вложением изображения ResNet50V2, визуализированным с помощью UMAP.

# Embed MNIST using ResNet50V2
embedding mnist mnist_resnet.npy --model resnet -p -s 582
Вход в полноэкранный режим Выход из полноэкранного режима

Похоже, что в обоих вкраплениях изображений 0 и 1 являются наиболее очевидными цифрами для идентификации. Хотя в вкраплениях изображений ResNet 0 и 1 разделены немного лучше, чем на графике PCA, это, на удивление, не намного лучше. Возможно, мы можем использовать меньше слоев на этом более простом наборе данных.

После некоторых экспериментов я обнаружил, что удаление блоков 4 и 5 из модели ResNet50V2 дает вкрапления изображений, которые ближе к линейно разделяемым. Удаление блоков 4 и 5 соответствует удалению первых 82 слоев модели.

# Embed MNIST using 82 layers of ResNet50V2
embedding mnist mnist_resnet_82layers.npy --model resnet -p -s 582 -l 82
Вход в полноэкранный режим Выход из полноэкранного режима

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

Использование предварительно обученных моделей — важный инструмент в арсенале инженеров машинного обучения. Предварительно обученные вкрапления изображений часто используются в качестве эталона или отправной точки при решении новых задач компьютерного зрения. Несколько архитектур, которые показали отличные результаты в классификации ImageNet, доступны в готовом виде с предварительно обученными весами из Keras, Pytorch, Hugging Face и др.

Здесь мы создали простой пакет Python, чтобы помочь в вычислении вкраплений изображений из командной строки. Мы использовали наш embedding CLI для вычисления и визуализации вкраплений изображений MNIST с помощью PCA и модели ResNet50V2. На этом относительно простом наборе данных мы обнаружили, что использование подмножества слоев из модели ResNet50V2 качественно лучше, чем PCA, различает рукописные цифры (без контролируемого обучения).

Мы могли ожидать, что ранние слои модели ResNet50V2 будут более применимы к набору данных MNIST, поскольку известно, что ранние слои CNN извлекают признаки, связанные с краями и текстурами. Высокоуровневые визуальные признаки, необходимые для классификации тысяч естественных изображений RGB в ImageNet, могут оказаться не столь полезными при работе с черно-белыми изображениями рукописных цифр.

Доступность источников

Все исходные материалы для этой статьи доступны здесь, на GitHub-репо моего блога.

Оцените статью
devanswers.ru
Добавить комментарий