Что найти?

Автоэнкодеры для восстановления изображений в Python и Keras

/
/

В настоящее время у нас есть огромные объемы данных почти во всех приложениях, которые мы используем – при прослушивании музыки в Spotify, просмотре изображений друзей в Instagram или, возможно, просмотре нового трейлера на YouTube. Всегда есть данные, передаваемые с серверов к вам.

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

Существует множество методов сжатия, которые различаются по использованию и совместимости. Например, некоторые методы сжатия работают только с аудиофайлами, например, знаменитый кодек MPEG-2 Audio Layer III (MP3).

Есть два основных типа сжатия:

  • Без потерь: целостность и точность данных предпочтительны, даже если мы не особо «сжимаем».
  • С потерями: целостность и точность данных не так важны, как скорость их обслуживания — представьте себе передачу видео в реальном времени, где важнее быть «живым», чем иметь видео высокого качества.

Например, с помощью автоэнкодеров мы можем разложить это изображение и представить его как 32-векторный код ниже. Используя его, мы можем реконструировать изображение. Конечно, это пример сжатия с потерями, поскольку мы потеряли довольно много информации.

Реконструкция изображения

Хотя мы можем использовать ту же технику, чтобы сделать это гораздо точнее, выделив больше места для представления:

Тип сжатия

Что такое автоэнкодеры?

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

В этой задаче есть два ключевых компонента:

  • Кодировщик: узнает, как сжать исходный ввод в небольшую кодировку.
  • Декодер: узнает, как восстановить исходные данные из кодировки, сгенерированной кодировщиком.

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

Реконструкция исходных данных

Кодировщик

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

Думайте об этом так, как будто вы пытаетесь что-то запомнить, например, запоминать большое число – вы пытаетесь найти в нем шаблон, который вы можете запомнить, и восстановить всю последовательность из этого шаблона, так как будет легко запомнить более короткий шаблон чем целое число.

Кодеры в своей простейшей форме представляют собой простые искусственные нейронные сети (ИНС). Однако есть определенные кодеры, они используют сверточные нейронные сети (CNN), которые являются очень специфическим типом ANN.

Кодировщик берет входные данные и генерирует их закодированную версию – сжатые данные. Затем мы можем использовать эти сжатые данные для отправки их пользователю, где они будут декодированы и реконструированы. Давайте посмотрим на кодировку для примера набора данных LFW:

Набор данных LFW

Кодировка здесь не имеет для нас особого смысла, но для декодера ее достаточно. Теперь уместно задать вопрос: «Но как кодировщик научился сжимать такие изображения?» Именно здесь вступает в игру симбиоз во время тренировок.

Декодер

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

Результат оценивается путем сравнения восстановленного изображения с исходным, используя среднеквадратическую ошибку (MSE) – чем больше он похож на оригинал, тем меньше ошибка.

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

Создание автоэнкодера

Keras – это среда Python, которая упрощает построение нейронных сетей. Это позволяет нам складывать слои разных типов для создания глубокой нейронной сети – что мы и сделаем для создания автокодировщика.

Во-первых, давайте установим Keras с помощью pip:

$ pip install keras

Предварительная обработка данных

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

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

# http://www.cs.columbia.edu/CAVE/databases/pubfig/download/lfw_attributes.txt
ATTRS_NAME = "lfw_attributes.txt"

# http://vis-www.cs.umass.edu/lfw/lfw-deepfunneled.tgz
IMAGES_NAME = "lfw-deepfunneled.tgz"

# http://vis-www.cs.umass.edu/lfw/lfw.tgz
RAW_IMAGES_NAME = "lfw.tgz"

Затем мы будем использовать две функции – одну для преобразования необработанной матрицы в изображение и изменения цветовой системы на RGB:

def decode_image_from_raw_bytes(raw_bytes):
    img = cv2.imdecode(np.asarray(bytearray(raw_bytes), dtype=np.uint8), 1)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    return img

А другой – для загрузки набора данных и адаптации его к нашим потребностям:

def load_lfw_dataset(
        use_raw=False,
        dx=80, dy=80,
        dimx=45, dimy=45):

    # Read attrs
    df_attrs = pd.read_csv(ATTRS_NAME, sep='\t', skiprows=1)
    df_attrs = pd.DataFrame(df_attrs.iloc[:, :-1].values, columns=df_attrs.columns[1:])
    imgs_with_attrs = set(map(tuple, df_attrs[["person", "imagenum"]].values))

    # Read photos
    all_photos = []
    photo_ids = []

    # tqdm in used to show progress bar while reading the data in a notebook here, you can change
    # tqdm_notebook to use it outside a notebook
    with tarfile.open(RAW_IMAGES_NAME if use_raw else IMAGES_NAME) as f:
        for m in tqdm.tqdm_notebook(f.getmembers()):
            # Only process image files from the compressed data
            if m.isfile() and m.name.endswith(".jpg"):
                # Prepare image
                img = decode_image_from_raw_bytes(f.extractfile(m).read())

                # Crop only faces and resize it
                img = img[dy:-dy, dx:-dx]
                img = cv2.resize(img, (dimx, dimy))

                # Parse person and append it to the collected data
                fname = os.path.split(m.name)[-1]
                fname_splitted = fname[:-4].replace('_', ' ').split()
                person_id = ' '.join(fname_splitted[:-1])
                photo_number = int(fname_splitted[-1])
                if (person_id, photo_number) in imgs_with_attrs:
                    all_photos.append(img)
                    photo_ids.append({'person': person_id, 'imagenum': photo_number})

    photo_ids = pd.DataFrame(photo_ids)
    all_photos = np.stack(all_photos).astype('uint8')

    # Preserve photo_ids order!
    all_attrs = photo_ids.merge(df_attrs, on=('person', 'imagenum')).drop(["person", "imagenum"], axis=1)

    return all_photos, all_attrs

Реализация автоэнкодера

import numpy as np
X, attr = load_lfw_dataset(use_raw=True, dimx=32, dimy=32)

Наши данные находятся в матрице X в форме 3D-матрицы, которая является представлением по умолчанию для изображений RGB. Предоставляя три матрицы – красную, зеленую и синюю, комбинация этих трех генерирует цвет изображения.

Эти изображения будут иметь большие значения для каждого пикселя, от 0 до 255. Обычно в машинном обучении мы склонны делать значения маленькими и сосредоточенными вокруг 0, так как это помогает нашей модели обучаться быстрее и получать лучшие результаты, поэтому давайте нормализуем наши изображения:

X = X.astype('float32') / 255.0 - 0.5

К настоящему времени, если мы проверим массив X на минимальное и максимальное значение, он будет -,5 и 0,5, что вы можете проверить:

print(X.max(), X.min())
0.5 -0.5

Чтобы увидеть изображение, давайте создадим функцию show_image. Он добавит 0,5 к изображениям, поскольку значение пикселя не может быть отрицательным:

import matplotlib.pyplot as plt
def show_image(x):
    plt.imshow(np.clip(x + 0.5, 0, 1))

А теперь взглянем на наши данные:

show_image(X[6])

Результат

Отлично, теперь давайте разделим наши данные на обучающий и тестовый набор:

from sklearn.model_selection import train_test_split
X_train, X_test = train_test_split(X, test_size=0.1, random_state=42)

Функция sklearn train_test_split() может разделять данные, задавая им тестовый коэффициент, а остальное, конечно же, является размером обучения. Random_state, которое вы часто будете видеть в машинном обучении, используется для получения тех же результатов независимо от того, сколько раз вы запускаете код.

Теперь время для модели:

from keras.layers import Dense, Flatten, Reshape, Input, InputLayer
from keras.models import Sequential, Model

def build_autoencoder(img_shape, code_size):
    # The encoder
    encoder = Sequential()
    encoder.add(InputLayer(img_shape))
    encoder.add(Flatten())
    encoder.add(Dense(code_size))

    # The decoder
    decoder = Sequential()
    decoder.add(InputLayer((code_size,)))
    decoder.add(Dense(np.prod(img_shape))) # np.prod(img_shape) is the same as 32*32*3, it's more generic than saying 3072
    decoder.add(Reshape(img_shape))

    return encoder, decoder

Эта функция принимает в качестве параметров image_shape (размеры изображения) и code_size (размер выходного представления). Форма изображения в нашем случае будет (32, 32, 3), где 32 представляют ширину и высоту, а 3 представляет матрицы цветовых каналов. При этом наше изображение имеет 3072 измерения.

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

Последовательная модель Keras в основном используется для последовательного добавления слоев и углубления нашей сети. Каждый слой переходит в следующий, и здесь мы просто начинаем с InputLayer (заполнитель для ввода) с размером вектора ввода – image_shape.

Задача слоя Flatten – сгладить матрицу (32,32,3) в одномерный массив (3072), поскольку сетевая архитектура не принимает трехмерные матрицы.

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

Декодер также является последовательной моделью. Он принимает ввод (кодировку) и пытается восстановить его в виде строки. Затем он складывает его в матрицу 32x32x3 через слой Dense. Последний слой Reshape преобразует его в изображение.

Теперь давайте соединим их вместе и запустим нашу модель:

# Same as (32,32,3), we neglect the number of instances from shape
IMG_SHAPE = X.shape[1:]
encoder, decoder = build_autoencoder(IMG_SHAPE, 32)

inp = Input(IMG_SHAPE)
code = encoder(inp)
reconstruction = decoder(code)

autoencoder = Model(inp,reconstruction)
autoencoder.compile(optimizer='adamax', loss='mse')

print(autoencoder.summary())

Этот код довольно прост – наша переменная кода является выходом кодера, который мы помещаем в декодер и генерируем переменную восстановления.

После этого мы связываем их оба, создавая модель с параметрами inp и восстановления и компилируя их с помощью оптимизатора adamax и функции потерь mse.

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

На данный момент мы можем подвести итоги:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_6 (InputLayer)         (None, 32, 32, 3)         0
_________________________________________________________________
sequential_3 (Sequential)    (None, 32)                98336
_________________________________________________________________
sequential_4 (Sequential)    (None, 32, 32, 3)         101376
=================================================================
Total params: 199,712
Trainable params: 199,712
Non-trainable params: 0
_________________________________________________________________

Здесь мы видим, что ввод 32,32,3. Обратите внимание, что None здесь относится к индексу экземпляра, поскольку мы передаем данные модели, он будет иметь форму (m, 32,32,3), где m – количество экземпляров, поэтому мы оставляем его как None.

Скрытый уровень – 32, что действительно является размером кодирования, который мы выбрали, и, наконец, вывод декодера, как вы видите, равен (32,32,3).

Теперь давайте торгуем моделью:

history = autoencoder.fit(x=X_train, y=X_train, epochs=20,
                validation_data=[X_test, X_test])

В нашем случае мы будем сравнивать построенные изображения с исходными, поэтому и x, и y равны X_train. В идеале вход равен выходу.

Переменная epochs определяет, сколько раз мы хотим, чтобы обучающие данные передавались через модель, а validation_data – это набор проверки, который мы используем для оценки модели после обучения:

Train on 11828 samples, validate on 1315 samples
Epoch 1/20
11828/11828 [==============================] - 3s 272us/step - loss: 0.0128 - val_loss: 0.0087
Epoch 2/20
11828/11828 [==============================] - 3s 227us/step - loss: 0.0078 - val_loss: 0.0071
.
.
.
Epoch 20/20
11828/11828 [==============================] - 3s 237us/step - loss: 0.0067 - val_loss: 0.0066

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

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

Пример визуализации

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

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

Теперь самое ожидаемое – визуализируем результаты:

def visualize(img,encoder,decoder):
    """Draws original, encoded and decoded images"""
    # img[None] will have shape of (1, 32, 32, 3) which is the same as the model input
    code = encoder.predict(img[None])[0]
    reco = decoder.predict(code[None])[0]

    plt.subplot(1,3,1)
    plt.title("Original")
    show_image(img)

    plt.subplot(1,3,2)
    plt.title("Code")
    plt.imshow(code.reshape([code.shape[-1]//2,-1]))

    plt.subplot(1,3,3)
    plt.title("Reconstructed")
    show_image(reco)
    plt.show()

for i in range(5):
    img = X_test[i]
    visualize(img,encoder,decoder)

Результат 1

Результат 2

Результат 3

Результат 4

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

Теперь давайте увеличим code_size до 1000:

Увеличение code_size до 1000»

code_size»

Приимер увеличения»

Приимер увеличения 2»

Приимер увеличения 3

По мере того как вы даете модели больше места для работы, она сохраняет более важную информацию об изображении.

Примечание. Кодировка не является двумерной, как показано выше. Это просто для иллюстрации. На самом деле это одномерный массив из 1000 измерений.

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

Анализ главных компонентов – очень популярное использование автоэнкодеров.

Снижение шума изображения

Еще одно популярное использование автоэнкодеров – шумоподавление. Давайте добавим случайный шум к нашим изображениям:

def apply_gaussian_noise(X, sigma=0.1):
    noise = np.random.normal(loc=0.0, scale=sigma, size=X.shape)
    return X + noise

Здесь мы добавляем случайный шум из стандартного нормального распределения со шкалой сигмы, которая по умолчанию равна 0,1.

Для справки, вот как выглядит шум с разными значениями сигмы:

plt.subplot(1,4,1)
show_image(X_train[0])
plt.subplot(1,4,2)
show_image(apply_gaussian_noise(X_train[:1],sigma=0.01)[0])
plt.subplot(1,4,3)
show_image(apply_gaussian_noise(X_train[:1],sigma=0.1)[0])
plt.subplot(1,4,4)
show_image(apply_gaussian_noise(X_train[:1],sigma=0.5)[0])

Увеличение сигмы до 0,5

Как мы видим, при увеличении сигмы до 0,5 изображение почти не видно. Мы попытаемся восстановить исходное изображение из зашумленных с сигмой 0,1.

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

code_size = 100

# We can use bigger code size for better quality
encoder, decoder = build_autoencoder(IMG_SHAPE, code_size=code_size)

inp = Input(IMG_SHAPE)
code = encoder(inp)
reconstruction = decoder(code)

autoencoder = Model(inp, reconstruction)
autoencoder.compile('adamax', 'mse')

for i in range(25):
    print("Epoch %i/25, Generating corrupted samples..."%(i+1))
    X_train_noise = apply_gaussian_noise(X_train)
    X_test_noise = apply_gaussian_noise(X_test)

    # We continue to train our model with new noise-augmented data
    autoencoder.fit(x=X_train_noise, y=X_train, epochs=1,
                    validation_data=[X_test_noise, X_test])

Теперь посмотрим на результаты модели:

X_test_noise = apply_gaussian_noise(X_test)
for i in range(5):
    img = X_test_noise[i]
    visualize(img,encoder,decoder)

Пример 1

Пример 2

Пример 3

Пример 4

Приложения автоэнкодера

Есть еще много вариантов использования автокодировщиков, помимо тех, которые мы изучили до сих пор.

Автоэнкодер можно использовать в таких приложениях, как Deepfakes, где у вас есть кодировщик и декодер разных моделей.

Например, предположим, что у нас есть два автокодера для Person X и один для Person Y. Ничто не мешает нам использовать кодировщик Person X и декодер Person Y, а затем сгенерировать изображения Person Y с характерными особенностями Person X:

Приложения автоэнкодера
Автоэнкодеры

Автоэнкодеры также могут использоваться для сегментации изображений – например, в автономных транспортных средствах, где вам нужно сегментировать различные элементы, чтобы транспортное средство могло принять решение:

Сегментация изображений

Заключение

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

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

This div height required for enabling the sticky sidebar