Автоэнкодеры для восстановления изображений в Python и Keras
В настоящее время у нас есть огромные объемы данных почти во всех приложениях, которые мы используем – при прослушивании музыки в Spotify, просмотре изображений друзей в Instagram или, возможно, просмотре нового трейлера на YouTube. Всегда есть данные, передаваемые с серверов к вам.
Для одного пользователя это не будет проблемой. Но представьте себе одновременную обработку тысяч, если не миллионов запросов с большими данными. Эти потоки данных должны быть каким-то образом сокращены, чтобы мы могли физически предоставлять их пользователям – вот где начинается сжатие данных.
Существует множество методов сжатия, которые различаются по использованию и совместимости. Например, некоторые методы сжатия работают только с аудиофайлами, например, знаменитый кодек MPEG-2 Audio Layer III (MP3).
Есть два основных типа сжатия:
- Без потерь: целостность и точность данных предпочтительны, даже если мы не особо «сжимаем».
- С потерями: целостность и точность данных не так важны, как скорость их обслуживания — представьте себе передачу видео в реальном времени, где важнее быть «живым», чем иметь видео высокого качества.
Например, с помощью автоэнкодеров мы можем разложить это изображение и представить его как 32-векторный код ниже. Используя его, мы можем реконструировать изображение. Конечно, это пример сжатия с потерями, поскольку мы потеряли довольно много информации.
Хотя мы можем использовать ту же технику, чтобы сделать это гораздо точнее, выделив больше места для представления:
Что такое автоэнкодеры?
Автоэнкодер – это, по определению, метод автоматического кодирования чего-либо. Используя нейронную сеть, автокодировщик может научиться разлагать данные (в нашем случае изображения) на довольно маленькие биты данных, а затем, используя это представление, восстанавливать исходные данные как можно ближе к оригиналу.
В этой задаче есть два ключевых компонента:
- Кодировщик: узнает, как сжать исходный ввод в небольшую кодировку.
- Декодер: узнает, как восстановить исходные данные из кодировки, сгенерированной кодировщиком.
Эти двое обучаются вместе в симбиозе, чтобы получить наиболее эффективное представление данных, из которых мы можем реконструировать исходные данные, не теряя при этом слишком много.
Кодировщик
Перед кодировщиком стоит задача найти наименьшее возможное представление данных, которое он может хранить, – извлечь наиболее характерные особенности исходных данных и представить их таким образом, чтобы декодер мог их понять.
Думайте об этом так, как будто вы пытаетесь что-то запомнить, например, запоминать большое число – вы пытаетесь найти в нем шаблон, который вы можете запомнить, и восстановить всю последовательность из этого шаблона, так как будет легко запомнить более короткий шаблон чем целое число.
Кодеры в своей простейшей форме представляют собой простые искусственные нейронные сети (ИНС). Однако есть определенные кодеры, они используют сверточные нейронные сети (CNN), которые являются очень специфическим типом ANN.
Кодировщик берет входные данные и генерирует их закодированную версию – сжатые данные. Затем мы можем использовать эти сжатые данные для отправки их пользователю, где они будут декодированы и реконструированы. Давайте посмотрим на кодировку для примера набора данных 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)
Вы можете видеть, что результаты не очень хорошие. Однако, если учесть, что все изображение закодировано в очень маленьком векторе 32, видимом в середине, это совсем не плохо. При сжатии с 3072 измерений до 32 мы теряем много данных.
Теперь давайте увеличим code_size до 1000:
»
»
»
»
По мере того как вы даете модели больше места для работы, она сохраняет более важную информацию об изображении.
Примечание. Кодировка не является двумерной, как показано выше. Это просто для иллюстрации. На самом деле это одномерный массив из 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,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)
Приложения автоэнкодера
Есть еще много вариантов использования автокодировщиков, помимо тех, которые мы изучили до сих пор.
Автоэнкодер можно использовать в таких приложениях, как Deepfakes, где у вас есть кодировщик и декодер разных моделей.
Например, предположим, что у нас есть два автокодера для Person X и один для Person Y. Ничто не мешает нам использовать кодировщик Person X и декодер Person Y, а затем сгенерировать изображения Person Y с характерными особенностями Person X:

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