Чтобы получить представление о VAE, мы начнем с простой сети и будем добавлять части шаг за шагом.

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

http://kvfrans.com/content/images/2016/08/dat.jpg

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

Теперь попробуем на нескольких изображениях. Вместо вектора единичных будем использовать для входных данных однократный вектор. [1, 0, 0, 0] может означать изображение кошки, в то время как [0, 1, 0, 0] может означать собаку. Это работает, но мы будем хранить только до 4-х изображений. Использование более длинного вектора означает добавление дополнительных и дополнительных параметров, поэтому сеть может запоминать различные изображения.

Для исправления этого мы используем вектор реальных чисел, а не одноразовый вектор. Рассмотрим это как код картинки, откуда берутся термины “кодировать/декодировать”. Например, [3.3, 4.5, 2.1, 9.8] может представлять изображение кошки, в то время как [3.4, 2.1, 6.7, 4.2] может представлять собаку. Этот первый вектор понимается как наши латентные переменные.

http://kvfrans.com/content/images/2016/08/autoenc.jpg

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

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

Однако, мы пытаемся создать здесь генеративную модель, а не просто нечёткое расположение, которое “запомнит” картинки. Пока мы ничего не будем генерировать, так как у нас нет навыков создания скрытых векторов, кроме кодирования их из изображений.

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

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

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

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

generation_loss = mean(square(generated_image – real_image))

latent_loss = KL-Дивергенция(latent_variable, unit_gaussian)

убыток = генерация_убытков + латентный_убыток

http://kvfrans.com/content/images/2016/08/vae.jpg

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

Это позволит нам вычислить расходимость KL следующим образом:

# z_mean и z_stddev – два вектора, генерируемых сетью кодеров.

latent_loss = 0.5 * tf.reduce_sum(tf.square(z_mean) + tf.square(z_stddev) – tf.log(tf.square(z_stddev)) – 1,1)

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

samples = tf.random_normal([batchsize,n_z],0,1,dtype=tf.float32)

samplepled_z = z_mean + (z_stddev * samples)

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

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

Допустим, вы получили кучу пар вещественных чисел между [0, 10], наряду с репутацией. Например, 5.43 означает яблоко, а 5.44 – банан. Когда кто-то дает вам сумму 5.43, вы признаете, нет необходимости говорить, что они говорят о яблоке. мы, по сути, будем кодировать бесконечную информацию таким образом, так как нет никаких ограничений на то, какой процент различных реальных чисел мы будем иметь между [0, 10].

Однако, что, если при попытке сообщить вам число был добавлен гауссовский шум в 1? Теперь, как только вы получите сумму 5.43, первое число может быть где угодно [4.4 ~ 6.4], поэтому другой человек может даже иметь в виду банан (5.44).

Чем больше дисперсия на добавленном шуме, тем меньше информации мы передадим, используя эту одну переменную.

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

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

Насколько хорошо это работает?

http://kvfrans.com/content/images/2016/08/mnist.jpg

Я провел пару тестов, чтобы выяснить, насколько хорошо вариационный автокодировщик будет работать с набором данных рукописного ввода MNIST.