ai_learning/modules/12_generative_handwriting/notes/02_gan_diffusion.md

12 KiB
Raw Blame History

GAN и diffusion: цели, обучение и типичные сбои

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

Мотивация и интуиция

VAE задаёт likelihood-bound, но простой pixel decoder часто усредняет допустимые варианты и размывает штрихи. GAN учит generator через discriminator: один создаёт образцы, другой отличает их от данных. Получаются резкие изображения, но обучение может быть нестабильным и пропускать варианты.

Diffusion намеренно постепенно зашумляет данные, а затем учит сеть обращать каждый малый шаг. Обучение стабильно, режимы покрытия обычно лучше, но sampling требует многих вызовов сети. Ни один подход не гарантирует читаемость или правомерность сгенерированного почерка.

GAN: minimax-цель

Пусть \mathbf{z}\sim p(\mathbf{z}), generator G_{\boldsymbol{\theta}}(\mathbf{z}), discriminator D_{\boldsymbol{\psi}}(\mathbf{x})\in(0,1). Исходная игра:


\min_G\max_D V(D,G)=
\mathbb{E}_{\mathbf{x}\sim p_{\mathrm{data}}}\log D(\mathbf{x})+
\mathbb{E}_{\mathbf{z}}\log(1-D(G(\mathbf{z}))).

Для фиксированного G оптимизируем каждую точку \mathbf{x}:


p_d(\mathbf{x})\log D(\mathbf{x})+
p_g(\mathbf{x})\log(1-D(\mathbf{x})).

Производная равна p_d/D-p_g/(1-D); приравнивая к нулю, получаем


D^*(\mathbf{x})=\frac{p_d(\mathbf{x})}{p_d(\mathbf{x})+p_g(\mathbf{x})}.

Подстановка даёт V(D^*,G)=-\log4+2\,D_{JS}(p_d\|p_g), минимум при p_g=p_d. На практике saturating loss \log(1-D(G(z))) даёт слабый градиент в начале, поэтому generator минимизирует non-saturating -\mathbb{E}\log D(G(z)).

Conditional GAN использует label y: G(\mathbf{z},y), D(\mathbf{x},y). Label embedding можно конкатенировать с latent, а discriminator строить projection-методом. Условие помогает content, но style diversity всё ещё требует latent и данных.

Сбои GAN

  • Mode collapse: разные \mathbf{z} дают несколько похожих глифов.
  • Discriminator dominance: почти идеальный D оставляет слабый сигнал G.
  • Oscillation/non-convergence: игроки преследуют меняющиеся цели.
  • Artifacts: transpose convolution создаёт checkerboard; resize-convolution часто безопаснее.
  • Memorization: резкий образец может быть копией train, особенно на малых данных.

Помогают balanced update rates, spectral normalization, gradient penalty, augmentation, EMA generator weights и проверка diversity. WGAN заменяет JS-подобную цель приближением Wasserstein-1 с 1-Lipschitz critic; Lipschitz ограничивают gradient penalty, а не sigmoid classification.

Forward diffusion

Задаём schedule \beta_t\in(0,1), \alpha_t=1-\beta_t, \bar\alpha_t=\prod_{s=1}^t\alpha_s:


q(\mathbf{x}_t\mid\mathbf{x}_{t-1})
=\mathcal{N}(\sqrt{\alpha_t}\mathbf{x}_{t-1},\beta_t\mathbf{I}).

Повторная подстановка Gaussian переходов даёт закрытую форму:


\mathbf{x}_t=\sqrt{\bar\alpha_t}\mathbf{x}_0+
\sqrt{1-\bar\alpha_t}\boldsymbol{\varepsilon},\qquad
\boldsymbol{\varepsilon}\sim\mathcal{N}(\mathbf{0},\mathbf{I}).

Значит, для train не нужно последовательно выполнять все t шагов: выбираем t, семплируем \varepsilon, сразу строим \mathbf{x}_t.

Reverse process и noise prediction

Обратный переход моделируется:


p_{\boldsymbol{\theta}}(\mathbf{x}_{t-1}\mid\mathbf{x}_t)
=\mathcal{N}(\boldsymbol{\mu}_{\boldsymbol{\theta}}(\mathbf{x}_t,t),
\boldsymbol{\Sigma}_t).

Популярная параметризация учит UNet предсказывать добавленный шум:


\mathcal{L}_{simple}=
\mathbb{E}_{\mathbf{x}_0,t,\varepsilon}
\|\varepsilon-\varepsilon_{\boldsymbol{\theta}}(\mathbf{x}_t,t)\|_2^2.

Из оценки шума восстанавливается оценка чистого изображения:


\hat{\mathbf{x}}_0=
\frac{\mathbf{x}_t-\sqrt{1-\bar\alpha_t}\,
\varepsilon_{\boldsymbol{\theta}}(\mathbf{x}_t,t)}
{\sqrt{\bar\alpha_t}}.

Почему noise predictor обычно является UNet

UNet имеет нисходящий путь из convolution/residual-блоков с downsampling, узкое bottleneck и восходящий путь с upsampling. Skip-связи передают feature maps одинакового разрешения из encoder в decoder, поэтому bottleneck видит глобальный контекст, а decoder не теряет точные границы штрихов. Embedding шага t добавляется в блоки и сообщает уровень шума. Условие класса можно подать через embedding/adaptive normalization, а текст — через cross-attention. Свёртки и формы разобраны в модуле 10, cross-attention — в модуле 13.

Один обратный шаг DDPM

Для noise-prediction параметризации среднее перехода можно записать:


\boldsymbol\mu_\theta(\mathbf x_t,t)=
\frac{1}{\sqrt{\alpha_t}}\left(
\mathbf x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}
\varepsilon_\theta(\mathbf x_t,t)\right).

Затем семплируют \mathbf x_{t-1}=\boldsymbol\mu_\theta+\sqrt{\tilde\beta_t}\mathbf z, где \mathbf z\sim\mathcal N(0,I), а на последнем шаге шум не добавляют. Полный sampler: начать с \mathbf x_T\sim\mathcal N(0,I), пройти t=T,T-1,\ldots,1, на каждом шаге предсказать шум и получить \mathbf x_{t-1}. Формула зависит от выбранной variance/sampler параметризации, поэтому schedule обучения и sampling-код должны совпадать.

DDPM sampling добавляет рассчитанное среднее reverse posterior и шум на каждом шаге (кроме последнего). DDIM и современные solvers сокращают число шагов, меняя траекторию/аппроксимацию; меньше шагов обычно обменивает скорость на качество.

Conditional diffusion подаёт embedding символа, стиля или reference через concatenation, adaptive normalization или cross-attention. Classifier-free guidance комбинирует unconditional и conditional noise estimates:


\hat\varepsilon=(1+w)\varepsilon_{\rm cond}-w\varepsilon_{\rm uncond}.

Большой w усиливает соответствие условию, но снижает diversity и может искажать штрихи.

Разобранный пример forward process

Пусть \bar\alpha_t=0.64, скаляр x_0=0.5, шум \varepsilon=-1. Тогда


x_t=0.8\cdot0.5+0.6\cdot(-1)=-0.2.

Если сеть идеально предскажет -1, то \hat x_0=(-0.2-0.6(-1))/0.8=0.5. При больших t, когда \bar\alpha_t\to0, восстановление особенно чувствительно к ошибке noise model.

Runnable PyTorch-код forward diffusion

import torch

T = 100
betas = torch.linspace(1e-4, 0.02, T)
alphas = 1.0 - betas
alpha_bar = torch.cumprod(alphas, dim=0)

x0 = torch.rand(8, 1, 28, 28) * 2 - 1
t = torch.randint(0, T, (x0.shape[0],))
noise = torch.randn_like(x0)
a = alpha_bar[t].view(-1, 1, 1, 1)
xt = a.sqrt() * x0 + (1 - a).sqrt() * noise

# Заглушка показывает интерфейс noise predictor.
model = torch.nn.Conv2d(1, 1, kernel_size=3, padding=1)
pred = model(xt)  # реальная модель также обязана получить embedding t
loss = torch.nn.functional.mse_loss(pred, noise)
loss.backward()
print(xt.shape, float(loss))

Одной convolution недостаточно: реальная UNet получает time embedding и имеет receptive field на весь глиф. Этот фрагмент проверяет формулы и формы массива.

Сравнение и оценка

GAN генерирует за один проход, diffusion — итеративно. GAN часто резче при малом размере, diffusion обычно проще оптимизировать и лучше покрывает распределение. Сравнение при разном compute нечестно. FID на natural-image features может плохо соответствовать качеству чёрно-белых глифов; добавляйте accuracy независимого character recognizer, diversity внутри класса, nearest-neighbor к train и человеческую оценку читаемости вслепую.

Типичные ошибки и ограничения

  • Считать низкий generator loss признаком качества без просмотра и метрик.
  • Обучать discriminator до насыщения или забывать detach при его update.
  • Называть critic WGAN discriminator-вероятностью и добавлять sigmoid.
  • Путать \alpha_t и cumulative \bar\alpha_t.
  • Не передавать timestep diffusion-модели.
  • Использовать schedule/sampler, не соответствующий обученной параметризации epsilon, x0 или v.
  • Повышать guidance до потери diversity.
  • Не проверять memorization и генерацию имитаций подписей реальных людей.

Краткое резюме

  • GAN решает игру generator/discriminator; оптимальный discriminator сравнивает плотности данных и модели.
  • Mode collapse и нестабильность требуют независимой оценки diversity.
  • Diffusion имеет известный forward Gaussian process и учит обратное denoising.
  • Условность усиливает контроль, но не гарантирует читаемость, оригинальность и безопасное применение.

Связанные материалы