Фаза 04 · урок 09

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

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

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

Курс
AI Engineering from Scratch
Фаза
Компьютерное зрение
Чтение
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 · Полный каталог