Фаза 04 · урок 09
Генерация изображений — GAN
Цель урока: Классификация учит сеть сопоставлять изображения с метками. Генерация переворачивает задачу: нужно сэмплировать новые изображения, выглядящие так, будто они получены из того же распределения. Нет «правильного» выхода, с которым можно…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Две сети
- Игра
- Non-saturating loss
- Правила архитектуры DCGAN
- Режимы отказа и их признаки
- Оценка
- Соберите
- Шаг 1: Генератор
- Шаг 2: Дискриминатор
- Шаг 3: Шаг обучения
- Шаг 4: Полный цикл обучения на синтетических фигурах
- Шаг 5: Сэмплирование
- Шаг 6: Спектральная нормализация
- Используйте
- Поставьте
- Упражнения
- Ключевые термины
- Дополнительные материалы
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.
Концепция
Две сети
Генератор 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:
- Замените pooling на свёртки с шагом (в обеих сетях).
- Используйте batch norm и в генераторе, и в дискриминаторе, кроме выхода G и входа D.
- Уберите полносвязные слои в глубоких архитектурах.
- G использует ReLU на всех слоях, кроме выхода (
tanhдля выхода в диапазоне [-1, 1]). - D использует LeakyReLU (
negative_slope=0.2) на всех слоях.
Каждая современная GAN на основе свёрток (StyleGAN, BigGAN, GigaGAN) всё ещё начинает с этих правил и заменяет части по одной за раз.
Режимы отказа и их признаки
- Коллапс мод (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, включая цикл обучения и сохранение образцов.
Упражнения
- (Легко) Обучите DCGAN выше на наборе синтетических окружностей и сохраняйте сетку из 16 образцов в конце каждой эпохи. На какой эпохе сгенерированные окружности становятся явно круглыми?
- (Средне) Замените batch norm дискриминатора на spectral norm. Обучите обе версии параллельно. Какая из них сходится быстрее? У какой меньше дисперсия по трём seed?
- (Сложно) Реализуйте 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 реальных и сгенерированных наборов; стандартная метрика оценки |
Дополнительные материалы
- Generative Adversarial Networks (Goodfellow et al., 2014) — статья, с которой всё началось
- DCGAN (Radford, Metz, Chintala, 2015) — правила архитектуры, сделавшие GAN обучаемыми
- Spectral Normalization for GANs (Miyato et al., 2018) — самый полезный отдельный приём стабилизации
- StyleGAN3 (Karras et al., 2021) — SOTA GAN; читается как альбом лучших хитов всех приёмов последнего десятилетия
Источник: Image Generation — GANs 04.08 — Instance Segmentation — Mask R-CNN · Фаза 04 — Компьютерное зрение · 04.10 — Image Generation — Diffusion Models · Полный каталог