Во время погружения в Deep Learning зацепила меня тема автоэнкодеров, особенно с точки зрения генерации новых объектов. Стремясь улучшить качество генерации, читал различные блоги и литературу на тему генеративных подходов. В результате набравшийся опыт решил облечь в небольшую серию статей, в которой постарался кратко и с примерами описать все те проблемные места с которыми сталкивался сам, заодно вводя в синтаксис Keras.
Автоэнкодеры
Автоэнкодеры — это нейронные сети прямого распространения, которые восстанавливают входной сигнал на выходе. Внутри у них имеется скрытый слой, который представляет собой код, описывающий модель. Автоэнкодеры конструируются таким образом, чтобы не иметь возможность точно скопировать вход на выходе. Обычно их ограничивают в размерности кода (он меньше, чем размерность сигнала) или штрафуют за активации в коде. Входной сигнал восстанавливается с ошибками из-за потерь при кодировании, но, чтобы их минимизировать, сеть вынуждена учиться отбирать наиболее важные признаки.
Кому интересно, добро пожаловать под кат
Автоэнкодеры состоят из двух частей: энкодера и декодера . Энкодер переводит входной сигнал в его представление (код): , а декодер восстанавливает сигнал по его коду: .
Автоэнкодер, изменяя и , стремится выучить тождественную функцию , минимизируя какой-то функционал ошибки.
При этом семейства функций энкодера и декодера как-то ограничены, чтобы автоэнкодер был вынужден отбирать наиболее важные свойства сигнала.
Сама по себе способность автоэнкодеров сжимать данные используется редко, так как обычно они работают хуже, чем вручную написанные алгоритмы для конкретных типов данных вроде звуков или изображений. А также для них критически важно, чтобы данные принадлежали той генеральной совокупности, на которой сеть обучалась. Обучив автоэнкодер на цифрах, его нельзя применять для кодирования чего-то другого (например, человеческих лиц).
Однако автоэнкодеры можно использовать для предобучения, например, когда стоит задача классификации, а размеченных пар слишком мало. Или для понижения размерности в данных для последующей визуализации. Либо когда просто надо научиться различать полезные свойства входного сигнала.
Более того, некоторые их развития (о которых тоже будет написано далее), такие как вариационный автоэнкодер (VAE), а также его сочетание с состязающимися генеративным сетями (GAN), дают очень интересные результаты и находятся сейчас на переднем крае науки о генеративных моделях.
Keras
Keras — это очень удобная высокоуровневая библиотека для глубокого обучения, работающая поверх theano или tensorflow. В ее основе лежат слои, соединяя которые между собой, получаем модели. Созданные однажды модели и слои сохраняют в себе свои внутренние параметры, и потому, например, можно обучить слой в одной модели, а использовать его уже в другой, что очень удобно.
Модели keras легко сохранять/загружать, у них простой, но в тоже время глубоко настраиваемый процесс обучения; модели свободно встраиваются в tensorflow/theano код (как операции над тензорами).
В качестве данных будем использовать датасет рукописных цифр MNIST
Для начала создадим наиболее простой (сжимающий, undercomplete) автоэнкодер с кодом малой размерности из двух полносвязных слоев: енкодера и декодера.
Так как интенсивность цвета нормирована на единицу, то активацию выходного слоя возьмем сигмоидой.
Напишем отдельные модели для энкодера, декодера и целого автоэнкодера. Для этого создадим экземпляры слоев и применим их один за другим, в конце все объединив в модели.
from keras.layers import Input, Dense, Flatten, Reshape
from keras.models import Model
def create_dense_ae():
# Размерность кодированного представления
encoding_dim = 49
# Энкодер
# Входной плейсхолдер
input_img = Input(shape=(28, 28, 1)) # 28, 28, 1 - размерности строк, столбцов, фильтров одной картинки, без батч-размерности
# Вспомогательный слой решейпинга
flat_img = Flatten()(input_img)
# Кодированное полносвязным слоем представление
encoded = Dense(encoding_dim, activation='relu')(flat_img)
# Декодер
# Раскодированное другим полносвязным слоем изображение
input_encoded = Input(shape=(encoding_dim,))
flat_decoded = Dense(28*28, activation='sigmoid')(input_encoded)
decoded = Reshape((28, 28, 1))(flat_decoded)
# Модели, в конструктор первым аргументом передаются входные слои, а вторым выходные слои
# Другие модели можно так же использовать как и слои
encoder = Model(input_img, encoded, name="encoder")
decoder = Model(input_encoded, decoded, name="decoder")
autoencoder = Model(input_img, decoder(encoder(input_img)), name="autoencoder")
return encoder, decoder, autoencoder
Создадим и скомпилируем модель (под компиляцией в данном случае понимается построение графа вычислений обратного распространения ошибки)
%matplotlib inline
import seaborn as sns
import matplotlib.pyplot as plt
def plot_digits(*args):
args = [x.squeeze() for x in args]
n = min([x.shape[0] for x in args])
plt.figure(figsize=(2*n, 2*len(args)))
for j in range(n):
for i in range(len(args)):
ax = plt.subplot(len(args), n, i*n + j + 1)
plt.imshow(args[i][j])
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
Закодируем несколько изображений и, ради интереса, взглянем на пример кода
Никто не мешает нам сделать такой же автоэнкодер, но с большим числом слоев. В таком случае он сможет вычленять более сложные нелинейные закономерности
Видим, что лосс насыщается на значительно меньшей величине, да и циферки немного более приятные
Сверточный автоэнкодер
Так как мы работаем с картинками, в данных должна присутствовать некоторая пространственная инвариантность. Попробуем этим воспользоваться: построим сверточный автоэнкодер
from keras.layers import Conv2D, MaxPooling2D, UpSampling2D
def create_deep_conv_ae():
input_img = Input(shape=(28, 28, 1))
x = Conv2D(128, (7, 7), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(32, (2, 2), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
encoded = Conv2D(1, (7, 7), activation='relu', padding='same')(x)
# На этом моменте представление (7, 7, 1) т.е. 49-размерное
input_encoded = Input(shape=(7, 7, 1))
x = Conv2D(32, (7, 7), activation='relu', padding='same')(input_encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(128, (2, 2), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (7, 7), activation='sigmoid', padding='same')(x)
# Модели
encoder = Model(input_img, encoded, name="encoder")
decoder = Model(input_encoded, decoded, name="decoder")
autoencoder = Model(input_img, decoder(encoder(input_img)), name="autoencoder")
return encoder, decoder, autoencoder
c_encoder, c_decoder, c_autoencoder = create_deep_conv_ae()
c_autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
c_autoencoder.summary()
Несмотря на то, что количество параметров у этой сети намного меньше чем у полносвязных сетей, функция ошибки насыщается на значительно меньшей величине.
Denoising автоэнкодер
Автоэнкодеры можно обучить убирать шум из данных: для этого надо на вход подавать зашумленные данные и на выходе сравнивать с данными без шума:
где — зашумленные данные.
В Keras можно оборачивать произвольные операции из нижележащего фреймворка в Lambda слой. Обращаться к операциям из tensorflow или theano можно через модуль backend.
Создадим модель, которая будет зашумлять входное изображение, а избавлять от шума переобучим какой-либо из уже созданных автоэнкодеров.
import keras.backend as K
from keras.layers import Lambda
batch_size = 16
def create_denoising_model(autoencoder):
def add_noise(x):
noise_factor = 0.5
x = x + K.random_normal(x.get_shape(), 0.5, noise_factor)
x = K.clip(x, 0., 1.)
return x
input_img = Input(batch_shape=(batch_size, 28, 28, 1))
noised_img = Lambda(add_noise)(input_img)
noiser = Model(input_img, noised_img, name="noiser")
denoiser_model = Model(input_img, autoencoder(noiser(input_img)), name="denoiser")
return noiser, denoiser_model
noiser, denoiser_model = create_denoising_model(autoencoder)
denoiser_model.compile(optimizer='adam', loss='binary_crossentropy')
Цифры на зашумленных изображениях с трудом проглядываются, однако denoising autoencoder неплохо убрал шум и цифры стали вполне читаемыми.
Разреженный (Sparse) автоэнкодер
Разреженный автоэнкодер — это просто автоэнкодер, у которого в функцию потерь добавлен штраф за величины значений в коде, то есть автоэнкодер стремится минимизировать такую функцию ошибки:
где — код,
— обычный регуляризатор (например L1):
Разреженный автоэнкодер не обязательно сужается к центру. Его код может иметь и большую размерность, чем входной сигнал. Обучаясь приближать тождественную функцию , он учится в коде выделять полезные свойства сигнала. Из-за регуляризатора даже расширяющийся к центру разреженный автоэнкодер не может выучить тождественную функцию напрямую.
Посмотрим, можно ли как-то интерпретировать размерности в кодах.
Возьмем среднее из всех кодов, а потом по очереди каждую размерность в усредненном коде заменим на максимальное ее значение.
Какие-то черты проглядываются, но ничего толкового тут не видно.
Значения в кодах по одиночке никакого очевидного смысла не несут, лишь хитрое взаимодействие между значениями, происходящее в слоях декодера, позволяет ему по коду восстановить входной сигнал.
Можно ли из кодов генерировать объекты по собственному желанию?
Для того, чтобы ответить на этот вопрос, следует лучше изучить, что такое коды, и как их можно интепретировать. Про это в следующей части.
Полезные ссылки и литература
Этот пост основан на собственной интерпретации первой части поста создателя KerasFrancois Chollet про автоэнкодеры в Keras.
А также главы про автоэнкодеры в Deep Learning Book.