Траектория «Нейросети» · конспект 9 из 12

Автоэнкодеры и вариационные автоэнкодеры

О чём эта тема
Первый конспект про обучение без учителя: сеть учится сжимать объекты в компактное скрытое представление и восстанавливать их обратно. Разбираются автоэнкодер и его применения (очистка от шума, поиск аномалий), пределы автоэнкодера как генератора и вариационный автоэнкодер (VAE) — первая генеративная модель траектории. По материалам лекции курса об автоэнкодерах.
Аннотация
Конспект открывается постановкой задачи обучения без учителя и идеей обучения представлений. Затем вводится снижение размерности: предположение о многообразии и метод главных компонент с интерактивным тренажёром проекции. Центральная часть посвящена автоэнкодеру — архитектуре из кодировщика и декодировщика, обучаемой восстанавливать собственный вход, — и его применениям: очистке изображений от шума, обнаружению аномалий по ошибке реконструкции, предобучению. Далее показываются пределы автоэнкодера как генератора: дыры в латентном пространстве и артефакты интерполяции. Из этих проблем выводится вариационный автоэнкодер: кодировщик предсказывает параметры распределения, репараметризация пропускает градиент через выборку, KL-дивергенция стягивает латентное пространство к стандартному нормальному распределению. Приводится реализация VAE на Keras и кратко обсуждается условный вариант (CVAE).
Пререквизиты
Конспект 2 (функция ошибки, градиент), конспект 3 (Dense, Keras, MNIST-подобные данные, функциональный API — конспект 7, раздел 4), конспект 5 (переобучение). Нормальное распределение — из курса теории вероятностей.
Мотивация
Во всех предыдущих конспектах обучение требовало меток, а разметка — самая дорогая часть подготовки данных (конспект 5, раздел 5.1). При этом неразмеченных данных обычно на порядки больше, чем размеченных. Автоэнкодер учится на данных без всяких меток — «меткой» служит сам объект — и извлекает из них представление, которое затем можно использовать для сжатия, очистки от шума, поиска аномалий и генерации новых объектов.

1. Обучение без учителя

«Если интеллект — это торт, то основная часть торта — обучение без учителя, глазурь на нём — обучение с учителем, а вишенка сверху — обучение с подкреплением» (Ян Лекун)

До сих пор каждая задача была задачей обучения с учителем: каждому объекту сопоставлена метка, и сеть учится её предсказывать. В обучении без учителя (англ. unsupervised learning) объекты известны, но меток нет — модель должна сама найти в данных структуру, неявно разделяющую объекты по похожести.

Зачем это нужно? Во-первых, размеченных объектов часто слишком мало, а неразмеченных — много, и хочется, чтобы данные «сами разделились» согласованно с будущими метками. Во-вторых, обучение без учителя часто даёт представления, позволяющие быстро адаптироваться к новым задачам — иногда эффективнее, чем перенос обучения (конспект 7). Область, изучающая такие представления, называется обучением представлений (англ. representation learning): вместо ручного конструирования признаков сеть выучивает их сама.

2. Снижение размерности и метод главных компонент

Почему объект вообще можно описать меньшим числом признаков? В глубоком обучении используют предположение о многообразии (англ. manifold assumption): реальные данные не распределены равномерно по всему пространству признаков, а занимают лишь его малую часть — многообразие. Лица людей на фотографиях 300×300 лежат в пространстве заметно меньшей размерности, чем 90 000: далеко не каждая матрица чисел даёт изображение человека.

Классический линейный способ снижения размерности — метод главных компонент (англ. Principal Component Analysis, PCA): объекты проецируются на подпространство, выбранное так, чтобы восстановление обратно давало наименьшую ошибку. Каждая следующая компонента перпендикулярна предыдущим и объясняет наибольшую часть оставшейся дисперсии.

Тренажёр: проекция и ошибка восстановления

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

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

3. Автоэнкодер

Автоэнкодер — архитектура из двух сетей. Кодировщик \(E\) сжимает признаковое описание объекта в вектор небольшой размерности \(z = E(x)\) — скрытое (латентное) представление. Декодировщик \(D\) восстанавливает из этого вектора объект в исходном пространстве:

\[ \hat{x} = D(E(x)), \qquad \dim z \ll \dim x \tag{9.1}\]

Автоэнкодер для MNIST (изображения 28×28 = 784): узкое место в середине — латентное представление. Левая половина — кодировщик, правая — декодировщик.

Обучается автоэнкодер минимизацией ошибки реконструкции — расхождения между входом и восстановлением; меткой объекта служит сам объект:

\[ L_{\text{AE}} = \bigl\| x - D(E(x)) \bigr\|^2 \tag{9.2}\]

(вместо квадрата отклонения, как в конспекте 2, формула (2.2), для изображений с яркостями в \([0,1]\) часто берут бинарную перекрёстную энтропию). На Keras:

from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model

latent_dim = 24

x = Input(shape=(784,))
h = Dense(256, activation='relu')(x)
z = Dense(latent_dim, activation='relu')(h)      # скрытое представление
h_dec = Dense(256, activation='relu')(z)
x_hat = Dense(784, activation='sigmoid')(h_dec)  # восстановление

autoencoder = Model(x, x_hat)
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')

# вход и «метка» — одно и то же изображение
autoencoder.fit(x_train, x_train, epochs=20, batch_size=100,
                validation_data=(x_test, x_test))
Два ряда рукописных цифр: исходные изображения и их восстановления автоэнкодером, слегка размытые
Вверху — исходные цифры MNIST, внизу — восстановленные автоэнкодером: детали сглажены, но цифры узнаваемы. Из лекции курса об автоэнкодерах
Типичная ошибка Делают латентное представление не меньше входа. Если узкого места нет, сети выгодно выучить тождественное отображение — скопировать вход на выход с нулевой ошибкой, не извлекая никакой структуры. Польза автоэнкодера возникает именно из ограничения: сжать и восстановить.

4. Применения автоэнкодера

4.1. Очистка от шума

Шумоподавляющий автоэнкодер (англ. denoising autoencoder) обучается восстанавливать чистое изображение по зашумлённому входу: в fit подаётся пара «зашумлённый вход → чистая цель». Латентное пространство хранит существенную структуру объекта, а случайный шум в него просто не помещается:

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

4.2. Обнаружение аномалий

Автоэнкодер обучают только на «нормальных» данных. Нормальные примеры он восстанавливает с малой ошибкой реконструкции; аномальные, которых он не видел при обучении, — с большой. По распределению ошибок на обучающих данных выбирают порог: всё, что восстановилось хуже порога, объявляется аномалией. Выбор порога — компромисс между пропуском аномалий и ложными срабатываниями (те же ошибки первого и второго рода, что в конспекте 4).

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

4.3. Предобучение

Кодировщик, обученный без меток на большом корпусе, можно использовать как экстрактор признаков для задачи с малым числом размеченных примеров — та же логика, что у переноса обучения (конспект 7, раздел 8), но без необходимости в чужой размеченной выборке вроде ImageNet.

5. Автоэнкодер как генератор и его пределы

Декодировщик превращает вектор латентного пространства в изображение — значит, подавая ему новые векторы, можно генерировать новые объекты. Но какие векторы подавать? Латентным пространством автоэнкодера никто не управлял, и оно получается неудобным:

Диаграмма рассеяния латентных представлений цифр MNIST у обычного автоэнкодера: классы вытянуты лучами с пустыми зонами между ними
Латентное пространство обычного автоэнкодера (размерность 2): классы вытянуты «лучами», между которыми — пустые зоны. Из лекции курса об автоэнкодерах

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

Ряд изображений: плавный переход от цифры 7 к цифре 6 у обычного автоэнкодера, промежуточные изображения нечитаемы
Интерполяция от «7» к «6» у обычного автоэнкодера: в середине — объекты, не похожие на цифры. Из лекции курса об автоэнкодерах

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

6. Вариационный автоэнкодер

6.1. Кодировать распределение, а не точку

Вариационный автоэнкодер (англ. Variational Autoencoder, VAE; Kingma, Welling, 2013) меняет кодировщик: вместо одной точки латентного пространства он предсказывает для объекта распределение — вектор средних \(\mu\) и вектор логарифмов дисперсий \(\log\sigma^2\) многомерного нормального распределения с независимыми компонентами. Латентное представление получается выборкой из этого распределения, а декодировщик обязан восстанавливать объект из любой такой выборки — окрестность каждого объекта в латентном пространстве становится «осмысленной».

6.2. Репараметризация

Через генератор случайных чисел нельзя пропустить градиент. Помогает свойство нормального распределения \(N(\mu, \sigma^2) = \mu + \sigma \cdot N(0, 1)\): выборку берут из стандартного нормального распределения и детерминированно преобразуют:

\[ z = \mu + \sigma \odot \varepsilon, \qquad \varepsilon \sim N(0, I) \tag{9.3}\]

Случайность вынесена в независимый вход \(\varepsilon\), а путь градиента к \(\mu\) и \(\sigma\) полностью детерминирован — это репараметризация (англ. reparametrization trick).

6.3. KL-дивергенция: стянуть пространство к N(0, 1)

Осталась проблема: сети выгодно занулить \(\sigma\) и вернуться к обычному автоэнкодеру с его дырами. Поэтому вводится регуляризация: распределение каждого объекта должно быть близко к стандартному нормальному. Мерой близости распределений служит дивергенция Кульбака–Лейблера; для пары нормальных распределений она вычисляется аналитически:

\[ KL\bigl(N(\mu,\sigma^2)\,\|\,N(0,1)\bigr) = -\tfrac{1}{2}\bigl(1 + \log\sigma^2 - \mu^2 - \sigma^2\bigr) \tag{9.4}\]
Тренажёр: репараметризация и KL-дивергенция

Пунктир — целевое распределение N(0, 1), сплошная кривая — распределение кодировщика N(μ, σ²), точки на оси — выборка z = μ + σε по формуле (9.3) при фиксированных ε. KL-дивергенция (9.4) равна нулю только при μ = 0, σ = 1 и растёт при любом отклонении — в том числе при σ → 0, что и не даёт распределению схлопнуться в точку.

Если обучать сеть только на KL-дивергенцию, декодировщик игнорируется, и кодировщик просто отображает все объекты в стандартное нормальное распределение — классы полностью перемешиваются:

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

Поэтому полная функция ошибки VAE складывается из ошибки реконструкции (как у обычного автоэнкодера) и KL-дивергенции:

\[ L_{\text{VAE}} = \mathrm{BCE}(x, \hat{x}) + KL\bigl(Q(z|x)\,\|\,N(0,1)\bigr) \tag{9.5}\]

Первое слагаемое заставляет декодировщик корректно восстанавливать объекты, второе — держит латентное пространство связным и известным. Результат — компактное пространство без дыр, где близкие по смыслу объекты лежат рядом:

Латентное пространство VAE: классы цифр образуют компактные соседствующие области без пустых зон
Латентное пространство VAE: связное, без дыр
Сетка изображений цифр, сгенерированных декодировщиком VAE по регулярной сетке точек латентного пространства
Генерация по регулярной сетке латентных точек: плавные переходы между цифрами

Из лекции курса об автоэнкодерах

Ряд изображений: плавный переход от цифры 7 к цифре 6 у VAE, все промежуточные изображения похожи на цифры
Та же интерполяция от «7» к «6», но у VAE: промежуточные изображения остаются правдоподобными. Из лекции курса об автоэнкодерах

6.4. Реализация на Keras

Код по мотивам ноутбука курса (VAE для MNIST, латентная размерность 2):

import tensorflow as tf
from tensorflow.keras.layers import Input, Dense, Lambda
from tensorflow.keras.models import Model

original_dim, intermediate_dim, latent_dim = 784, 256, 2

# кодировщик: из изображения — параметры распределения
x = Input(shape=(original_dim,))
h = Dense(intermediate_dim, activation='relu')(x)
z_mean = Dense(latent_dim)(h)
z_log_var = Dense(latent_dim)(h)

# репараметризация: z = mu + sigma * eps — формула (9.3)
def sampling(args):
    z_mean, z_log_var = args
    eps = tf.random.normal(shape=tf.shape(z_mean))
    return z_mean + tf.exp(z_log_var / 2) * eps

z = Lambda(sampling)([z_mean, z_log_var])

# декодировщик (слои создаются отдельно, чтобы переиспользовать их в генераторе)
decoder_h = Dense(intermediate_dim, activation='relu')
decoder_out = Dense(original_dim, activation='sigmoid')
x_hat = decoder_out(decoder_h(z))

vae = Model(x, x_hat)

# ошибка (9.5): реконструкция + KL-дивергенция (9.4)
recon = original_dim * tf.keras.losses.binary_crossentropy(x, x_hat)
kl = -0.5 * tf.reduce_sum(1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var), axis=-1)
vae.add_loss(tf.reduce_mean(recon + kl))
vae.compile(optimizer='rmsprop')

vae.fit(x_train, epochs=50, batch_size=100, validation_data=(x_test, None))

# генератор новых цифр: отдельная модель из обученных слоёв декодировщика
z_in = Input(shape=(latent_dim,))
generator = Model(z_in, decoder_out(decoder_h(z_in)))
Типичная ошибка Путают \(\sigma\) и \(\log\sigma^2\). Кодировщик выдаёт именно логарифм дисперсии (он может быть любым вещественным числом, что удобно для обучения), поэтому в репараметризации стоит tf.exp(z_log_var / 2), а в KL-дивергенции — tf.exp(z_log_var). Подстановка выхода сети как «сигмы» напрямую ломает обе формулы.

7. Условный вариационный автоэнкодер

У обученного VAE нельзя попросить «сгенерируй именно пятёрку»: класс и стиль перемешаны в одном латентном векторе. Условный VAE (CVAE) решает это, подавая метку класса дополнительным входом и кодировщику, и декодировщику. Тогда латентному пространству остаётся кодировать только стиль написания цифры (наклон, толщину, форму штрихов), а класс задаётся явно: можно генерировать заданную цифру со случайным стилем или переносить стиль одной цифры на другую — разделение (англ. disentangling) стиля и метки.

Сетка изображений одной и той же цифры, сгенерированных CVAE: класс задан меткой, вариации задаются латентным пространством стиля
CVAE: метка фиксирует класс цифры, латентное пространство задаёт стиль. Из лекции курса об автоэнкодерах

У «ванильного» VAE есть и ограничения: изображения получаются размытыми — перекрёстная энтропия на пикселях поощряет усреднённые, «безопасные» реконструкции. Следующие два конспекта — про генеративные модели, дающие резкие изображения: диффузионные модели и GAN.

Контрольные вопросы