Перейти к содержимому
ПереводыHugging Face

Diffusion Models Course, юнит 3: Stable Diffusion

Перевод юнита 3 курса Hugging Face о диффузионных моделях: латентная диффузия и VAE, текстовое условие через CLIP и перекрёстное внимание, classifier-free guidance, компоненты конвейера Stable Diffusion, свой цикл сэмплирования, img2img, inpainting и depth2img.

Это перевод юнита 3 курса Diffusion Models Course: вводный текст юнита и ноутбук «Stable Diffusion Introduction». Предыдущий юнит — дообучение, guidance и условия, следующий — продвинутые техники: инверсия DDIM и диффузия для звука. Карта курса — в README оригинала.

Добро пожаловать в юнит 3 курса Hugging Face о диффузионных моделях! В этом юните вы познакомитесь с мощной диффузионной моделью Stable Diffusion (SD) и посмотрите, что она умеет.

Шаги этого юнита:

  • Убедитесь, что записались на курс, чтобы получать уведомления о новых материалах.
  • Прочитайте материал ниже — обзор ключевых идей юнита.
  • Откройте ноутбук Stable Diffusion Introduction, чтобы увидеть SD на практике в нескольких типичных сценариях.
  • С помощью ноутбука DreamBooth в папке hackathon дообучите свою версию Stable Diffusion и поделитесь ею с сообществом — с шансом выиграть призы и сувениры.
  • (Необязательно) Посмотрите видео Stable Diffusion Deep Dive и сопровождающий его ноутбук: там глубже разобраны разные компоненты и то, как их менять ради разных эффектов. Этот материал сделан для нового курса FastAI «Stable Diffusion from the Foundations»; курс опубликован целиком и отлично дополняет наш для всех, кому интересно построить такие модели полностью с нуля.

📢 Не забудьте присоединиться к Discord, где можно обсуждать материал и делиться тем, что получилось, в канале #diffusion-models-class.

(Хакатон DreamBooth проходил зимой 2022–2023 годов; призы давно разыграны, но ноутбук по-прежнему полезен как учебный пример.)

(В оригинале — подборка изображений, сгенерированных Stable Diffusion: пейзажи, портреты, фантастические сцены.)

Stable Diffusion — мощная латентная диффузионная модель с текстовым условием. Не волнуйтесь, мы скоро объясним эти слова! Её умение создавать потрясающие изображения по текстовым описаниям сделало её сенсацией интернета. В этом юните мы разберём, как работает SD, и посмотрим, какие ещё трюки она умеет.

С ростом размера изображения растут и вычислительные затраты на работу с ним. Особенно это заметно в операции self-attention, где число операций растёт квадратично с числом входов. В квадратном изображении 128 пикселей в 4 раза больше пикселей, чем в изображении 64 пикселя, поэтому в слое self-attention ему нужно в 16 раз (то есть 4²) больше памяти и вычислений. Это проблема для всех, кто хочет генерировать изображения высокого разрешения!

(В оригинале — схема из статьи о латентной диффузии: слева пиксельное пространство с кодировщиком E\mathcal{E} и декодером D\mathcal{D}, в центре — диффузия в латентном пространстве с U-Net и перекрёстным вниманием, справа — условия (текст, семантическая карта, изображения) и их кодировщик τθ\tau_\theta.)

Латентная диффузия смягчает эту проблему с помощью отдельной модели — вариационного автокодировщика (Variational Auto-Encoder, VAE), — который сжимает изображения до меньшего пространственного размера. Идея в том, что в изображениях много избыточной информации: при достаточном объёме обучающих данных VAE, надеемся, научится выдавать гораздо меньшее представление входного изображения, а затем восстанавливать изображение по этому маленькому латентному представлению с высокой точностью. VAE в SD принимает трёхканальные изображения и выдаёт четырёхканальное латентное представление, уменьшенное в 8 раз по каждому пространственному измерению. То есть квадратное изображение 512 пикселей сжимается до латента 4×64×64.

Применяя процесс диффузии к этим латентным представлениям, а не к изображениям полного разрешения, мы получаем многие преимущества маленьких изображений (меньше памяти, меньше слоёв в U-Net, быстрее генерация…) и всё равно можем декодировать результат обратно в изображение высокого разрешения, когда захотим посмотреть на итог. Это нововведение резко снижает стоимость обучения и запуска таких моделей.

(Пример с числами от переводчика: картинка 512×512×3 — это 786 432 числа, латент 4×64×64 — 16 384, в 48 раз меньше. Внимание на карте 64×64 сравнивает 4096 позиций друг с другом — около 17 млн пар, а на 512×512 было бы 262 144 позиции и почти 69 млрд пар.)

В юните 2 мы показали, что дополнительная информация, поданная в U-Net, даёт дополнительный контроль над типом генерируемых изображений. Мы называем это условием. По зашумлённой версии изображения модель должна предсказать очищенную версию, опираясь на дополнительные подсказки — например, метку класса или, в случае Stable Diffusion, текстовое описание изображения. При инференсе мы подаём описание изображения, которое хотим увидеть, и чистый шум как стартовую точку, а модель изо всех сил «очищает» случайный вход до чего-то, что соответствует подписи.

(В оригинале — схема кодирования текста: промпт превращается токенизатором в токены, текстовый кодировщик выдаёт набор текстовых эмбеддингов (encoder_hidden_states), которые затем подаются в U-Net как условие.)

Чтобы это работало, нужно числовое представление текста, которое отражает важную информацию о том, что он описывает. Для этого SD использует предобученный трансформер на основе CLIP. Текстовый кодировщик CLIP создавался, чтобы превращать подписи к изображениям в форму, в которой изображения и тексты можно сравнивать, поэтому он хорошо подходит для получения полезных представлений описаний изображений. Входной промпт сначала токенизируется (по большому словарю, где каждому слову или части слова сопоставлен свой токен), а затем проходит через текстовый кодировщик CLIP, который выдаёт 768-мерный (в SD 1.X) или 1024-мерный (в SD 2.X) вектор для каждого токена. Для единообразия промпты всегда дополняются или обрезаются до длины 77 токенов, так что итоговое представление, которое мы используем как условие, — тензор формы 77×1024 на промпт.

(В оригинале — цветная схема U-Net Stable Diffusion: на каждом уровне блоки ResNet получают эмбеддинг шага, а блоки перекрёстного внимания — текстовые эмбеддинги.)

Хорошо, а как именно подать эту информацию об условии в U-Net, чтобы та использовала её в предсказаниях? Ответ — перекрёстное внимание (cross-attention). Слои перекрёстного внимания разбросаны по всей U-Net. Каждая позиция в U-Net может «обращать внимание» на разные токены текстового условия и подтягивать нужную информацию из промпта. Схема выше показывает, как это текстовое условие (а также условие на шаг) подаётся в разных местах. Как видите, на каждом уровне у U-Net есть масса возможностей воспользоваться условием!

flowchart LR
P["Промпт"] --> T["Токенизатор CLIP<br/>77 токенов"] --> TE["Текстовый кодировщик<br/>77×1024"]
N["Шум 4×64×64"] --> U["U-Net<br/>+ cross-attention<br/>+ эмбеддинг шага t"]
TE --> U
U --> S["Планировщик:<br/>шаг x_t → x_(t−1)"] --> U
S --> D["Декодер VAE"] --> I["Картинка 512×512"]
Конвейер Stable Diffusion при генерации (рисунок переводчика)

Оказывается, даже при всех усилиях сделать текстовое условие как можно полезнее модель всё равно склонна в своих предсказаниях полагаться в основном на зашумлённое входное изображение, а не на промпт. В каком-то смысле это логично: многие подписи лишь отдалённо связаны со своими изображениями, и модель учится не слишком доверять описаниям! Но при генерации новых изображений это нежелательно: если модель не следует промпту, мы можем получить изображения, никак не связанные с нашим описанием.

(В оригинале — изображения по промпту «An oil painting of a collie in a top hat» («Картина маслом: колли в цилиндре») при масштабе CFG 0, 1, 2 и 10 слева направо: при 0 и 1 — что-то невнятное, при 10 — чёткая собака в шляпе.)

Чтобы это исправить, используют приём под названием classifier-free guidance (CFG, «направление без классификатора»; в оригинале опечатка — CGF). При обучении текстовое условие иногда оставляют пустым, заставляя модель учиться очищать изображения вообще без текстовой информации (безусловная генерация). Затем при инференсе делают два отдельных предсказания: одно с текстовым промптом в качестве условия и одно без него. Разность этих двух предсказаний используют, чтобы получить итоговое комбинированное предсказание, которое сдвигается ещё дальше в направлении, указанном предсказанием с текстом, на некоторый множитель (масштаб guidance), — в надежде получить изображение, лучше соответствующее промпту. Картинка выше показывает результаты по одному промпту при разных масштабах guidance: как видите, чем больше значение, тем лучше изображение соответствует описанию.

(Формула, которая стоит за этим: ε^=ε^∅+w (ε^текст−ε^∅)\hat\varepsilon = \hat\varepsilon_{\varnothing} + w\,(\hat\varepsilon_{\text{текст}} - \hat\varepsilon_{\varnothing}), где ww — масштаб guidance. При w=1w = 1 это просто предсказание с текстом, при w=7.5w = 7.5 — «в 7,5 раза больше того, что добавляет текст». Вывод — в уроке 9.1.2.)

Другие виды условий: сверхразрешение, inpainting и depth-to-image

Заголовок раздела «Другие виды условий: сверхразрешение, inpainting и depth-to-image»

Можно создавать версии Stable Diffusion, которые принимают дополнительные виды условий. Например, у модели Depth-to-Image есть дополнительные входные каналы с информацией о глубине очищаемого изображения, и при инференсе мы можем подать карту глубины целевого изображения (оценённую отдельной моделью), чтобы, надеемся, получить изображение с похожей общей структурой.

(В оригинале — пример StabilityAI: одно исходное фото и несколько сгенерированных по его карте глубины изображений с той же композицией, но разным содержанием.)

Похожим образом можно подать в качестве условия изображение низкого разрешения, и модель сгенерирует версию высокого разрешения (так работает Stable Diffusion Upscaler). Наконец, можно подать маску, отмечающую область изображения, которую нужно сгенерировать заново, — это задача inpainting («дорисовки»): области вне маски должны остаться нетронутыми, а для области под маской генерируется новое содержание.

(В оригинале — иллюстрация со страницы проекта DreamBooth на модели Imagen: по нескольким фотографиям конкретной собаки модель генерирует её в новых сценах.)

DreamBooth — техника дообучения модели «текст → изображение», которая «учит» её новому понятию, например конкретному предмету или стилю. Изначально техника была разработана для модели Imagen от Google, но её быстро адаптировали для Stable Diffusion. Результаты бывают очень впечатляющими (если недавно вы видели у кого-то в соцсетях аватар, сделанный ИИ, скорее всего, он сделан сервисом на основе DreamBooth), но техника чувствительна к настройкам, поэтому загляните в наш ноутбук и в этот отличный разбор разных параметров обучения, чтобы найти советы, как добиться лучшего результата.

ГлаваГде запустить
Stable Diffusion IntroductionColab, Kaggle
DreamBooth Hackathon NotebookColab
Stable Diffusion Deep DiveColab

Теперь вы знаете достаточно, чтобы взяться за ноутбуки! Откройте их на удобной платформе по ссылкам выше. DreamBooth требует много вычислений, поэтому в Kaggle или Google Colab обязательно выберите тип среды «GPU».

Ноутбук «Stable Diffusion Introduction» — короткое введение в Stable Diffusion с библиотекой 🤗 Diffusers: несколько базовых примеров использования конвейеров для генерации и изменения изображений.

В ноутбуке DreamBooth Hackathon (в папке hackathon) мы показываем, как дообучить SD на своих изображениях, чтобы получить свою версию модели с новым стилем или понятием.

Наконец, ноутбук и видео «Stable Diffusion Deep Dive» разбирают каждый шаг типичного конвейера генерации и предлагают новые способы менять каждый этап для дополнительного творческого контроля.

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

Нашли ещё отличные материалы? Сообщите нам, и мы добавим их в список.

Этот ноутбук разбирает основы того, как с помощью Stable Diffusion создавать и изменять изображения готовыми конвейерами. Мы также кратко посмотрим на ключевые компоненты конвейера, а их подробное изучение оставим ноутбуку deep dive. А именно, мы разберём:

  • генерацию изображений по тексту с StableDiffusionPipeline и эксперименты с доступными аргументами;
  • ключевые компоненты конвейера в действии:
    • VAE, который делает её «латентной диффузионной моделью»;
    • токенизатор и текстовый кодировщик, которые обрабатывают промпт;
    • саму U-Net;
    • планировщик и разные планировщики;
  • воспроизведение цикла сэмплирования из компонентов конвейера;
  • редактирование существующих изображений конвейером Img2Img;
  • конвейеры inpainting и Depth2Img.

❓ Если появятся вопросы, пишите в канал #diffusion-models-class на Discord-сервере Hugging Face. Если ещё не зарегистрировались, это можно сделать здесь: https://huggingface.co/join/discord

%pip install -Uq diffusers ftfy accelerate
# Пока ставим transformers из исходников: для Depth2Img нужна самая свежая версия
%pip install -Uq git+https://github.com/huggingface/transformers

(Сейчас достаточно обычного pip install -U transformers — нужные возможности давно в релизах.)

import torch
import requests
from PIL import Image
from io import BytesIO
from matplotlib import pyplot as plt
# Сегодня мы разберём сразу несколько конвейеров!
from diffusers import (
StableDiffusionPipeline,
StableDiffusionImg2ImgPipeline,
StableDiffusionInpaintPipeline,
StableDiffusionDepth2ImgPipeline
)
# Пара демонстрационных изображений понадобится дальше в ноутбуке
def download_image(url):
response = requests.get(url)
return Image.open(BytesIO(response.content)).convert("RGB")
# Скачиваем изображения для примера inpainting
img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png"
mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png"
img_height = 512
img_width = 512
init_image = download_image(img_url).resize((img_height, img_width))
mask_image = download_image(mask_url).resize((img_height, img_width))

(Демонстрационное изображение — собака на скамейке в парке, маска — белый силуэт собаки на чёрном фоне.)

# Выбираем устройство
device = (
"mps"
if torch.backends.mps.is_available()
else "cuda"
if torch.cuda.is_available()
else "cpu"
)

Загрузим конвейер Stable Diffusion и посмотрим, что он умеет. Есть несколько версий Stable Diffusion; на момент написания последняя — 2.1. Если хотите попробовать более старую версию, просто замените идентификатор модели на нужный (например, попробуйте "CompVis/stable-diffusion-v1-4" или выберите модель из библиотеки понятий DreamBooth).

# Загружаем конвейер
model_id = "stabilityai/stable-diffusion-2-1-base"
pipe = StableDiffusionPipeline.from_pretrained(model_id).to(device)

Если не хватает памяти GPU, можно кое-что сделать, чтобы снизить её расход:

  • Загрузить версию FP16 (поддерживается не на всех системах). Тогда, возможно, придётся переводить тензоры в torch.float16, экспериментируя с отдельными компонентами конвейера:

    pipe = StableDiffusionPipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16).to(device)

  • Включить attention slicing («нарезку внимания»). Это снижает расход памяти GPU ценой небольшого замедления:

    pipe.enable_attention_slicing()

  • Уменьшить размер генерируемых изображений.

(Примечание переводчика: аргумент revision="fp16" устарел — пишите variant="fp16", torch_dtype=torch.float16. Если модель Stability AI недоступна на Hub (некоторые старые репозитории закрыты или перенесены), возьмите другую модель SD — но учтите, что у SD 1.x текстовые эмбеддинги 768-мерные, а не 1024-мерные, как в примерах ниже.)

Когда конвейер загружен, сгенерировать изображение по промпту можно таким кодом:

# Генератор случайных чисел для воспроизводимости
generator = torch.Generator(device=device).manual_seed(42)
# Запускаем конвейер, показывая часть доступных аргументов
pipe_output = pipe(
prompt="Palette knife painting of an autumn cityscape", # что генерировать
negative_prompt="Oversaturated, blurry, low quality", # чего НЕ генерировать
height=480, width=640, # размер изображения
guidance_scale=8, # насколько сильно следовать промпту
num_inference_steps=35, # сколько шагов делать
generator=generator # фиксированное начальное значение генератора
)
# Смотрим на результат
pipe_output.images[0]

(Промпт: «Осенний городской пейзаж, написанный мастихином». Негативный промпт: «Перенасыщенно, размыто, низкое качество».)

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

Главные аргументы, которые стоит менять:

  • Ширина и высота задают размер генерируемого изображения. Они должны делиться на 8, чтобы работал VAE (это мы увидим в одном из следующих разделов).
  • Число шагов влияет на качество генерации. Значение по умолчанию (50) работает хорошо, но иногда можно обойтись и 20 шагами, что удобно для экспериментов.
  • Негативный промпт используется в процессе classifier-free guidance и может быть полезным способом дополнительного контроля. Его можно не указывать, но многие считают полезным перечислять в нём нежелательные описания, как в примере выше.
  • Аргумент guidance_scale задаёт силу classifier-free guidance (CFG). Большие значения заставляют изображения лучше соответствовать промпту, но при слишком больших значениях результат может стать перенасыщенным и неприятным.

(Как работает негативный промпт: в формуле CFG вместо «пустого» предсказания ε^∅\hat\varepsilon_{\varnothing} подставляется предсказание с негативным промптом. Тогда генерация сдвигается не просто к промпту, а к промпту и прочь от негативного.)

Если ищете вдохновения для промптов, начните со Stable Diffusion Prompt Book.

Эффект увеличения масштаба guidance виден в следующей ячейке:

#@markdown сравниваем масштабы guidance:
cfg_scales = [1.1, 8, 12] #@param
prompt = "A collie with a pink hat" #@param
fig, axs = plt.subplots(1, len(cfg_scales), figsize=(16, 5))
for i, ax in enumerate(axs):
im = pipe(prompt, height=480, width=480,
guidance_scale=cfg_scales[i], num_inference_steps=35,
generator=torch.Generator(device=device).manual_seed(42)).images[0]
ax.imshow(im); ax.set_title(f'CFG Scale {cfg_scales[i]}');

(Промпт: «Колли в розовой шляпе».)

Поменяйте значения выше, чтобы попробовать другие масштабы и промпты. Оценка, конечно, субъективна, но на мой взгляд всё в диапазоне 8–12 даёт лучшие результаты, чем значения ниже или выше.

StableDiffusionPipeline, которым мы пользуемся, чуть сложнее DDPMPipeline из прошлых юнитов. Кроме U-Net и планировщика, в конвейер входит ещё несколько компонентов:

print(list(pipe.components.keys())) # список компонентов

Вывод:

['vae', 'text_encoder', 'tokenizer', 'unet', 'scheduler', 'safety_checker', 'feature_extractor', 'image_encoder']

(safety_checker и feature_extractor — фильтр нежелательного содержимого и его предобработчик; у SD 2.1 их нет, они остаются пустыми. image_encoder нужен для вариантов с условием на изображение.)

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

(В оригинале — схема VAE: изображение 3×512×512 проходит через кодировщик и превращается в латент 4×64×64, декодер восстанавливает из него изображение 3×512×512.)

VAE (вариационный автокодировщик) — модель, которая умеет кодировать вход в сжатое представление, а затем декодировать это «латентное» представление обратно во что-то близкое к исходному входу. Генерируя изображения Stable Diffusion, мы сначала генерируем латенты, применяя процесс диффузии в «латентном пространстве» VAE, а в конце декодируем их, чтобы увидеть итоговое изображение.

Вот код, который берёт входное изображение, кодирует его в латентное представление и снова декодирует с помощью VAE:

# Создаём фиктивные данные (случайное изображение в диапазоне (-1, 1))
images = torch.rand(1, 3, 512, 512).to(device) * 2 - 1
print("Input images shape:", images.shape)
# Кодируем в латентное пространство
with torch.no_grad():
latents = 0.18215 * pipe.vae.encode(images).latent_dist.mean
print("Encoded latents shape:", latents.shape)
# Снова декодируем
with torch.no_grad():
decoded_images = pipe.vae.decode(latents / 0.18215).sample
print("Decoded images shape:", decoded_images.shape)

Вывод:

Input images shape: torch.Size([1, 3, 512, 512])
Encoded latents shape: torch.Size([1, 4, 64, 64])
Decoded images shape: torch.Size([1, 3, 512, 512])

Как видите, изображение 512×512 сжато до латентного представления 64×64 (с четырьмя каналами). Это восьмикратное уменьшение по каждому пространственному измерению — причина, по которой ширина и высота должны быть кратны 8.

Работать с этими насыщенными информацией латентами 4×64×64 эффективнее, чем с огромными изображениями 512 пикселей: диффузионные модели работают быстрее и требуют меньше ресурсов для обучения и использования. Декодирование VAE неидеально, но достаточно хорошее, так что небольшая потеря качества обычно того стоит.

NB: в примере кода выше есть масштабный множитель 0,18215 — он нужен, чтобы совпасть с обработкой, которая использовалась при обучении SD.

(Зачем множитель: латенты VAE имеют разброс заметно больше единицы, а диффузия ожидает данные с дисперсией около 1 — такой же, как у шума. Авторы посчитали стандартное отклонение латентов на обучающих данных и делят на него; 0.18215≈1/5.490.18215 \approx 1/5.49. В коде лучше брать его из конфигурации: pipe.vae.config.scaling_factor. latent_dist.mean — среднее распределения VAE; при кодировании можно брать и случайный сэмпл latent_dist.sample(), как ниже в img2img.)

(В оригинале — схема: промпт → токенизатор → id токенов → текстовый кодировщик → эмбеддинги 77×1024.)

Задача текстового кодировщика — превратить входную строку (промпт) в числовое представление, которое можно подать в U-Net как условие. Сначала токенизатор конвейера превращает текст в последовательность токенов. Словарь текстового кодировщика — около 50 тысяч токенов; любое слово, которого нет в словаре, разбивается на части поменьше. Затем токены проходят через сам текстовый кодировщик — трансформер, исходно обученный как текстовый кодировщик CLIP. Надежда в том, что этот предобученный трансформер выучил богатые представления текста, полезные и для задачи диффузии.

Проверим этот процесс на примере промпта: сначала вручную токенизируем его и пропустим через текстовый кодировщик, а потом воспользуемся методом конвейера encode_prompt, чтобы показать весь процесс, включая дополнение/обрезку до максимальной длины 77 токенов:

# Токенизируем и кодируем пример промпта вручную
# Токенизация
input_ids = pipe.tokenizer(["A painting of a flooble"])['input_ids']
print("Input ID -> decoded token")
for input_id in input_ids[0]:
print(f"{input_id} -> {pipe.tokenizer.decode(input_id)}")
# Пропускаем через текстовый кодировщик CLIP
input_ids = torch.tensor(input_ids).to(device)
with torch.no_grad():
text_embeddings = pipe.text_encoder(input_ids)['last_hidden_state']
print("Text embeddings shape:", text_embeddings.shape)

Вывод:

Input ID -> decoded token
49406 -> <|startoftext|>
320 -> a
3086 -> painting
539 -> of
320 -> a
4062 -> floo
1059 -> ble
49407 -> <|endoftext|>
Text embeddings shape: torch.Size([1, 8, 1024])

(«flooble» — выдуманное слово, его нет в словаре, поэтому токенизатор разбил его на «floo» + «ble». Так работает BPE, разобранный в уроке 4.2.2. Токенизатор CLIP почти не знает кириллицы — русские промпты в SD 1.x/2.x работают плохо.)

# Получаем итоговые текстовые эмбеддинги функцией конвейера encode_prompt
text_embeddings = pipe._encode_prompt("A painting of a flooble", device, 1, True, '')
text_embeddings.shape

Вывод:

torch.Size([2, 77, 1024])

(Почему 2: при do_classifier_free_guidance=True (четвёртый аргумент) функция возвращает эмбеддинги и для пустого (или негативного) промпта, и для нашего — склеенные в один батч. Примечание переводчика: _encode_prompt устарел; в новых версиях diffusers вызывайте prompt_embeds, negative_embeds = pipe.encode_prompt("…", device, 1, True, "") и склеивайте сами: text_embeddings = torch.cat([negative_embeds, prompt_embeds]) — именно в таком порядке, сначала безусловные.)

Эти текстовые эмбеддинги (так называемые «скрытые состояния» (hidden states) последнего блока трансформера в текстовом кодировщике) будут подаваться в U-Net дополнительным аргументом метода forward — это мы увидим в следующем разделе.

(В оригинале — схема U-Net Stable Diffusion: на вход латент, шаг tt и текстовые эмбеддинги, на выход — предсказанный шум той же формы, что латент.)

U-Net принимает зашумлённый вход и предсказывает шум — так же, как U-Net из прошлых юнитов. В отличие от тех примеров, вход здесь не изображение, а его латентное представление. И кроме условия на шаг, эта U-Net принимает дополнительным входом текстовые эмбеддинги промпта. Вот она делает предсказания на фиктивных данных:

# Фиктивные входы
timestep = pipe.scheduler.timesteps[0]
latents = torch.randn(1, 4, 64, 64).to(device)
text_embeddings = torch.randn(1, 77, 1024).to(device)
# Предсказание модели
with torch.no_grad():
unet_output = pipe.unet(latents, timestep, text_embeddings).sample
print('UNet output shape:', unet_output.shape) # та же форма, что у входных латентов

Вывод:

UNet output shape: torch.Size([1, 4, 64, 64])

Планировщик хранит расписание шума и управляет обновлением зашумлённого сэмпла по предсказаниям модели. Планировщик по умолчанию — PNDMScheduler, но можно использовать и другие (например, LMSDiscreteScheduler), если инициализировать их той же конфигурацией.

Построим график расписания шума, чтобы увидеть уровень шума (по αˉ\bar{\alpha}) во времени:

plt.plot(pipe.scheduler.alphas_cumprod, label=r'$\bar{\alpha}$')
plt.xlabel('Timestep (high noise to low noise ->)');
plt.title('Noise schedule');plt.legend();

(В оригинале — убывающая кривая αˉt\bar\alpha_t от 1 до почти 0 по 1000 шагам. Подпись оси в оригинале перепутана: слева направо шум растёт — t=0t = 0 соответствует чистой картинке.)

Если хотите попробовать другой планировщик, подставьте его так:

from diffusers import LMSDiscreteScheduler
# Заменяем планировщик
pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config)
# Печатаем конфигурацию
print('Scheduler config:', pipe.scheduler)
# Генерируем изображение с новым планировщиком
pipe(prompt="Palette knife painting of an winter cityscape", height=480, width=480,
generator=torch.Generator(device=device).manual_seed(42)).images[0]

Вывод (начало):

Scheduler config: LMSDiscreteScheduler {
"_class_name": "LMSDiscreteScheduler",
"_diffusers_version": "0.30.3",
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"clip_sample": false,
"num_train_timesteps": 1000,
...

Подробнее об использовании разных планировщиков — здесь.

Посмотрев на все компоненты в действии, соберём их вместе и воспроизведём работу конвейера:

guidance_scale = 8 #@param
num_inference_steps = 30 #@param
prompt = "Beautiful picture of a wave breaking" #@param
negative_prompt = "zoomed in, blurry, oversaturated, warped" #@param
# Кодируем промпт
text_embeddings = pipe._encode_prompt(prompt, device, 1, True, negative_prompt)
# Создаём случайную стартовую точку
latents = torch.randn((1, 4, 64, 64), device=device, generator=generator)
latents *= pipe.scheduler.init_noise_sigma
# Готовим планировщик
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Проходим по шагам сэмплирования
for i, t in enumerate(pipe.scheduler.timesteps):
# Удваиваем латенты, раз используем classifier-free guidance
latent_model_input = torch.cat([latents] * 2)
# Применяем масштабирование, которое требует планировщик
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# Предсказываем шум U-Net
with torch.no_grad():
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# Применяем guidance
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# Вычисляем предыдущий зашумлённый сэмпл x_t -> x_t-1
latents = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# Декодируем итоговые латенты в изображение
with torch.no_grad():
image = pipe.decode_latents(latents.detach())
# Смотрим
pipe.numpy_to_pil(image)[0]

(Промпт: «Красивая картина: разбивающаяся волна», негативный: «крупный план, размыто, перенасыщенно, искажено». init_noise_sigma — во сколько раз планировщик ждёт начальный шум больше стандартного: у DDPM/DDIM это 1, у LMS и других «сигма-планировщиков» — около 14,6. Примечание переводчика: decode_latents в новых версиях удалён — пишите image = pipe.vae.decode(latents / pipe.vae.config.scaling_factor).sample и pipe.image_processor.postprocess(image)[0].)

В большинстве случаев проще пользоваться готовыми конвейерами, но такой «разбираемый» цикл сэмплирования полезен, чтобы понимать и менять работу каждого компонента. Если хотите увидеть этот код и подробное изучение и изменение всех компонентов, посмотрите ноутбук и видео «Stable Diffusion Deep Dive».

Что ещё можно делать, кроме генерации изображений по промпту? Очень многое! В этом разделе мы покажем несколько интересных конвейеров, чтобы дать представление о других задачах, для которых подходит Stable Diffusion. Для некоторых нужно скачать новые модели, так что, если торопитесь, можно просто просмотреть раздел и готовые результаты, не скачивая и не запуская все модели.

В примерах до сих пор мы генерировали изображения полностью с нуля: начинали со случайных латентов и применяли полный цикл диффузионного сэмплирования. Но начинать с нуля не обязательно. Конвейер Img2Img сначала кодирует существующее изображение в латенты, затем добавляет к ним шум и использует результат как стартовую точку. Количество добавленного шума и число шагов очистки определяют «силу» (strength) процесса img2img. Если добавить совсем немного шума (малая сила), изменений будет очень мало, а если добавить максимум шума и пройти полный процесс очистки, получится изображение, которое почти не похоже на вход, кроме некоторого сходства в общей структуре.

Чтобы лучше понять процесс img2img, реализуем конвейер вручную. Это поможет разобраться, как кодируются латенты, добавляется шум и применяется диффузия. Реализовав конвейер вручную, посмотрим, как библиотека diffusers даёт готовый StableDiffusionImg2ImgPipeline для простоты и эффективности.

Вот как реализовать конвейер img2img вручную.

import numpy as np
# Кодируем init_image
init_image_tensor = torch.from_numpy(np.array(init_image).transpose(2, 0, 1)).float() / 255.0 # 0~255 => 0~1
init_image_tensor = 2.0 * init_image_tensor - 1.0 # 0~1 => -1~1
init_image_tensor = init_image_tensor.unsqueeze(0).to(device) # добавляем измерение батча
with torch.no_grad():
init_image_latents = pipe.vae.encode(init_image_tensor).latent_dist.sample() * pipe.vae.config.scaling_factor
guidance_scale = 7.5 #@param
num_inference_steps = 30 #@param
strength = 0.6
prompt = "An oil painting of a man on a bench" #@param
# Кодируем промпт
text_embeddings = pipe._encode_prompt(prompt, device, 1, True, '')
# Готовим планировщик
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Готовим латентные переменные
# Мы используем не все шаги планировщика:
# по `strength` вычисляем, какую часть шагов применить к исходному изображению.
init_timestep = min(int(num_inference_steps * strength), num_inference_steps)
t_start = max(num_inference_steps - init_timestep, 0)
timesteps = pipe.scheduler.timesteps[t_start:]
# Первый из новых шагов — точка, до которой зашумляем исходное изображение.
latent_timestep = timesteps[:1]
# Добавляем к init_image_latents шум уровня latent_timestep.
noise = torch.randn((1, 4, 64, 64), device=device,
generator=torch.Generator(device=device).manual_seed(42))
latents = pipe.scheduler.add_noise(init_image_latents, noise, latent_timestep)
# Проходим по шагам сэмплирования
for i, t in enumerate(timesteps):
# Удваиваем латенты, раз используем classifier-free guidance
latent_model_input = torch.cat([latents] * 2)
# Применяем масштабирование, которое требует планировщик
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# Предсказываем шум U-Net
with torch.no_grad():
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# Применяем guidance
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# Вычисляем предыдущий зашумлённый сэмпл x_t -> x_t-1
latents = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# Декодируем латенты
latents_norm = latents / pipe.vae.config.scaling_factor
with torch.no_grad():
result_image = pipe.vae.decode(latents_norm).sample
result_image = (result_image / 2 + 0.5).clamp(0, 1).squeeze()
result_image = (result_image.permute(1, 2, 0) * 255).to(torch.uint8).cpu().numpy()
result_image = Image.fromarray(result_image)
# Смотрим результат
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(result_image);axs[1].set_title('Result');

(Пример с числами: при 30 шагах и strength=0.6 выполняются последние ⌊30⋅0.6⌋=18\lfloor 30 \cdot 0.6 \rfloor = 18 шагов из 30. Картинка зашумляется до уровня 13-го шага расписания и очищается оттуда; верхние, самые шумные 12 шагов пропускаются. Промпт: «Картина маслом: мужчина на скамейке».)

Реализовав img2img вручную, посмотрим, как получить тот же результат проще с StableDiffusionImg2ImgPipeline из библиотеки diffusers.

Этому конвейеру не нужны особые модели: раз идентификатор модели тот же, что в примере «текст → изображение», новые файлы скачивать не придётся.

# Загружаем конвейер Img2Img
model_id = "stabilityai/stable-diffusion-2-1-base"
img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id).to(device)

В разделе «Подготовка» мы загрузили пример init_image для этой демонстрации, но можно заменить его своим изображением. Вот конвейер в действии:

# Применяем Img2Img
result_image = img2img_pipe(
prompt="An oil painting of a man on a bench",
image=init_image, # стартовое изображение
strength=0.6, # 0 — без изменений, 1.0 — максимальная сила
).images[0]
# Смотрим результат
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(result_image);axs[1].set_title('Result');

(Вместо загрузки модели второй раз можно собрать конвейер из уже загруженных компонентов: StableDiffusionImg2ImgPipeline(**pipe.components).)

Упражнение: поэкспериментируйте с этим конвейером. Попробуйте свои изображения или поиграйте с силой и промптами. Многие аргументы те же, что у конвейера «текст → изображение», так что пробуйте разные размеры, число шагов и т. д.

А если мы хотим оставить часть входного изображения без изменений, а в других частях сгенерировать что-то новое? Это называется inpainting («дорисовка»). Это можно сделать той же моделью, что в прошлых демонстрациях (через StableDiffusionInpaintPipelineLegacy), но лучшие результаты даёт специально дообученная версия Stable Diffusion, которая принимает замаскированное изображение и саму маску как дополнительное условие. Изображение маски должно быть той же формы, что и входное изображение: белым — области, которые надо заменить, чёрным — области, которые надо оставить без изменений.

Чтобы глубже понять процесс inpainting, реализуем вручную логику StableDiffusionInpaintPipelineLegacy. Так станет понятнее, как inpainting работает на низком уровне и как Stable Diffusion обрабатывает входы. Затем посмотрим на дообученный конвейер для сравнения. Вот как реализовать конвейер inpainting вручную и применить его к примеру изображения и маски, загруженным в разделе «Подготовка».

(В оригинале — схема ручного inpainting: на каждом шаге область под маской берётся из генерируемых латентов, а область вне маски — из латентов исходного изображения, зашумлённых до уровня текущего шага.)

# Уменьшаем маску до размера латентов
mask_image_latent_size = mask_image.resize((64,64))
mask_image_latent_size = torch.tensor( (np.array(mask_image_latent_size)[...,0] > 5).astype(np.float32) )
plt.imshow(mask_image_latent_size.numpy(), cmap='gray')
mask_image_latent_size = mask_image_latent_size.to(device)
mask_image_latent_size.shape

Вывод:

torch.Size([64, 64])

Снова пишем цикл очистки от шума.

guidance_scale = 8 #@param
num_inference_steps = 30 #@param
prompt = "A small robot, high resolution, sitting on a park bench"
negative_prompt = "zoomed in, blurry, oversaturated, warped"
generator = torch.Generator(device=device).manual_seed(42)
# Кодируем промпт
text_embeddings = pipe._encode_prompt(prompt, device, 1, True, negative_prompt)
# Создаём случайную стартовую точку
latents = torch.randn((1, 4, 64, 64), device=device, generator=generator)
latents *= pipe.scheduler.init_noise_sigma
# Готовим планировщик
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
for i, t in enumerate(pipe.scheduler.timesteps):
# Удваиваем латенты, раз используем classifier-free guidance
latent_model_input = torch.cat([latents] * 2)
# Применяем масштабирование, которое требует планировщик
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# Предсказываем шум U-Net
with torch.no_grad():
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# Применяем guidance
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# Вычисляем предыдущий зашумлённый сэмпл x_t -> x_t-1
latents = pipe.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
# Делаем inpainting: заполняем область под маской
if i < len(pipe.scheduler.timesteps)-1:
# Зашумляем латенты исходного изображения до предыдущего шага t-1
noise = torch.randn(init_image_latents.shape, generator=generator, device=device, dtype=torch.float32)
background = pipe.scheduler.add_noise(init_image_latents, noise, torch.tensor([pipe.scheduler.timesteps[i+1]]))
latents = latents*mask_image_latent_size # белое — области под маской
background = background * (1-mask_image_latent_size) # чёрное — остальные области
# Объединяем сгенерированные и исходные латенты по маске
latents += background
# Декодируем латенты
latents_norm = latents / pipe.vae.config.scaling_factor
with torch.no_grad():
inpainted_image = pipe.vae.decode(latents_norm).sample
inpainted_image = (inpainted_image / 2 + 0.5).clamp(0, 1).squeeze()
inpainted_image = (inpainted_image.permute(1, 2, 0) * 255).to(torch.uint8).cpu().numpy()
inpainted_image = Image.fromarray(inpainted_image)
inpainted_image

(Промпт: «Маленький робот, высокое разрешение, сидит на скамейке в парке». Почему фон зашумляется до следующего шага: после step() латенты уже соответствуют шагу ti+1t_{i+1}, и фон должен быть на том же уровне шума, иначе граница маски будет выделяться. На последнем шаге фон не подмешивается, поэтому область вне маски слегка меняется — это видно по результату.)

Реализовав логику inpainting вручную, посмотрим, как пользоваться дообученным конвейером, созданным специально для задач inpainting. Вот как загрузить такой конвейер и применить его к примеру изображения и маски из раздела «Подготовка»:

(В оригинале — результат: исходное фото с собакой, маска и картинка, где на месте собаки сидит робот.)

# Загружаем конвейер inpainting (нужна подходящая модель inpainting)
# pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting")
# "runwayml/stable-diffusion-inpainting" больше недоступна.
# Поэтому используем модель "stabilityai/stable-diffusion-2-inpainting".
pipe = StableDiffusionInpaintPipeline.from_pretrained("stabilityai/stable-diffusion-2-inpainting")
pipe = pipe.to(device)
# Inpainting с промптом, описывающим, как должен выглядеть результат
prompt = "A small robot, high resolution, sitting on a park bench"
image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0]
# Смотрим результат
fig, axs = plt.subplots(1, 3, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(mask_image);axs[1].set_title('Mask')
axs[2].imshow(image);axs[2].set_title('Result');

Это особенно мощно в сочетании с другой моделью, которая автоматически строит маски. Например, этот демо-Space с помощью модели CLIPSeg маскирует объект, который нужно заменить, по текстовому описанию.

Изучение разных конвейеров и вариантов моделей может забить диск. Посмотреть, какие модели сейчас скачаны, можно так:

!ls ~/.cache/huggingface/hub/ # содержимое папки кэша

Вывод:

models--CompVis--stable-diffusion-v1-4
models--ddpm-bedroom-256
models--google--ddpm-bedroom-256
models--google--ddpm-celebahq-256
models--runwayml--stable-diffusion-inpainting
models--stabilityai--stable-diffusion-2-1-base

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

(Удобная команда: hf cache scan показывает размер каждой модели в кэше, а hf cache delete — удаляет выбранные. Каждая модель SD занимает от 2 до 5 ГБ.)

(В оригинале — примеры StabilityAI: входное изображение, его карта глубины и несколько сгенерированных вариантов.)

Img2Img отлично работает, но иногда хочется создать новое изображение с композицией исходного, но совсем другими цветами или текстурами. Бывает трудно подобрать силу Img2Img, которая сохраняет нужное в компоновке и при этом не оставляет исходных цветов.

Пора для ещё одной дообученной модели! Эта принимает при генерации информацию о глубине как дополнительное условие. Конвейер строит карту глубины моделью оценки глубины, а затем подаёт её в дообученную U-Net при генерации изображений, чтобы (надеемся) сохранить глубину и структуру исходного изображения, заполнив его совершенно новым содержанием.

# Загружаем конвейер Depth2Img (нужна подходящая модель)
pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth")
pipe = pipe.to(device)
# Генерация с промптом, описывающим, как должен выглядеть результат
prompt = "An oil painting of a man on a bench"
image = pipe(prompt=prompt, image=init_image).images[0]
# Смотрим результат
fig, axs = plt.subplots(1, 2, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(image);axs[1].set_title('Result');

Сравните результат с примером img2img: здесь цвета меняются гораздо сильнее, но общая структура по-прежнему верна оригиналу. В этом случае это не идеально — мужчине досталась очень странная анатомия, чтобы совпасть с формой собаки, — но в некоторых случаях это невероятно полезно. Пример «убойного применения» этого подхода — в этом твите: модель глубины используется, чтобы текстурировать 3D-сцену!

(Сегодня ту же задачу обычно решают через ControlNet — отдельную сеть-адаптер, которая подаёт в U-Net карту глубины, контуры или позу и работает с любой базовой моделью без её дообучения.)

Надеемся, это дало вам почувствовать, как много умеет Stable Diffusion! Когда наиграетесь с примерами этого ноутбука, откройте ноутбук DreamBooth hackathon и посмотрите, как дообучить свою версию Stable Diffusion, которую можно использовать с конвейерами «текст → изображение» или img2img, показанными здесь.

Если интересно глубже разобраться в работе разных компонентов, откройте ноутбук Stable Diffusion Deep Dive — там гораздо больше подробностей и показаны дополнительные трюки.

Обязательно делитесь своими работами с нами и сообществом!

EnglishРусский
latent diffusion, latent spaceлатентная диффузия, латентное пространство
variational autoencoder (VAE)вариационный автокодировщик
text encoder, tokenizerтекстовый кодировщик, токенизатор
hidden states, encoder_hidden_statesскрытые состояния (эмбеддинги текста для U-Net)
cross-attentionперекрёстное внимание
classifier-free guidance (CFG), guidance scaleguidance без классификатора, масштаб guidance
negative promptнегативный промпт
img2img, strength«картинка → картинка», сила изменения
inpainting, maskдорисовка (inpainting), маска
depth map, depth2imgкарта глубины, «глубина → картинка»
super-resolution, upscalerсверхразрешение, апскейлер
attention slicingнарезка внимания (экономия памяти)
DreamBoothDreamBooth — дообучение на новое понятие по нескольким фото