Генеративно-состязательная сеть PyTorch (GAN)
Генеративно-состязательная сеть (GAN)— одна из самых креативных архитектур моделей в глубоком обучении. Заставляя две нейронные сети конкурировать и учиться друг у друга, она в конечном итоге может генерировать очень реалистичные данные. GAN-модели широко используются в таких сценариях, как генерация изображений, перенос стиля и аугментация данных.
1. Основные принципы GAN
Основная идея GAN происходит из «игры с нулевой суммой» в теории игр. Она состоит из двух конкурирующих сетей:
- Генератор: учится генерировать поддельные данные, цель — сделать так, чтобы дискриминатор не мог отличить сгенерированные данные от реальных
- Дискриминатор: учится отличать реальные данные от сгенерированных, цель — делать суждения как можно более точными
Оба соревнуются друг с другом во время обучения и непрерывно улучшаются, в конечном итоге достигая состояния равновесия Нэша.
1.1 Целевая функция GAN
Задача обучения GAN может быть выражена в виде следующей минимаксной игры:
\[ \min_G \max_D \mathbb{E}_{x \sim p_{data}(x)}[\log D(x)] + \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))] \]
где:
- \(G\) обозначает сеть генератора
- \(D\) обозначает сеть дискриминатора
- \(x\) обозначает реальные данные
- \(z\) обозначает вектор случайного шума (обычно подчиняющийся стандартному нормальному распределению)
- \(G(z)\) обозначает поддельные данные, сгенерированные генератором из шума
1.2 Понимание процесса обучения
Обучение GAN разделено на два этапа:
Этап 1: Обучение дискриминатора
Заморозьте генератор и улучшите способность дискриминатора различать:
\[ \max_D \mathbb{E}_{x \sim p_{data}}[\log D(x)] + \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] \]
Этап 2: Обучение генератора
Заморозьте дискриминатор и улучшите способность генератора обманывать:
\[ \min_G \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] \]
В реальном обучении обычно сначала обучают дискриминатор в течение k шагов, затем генератор в течение 1 шага, чтобы поддерживать баланс.
2. Базовая реализация GAN
Ниже представлена минимальная реализация GAN — используемая для генерации двумерных точек данных.
2.1 Определение генератора и дискриминатора
Пример
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
# Установить случайное зерно
torch.manual_seed(42)
# ── Сеть генератора ──────────────────────────────────────
class Generator(nn.Module):
"""
Генератор: генерирует данные из случайного шума
Вход: вектор шума (batch_size, noise_dim)
Выход: сгенерированные данные (batch_size, data_dim)
"""
def __init__(self, noise_dim, data_dim, hidden_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(noise_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, data_dim),
# Выход не активируется; GAN изучит подходящее распределение
)
def forward(self, x):
return self.net(x)
# ── Сеть дискриминатора ──────────────────────────────────────
class Discriminator(nn.Module):
"""
Дискриминатор: отличает реальные данные от сгенерированных данных
Вход: точки данных (batch_size, data_dim)
Выход: вероятность реальных данных (batch_size, 1)
"""
def __init__(self, data_dim, hidden_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(data_dim, hidden_dim),
nn.LeakyReLU(0.2), # LeakyReLU предотвращает затухание градиентов
nn.Linear(hidden_dim, hidden_dim),
nn.LeakyReLU(0.2),
nn.Linear(hidden_dim, 1),
nn.Sigmoid() # Выходная вероятность
)
def forward(self, x):
return self.net(x)
# Гиперпараметры
NOISE_DIM = 16
DATA_DIM = 2
HIDDEN_DIM = 64
BATCH_SIZE = 128
# Создание сетей
generator = Generator(NOISE_DIM, DATA_DIM, HIDDEN_DIM)
discriminator = Discriminator(DATA_DIM, HIDDEN_DIM)
print(f"Количество параметров генератора: {sum(p.numel() for p in generator.parameters()):,}")
print(f"Количество параметров дискриминатора: {sum(p.numel() for p in discriminator.parameters()):,}")
2.2 Цикл обучения
Пример
lr = 0.001
g_optimizer = optim.Adam(generator.parameters(), lr=lr)
d_optimizer = optim.Adam(discriminator.parameters(), lr=lr)
# Функция потерь: бинарная кросс-энтропия
criterion = nn.BCELoss()
# ── Обучающие данные: кольцевое распределение ──────────────────────────
def generate_real_data(batch_size):
"""Сгенерировать реальные данные с кольцевым распределением"""
angles = torch.rand(batch_size) * 2 * torch.pi
radius = 1.0 + torch.randn(batch_size) * 0.1 # Радиус приблизительно равен 1
x = radius * torch.cos(angles)
y = radius * torch.sin(angles)
return torch.stack([x, y], dim=1)
# ── Цикл обучения ──────────────────────────────────────
NUM_EPOCHS = 1000
d_losses = []
g_losses = []
for epoch in range(NUM_EPOCHS):
# 1. Обучаем дискриминатор
# Сгенерировать фиктивные данные
noise = torch.randn(BATCH_SIZE, NOISE_DIM)
fake_data = generator(noise).detach() # detach, чтобы не вычислять градиенты генератора
# Генерируем реальные данные
real_data = generate_real_data(BATCH_SIZE)
# Потери дискриминатора
real_pred = discriminator(real_data)
fake_pred = discriminator(fake_data)
d_loss = criterion(real_pred, torch.ones_like(real_pred)) + \
criterion(fake_pred, torch.zeros_like(fake_pred))
# Обновляем дискриминатор
d_optimizer.zero_grad()
d_loss.backward()
d_optimizer.step()
# 2. Обучаем генератор
# Генерируем новый батч фейковых данных
noise = torch.randn(BATCH_SIZE, NOISE_DIM)
fake_data = generator(noise)
# Потери генератора: заставить дискриминатор поверить, что сгенерированные данные настоящие
fake_pred = discriminator(fake_data)
g_loss = criterion(fake_pred, torch.ones_like(fake_pred))
# Обновляем генератор
g_optimizer.zero_grad()
g_loss.backward()
g_optimizer.step()
# Записываем потери
d_losses.append(d_loss.item())
g_losses.append(g_loss.item())
if (epoch + 1) % 100 == 0:
print(f"Эпоха {epoch+1:4d} | D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")
print("Обучение завершено!")
2.3 Визуализация сгенерированных результатов
Пример
def visualize_results(generator, num_samples=1000):
noise = torch.randn(num_samples, NOISE_DIM)
generated_data = generator(noise).detach().numpy()
plt.figure(figsize=(6, 6))
plt.scatter(generated_data[:, 0], generated_data[:, 1],
alpha=0.5, s=10, c='blue', label='Сгенерированные')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Данные, сгенерированные GAN')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# Просмотр результатов генерации
visualize_results(generator)
3. DCGAN - глубокая сверточная GAN
DCGAN — это классическая архитектура, которая вводит сверточные нейронные сети в GAN, значительно улучшая качество генерации изображений.
3.1 Ключевые моменты архитектуры DCGAN
- Используйте транспонированную свертку для апсемплинга, чтобы генерировать изображения
- Используйте свертку с шагом для даунсемплинга, чтобы различать изображения
- Используйте BatchNorm и в генераторе, и в дискриминаторе (но не в выходном слое или входном слое)
- Генератор использует ReLU, дискриминатор использует LeakyReLU
3.2 Реализация DCGAN
Пример
import torch.nn as nn
# ── Генератор DCGAN ─────────────────────────────────
class DCGenerator(nn.Module):
"""
Генератор DCGAN: апсемплинг с помощью транспонированной свёртки
"""
def __init__(self, noise_dim=100, channels=3, features_g=64):
super().__init__()
self.noise_dim = noise_dim
# Вход: noise_dim x 1 x 1
self.net = nn.Sequential(
# Транспонированная свёртка: (batch, features_g*8, 4, 4)
nn.ConvTranspose2d(noise_dim, features_g * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(features_g * 8),
nn.ReLU(True),
# (batch, features_g*4, 8, 8)
nn.ConvTranspose2d(features_g * 8, features_g * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_g * 4),
nn.ReLU(True),
# (batch, features_g*2, 16, 16)
nn.ConvTranspose2d(features_g * 4, features_g * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_g * 2),
nn.ReLU(True),
# (batch, features_g, 32, 32)
nn.ConvTranspose2d(features_g * 2, features_g, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_g),
nn.ReLU(True),
# Выход: (batch, channels, 64, 64)
nn.ConvTranspose2d(features_g, channels, 4, 2, 1, bias=False),
nn.Tanh() # Диапазон выхода [-1, 1]
)
def forward(self, x):
# x: (batch, noise_dim) -> (batch, noise_dim, 1, 1)
x = x.view(x.size(0), x.size(1), 1, 1)
return self.net(x)
# ── Дискриминатор DCGAN ─────────────────────────────────
class DCDiscriminator(nn.Module):
"""
Дискриминатор DCGAN: понижение размерности с помощью свёртки
"""
def __init__(self, channels=3, features_d=64):
super().__init__()
# Вход: (batch, channels, 64, 64)
self.net = nn.Sequential(
# (batch, features_d, 32, 32)
nn.Conv2d(channels, features_d, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# (batch, features_d*2, 16, 16)
nn.Conv2d(features_d, features_d * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_d * 2),
nn.LeakyReLU(0.2, inplace=True),
# (batch, features_d*4, 8, 8)
nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_d * 4),
nn.LeakyReLU(0.2, inplace=True),
# (batch, features_d*8, 4, 4)
nn.Conv2d(features_d * 4, features_d * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_d * 8),
nn.LeakyReLU(0.2, inplace=True),
# Выход: (batch, 1, 1, 1)
nn.Conv2d(features_d * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x).view(x.size(0), -1)
# Проверка сети
noise_dim = 100
generator = DCGenerator(noise_dim=noise_dim, channels=3, features_g=64)
discriminator = DCDiscriminator(channels=3, features_d=64)
# Проверка прямого прохода
noise = torch.randn(2, noise_dim)
fake_images = generator(noise)
print(f"Форма сгенерированного изображения: {fake_images.shape}") # torch.Size([2, 3, 64, 64])
decision = discriminator(fake_images)
print(f"Форма результата дискриминации: {decision.shape}") # torch.Size([2, 1])
3.3 Полный код обучения DCGAN
Пример
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Используется устройство: {device}")
NOISE_DIM = 100
LEARNING_RATE = 0.0002
BETA1 = 0.5 # Параметры Adam
# Создание сетей
generator = DCGenerator(noise_dim=NOISE_DIM).to(device)
discriminator = DCDiscriminator().to(device)
# Оптимизатор
g_optimizer = optim.Adam(generator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))
d_optimizer = optim.Adam(discriminator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))
criterion = nn.BCELoss()
# ── Цикл обучения ─────────────────────────────────────
fixed_noise = torch.randn(64, NOISE_DIM, device=device) # Для визуализации
def train_dcgan(generator, discriminator, g_optimizer, d_optimizer, criterion,
num_epochs, device, fixed_noise):
G_losses = []
D_losses = []
for epoch in range(num_epochs):
for batch_idx in range(100): # Предположим, что в каждой эпохе 100 батчей
# Обучение дискриминатора
discriminator.zero_grad()
# Реальные изображения (предполагается, что доступны)
# real_images = ...
# Использовать случайный шум для имитации здесь
real_images = torch.randn(32, 3, 64, 64).to(device)
batch_size = real_images.size(0)
labels_real = torch.ones(batch_size, 1).to(device)
labels_fake = torch.zeros(batch_size, 1).to(device)
# Потери на реальных изображениях
output = discriminator(real_images)
d_loss_real = criterion(output, labels_real)
# Потери на сгенерированных изображениях
noise = torch.randn(batch_size, NOISE_DIM).to(device)
fake_images = generator(noise)
output = discriminator(fake_images.detach())
d_loss_fake = criterion(output, labels_fake)
# Суммарная потеря
d_loss = d_loss_real + d_loss_fake
d_loss.backward()
d_optimizer.step()
# Обучаем генератор
generator.zero_grad()
noise = torch.randn(batch_size, NOISE_DIM).to(device)
fake_images = generator(noise)
output = discriminator(fake_images)
g_loss = criterion(output, labels_real) # Надеемся, что сгенерированные изображения будут признаны реальными
g_loss.backward()
g_optimizer.step()
# Записываем потерю
if batch_idx % 50 == 0:
G_losses.append(g_loss.item())
D_losses.append(d_loss.item())
print(f"[{epoch}/{num_epochs}][{batch_idx}/100] "
f"D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")
return G_losses, D_losses
# Начать обучение
# G_losses, D_losses = train_dcgan(generator, discriminator, g_optimizer,
# d_optimizer, criterion, 5, device, fixed_noise)
print("Архитектура DCGAN определена, можно начинать обучение!")
4. Советы по обучению GAN
4.1 Распространённые проблемы и решения
| Проблема | Причина | Решение |
|---|---|---|
| Схлопывание режимов | Генератор производит лишь ограниченное разнообразие образцов | Используйте WGAN, добавьте дискриминацию мини-батчей, используйте сглаживание меток |
| Дискриминатор слишком силён | Градиенты генератора исчезают, из-за чего он не может обучаться | Обучайте генератор несколько раз, уменьшите скорость обучения дискриминатора, используйте LeakyReLU |
| Нестабильное обучение | Целевая функция GAN невыпукла и трудно сходится | Используйте спектральную нормализацию, штраф за градиент, разогрев скорости обучения |
| Плохое качество генерации | Недостаточная ёмкость сети или недостаточное обучение | Увеличьте глубину сети, используйте больше обучающих данных, обучайте дольше |
4.2 Улучшения функции потерь
Оригинальный GAN использует дивергенцию JS, которая имеет проблемы исчезающего градиента. WGAN использует расстояние Вассерштейна, которое более стабильно:
Пример
def wgan_d_loss(real_pred, fake_pred):
"""Потеря дискриминатора: реальные образцы оцениваются высоко, сгенерированные — низко"""
return -(torch.mean(real_pred) - torch.mean(fake_pred))
def wgan_g_loss(fake_pred):
"""Потеря генератора: сделать так, чтобы сгенерированные образцы оценивались высоко"""
return -torch.mean(fake_pred)
# Градиентный штраф - WGAN-GP
def gradient_penalty(discriminator, real_images, fake_images, device):
"""Слагаемое градиентного штрафа WGAN-GP"""
batch_size = real_images.size(0)
alpha = torch.rand(batch_size, 1, 1, 1).to(device)
# Интерполяция между реальными и сгенерированными изображениями
interpolated = alpha * real_images + (1 - alpha) * fake_images
interpolated.requires_grad = True
# Вычислить выход дискриминатора для интерполированных изображений
pred = discriminator(interpolated)
# Вычислить градиент
gradients = torch.autograd.grad(
outputs=pred,
inputs=interpolated,
grad_outputs=torch.ones_like(pred),
create_graph=True,
retain_graph=True,
only_inputs=True
)[0]
# Вычислить норму градиента
gradients = gradients.view(batch_size, -1)
gradient_norm = gradients.norm(2, dim=1)
penalty = ((gradient_norm - 1) ** 2).mean()
return penalty
4.3 Спектральная нормализация
Спектральная нормализация может стабилизировать обучение GAN, контролируя константу Липшица дискриминатора:
Пример
# Дискриминатор с использованием спектральной нормализации
class SNDiscriminator(nn.Module):
def __init__(self, channels=3, features_d=64):
super().__init__()
self.net = nn.Sequential(
spectral_norm(nn.Conv2d(channels, features_d, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(features_d, features_d * 2, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(features_d * 4, 1, 4, 1, 0)),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x).view(x.size(0), -1)
5. Условный GAN (cGAN)
Условный GAN позволяет задавать метки классов для генерируемых данных, обеспечивая условную генерацию.
5.1 Архитектура cGAN
Пример
import torch.nn as nn
class ConditionalGenerator(nn.Module):
"""Условный генератор: принимает как шум, так и метки классов"""
def __init__(self, noise_dim, num_classes, embed_dim, img_channels, features_g=64):
super().__init__()
self.label_emb = nn.Embedding(num_classes, embed_dim)
# Конкатенировать шум и эмбеддинги меток
self.net = nn.Sequential(
nn.Linear(noise_dim + embed_dim, features_g * 8 * 4 * 4),
nn.BatchNorm1d(features_g * 8 * 4 * 4),
nn.ReLU(),
# Затем примените транспонированные свёртки (аналогично DCGAN)
# ...
)
def forward(self, noise, labels):
# Встроить метки классов в ту же размерность, что и шум
label_embedding = self.label_emb(labels)
# Конкатенировать шум и эмбеддинги меток
x = torch.cat([noise, label_embedding], dim=1)
return self.net(x)
class ConditionalDiscriminator(nn.Module):
"""Условный дискриминатор: принимает и изображения, и метки классов"""
def __init__(self, img_channels, num_classes, embed_dim, features_d=64):
super().__init__()
self.label_emb = nn.Embedding(num_classes, embed_dim)
# Конкатенировать эмбеддинги изображений и меток
self.net = nn.Sequential(
nn.Conv2d(img_channels + embed_dim, features_d, 4, 2, 1),
nn.LeakyReLU(0.2),
# ...
)
def forward(self, img, labels):
# Преобразуйте эмбеддинги меток к тому же пространственному размеру, что и изображения
label_embedding = self.label_emb(labels)
# Отрегулируйте форму для конкатенации
label_embedding = label_embedding.unsqueeze(2).unsqueeze(3)
label_embedding = label_embedding.expand(-1, -1, img.size(2), img.size(3))
# Объедините изображение и метку
x = torch.cat([img, label_embedding], dim=1)
return self.net(x)
6. Метрики оценки GAN
6.1 Общие метрики оценки
| Метрика | Описание | Преимущества | Недостатки |
|---|---|---|---|
| Inception Score (IS) | Используйте Inception v3 для оценки качества и разнообразия сгенерированных изображений | Просто вычисляется, имеет некоторую корреляцию с человеческим суждением | Не оценивает переобучение и не может обнаружить коллапс мод |
| Расстояние Фреше Inception (FID) | Вычисляет расстояние между реальными и сгенерированными изображениями в пространстве признаков | Более чувствительна к шуму и коллапсу мод | Требует большого количества выборок и медленно вычисляется |
| Человеческая оценка | Люди оценивают качество сгенерированных изображений | Наиболее точно отражает человеческое восприятие | Субъективна и требует много времени |
6.2 Реализация вычисления FID
Пример
from scipy import linalg
def calculate_fid(real_activations, fake_activations):
"""
Вычислить Fréchet Inception Distance
real_activations: векторы признаков реальных изображений (N, dim)
fake_activations: векторы признаков сгенерированных изображений (N, dim)
"""
# Вычислить среднее и ковариацию
mu1, sigma1 = real_activations.mean(axis=0), np.cov(real_activations, rowvar=False)
mu2, sigma2 = fake_activations.mean(axis=0), np.cov(fake_activations, rowvar=False)
# Вычислить FID
diff = mu1 - mu2
# Вычислить сумму ковариационных матриц
covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)
# Избегаем комплексных чисел
if np.iscomplexobj(covmean):
covmean = covmean.real
fid = diff.dot(diff) + np.trace(sigma1 + sigma2 - 2 * covmean)
return fid
# Упрощённый пример: используйте случайные данные
np.random.seed(42)
real_acts = np.random.randn(1000, 2048) # размерность выхода Inception v3
fake_acts = np.random.randn(1000, 2048)
fid_score = calculate_fid(real_acts, fake_acts)
print(f"Оценка FID: {fid_score:.2f}")
# Чем ниже FID, тем лучше; обычно значение ниже 50 указывает на хорошее качество генерации
7. Распространённые варианты GAN
С момента разработки GAN было создано множество вариантов, подходящих для различных сценариев применения:
| Модель | Полное имя | Особенности | Применимые сценарии |
|---|---|---|---|
| DCGAN | Глубокий свёрточный GAN | Использует свёрточные сети, генерирует изображения высокого качества | Генерация изображений |
| WGAN | GAN Вассерштейна | Использует расстояние Вассерштейна, более стабильное обучение | Стабильное обучение |
| WGAN-GP | WGAN с градиентным штрафом | Градиентный штраф заменяет отсечение весов | Стабильное обучение |
| CGAN | Условный GAN | Добавляет условную информацию и управляемую генерацию | Условная генерация |
| CycleGAN | Циклически согласованный GAN | Перевод изображение-в-изображение без учителя | Перенос стиля |
| StyleGAN | GAN на основе стиля | Управление стилем, высококачественная генерация лиц | Генерация лиц |
| BigGAN | Big GAN | Крупномасштабная генерация изображений высокого качества | Изображения высокого разрешения |
| ProGAN | Прогрессивно растущий GAN | Постепенное увеличение разрешения | Генерация высокого разрешения |