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

Diffusion Models Course, юнит 4: продвинутые техники, инверсия DDIM и диффузия для звука

Перевод юнита 4 курса Hugging Face о диффузионных моделях: дистилляция для быстрого сэмплирования, улучшения обучения, редактирование изображений, видео и звук, DiT и «итеративное уточнение», а также ноутбуки об инверсии DDIM и о дообучении аудиодиффузии на спектрограммах.

Это перевод юнита 4 курса Diffusion Models Course: вводный текст юнита (обзор исследований) и два ноутбука — «DDIM Inversion» и «Diffusion for Audio». Предыдущий юнит — Stable Diffusion; этот юнит в курсе последний. Карта курса — в README оригинала.

Юнит 4: идём дальше с диффузионными моделями

Заголовок раздела «Юнит 4: идём дальше с диффузионными моделями»

Добро пожаловать в юнит 4 курса Hugging Face о диффузионных моделях! В этом юните мы посмотрим на некоторые из многочисленных улучшений и расширений диффузионных моделей, которые появляются в свежих исследованиях. Кода здесь меньше, чем в предыдущих юнитах: юнит задуман как стартовая площадка для ваших дальнейших исследований.

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

  • Убедитесь, что записались на курс, чтобы получать уведомления о новых юнитах.
  • Прочитайте материал ниже — обзор тем этого юнита.
  • Углубитесь в интересные вам темы с помощью видео и материалов по ссылкам.
  • Изучите демонстрационные ноутбуки, а затем прочитайте раздел «Что дальше?» с идеями проектов.

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

(Оглавление оригинала мы опускаем — на сайте оно справа от текста.)

Прогрессивная дистилляция (progressive distillation) — техника, которая берёт готовую диффузионную модель и с её помощью обучает новую версию модели, которой для инференса нужно меньше шагов. Модель-«ученик» инициализируется весами модели-«учителя». При обучении учитель делает два шага сэмплирования, а ученик пытается получить тот же результат за один шаг. Этот процесс можно повторять много раз: ученик предыдущей итерации становится учителем на следующем этапе. В итоге получается модель, которая выдаёт приличные сэмплы за гораздо меньшее число шагов (обычно 4 или 8), чем исходный учитель. Основной механизм показан на схеме из статьи, в которой предложена эта идея:

(В оригинале — рисунок из статьи с подписью «Прогрессивная дистилляция в картинках»: детерминированный сэмплер, который за 4 шага проходит от шума к изображению, дистиллируется сначала в сэмплер на 2 шага, а затем — на 1 шаг.)

flowchart LR
T["Учитель<br/>N шагов"] -->|"ученик учится за 1 шаг<br/>делать то, что учитель за 2"| S1["Ученик 1<br/>N/2 шагов"]
S1 -->|"ученик становится<br/>учителем"| S2["Ученик 2<br/>N/4 шагов"]
S2 --> D["…"] --> F["4–8 шагов"]
Прогрессивная дистилляция: каждый этап вдвое сокращает число шагов (рисунок переводчика)

(Пример с числами от переводчика: чтобы из 1024 шагов получить 4, нужно 8 этапов дистилляции: 1024 → 512 → 256 → 128 → 64 → 32 → 16 → 8 → 4.)

Идею «учить» новую модель с помощью готовой можно развить и получать модели с guidance: учитель применяет classifier-free guidance, а ученик должен научиться выдавать эквивалентный результат за один шаг, получая дополнительным входом нужный масштаб guidance. Это ещё сильнее сокращает число прогонов модели, необходимых для качественных сэмплов. Обзор подхода — в этом видео.

(«За один шаг» здесь значит «за один прогон сети». Без дистилляции classifier-free guidance на каждом шаге требует двух прогонов — с промптом и без, — и ученик учится заменять их одним.)

NB: дистиллированную версию Stable Diffusion можно попробовать здесь.

Ключевые работы:

(Примечание переводчика: с 2023 года дистилляция стала обязательной частью почти каждой крупной модели. Появились consistency models и Latent Consistency Models (LCM), состязательная дистилляция (SDXL-Turbo), а у FLUX.1 [schnell] по умолчанию 4 шага. Обзор — в уроке 9.1.4.)

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

(В оригинале — рисунок 2 из статьи ERNIE-ViLG 2.0: общая схема модели, в которой к обучению добавлены знания о тексте и изображении, а удаление шума разделено по шагам между несколькими «экспертами»-денойзерами.)

Главные улучшения обучения:

  • Настройка расписания шума (noise schedule), взвешивания функции потерь и траекторий сэмплирования для более эффективного обучения. Отличная статья, исследующая некоторые из этих проектных решений, — Elucidating the Design Space of Diffusion-Based Generative Models Карраса (Karras) и соавторов.
  • Обучение на изображениях с разным соотношением сторон, как рассказано в этом видео с мероприятия по запуску курса.
  • Каскадные диффузионные модели: одна модель обучается на низком разрешении, затем ещё одна или несколько моделей сверхразрешения (super-resolution). Так устроены DALL·E 2, Imagen и другие модели для генерации изображений высокого разрешения.
  • Более качественное условие: богатые текстовые эмбеддинги (Imagen использует большую языковую модель T5) или несколько видов условий сразу (eDiffi).
  • «Обогащение знаниями» (knowledge enhancement): в процесс обучения включаются предобученные модели для подписей к изображениям и для детекции объектов, чтобы получить более информативные подписи и лучшее качество (ERNIE-ViLG 2.0).
  • «Смесь экспертов-денойзеров» (Mixture of Denoising Experts, MoDE): разные варианты модели («эксперты») обучаются для разных уровней шума, как на рисунке выше из статьи ERNIE-ViLG 2.0.

Ключевые работы:

(Примечание переводчика: ERNIE-ViLG 2.0 — модель китайской компании Baidu. Идея «экспертов по уровню шума» вернулась в видеомодели Wan2.2 (Alibaba, 2025): там один эксперт работает на шумных ранних шагах и отвечает за композицию, другой — на поздних шагах и отвечает за детали. Расписания из статьи Карраса (EDM) встроены в diffusers — у многих планировщиков есть параметр use_karras_sigmas=True. Сильный текстовый кодировщик тоже стал нормой: SD3 и FLUX используют T5, а Qwen-Image — мультимодальную модель Qwen2.5-VL.)

Больше контроля при генерации и редактировании

Заголовок раздела «Больше контроля при генерации и редактировании»

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

(В оригинале — сэмплы, созданные «рисованием словами» (paint-with-words) в eDiffi: пользователь закрашивает на холсте области и подписывает их словами из промпта, а модель рисует картинку, где названные объекты стоят в этих местах.)

Видео «Editing Images with Diffusion Models» («Редактирование изображений диффузионными моделями») даёт обзор разных методов редактирования существующих изображений диффузионными моделями. Доступные техники можно разделить на четыре основные категории.

  1. Добавить шум, а затем удалить его с новым промптом. Это идея конвейера img2img, которую изменяли и развивали в разных статьях:
  • SDEdit и MagicMix построены на этой идее.
  • Инверсия DDIM (DDIM inversion) использует модель, чтобы «развернуть» траекторию сэмплирования вместо добавления случайного шума, — это даёт больше контроля. (В оригинале здесь пометка «TODO: ссылка на туториал»; этот туториал вышел — это ноутбук «DDIM Inversion», переведённый ниже.)
  • Null-text Inversion резко улучшает качество такого подхода: на каждом шаге оптимизирует безусловные текстовые эмбеддинги, которые используются в classifier-free guidance. Это позволяет редактировать изображения по тексту с очень высоким качеством.
  1. То же, что в пункте 1, но с маской, которая определяет, где применять эффект:
  • Blended Diffusion вводит базовую идею.
  • Это демо использует готовую модель сегментации (CLIPSeg), чтобы построить маску по текстовому описанию.
  • DiffEdit — отличная статья, которая показывает, как сама диффузионная модель может построить подходящую маску для редактирования изображения по тексту.
  • SmartBrush: Text and Shape Guided Object Inpainting with Diffusion Model («SmartBrush: дорисовка объектов диффузионной моделью по тексту и форме») дообучает диффузионную модель для более точной дорисовки (inpainting) по маске.
  1. Управление перекрёстным вниманием (cross-attention control): механизм перекрёстного внимания в диффузионных моделях используется, чтобы управлять тем, в каком месте картинки происходят правки, и получить более тонкий контроль:
  • Prompt-to-Prompt Image Editing with Cross Attention Control («Редактирование изображений „от промпта к промпту“ через управление перекрёстным вниманием») — ключевая статья, в которой предложена эта идея; позже технику применили к Stable Diffusion.
  • Эта же идея используется в «рисовании словами» (eDiffi, см. рисунок выше).
  1. Дообучить («переобучить») модель на одном изображении, а затем генерировать дообученной моделью. Варианты этой идеи примерно одновременно опубликовали две статьи:

Статья InstructPix2Pix: Learning to Follow Image Editing Instructions («InstructPix2Pix: учимся следовать инструкциям по редактированию изображений») примечательна тем, что использовала часть описанных выше техник редактирования, чтобы собрать синтетический датасет пар изображений вместе с инструкциями по правке (сгенерированными GPT-3.5), и обучила на нём новую модель, которая умеет редактировать изображения по инструкциям на естественном языке.

flowchart TD
E["Редактирование готовой картинки<br/>диффузионной моделью"] --> A["1. Зашумить и очистить<br/>с новым промптом<br/>SDEdit, инверсия DDIM, Null-text"]
E --> B["2. То же, но с маской<br/>Blended Diffusion, DiffEdit, SmartBrush"]
E --> C["3. Управлять перекрёстным вниманием<br/>Prompt-to-Prompt, paint-with-words"]
E --> D["4. Дообучить на одной картинке<br/>Imagic, UniTune"]
A --> I["Синтетические пары для обучения<br/>InstructPix2Pix"]
C --> I
Четыре семейства методов редактирования (рисунок переводчика)

(Примечание переводчика: почти сразу после выхода юнита, в феврале 2023 года, появился ControlNet — отдельная сеть-адаптер, которая подаёт в U-Net карту глубины, контуры или позу. Он стал главным способом пространственного контроля. Линия InstructPix2Pix привела к моделям, которые редактируют картинку по инструкции, — например, FLUX.1 Kontext и Qwen-Image-Edit (2025). В diffusers есть готовые DDIMInverseScheduler и конвейер StableDiffusionDiffEditPipeline для DiffEdit.)

(В оригинале — кадры из видео, сгенерированных Imagen Video; источник — страница Imagen Video.)

Видео можно представить как последовательность изображений, и основные идеи диффузионных моделей применимы к таким последовательностям. Недавние работы сосредоточены на поиске подходящих архитектур (например, «3D U-Net», которые работают с целой последовательностью) и на эффективной работе с видеоданными. В видео с высокой частотой кадров гораздо больше данных, чем в статичных изображениях, поэтому нынешние подходы обычно сначала генерируют видео низкого разрешения и с низкой частотой кадров, а затем применяют пространственное и временное сверхразрешение, чтобы получить итоговое качественное видео.

Ключевые работы:

(Примечание переводчика: в Imagen Video каскад из 7 моделей. Базовая выдаёт 16 кадров 40×24 при 3 кадрах в секунду, а после трёх ступеней временного и трёх ступеней пространственного сверхразрешения получается 128 кадров 1280×768 при 24 кадрах в секунду — около 5,3 с. С 2024 года (Sora, затем HunyuanVideo, Wan2.1/2.2 и другие) каскады уступили место одной большой модели: видео-VAE сжимает ролик и по времени, и по пространству, а DiT-трансформер очищает от шума сразу весь пространственно-временной латент.)

(В оригинале — спектрограмма, сгенерированная Riffusion; источник изображения.)

Были работы по генерации звука напрямую диффузионными моделями (например, DiffWave), но самым успешным подходом пока оказалось преобразовать звуковой сигнал в так называемую спектрограмму. Она фактически «кодирует» звук как двумерное «изображение», на котором можно обучать такие же диффузионные модели, какими мы привыкли генерировать картинки. Сгенерированные спектрограммы затем превращаются обратно в звук существующими методами. Этот подход лежит в основе недавно вышедшей Riffusion — в ней Stable Diffusion дообучили генерировать спектрограммы по тексту (попробуйте здесь).

Генерация звука развивается очень быстро. Только за последнюю неделю (на момент написания) объявили как минимум о 5 новых достижениях — в списке ниже они отмечены звёздочкой.

Ключевые работы:

  • DiffWave: A Versatile Diffusion Model for Audio Synthesis («DiffWave: универсальная диффузионная модель для синтеза звука»)
  • Riffusion (и код)
  • *MusicLM от Google генерирует связный звук по тексту; в качестве условия можно напеть или насвистеть мелодию.
  • *RAVE2 — новая версия вариационного автокодировщика, полезная для латентной диффузии в задачах со звуком. Он используется в модели *AudioLDM, анонс которой вот-вот выйдет.
  • *Noise2Music — диффузионная модель, обученная выдавать качественные 30-секундные аудиоклипы по текстовому описанию.
  • *Make-An-Audio: Text-To-Audio Generation with Prompt-Enhanced Diffusion Models («Make-An-Audio: генерация звука по тексту диффузионными моделями с улучшенными промптами») — диффузионная модель, обученная генерировать разнообразные звуки по тексту.
  • *Moûsai: Text-to-Music Generation with Long-Context Latent Diffusion («Moûsai: генерация музыки по тексту латентной диффузией с длинным контекстом»)

(Примечание переводчика: MusicLM — не диффузионная модель. Она, как языковая модель, авторегрессионно генерирует дискретные звуковые токены. Дальше развивались оба пути. Латентная диффузия на сжатом представлении звука — это AudioLDM 2 и Stable Audio, авторегрессия токенов — MusicGen. Как звук превращают в спектрограмму и в токены, разобрано в уроке 9.1.5.)

Новые архитектуры и подходы: к «итеративному уточнению»

Заголовок раздела «Новые архитектуры и подходы: к «итеративному уточнению»»

(В оригинале — рисунок 1 из статьи Cold Diffusion: прямой и обратный процессы для разных видов «порчи» изображения — не только гауссова шума, но и, например, размытия, маскирования и пикселизации.)

Мы постепенно выходим за рамки исходного узкого определения «диффузионных» моделей к более общему классу моделей, выполняющих итеративное уточнение (iterative refinement): некая форма «порчи» данных (как добавление гауссова шума в прямом процессе диффузии) постепенно обращается вспять, чтобы сгенерировать сэмплы. Статья «Cold Diffusion» показала, что для генерации изображений можно итеративно «отменять» и многие другие виды порчи (примеры — на рисунке выше), а недавние подходы на основе трансформеров продемонстрировали, что замена или маскирование токенов — эффективная стратегия зашумления.

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

Архитектуру U-Net, лежащую в основе многих нынешних диффузионных моделей, тоже заменяют разными альтернативами — прежде всего различными архитектурами на основе трансформеров. В Scalable Diffusion Models with Transformers (DiT) трансформер используется вместо U-Net в довольно стандартном подходе к диффузии — с отличными результатами. Recurrent Interface Networks применяют новую архитектуру на основе трансформера и новую стратегию обучения ради дополнительной эффективности. MaskGIT и MUSE используют трансформеры для работы с токенизированными представлениями изображений, хотя модель Paella показывает, что и в таких режимах на токенах можно успешно применять U-Net.

С каждой новой статьёй появляются всё более эффективные подходы, и, возможно, пройдёт немало времени, прежде чем мы увидим, каков потолок качества в подобных задачах итеративного уточнения. Исследовать предстоит ещё очень многое!

Ключевые работы:

(Примечание переводчика: прогноз сбылся. DiT вытеснил U-Net: на трансформерах построены SD3, FLUX, Sora, Wan, HunyuanVideo, Qwen-Image (урок 9.1.2). Flow matching (урок 9.1.4) — тоже итеративное уточнение, только вдоль прямого пути от шума к данным. Маскирование токенов как «зашумление» дошло до текста: диффузионные языковые модели вроде LLaDA (Китай, 2025) генерируют ответ, постепенно раскрывая замаскированные токены. Про дискретные токены изображений — урок 9.1.3.)

ГлаваГде запустить
DDIM InversionColab, Kaggle, Gradient, Studio Lab
Diffusion for AudioColab, Kaggle, Gradient, Studio Lab

В этом юните мы охватили ОЧЕНЬ много разных идей, и многие из них в будущем заслуживают гораздо более подробных отдельных уроков. Пока же две из многих тем можно разобрать в подготовленных нами практических ноутбуках:

  • DDIM Inversion показывает, как с помощью техники под названием «инверсия» редактировать изображения существующими диффузионными моделями.
  • Diffusion for Audio знакомит с идеей спектрограмм и показывает минимальный пример дообучения аудиодиффузионной модели на конкретном жанре музыки.

Оба ноутбука переведены ниже на этой странице.

Пока это последний юнит курса, а значит, что делать дальше — решать вам! Помните, что задавать вопросы и обсуждать свои проекты всегда можно в Discord Hugging Face. Ждём не дождёмся увидеть, что вы создадите 🤗.

(Примечание переводчика: новых юнитов в курсе так и не вышло. Продолжить можно с курсом fast.ai «Stable Diffusion from the Foundations», который упоминается в юните 3, с документацией 🤗 Diffusers и с уроками 9.1.2–9.1.5 нашего курса.)

В этом ноутбуке мы разберём инверсию (inversion), посмотрим, как она связана с сэмплированием, и применим её к задаче редактирования изображений со Stable Diffusion.

  • Как работает сэмплирование DDIM.
  • Чем детерминированные сэмплеры отличаются от стохастических.
  • Какая теория стоит за инверсией DDIM.
  • Как редактировать изображения с помощью инверсии.

Приступим!

%pip install -q transformers diffusers accelerate
import torch
import requests
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
from io import BytesIO
from tqdm.auto import tqdm
from matplotlib import pyplot as plt
from torchvision import transforms as tfms
from diffusers import StableDiffusionPipeline, DDIMScheduler
# Функция, которая пригодится дальше
def load_image(url, size=None):
response = requests.get(url,timeout=0.2)
img = Image.open(BytesIO(response.content)).convert('RGB')
if size is not None:
img = img.resize(size)
return img

(Примечание переводчика: timeout=0.2 — это 0,2 секунды на ответ сервера. На медленном соединении загрузка картинки упадёт с ошибкой — смело поставьте timeout=10.)

device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
# Загружаем конвейер
pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5").to(device)

(Примечание переводчика: репозиторий runwayml/stable-diffusion-v1-5 удалён с Hub; та же модель доступна под именем "stable-diffusion-v1-5/stable-diffusion-v1-5". В fp32 веса SD 1.5 занимают около 4 ГБ — нужна видеокарта примерно с 8 ГБ памяти. Переходить на fp16 здесь не советуем: дальше картинка кодируется вручную тензором float32, и придётся самим приводить типы.)

# Ставим планировщик DDIM
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
# Сэмплируем изображение, чтобы убедиться, что всё работает
prompt = 'Beautiful DSLR Photograph of a penguin on the beach, golden hour'
negative_prompt = 'blurry, ugly, stock photo'
im = pipe(prompt, negative_prompt=negative_prompt).images[0]
im.resize((256, 256)) # Уменьшаем для удобного просмотра

(Промпт: «Красивая фотография пингвина на пляже, снятая зеркальной камерой, золотой час». Негативный промпт: «размыто, некрасиво, фотосток». В выводе — фото пингвина на пляже.)

В момент tt зашумлённое изображение xtx_t — это смесь исходного изображения (x0x_0) и шума (ϵ\epsilon). Вот формула для xtx_t из статьи о DDIM, на которую мы будем опираться в этом разделе:

xt=αtx0+1−αtϵx_t = \sqrt{\alpha_t}x_0 + \sqrt{1-\alpha_t}\epsilon

ϵ\epsilon — гауссов шум с единичной дисперсией. αt\alpha_t («альфа») — величина, которую в статье о DDPM (!!), сбивая с толку, называют αˉ\bar{\alpha} («альфа с чертой», alpha_bar); она задаётся планировщиком шума. В Diffusers этот график альфа вычисляется заранее, а значения хранятся в scheduler.alphas_cumprod. Знаю, путаница! Нарисуем эти значения и запомним: до конца ноутбука мы пользуемся обозначениями DDIM.

# Рисуем 'alpha' (alpha_bar на языке DDPM, alphas_cumprod в Diffusers) для ясности
timesteps = pipe.scheduler.timesteps.cpu()
alphas = pipe.scheduler.alphas_cumprod[timesteps]
plt.plot(timesteps, alphas, label='alpha_t');
plt.legend();

Вначале (шаг 0, левая часть графика) у нас чистое изображение без шума: αt=1\alpha_t = 1. По мере роста шага мы приходим почти к чистому шуму, и αt\alpha_t падает к 0.

(На графике — кривая, которая плавно спадает от 1 при t=0t = 0 почти до 0 при t≈1000t \approx 1000.)

При сэмплировании мы начинаем с чистого шума на шаге 1000 и постепенно движемся к шагу 0. Чтобы вычислить следующую точку траектории сэмплирования (xt−1x_{t-1}, ведь мы движемся от больших tt к малым), мы предсказываем шум (ϵθ(xt)\epsilon_\theta(x_t) — это выход нашей модели) и по нему вычисляем предсказанное очищенное изображение x0x_0. Затем с помощью этого предсказания сдвигаемся на небольшое расстояние в «направлении, указывающем на xtx_t». Наконец, можно добавить немного дополнительного шума, масштабированного на σt\sigma_t. Вот раздел статьи, где это показано:

xt−1=αt−1 (xt−1−αt ϵθ(t)(xt)αt)⏟предсказанный x0+1−αt−1−σt2⋅ϵθ(t)(xt)⏟направление, указывающее на xt+σtϵt⏟случайный шумx_{t-1} = \sqrt{\alpha_{t-1}}\,\underbrace{\left(\frac{x_t - \sqrt{1-\alpha_t}\,\epsilon_\theta^{(t)}(x_t)}{\sqrt{\alpha_t}}\right)}_{\text{предсказанный } x_0} + \underbrace{\sqrt{1-\alpha_{t-1}-\sigma_t^2}\cdot\epsilon_\theta^{(t)}(x_t)}_{\text{направление, указывающее на } x_t} + \underbrace{\sigma_t\epsilon_t}_{\text{случайный шум}}

(В ноутбуке это скриншот раздела 4.1 статьи DDIM — формула (12) и пояснение к ней. Перевод пояснения: ϵt∼N(0,I)\epsilon_t \sim \mathcal{N}(0, I) — стандартный гауссов шум, не зависящий от xtx_t, и по определению α0:=1\alpha_0 := 1. Разные значения σ\sigma дают разные генеративные процессы при одной и той же модели ϵθ\epsilon_\theta, поэтому переобучать модель не нужно. При σt=(1−αt−1)/(1−αt)1−αt/αt−1\sigma_t = \sqrt{(1-\alpha_{t-1})/(1-\alpha_t)}\sqrt{1-\alpha_t/\alpha_{t-1}} для всех tt прямой процесс становится марковским, а генеративный процесс — это DDPM. Другой особый случай — σt=0\sigma_t = 0 для всех tt: коэффициент перед случайным шумом ϵt\epsilon_t обнуляется, и сэмплы получаются из латентных переменных фиксированной процедурой (от xTx_T к x0x_0). Такую модель авторы назвали denoising diffusion implicit model (DDIM, «неявная диффузионная модель удаления шума»): это неявная вероятностная модель, обученная с целевой функцией DDPM.)

Итак, у нас есть уравнение, как перейти от xtx_t к xt−1x_{t-1} с регулируемым количеством шума. Сегодня нас интересует именно случай, когда дополнительный шум не добавляется: это даёт полностью детерминированное сэмплирование DDIM. Посмотрим, как это выглядит в коде:

# Функция сэмплирования (обычный DDIM)
@torch.no_grad()
def sample(prompt, start_step=0, start_latents=None,
guidance_scale=3.5, num_inference_steps=30,
num_images_per_prompt=1, do_classifier_free_guidance=True,
negative_prompt='', device=device):
# Кодируем промпт
text_embeddings = pipe._encode_prompt(
prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt
)
# Задаём число шагов инференса
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Создаём случайную стартовую точку, если её ещё нет
if start_latents is None:
start_latents = torch.randn(1, 4, 64, 64, device=device)
start_latents *= pipe.scheduler.init_noise_sigma
latents = start_latents.clone()
for i in tqdm(range(start_step, num_inference_steps)):
t = pipe.scheduler.timesteps[i]
# Если используем classifier-free guidance, удваиваем латенты
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# Предсказываем шум (остаток)
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# Применяем guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# Обычно шаг обновления мы доверяем планировщику:
# latents = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# А здесь сделаем его сами:
prev_t = max(1, t.item() - (1000//num_inference_steps)) # t-1
alpha_t = pipe.scheduler.alphas_cumprod[t.item()]
alpha_t_prev = pipe.scheduler.alphas_cumprod[prev_t]
predicted_x0 = (latents - (1-alpha_t).sqrt()*noise_pred) / alpha_t.sqrt()
direction_pointing_to_xt = (1-alpha_t_prev).sqrt()*noise_pred
latents = alpha_t_prev.sqrt()*predicted_x0 + direction_pointing_to_xt
# Постобработка
images = pipe.decode_latents(latents)
images = pipe.numpy_to_pil(images)
return images
# Проверяем нашу функцию сэмплирования: генерируем изображение
sample('Watercolor painting of a beach sunset', negative_prompt=negative_prompt, num_inference_steps=50)[0].resize((256, 256))

(Промпт: «Акварель: закат на пляже». В выводе — акварельный пейзаж с закатом.)

Попробуйте сопоставить код с уравнением из статьи. Обратите внимание: σ=0\sigma = 0, ведь нас интересует только случай без дополнительного шума, поэтому соответствующие части уравнения можно опустить.

Цель инверсии — «обратить» процесс сэмплирования. Мы хотим получить такой зашумлённый латент, что если взять его стартовой точкой обычной процедуры сэмплирования, то сгенерируется исходное изображение.

Здесь в качестве исходного изображения мы загружаем фотографию, но можно и сгенерировать своё.

# https://www.pexels.com/photo/a-beagle-on-green-grass-field-8306128/
input_image = load_image('https://images.pexels.com/photos/8306128/pexels-photo-8306128.jpeg', size=(512, 512))
input_image

(В выводе — фото щенка бигля на зелёной траве.)

Инверсию мы тоже будем делать с classifier-free guidance, поэтому нужен промпт — введите описание изображения:

input_image_prompt = "Photograph of a puppy on the grass"

(«Фотография щенка на траве».)

Дальше нужно превратить это PIL-изображение в набор латентов, который станет стартовой точкой инверсии:

# Кодируем VAE
with torch.no_grad(): latent = pipe.vae.encode(tfms.functional.to_tensor(input_image).unsqueeze(0).to(device)*2-1)
l = 0.18215 * latent.latent_dist.sample()

(*2-1 переводит пиксели из диапазона [0, 1] в [−1, 1], как ждёт VAE. 0.18215 — масштабный множитель латентов VAE у SD 1.x, он же pipe.vae.config.scaling_factor.)

Теперь самое интересное. Эта функция похожа на функцию сэмплирования выше, но проходит по шагам в обратном направлении: начинает с t=0t = 0 и движется ко всё более сильному шуму. И вместо того чтобы обновлять латенты, делая их менее шумными, мы оцениваем предсказанный шум и с его помощью ОТМЕНЯЕМ шаг обновления, переводя латенты из tt в t+1t+1.

## Инверсия
@torch.no_grad()
def invert(start_latents, prompt, guidance_scale=3.5, num_inference_steps=80,
num_images_per_prompt=1, do_classifier_free_guidance=True,
negative_prompt='', device=device):
# Кодируем промпт
text_embeddings = pipe._encode_prompt(
prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt
)
# Теперь латенты — это заданные стартовые латенты
latents = start_latents.clone()
# По ходу инверсии будем сохранять список промежуточных латентов
intermediate_latents = []
# Задаём число шагов инференса
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# Шаги в обратном порядке <<<<<<<<<<<<<<<<<<<<
timesteps = reversed(pipe.scheduler.timesteps)
for i in tqdm(range(1, num_inference_steps), total=num_inference_steps-1):
# Последнюю итерацию пропускаем
if i >= num_inference_steps - 1: continue
t = timesteps[i]
# Если используем classifier-free guidance, удваиваем латенты
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
# Предсказываем шум (остаток)
noise_pred = pipe.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# Применяем guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
current_t = max(0, t.item() - (1000//num_inference_steps)) #t
next_t = t # min(999, t.item() + (1000//num_inference_steps)) # t+1
alpha_t = pipe.scheduler.alphas_cumprod[current_t]
alpha_t_next = pipe.scheduler.alphas_cumprod[next_t]
# Обращённый шаг обновления (переставляем члены уравнения шага, чтобы выразить x(t) (новые латенты) через x(t-1) (текущие латенты)
latents = (latents - (1-alpha_t).sqrt()*noise_pred)*(alpha_t_next.sqrt()/alpha_t.sqrt()) + (1-alpha_t_next).sqrt()*noise_pred
# Сохраняем
intermediate_latents.append(latents)
return torch.cat(intermediate_latents)

Запустив её на латентном представлении фото щенка, получаем набор всех промежуточных латентов, созданных по ходу инверсии:

inverted_latents = invert(l, input_image_prompt, num_inference_steps=50)
inverted_latents.shape

Вывод:

torch.Size([48, 4, 64, 64])

(Почему 48, а не 50: цикл идёт с i=1i = 1 до 49 — это 49 итераций, а последнюю функция пропускает.)

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

# Декодируем итоговые обращённые латенты
with torch.no_grad():
im = pipe.decode_latents(inverted_latents[-1].unsqueeze(0))
pipe.numpy_to_pil(im)[0]

(В выводе — пёстрая мозаика из цветных пятен без узнаваемых форм: декодированный «шумный» латент.)

Эти обращённые латенты можно передать в конвейер через обычный метод __call__:

pipe(input_image_prompt, latents=inverted_latents[-1][None], num_inference_steps=50, guidance_scale=3.5).images[0]

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

Но тут мы видим первую проблему: это не совсем то изображение, с которого мы начинали! Дело в том, что инверсия DDIM опирается на критическое допущение: предсказание шума в моменты tt и t+1t+1 будет одинаковым. Это неверно, когда мы обращаем всего 50 или 100 шагов. Можно взять больше шагов в надежде на более точную инверсию, а можно «схитрить»: начать сэмплирование, скажем, с 20-го шага из 50 с соответствующими промежуточными латентами, которые мы сохранили при инверсии:

# Вот зачем нам нужно уметь задавать стартовый шаг
start_step = 20
sample(input_image_prompt, start_latents=inverted_latents[-(start_step+1)][None],
start_step=start_step, num_inference_steps=50)[0]

Очень близко к исходному изображению! Зачем мы это делаем? Надежда в том, что если теперь сэмплировать с новым промптом, получится изображение, совпадающее с оригиналом ВСЮДУ, КРОМЕ мест, к которым относится новый промпт. Например, если заменить ‘puppy’ на ‘cat’, мы должны увидеть кошку с почти такими же лужайкой и фоном:

# Сэмплируем с новым промптом
start_step = 10
new_prompt = input_image_prompt.replace('puppy', 'cat')
sample(new_prompt, start_latents=inverted_latents[-(start_step+1)][None],
start_step=start_step, num_inference_steps=50)[0]

(Индексы: в inverted_latents латенты идут от малого шума к большому, а сэмплирование — от большого к малому. Поэтому для старта с шага start_step берётся элемент -(start_step+1), отсчитанный с конца списка.)

Зачем возиться с инверсией? Нельзя ли просто добавить шум к исходному изображению и очистить его с новым промптом? Можно, но тогда изменения получатся либо гораздо более радикальными везде (если добавить много шума), либо недостаточными где бы то ни было (если добавить мало). Попробуйте сами:

start_step = 10
num_inference_steps = 50
pipe.scheduler.set_timesteps(num_inference_steps)
noisy_l = pipe.scheduler.add_noise(l, torch.randn_like(l), pipe.scheduler.timesteps[start_step])
sample(new_prompt, start_latents=noisy_l, start_step=start_step, num_inference_steps=num_inference_steps)[0]

Обратите внимание, насколько сильнее изменились лужайка и фон.

(В выводе — кошка на траве, но лужайка и фон уже заметно другие, чем на исходной фотографии.)

Обернём написанный код в простую функцию, которая принимает изображение и два промпта и выполняет правку с помощью инверсии:

def edit(input_image, input_image_prompt, edit_prompt, num_steps=100, start_step=30, guidance_scale=3.5):
with torch.no_grad(): latent = pipe.vae.encode(tfms.functional.to_tensor(input_image).unsqueeze(0).to(device)*2-1)
l = 0.18215 * latent.latent_dist.sample()
inverted_latents = invert(l, input_image_prompt, num_inference_steps=num_steps)
final_im = sample(edit_prompt, start_latents=inverted_latents[-(start_step+1)][None],
start_step=start_step, num_inference_steps=num_steps, guidance_scale=guidance_scale)[0]
return final_im

В действии:

edit(input_image, 'A puppy on the grass', 'an old grey dog on the grass', num_steps=50, start_step=10)
edit(input_image, 'A puppy on the grass', 'A blue dog on the lawn', num_steps=50, start_step=12, guidance_scale=6)

(Промпты: «щенок на траве» → «старая седая собака на траве»; «щенок на траве» → «синяя собака на лужайке».)

# Упражнение: попробуйте это на других изображениях! Исследуйте разные параметры.
flowchart LR
I["Фото"] -->|"кодировщик VAE"| L["Латент x_0"]
L -->|"инверсия DDIM<br/>с исходным промптом"| N["Латенты x_1 … x_T<br/>(сохраняем все)"]
N -->|"берём x на шаге start_step"| S["Сэмплирование DDIM<br/>с новым промптом"]
S -->|"декодер VAE"| O["Отредактированное фото"]
Функция edit: инверсия, затем сэмплирование с новым промптом (рисунок переводчика)

Если инверсия получается неточной, попробуйте взять больше шагов (ценой более долгой работы). Чтобы проверить инверсию, можно вызвать нашу функцию edit с одним и тем же промптом:

# Проверка инверсии с гораздо большим числом шагов
edit(input_image, 'A puppy on the grass', 'A puppy on the grass', num_steps=350, start_step=1)

Намного лучше! Попробуем теперь правку:

edit(input_image, 'A photograph of a puppy', 'A photograph of a grey cat', num_steps=150, start_step=30, guidance_scale=5.5)
# источник: https://www.pexels.com/photo/girl-taking-photo-1493111/
face = load_image('https://images.pexels.com/photos/1493111/pexels-photo-1493111.jpeg', size=(512, 512))
face
edit(face, 'A photograph of a face', 'A photograph of a face with sunglasses', num_steps=250, start_step=30, guidance_scale=3.5)
edit(face, 'A photograph of a face', 'Acrylic palette knife painting of a face, colorful', num_steps=250, start_step=65, guidance_scale=5.5)

(Промпты: «фотография щенка» → «фотография серой кошки»; «фотография лица» → «фотография лица в солнечных очках»; «фотография лица» → «акриловая живопись мастихином: лицо, красочно». Чем больше start_step, тем ближе к концу траектории начинается сэмплирование и тем меньше шума остаётся — значит, тем сильнее результат похож на оригинал. Для смены стиля в последнем примере взят start_step=65 из 250: первые 26% траектории пропускаются, и сэмплирование стартует примерно с t=740t = 740.)

Вооружившись знаниями из этого ноутбука, рекомендую разобраться с Null-text Inversion: этот метод развивает DDIM, оптимизируя при инверсии «пустой» текст (безусловный текстовый промпт), чтобы инверсия была точнее, а правки — лучше.

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

  • Как звук представлен в компьютере.
  • Как переводить сырые аудиоданные в спектрограммы и обратно.
  • Как подготовить загрузчик данных со своей функцией collate, которая превращает фрагменты звука в спектрограммы.
  • Как дообучить готовую аудиодиффузионную модель на конкретном жанре музыки.
  • Как выгрузить свой конвейер на Hugging Face Hub.

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

Приступим!

%pip install -q datasets diffusers torchaudio accelerate
import torch, random
import numpy as np
import torch.nn.functional as F
from tqdm.auto import tqdm
from IPython.display import Audio
from matplotlib import pyplot as plt
from diffusers import DiffusionPipeline
from torchaudio import transforms as AT
from torchvision import transforms as IT

Сэмплирование из предобученного аудиоконвейера

Заголовок раздела «Сэмплирование из предобученного аудиоконвейера»

Для начала, следуя документации Audio Diffusion, загрузим готовый конвейер аудиодиффузионной модели:

# Загружаем предобученный конвейер аудиодиффузии
device = "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu"
pipe = DiffusionPipeline.from_pretrained("teticio/audio-diffusion-instrumental-hiphop-256").to(device)

(Примечание переводчика: модель teticio/audio-diffusion-instrumental-hiphop-256 обучена на инструментальном хип-хопе. Ноутбук писался под diffusers 0.12 (версия видна в выводе pipe.mel ниже). С тех пор конвейер AudioDiffusion перенесён в раздел устаревших (deprecated) конвейеров diffusers и больше не развивается. Если на свежей версии библиотеки загрузка или вызов падает, проще всего поставить в отдельное окружение старую версию diffusers.)

Как и с конвейерами из предыдущих юнитов, сэмплы создаются вызовом конвейера:

# Сэмплируем из конвейера и показываем результаты
output = pipe()
display(output.images[0])
display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate()))

(В выводе — серая картинка-спектрограмма 256×256 и аудиоплеер. Конвейер делает 1000 шагов DDPM, так что генерация занимает заметное время даже на GPU.)

Здесь аргумент rate задаёт частоту дискретизации (sampling rate) звука; подробнее мы разберём её ниже. Вы также заметите, что конвейер возвращает несколько вещей. Что тут происходит? Посмотрим внимательнее на оба выхода.

Первый — массив данных, представляющий сгенерированный звук:

# Массив звука
output.audios[0].shape

Вывод:

(1, 130560)

Второй похож на изображение в оттенках серого:

# Выходное изображение (спектрограмма)
output.images[0].size

Вывод:

(256, 256)

Это подсказывает, как работает конвейер. Звук генерируется диффузией не напрямую: в конвейере та же двумерная U-Net, что и в конвейерах безусловной генерации изображений из юнита 1 (перевод). Она генерирует спектрограмму, а та затем постобработкой превращается в итоговый звук.

У конвейера есть дополнительный компонент, который отвечает за эти преобразования; доступ к нему — через pipe.mel:

pipe.mel

Вывод:

Mel {
"_class_name": "Mel",
"_diffusers_version": "0.12.0.dev0",
"hop_length": 512,
"n_fft": 2048,
"n_iter": 32,
"sample_rate": 22050,
"top_db": 80,
"x_res": 256,
"y_res": 256
}

Звуковая «волна» (waveform) кодирует сырые отсчёты звука во времени — например, это может быть электрический сигнал с микрофона. С таким представлением во «временной области» (time domain) работать бывает неудобно, поэтому его принято переводить в другую форму, обычно в так называемую спектрограмму. Спектрограмма показывает интенсивность разных частот (ось y) во времени (ось x):

# Вычисляем и показываем спектрограмму нашего сгенерированного звука средствами torchaudio
spec_transform = AT.Spectrogram(power=2)
spectrogram = spec_transform(torch.tensor(output.audios[0]))
print(spectrogram.min(), spectrogram.max())
log_spectrogram = spectrogram.log()
plt.imshow(log_spectrogram[0], cmap='gray');

Вывод:

tensor(0.) tensor(6.0842)

Значения у спектрограммы, которую мы только что построили, лежат между 0.0000000000001 и 1, причём большинство — у нижнего края этого диапазона. Это неудобно и для визуализации, и для моделирования: нам даже пришлось взять логарифм этих значений, чтобы на сером графике была видна хоть какая-то детализация. Поэтому обычно используют особый вид спектрограммы — мел-спектрограмму (Mel spectrogram). Она устроена так, чтобы сохранять информацию, важную для человеческого слуха: для этого к разным частотным составляющим сигнала применяются определённые преобразования.

(Примечание переводчика: в сохранённом выводе ячейки минимум 0, а максимум около 6 — числа в тексте, видимо, остались от другого запуска. Суть та же: почти все значения крошечные, а разброс огромный, поэтому и берут логарифм.)

(В оригинале — схема из документации torchaudio с подписью «Некоторые преобразования звука из документации torchaudio»: как из звуковой волны получают спектрограмму, мел-спектрограмму и другие признаки.)

flowchart LR
W["Звуковая волна<br/>22 050 отсчётов/с"] -->|"окна по 2048, сдвиг 512<br/>преобразование Фурье"| S["Спектрограмма<br/>частота × время"]
S -->|"мел-шкала<br/>+ логарифм (дБ)"| M["Мел-спектрограмма<br/>картинка 256×256"]
M -->|"U-Net учится<br/>на этих картинках"| G["Сгенерированная<br/>спектрограмма"]
G -->|"обратная мел-шкала<br/>+ Гриффин — Лим (фаза)"| A["Звук"]
Как конвейер переходит от звука к картинке и обратно (рисунок переводчика)

К счастью, об этих преобразованиях можно особо не беспокоиться: функциональность mel конвейера берёт эти детали на себя. С её помощью спектрограмму-изображение можно превратить в звук так:

a = pipe.mel.image_to_audio(output.images[0])
a.shape

Вывод:

(130560,)

А чтобы превратить массив аудиоданных в изображения-спектрограммы, сначала загружаем сырые аудиоданные, затем вызываем функцию audio_slice_to_image(). Более длинные клипы автоматически режутся на куски нужной длины, чтобы получалась спектрограмма 256×256:

pipe.mel.load_audio(raw_audio=a)
im = pipe.mel.audio_slice_to_image(0)
im

Звук представлен длинным массивом чисел. Чтобы проиграть его вслух, нужна ещё одна ключевая величина — частота дискретизации. Сколько отсчётов (отдельных значений) мы используем, чтобы представить одну секунду звука?

Частоту дискретизации, с которой обучался этот конвейер, можно посмотреть так:

sample_rate_pipeline = pipe.mel.get_sample_rate()
sample_rate_pipeline

Вывод:

22050

Если указать частоту дискретизации неправильно, звук получится ускоренным или замедленным:

display(Audio(output.audios[0], rate=44100)) # скорость ×2

Теперь, когда мы примерно понимаем, как работает конвейер, дообучим его на новых аудиоданных!

Датасет — коллекция аудиоклипов разных жанров; загрузить его с Hub можно так:

from datasets import load_dataset
dataset = load_dataset('lewtun/music_genres', split='train')
dataset

Вывод:

Dataset({
features: ['audio', 'song_id', 'genre_id', 'genre'],
num_rows: 19909
})

(Примечание переводчика: это почти 20 тысяч 30-секундных клипов — скачивание займёт время и место на диске.)

Код ниже показывает, какие жанры есть в датасете и сколько в каждом примеров:

for g in list(set(dataset['genre'])):
print(g, sum(x==g for x in dataset['genre']))

Вывод:

Pop 945
Blues 58
Punk 2582
Old-Time / Historic 408
Experimental 1800
Folk 1214
Electronic 3071
Spoken 94
Classical 495
Country 142
Instrumental 1044
Chiptune / Glitch 1181
International 814
Ambient Electronic 796
Jazz 306
Soul-RnB 94
Hip-Hop 1757
Easy Listening 13
Rock 3095

Звук в датасете хранится массивами:

audio_array = dataset[0]['audio']['array']
sample_rate_dataset = dataset[0]['audio']['sampling_rate']
print('Audio array shape:', audio_array.shape)
print('Sample rate:', sample_rate_dataset)
display(Audio(audio_array, rate=sample_rate_dataset))

Вывод:

Audio array shape: (1323119,)
Sample rate: 44100

(1 323 119 отсчётов при 44 100 Гц — это 30 с звука.)

Обратите внимание: частота дискретизации у этого звука выше, и чтобы использовать готовый конвейер, звук нужно «передискретизировать» (resample) под него. Кроме того, клипы длиннее тех, на которые рассчитан конвейер. К счастью, когда мы загружаем звук через pipe.mel, он автоматически режет клип на куски поменьше:

a = dataset[0]['audio']['array'] # Берём массив звука
pipe.mel.load_audio(raw_audio=a) # Загружаем его в pipe.mel
pipe.mel.audio_slice_to_image(0) # Смотрим на первый «кусок» как на спектрограмму

Нужно не забыть поправить частоту дискретизации: в этом датасете отсчётов в секунду вдвое больше:

sample_rate_dataset = dataset[0]['audio']['sampling_rate']
sample_rate_dataset

Вывод:

44100

Здесь мы используем преобразования torchaudio (импортированы как AT), чтобы передискретизировать звук, mel конвейера — чтобы превратить звук в изображение, и преобразования torchvision (импортированы как IT) — чтобы превратить изображения в тензоры. Так получается функция, которая превращает аудиоклип в тензор спектрограммы для обучения:

resampler = AT.Resample(sample_rate_dataset, sample_rate_pipeline, dtype=torch.float32)
to_t = IT.ToTensor()
def to_image(audio_array):
audio_tensor = torch.tensor(audio_array).to(torch.float32)
audio_tensor = resampler(audio_tensor)
pipe.mel.load_audio(raw_audio=np.array(audio_tensor))
num_slices = pipe.mel.get_number_of_slices()
slice_idx = random.randint(0, num_slices-1) # Каждый раз берём случайный кусок (кроме последнего, короткого)
im = pipe.mel.audio_slice_to_image(slice_idx)
return im

Функцию to_image() мы используем внутри своей функции collate, чтобы превратить датасет в загрузчик данных для обучения. Функция collate определяет, как превратить батч примеров из датасета в итоговый батч данных, готовый к обучению. В нашем случае мы превращаем каждый аудиопример в изображение-спектрограмму и складываем получившиеся тензоры в стопку:

def collate_fn(examples):
# в изображение -> в тензор -> масштабируем в (-1, 1) -> складываем в батч
audio_ims = [to_t(to_image(x['audio']['array']))*2-1 for x in examples]
return torch.stack(audio_ims)
# Создаём датасет только из песен жанра 'Chiptune / Glitch'
batch_size = 4 # 4 в Colab, 12 на A100
chosen_genre = 'Electronic' # <<< Попробуйте обучить на разных жанрах <<<
indexes = [i for i, g in enumerate(dataset['genre']) if g == chosen_genre]
filtered_dataset = dataset.select(indexes)
dl = torch.utils.data.DataLoader(filtered_dataset.shuffle(), batch_size=batch_size, collate_fn=collate_fn, shuffle=True)
batch = next(iter(dl))
print(batch.shape)

Вывод:

torch.Size([4, 1, 256, 256])

(Примечание переводчика: комментарий про ‘Chiptune / Glitch’ остался в оригинале от прошлой версии, на деле выбран жанр 'Electronic'. Форма батча: 4 спектрограммы, 1 канал (оттенки серого), 256×256.)

NB: если у вас не очень много видеопамяти GPU, понадобится небольшой размер батча (например, 4).

Вот простой цикл обучения, который несколько эпох проходит по загрузчику данных и дообучает U-Net конвейера. Эту ячейку можно пропустить и загрузить конвейер кодом из следующей ячейки.

epochs = 3
lr = 1e-4
pipe.unet.train()
pipe.scheduler.set_timesteps(1000)
optimizer = torch.optim.AdamW(pipe.unet.parameters(), lr=lr)
for epoch in range(epochs):
for step, batch in tqdm(enumerate(dl), total=len(dl)):
# Готовим входные изображения
clean_images = batch.to(device)
bs = clean_images.shape[0]
# Для каждого изображения берём случайный шаг
timesteps = torch.randint(
0, pipe.scheduler.num_train_timesteps, (bs,), device=clean_images.device
).long()
# Добавляем к чистым изображениям шум той величины, которая соответствует каждому шагу
noise = torch.randn(clean_images.shape).to(clean_images.device)
noisy_images = pipe.scheduler.add_noise(clean_images, noise, timesteps)
# Получаем предсказание модели
noise_pred = pipe.unet(noisy_images, timesteps, return_dict=False)[0]
# Считаем функцию потерь
loss = F.mse_loss(noise_pred, noise)
loss.backward(loss)
# Обновляем параметры модели оптимизатором
optimizer.step()
optimizer.zero_grad()

(Примечание переводчика: та же ошибка, что в юнитах 1 и 2, — loss.backward(loss) умножает градиенты на значение loss; пишите loss.backward(). И pipe.scheduler.num_train_timesteps в новых версиях diffusers лучше писать как pipe.scheduler.config.num_train_timesteps. Объём работы: в жанре Electronic 3071 клип, при батче 4 это 768 шагов на эпоху, за 3 эпохи — около 2300 шагов. Каждый батч ещё и готовится на CPU (передискретизация и мел-спектрограмма), поэтому обучение займёт заметное время.)

# ИЛИ: загружаем версию, которую я обучил заранее
pipe = DiffusionPipeline.from_pretrained("johnowhitaker/Electronic_test").to(device)
output = pipe()
display(output.images[0])
display(Audio(output.audios[0], rate=22050))
# Делаем сэмпл подлиннее: подаём стартовый шумовой тензор другой формы
noise = torch.randn(1, 1, pipe.unet.sample_size[0], pipe.unet.sample_size[1]*4).to(device)
output = pipe(noise=noise)
display(output.images[0])
display(Audio(output.audios[0], rate=22050))

(В выводе — спектрограмма 1024×256, то есть клип вчетверо длиннее, примерно 24 с. U-Net в основном свёрточная, поэтому принимает вход другой ширины, хотя обучалась на квадратах 256×256.)

Звучит не слишком впечатляюще, но для начала неплохо :) Поэкспериментируйте со скоростью обучения и числом эпох и делитесь лучшими результатами в Discord, чтобы мы улучшали их вместе!

Над чем стоит подумать:

  • Мы работаем с квадратными спектрограммами 256 пикселей, и это ограничивает размер батча. Удастся ли восстановить звук достаточного качества из спектрограммы 128×128?
  • Вместо случайной аугментации изображений мы каждый раз берём разные куски аудиоклипа. Но, может быть, при обучении на много эпох это можно улучшить другими видами аугментации?
  • Как ещё можно генерировать этим способом более длинные клипы? Например, сгенерировать начальный клип на 5 с, а затем, вдохновившись идеями inpainting, генерировать дополнительные фрагменты звука, продолжающие начальный клип…
  • Что в контексте диффузии на спектрограммах будет аналогом image-to-image?

Когда модель вас устроит, её можно сохранить и выгрузить на Hub, чтобы и другие могли ею насладиться:

from huggingface_hub import get_full_repo_name, HfApi, create_repo, ModelCard
# Выбираем имя для модели
model_name = "audio-diffusion-electronic"
hub_model_id = get_full_repo_name(model_name)
# Сохраняем конвейер локально
pipe.save_pretrained(model_name)
# Смотрим содержимое папки
!ls {model_name}

Вывод:

mel model_index.json scheduler unet
# Создаём репозиторий
create_repo(hub_model_id)
# Выгружаем файлы
api = HfApi()
api.upload_folder(
folder_path=f"{model_name}/scheduler", path_in_repo="scheduler", repo_id=hub_model_id
)
api.upload_folder(
folder_path=f"{model_name}/mel", path_in_repo="mel", repo_id=hub_model_id
)
api.upload_folder(folder_path=f"{model_name}/unet", path_in_repo="unet", repo_id=hub_model_id)
api.upload_file(
path_or_fileobj=f"{model_name}/model_index.json",
path_in_repo="model_index.json",
repo_id=hub_model_id,
)
# Выгружаем карточку модели
content = f"""
---
license: mit
tags:
- pytorch
- diffusers
- unconditional-audio-generation
- diffusion-models-class
---
# Model Card for Unit 4 of the [Diffusion Models Class 🧨](https://github.com/huggingface/diffusion-models-class)
This model is a diffusion model for unconditional audio generation of music in the genre {chosen_genre}
## Usage
<pre>
from IPython.display import Audio
from diffusers import DiffusionPipeline
pipe = DiffusionPipeline.from_pretrained("{hub_model_id}")
output = pipe()
display(output.images[0])
display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate()))
</pre>
"""
card = ModelCard(content)
card.push_to_hub(hub_model_id)

(Текст карточки по-русски: «Карточка модели для юнита 4 курса Diffusion Models Class 🧨. Это диффузионная модель для безусловной генерации музыки в жанре chosen_genre. Использование: …» — и пример кода загрузки. Карточку обычно оставляют на английском, чтобы модель нашли пользователи Hub.)

(Примечание переводчика: перед выгрузкой войдите в аккаунт Hugging Face с токеном с правом записи — например, from huggingface_hub import notebook_login; notebook_login() в ноутбуке или huggingface-cli login в терминале (в новых версиях библиотеки — hf auth login).)

Надеемся, этот ноутбук дал вам немного почувствовать, какие возможности открывает генерация звука. Загляните в материалы по ссылкам из введения к этому юниту: там более изощрённые методы и поразительные сэмплы, которые они создают!

EnglishРусский
progressive distillation, teacher, studentпрогрессивная дистилляция, учитель, ученик
noise schedule, loss weightingрасписание шума, взвешивание функции потерь
cascaded diffusion, super-resolutionкаскадная диффузия, сверхразрешение
mixture of denoising experts (MoDE)смесь экспертов-денойзеров
knowledge enhancementобогащение знаниями
inversion, DDIM inversionинверсия, инверсия DDIM
null-text inversionинверсия с оптимизацией «пустого» текста
cross-attention controlуправление перекрёстным вниманием
inpainting, maskдорисовка (inpainting), маска
deterministic / stochastic samplerдетерминированный / стохастический сэмплер
predicted x0x_0, direction pointing to xtx_tпредсказанный x0x_0, направление, указывающее на xtx_t
iterative refinement, corruptionитеративное уточнение, «порча» данных
masked token modelingгенерация через маскирование токенов
Diffusion Transformer (DiT)диффузионный трансформер
waveform, time domainзвуковая волна, временная область
spectrogram, mel spectrogramспектрограмма, мел-спектрограмма
sample rate, resamplingчастота дискретизации, передискретизация
Griffin-Limалгоритм Гриффина — Лима (восстановление фазы)
collate function, dataloaderфункция collate (сборка батча), загрузчик данных
model cardкарточка модели