Фаза 04 · урок 09

Генерация изображений — GAN

Цель урока: Классификация учит сеть сопоставлять изображения с метками. Генерация переворачивает задачу: нужно сэмплировать новые изображения, выглядящие так, будто они получены из того же распределения. Нет «правильного» выхода, с которым можно…

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering с нуля
Фаза
Компьютерное зрение
Чтение
12 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Две сети
  5. Игра
  6. Non-saturating loss
  7. Правила архитектуры DCGAN
  8. Режимы отказа и их признаки
  9. Оценка
  10. Соберите
  11. Шаг 1: Генератор
  12. Шаг 2: Дискриминатор
  13. Шаг 3: Шаг обучения
  14. Шаг 4: Полный цикл обучения на синтетических фигурах
  15. Шаг 5: Сэмплирование
  16. Шаг 6: Спектральная нормализация
  17. Используйте
  18. Поставьте
  19. Упражнения
  20. Ключевые термины
  21. Дополнительные материалы

GAN — это две нейронные сети, участвующие в фиксированной игре. Одна рисует, другая критикует. Они совершенствуются вместе, пока рисунки не начнут обманывать критика.

Тип: Сборка Языки: Python Предварительные требования: Фаза 4, урок 03 (CNN), фаза 3, урок 06 (Оптимизаторы), фаза 3, урок 07 (Регуляризация) Время: ~75 минут

Цели обучения

  • Объяснить минимаксную игру между генератором и дискриминатором и почему равновесие соответствует p_model = p_data
  • Реализовать DCGAN в PyTorch и заставить её генерировать связные синтетические изображения 32x32 менее чем в 60 строках
  • Стабилизировать обучение GAN тремя стандартными приёмами: non-saturating loss, spectral norm, TTUR (two-timescale update rule)
  • Читать кривые обучения, отличающие здоровую сходимость от коллапса мод, осцилляции и полной победы дискриминатора

Проблема

Классификация учит сеть сопоставлять изображения с метками. Генерация переворачивает задачу: нужно сэмплировать новые изображения, выглядящие так, будто они получены из того же распределения. Нет «правильного» выхода, с которым можно выполнить diff; есть только распределение, которое вы хотите имитировать.

Стандартные функции потерь (MSE, cross-entropy) не умеют измерять: «взята ли эта выборка из реального распределения?» Минимизация ошибки по пикселям даёт размытые средние, а не реалистичные образцы. Прорыв состоял в том, чтобы выучить функцию потерь: обучить вторую сеть отличать реальное от поддельного и использовать её оценку, чтобы подталкивать генератор.

GAN (Goodfellow и др., 2014) задали эту схему. К 2018 году StyleGAN генерировала лица 1024x1024, неотличимые от фотографий. С тех пор диффузионные модели заняли первое место по качеству и управляемости, но каждый приём, делающий диффузию практичной — выбор нормализации, латентные пространства, потери по признакам, — впервые был понят на GAN.

Концепция

Две сети

Диаграмма к уроку «Генерация изображений — GAN»

Генератор G принимает вектор шума z и выдаёт изображение. Дискриминатор D принимает изображение и выдаёт единственный скаляр: вероятность того, что изображение реально.

Игра

G хочет, чтобы D ошибалась. D хочет быть права. Формально:

min_G max_D  E_x[log D(x)] + E_z[log(1 - D(G(z)))]

Читайте справа налево: D максимизирует точность на реальных (log D(real)) и поддельных (log (1 - D(fake))) изображениях. G минимизирует точность D на подделках — она хочет, чтобы D(G(z)) было высоким.

Гудфеллоу доказал, что у этой минимаксной задачи есть глобальное равновесие, в котором p_G = p_data, D везде выдаёт 0.5, а дивергенция Йенсена—Шеннона между сгенерированным и реальным распределениями равна нулю. Трудность в том, чтобы туда попасть.

Non-saturating loss

Форма выше численно нестабильна. В начале обучения D(G(z)) почти равна нулю для каждой подделки, поэтому log(1 - D(G(z))) имеет затухающие градиенты по отношению к G. Исправление: перевернуть функцию потерь G.

L_D = -E_x[log D(x)] - E_z[log(1 - D(G(z)))]
L_G = -E_z[log D(G(z))]                          # non-saturating

Теперь, когда D(G(z)) близка к нулю, потеря G велика и её градиент информативен. Каждая современная GAN обучается с этим вариантом.

Правила архитектуры DCGAN

Рэдфорд, Мец и Чинтала (2015) свели годы неудачных экспериментов к пяти правилам, стабилизирующим обучение GAN:

  1. Замените pooling на свёртки с шагом (в обеих сетях).
  2. Используйте batch norm и в генераторе, и в дискриминаторе, кроме выхода G и входа D.
  3. Уберите полносвязные слои в глубоких архитектурах.
  4. G использует ReLU на всех слоях, кроме выхода (tanh для выхода в диапазоне [-1, 1]).
  5. D использует LeakyReLU (negative_slope=0.2) на всех слоях.

Каждая современная GAN на основе свёрток (StyleGAN, BigGAN, GigaGAN) всё ещё начинает с этих правил и заменяет части по одной за раз.

Режимы отказа и их признаки

Диаграмма к уроку «Генерация изображений — GAN»

  • Коллапс мод (mode collapse): G находит одно изображение, обманывающее D, и генерирует только его. Исправление: добавить minibatch discrimination, spectral norm или conditioning по меткам.
  • Победа дискриминатора: D слишком быстро становится слишком сильной, и градиенты G затухают. Исправление: уменьшить D, понизить learning rate D или применить label smoothing к реальным меткам.
  • Осцилляция: две сети обмениваются победами, так и не приближаясь к равновесию. Исправление: TTUR (D обучается в 2–4 раза быстрее G) или переход на Wasserstein loss.

Оценка

У GAN нет ground truth, поэтому как понять, что они работают?

  • Просмотр образцов — просто просматривайте 64 образца в конце каждой эпохи. Это обязательно.
  • FID (Fréchet Inception Distance) — расстояние между распределениями признаков реального и сгенерированного наборов в Inception-v3. Меньше — лучше. Стандарт сообщества.
  • Inception Score — старее и более хрупкая метрика; предпочитайте FID.
  • Precision/Recall для генеративных моделей — отдельно измеряет качество (precision) и покрытие (recall). Информативнее одной FID.

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

Соберите

Шаг 1: Генератор

Небольшой генератор DCGAN, принимающий 64-мерный шум и создающий изображение 32x32.

import torch
import torch.nn as nn

class Generator(nn.Module):
    def __init__(self, z_dim=64, img_channels=3, feat=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.ConvTranspose2d(z_dim, feat * 4, kernel_size=4, stride=1, padding=0, bias=False),
            nn.BatchNorm2d(feat * 4),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(feat * 4, feat * 2, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(feat * 2),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(feat * 2, feat, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(feat),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(feat, img_channels, kernel_size=4, stride=2, padding=1, bias=False),
            nn.Tanh(),
        )

    def forward(self, z):
        return self.net(z.view(z.size(0), -1, 1, 1))

Четыре транспонированные свёртки, каждая с kernel_size=4, stride=2, padding=1, поэтому они чисто удваивают пространственный размер. Выходные активации находятся в [-1, 1] благодаря tanh.

Шаг 2: Дискриминатор

Зеркальное отражение генератора. LeakyReLU, свёртки с шагом, на выходе — единственный logit.

class Discriminator(nn.Module):
    def __init__(self, img_channels=3, feat=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv2d(img_channels, feat, kernel_size=4, stride=2, padding=1),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feat, feat * 2, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(feat * 2),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feat * 2, feat * 4, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(feat * 4),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feat * 4, 1, kernel_size=4, stride=1, padding=0),
        )

    def forward(self, x):
        return self.net(x).view(-1)

Последняя свёртка уменьшает карту признаков 4x4 до 1x1. Выход — один скаляр на изображение; применяйте sigmoid только при вычислении потерь.

Шаг 3: Шаг обучения

Чередуйте: обновите D один раз, затем G один раз, в каждом batch.

import torch.nn.functional as F

def train_step(G, D, real, z, opt_g, opt_d, device):
    real = real.to(device)
    bs = real.size(0)

    # D step
    opt_d.zero_grad()
    d_real = D(real)
    d_fake = D(G(z).detach())
    loss_d = (F.binary_cross_entropy_with_logits(d_real, torch.ones_like(d_real))
              + F.binary_cross_entropy_with_logits(d_fake, torch.zeros_like(d_fake)))
    loss_d.backward()
    opt_d.step()

    # G step
    opt_g.zero_grad()
    d_fake = D(G(z))
    loss_g = F.binary_cross_entropy_with_logits(d_fake, torch.ones_like(d_fake))
    loss_g.backward()
    opt_g.step()

    return loss_d.item(), loss_g.item()

G(z).detach() на шаге D критически важен: мы не хотим, чтобы градиенты попадали в G при обновлении D. Забыть это — классическая ошибка новичка.

Шаг 4: Полный цикл обучения на синтетических фигурах

from torch.utils.data import DataLoader, TensorDataset
import numpy as np

def synthetic_images(num=2000, size=32, seed=0):
    rng = np.random.default_rng(seed)
    imgs = np.zeros((num, 3, size, size), dtype=np.float32) - 1.0
    for i in range(num):
        r = rng.uniform(6, 12)
        cx, cy = rng.uniform(r, size - r, size=2)
        yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
        mask = (xx - cx) ** 2 + (yy - cy) ** 2 < r ** 2
        color = rng.uniform(-0.5, 1.0, size=3)
        for c in range(3):
            imgs[i, c][mask] = color[c]
    return torch.from_numpy(imgs)

device = "cuda" if torch.cuda.is_available() else "cpu"
data = synthetic_images()
loader = DataLoader(TensorDataset(data), batch_size=64, shuffle=True)

G = Generator(z_dim=64, img_channels=3, feat=32).to(device)
D = Discriminator(img_channels=3, feat=32).to(device)
opt_g = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
opt_d = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))

for epoch in range(10):
    for (batch,) in loader:
        z = torch.randn(batch.size(0), 64, device=device)
        ld, lg = train_step(G, D, batch, z, opt_g, opt_d, device)
    print(f"epoch {epoch}  D {ld:.3f}  G {lg:.3f}")

Adam(lr=2e-4, betas=(0.5, 0.999)) — настройка DCGAN по умолчанию: низкий beta1 не даёт члену momentum слишком сильно стабилизировать состязательную игру.

Шаг 5: Сэмплирование

@torch.no_grad()
def sample(G, n=16, z_dim=64, device="cpu"):
    G.eval()
    z = torch.randn(n, z_dim, device=device)
    imgs = G(z)
    imgs = (imgs + 1) / 2
    return imgs.clamp(0, 1)

Перед сэмплированием всегда переключайтесь в режим eval. Для DCGAN это важно, поскольку batch norm использует накопленную статистику, а не статистику текущего batch.

Шаг 6: Спектральная нормализация

Готовая замена BN в дискриминаторе, гарантирующая, что сеть является 1-Lipschitz. Исправляет большинство сбоев вида «D побеждает слишком сильно».

from torch.nn.utils import spectral_norm

def build_sn_discriminator(img_channels=3, feat=64):
    return nn.Sequential(
        spectral_norm(nn.Conv2d(img_channels, feat, 4, 2, 1)),
        nn.LeakyReLU(0.2, inplace=True),
        spectral_norm(nn.Conv2d(feat, feat * 2, 4, 2, 1)),
        nn.LeakyReLU(0.2, inplace=True),
        spectral_norm(nn.Conv2d(feat * 2, feat * 4, 4, 2, 1)),
        nn.LeakyReLU(0.2, inplace=True),
        spectral_norm(nn.Conv2d(feat * 4, 1, 4, 1, 0)),
    )

Замените Discriminator на build_sn_discriminator(), и часто приём TTUR больше не понадобится. Spectral norm — самое простое одиночное улучшение устойчивости, которое можно применить.

Используйте

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

  • torch_fidelity вычисляет FID / IS для вашего генератора без написания собственного кода оценки.
  • pytorch-gan-zoo (устаревшая) и StudioGAN предоставляют протестированные реализации DCGAN, WGAN-GP, SN-GAN, StyleGAN и BigGAN.

В 2026 году GAN всё ещё лучше всего подходят для: генерации изображений в реальном времени (задержка <10 мс), переноса стиля, перевода изображение-в-изображение с точным управлением (Pix2Pix, CycleGAN). Диффузия побеждает в фотореализме и текстовом управлении.

Поставьте

Этот урок создаёт:

  • outputs/prompt-gan-training-triage.md — промпт, который читает описание кривой обучения, определяет режим отказа (коллапс мод, победа D, осцилляция) и выбирает единственное рекомендуемое исправление.
  • outputs/skill-dcgan-scaffold.md — навык, который пишет каркас DCGAN из z_dim, целевого image_size и num_channels, включая цикл обучения и сохранение образцов.

Упражнения

  1. (Легко) Обучите DCGAN выше на наборе синтетических окружностей и сохраняйте сетку из 16 образцов в конце каждой эпохи. На какой эпохе сгенерированные окружности становятся явно круглыми?
  2. (Средне) Замените batch norm дискриминатора на spectral norm. Обучите обе версии параллельно. Какая из них сходится быстрее? У какой меньше дисперсия по трём seed?
  3. (Сложно) Реализуйте conditional DCGAN: подайте метку класса и в G, и в D (склейте one-hot с шумом в G, склейте канал встраивания класса в D). Обучите на синтетическом наборе «окружности против квадратов» из урока 7 и покажите, что conditioning по классу работает, сэмплируя конкретные метки.

Ключевые термины

ТерминКак обычно говорятЧто это на самом деле означает
Генератор (G)«Сеть, которая рисует»Отображает шум в изображения; обучается обманывать дискриминатор
Дискриминатор (D)«Критик»Бинарный классификатор; обучается отличать реальные изображения от сгенерированных
Минимакс«Игра»min по G, max по D для состязательной потери; равновесие имеет вид p_G = p_data
Non-saturating loss«Численно разумная версия»Потеря G равна -log(D(G(z))) вместо log(1 - D(G(z))), чтобы избежать затухающих градиентов в начале обучения
Коллапс мод«Генератор делает одну вещь»G создаёт только малое подмножество распределения данных; исправляется SN, minibatch discrimination или большим batch
TTUR«Две скорости обучения»D обучается быстрее G, обычно в 2–4 раза; стабилизирует обучение
Spectral norm«1-Lipschitz слой»Нормализация весов, ограничивающая константу Липшица каждого слоя; не даёт D стать сколь угодно крутой
FID«Fréchet Inception Distance»Расстояние между распределениями признаков Inception-v3 реальных и сгенерированных наборов; стандартная метрика оценки

Дополнительные материалы


Источник: Image Generation — GANs 04.08 — Instance Segmentation — Mask R-CNN · Фаза 04 — Компьютерное зрение · 04.10 — Image Generation — Diffusion Models · Полный каталог