Генеративно-состязательные сети: GAN и cGAN
- О чём эта тема
- Генеративно-состязательная сеть учит генератор не по формуле ошибки, а второй нейросетью: дискриминатор отличает подделки от реальных данных, а генератор учится его обманывать. Разбираются постановка задачи генерации, минимаксная игра, свёрточный вариант DCGAN с реализацией на Keras и условный GAN. По материалам лекции курса о генеративных моделях.
- Аннотация
- Конспект начинается с постановки задачи генерации и роли случайности: входного латентного пространства. Затем демонстрируется, почему наивный подход — обучать генератор обычной регрессией — проваливается, и из этой неудачи выводится идея дискриминатора: второй сети, которая сама учится отличать реальные объекты от сгенерированных. Формулируется минимаксная игра генератора и дискриминатора с аналогией фальшивомонетчика и банкира, выводятся функции потерь обеих сетей из перекрёстной энтропии, а интерактивный тренажёр предлагает обмануть оптимальный дискриминатор вручную. Далее разбирается DCGAN — свёрточный GAN с транспонированными свёртками в генераторе — с полной реализацией на Keras и картиной хода обучения, затем условный GAN (cGAN) и кодирование меток эмбеддингами. Обсуждаются оценка качества генерации (метрика FID, интерполяция в латентном пространстве), типовые проблемы — затухающие градиенты и коллапс моды — с приёмами стабильного обучения, и семейство архитектур на состязательной основе: ProGAN, Pix2pix, CycleGAN, SRGAN.
- Пререквизиты
- Конспект 9 (задача генерации без учителя, латентное пространство), конспект 6 (транспонированная свёртка и её артефакты), конспект 4 (свёртка, бинарная классификация), конспект 10 (для сравнения подходов к генерации).
- Мотивация
- Чтобы обучать генератор «в лоб», нужна функция ошибки, которая скажет, насколько сгенерированное изображение «похоже на настоящее». Записать такую функцию формулой не получается: попиксельное сравнение со случайным реальным примером требует от сети угадать именно этот пример. Идея GAN (Гудфеллоу и соавт., 2014) — не выдумывать функцию ошибки, а выучить её: пусть качество подделки оценивает вторая нейросеть.
1. Задача генерации и латентное пространство
Постановка задачи генерации: дано множество неразмеченных объектов; требуется создавать новые объекты, которые похожи на исходные, но не повторяют их в точности. Чтобы результат различался от запуска к запуску, нужен элемент случайности.
Идея «превратить простой шум в нужное распределение» — классическая. Преобразование Бокса–Мюллера превращает пару равномерно распределённых чисел \(U_1, U_2\) в пару нормально распределённых: \(X = \sqrt{-2\log U_1}\,\cos(2\pi U_2)\). Генератор нейросетевой модели делает то же самое, только преобразование не выведено аналитически, а выучено, и целевое распределение — не нормальное, а распределение реальных изображений.
На вход генератора подаётся вектор случайного шума \(z\). Почему вектор, а не одно число? Каждую компоненту можно рассматривать как независимую «шкалу» свойств генерируемого объекта: чем больше степеней свободы, тем разнообразнее результат. Пространство таких векторов называют входным латентным пространством — термин тот же, что у автоэнкодеров (конспект 9), но здесь распределение задано заранее (обычно \(N(0, \mathbf{I})\)), а не выучено.
2. Наивный подход и его провал
Первая мысль — обучить генератор как обычную регрессию: подать шум на вход, случайные реальные объекты в качестве целей и минимизировать квадрат отклонения. Лекция курса проверяет это на игрушечной задаче — генерации точек параболы \(y = x^2\):
Причина провала принципиальна: шум и цели сопоставлены случайно, поэтому лучший ответ сети на любой вход — среднее по всем целям. Квадратичная ошибка поощряет усреднение (та же причина, по которой изображения VAE размыты — конспект 9). Нужен способ оценивать не «попал ли в конкретный пример», а «похож ли результат на реальные данные вообще».
3. Дискриминатор и минимаксная игра
Решение: наказывать генератор не формулой, а второй сетью. Дискриминатор (его также называют критиком) — бинарный классификатор (конспект 4), который учится определять, принадлежит ли объект распределению обучающей выборки. Классическая аналогия из лекции: фальшивомонетчик и банкир. Фальшивомонетчик \(G\) черпает «вдохновение» из случайного шума \(z\) и печатает подделки \(G(z)\); банкир \(D\) получает купюры и для каждой сообщает вероятность того, что она настоящая. Цель банкира — выдавать единицы на настоящих деньгах \(x\) и нули на подделках \(G(z)\); цель фальшивомонетчика — прямо противоположная. Формально это минимаксная игра:
При фиксированном генераторе оптимальный дискриминатор выписывается явно:
где \(p_G\) — распределение сгенерированных объектов. Если генератор идеален (\(p_G = p_{\text{данных}}\)), то \(D^*(x) = 1/2\) всюду — дискриминатор гадает вслепую, а значение игры (11.1) достигает минимума \(-2\log 2\).
3.1. Функции потерь из перекрёстной энтропии
Слагаемые формулы (11.1) — не изобретение, а обычная бинарная перекрёстная энтропия (конспект 3), расписанная для двух случаев. Для примера с меткой \(y\) и предсказанием \(\hat y\) она равна \(y\ln\hat y + (1-y)\ln(1-\hat y)\). При обучении дискриминатора на настоящих данных \(y = 1\), \(\hat y = D(x)\) — остаётся \(\ln D(x)\); на подделках \(y = 0\), \(\hat y = D(G(z))\) — остаётся \(\ln(1 - D(G(z)))\). Дискриминатор максимизирует их сумму:
Формально генератор минимизирует то же выражение, но на практике вместо минимизации \(\ln(1 - D(G(z)))\) генератор максимизирует \(\ln D(G(z))\):
— обе формы имеют один и тот же смысл («убедить дискриминатор»), но у второй градиент не затухает в начале обучения, когда дискриминатор легко отличает ещё слабые подделки. В коде обе потери выражаются через готовую бинарную перекрёстную энтропию; реализация из презентации курса (с размытием меток — о нём в разделе 7):
import tensorflow as tf class GANLoss: def __init__(self, from_logits=True, smoothing=0.4): self.bce = tf.keras.losses.BinaryCrossentropy( from_logits=from_logits, label_smoothing=smoothing) def discriminator_loss(self, real_output, fake_output): real_part = self.bce(tf.ones_like(real_output), real_output) # настоящие -> 1 fake_part = self.bce(tf.zeros_like(fake_output), fake_output) # подделки -> 0 return real_part + fake_part def generator_loss(self, real_output, fake_output): return self.bce(tf.ones_like(fake_output), fake_output) # формула (11.4)
Обучение чередуется: шаг дискриминатора (обычная бинарная классификация «реальное/подделка»), затем шаг генератора — по градиенту, протекающему сквозь дискриминатор (обратное распространение, конспект 2, не останавливается на границе сетей). Схема трёх шагов из презентации курса:
Вы играете за генератор. Пунктир — распределение реальных данных (N(2, 0,5²), данные иллюстративные), сплошная кривая — ваше распределение N(m, s²). Против вас всегда играет оптимальный дискриминатор (11.2) — его точность и значение V(D*, G) вычисляются численно. Подберите m и s так, чтобы точность упала до 50 %: ровно это делает градиентный спуск при обучении настоящего GAN.
На той же игрушечной параболе состязательное обучение справляется: после нескольких тысяч шагов сгенерированные точки покрывают всю кривую, а не её «среднее»:
4. DCGAN: свёрточный генератор
Для изображений полносвязный генератор работает плохо. DCGAN (Radford et al., 2015) использует в дискриминаторе обычные свёртки с шагом 2 (вместо пулинга), а в генераторе — транспонированные свёртки, повышающие разрешение: каждый пиксель входа умножается на обучаемое ядро и превращается во фрагмент, перекрывающиеся фрагменты суммируются. Это ровно та операция, чьи шахматные артефакты разбирались в конспекте 6 — и туда же уходит рецепт замены на resize-свёртку, если артефакты мешают:
Генератор DCGAN — зеркальное отражение классификатора: из вектора шума полносвязный слой делает маленькую «карту признаков», которую свёрточные блоки шаг за шагом доращивают до изображения:
Реализация из ноутбука курса (MNIST, вход — шум размерности 100):
from tensorflow.keras.models import Sequential, Model from tensorflow.keras.layers import (Input, Dense, Reshape, Flatten, Dropout, Conv2D, UpSampling2D, BatchNormalization, LeakyReLU) from tensorflow.keras.optimizers import Adam import numpy as np # генератор: шум 100 -> изображение 28×28×1 в диапазоне [-1, 1] generator = Sequential([ Dense(128 * 7 * 7, input_dim=100), LeakyReLU(0.2), BatchNormalization(), Reshape((7, 7, 128)), UpSampling2D(), # 7×7 -> 14×14 (resize-свёртка, конспект 6) Conv2D(64, (5, 5), padding='same'), LeakyReLU(0.2), BatchNormalization(), UpSampling2D(), # 14×14 -> 28×28 Conv2D(1, (5, 5), padding='same', activation='tanh'), ]) # дискриминатор: изображение -> вероятность «настоящее» discriminator = Sequential([ Conv2D(64, (5, 5), strides=(2, 2), input_shape=(28, 28, 1), padding='same'), LeakyReLU(0.2), Dropout(0.3), Conv2D(128, (5, 5), strides=(2, 2), padding='same'), LeakyReLU(0.2), Dropout(0.3), Flatten(), Dense(1, activation='sigmoid'), ]) discriminator.compile(loss='binary_crossentropy', optimizer=Adam()) # связка: шум -> генератор -> дискриминатор; веса дискриминатора заморожены discriminator.trainable = False gan_input = Input(shape=(100,)) gan = Model(gan_input, discriminator(generator(gan_input))) gan.compile(loss='binary_crossentropy', optimizer=Adam())
Цикл обучения чередует два шага — дискриминатор на смеси реальных изображений и подделок,
затем генератор через связку gan с целями «настоящее»:
def train(epochs=30, batch_size=128): for epoch in range(epochs): for _ in range(X_train.shape[0] // batch_size): # --- шаг дискриминатора: подделки + реальные изображения --- noise = np.random.normal(size=(batch_size, 100)) fakes = generator.predict(noise, verbose=0) reals = X_train[np.random.randint(0, X_train.shape[0], batch_size)] X = np.concatenate([fakes, reals]) y = [0] * batch_size + [1] * batch_size # 0 — подделка, 1 — настоящее discriminator.trainable = True discriminator.train_on_batch(X, y) # --- шаг генератора: убедить дискриминатор, что подделки настоящие --- noise = np.random.normal(size=(batch_size, 100)) discriminator.trainable = False gan.train_on_batch(noise, [1] * batch_size)
discriminator.trainable. Если дискриминатор остаётся
обучаемым внутри связки gan, шаг генератора «ломает» дискриминатор, подстраивая
его веса под ложные метки, — состязание вырождается. Дискриминатор обучается только на своём
шаге, при обучении связки его веса заморожены.
Ход обучения DCGAN — от чистого шума к всё более структурным объектам:
5. cGAN: GAN с условием
Как и VAE (конспект 9, раздел 7), GAN не принимает заказов: попросить у обученного генератора «именно пятёрку» нельзя. Условный GAN (англ. Conditional GAN, cGAN) добавляет метку класса и на вход генератора (вместе с шумом), и на вход дискриминатора — тот проверяет уже пару «объект + метка», поэтому для обучения нужен размеченный набор данных. Обучение не меняется: чередование шагов с реальными и сгенерированными примерами, только всюду добавлена метка.
Подавать метку сырым числом (0,1 для «единицы», 0,5 для «пятёрки») нельзя: вход становится
непрерывной шкалой, малое изменение которого должно резко менять генерируемый класс, — сети
трудно это выучить. Метку кодируют эмбеддингом — обучаемым вектором на каждый
класс (слой tf.keras.layers.Embedding(10, 10) для MNIST); эмбеддинг конкатенируется
со входами обеих сетей. У cGAN есть развития: Semi-Supervised GAN (дискриминатор сам
предсказывает метку), InfoGAN (метка не подаётся, а восстанавливается), AC-GAN (дискриминатор
дополнительно классифицирует объект — стабилизирует обучение).
6. Оценка качества и латентное пространство
Как понять, что GAN обучился хорошо? Качественно смотрят на три вещи: сходство сгенерированных изображений с обучающей выборкой, отсутствие точных дубликатов из неё и разнообразие результатов. Количественная метрика — FID (Frechet Inception Distance): оба набора изображений — реальный и сгенерированный — пропускаются через сеть InceptionV3 (конспект 7) без классифицирующего слоя, и сравниваются распределения полученных признаков:
где \(\mu\) и \(\Sigma\) — средние и ковариационные матрицы признаков реальных (\(r\)) и сгенерированных (\(g\)) изображений. Чем FID меньше, тем ближе распределения — тем лучше модель.
Полезный диагностический приём — интерполяция в латентном пространстве (тот же приём, что у VAE в конспекте 9): взяв два вектора \(z_1, z_2\) и двигаясь по отрезку
получают плавный переход между изображениями. У хорошо обученного генератора все промежуточные изображения правдоподобны — латентное пространство покрыто без «дыр»:
7. Проблемы и тонкости обучения GAN
GAN сложно обучать — многие модели вовсе не сходятся к приемлемой точке. Две типовые проблемы имеют собственные имена.
Затухающие градиенты. Если дискриминатор слишком хорош, обучение генератора останавливается: оптимальный дискриминатор уверенно выдаёт нули на подделках, и градиент, протекающий через него к генератору, практически исчезает — недостаточно информации для обучения. Отсюда и небольшая «фора» слабых подделок в форме потерь (11.4).
Коллапс моды (англ. mode collapse) — режим, при котором генератор выдаёт лишь сильно ограниченный набор изображений: он нашёл несколько подделок, стабильно обманывающих дискриминатор, и перестал покрывать остальное разнообразие данных:
Практические приёмы стабильного обучения (сводка из материалов курса):
- Диапазоны данных должны совпадать. Реальные изображения нормируют в
\([-1, 1]\), на выходе генератора ставят
tanh; если генератор выдаёт \([0, 1]\) — это готовая подсказка дискриминатору, состязание не начнётся. То же с размерами изображений. - Шум — из нормального распределения, а не равномерного.
- Раздельные батчи для настоящих и поддельных изображений на шаге дискриминатора.
- Размытие меток (англ. label smoothing): вместо жёстких 0 и 1 —
значения из диапазонов [0; 0,3] и [0,7; 1,2] (параметр
label_smoothingвGANLossвыше) — не даёт дискриминатору становиться самоуверенным. - LeakyReLU вместо ReLU и свёртки с шагом больше 1 вместо max-пулинга.
- Осторожнее с батч-нормализацией в дискриминаторе. Её статистика при обучении считается по минибатчу, а при применении — по скользящему среднему, что для GAN оборачивается непредсказуемым поведением; если нормализация нужна, предпочтительнее InstanceNorm.
- Преимущество дискриминатору. Если GAN обучается плохо, дискриминатор можно обучать чаще генератора (например, 5 шагов против одного): чем лучше он различает подделки, тем информативнее градиент для генератора.
- Оптимизатор — Adam (с моментом 0,5): практически все работы по GAN используют его; если качество плохое, причина почти наверняка не в оптимизаторе.
8. Семейство GAN
На базовой состязательной схеме выросло целое семейство архитектур; несколько заметных представителей из презентации курса.
ProGAN (progressive growing GAN) — обучение начинается с крошечных изображений 4×4, и по мере сходимости обе сети наращивают новые слои, поднимая разрешение до 1024×1024. Обучение стабильнее, изображения реалистичнее, но время обучения растёт:
Pix2pix — GAN для перевода «изображение → изображение»: генератор получает на вход не шум, а изображение (например, контурный набросок), дискриминатор проверяет пару «вход + результат». Требует парных примеров «до/после»:
CycleGAN снимает требование парности: два генератора учатся переводить изображения между двумя доменами (лошади ↔ зебры, фотография ↔ живопись) с условием цикличности: перевод туда и обратно должен вернуть исходное изображение:
SRGAN (super resolution GAN) повышает разрешение изображения: генератор достраивает детали, а дискриминатор не даёт ему ограничиться простым сглаживанием, как это делает интерполяция:
Упомянем также StackGAN (генерация изображения по текстовому описанию в два этапа) и WGAN (замена перекрёстной энтропии на расстояние Вассерштейна, радикально стабилизирующая обучение).
Итоговое сравнение трёх генеративных подходов траектории: VAE (конспект 9) обучается устойчиво, но даёт размытые изображения; GAN генерирует резкие изображения за один проход, но капризен в обучении и склонен к коллапсу моды; диффузионные модели (конспект 10) обучаются устойчиво и дают лучшее качество, но требуют сотен проходов сети на одно изображение.
Контрольные вопросы
-
Шум и цели сопоставлены случайно, поэтому оптимальный ответ сети на любой вход — среднее по всем целям: сгенерированные объекты сжимаются к «среднему» вместо того, чтобы покрыть всё распределение данных.
-
Дискриминатор — бинарный классификатор, максимизирующий V: стремится выдавать 1 на реальных данных и 0 на подделках. Генератор минимизирует V: подстраивает подделки так, чтобы дискриминатор принимал их за настоящие. Обучение чередует шаги двух сетей.
-
По формуле (11.2) D*(x) = 1/2 для любого объекта: распределения совпадают, различить их невозможно, точность дискриминатора падает до 50 % — это и есть целевое состояние обучения GAN.
-
Генератор — зеркальное отражение классификатора: из вектора шума полносвязный слой создаёт маленькую карту признаков, которую слои повышения разрешения доращивают до изображения. Разрешение растят транспонированные свёртки либо связка UpSampling2D + Conv2D (resize-свёртка из конспекта 6, не дающая шахматных артефактов).
-
Число делает вход непрерывной шкалой: малое изменение значения должно резко менять генерируемый класс, что сети трудно выучить. Эмбеддинг даёт каждому классу отдельный обучаемый вектор без ложной упорядоченности классов.
-
Шаг генератора обучается на ложных метках («подделка = настоящее»); если дискриминатор при этом обучаем, эти метки подстроят и его веса, разрушая его способность различать. Дискриминатор обучается только на собственном шаге с честными метками.
-
Режим, при котором генератор выдаёт лишь сильно ограниченный набор изображений, нашедших «слабое место» дискриминатора, вместо всего разнообразия данных. Замечают по повторяющимся почти одинаковым результатам при разных векторах шума — ещё одна причина смотреть на сами изображения, а не только на графики ошибок.
-
Реальные и сгенерированные изображения пропускают через InceptionV3 без классифицирующего слоя и сравнивают распределения признаков: расстояние между средними плюс член с ковариационными матрицами (формула (11.5)). Чем меньше FID, тем ближе распределение подделок к реальным данным — у хорошей модели FID ниже.