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

Генеративно-состязательные сети: 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)\); цель фальшивомонетчика — прямо противоположная. Формально это минимаксная игра:

\[ \min_G \max_D V(D, G) = E_{x \sim p_{\text{данных}}}\bigl[\log D(x)\bigr] + E_{z \sim N(0,\mathbf{I})}\bigl[\log\bigl(1 - D(G(z))\bigr)\bigr] \tag{11.1}\]

При фиксированном генераторе оптимальный дискриминатор выписывается явно:

\[ D^*(x) = \frac{p_{\text{данных}}(x)}{p_{\text{данных}}(x) + p_G(x)}, \tag{11.2}\]

где \(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)))\). Дискриминатор максимизирует их сумму:

\[ L_D = \max_D\; \bigl[\ln D(x) + \ln\bigl(1 - D(G(z))\bigr)\bigr] \tag{11.3}\]

Формально генератор минимизирует то же выражение, но на практике вместо минимизации \(\ln(1 - D(G(z)))\) генератор максимизирует \(\ln D(G(z))\):

\[ L_G = \max_G\; \ln D\bigl(G(z)\bigr) \tag{11.4}\]

— обе формы имеют один и тот же смысл («убедить дискриминатор»), но у второй градиент не затухает в начале обучения, когда дискриминатор легко отличает ещё слабые подделки. В коде обе потери выражаются через готовую бинарную перекрёстную энтропию; реализация из презентации курса (с размытием меток — о нём в разделе 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, не останавливается на границе сетей). Схема трёх шагов из презентации курса:

Три схемы обучения: дискриминатор учится выдавать единицу на реальных изображениях, ноль на подделках, затем связка с замороженным дискриминатором учится выдавать единицу на подделках
Три шага цикла обучения: дискриминатор → «1» на реальных; дискриминатор → «0» на подделках; генератор с замороженным дискриминатором → «1» на подделках. Из презентации курса о GAN
Тренажёр: обмани оптимальный дискриминатор

Вы играете за генератор. Пунктир — распределение реальных данных (N(2, 0,5²), данные иллюстративные), сплошная кривая — ваше распределение N(m, s²). Против вас всегда играет оптимальный дискриминатор (11.2) — его точность и значение V(D*, G) вычисляются численно. Подберите m и s так, чтобы точность упала до 50 %: ровно это делает градиентный спуск при обучении настоящего GAN.

На той же игрушечной параболе состязательное обучение справляется: после нескольких тысяч шагов сгенерированные точки покрывают всю кривую, а не её «среднее»:

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

4. DCGAN: свёрточный генератор

Для изображений полносвязный генератор работает плохо. DCGAN (Radford et al., 2015) использует в дискриминаторе обычные свёртки с шагом 2 (вместо пулинга), а в генераторе — транспонированные свёртки, повышающие разрешение: каждый пиксель входа умножается на обучаемое ядро и превращается во фрагмент, перекрывающиеся фрагменты суммируются. Это ровно та операция, чьи шахматные артефакты разбирались в конспекте 6 — и туда же уходит рецепт замены на resize-свёртку, если артефакты мешают:

Слева случайное цветное изображение 10 на 10, справа результат транспонированной свёртки размером 12 на 12
Транспонированная свёртка со случайным ядром увеличивает изображение 10×10 до 12×12. Из лекции курса

Генератор DCGAN — зеркальное отражение классификатора: из вектора шума полносвязный слой делает маленькую «карту признаков», которую свёрточные блоки шаг за шагом доращивают до изображения:

Схема генератора DCGAN: вектор шума 100 преобразуется в тензор 4×4×1024 и через четыре транспонированные свёртки дорастает до изображения 64×64×3
Генератор DCGAN (Radford et al., 2015): шум 100 → 4×4×1024 → … → изображение 64×64×3. Из презентации курса о GAN

Реализация из ноутбука курса (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 — от чистого шума к всё более структурным объектам:

Четыре панели сгенерированных изображений: сначала серый шум, затем размытые пятна, затем всё более чёткие силуэты одежды
Сгенерированные изображения по мере обучения DCGAN: шум → пятна → силуэты. Из лекции курса
Два ряда изображений одежды, сгенерированных обученной DCGAN
Результат обученной 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 (дискриминатор дополнительно классифицирует объект — стабилизирует обучение).

Сетка цифр, сгенерированных условным GAN: каждая строка — один заданный класс от 0 до 9, внутри строки цифры различаются стилем
Генерация cGAN: каждая строка — заданный меткой класс, вариации внутри строки задаёт шум. Из презентации курса о GAN

6. Оценка качества и латентное пространство

Как понять, что GAN обучился хорошо? Качественно смотрят на три вещи: сходство сгенерированных изображений с обучающей выборкой, отсутствие точных дубликатов из неё и разнообразие результатов. Количественная метрика — FID (Frechet Inception Distance): оба набора изображений — реальный и сгенерированный — пропускаются через сеть InceptionV3 (конспект 7) без классифицирующего слоя, и сравниваются распределения полученных признаков:

\[ \mathrm{FID} = \bigl\|\mu_r - \mu_g\bigr\|^2 + \mathrm{Tr}\Bigl(\Sigma_r + \Sigma_g - 2\bigl(\Sigma_r \Sigma_g\bigr)^{1/2}\Bigr), \tag{11.5}\]

где \(\mu\) и \(\Sigma\) — средние и ковариационные матрицы признаков реальных (\(r\)) и сгенерированных (\(g\)) изображений. Чем FID меньше, тем ближе распределения — тем лучше модель.

Полезный диагностический приём — интерполяция в латентном пространстве (тот же приём, что у VAE в конспекте 9): взяв два вектора \(z_1, z_2\) и двигаясь по отрезку

\[ z = t \cdot z_1 + (1 - t) \cdot z_2, \qquad t \in [0, 1], \tag{11.6}\]

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

Сетка изображений кошачьих морд: каждая строка — плавный переход от одной морды к другой через промежуточные правдоподобные варианты
Интерполяция (11.6) между латентными векторами обученного GAN: каждая строка — плавный переход между двумя сгенерированными кошками. Из презентации курса о GAN

7. Проблемы и тонкости обучения GAN

GAN сложно обучать — многие модели вовсе не сходятся к приемлемой точке. Две типовые проблемы имеют собственные имена.

Затухающие градиенты. Если дискриминатор слишком хорош, обучение генератора останавливается: оптимальный дискриминатор уверенно выдаёт нули на подделках, и градиент, протекающий через него к генератору, практически исчезает — недостаточно информации для обучения. Отсюда и небольшая «фора» слабых подделок в форме потерь (11.4).

Коллапс моды (англ. mode collapse) — режим, при котором генератор выдаёт лишь сильно ограниченный набор изображений: он нашёл несколько подделок, стабильно обманывающих дискриминатор, и перестал покрывать остальное разнообразие данных:

Сетка сгенерированных кошачьих морд, среди которых многократно повторяются почти одинаковые изображения
Коллапс моды: генератор повторяет почти одинаковых кошек. Из презентации курса о GAN

Практические приёмы стабильного обучения (сводка из материалов курса):

8. Семейство GAN

На базовой состязательной схеме выросло целое семейство архитектур; несколько заметных представителей из презентации курса.

ProGAN (progressive growing GAN) — обучение начинается с крошечных изображений 4×4, и по мере сходимости обе сети наращивают новые слои, поднимая разрешение до 1024×1024. Обучение стабильнее, изображения реалистичнее, но время обучения растёт:

Схема прогрессивного роста GAN: генератор и дискриминатор начинают с разрешения 4 на 4 и постепенно добавляют слои до 1024 на 1024
Прогрессивный рост генератора и дискриминатора. Из презентации курса о GAN
Фотореалистичные лица, сгенерированные ProGAN
Лица, сгенерированные ProGAN. Из презентации курса о GAN

Pix2pix — GAN для перевода «изображение → изображение»: генератор получает на вход не шум, а изображение (например, контурный набросок), дискриминатор проверяет пару «вход + результат». Требует парных примеров «до/после»:

Контурный рисунок кота слева превращается моделью pix2pix в фотореалистичное изображение кота справа
Pix2pix: из наброска — изображение. Из презентации курса о GAN

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

Примеры CycleGAN: картины Моне превращаются в фотографии, зебры в лошадей, летние пейзажи в зимние
CycleGAN: перенос между доменами без парных примеров. Из презентации курса о GAN

SRGAN (super resolution GAN) повышает разрешение изображения: генератор достраивает детали, а дискриминатор не даёт ему ограничиться простым сглаживанием, как это делает интерполяция:

Сравнение повышения разрешения: бикубическая интерполяция размыта, SRGAN восстанавливает детали, близкие к оригиналу
SRGAN против бикубической интерполяции. Из презентации курса о GAN

Упомянем также StackGAN (генерация изображения по текстовому описанию в два этапа) и WGAN (замена перекрёстной энтропии на расстояние Вассерштейна, радикально стабилизирующая обучение).

Итоговое сравнение трёх генеративных подходов траектории: VAE (конспект 9) обучается устойчиво, но даёт размытые изображения; GAN генерирует резкие изображения за один проход, но капризен в обучении и склонен к коллапсу моды; диффузионные модели (конспект 10) обучаются устойчиво и дают лучшее качество, но требуют сотен проходов сети на одно изображение.

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