Траектория «Нейросети» · конспект 10 из 12

Диффузионные модели

О чём эта тема
Диффузионная модель генерирует изображения, обучаясь обращать вспять процесс постепенного зашумления: сеть учится предсказывать шум, и, вычитая его шаг за шагом из чистого случайного шума, «проявляет» новое изображение. Это подход, на котором построены DALL-E 2 и другие современные генераторы изображений. Перевод и адаптация лекции Й. Маухера (HdM Stuttgart) о вероятностных моделях шумоподавляющей диффузии.
Аннотация
Конспект начинается с идеи диффузии, заимствованной из термодинамики, и общей схемы модели: прямой процесс постепенно превращает изображение в чистый шум, обратный — восстанавливает изображение из шума. Затем прямой процесс описывается формально: гауссовское зашумление на каждом шаге, расписание дисперсий и диффузионное ядро, позволяющее получить любой шаг сразу, без последовательного прохода, — с интерактивным тренажёром. Далее рассматривается обратный процесс и его аппроксимация нейросетью, выводится (на уровне идей) функция ошибки: от вариационной нижней границы до упрощённой формы «предскажи добавленный шум». Приводятся алгоритмы обучения и генерации из статьи Хо и соавторов и реализация на Keras, написанная с нуля. В завершение обсуждается роль U-Net (конспект 7) как стандартной архитектуры для предсказания шума.
Пререквизиты
Конспект 9 (VAE: латентное пространство, KL-дивергенция, репараметризация — прямой предшественник по идеям), конспект 7, раздел 6 (U-Net), конспект 2 (градиентный спуск). Нормальное распределение и условная вероятность — из курса теории вероятностей.
Мотивация
У генеративных моделей из конспекта 9 есть слабые места: изображения VAE получаются размытыми, а состязательные сети (конспект 11) капризны в обучении и часто дают однообразные результаты. Диффузионные модели обучаются устойчиво — обычной регрессией на предсказание шума — и при этом дают изображения фотографического качества; именно они стоят за DALL-E 2, Imagen и Stable Diffusion.

1. Идея: испортить и научиться чинить

Понятие диффузии заимствовано из термодинамики: молекулы газа перемещаются из областей высокой плотности в области низкой, энтропия при этом растёт. В теории информации рост энтропии соответствует потере информации — и такую потерю можно устроить искусственно, добавляя к данным шум.

Вероятностная модель шумоподавляющей диффузии (англ. Denoising Diffusion Probabilistic Model, DDPM; Соль-Дикстейн и соавт., 2015; существенно улучшена Хо и соавт., 2020) состоит из двух процессов:

Схема: ряд изображений от чёткой фотографии до чистого шума; прямой процесс идёт слева направо и фиксирован, обратный — справа налево и обучается
Прямой процесс (слева направо) фиксирован; обратный, генеративный (справа налево), — обучается. Из лекции курса (источник: CVPR 2022 tutorial on diffusion models)

Ключевое наблюдение: если известно более зашумлённое изображение \(x_t\) и сам шум \(\epsilon_t\), то менее зашумлённое \(x_{t-1}\) получается простым вычитанием. Значит, для обратного процесса достаточно нейросети, оценивающей шум между двумя соседними шагами.

Сетка фотореалистичных изображений животных и предметов, сгенерированных диффузионной моделью
Изображения, сгенерированные диффузионной моделью (Dhariwal, Nichol, 2021). Из лекции курса

2. Прямой диффузионный процесс

Начав с реального изображения \(x_0\) из распределения данных \(q(x_0)\), прямой процесс на каждом шаге добавляет небольшую порцию гауссовского шума, порождая последовательность всё более зашумлённых версий \(x_0, x_1, \ldots, x_T\). Условное распределение каждого шага — нормальное:

\[ q(x_t | x_{t-1}) = N\bigl(x_t;\; \sqrt{1-\beta_t}\, x_{t-1},\; \beta_t \mathbf{I}\bigr) \tag{10.1}\]

Набор \(\{\beta_t \in (0,1)\}_{t=1}^{T}\) называется расписанием дисперсий (англ. variance schedule) — он задаёт, сколько шума добавляется на каждом шаге. Типичный выбор — линейный рост от \(\beta_1 = 10^{-4}\) до \(\beta_T = 0{,}02\) при \(T = 1000\) шагов (Хо и соавт., 2020). Расписание подобрано так, что \(q(x_T | x_0) \approx N(0, \mathbf{I})\): в конце от изображения не остаётся ничего.

Чтобы получить зашумлённую версию шага \(t\), не нужно выполнять \(t\) шагов подряд. Обозначив

\[ \bar{\alpha}_t = \prod_{s=1}^{t} (1 - \beta_s), \tag{10.2}\]

любой шаг выражается напрямую через исходное изображение и один вектор шума — это распределение называют диффузионным ядром:

\[ x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \epsilon, \qquad \epsilon \sim N(0, \mathbf{I}) \tag{10.3}\]

С ростом \(t\) коэффициент при изображении \(\sqrt{\bar\alpha_t}\) падает от 1 к 0, а коэффициент при шуме растёт от 0 к 1 — сигнал плавно замещается шумом:

Тренажёр: диффузионное ядро
x₀ — исходное изображение
xₜ — по формуле (10.3)

Линейное расписание дисперсий: β от 10⁻⁴ до 0,02, T = 1000 (как у Хо и соавт.). Изображение — иллюстративная «цифра 7» 16×16. К шагу t ≈ 300 сигнал ещё различим (ᾱ ≈ 0,40), к t = 1000 не остаётся ничего (ᾱ ≈ 0): x₁₀₀₀ — чистый шум. Для отображения значения пикселей обрезаются в диапазон [0, 1].

Ряд графиков распределения данных: от многомодального распределения q(x0) к нормальному распределению q(xT)
Тот же процесс на языке распределений: сложное распределение данных \(q(x_0)\) шаг за шагом превращается в нормальное \(q(x_T)\). Из лекции курса (источник: CVPR 2022 tutorial)
Типичная ошибка Зашумляют изображение последовательным циклом из \(t\) шагов по формуле (10.1) — при обучении это тысячи лишних операций на каждый пример. Диффузионное ядро (10.3) даёт \(x_t\) за одно умножение: именно поэтому при обучении можно на каждом шаге брать случайный \(t\) и сразу строить зашумлённую версию.

3. Обратный диффузионный процесс

Цель генерации — выборка из \(q(x_{t-1} | x_t)\): взяв чистый шум \(x_T \sim N(0,\mathbf{I})\) и пройдя все шаги в обратном порядке, получить объект из распределения данных. Но истинное \(q(x_{t-1} | x_t)\) вычислить нельзя — оно требует знания распределения всех данных. Решение — аппроксимировать его нейросетью. Если шаги \(\beta_t\) малы, обратное распределение тоже близко к нормальному, поэтому сети достаточно предсказывать его среднее:

\[ p_{\Theta}(x_{t-1} | x_t) = N\bigl(x_{t-1};\; \mu_{\Theta}(x_t, t),\; \sigma^2 \mathbf{I}\bigr) \tag{10.4}\]
Ряд графиков распределений с отмеченными точками: обратный процесс шаг за шагом переводит выборку из нормального распределения в выборку из распределения данных
Обратный процесс: начав с выборки из \(N(0, \mathbf{I})\), на каждом шаге предсказываем и вычитаем шум, приходя к объекту из распределения данных. Из лекции курса (источник: CVPR 2022 tutorial)

4. Чему обучать сеть: предсказанию шума

Формально модель обучается максимизировать правдоподобие данных — минимизировать \(E[-\log p_\Theta(x_0)]\). Напрямую эта величина не вычисляется, поэтому, как и в VAE (конспект 9), минимизируют её вариационную границу, которая распадается в сумму KL-дивергенций по шагам процесса. После подстановки нормальных распределений (10.1) и (10.4) каждое слагаемое сводится к разнице между истинным шумом и шумом, предсказанным сетью. Хо и соавторы показали, что взвешивающие коэффициенты можно просто отбросить — качество только улучшается, — и остаётся поразительно простая функция ошибки:

\[ L_{\text{simple}} = E_{t,\, x_0,\, \epsilon} \Bigl[\, \bigl\| \epsilon - \epsilon_{\Theta}\bigl(\underbrace{\sqrt{\bar\alpha_t}\, x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon}_{x_t},\; t\bigr) \bigr\|^2 \Bigr] \tag{10.5}\]

Словами: взять случайный шаг \(t\), зашумить изображение по ядру (10.3) случайным шумом \(\epsilon\), попросить сеть \(\epsilon_\Theta\) угадать этот шум по зашумлённому изображению и номеру шага — и штрафовать квадратом отклонения. Это обычная регрессия (конспект 2, формула (2.2)) — никакой состязательности, никакого балансирования двух сетей.

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

\[ x_{t-1} = \frac{1}{\sqrt{1-\beta_t}} \Bigl( x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\, \epsilon_{\Theta}(x_t, t) \Bigr) + \sigma_t z, \qquad z \sim N(0, \mathbf{I}) \tag{10.6}\]

где случайная добавка \(\sigma_t z\) присутствует на всех шагах, кроме последнего. Оба алгоритма целиком — обучение и генерация — умещаются в несколько строк:

Псевдокод двух алгоритмов из статьи Ho et al. 2020: обучение (случайный шаг, зашумление, градиентный шаг по ошибке предсказания шума) и генерация (последовательное вычитание предсказанного шума)
Алгоритмы обучения и генерации из статьи Хо и соавт. (2020). Из лекции курса

5. Реализация на Keras

В источнике готового кода нет — ниже минимальная реализация DDPM для MNIST, написанная по формулам этого конспекта. Расписание дисперсий и зашумление:

import tensorflow as tf
import numpy as np

T = 1000
beta = np.linspace(1e-4, 0.02, T).astype('float32')   # расписание дисперсий
alpha_bar = np.cumprod(1.0 - beta)                       # формула (10.2)

def q_sample(x0, t, eps):
    # зашумление сразу до шага t — диффузионное ядро (10.3)
    a = tf.gather(alpha_bar, t)[:, None, None, None]
    return tf.sqrt(a) * x0 + tf.sqrt(1.0 - a) * eps

Сеть \(\epsilon_\Theta(x_t, t)\) получает зашумлённое изображение и номер шага; простейший способ подать номер шага — дополнительным каналом (в серьёзных реализациях используют синусоидальные эмбеддинги, как в трансформерах):

from tensorflow.keras import layers, Model

def build_eps_model():
    img = layers.Input(shape=(28, 28, 1))
    t_in = layers.Input(shape=(), dtype='int32')          # номер шага t
    t_map = layers.Lambda(lambda t: tf.cast(t[:, None, None, None], 'float32')
                          / T * tf.ones((1, 28, 28, 1)))(t_in)
    x = layers.Concatenate()([img, t_map])               # t — дополнительным каналом

    # мини-U-Net: кодировщик
    c1 = layers.Conv2D(32, 3, padding='same', activation='relu')(x)
    p1 = layers.MaxPooling2D()(c1)
    c2 = layers.Conv2D(64, 3, padding='same', activation='relu')(p1)
    p2 = layers.MaxPooling2D()(c2)
    b  = layers.Conv2D(128, 3, padding='same', activation='relu')(p2)
    # декодировщик с конкатенациями (конспект 7, раздел 6)
    u2 = layers.Conv2D(64, 2, padding='same', activation='relu')(layers.UpSampling2D()(b))
    m2 = layers.Concatenate()([c2, u2])
    c3 = layers.Conv2D(64, 3, padding='same', activation='relu')(m2)
    u1 = layers.Conv2D(32, 2, padding='same', activation='relu')(layers.UpSampling2D()(c3))
    m1 = layers.Concatenate()([c1, u1])
    c4 = layers.Conv2D(32, 3, padding='same', activation='relu')(m1)
    out = layers.Conv2D(1, 1)(c4)                          # предсказанный шум
    return Model([img, t_in], out)

model = build_eps_model()
optimizer = tf.keras.optimizers.Adam(2e-4)

Шаг обучения — алгоритм 1 из рисунка выше, дословно по формуле (10.5):

@tf.function
def train_step(x0):
    n = tf.shape(x0)[0]
    t = tf.random.uniform((n,), 0, T, dtype=tf.int32)   # случайный шаг для каждого примера
    eps = tf.random.normal(tf.shape(x0))                 # истинный шум
    with tf.GradientTape() as tape:
        eps_pred = model([q_sample(x0, t, eps), t], training=True)
        loss = tf.reduce_mean(tf.square(eps - eps_pred)) # формула (10.5)
    grads = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))
    return loss

Генерация — алгоритм 2, формула (10.6):

def sample(n):
    x = tf.random.normal((n, 28, 28, 1))               # x_T — чистый шум
    for t in reversed(range(T)):
        eps_pred = model([x, tf.fill((n,), t)], training=False)
        x = (x - beta[t] / np.sqrt(1 - alpha_bar[t]) * eps_pred) / np.sqrt(1 - beta[t])
        if t > 0:                                     # случайная добавка — кроме последнего шага
            x += np.sqrt(beta[t]) * tf.random.normal(tf.shape(x))
    return x
Типичная ошибка Добавляют случайное слагаемое \(\sigma_t z\) и на последнем шаге \(t = 0\). Последний шаг должен вернуть чистое изображение — добавка шума в самом конце заметно портит результат; в алгоритме 2 из статьи это условие «если \(t > 1\), иначе \(z = 0\)».
Типичная ошибка Обучают сеть предсказывать само изображение \(x_0\) или среднее \(\mu\), а не шум \(\epsilon\). Математически параметризации эквивалентны, но на практике предсказание шума (10.5) обучается заметно устойчивее — это одно из ключевых наблюдений статьи Хо и соавторов.

6. Архитектура: U-Net с вниманием

Сеть \(\epsilon_\Theta\) должна принимать изображение и возвращать «изображение» того же размера (карту шума) — это ровно та задача, для которой в конспекте 7 вводилась U-Net: сжимающий путь копит признаки, разжимающий восстанавливает разрешение, горизонтальные связи переносят детали. В диффузионных моделях используется модификация U-Net: в блоки добавлены остаточные связи (конспект 7, раздел 5) с батч-нормализацией и блоки многоголового внимания (англ. multi-head attention), а номер шага \(t\) подаётся в каждый блок через эмбеддинг:

Схема U-Net для диффузионных моделей: сжимающий и разжимающий пути из остаточных блоков, между ними блоки многоголового внимания, горизонтальные связи-конкатенации
U-Net обратного диффузионного пути: жёлтые блоки — многоголовое внимание, синие — остаточные блоки. Из лекции курса (источник: AssemblyAI)
Схема остаточного блока: две последовательности из батч-нормализации, ReLU и свёртки 3 на 3, обходная связь суммируется с выходом
Остаточный блок этой U-Net: две связки «батч-нормализация → ReLU → свёртка 3×3» с обходной связью. Из лекции курса (источник: AssemblyAI)

Применения диффузионных моделей те же, что у других генеративных подходов, и шире: генерация изображений по тексту (DALL-E 2, Imagen), преобразование изображения в изображение, повышение разрешения, сегментация, достройка трёхмерных форм. Платой за качество служит скорость: для одного изображения требуется \(T\) последовательных проходов сети, тогда как GAN (конспект 11) генерирует за один проход.

Контрольные вопросы