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: Stable Diffusion
Заголовок раздела «Юнит 3: Stable Diffusion»Добро пожаловать в юнит 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²) больше памяти и вычислений. Это проблема для всех, кто хочет генерировать изображения высокого разрешения!
(В оригинале — схема из статьи о латентной диффузии: слева пиксельное пространство с кодировщиком и декодером , в центре — диффузия в латентном пространстве с U-Net и перекрёстным вниманием, справа — условия (текст, семантическая карта, изображения) и их кодировщик .)
Латентная диффузия смягчает эту проблему с помощью отдельной модели — вариационного автокодировщика (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"]
Classifier-free guidance
Заголовок раздела «Classifier-free guidance»Оказывается, даже при всех усилиях сделать текстовое условие как можно полезнее модель всё равно склонна в своих предсказаниях полагаться в основном на зашумлённое входное изображение, а не на промпт. В каком-то смысле это логично: многие подписи лишь отдалённо связаны со своими изображениями, и модель учится не слишком доверять описаниям! Но при генерации новых изображений это нежелательно: если модель не следует промпту, мы можем получить изображения, никак не связанные с нашим описанием.
(В оригинале — изображения по промпту «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: как видите, чем больше значение, тем лучше изображение соответствует описанию.
(Формула, которая стоит за этим: , где — масштаб guidance. При это просто предсказание с текстом, при — «в 7,5 раза больше того, что добавляет текст». Вывод — в уроке 9.1.2.)
Другие виды условий: сверхразрешение, inpainting и depth-to-image
Заголовок раздела «Другие виды условий: сверхразрешение, inpainting и depth-to-image»Можно создавать версии Stable Diffusion, которые принимают дополнительные виды условий. Например, у модели Depth-to-Image есть дополнительные входные каналы с информацией о глубине очищаемого изображения, и при инференсе мы можем подать карту глубины целевого изображения (оценённую отдельной моделью), чтобы, надеемся, получить изображение с похожей общей структурой.
(В оригинале — пример StabilityAI: одно исходное фото и несколько сгенерированных по его карте глубины изображений с той же композицией, но разным содержанием.)
Похожим образом можно подать в качестве условия изображение низкого разрешения, и модель сгенерирует версию высокого разрешения (так работает Stable Diffusion Upscaler). Наконец, можно подать маску, отмечающую область изображения, которую нужно сгенерировать заново, — это задача inpainting («дорисовки»): области вне маски должны остаться нетронутыми, а для области под маской генерируется новое содержание.
Дообучение с DreamBooth
Заголовок раздела «Дообучение с DreamBooth»(В оригинале — иллюстрация со страницы проекта DreamBooth на модели Imagen: по нескольким фотографиям конкретной собаки модель генерирует её в новых сценах.)
DreamBooth — техника дообучения модели «текст → изображение», которая «учит» её новому понятию, например конкретному предмету или стилю. Изначально техника была разработана для модели Imagen от Google, но её быстро адаптировали для Stable Diffusion. Результаты бывают очень впечатляющими (если недавно вы видели у кого-то в соцсетях аватар, сделанный ИИ, скорее всего, он сделан сервисом на основе DreamBooth), но техника чувствительна к настройкам, поэтому загляните в наш ноутбук и в этот отличный разбор разных параметров обучения, чтобы найти советы, как добиться лучшего результата.
Практические ноутбуки
Заголовок раздела «Практические ноутбуки»| Глава | Где запустить |
|---|---|
| Stable Diffusion Introduction | Colab, Kaggle |
| DreamBooth Hackathon Notebook | Colab |
| Stable Diffusion Deep Dive | Colab |
Теперь вы знаете достаточно, чтобы взяться за ноутбуки! Откройте их на удобной платформе по ссылкам выше. DreamBooth требует много вычислений, поэтому в Kaggle или Google Colab обязательно выберите тип среды «GPU».
Ноутбук «Stable Diffusion Introduction» — короткое введение в Stable Diffusion с библиотекой 🤗 Diffusers: несколько базовых примеров использования конвейеров для генерации и изменения изображений.
В ноутбуке DreamBooth Hackathon (в папке hackathon) мы показываем, как дообучить SD на своих изображениях, чтобы получить свою версию модели с новым стилем или понятием.
Наконец, ноутбук и видео «Stable Diffusion Deep Dive» разбирают каждый шаг типичного конвейера генерации и предлагают новые способы менять каждый этап для дополнительного творческого контроля.
Время проекта
Заголовок раздела «Время проекта»Следуя инструкциям в ноутбуке DreamBooth, обучите свою модель в одной из заданных категорий. Обязательно приложите к работе примеры результатов, чтобы мы могли выбрать лучшие модели в каждой категории! Подробности о призах, кредитах на GPU и прочем — в информации о хакатоне.
Дополнительные материалы
Заголовок раздела «Дополнительные материалы»- High-Resolution Image Synthesis with Latent Diffusion Models — статья, где предложен подход, лежащий в основе Stable Diffusion.
- CLIP — CLIP учится связывать текст с изображениями, а его текстовый кодировщик используется, чтобы превратить промпт в богатое числовое представление для SD. См. также эту статью об OpenCLIP — о недавних открытых вариантах CLIP (один из них используется в SD версии 2).
- GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models — одна из первых статей, показавших текстовое условие и CFG.
Нашли ещё отличные материалы? Сообщите нам, и мы добавим их в список.
Ноутбук: введение в Stable Diffusion
Заголовок раздела «Ноутбук: введение в Stable Diffusion»Этот ноутбук разбирает основы того, как с помощью 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 torchimport requestsfrom PIL import Imagefrom io import BytesIOfrom 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")
# Скачиваем изображения для примера inpaintingimg_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 = 512img_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 вместо «пустого» предсказания подставляется предсказание с негативным промптом. Тогда генерация сдвигается не просто к промпту, а к промпту и прочь от негативного.)
Если ищете вдохновения для промптов, начните со Stable Diffusion Prompt Book.
Эффект увеличения масштаба guidance виден в следующей ячейке:
#@markdown сравниваем масштабы guidance:cfg_scales = [1.1, 8, 12] #@paramprompt = "A collie with a pink hat" #@paramfig, 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 - 1print("Input images shape:", images.shape)
# Кодируем в латентное пространствоwith torch.no_grad(): latents = 0.18215 * pipe.vae.encode(images).latent_dist.meanprint("Encoded latents shape:", latents.shape)
# Снова декодируемwith torch.no_grad(): decoded_images = pipe.vae.decode(latents / 0.18215).sampleprint("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 —
такой же, как у шума. Авторы посчитали стандартное отклонение латентов на обучающих данных и делят на него;
. В коде лучше брать его из конфигурации: 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)}")
# Пропускаем через текстовый кодировщик CLIPinput_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 token49406 -> <|startoftext|>320 -> a3086 -> painting539 -> of320 -> a4062 -> floo1059 -> ble49407 -> <|endoftext|>Text embeddings shape: torch.Size([1, 8, 1024])(«flooble» — выдуманное слово, его нет в словаре, поэтому токенизатор разбил его на «floo» + «ble». Так работает BPE, разобранный в уроке 4.2.2. Токенизатор CLIP почти не знает кириллицы — русские промпты в SD 1.x/2.x работают плохо.)
# Получаем итоговые текстовые эмбеддинги функцией конвейера encode_prompttext_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: на вход латент, шаг и текстовые эмбеддинги, на выход — предсказанный шум той же формы, что латент.)
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).sampleprint('UNet output shape:', unet_output.shape) # та же форма, что у входных латентовВывод:
UNet output shape: torch.Size([1, 4, 64, 64])Планировщик
Заголовок раздела «Планировщик»Планировщик хранит расписание шума и управляет обновлением зашумлённого сэмпла по предсказаниям модели. Планировщик по
умолчанию — PNDMScheduler, но можно использовать и другие (например, LMSDiscreteScheduler), если инициализировать
их той же конфигурацией.
Построим график расписания шума, чтобы увидеть уровень шума (по ) во времени:
plt.plot(pipe.scheduler.alphas_cumprod, label=r'$\bar{\alpha}$')plt.xlabel('Timestep (high noise to low noise ->)');plt.title('Noise schedule');plt.legend();(В оригинале — убывающая кривая от 1 до почти 0 по 1000 шагам. Подпись оси в оригинале перепутана: слева направо шум растёт — соответствует чистой картинке.)
Если хотите попробовать другой планировщик, подставьте его так:
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 #@paramnum_inference_steps = 30 #@paramprompt = "Beautiful picture of a wave breaking" #@paramnegative_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
Заголовок раздела «Img2Img»В примерах до сих пор мы генерировали изображения полностью с нуля: начинали со случайных латентов и применяли полный цикл диффузионного сэмплирования. Но начинать с нуля не обязательно. Конвейер Img2Img сначала кодирует существующее изображение в латенты, затем добавляет к ним шум и использует результат как стартовую точку. Количество добавленного шума и число шагов очистки определяют «силу» (strength) процесса img2img. Если добавить совсем немного шума (малая сила), изменений будет очень мало, а если добавить максимум шума и пройти полный процесс очистки, получится изображение, которое почти не похоже на вход, кроме некоторого сходства в общей структуре.
Чтобы лучше понять процесс img2img, реализуем конвейер вручную. Это поможет разобраться, как кодируются латенты,
добавляется шум и применяется диффузия. Реализовав конвейер вручную, посмотрим, как библиотека diffusers даёт готовый
StableDiffusionImg2ImgPipeline для простоты и эффективности.
Вот как реализовать конвейер img2img вручную.
Свой цикл Img2Img
Заголовок раздела «Свой цикл Img2Img»import numpy as np
# Кодируем init_imageinit_image_tensor = torch.from_numpy(np.array(init_image).transpose(2, 0, 1)).float() / 255.0 # 0~255 => 0~1init_image_tensor = 2.0 * init_image_tensor - 1.0 # 0~1 => -1~1init_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_factorguidance_scale = 7.5 #@paramnum_inference_steps = 30 #@paramstrength = 0.6prompt = "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.
Картинка зашумляется до уровня 13-го шага расписания и очищается оттуда; верхние, самые шумные 12 шагов пропускаются.
Промпт: «Картина маслом: мужчина на скамейке».)
Реализовав img2img вручную, посмотрим, как получить тот же результат проще с StableDiffusionImg2ImgPipeline
из библиотеки diffusers.
Этому конвейеру не нужны особые модели: раз идентификатор модели тот же, что в примере «текст → изображение», новые файлы скачивать не придётся.
Конвейер Img2Img
Заголовок раздела «Конвейер Img2Img»# Загружаем конвейер Img2Imgmodel_id = "stabilityai/stable-diffusion-2-1-base"img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id).to(device)В разделе «Подготовка» мы загрузили пример init_image для этой демонстрации, но можно заменить его своим изображением.
Вот конвейер в действии:
# Применяем Img2Imgresult_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
Заголовок раздела «Inpainting»А если мы хотим оставить часть входного изображения без изменений, а в других частях сгенерировать что-то новое? Это
называется inpainting («дорисовка»). Это можно сделать той же моделью, что в прошлых демонстрациях (через
StableDiffusionInpaintPipelineLegacy), но лучшие результаты даёт специально дообученная версия Stable Diffusion,
которая принимает замаскированное изображение и саму маску как дополнительное условие. Изображение маски должно быть
той же формы, что и входное изображение: белым — области, которые надо заменить, чёрным — области, которые надо
оставить без изменений.
Чтобы глубже понять процесс inpainting, реализуем вручную логику StableDiffusionInpaintPipelineLegacy. Так станет
понятнее, как inpainting работает на низком уровне и как Stable Diffusion обрабатывает входы. Затем посмотрим на
дообученный конвейер для сравнения. Вот как реализовать конвейер inpainting вручную и применить его к примеру
изображения и маски, загруженным в разделе «Подготовка».
(В оригинале — схема ручного inpainting: на каждом шаге область под маской берётся из генерируемых латентов, а область вне маски — из латентов исходного изображения, зашумлённых до уровня текущего шага.)
Свой цикл 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 #@paramnum_inference_steps = 30 #@paramprompt = "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() латенты уже соответствуют шагу , и фон должен быть на том же уровне шума, иначе граница
маски будет выделяться. На последнем шаге фон не подмешивается, поэтому область вне маски слегка меняется — это видно
по результату.)
Конвейер inpainting
Заголовок раздела «Конвейер inpainting»Реализовав логику 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-4models--ddpm-bedroom-256models--google--ddpm-bedroom-256models--google--ddpm-celebahq-256models--runwayml--stable-diffusion-inpaintingmodels--stabilityai--stable-diffusion-2-1-baseЗагляните в документацию о кэшировании, чтобы узнать, как смотреть кэш и эффективно им управлять.
(Удобная команда: hf cache scan показывает размер каждой модели в кэше, а hf cache delete — удаляет выбранные.
Каждая модель SD занимает от 2 до 5 ГБ.)
Depth2Image
Заголовок раздела «Depth2Image»(В оригинале — примеры 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 scale | guidance без классификатора, масштаб guidance |
| negative prompt | негативный промпт |
| img2img, strength | «картинка → картинка», сила изменения |
| inpainting, mask | дорисовка (inpainting), маска |
| depth map, depth2img | карта глубины, «глубина → картинка» |
| super-resolution, upscaler | сверхразрешение, апскейлер |
| attention slicing | нарезка внимания (экономия памяти) |
| DreamBooth | DreamBooth — дообучение на новое понятие по нескольким фото |