Содержание курса 30 / 33

30 / 33 · Диффузионные модели

Редактирование изображений по точкам: DragGAN и DragDiffusion

Перемещение точки изображения к цели оптимизацией латентного кода генеративной модели: пространство W+ генератора StyleGAN2 и обращение GAN, motion supervision и отслеживание точек в DragGAN, а также дообучение LoRA, обращение DDIM, оптимизация латента и reference-latent-control в DragDiffusion с оценкой на DragBench.

На этой странице 12

Заметка о латентной диффузии закончилась четырьмя способами редактировать изображение обученной моделью: image-to-image, инпейнтинг, LoRA и textual inversion. Все четыре управляют тем, что показывает изображение, — через промпт, маску или новое понятие. Ни один из них не говорит, где что должно находиться. Многие правки геометрические: открыть рот, повернуть голову, поднять ухо, сделать машину выше. Пользователь может задать такую правку двумя щелчками: управляющей точкой (handle point) на объекте и целевой точкой (target point), куда она должна попасть. Редактирование по точкам (point-based editing) превращает щелчки в оптимизацию по латентному коду генеративной модели, так что каждый промежуточный результат — изображение, которое модель способна породить. DragGAN (Pan et al., 2023) делает это в StyleGAN2. DragDiffusion (Shi et al., 2024) переносит тот же цикл в Stable Diffusion — модель, которая может представить почти любую фотографию.

Обозначения. Управляющие точки p1,…,pnp_1, \dots, p_n и их цели g1,…,gng_1, \dots, g_n — позиции пикселей; мы пишем gig_i (goal, цель) там, где DragGAN пишет tit_i, потому что tt — шаг диффузии. pi0p_i^0 — начальное положение управляющей точки, pikp_i^k — её положение после kk итераций. MM — необязательная бинарная маска, равная 1 там, где изображение может меняться. di=(gi−pi)/∥gi−pi∥2d_i = (g_i - p_i)/\lVert g_i - p_i \rVert_2 — единичный вектор от точки к её цели. Fq(v)∈RCF_q(v) \in \mathbb R^C — вектор признаков в позиции qq карты признаков, вычисленной по оптимизируемой переменной vv; в нецелой позиции qq он билинейно интерполируется по четырём соседним позициям. sg[⋅]\mathrm{sg}[\cdot] — stop-gradient (остановка градиента): значение проходит вперёд, а градиент равен нулю. Ω1(p,r1)={q:∥q−p∥2<r1}\Omega_1(p, r_1) = \{q : \lVert q - p \rVert_2 < r_1\} — круг позиций вокруг pp, а Ω2(p,r2)={q:∣qx−px∣<r2, ∣qy−py∣<r2}\Omega_2(p, r_2) = \{q : |q_x - p_x| < r_2,\ |q_y - p_y| < r_2\} — квадрат. ∥⋅∥1\lVert \cdot \rVert_1 — сумма модулей. Обозначения диффузии ztz_t, αˉt\bar\alpha_t, εθ\varepsilon_\theta, τ(y)\tau(y), E\mathcal E и D\mathcal D — те же, что в заметке о латентной диффузии.

Чего требует перетаскивание#

DragGAN называет три свойства идеального инструмента для такого управления (Pan et al., 2023, раздел 1). Он должен быть гибким: менять можно положение, позу, форму, выражение и компоновку. Он должен быть точным: управляющая точка оказывается в цели, а не рядом с ней. Он должен быть общим: не привязанным к одной категории объектов. Прежние подходы не выполняют хотя бы одно из этих требований. Методы, построенные на 3D-моделях или обученные на ручной разметке, например условный GAN, управляемый ключевыми точками или картами сегментации, дают только те средства управления, для которых обучены, и не переносятся на новые категории. Текст — общее средство, но он не может сказать «сдвинь это на 40 пикселей».

Четвёртое свойство даёт сам выбор инструмента. Если правка делается изменением латентного кода предобученного генератора, каждый промежуточный результат — выход этого генератора, так что генератор служит априорным знанием, тянущим к реалистичным изображениям. Содержимое, которое открывается при движении, придумывает модель: DragGAN открывает пасть льва, и внутри появляются зубы. Деформация пикселей так не умеет. Реалистичность, однако, не гарантирована. Быть выходом генератора, то есть лежать в его математическом образе, — не то же самое, что лежать там, где сосредоточены его обучающие данные, и оптимизированный код в W+\mathcal W^+ или зашумлённый латент диффузии может уйти из этой области; ограничения перечисляют артефакты и смену идентичности, к которым это приводит. Реалистичность и сохранение идентичности нужно проверять на каждом результате. Остаётся дифференцируемым образом сказать «эта точка должна сдвинуться на один шаг вон туда» и знать, где точка находится после каждого шага.

Генератор StyleGAN2 вкратце#

До сих пор курс использовал генераторы GAN как чёрные ящики, переводящие шум в изображения (заметка о GAN). DragGAN опирается на четыре свойства одного конкретного генератора — StyleGAN2 (Karras et al., 2020), обученного обычной состязательной игрой.

  • Отображающая сеть (mapping network). Вектор шума z∈R512z \in \mathbb R^{512}, z∼N(0,I)z \sim \mathcal N(0, I), сначала проходит через 8-слойный MLP ff, который даёт промежуточный код w=f(z)∈W⊂R512w = f(z) \in \mathcal W \subset \mathbb R^{512} (Karras, Laine and Aila, 2019, раздел 2). W\mathcal W не обязано быть гауссовским, и статья о StyleGAN утверждает, что оно менее запутано (entangled), чем пространство zz: движение вдоль направления в W\mathcal W, как правило, меняет меньше атрибутов одновременно.

  • Модуляция стилем (style modulation). Синтезирующая сеть начинает с обучаемой константы 4×4×5124 \times 4 \times 512 и блок за блоком удваивает разрешение до R×RR \times R. Каждая свёртка получает стиль (style) s=A(w)s = A(w) — обучаемую аффинную функцию от ww с одним масштабом на входной канал. StyleGAN2 применяет стиль к весам. Если ϑijk\vartheta_{ijk} — вес от входного канала ii к выходному каналу jj в позиции ядра kk, то

    ϑijk′=si ϑijk,ϑijk′′=ϑijk′∑i,k(ϑijk′)2+ϵ,\vartheta'_{ijk} = s_i\, \vartheta_{ijk}, \qquad \vartheta''_{ijk} = \frac{\vartheta'_{ijk}}{\sqrt{\sum_{i,k} \big(\vartheta'_{ijk}\big)^2 + \epsilon}} ,

    где второй шаг, демодуляция (demodulation), делит каждый выходной канал на норму его весов, так что входы с единичной дисперсией снова дают выходы с единичной дисперсией (Karras et al., 2020, раздел 2.2). Попиксельные карты шума добавляют случайные детали, например расположение волос.

  • Расширенное пространство W+\mathcal W^+. Ничто не заставляет все слои получать одно и то же ww. Если каждому входу стиля подать собственный код, получится w=(w1,…,wl)∈Rl×512w = (w_1, \dots, w_l) \in \mathbb R^{l \times 512} — пространство W+\mathcal W^+ (Xia et al., 2022, раздел IV-A). Генератор с выходом R×RR \times R имеет l=2log⁡2R−2l = 2\log_2 R - 2 входов стиля: 14 при 2562256^2, 16 при 5122512^2 и 18 при 102421024^2. Первые коды действуют на грубых разрешениях. В экспериментах StyleGAN со смешиванием стилей (style mixing) коды слоёв от 424^2 до 828^2 несут позу, форму лица и общую причёску, коды 16216^2 и 32232^2 — более мелкие черты лица, а самые тонкие слои — цвет и микроструктуру (Karras, Laine and Aila, 2019, раздел 3.1). Первые шесть кодов управляют слоями примерно до 16×1616 \times 16.

  • Промежуточные признаки. Каждый блок синтеза выдаёт карту признаков своего разрешения. DragGAN использует карту после шестого блока, билинейно приведённую к размеру изображения. Из проверенных блоков этот дал самые точные правки: более ранние слишком грубы, более поздние дают менее различающие признаки (Pan et al., 2023, разделы 3.2 и 4.2). Если считать от блока 4×44 \times 4, шестой блок выдаёт карту 128×128128 \times 128, и именно её использует официальный код. В статье указано 256×256256 \times 256 (раздел 3.3) — разрешение седьмого блока.

Редактирование реального фото: обращение GAN#

У сгенерированного изображения есть латентный код. У фотографии его нет, поэтому сначала выполняется обращение GAN (GAN inversion) — поиск кода, изображение которого её воспроизводит:

w∗=arg⁡min⁡w ℓ(G(w), x),w^* = \arg\min_{w}\ \ell\big(G(w),\ x\big),

где ℓ\ell — расстояние в пространстве пикселей или признаков (Xia et al., 2022, раздел II). Обзор делит методы на три семейства (раздел IV-B).

  • Методы на основе оптимизации решают задачу напрямую для каждого изображения градиентным спуском. Собственный проектор (projector) StyleGAN2 оптимизирует ww и карты шума в течение 1000 итераций Adam по расстоянию LPIPS, вычисленному на обоих изображениях, уменьшенных до 256×256256 \times 256 (Karras et al., 2020, раздел 5 и приложение D). Точно, но медленно.
  • Методы на основе энкодера один раз обучают сеть E:x↦wE: x \mapsto w на множестве изображений. Тогда обращение — один прямой проход, ценой менее точной реконструкции.
  • Гибридные методы начинают с кода, выданного энкодером, и уточняют его оптимизацией.

Выбор пространства важен. У W+\mathcal W^+ в ll раз больше степеней свободы, чем у W\mathcal W, и оно точнее воспроизводит фотографии, но код, найденный далеко от кодов, которые порождает отображающая сеть, плохо редактируется: обзор называет это компромиссом между искажением и редактируемостью (distortion–editability trade-off; раздел IV-A). Pivotal tuning inversion (PTI, обращение с донастройкой генератора вокруг опорного кода; Roich et al., 2022) обходит этот компромисс, меняя вместо кода генератор. Сначала фото обращается в W\mathcal W, что даёт опорный код (pivot) wpw_p (450 шагов оптимизации по LPIPS с регуляризатором шума). Затем веса генератора дообучаются 350 шагов, чтобы G(wp)G(w_p) совпадало с фото по LPIPS плюс квадратичной ошибке, с регуляризатором, который оставляет генератор неизменным вдали от wpw_p. Вся процедура занимает меньше трёх минут на изображение на одной RTX 2080. Именно так DragGAN редактирует реальные фото: сначала PTI, затем цикл перетаскивания на полученном коде (Pan et al., 2023, раздел 4.1).

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

DragGAN: motion supervision и отслеживание точек#

DragGAN начинает с кода w0w_0, его признаков F0=F(w0)F_0 = F(w_0) и начальных управляющих точек pi0p_i^0. Каждая итерация состоит из двух подшагов (Pan et al., 2023, раздел 3.1). Motion supervision (контроль движения) сдвигает каждую точку на небольшой шаг к цели одним градиентным шагом по ww. Отслеживание точек (point tracking) затем находит, куда сместилась каждая точка. Подшаги повторяются, пока все точки не достигнут своих целей. Ничего не обучается: генератор заморожен, и единственная переменная — код редактируемого изображения.

Рисунок

Одна итерация DragGAN: motion supervision и отслеживание точки

Итерация DragGANЛатент w из l кодов, из которых обучаются первые шесть, идёт в замороженный StyleGAN2; тот выдаёт изображение и карту признаков F. На карте точка p, цель g, участок Ω₁ вокруг p и тот же участок, сдвинутый на шаг d. Потеря motion supervision даёт градиент по w₁…w₆; после шага Adam генератор запускается снова, и поиск ближайшего соседа в окне Ω₂ находит новое положение точки p′.w₁w₂w₃w₄w₅w₆w₇⋮wₗлатент w ∈ W+w₁…w₆ обучаютсяw₇…wₗ замороженыGStyleGAN2замороженgpпризнаки F после блока 6128 × 128, к размеру изображенияблок 6изображение512 × 512dΩ₁motion supervisionΣ по точкам i, q ∈ Ω₁(pᵢ):‖F(q + dᵢ) − sg[F(q)]‖₁+ λ ‖(F − F₀)·(1 − M)‖₁от пользователя: pᵢ, gᵢ, маска M∂L/∂(w₁…w₆): один шаг Adamw′ → F′Ω₂p′F′отслеживание точекpᵢ ← argmin по q ∈ Ω₂(pᵢ):‖F′(q) − F₀(pᵢ⁰)‖₁сновастоп, когда все ‖pᵢ − gᵢ‖ ≤ 1 пикс.12345входсостояниевеса и гейтывыходИтерация DragGANЛатент w из l кодов, из которых обучаются первые шесть, идёт в замороженный StyleGAN2; тот выдаёт изображение и карту признаков F. На карте точка p, цель g, участок Ω₁ вокруг p и тот же участок, сдвинутый на шаг d. Потеря motion supervision даёт градиент по w₁…w₆; после шага Adam генератор запускается снова, и поиск ближайшего соседа в окне Ω₂ находит новое положение точки p′.w₁w₂w₃w₄w₅w₆w₇⋮wₗлатент w ∈ W+обучаютсязамороженыGStyleGAN2, замороженgpпризнаки Fпосле блока 6,128 × 128блок 6изображение512 × 512dΩ₁motion supervisionΣ по точкам i, q ∈ Ω₁(pᵢ):‖F(q + dᵢ) − sg[F(q)]‖₁+ λ ‖(F − F₀)·(1 − M)‖₁от пользователя: pᵢ, gᵢ, маска M→ ∂L/∂(w₁…w₆): шаг Adamw′ → F′Ω₂p′F′отслеживание точекpᵢ ← argmin по q ∈ Ω₂(pᵢ):‖F′(q) − F₀(pᵢ⁰)‖₁сновастоп, когда все ‖pᵢ − gᵢ‖ ≤ 1 пикс.12345входсостояниевеса и гейтывыход
Синие — латент и признаки, которые он порождает; жёлтые — замороженный генератор и окна Ω₁ и Ω₂; зелёные — то, что задаёт пользователь (цели gᵢ, маска M); красные — изображение, положения точек и градиент. Оптимизируются только w₁ … w₆, коды грубых слоёв. Карта признаков нарисована схематично: 10 × 10 клеток, участок Ω₁ 3 × 3 и окно Ω₂ 5 × 5; при 512 × 512 DragGAN берёт круг радиуса r₁ = 3 и квадрат с r₂ = 12, λ = 20 и Adam с шагом 2·10⁻³.
  1. Генератор переводит ww в изображение и, после шестого блока, в карту признаков FF.
  2. Motion supervision сравнивает признаки в участке Ω1\Omega_1 вокруг каждой точки с тем же участком, сдвинутым на один пиксель вдоль did_i.
  3. Градиент функции потерь распространяется назад до первых шести кодов w1,…,w6w_1, \dots, w_6, и Adam делает один шаг.
  4. Генератор снова запускается с новым кодом w′w'; в его признаках F′F' содержимое вокруг каждой точки немного сдвинулось.
  5. Отслеживание точек находит новые положения точек поиском ближайшего соседа в F′F'. Цикл возвращается к шагу 2, пока каждая точка не окажется достаточно близко к своей цели.

Motion supervision#

Функция потерь одной итерации —

L(w)=∑i=1n∑q∈Ω1(pi,r1)∥Fq+di(w)−sg[Fq(w)]∥1+λ ∥(F(w)−F0)⊙(1−M)∥1.\begin{aligned} \mathcal L(w) = {}&\sum_{i=1}^{n} \sum_{q \in \Omega_1(p_i, r_1)} \big\lVert F_{q + d_i}(w) - \mathrm{sg}\big[F_q(w)\big] \big\rVert_1 \\ &+ \lambda\, \big\lVert \big(F(w) - F_0\big) \odot (1 - M) \big\rVert_1 . \end{aligned}

Первое слагаемое сравнивает участок признаков вокруг каждой точки с тем же участком, сдвинутым на один пиксель к цели. Эталон Fq(w)F_q(w) заморожен, поэтому уменьшить слагаемое можно лишь одним способом: менять признаки в сдвинутых позициях q+diq + d_i, пока они не станут похожи на текущие признаки в qq, — содержимое вокруг pip_i копируется на один шаг вдоль did_i. Позиции q+diq + d_i нецелые, поэтому Fq+diF_{q + d_i} читается билинейной интерполяцией. Именно интерполяция позволяет единичному шагу указывать в любом направлении, а не только вдоль сетки, и сохраняет сдвинутые признаки дифференцируемой функцией карты признаков. Если точка уже стоит в цели, did_i полагают равным 0; тогда её слагаемое тождественно равно нулю и больше на эту точку не действует. Второе слагаемое, которое используется, когда нарисована маска, сохраняет признаки вне подвижной области равными признакам исходного изображения.

Оптимизируемая переменная лежит в W+\mathcal W^+, и обновляются только первые шесть её кодов. Статья отмечает, что пространственные атрибуты задаются в основном этими кодами, а более поздние отвечают за внешний вид; если поздние коды зафиксировать, содержимое сдвигается без перерисовки — то же разделение, что и при смешивании стилей (Pan et al., 2023, раздел 3.2). W+\mathcal W^+ вместо W\mathcal W оставляет место для правок, редких в обучающих данных; пример из статьи — закрыть кошке один глаз, что в W\mathcal W не удаётся (рисунок 16).

Почему эталон заморожен#

Без stop-gradient обе части каждой разности зависят от ww, и градиент становится разностью двух почти равных градиентов. Он больше не предпочитает направление did_i: функцию потерь можно уменьшить так же хорошо, сдвигая содержимое назад, чтобы FqF_q стал похож на Fq+diF_{q + d_i}, или сглаживая признаки в участке, пока все позиции не станут одинаковыми.

Разобранный пример. Одномерный игрушечный генератор рисует горб, положение которого задаёт латент ss, и читает два канала признаков в позиции xx:

Fx(s)=(ϕ(x−s), x−sσ ϕ(x−s)),ϕ(u)=e−u2/(2σ2),σ=2.F_x(s) = \Big(\phi(x - s),\ \tfrac{x - s}{\sigma}\, \phi(x - s)\Big), \qquad \phi(u) = e^{-u^2 / (2\sigma^2)}, \quad \sigma = 2 .

Второй канал отличает левый склон горба от правого. Возьмём s=0s = 0, управляющую точку p=1p = 1, цель g=4g = 4, так что d=1d = 1, и участок из одной позиции, Ω1={1}\Omega_1 = \{1\}. Эталон — F1(0)=(0.8825,0.4412)F_1(0) = (0.8825, 0.4412), сдвинутый признак — F2(0)=(0.6065,0.6065)F_2(0) = (0.6065, 0.6065), поэтому функция потерь равна 0.276+0.165=0.4410.276 + 0.165 = 0.441; первая разность отрицательна, вторая положительна.

Со stop-gradient меняется только F2(s)F_2(s). Его производные — ∂F2/∂s=(0.3033,0)\partial F_2 / \partial s = (0.3033, 0), поэтому

∂L∂s=−0.3033+0=−0.3033.\frac{\partial \mathcal L}{\partial s} = -0.3033 + 0 = -0.3033 .

Градиентный спуск увеличивает ss и двигает горб к цели. При замороженном эталоне функция потерь как функция ss равна 0.2530.253 при s=0.5s = 0.5, 00 при s=1s = 1 и 0.2860.286 при s=1.5s = 1.5: её минимум — ровно сдвиг содержимого на dd.

Без stop-gradient двигается и эталон, с ∂F1/∂s=(0.2206,−0.3309)\partial F_1 / \partial s = (0.2206, -0.3309), и

∂L∂s=−(0.3033−0.2206)+(0−(−0.3309))=0.248.\frac{\partial \mathcal L}{\partial s} = -(0.3033 - 0.2206) + \big(0 - (-0.3309)\big) = 0.248 .

Знак меняется: теперь градиентный спуск уводит горб от цели.

Отслеживание точек#

После шага содержимое сдвинулось, но на величину, которую функция потерь не задаёт. Один градиентный шаг может сдвинуть его меньше чем на пиксель или больше — в зависимости от шага обучения и от объекта. Если бы следующий шаг контролировал старое положение, он толкал бы не то содержимое. Поэтому DragGAN заново находит каждую точку поиском ближайшего соседа в пространстве признаков (Pan et al., 2023, раздел 3.3):

pi←arg⁡min⁡q∈Ω2(pi,r2)∥Fq(w′)−fi∥1,fi=Fpi0(w0),p_i \leftarrow \arg\min_{q \in \Omega_2(p_i, r_2)} \big\lVert F_q(w') - f_i \big\rVert_1, \qquad f_i = F_{p_i^0}(w_0),

где w′w' — обновлённый код, а fif_i — признак точки в исходном изображении. Эталоном всегда служит исходный признак, а не признак с предыдущей итерации, поэтому малые ошибки отслеживания не накапливаются в дрейф. В разобранном примере, после того как горб сдвинулся в s=1s = 1, при r2=3r_2 = 3 кандидаты x=−1,0,1,2,3x = -1, 0, 1, 2, 3 находятся на расстояниях 1.324,0.882,0.559,0,0.4411.324, 0.882, 0.559, 0, 0.441 от f=(0.8825,0.4412)f = (0.8825, 0.4412): точка найдена в x=2=p+dx = 2 = p + d. По одному первому каналу x=0x = 0 оказалось бы наравне с x=2x = 2; для отслеживания нужны признаки, различающие позиции.

Почему не специальный трекер? Признаки генератора уже вычислены в прямом проходе и достаточно хорошо различают позиции. Замена поиска ближайшего соседа сетью оптического потока (RAFT) или трекером точек (PIPs) в тестах статьи дала бо́льшие итоговые ошибки и более медленные правки (таблица ниже). Motion supervision и отслеживание разделены, потому что отвечают на разные вопросы: функции потерь нужно текущее положение точки, чтобы разместить участок, а где это положение теперь, может сказать только поиск после шага.

Цикл и его параметры#

  • Оптимизатор — Adam по шести кодам с шагом обучения 2⋅10−32 \cdot 10^{-3} для FFHQ, AFHQ-Cat и LSUN Car и 10−310^{-3} для остальных наборов данных.
  • λ=20\lambda = 20, r1=round(3R/512)r_1 = \mathrm{round}(3R/512) и r2=round(12R/512)r_2 = \mathrm{round}(12R/512) для изображения R×RR \times R: 3 и 12 пикселей при R=512R = 512, 6 и 24 при R=1024R = 1024.
  • Цикл останавливается, когда каждая точка находится не дальше 1 пикселя от своей цели или не дальше 2 пикселей, если точек больше пяти (Pan et al., 2023, раздел 3.4).
  • Правка обычно занимает от 30 до 200 итераций, несколько секунд на одной RTX 3090 (разделы 1 и 3.1). Пользователь может остановиться в любой момент, передвинуть цели, добавить точки и продолжить.

Игрушечная модель ниже прогоняет весь цикл в двух измерениях; латентом служит положение пятна.

Рисунок

Игрушечный DragGAN: одна точка, сдвиг пятна как латент

Перетаскивание точки в игрушечной моделиСлева: карта 32 на 20 с пятном после 13 шагов, путь отслеженной точки от (11, 9) до (20, 13) рядом с целью (21, 13), окна Ω₁ и Ω₂ в начале. Справа: расстояние до цели по итерациям; со stop-gradient оно падает с 10.77 до 1, без него растёт до 11.40.карта признаков (канал 1) после 13 шаговp₀p₁₃цель gбез sgотслеженная точка pцель gΩ₁, r₁ = 3Ω₂, r₂ = 5пятно в началерасстояние ‖p − g‖ до цели, пиксели05101304812итерация kстоп: 1 пиксель11.40со stop-gradientбез негоПеретаскивание точки в игрушечной моделиСлева: карта 32 на 20 с пятном после 13 шагов, путь отслеженной точки от (11, 9) до (20, 13) рядом с целью (21, 13), окна Ω₁ и Ω₂ в начале. Справа: расстояние до цели по итерациям; со stop-gradient оно падает с 10.77 до 1, без него растёт до 11.40.карта признаков (канал 1) после 13 шаговp₀p₁₃цель gбез sgотслеженная точка pцель gΩ₁, r₁ = 3Ω₂, r₂ = 5пятно в началерасстояние ‖p − g‖ до цели, пиксели05101304812итерация kстоп: 1 пиксель11.40со stop-gradientбез него
Игрушечный «генератор» рисует одно гладкое пятно (σ = 3), положение которого и есть латент s = (s₁, s₂); у карты три канала — само пятно и две его копии, умноженные на горизонтальное и вертикальное смещение от центра, так что у каждой точки рядом с пятном свой вектор признаков. Точка p₀ = (11, 9) лежит на правом верхнем склоне пятна, цель g = (21, 13) — в 10.77 пикселя. Каждая итерация делает один шаг градиентного спуска по s (шаг 0.1) с потерей motion supervision по 25 пикселям Ω₁ (r₁ = 3), затем ищет точку ближайшим соседом её исходного признака в окне Ω₂ 9 × 9 (r₂ = 5). Со stop-gradient точка доходит до цели (расстояние 1 пиксель) за 13 итераций; содержимое на самом деле сдвинулось на (9.11, 4.01), так что отслеженная позиция (20, 13) отстоит от истинной на 0.11 пикселя. Без stop-gradient градиент на первом шаге в десять раз меньше (0.80 против 7.84) и направлен под углом 111° к d вместо 4°: за те же 13 итераций точка уходит от цели, до 11.40 пикселя. Числа игрушечные.

Две особенности игрушки переносятся на настоящий метод. Функция потерь не убывает от итерации к итерации: всё время она остаётся между 7.4 и 8.5, потому что каждая итерация задаёт новый участок в новом положении точки. Прогресс измеряется расстоянием до цели, а не функцией потерь. Кроме того, отслеживание возвращает позиции сетки, а содержимое движется непрерывно: в конце содержимое сдвинулось на (9.11,4.01)(9.11, 4.01), а отслеженная точка находится в 0.11 пикселя от того места, где на самом деле оказалось её содержимое.

Результаты#

Основной количественный тест — перемещение ключевых точек лица (face landmark manipulation; Pan et al., 2023, раздел 4.2). Моделью StyleGAN2, обученной на FFHQ при 512×512512 \times 512, генерируются два случайных лица; ключевые точки первого лица, найденные детектором dlib, служат управляющими точками, а точки второго лица — целями. Среднее расстояние (mean distance, MD) между ключевыми точками, найденными на отредактированном изображении, и целями усредняется по 1000 тестам, не более 300 итераций на правку. FID между отредактированными и исходными изображениями проверяет, что правки остаются реалистичными.

МетодMD, 1 точкаMD, 5 точекMD, 68 точекFIDВремя, с
Без правки12.9311.6616.02––
UserControllableLT11.6410.4110.1525.320.03
DragGAN с отслеживанием RAFT13.4313.5915.9251.3715.4
DragGAN с отслеживанием PIPs2.984.835.3031.876.6
DragGAN2.443.184.739.282.0

FID и время приведены для задачи с одной точкой. UserControllableLT, более ранний метод редактирования по точкам, быстр, но почти не сдвигает ключевые точки. Вблизи выбранного блока признаков выбор блока мало что меняет: блок 6 даёт MD 2.44, блоки 5 и 7 дают 2.50 и 2.51 для motion supervision и 2.55 и 2.58 для отслеживания, а блок 4 — 2.73 и 3.61 (таблица 3). Радиус r1r_1 влияет ещё меньше: MD от 2.44 до 2.51 при r1r_1 от 1 до 5 (таблица 4). Во втором тесте изображение, порождённое возмущённым кодом, нужно восстановить по исходному изображению и 32 перетаскиваниям, взятым из оптического потока между ними; у DragGAN наименьшие квадратичная ошибка и LPIPS во всех четырёх категориях (таблица 2).

DragDiffusion: тот же цикл в латенте диффузии#

DragGAN редактирует только то, что может сгенерировать его GAN, а реальные фото — лишь настолько хорошо, насколько удаётся обращение. Диффузионная модель «текст в изображение» покрывает гораздо больше изображений, чем любой GAN одной категории, но у неё нет единого кода вроде ww: изображение — конец цепочки шагов очистки от шума (денойзинга). DragDiffusion сохраняет оба подшага DragGAN и меняет то, что оптимизируется: один зашумлённый латент Stable Diffusion 1.5 на одном промежуточном шаге (Shi et al., 2024). Два дополнения сохраняют верность отредактированного изображения оригиналу: дообучение U-Net с LoRA на входном изображении перед редактированием и изменение self-attention при финальной очистке от шума.

Рисунок

DragDiffusion: четыре этапа

Конвейер DragDiffusionРяд блоков: фото x, латент z₀, z₃₅ после обращения, изменённый ẑ₃₅ и результат. Над ними U-Net с LoRA, обученная на z₀ и используемая на всех следующих этапах. Под ними опорный проход по исходному z₃₅, который передаёт ключи и значения в денойзинг изменённого латента.x512 × 512фото xEz₀4 × 64 × 64U-Net с LoRA Δθранг 16, 80 шагов обучения на одном z₀обращение35 из 50z₃₅t = 35оптимизация≤ 80 шаговẑ₃₅изменёнпризнаки декодера U-Netr₁ = 1, r₂ = 3, λ = 0.1денойзингD(ẑ₀)измененорезультатопорный проходденойзинг исходного z₃₅K, V1234входсостояниевеса и гейтывыходКонвейер DragDiffusionСтолбец блоков: фото x, латент z₀, z₃₅ после обращения, изменённый ẑ₃₅ и результат. Слева LoRA, справа опорный проход, передающий ключи и значения в денойзинг.x512 × 512Ez₀4 × 64 × 64LoRA Δθ80 шагов на z₀нужна в 2–4обращение35 из 50z₃₅t = 35оптимизация≤ 80 шаговẑ₃₅изменёнпризнаки U-Netr₁ = 1, r₂ = 3λ = 0.1денойзинг + DD(ẑ₀)измененоопорныйпроход по z₃₅K, V1234входсостояниевеса и гейтывыход
Зелёное — исходное фото, синие — латенты Stable Diffusion 1.5 (4 × 64 × 64 для фото 512 × 512), жёлтое — U-Net с весами LoRA, красное — результат. Оптимизируется один латент z₃₅ на шаге 35 из 50 шагов DDIM; всё остальное заморожено после этапа 1. Classifier-free guidance для реальных фото выключен и в инверсии, и в денойзинге.
  1. Дообучение с сохранением идентичности обучает веса LoRA в U-Net только на входном изображении.
  2. Обращение DDIM переводит латент изображения z0z_0 в зашумлённый латент z35z_{35}, отстоящий от изображения на 35 шагов 50-шагового расписания.
  3. Оптимизация латента сдвигает точки с помощью motion supervision и отслеживания точек на признаках U-Net; меняется только z35z_{35}.
  4. Очистка от шума с reference-latent-control возвращает отредактированный латент в изображение, заимствуя ключи и значения из очистки исходного латента.

Дообучение с сохранением идентичности#

Обращение DDIM с последующей очисткой от шума не возвращает в точности входное фото, а после правки латента расхождение растёт: лица меняют идентичность, меняются текстуры. Поэтому перед редактированием DragDiffusion дообучает (fine-tuning) U-Net на одном изображении обновлениями LoRA в проекциях запросов, ключей и значений всех слоёв внимания. Функция потерь — целевая функция DDPM из заметки о латентной диффузии, ограниченная одним латентом z0=E(x)z_0 = \mathcal E(x):

Lft(Δθ)=Eε,t[∥ε−εθ+Δθ(αˉt z0+1−αˉt ε, t, τ(y))∥22],\mathcal L_{\text{ft}}(\Delta\theta) = \mathbb E_{\varepsilon, t}\Big[\big\lVert \varepsilon - \varepsilon_{\theta + \Delta\theta}\big(\sqrt{\bar\alpha_t}\, z_0 + \sqrt{1 - \bar\alpha_t}\,\varepsilon,\ t,\ \tau(y)\big)\big\rVert_2^2\Big] ,

где θ\theta заморожены, Δθ\Delta\theta — веса LoRA, а yy — короткий промпт, описывающий изображение. Статья записывает зашумлённый латент как αtz+σtε\alpha_t z + \sigma_t \varepsilon; это то же самое при αt=αˉt\alpha_t = \sqrt{\bar\alpha_t} и σt=1−αˉt\sigma_t = \sqrt{1 - \bar\alpha_t}. Параметры: ранг 16, AdamW с шагом обучения 5⋅10−45 \cdot 10^{-4}, размер батча 4 и 80 шагов, около 25 секунд на A100 (Shi et al., 2024, раздел 4.1 и приложение D). После дообучения модель точно восстанавливает это изображение, и результат правки остаётся узнаваемо той же картинкой. Без дообучения среднее расстояние резко растёт, а верность изображению (image fidelity) падает; обе величины быстро улучшаются с ростом числа шагов и выходят на плато, а меньше 80 шагов оставляют артефакты вроде искажённых лиц (разделы 4.4 и 4.6, приложение G).

Обращение DDIM до одного шага#

Сэмплирование с DDIM детерминировано. Пусть 50 шагов сэмплера пронумерованы через tt, а αˉt\bar\alpha_t взяты в соответствующих шагах обучения; тогда один шаг —

zt−1=αˉt−1  z^0(zt)+1−αˉt−1  εθ(zt,t),z^0(zt)=zt−1−αˉt  εθ(zt,t)αˉt:\begin{aligned} z_{t-1} &= \sqrt{\bar\alpha_{t-1}}\; \hat z_0(z_t) + \sqrt{1 - \bar\alpha_{t-1}}\; \varepsilon_\theta(z_t, t), \\ \hat z_0(z_t) &= \frac{z_t - \sqrt{1 - \bar\alpha_t}\; \varepsilon_\theta(z_t, t)}{\sqrt{\bar\alpha_t}} : \end{aligned}

предсказать чистый латент, затем снова зашумить его до следующего уровня вдоль предсказанного шума (Song, Meng and Ermon, 2021, формула 12 при σt=0\sigma_t = 0). Обращение DDIM (DDIM inversion) выполняет то же обновление вверх, от ztz_t к zt+1z_{t+1}, подставляя шум, предсказанный в ztz_t, вместо шума в неизвестном zt+1z_{t+1}. При малых шагах подстановка мало что меняет, и очистка обращённого латента возвращает изображение, близкое к исходному (раздел 4.3 статьи о DDIM рассматривает обновление как дискретизацию ОДУ, которое можно решать в любом направлении).

DragDiffusion обращает фото на 35 из 50 шагов и редактирует z35z_{35}; tt отсчитывается от изображения, так что t=0t = 0 — фото, а t=50t = 50 — чистый шум. Для реальных фото classifier-free guidance выключен и при обращении, и при очистке от шума, потому что он усиливает ошибку обращения (Shi et al., 2024, раздел 4.1).

Почему достаточно одного латента? Перетаскивание меняет компоновку, а компоновка диффузионного сэмпла устанавливается рано в цепочке, при сильном шуме; поздние шаги добавляют детали. Статья показывает, что признаки U-Net при t=35t = 35 уже содержат форму и позу объекта (рисунок 2), так что оптимизация этого одного латента может сдвинуть объект. Тогда каждая итерация стоит одного прямого и одного обратного прохода через U-Net, а не обратного прохода через всю цепочку шагов очистки. Выбор шага — компромисс (раздел 4.5). Верность изображению монотонно убывает с ростом tt, потому что более зашумлённый латент свободнее меняется. Среднее расстояние минимально при tt от 30 до 40: при меньших tt латент слишком скован, чтобы совершить перемещение, а при больших tt редактирование становится неустойчивым и идентичность теряется.

Оптимизация латента на признаках U-Net#

Motion supervision из DragGAN переносится с латентом вместо ww и признаками U-Net вместо признаков генератора. Пусть z^tk\hat z_t^k — латент после kk обновлений, z^t0=zt\hat z_t^0 = z_t; тогда

Lms(z^tk)=∑i=1n∑q∈Ω(pik,r1)∥Fq+di(z^tk)−sg[Fq(z^tk)]∥1+λ ∥(z^t−1k−sg[z^t−10])⊙(1−M)∥1,\begin{aligned} \mathcal L_{\text{ms}}(\hat z_t^k) = {}&\sum_{i=1}^{n} \sum_{q \in \Omega(p_i^k, r_1)} \big\lVert F_{q + d_i}(\hat z_t^k) - \mathrm{sg}\big[F_q(\hat z_t^k)\big] \big\rVert_1 \\ &+ \lambda\, \big\lVert \big(\hat z_{t-1}^k - \mathrm{sg}[\hat z_{t-1}^0]\big) \odot (1 - M) \big\rVert_1 , \end{aligned}

и каждое обновление — градиентный шаг по латенту, z^tk+1=z^tk−η ∂Lms/∂z^tk\hat z_t^{k+1} = \hat z_t^k - \eta\, \partial \mathcal L_{\text{ms}} / \partial \hat z_t^k, на практике выполняемый с Adam (Shi et al., 2024, раздел 3.3). От DragGAN отличаются три вещи.

  • Признаки. F(z^t)F(\hat z_t) — выход одного блока декодера U-Net, когда U-Net очищает от шума z^t\hat z_t на шаге tt. Из четырёх блоков повышения разрешения третий даёт наименьшее среднее расстояние при относительно высокой верности. Первые блоки слишком грубы для точного управления, а последний несёт текстуру, а не геометрию, и почти ничего не сдвигает (раздел 4.7). Официальный код билинейно приводит эту карту к половине разрешения изображения, 256×256256 \times 256 для изображения 512×512512 \times 512, и соответственно масштабирует точки; Fq+diF_{q + d_i} читается билинейной интерполяцией, как в DragGAN.
  • Участки. Ω(p,r)\Omega(p, r) — квадрат с полушириной rr: 3×33 \times 3 позиции для motion supervision (r1=1r_1 = 1) и 7×77 \times 7 для отслеживания (r2=3r_2 = 3) на этой карте.
  • Слагаемое маски. Оно действует на латенты, а не на признаки. z^t−1k\hat z_{t-1}^k — один шаг очистки DDIM от текущего латента, а z^t−10\hat z_{t-1}^0 — тот же шаг от неизменённого латента, так что вне маски следующий, чуть более чистый латент не должен меняться. По умолчанию λ=0.1\lambda = 0.1; пользователь может увеличить это значение, если область вне маски всё же сдвигается.

Отслеживание — как в DragGAN, но на признаках U-Net:

pik+1=arg⁡min⁡q∈Ω(pik,r2)∥Fq(z^tk+1)−Fpi0(zt)∥1.p_i^{k+1} = \arg\min_{q \in \Omega(p_i^k, r_2)} \big\lVert F_q(\hat z_t^{k+1}) - F_{p_i^0}(z_t) \big\rVert_1 .

Признаки U-Net играют ту же роль, что признаки генератора в DragGAN. Они всё равно вычисляются в прямом проходе, дифференцируемо зависят от оптимизируемого латента и несут соответствия между изображениями (Tang et al., 2023). Сети оптического потока понадобилось бы декодированное изображение на каждой итерации, тогда как для motion supervision и так нужны признаки, дифференцируемо зависящие от латента. Adam работает с шагом обучения 0.01 не более 80 шагов, от 10 до 30 секунд на A100 в зависимости от размера перетаскивания (раздел 4.1 и приложение D).

Reference-latent-control#

После оптимизации z^t\hat z_t очищается от шума за оставшиеся tt шагов и декодируется. Обычная очистка может уйти от оригинала по идентичности и текстуре. Reference-latent-control (управление через опорный латент) параллельно выполняет очистку исходного ztz_t и на каждом шаге в слоях self-attention блоков повышения разрешения U-Net заменяет ключи и значения отредактированной ветви ключами и значениями исходной:

Attention(Q^,K,V)=softmax(Q^K⊤d)V,Q^=WQ φ^,K=WK φ,V=WV φ,\begin{aligned} \mathrm{Attention}(\hat Q, K, V) &= \mathrm{softmax}\Big(\frac{\hat Q K^\top}{\sqrt{d}}\Big) V, \\ \hat Q = W_Q\, \hat\varphi, \quad K &= W_K\, \varphi, \quad V = W_V\, \varphi , \end{aligned}

где φ^\hat\varphi и φ\varphi — вытянутые в вектор признаки отредактированной и исходной ветвей, а dd — размерность ключа (Shi et al., 2024, разделы 3.4 и 4.1). Механизм повторяет взаимный self-attention (mutual self-attention) из MasaCtrl (Cao et al., 2023). Каждая позиция отредактированного изображения находит позиции оригинала, ключи которых подходят к её запросу, и копирует их значения, так что кошка со сдвинутым ухом всё равно получает шерсть исходной кошки. В перекрёстном внимании из заметки о латентной диффузии ключи и значения берутся из промпта; здесь они берутся из исходного изображения. Удаление этого шага в основном снижает верность изображению (раздел 4.4). Финальная очистка от шума занимает 1–2 секунды.

Оценка на DragBench#

DragBench создан специально для этой задачи (Shi et al., 2024, раздел 4.2 и приложение A). В нём 205 изображений с 349 парами управляющих и целевых точек в десяти категориях: животные, произведения искусства, здания (виды города и сельской местности), люди — голова, по пояс и в полный рост, — дизайн интерьера, пейзажи и прочие объекты. Изображения людей сгенерированы Midjourney; остальные — фотографии с Unsplash, Pexels и Pixabay. Сообщаются две меры.

  • Верность изображению IF=1−LPIPS(x,xedit)\mathrm{IF} = 1 - \mathrm{LPIPS}(x, x_{\text{edit}}), усреднённая по изображениям: сколько осталось от оригинала; 1 для неизменённого изображения (LPIPS).
  • Среднее расстояние MD: положение, в которое попало содержимое управляющей точки, находится на отредактированном изображении с помощью DIFT — метода поиска соответствий на признаках диффузии (Tang et al., 2023), — и его расстояние до цели усредняется по всем парам; 0 для идеального перетаскивания.

Две меры тянут в противоположные стороны. Бездействие даёт идеальную верность и начальное расстояние; радикальное изменение может достичь целей и потерять изображение. Их нужно читать вместе, и статья откладывает одну против другой (рисунок 8). Для DragGAN каждое изображение редактируется моделью StyleGAN, обученной на наборе одного класса — того, который по сходству CLIP лучше всего соответствует описанию изображения (раздел 4.4).

КатегорияMD, DragGANMD, DragDiffusionIF, DragGANIF, DragDiffusion
произведения искусства59.5130.740.710.88
пейзаж47.6036.550.840.88
город41.9426.180.740.89
сельская местность46.9643.210.790.88
животные60.1239.220.720.87
голова65.1436.430.910.85
по пояс82.9839.750.330.89
в полный рост37.0120.560.310.95
дизайн интерьера75.6524.830.570.90
прочие объекты58.2539.520.710.87

Значения взяты из таблиц 1 и 2 статьи; MD измеряется в пикселях, и чем оно меньше, тем лучше, а IF — чем больше, тем лучше. DragDiffusion оставляет точки ближе к целям во всех десяти категориях и сохраняет больше изображения в девяти. Исключение — категория «голова», где DragGAN сохраняет больше (0.91 против 0.85), но оставляет точки гораздо дальше от целей (65.14 против 36.43). В категориях «по пояс» и «в полный рост» верность DragGAN падает до 0.33 и 0.31: обращённые изображения теряют бо́льшую часть оригинала.

Что изменилось#

Image-to-image и инпейнтингDragGANDragDiffusion
Пользователь задаётпромпт, маскуточки, цели, необязательную маскуточки, цели, необязательную маску, промпт
МодельStable DiffusionStyleGAN2 одной категорииStable Diffusion 1.5
Что оптимизируетсяничего во время правкипервые 6 кодов w∈W+w \in \mathcal W^+, 6⋅512=30726 \cdot 512 = 3072 чиселодин латент z35z_{35}, 4⋅64⋅64=16 3844 \cdot 64 \cdot 64 = 16\,384 числа
Признакинетгенератора, после блока 6декодера U-Net, третий блок повышения разрешения
Сохранение остальногомаскаλ=20\lambda = 20 на признаках вне маскиλ=0.1\lambda = 0.1 на следующем латенте, LoRA, reference-latent-control
Реальные фотодапосле обращения PTIпосле LoRA и обращения DDIM
Время на правкуодин запуск сэмплированиянесколько секундоколо 25 + 10–30 + 1–2 с

Ограничения#

  • Возможности модели ограничивают правку. DragGAN не может выйти за пределы области своего GAN, а позы вне распределения дают артефакты (Pan et al., 2023, раздел 4.3). У DragDiffusion диапазон гораздо шире, но каждая правка фото проходит через приближённое обращение.
  • Отслеживание может сбиться. Поиск ближайшего соседа по признакам дрейфует в областях без текстуры, где многие позиции выглядят одинаково, и на симметричных объектах, как показывает одноканальный случай разобранного примера. DragGAN рекомендует ставить точки на участках с богатой текстурой.
  • Не каждая точка доходит до цели. DragDiffusion иногда оставляет точки, не дошедшие до целей (приложение E); на DragBench её среднее расстояние всё ещё составляет от 20.56 до 43.21 пикселя в зависимости от категории.
  • Перетаскивание неоднозначно. Несколько точек не определяют изображение. Без маски DragGAN склонен выбирать решение, ближайшее к выученному многообразию, например двигать всю собаку, когда перетаскивали только её голову (раздел 4.3).
  • Стоимость на изображение. PTI для DragGAN на фото и дообучение LoRA для DragDiffusion добавляют запуск обучения перед каждым новым изображением.
  • Оценка — пара чисел. Верность поощряет бездействие, а среднее расстояние — достижение цели любой ценой.
  • Злоупотребления. Оба инструмента позволяют легко изменить позу, выражение лица или фигуру реального человека на фото (раздел 4.3).

Итоги#

  • Перетаскивание — это оптимизация латента генератора: каждый промежуточный результат — выход модели, которая служит априорным знанием о реалистичности и позволяет придумывать открывшееся содержимое. Реалистичность и идентичность всё равно нужно проверять, потому что оптимизированный код может уйти из области, которую поддерживают обучающие данные.
  • StyleGAN2 переводит zz в ww, модулирует каждую свёртку стилем из ww и в W+\mathcal W^+ берёт отдельный код для каждого слоя; первые коды задают компоновку, поздние — внешний вид. Реальные фото попадают в модель через обращение GAN, для DragGAN — через PTI.
  • DragGAN чередует motion supervision — функцию потерь L1, которая копирует участок признаков на один пиксель к цели при эталоне, замороженном stop-gradient, — и отслеживание точек поиском ближайшего соседа к исходному признаку точки. Обновляются только первые шесть кодов ww.
  • DragDiffusion запускает тот же цикл в Stable Diffusion: дообучение LoRA на входном изображении, обращение DDIM до t=35t = 35 из 50, оптимизация этого одного латента на признаках декодера U-Net и очистка от шума с ключами и значениями, заимствованными у оригинала.
  • DragGAN быстр и точен на изображениях, которые может породить его GAN. DragDiffusion работает с реальными фото любого рода и на DragBench подходит к целям ближе в каждой категории и сохраняет больше изображения во всех категориях, кроме одной.

Ноутбук#

Ноутбук о DragGAN из репозитория HSE-LAMBDA Deep Generative Models охватывает оба метода: краткие изложения в текстовых ячейках, два приложения Gradio (неофициальную реализацию DragGAN от сообщества и официальный интерфейс DragDiffusion) и две короткие реализации, написанные с нуля (ячейки нумеруются с 1, считая все ячейки подряд, включая текстовые). Сохранённых выходов ячеек в нём нет, поэтому он документирует процедуры, и заметка не приводит из него результатов. Изложение выше от этого раздела не зависит: он фиксирует, что делают ячейки и где они отступают от двух методов, для тех, кто запускает ноутбук.

  • Ячейка 1: значок «Open in Colab», указывающий на копию на Google Drive, которая может отличаться от версии в репозитории.
  • Ячейки 2–4: DragGAN. План, мотивация (ячейка 3) и метод (ячейка 4): конвейер, обе функции потерь, что обучается (ничего, кроме латента), параметры (Adam, шаг обучения 2⋅10−32 \cdot 10^{-3} или 10−310^{-3}, r1≈3r_1 \approx 3, r2≈12r_2 \approx 12 с масштабированием по размеру изображения, λ=20\lambda = 20, остановка в пределах 1–2 пикселей или по лимиту итераций, например 300), стоимость (секунды на RTX 3090) и обращение GAN для реальных фото. Ячейка 4 говорит, что исходное изображение получено от «a pre-trained StarGANv2»; на самом деле это StyleGAN2. Она говорит, что градиент течёт «through the target locations, not the original handle points»; в функции потерь градиент течёт через сдвинутые признаки Fq+diF_{q + d_i} участка, а не через эталон FqF_q, а сама цель входит только через did_i. Сумма в motion supervision идёт от i=0i = 0 до nn, как в статье; для nn точек это i=1,…,ni = 1, \dots, n.
  • Ячейки 5–10: приложение Gradio для DragGAN. Ячейка 6 клонирует Zeqiang-Lai/DragGAN, который теперь перенаправляет на OpenGVLab/DragGAN, — неофициальную реализацию (официальная — XingangPan/DragGAN); ячейка 7 устанавливает её зависимости, которые фиксируют Gradio 3.34.0; ячейки 8 и 10 импортируют gradio_app.main и запускают его с queue(concurrency_count=1, max_size=20) — аргументом, который существует только в Gradio 3. Приложению нужен GPU с CUDA (device = 'cuda' жёстко прописан в коде), а по умолчанию оно использует модель AFHQ-Cat при 512×512512 \times 512. Его drag_gan следует методу: оптимизирует первые шесть кодов W+\mathcal W^+ с Adam при 2⋅10−32 \cdot 10^{-3}, берёт признаки седьмого блока синтеза, 256×256256 \times 256, билинейно приведённые к размеру изображения (официальный код берёт шестой, 128×128128 \times 128), использует круг с r1=3r_1 = 3 и квадрат с r2=12r_2 = 12, отсоединяет (detach) эталонный участок и останавливается в пределах 2 пикселей. Для пользователя важны два отклонения: слагаемое маски закомментировано, так что нарисованная в интерфейсе маска ни на что не влияет, а шаг отслеживания использует признаки, вычисленные до шага Adam, так что отслеживание и показанное изображение отстают от кода на одну итерацию. Проверка остановки, torch.allclose(handle_points, target_points, atol=2), проверяет каждую координату отдельно, а не расстояние.
  • Ячейки 11–20: DragGAN «с нуля». Конфигурация, модель и конвейер в стиле классов Hugging Face оборачивают генератор StyleGAN2-ADA AFHQ-Cat (ada/afhqcat.pkl из ZeqiangLai/StyleGAN2-pkl); ячейка 17 клонирует stylegan2-ada-pytorch, а ячейка 14 загружает pickle из пути Colab /content/stylegan2-ada-pytorch. Ячейка 18 перетаскивает две точки, заданные как (y,x)(y, x), из (200,250)(200, 250) и (120,160)(120, 160) в (400,450)(400, 450) и (320,410)(320, 410) с Adam при шаге обучения 0.01 за 20 шагов, а ячейки 19–20 показывают изображения до и после. Это не DragGAN. Перед генератором стоит необученный слой Linear + LeakyReLU, а собственная отображающая сеть генератора применяется к его выходу, так что переменной служит вектор шума, а не первые шесть кодов W+\mathcal W^+. Функция потерь, F.l1_loss(handle_feats, target_feats), сравнивает значения RGB выходного изображения в пикселе точки и в пикселе цели, удалённых друг от друга на 283 и 320 пикселей. Нет ни сдвинутого участка, ни stop-gradient, ни отслеживания, ни маски: функцию потерь можно уменьшить, меняя пиксель цели так же, как пиксель точки, и она требует, чтобы два пикселя совпали по цвету, а не чтобы содержимое сдвинулось. Корректная версия регистрирует хук на шестом блоке синтеза (G.synthesis.b128, 128×128128 \times 128, как в официальном коде), получает ws = G.mapping(z, None), оптимизирует ws[:, :6], вычисляет функцию потерь сдвинутого участка с grid_sample относительно отсоединённого эталона и после каждого шага выполняет отслеживание; приложение из ячеек 6–10 делает то же самое на седьмом блоке.
  • Ячейки 21–22: DragDiffusion. Мотивация, три этапа, функция потерь дообучения и её параметры (LoRA в модулях внимания, AdamW, 5⋅10−45 \cdot 10^{-4}, батч 4, 80 шагов, ранг 16, около 25 с на A100), обращение DDIM до t=35t = 35, функция потерь motion supervision с её обозначениями, отслеживание, параметры оптимизации (Adam, 0.01, не более 80 шагов, r1=1r_1 = 1, r2=3r_2 = 3, λ=0.1\lambda = 0.1), reference-latent-control, кратко изложенный конвейер инференса, DragBench с IF и MD, время работы и ограничения. Текст говорит, что GAN ограничивают правки «to synthetic images»; DragGAN редактирует реальные фото через обращение, а предел задаёт ёмкость GAN. «DDIM inversion steps: typically 35» означает 35 шагов из 50-шагового расписания. «Less precise on synthetic data» — не результат ни одной из статей, а «slower than DragGAN» верно для сэмплов GAN; на реальном фото DragGAN сначала требуется PTI, который занимает минуты.
  • Ячейки 23–28: приложение Gradio для DragDiffusion. Ячейка 24 клонирует Yujun-Shi/DragDiffusion; ячейка 25 фиксирует Gradio 3.41.1, Diffusers 0.24.0, Transformers 4.27.0, Accelerate 0.17.0, PEFT 0.4.0, huggingface_hub 0.20.3 и PyTorch Lightning 1.5.0; ячейка 27 описывает два сценария работы с интерфейсом (реальное изображение: ввести промпт, обучить LoRA, нарисовать маску, отметить точки и цели, запустить; сгенерированное изображение: сгенерировать, затем перетаскивать); ячейка 28 запускает drag_ui.py. Официальный код берёт признаки третьего блока повышения разрешения, приведённые к половине разрешения изображения, с r1=1r_1 = 1 и r2=3r_2 = 3 на этой карте; его README требует около 14 ГБ видеопамяти.
  • Ячейки 29–37: DragDiffusion «с нуля», без LoRA. DragDiffusionPipeline (ячейка 30) кодирует промпт, сэмплирует латент из шума с classifier-free guidance с масштабом 7.5, при необходимости выполняет drag_step и декодирует. drag_step делает 5 простых градиентных шагов размера 0.1 по итоговому чистому латенту с функцией потерь F.mse_loss(latents[0,:,sy,sx], latents[0,:,ty,tx]). Ячейка 31 загружает Stable Diffusion 1.5 с LMSDiscreteScheduler; ячейки 33–34 генерируют «A photograph of a cute cat in a hat» при 512×512512 \times 512 за 20 шагов без перетаскивания; ячейки 36–37 генерируют «A cartoon cat on the table» при 256×256256 \times 256 с перетаскиванием из (32,32)(32, 32) в (32,64)(32, 64). Проблемы:
    • Цикл сэмплирования не умножает начальный шум на scheduler.init_noise_sigma (около 14.6 для этого расписания) и не пропускает вход U-Net через scheduler.scale_model_input; LMSDiscreteScheduler нужно и то и другое, поэтому сэмплы неверны. С DDIMScheduler обе операции тождественны.
    • Перетаскивание — не DragDiffusion: оно действует на чистый латент после сэмплирования, а не на обращённый ztz_t; сравнивает сырые каналы латента в двух позициях вместо признаков U-Net в сдвинутом участке; и в нём нет stop-gradient, отслеживания, маски, LoRA, обращения реального изображения и reference-latent-control. Вызов U-Net внутри drag_step на случайном шаге вычисляется и отбрасывается: функция потерь от него не зависит.
    • Точки — координаты на сетке латента, обрезанные по её границам. При 256×256256 \times 256 латент имеет размер 32×3232 \times 32, поэтому и (32,32)(32, 32), и (32,64)(32, 64) становятся (31,31)(31, 31): источник и цель совпадают, функция потерь тождественно равна нулю, и перетаскивание ничего не меняет.
    • Два запуска используют разные промпты, размеры и шум без фиксированного начального значения генератора (seed), так что это не пара «до и после», а Stable Diffusion 1.5 при 256×256256 \times 256 работает намного ниже своего разрешения обучения.
    • runwayml/stable-diffusion-v1-5 удалён с Hugging Face Hub; те же веса опубликованы как stable-diffusion-v1-5/stable-diffusion-v1-5.
  • Ячейка 38: заключение и одиннадцать вопросов. Заключение противопоставляет DragGAN, точный на изображениях, которые генерирует его GAN, и DragDiffusion, распространяющий перетаскивание на реальные изображения. Ответы на вопросы есть в этой заметке: как два метода контролируют движение и что оптимизируют их целевые функции (вопросы 1 и 10) — в разделах Оптимизация латента на признаках U-Net и Что изменилось; зачем нужно отслеживание, почему оно использует признаки, а не оптический поток, и почему оно отделено от motion supervision (2, 5, 8) — в разделе Отслеживание точек и после формулы отслеживания DragDiffusion; чего не умеют GAN (3) — в разделе Обращение GAN; почему достаточно одного шага диффузии и как результат зависит от этого шага (4, 11) — в разделе Обращение DDIM до одного шага; роль LoRA (6) — в разделе Дообучение с сохранением идентичности; какие слои W+\mathcal W^+ оптимизируются (7) и зачем нужна билинейная интерполяция (9) — в разделе Motion supervision.
  • Ячейка 39 пуста.

README папки также ссылается на статью о выделении фона (background matting) как на «Sengupta et al., 2020, arXiv:2011.02225»; этот идентификатор принадлежит не связанной с ней статье по материаловедению, статья о выделении фона — Lin et al. (2021), arXiv:2012.07810, а в ноутбуке выделение фона нигде не используется.

Код#

Разобранный пример на PyTorch, с detach() в роли stop-gradient. Он выполняется примерно за секунду на CPU.

import torch

sigma = 2.0

def features(x, s):
    """Toy generator: a bump at position s, read at positions x; two channels."""
    u = x - s
    phi = torch.exp(-u**2 / (2 * sigma**2))
    return torch.stack([phi, u / sigma * phi])

p, g = 1.0, 4.0
d = 1.0 if g > p else -1.0  # единичный шаг от точки к цели

for stop_grad in (True, False):
    s = torch.tensor(0.0, requires_grad=True)       # латент: положение горба
    ref = features(torch.tensor(p), s)
    if stop_grad:
        ref = ref.detach()                          # sg[F_p(s)]
    loss = (features(torch.tensor(p + d), s) - ref).abs().sum()
    loss.backward()
    print(stop_grad, round(loss.item(), 4), round(s.grad.item(), 4))
# True 0.4412 -0.3033
# False 0.4412 0.2483

# Отслеживание точки после того, как содержимое сдвинулось на d (s = 1):
f = features(torch.tensor(p), torch.tensor(0.0))   # исходный признак точки
xs = torch.arange(-1.0, 4.0)                        # Omega_2 вокруг p = 1 при r2 = 3
dist = (features(xs, torch.tensor(1.0)) - f[:, None]).abs().sum(0)
print(xs[dist.argmin()].item(), [round(v, 3) for v in dist.tolist()])
# 2.0 [1.324, 0.882, 0.559, 0.0, 0.441]

Дальше#

Оба метода двигают изображение по многообразию модели, которая порождает всё изображение сразу — из латентного кода или многократной очисткой от шума всей картинки, — и ни один не даёт вероятности результата. Последнее семейство курса строит изображение по частям: авторегрессионные модели раскладывают плотность изображения в произведение условных распределений, предсказывают по одному пикселю или токену за раз и точно вычисляют правдоподобие.

Литература#