Диффузионные модели — кратко
Как нейросеть рисует картинки и видео из чистого шума — от идеи DDPM и игрушечной модели на PyTorch до латентной диффузии, DiT и китайских генераторов Wan, HunyuanVideo и Qwen-Image.
Зачем это нужно
Заголовок раздела «Зачем это нужно»LLM генерирует текст авторегрессионно: токен за токеном, слева направо. Картинки и видео в 2026 году почти всегда генерируют иначе — диффузионными моделямидиффузионная модельEN diffusion model. Они начинают с картинки из чистого шума и за десятки шагов «проявляют» её, как фотографию. Так работают Stable Diffusion, Sora, китайские Wan, Kling, HunyuanVideo, Qwen-Image.
Для LLM-инженера это важно по трём причинам. Во-первых, мультимодальные продукты почти всегда склеивают VLM из прошлого урока (понимание) и диффузию (генерацию). Во-вторых, современный денойзер — это трансформер по патчам, то есть всё, что вы знаете о внимании и KV-кэше, переносится напрямую. В-третьих, диффузия пробралась и в текст: есть языковые модели, которые генерируют весь ответ сразу, постепенно «расшумляя» его. Урок — краткий: интуиция, формулы DDPM, работающий код и карта современных моделей.
Интуиция: снести дом и построить заново
Заголовок раздела «Интуиция: снести дом и построить заново»Су Цзяньлинь (автор RoPE) объясняет DDPM метафорой «снос + стройка» — «снести здание и построить заново».
- Прямой процесс (снос): берём настоящую картинку и шаг за шагом добавляем немного гауссова шума. Через шагов от картинки не остаётся ничего — только шум. Этот процесс фиксирован, учить тут нечего.
- Обратный процесс (стройка): учим нейросеть делать один маленький шаг назад — убирать чуть-чуть шума. Повторив этот шаг 1000 раз, начиная с чистого шума, получаем новую картинку.
Почему не одним прыжком «шум → картинка»? Потому что это безнадёжно трудная задача: из шума можно получить что угодно. А убрать немного шума с почти чистой картинки — задача простая и почти однозначная. Диффузия разбивает невозможную задачу на тысячу простых — так же, как LLM разбивает генерацию текста на простые шаги «предскажи следующий токен».
flowchart LR X0["x₀: картинка"] -->|"+ немного шума"| X1["x₁"] -->|"+ шум"| X2["…"] -->|"+ шум"| XT["x_T: чистый шум"] XT -.->|"нейросеть убирает шум"| R2["…"] -.-> R1["x₁"] -.-> R0["x₀: новая картинка"]
Прямой процесс: формулы
Заголовок раздела «Прямой процесс: формулы»Один шаг зашумления в DDPMDDPM (вероятностная диффузионная модель с шумоподавлением)EN Denoising Diffusion Probabilistic Model (DDPM) (Ho и соавторы, 2020):
Что здесь написано: немного «приглушаем» картинку (множитель чуть меньше 1) и добавляем немного шума. Запись значит «стандартный нормальный шум» (урок 1.3.1): массив той же формы, что картинка, где каждое число независимо взято из нормального распределения со средним 0 и дисперсией 1 ( — единичная матрица, то есть числа не связаны между собой). Числа — это расписание шумарасписание шумаEN noise scheduleсколько шума добавлять на каждом шаге прямого процесса (β₁…β_T); в статье они растут линейно от до . Зачем корень из ? Чтобы дисперсия оставалась равной 1. Два правила из теории вероятностей: при умножении величины на её дисперсия умножается на , а дисперсии независимых слагаемых складываются. Поэтому если у дисперсия 1, то у она . Картинка не «взрывается» и не гаснет — она плавно заменяется шумом.
Главный трюк — не нужно делать шагов, чтобы получить . Обозначим и . Сумма гауссовых шумов — снова гауссов шум, и получается формула «прыжка»:
— сколько осталось от картинки, — сколько в ней шума. Посчитаем для расписания из статьи:
import torch
T = 1000betas = torch.linspace(1e-4, 0.02, T) # линейное расписание из статьи DDPMalphas = 1 - betasalpha_bar = torch.cumprod(alphas, dim=0) # ᾱ_t = α_1 · α_2 · … · α_t
print(" t ᾱ_t √ᾱ_t (сигнал) √(1−ᾱ_t) (шум)")for t in [1, 50, 100, 250, 500, 750, 1000]: a = alpha_bar[t - 1] print(f"{t:4d} {a:.5f} {a.sqrt():.4f} {(1 - a).sqrt():.4f}") t ᾱ_t √ᾱ_t (сигнал) √(1−ᾱ_t) (шум) 1 0.99990 0.9999 0.0100 50 0.97102 0.9854 0.1702 100 0.89702 0.9471 0.3209 250 0.52409 0.7239 0.6899 500 0.07859 0.2803 0.9599 750 0.00335 0.0579 0.99831000 0.00004 0.0064 1.0000К шагу 250 коэффициенты сигнала и шума примерно равны, к шагу 500 картинку едва видно (коэффициент сигнала 0.28 против
0.96 у шума), к шагу 1000 остался чистый шум. torch.cumprod — накопленное произведение: та же идея, что cumsum, только с умножением.
Обратный процесс и обучение
Заголовок раздела «Обратный процесс и обучение»Что учит нейросеть
Заголовок раздела «Что учит нейросеть»Нейросеть получает зашумлённую картинку и номер шага, а предсказывает шум, который был добавлен. Зная шум, легко получить и картинку: из формулы прыжка . Предсказывать именно шум удобно: это всегда стандартная гауссова величина одного масштаба, на любом шаге.
Обучение (Algorithm 1 из статьи) — до смешного короткое:
- Берём настоящий пример из данных.
- Выбираем случайный шаг от 1 до и случайный шум .
- Прыгаем: .
- Минимизируем — обычную MSE из урока 2.2.1.
Сравните с LLM: там тоже «испортили» вход (спрятали следующий токен) и учим восстановить. Это самообучениесамообучение, самоконтролируемое обучениеEN self-supervised learningМетки берутся из самих данных (например, следующий токен текста). Так идёт предобучение LLM.: разметка не нужна, данные сами себе учитель.
Генерация
Заголовок раздела «Генерация»Сэмплирование (Algorithm 2) идёт от к :
Словами: вычитаем предсказанный шум (с нужным весом), немного масштабируем и добавляем свежий маленький шум (в простейшем варианте ; на последнем шаге шум не добавляют). Свежий шум делает генерацию случайной: из одного и того же получаются разные картинки — как сэмплирование с температурой в LLM.
Практика: DDPM на PyTorch за полминуты
Заголовок раздела «Практика: DDPM на PyTorch за полминуты»Возьмём данные попроще картинок: точки на окружности радиуса 1. «Картинка» здесь — всего 2 числа , зато весь алгоритм ровно тот же. Модель должна научиться из гауссова шума делать точки на кольце.
import mathimport torchimport torch.nn as nn
torch.manual_seed(0)
# ── Данные: точки на окружности радиуса 1 (наша «картинка» — всего 2 числа) ──def sample_data(n): angle = torch.rand(n) * 2 * math.pi r = 1 + 0.05 * torch.randn(n) return torch.stack([r * torch.cos(angle), r * torch.sin(angle)], dim=1)
# ── Расписание шума (как в DDPM, но T = 200 шагов, чтобы всё шло быстро) ──T = 200betas = torch.linspace(1e-4, 0.05, T)alphas = 1 - betasalpha_bar = torch.cumprod(alphas, dim=0)
# ── Модель: MLP, который по (x_t, t) предсказывает шум ε ──class EpsModel(nn.Module): def __init__(self, h=128): super().__init__() self.net = nn.Sequential( nn.Linear(2 + 16, h), nn.SiLU(), nn.Linear(h, h), nn.SiLU(), nn.Linear(h, 2), )
def forward(self, x, t): # синусоидальное кодирование шага t — тот же приём, что позиции в трансформере freqs = torch.exp(-math.log(1000) * torch.arange(8) / 8) ang = t[:, None].float() * freqs[None, :] t_emb = torch.cat([ang.sin(), ang.cos()], dim=1) return self.net(torch.cat([x, t_emb], dim=1))
model = EpsModel()opt = torch.optim.Adam(model.parameters(), lr=2e-3)
# ── Обучение: зашумили → предсказали шум → MSE ──for step in range(3001): x0 = sample_data(512) t = torch.randint(0, T, (512,)) eps = torch.randn_like(x0) ab = alpha_bar[t][:, None] xt = ab.sqrt() * x0 + (1 - ab).sqrt() * eps # прыжок сразу на шаг t loss = ((model(xt, t) - eps) ** 2).mean() opt.zero_grad(); loss.backward(); opt.step() if step % 1000 == 0: print(f"шаг {step:4d} loss {loss.item():.3f}")
# ── Генерация: от чистого шума назад, шаг за шагом ──@torch.no_grad()def sample(n): x = torch.randn(n, 2) for t in reversed(range(T)): tt = torch.full((n,), t) eps = model(x, tt) x = (x - betas[t] / (1 - alpha_bar[t]).sqrt() * eps) / alphas[t].sqrt() if t > 0: x = x + betas[t].sqrt() * torch.randn_like(x) # σ_t = √β_t return x
noise = torch.randn(2000, 2)gen = sample(2000)for name, pts in [("чистый шум", noise), ("сгенерировано", gen), ("настоящие данные", sample_data(2000))]: r = pts.norm(dim=1) print(f"{name:17s} радиус: среднее {r.mean():.3f}, разброс {r.std():.3f}")шаг 0 loss 0.951шаг 1000 loss 0.293шаг 2000 loss 0.277шаг 3000 loss 0.267чистый шум радиус: среднее 1.252, разброс 0.659сгенерировано радиус: среднее 0.989, разброс 0.095настоящие данные радиус: среднее 0.998, разброс 0.049На свободном 4-ядерном CPU это занимает около полуминуты (на загруженной машине — несколько минут). Как читать результат:
- Loss не стремится к нулю — и не должен. На сильно зашумлённых шагах шум принципиально невозможно угадать точно. Важно, что loss упал с ≈1 (модель отвечает «ноль», MSE с гауссовым шумом равна 1) до ≈0.27.
- Радиус сгенерированных точек ≈0.99 при радиусе данных 1.0, хотя начинали с шума со средним радиусом 1.25 и огромным разбросом. Модель выучила кольцо. Разброс 0.095 вдвое больше настоящего — маленькой сети и 3000 шагов не хватает на точную «толщину» кольца.
- Нарисуйте точки (
plt.scatter(gen[:, 0], gen[:, 1], s=2), урок 0.3.3) и сохранитеxкаждые 50 шагов генерации — увидите, как облако шума «стягивается» в кольцо.
Ваши числа могут немного отличаться на другой версии PyTorch. Типичные ошибки в шаге генерации ведут себя по-разному — мы проверили их на этой же обученной модели:
- перепутан знак перед предсказанным шумом — видно сразу: радиус «улетает» в тысячи и больше;
- забыто деление на
alphas[t].sqrt()— радиус ≈0.97 вместо 0.99, почти незаметно. Такие ошибки числами не ловятся, поэтому сверяйте код с формулой построчно; - убран свежий шум — точки сжимаются внутрь кольца (средний радиус ≈0.89): без шума генерация «усредняет» и теряет разброс.
Обратите внимание и на расписание: у нас , то есть в осталось лишь ≈8% амплитуды данных — почти чистый шум, с которого и стартует генерация. Если уменьшить последний до , получится : в останется 60% сигнала. Генерация же стартует с чистого , то есть модель получит на вход не то, на чём училась, — и кольцо выйдет хуже.
От игрушки к Stable Diffusion: латентная диффузия
Заголовок раздела «От игрушки к Stable Diffusion: латентная диффузия»Картинка 512×512 — это числа, и тысяча шагов денойзера по такому массиву стоит дорого. При этом большая часть пикселей — мелкая текстура, которую нет смысла «придумывать» диффузией.
Латентная диффузиялатентная диффузияEN latent diffusionдиффузия идёт не по пикселям, а в сжатом пространстве VAE (Stable Diffusion) (Ромбах и соавторы, 2021 — это Stable Diffusion) делит работу на две части:
- Вариационный автокодировщик (VAE)вариационный автокодировщик (VAE)EN variational autoencoder (VAE) учится сжимать картинку в латент и разжимать обратно. В Stable Diffusion 512×512×3 → 64×64×4: чисел, в 48 раз меньше. Как устроен VAE и почему его латент удобен для диффузии — в уроке 9.1.3.
- Диффузия работает в латенте. В конце декодер VAE превращает латент в пиксели.
Текстовый промпт подаётся в денойзер через перекрёстное вниманиеперекрёстное внимание (cross-attention)EN cross-attentionЗапросы из одной последовательности, ключи и значения — из другой (декодер смотрит на выход энкодера). Ср. self-attention, где все три из одной последовательности. (урок 4.5.1): запросы — из латента, ключи и значения — из эмбеддингов текстового энкодера. В первых версиях Stable Diffusion это был текстовый энкодер CLIP из прошлого урока — вот где он пригодился.
Classifier-free guidance: «сильнее слушай промпт»
Заголовок раздела «Classifier-free guidance: «сильнее слушай промпт»»Если просто учить модель с условием, картинки получаются «вялыми» — промпт учитывается слабо. Classifier-free guidanceguidance без классификатора (CFG)EN classifier-free guidance (CFG) (Хо и Салиманс, 2022) решает это простым трюком:
- при обучении в части примеров (обычно 10–20%) условие выбрасывают — модель учится и без промпта;
- при генерации делают два прогона и экстраполируют:
Разность — это «что промпт добавляет к картинке». При получаем обычную
условную модель, при усиливаем влияние промпта. Пример на одном числе: ,
, → . Цена — два прогона денойзера на шаг и
риск пересвеченных, «пластиковых» картинок при слишком большом . В README Wan2.1 пример команды использует
--sample_guide_scale 6.
DiT: трансформер вместо U-Net
Заголовок раздела «DiT: трансформер вместо U-Net»Долгое время денойзером служила U-NetU-NetEN U-Netсвёрточная сеть «сжать — разжать» с обходными связями; классический денойзер диффузии до DiT — свёрточная сеть «сжать — разжать» со skip-связями (её устройство и код — в уроке 3.5.2). В 2022 году Уильям Пиблс и Се Сайнин показали, что её можно заменить трансформером, — так появился DiT (Diffusion Transformer)диффузионный трансформер (DiT)EN Diffusion Transformer (DiT). Устройство вам уже знакомо по ViT:
- Латент 32×32×4 (картинка 256×256 после VAE) режется на патчи 2×2 → токенов.
- Токены идут через блоки трансформера.
- Номер шага и класс (или текст) подмешиваются через adaLN-Zero: LayerNorm, у которого масштаб и сдвиг предсказываются из условия, а выход блока в начале обучения умножается на ноль — блок стартует как тождественное отображение (вспомните остаточные связи, урок 3.4.2).
Главный результат статьи — DiT масштабируется, как языковые модели: больше вычислений на шаг → стабильно лучше качество. Лучшая модель DiT-XL/2 достигла FID 2.27 на ImageNet 256×256 — рекорд на тот момент (FID — Fréchet Inception Distance — мера того, насколько распределение сгенерированных картинок отличается от распределения настоящих; чем меньше, тем лучше). С тех пор большие генераторы картинок и видео почти все строятся на DiT и его вариантах (MMDiT у Stable Diffusion 3 и Qwen-Image: токены текста и картинки обрабатываются разными весами, но «смотрят» друг на друга в общем внимании). Вместо классической DDPM их всё чаще учат через flow matchingflow matching (сопоставление потоков)EN flow matching: модель предсказывает не шум, а скорость движения по прямой от шума к данным. Идея та же — много простых шагов от шума к картинке; подробно — в уроке 9.1.4.
Видео: сколько токенов в пяти секундах
Заголовок раздела «Видео: сколько токенов в пяти секундах»Видео — это картинка плюс ось времени. 3D-VAE сжимает и пространство, и время: у HunyuanVideo коэффициенты 4 по времени, 8×8 по пространству и 16 каналов латента. Посчитаем для 5 секунд 720p:
# Сколько чисел и токенов в видео до и после 3D-VAE (сжатие 4 по времени, 8×8 по пространству, 16 каналов)frames, H, W = 121, 720, 1280 # 5 секунд при 24 к/с (+1 первый кадр)pixels = frames * 3 * H * Wlat_t, lat_h, lat_w = (frames - 1) // 4 + 1, H // 8, W // 8latent = lat_t * 16 * lat_h * lat_wtokens = lat_t * (lat_h // 2) * (lat_w // 2) # DiT режет латент на кубики 1×2×2print(f"пикселей: {pixels:,}")print(f"латент: {lat_t}×16×{lat_h}×{lat_w} = {latent:,} чисел (сжатие в {pixels / latent:.0f} раз)")print(f"токенов для DiT: {tokens:,}")print(f"пар в матрице внимания: {tokens**2:.2e}")img_px, img_lat = 512 * 512 * 3, 64 * 64 * 4print(f"Stable Diffusion: {img_px:,} → {img_lat:,} (в {img_px // img_lat} раз)")пикселей: 334,540,800латент: 31×16×90×160 = 7,142,400 чисел (сжатие в 47 раз)токенов для DiT: 111,600пар в матрице внимания: 1.25e+10Stable Diffusion: 786,432 → 16,384 (в 48 раз)Пять секунд видео — это ≈112 тысяч токенов, и каждый из десятков шагов денойзера гоняет по ним полное внимание. Поэтому видео-генерация — один из главных потребителей разреженного внимания (урок 5.5.2) и параллелизма по последовательности: длинную последовательность токенов делят между несколькими GPU, а для внимания карты обмениваются нужными кусками данных (родственник видов параллелизма из урока 5.4.3). Кубик 1×2×2 — типичный выбор, но точная нарезка у разных моделей своя.
Китайские image- и video-модели (обзорно)
Заголовок раздела «Китайские image- и video-модели (обзорно)»| Модель | Кто | Когда | Что важно |
|---|---|---|---|
| U-ViT → Vidu | Университет Цинхуа, затем ShengShu Technology | 2022 → 04.2024 | U-ViT — один из первых ViT-денойзеров (одновременно с DiT): все входы, включая время и условие, — токены, плюс длинные обходные связи. По словам разработчиков, на этой архитектуре построена видеомодель Vidu |
| Kling | Kuaishou | 06.2024 | одна из первых сильных видеомоделей после Sora; закрытая, доступна через сервис |
| CogVideoX | Zhipu и Цинхуа | 08.2024 | 3D-VAE + «экспертный» трансформер; 10-секундные ролики 768×1360 при 16 к/с; открытые веса |
| HunyuanVideo | Tencent | 12.2024 | более 13 млрд параметров — на момент выхода крупнейшая открытая видеомодель |
| Wan 2.1 | Alibaba | 02.2025 | 14B и 1.3B; младшей нужно 8.19 ГБ видеопамяти; Apache-2.0 |
| Qwen-Image | Alibaba | 08.2025 | 20B MMDiT, Apache-2.0; сильная сторона — рендеринг текста в картинке, особенно китайских иероглифов |
| Janus-Pro | DeepSeek | 01.2025 | не диффузия: одна авторегрессионная модель и понимает, и генерирует картинки |
| Seedream, Seedance | ByteDance Seed | 2025–2026 | закрытые модели картинок и видео; доступны через продукты и API ByteDance |
Обратите внимание на рендеринг текста: нарисовать на вывеске правильные иероглифы гораздо труднее, чем латиницу, — символов тысячи, и в каждом важен каждый штрих. Это одна из областей, где китайские модели заметно вложились в данные.
Проверь себя
Заголовок раздела «Проверь себя»- Диффузия разбивает генерацию на много простых шагов: прямой процесс постепенно превращает данные в шум, нейросеть учится убирать шум по шагу. Формула прыжка позволяет учиться на любом шаге сразу.
- Обучение DDPM — это MSE между настоящим и предсказанным шумом; генерация — тысяча (или, с DDIM, полсотни) шагов от чистого шума.
- Латентная диффузия экономит вычисления в десятки раз, CFG усиливает следование промпту, DiT заменил U-Net трансформером и сделал генераторы масштабируемыми, как LLM.
- Китайские лаборатории — среди лидеров открытой генерации: Wan и Qwen-Image (Alibaba), HunyuanVideo (Tencent), CogVideoX (Zhipu), Vidu (ShengShu и Цинхуа); а LLaDA переносит диффузию в языковые модели.
Дальше: автоэнкодеры, VAE и дискретные токены изображений — как устроен латент, в котором работает диффузия, и как картинка превращается в токены для LLM. А следом, в уроке 9.1.4, — flow matching, на котором учат многие современные генераторы.