Введение в вариационные автоэнкодеры с использованием Keras

Введение в вариационные автоэнкодеры с использованием Keras

6 апреля 2022 г.

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


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


Вариационные автоэнкодеры были изобретены для достижения цели генерации данных и с момента их [введения] (https://arxiv.org/abs/1312.6114) в 2013 году привлекли большое внимание как благодаря впечатляющим результатам, так и лежащим в их основе простота. Ниже вы увидите два изображения человеческих лиц. Эти изображения не реальных людей — они были сгенерированы с использованием [VQ-VAE 2] (https://arxiv.org/abs/1906.00446), модели DeepMind Variational Autoencoder (VAE).


Источник изображения


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


Если вы хотите сразу перейти к коду, вы можете сделать это [здесь] (https://www.assemblyai.com/blog/variational-autoencoders-for-dummies/#building-a-variational-autoencoder).


Введение


Генерация убедительных данных, имитирующих распределение обучающей выборки, — сложная задача, в которой есть несколько особенностей, которые делают ее уникально сложной задачей. Задача не контролируется и требует, чтобы мы рассматривали данные как представители распределения. То есть, вместо того, чтобы выполнять операции с точками данных как точками самостоятельно для достижения какой-либо цели, которая является ценной сама по себе, например кластеризация с помощью K-средних, нам нужно определить базовую структуру данных достаточно, чтобы мы могли использовать их для создания убедительных подделок. Имея миллион изображений человеческих лиц, как нам обучить модель, которая может автоматически выводить реалистичные изображения человеческих лиц?


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


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


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


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


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


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


Сетевая архитектура для обычного автоэнкодера


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


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


![Сетевая архитектура для сверточного обычного автоэнкодера]


Как автоэнкодер на самом деле выполняет это сжатие? Сверточные слои в сети извлекают характерные особенности каждой цифры, например тот факт, что 8 замкнута и имеет две петли, а 9 открыта и имеет одну петлю. Затем полносвязная сеть отображает эти функции в скрытом пространстве более низкого измерения, помещая их в это пространство в соответствии с тем, какие функции и в какой степени присутствуют на изображении. Если мы уже сопоставляем изображения с репрезентативным пространством признаков, можем ли мы не использовать это пространство для генерации изображений?


Можно ли использовать автоэнкодеры для генерации данных?


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


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


Процесс генерации данных с помощью обычных автоэнкодеров


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


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


Но Почему это не работает?


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


Двумерные точки данных


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


Точки данных с интерполированной кривой


А затем сжимайте данные до одного измерения, используя пути точек вдоль этой кривой в качестве их местоположения в одномерном пространстве. Ниже вы можете увидеть, как это будет работать. Расстояния пути двух точек показаны красной и зеленой кривыми в двумерном пространстве слева. Длины этих кривых представляют собой расстояния от одних и тех же точек до начала координат в одномерном пространстве (вдоль оси x) справа.


![Кодирование в одномерное изображение на основе интерполированного расстояния между кривыми]


Кодирование в одномерное изображение на основе интерполированного расстояния пути кривой


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


Отсюда генерация данных кажется простой задачей — нам просто нужно выбрать случайный скрытый вектор и позволить декодеру сделать свою работу:


![Генерация в двух измерениях на основе расстояния между интерполированными кривыми]


Просто как тот! Верно?


Неправильно. Хотя кажется, что мы, возможно, попали в самую точку, мы только научились генерировать точки вдоль нашей интерполированной кривой в исходном пространстве. Наша сеть только что изучила одну такую ​​кривую в исходном пространстве, которая может представлять истинное базовое распределение данных. В двумерном пространстве есть бесконечное количество кривых, которые интерполируют наши точки данных. Предположим, что истинное базовое распределение выглядит так:


Истинная базовая генеративная кривая


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


Предыдущий метод генерации данных не смог создать убедительные данные


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


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


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


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


Вариационные автоэнкодеры выполняют эту задачу с помощью простого, но важного дифференцирующего фактора: вместо того, чтобы сопоставлять входные данные с точками в скрытом пространстве, они сопоставляются с параметрами распределения, которые описывают, где* данные «должны» быть отображены (вероятностно). ) в скрытом пространстве, в соответствии с его особенностями.


![Сетевая архитектура для сверточного вариационного автоэнкодера]


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


Изображение взято из источника


Понимание вариационных автоэнкодеров с MNIST


Чтобы понять, как работают VAE, давайте рассмотрим конкретный пример. Мы рассмотрим, как Keras VAE учится характеризовать скрытое пространство как ландшафт признаков для набора данных [MNIST Рукописная цифра] (http://yann.lecun.com/exdb/mnist/). Набор цифр MNIST содержит десятки тысяч полутоновых изображений цифр размером 28 на 28 пикселей. [Здесь] (https://camo.githubusercontent.com/01c057a753e92a9bc70b8c45d62b295431851c09cffadf53106fc0aea7e2843f/687474703a2f2f692e7974696d672e636f6d2f76692f3051493378675875422d512f687164656661756c742e6a7067) некоторые примеры изображений, чтобы освоиться [1] (https://www.assemblyai.com/blog/variational-autoencoders-for-dummies/# сноски). Начнем с некоторых исходных предположений.


Настройка проблемы


  1. Во-первых, давайте предположим, что извлекатели сверточных признаков в нашей сети кодировщика уже обучены. Таким образом, обучение, которое выполняет кодировщик, заключается в том, как сопоставлять извлеченные признаки с параметрами распределения.

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

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

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

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


Тренировка на шестерке


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


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


Теперь мы делаем выборку из этого распределения и передаем полученную точку данных в декодер. Ошибка измеряется относительно этой случайно сгенерированной точки. Именно эта разница отличает обычные автоэнкодеры от вариационных и делает VAE полезными для генерации данных. Случайно выбранная точка представлена ​​зеленой точкой на изображении ниже.


![Распределение закодированного изображения (красный), точка случайной выборки (зеленый)]


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


Декодированное изображение произвольно выбранной точки


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


Обучение на одном


Далее во время обучения допустим, что изображение one вводится в нашу сеть3. В качестве примера предположим, что сеть кодировщика сопоставляет извлеченные функции изображения с параметрами распределения, показанными ниже, где снова красная точка представляет среднее распределения, а красная кривая представляет его 1 -сигма кривая.


Распространение закодированного изображения


Обратите внимание, что эти параметры распределения попадают в основную часть распределения в области, которые мы ранее видели представленными (и, следовательно, декодированными) шестиподобными изображениями. И снова точка будет случайным образом выбрана из этой области и передана декодеру для расчета потерь. Помните, что декодер априори не знает о том, что точка была выбрана из распределения, относящегося к входному изображению. Все, что видит декодер, — это точка в области латентного пространства, в которой есть особенности, видимые на изображениях, которые выглядят как «6» , поэтому, когда точка случайным образом выбирается из этого распределения, она будет декодирована, чтобы выглядеть примерно так это:



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


Обучение на нуле


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



«6» и «0» намного «ближе» по существенным признакам, чем «6» и «1» — они оба имеют петлю и могут относительно легко непрерывно трансформироваться из одного в другое 4. Следовательно, наше декодированное изображение можно разумно интерпретировать как шесть или как ноль. На самом деле, если вы внимательно посмотрите, вы увидите, что кривая, общая для 6 и 0, сильно выражена в декодированном изображении (обведена красным), тогда как кривая, уникальная для 0 (обведена синим), и кривая, уникальная для 6 ( обведены зеленым) слабее.



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


Следовательно, эта общая область скрытого пространства будет представлять и шестерки, и нули, потому что они имеют сходные черты. Между скрытыми точками пространства, которые представляют «чистую» шестерку и «чистый» ноль (т. е. фигуру, которая, очевидно, является 6 и не может быть интерпретирована как ноль и наоборот), вариационный автоэнкодер научится отображать промежуточные точки на изображения, которые можно разумно интерпретировать как «6» или «0». Декодирование промежуточных точек дает моментальные снимки непрерывных преобразований одной формы в другую.


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



Характеристика остального скрытого пространства


Описанный выше процесс будет повторяться с каждым изображением во время обучения по всему скрытому пространству. Изображения, которые не похожи на 6 или 0, будут отброшены, но точно так же слипнутся с похожими изображениями. Ниже мы видим патч, который представляет девятки и семерки, и патч, который представляет восьмерки и единицы.



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


Ниже вы можете увидеть пример одного такого пути, который соединяет 8 с 6. Многие точки на пути создают убедительные данные, включая изображения, которые выглядят как пятерки, тройки и двойки:



Мы хотели бы еще раз подчеркнуть, что наше скрытое пространство было охарактеризовано как ландшафт характеристики, а не как ландшафт цифры. Декодер даже не знает, что такое «цифры» в том смысле, что информация о метках в наборе данных MNIST никогда не появляется в процессе обучения, однако декодер все еще может создавать убедительные изображения цифр. Следовательно, мы можем получить карту, как показано ниже, где каждая характерная особенность связана с определенным локусом:



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


  • Красный = чисто подключенная петля

  • Синий = соединенная петля с линией

  • Зеленый = несколько открытых петель

  • Фиолетовый = угловатые формы

  • Оранжевый = чистая вертикальная линия

  • Желтый = наклонная линия, частично открытая

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


Создание вариационного автоэнкодера с помощью Keras


Теперь, когда мы концептуально понимаем, как работают вариационные автоэнкодеры, давайте запачкаем руки и создадим вариационный автоэнкодер с помощью Keras! Вместо цифр мы воспользуемся набором данных Fashion MNIST, который содержит изображения разных предметов одежды в оттенках серого 28 на 28. [5] (https://www.assemblyai.com/blog/variational-autoencoders-for-dummies/#footnotes).


Настраивать


Во-первых, немного импорта, чтобы мы начали.


```javascript


из отображения импорта IPython


импортировать глобус


импорт изображения


импортировать matplotlib.pyplot как plt


импортировать numpy как np


импорт PIL


импортировать тензорный поток как tf


импортировать tensorflow_probability как tfp


время импорта


Давайте импортируем данные, используя встроенный в TensorFlow набор данных fashion_mnist. Мы показываем пример изображения, в данном случае ботинок, чтобы получить представление о том, как выглядит изображение.


```javascript


(train_images, ), (test_images, ) = tf.keras.datasets.fashion_mnist.load_data()


plt.imshow (train_images [0,:,:], cmap='gray_r')


плт.ось("выкл")



Мы моделируем каждый пиксель с помощью распределения Бернулли. Напомним, что распределение Бернулли эквивалентно биномиальному распределению с n = 1 и моделирует единственную реализацию эксперимента с бинарным результатом. В этом случае значение случайной величины 𝝌 соответствует тому, является ли пиксель «включенным» или «выключенным». То есть 𝝌=0 представляет полностью белый пиксель (интенсивность пикселя = 255), а 1 представляет полностью черный пиксель (интенсивность пикселя = 0). Обратите внимание, что приведенная выше цветовая карта перевернута, поэтому не смущайтесь, если значения пикселей кажутся перевернутыми.


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


Наконец, мы инициализируем некоторые соответствующие переменные и создаем объекты набора данных из данных. Объект набора данных перемешивает данные и сегментирует их на пакеты.


```javascript


определение preprocess_images (изображения):


images = images.reshape((images.shape[0], 28, 28, 1)) / 255.


вернуть np.where(изображения > .5, 1.0, 0.0).astype('float32')


train_images = предварительные_изображения (train_images)


test_images = предварительные_изображения (test_images)


plt.imshow (train_images [0,:,:], cmap='gray_r')


плт.ось("выкл")


plt.tight_layout()


train_size = train_images.shape[0]


размер партии = 32


test_size = test_images.shape[0]


train_dataset = (tf.data.Dataset.from_tensor_slices(train_images)


.shuffle(train_size).batch(batch_size))


test_dataset = (tf.data.Dataset.from_tensor_slices(test_images)


.shuffle(test_size).batch(batch_size))



Определение вариационного автоэнкодера


Сеть кодировщика


Теперь мы можем перейти к определению самой модели вариационного автоэнкодера Keras. Для начала мы определим сеть кодирования, которая представляет собой простую последовательность сверточных слоев с активацией ReLU. Обратите внимание, что окончательная свертка не имеет активацию. VAE со сверточными слоями иногда называют «CVAE» — Convolutional Variational AutoEncoders.


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


```javascript


класс CVAE (tf.keras.Model):


"""Сверточный вариационный автоэнкодер."""


def init(я, скрытый_дим):


супер(CVAE, сам).init()


self.latent_dim = скрытый_dim


self.encoder = tf.keras.Sequential(


tf.keras.layers.InputLayer (input_shape = (28, 28, 1)),


tf.keras.layers.Conv2D(


фильтры = 32, размер ядра = 3, шаги = (2, 2), активация = 'relu'),


tf.keras.layers.Conv2D(


фильтры = 64, размер ядра = 3, шаги = (2, 2), активация = 'relu'),


tf.keras.layers.Flatten(),


Нет активации


tf.keras.layers.Dense (скрытый_dim + скрытый_dim),


Сеть декодера


Далее нужно определить нашу сеть декодера. Вместо полностью подключенной к softmax последовательности, которая используется для сетей классификации, наша сеть декодера эффективно отражает сеть кодировщика. Автоэнкодеры обладают приятной симметрией — кодировщик изучает функцию f, которая отображается в скрытое пространство; декодер изучает обратную функцию f -1, которая отображает скрытое пространство обратно в исходное пространство. Слои Conv2DTranspose обеспечивают обучаемое повышение дискретизации для инвертирования сверточных слоев.


```javascript


self.decoder = tf.keras.Sequential(


tf.keras.layers.InputLayer (input_shape = (latent_dim,)),


tf.keras.layers.Dense (единицы = 7 * 7 * 32, активация = tf.nn.relu),


tf.keras.layers.Reshape (target_shape = (7, 7, 32)),


tf.keras.layers.Conv2DTranspose(


фильтры = 64, размер ядра = 3, шаги = 2, отступы = «то же самое»,


активация='релу'),


tf.keras.layers.Conv2DTranspose(


фильтры = 32, размер ядра = 3, шаги = 2, отступы = «то же самое»,


активация='релу'),


Нет активации


tf.keras.layers.Conv2DTranspose(


фильтры = 1, размер ядра = 3, шаги = 1, заполнение = «то же самое»),


Функции прямого прохода


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


Функция кодирования

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


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


```javascript


деф кодировать (я, х):


значит, logvar = tf.split(self.encoder(x), num_or_size_splits=2, axis=1)


среднее значение возврата, logvar


Функция репараметризации

Напомним, что мы не декодируем закодированный ввод напрямую, а скорее используем кодирование, чтобы определить, как мы делаем выборку из скрытого пространства. Вместо этого мы декодируем точку в скрытом пространстве, которая выбирается случайным образом в соответствии с распределением, определяемым параметрами, выдаваемыми нашей сетью кодирования. Может возникнуть соблазн просто использовать tf.random.normal() для выборки такой точки; но помните, что мы тренируем нашу модель, а это значит, что нам нужно выполнить обратное распространение. Это проблематично, потому что backprop не может проходить через случайный процесс, поэтому мы должны реализовать то, что известно как трюк перепараметризации:


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


```javascript


def reparameterize(self, mean, logvar):


eps = tf.random.normal(form=mean.shape)


вернуть eps * tf.exp(logvar * .5) + среднее значение


Функция декодирования

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


```javascript


def decode(self, z, apply_sigmoid=False):


логиты = self.decoder(z)


если применить_сигмоид:


probs = tf.sigmoid (логиты)


обратные пробы


возвращать логиты


Функция выборки

Учитывая репараметризованную выборку из распределения, функция выборки просто декодирует ввод. Если такой ввод не предоставлен, он будет случайным образом вводить 100 точек в скрытом пространстве, выбранном из стандартного нормального распределения.


Функция украшена @tf.function, чтобы преобразовать функцию в граф для более быстрого выполнения.


```javascript


@tf.function


образец определения (я, z = нет):


если г нет:


z = tf.random.normal (форма = (100, self.latent_dim))


вернуть self.decode(z, apply_sigmoid=True)


Расчет потерь


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


Уравнение из источника


На практике вычисляется только одна выборочная оценка ELBO по методу Монте-Карло:


Уравнение из источника


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


```javascript


def log_normal_pdf (выборка, среднее значение, logvar, raxis = 1):


log2pi = tf.math.log(2. * np.pi)


вернуть tf.reduce_sum(


-.5 * ((выборка - среднее) ** 2. * tf.exp(-logvar) + logvar + log2pi),


ось = ось)


Теперь мы определяем нашу функцию потерь, которая состоит из следующих шагов:


  1. Рассчитать параметры распределения для изображения с помощью кодирования.

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

  1. Рассчитайте бинарную перекрестную энтропию между входным изображением и декодированным изображением.

  1. Рассчитайте значения условного распределения, скрытого априорного распределения (смоделированного в виде единичного гауссова распределения) и приблизительного апостериорного распределения.

  1. Рассчитать ЭЛБО

  1. Отменить ELBO и вернуть его

  1. \

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


Наконец, отметим, что tf.nn.sigmoid_cross_entropy_with_logits() используется для численной стабильности, поэтому мы вычисляем логиты и не пропускаем их через сигмоид при декодировании.


```javascript


определение потери_вычисления (модель, х):


значит, logvar = model.encode(x)


z = model.reparameterize (среднее, logvar)


x_logit = model.decode(z)


cross_ent = tf.nn.sigmoid_cross_entropy_with_logits (логиты = x_logit, метки = x)


logpx_z = -tf.reduce_sum (cross_ent, ось = [1, 2, 3])


logpz = log_normal_pdf(z, 0., 0.)


logqz_x = log_normal_pdf(z, среднее, logvar)


вернуть -tf.reduce_mean (logpx_z + logpz - logqz_x)


Шаг обучения


Наконец, мы определяем наш шаг обучения обычным способом. Мы вычисляем потери на GradientTape, выполняем обратное распространение для вычисления градиента, а затем делаем шаг с оптимизатором, учитывая градиент. Опять же, мы украшаем этот метод как tf.function для увеличения скорости.


```javascript


@tf.function


def train_step (модель, x, оптимизатор):


"""Выполняет один шаг обучения и возвращает проигрыш.


Эта функция вычисляет потери и градиенты и использует последние для


обновить параметры модели.


с tf.GradientTape() в качестве ленты:


потеря = вычислить_потеря (модель, х)


градиенты = лента.градиент (потери, модель.trainable_variables)


оптимизатор.apply_gradients(zip(градиенты, model.trainable_variables))


Повышение квалификации


Настраивать


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


```javascript


скрытый_дим = 2


эпох = 10


модель = CVAE (latent_dim)


Функция построения графика


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


```javascript


def plot_latent_images (модель, n, эпоха, im_size = 28, save = True, first_epoch = False, f_ep_count = 0):


Создаем матрицу изображений


image_width = im_size*n


высота_изображения = ширина_изображения


изображение = np.zeros((image_height, image_width))


Создать список значений, которые равномерно распределены относительно массы вероятности


норма = tfp.distributions.Normal(0, 1)


grid_x = norm.quantile (np.linspace (0,05, 0,95, n))


grid_y = norm.quantile (np.linspace (0,05, 0,95, n))


Для каждой точки сетки в скрытом пространстве декодировать и


копируем изображение в массив изображений


для i, yi в перечислении (grid_x):


для j, xi в перечислении (grid_y):


z = np.array([[xi, yi]])


x_decoded = model.sample(z)


цифра = tf.reshape (x_decoded [0], (im_size, im_size))


изображение[i * im_size: (i + 1) * im_size,


j * im_size: (j + 1) * im_size] = digit.numpy()


Построение массива изображений


plt.figure(figsize=(10, 10))


plt.imshow (изображение, cmap = 'Greys_r')


плт.ось('Выкл')


Потенциально сохранить с другим форматированием, если в первой эпохе


если сохранить и first_epoch:


plt.savefig('tf_grid_at_epoch_{:04d}.{:04d}.png'.format(эпоха, f_ep_count))


Элиф сохранить:


plt.savefig('tf_grid_at_epoch_{:04d}.png'.format(эпоха))


plt.show()


Тренировочный цикл


Наконец-то мы готовы начать обучение! Мы сохраняем снимок нашего скрытого пространства, используя функцию выше, прежде чем мы начнем изучать и создавать экземпляр оптимизатора Adam. После этого мы входим в наш тренировочный цикл, который просто включает в себя повторение каждого тренировочного пакета и выполнение train_step(). После того, как все пакеты обработаны, мы вычисляем потери в тестовом наборе с помощью compute_loss(), а затем возвращаем отрицательную величину средних потерь, чтобы получить ELBO. Здесь мы возвращаем отрицательное среднее значение потерь, потому что мы изменили знак в нашей функции compute_loss(), чтобы использовать обучение с градиентным спуском.


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


```javascript


tf.config.run_functions_eagerly(Истина)


plot_latent_images (модель, 20, эпоха = 0)


оптимизатор = tf.keras.optimizers.Adam(1e-4)


для эпохи в диапазоне (1, эпохи + 1):


start_time = время.время()


для idx, train_x в перечислении (train_dataset):


train_step (модель, train_x, оптимизатор)


если эпоха == 1 и idx% 75 == 0:


plot_latent_images (модель, 20, эпоха = эпоха, first_epoch = True, f_ep_count = idx)


end_time = время.время()


потеря = tf.keras.metrics.Mean()


для test_x в test_dataset:


потеря (compute_loss (модель, test_x))


Эльбо = -потеря.результат()


display.clear_output(wait=False)


print('Эпоха: {}, тестовый набор ELBO: {}, истекшее время для текущей эпохи: {}'


.format(эпоха, elbo, end_time - start_time))


если эпоха != 1:


plot_latent_images (модель, 20, эпоха = эпоха)


Результаты


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


```javascript


аним_файл = 'сетка.gif'


с imageio.get_writer(anim_file, mode='I') в качестве автора:


имена файлов = glob.glob('tf_grid*.png')


имена файлов = отсортированные (имена файлов)


для имени файла в именах файлов:


печать (имя файла)


изображение = imageio.imread (имя файла)


писатель.append_data(изображение)


изображение = imageio.imread (имя файла)


писатель.append_data(изображение)


Вот пример обучающего GIF, сгенерированного с помощью этой функции:



А вот и финальный снимок по окончанию обучения:



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


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


VAE — бесценный метод генерации данных, и в настоящее время они доминируют в области генерации данных в сочетании с [GAN] (https://www.assemblyai.com/blog/pytorch-lightning-for-dummies/#building-a- ган-с-пыторч-молнией)с. Мы увидели, как и почему автоэнкодеры не могут предоставить убедительные данные, и как вариационные автоэнкодеры просто, но мощно расширяют эти архитектуры, чтобы быть специально адаптированными для задачи создания изображений. Мы создали вариационный автоэнкодер Keras с помощью Python и использовали этот MNIST VAE для создания правдоподобных изображений одежды.


Сноски


  1. Это изображение получено из [этого] (https://github.com/cazala/mnist) репозитория GitHub.

  1. Изображение взято с [этой] (https://www.saraai.com/blog/tag/mnist) страницы.

  1. Изображение взято с [этой] (https://www.researchgate.net/figure/Example-1-of-MNIST-M-1-digit-one_fig4_357264093) страницы.

  1. Это преобразование на самом деле не является непрерывным, потому что нам нужно «сломать» ноль, а затем снова соединить его с другой частью самого себя, но остальная часть преобразования непрерывна.

  1. Этот пример адаптирован с веб-сайта TensorFlow.

Также опубликовано [Здесь] (https://www.assemblyai.com/blog/introduction-to-variational-autoencoders-using-keras/)



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE