Перейти к содержимому
Ступень 99.1 Мультимодальные модели

Диффузионные модели — кратко

Как нейросеть рисует картинки и видео из чистого шума — от идеи DDPM и игрушечной модели на PyTorch до латентной диффузии, DiT и китайских генераторов Wan, HunyuanVideo и Qwen-Image.

УРОКstage-9/9-1-multimodal/02-diffusion.mdx1 ч 50 минУрок 2 из 5
После урока вы сможете
  • Объяснить прямой (зашумление) и обратный (восстановление) процессы и формулу «прыжка» xt=αˉt x0+1−αˉt εx_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\varepsilon
  • Обучить игрушечную DDPM на PyTorch за полминуты на CPU и сгенерировать новые точки
  • Объяснить, зачем нужны латентная диффузия, DiT и classifier-free guidance, и посчитать, сколько токенов «видит» видеомодель
  • Ориентироваться в китайских image- и video-моделях (Wan, HunyuanVideo, CogVideoX, Qwen-Image, Vidu)

LLM генерирует текст авторегрессионно: токен за токеном, слева направо. Картинки и видео в 2026 году почти всегда генерируют иначе — диффузионными моделямидиффузионная модельEN diffusion model. Они начинают с картинки из чистого шума и за десятки шагов «проявляют» её, как фотографию. Так работают Stable Diffusion, Sora, китайские Wan, Kling, HunyuanVideo, Qwen-Image.

Для LLM-инженера это важно по трём причинам. Во-первых, мультимодальные продукты почти всегда склеивают VLM из прошлого урока (понимание) и диффузию (генерацию). Во-вторых, современный денойзер — это трансформер по патчам, то есть всё, что вы знаете о внимании и KV-кэше, переносится напрямую. В-третьих, диффузия пробралась и в текст: есть языковые модели, которые генерируют весь ответ сразу, постепенно «расшумляя» его. Урок — краткий: интуиция, формулы DDPM, работающий код и карта современных моделей.

Су Цзяньлинь (автор RoPE) объясняет DDPM метафорой «снос + стройка» — «снести здание и построить заново».

  • Прямой процесс (снос): берём настоящую картинку и шаг за шагом добавляем немного гауссова шума. Через T=1000T = 1000 шагов от картинки не остаётся ничего — только шум. Этот процесс фиксирован, учить тут нечего.
  • Обратный процесс (стройка): учим нейросеть делать один маленький шаг назад — убирать чуть-чуть шума. Повторив этот шаг 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):

xt=1−βt  xt−1+βt  εt,εt∼N(0,I)x_t = \sqrt{1-\beta_t}\; x_{t-1} + \sqrt{\beta_t}\; \varepsilon_t, \qquad \varepsilon_t \sim \mathcal{N}(0, I)

Что здесь написано: немного «приглушаем» картинку (множитель чуть меньше 1) и добавляем немного шума. Запись εt∼N(0,I)\varepsilon_t \sim \mathcal{N}(0, I) значит «стандартный нормальный шум» (урок 1.3.1): массив той же формы, что картинка, где каждое число независимо взято из нормального распределения со средним 0 и дисперсией 1 (II — единичная матрица, то есть числа не связаны между собой). Числа β1,…,βT\beta_1, \dots, \beta_T — это расписание шумарасписание шумаEN noise scheduleсколько шума добавлять на каждом шаге прямого процесса (β₁…β_T); в статье они растут линейно от 10−410^{-4} до 0.020.02. Зачем корень из 1−βt1-\beta_t? Чтобы дисперсия оставалась равной 1. Два правила из теории вероятностей: при умножении величины на aa её дисперсия умножается на a2a^2, а дисперсии независимых слагаемых складываются. Поэтому если у xt−1x_{t-1} дисперсия 1, то у xtx_t она (1−βt)2⋅1+(βt)2⋅1=(1−βt)+βt=1(\sqrt{1-\beta_t})^2 \cdot 1 + (\sqrt{\beta_t})^2 \cdot 1 = (1-\beta_t) + \beta_t = 1. Картинка не «взрывается» и не гаснет — она плавно заменяется шумом.

Главный трюк — не нужно делать tt шагов, чтобы получить xtx_t. Обозначим αt=1−βt\alpha_t = 1 - \beta_t и αˉt=α1α2⋯αt\bar\alpha_t = \alpha_1 \alpha_2 \cdots \alpha_t. Сумма гауссовых шумов — снова гауссов шум, и получается формула «прыжка»:

xt=αˉt  x0+1−αˉt  ε,ε∼N(0,I)x_t = \sqrt{\bar\alpha_t}\; x_0 + \sqrt{1-\bar\alpha_t}\; \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, I)

αˉt\sqrt{\bar\alpha_t} — сколько осталось от картинки, 1−αˉt\sqrt{1-\bar\alpha_t} — сколько в ней шума. Посчитаем для расписания из статьи:

import torch
T = 1000
betas = torch.linspace(1e-4, 0.02, T) # линейное расписание из статьи DDPM
alphas = 1 - betas
alpha_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.9983
1000 0.00004 0.0064 1.0000

К шагу 250 коэффициенты сигнала и шума примерно равны, к шагу 500 картинку едва видно (коэффициент сигнала 0.28 против 0.96 у шума), к шагу 1000 остался чистый шум. torch.cumprod — накопленное произведение: та же идея, что cumsum, только с умножением.

Нейросеть εθ(xt,t)\varepsilon_\theta(x_t, t) получает зашумлённую картинку и номер шага, а предсказывает шум, который был добавлен. Зная шум, легко получить и картинку: из формулы прыжка x0=(xt−1−αˉt ε)/αˉtx_0 = (x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon)/\sqrt{\bar\alpha_t}. Предсказывать именно шум удобно: это всегда стандартная гауссова величина одного масштаба, на любом шаге.

Обучение (Algorithm 1 из статьи) — до смешного короткое:

  1. Берём настоящий пример x0x_0 из данных.
  2. Выбираем случайный шаг tt от 1 до TT и случайный шум ε∼N(0,I)\varepsilon \sim \mathcal{N}(0, I).
  3. Прыгаем: xt=αˉt x0+1−αˉt εx_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\varepsilon.
  4. Минимизируем ∥ε−εθ(xt,t)∥2\lVert \varepsilon - \varepsilon_\theta(x_t, t) \rVert^2 — обычную MSE из урока 2.2.1.

Сравните с LLM: там тоже «испортили» вход (спрятали следующий токен) и учим восстановить. Это самообучениесамообучение, самоконтролируемое обучениеEN self-supervised learningМетки берутся из самих данных (например, следующий токен текста). Так идёт предобучение LLM.: разметка не нужна, данные сами себе учитель.

Сэмплирование (Algorithm 2) идёт от xT∼N(0,I)x_T \sim \mathcal{N}(0, I) к x0x_0:

xt−1=1αt(xt−βt1−αˉt εθ(xt,t))+σtz,z∼N(0,I)x_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\,\varepsilon_\theta(x_t, t)\right) + \sigma_t z, \qquad z \sim \mathcal{N}(0, I)

Словами: вычитаем предсказанный шум (с нужным весом), немного масштабируем и добавляем свежий маленький шум σtz\sigma_t z (в простейшем варианте σt=βt\sigma_t = \sqrt{\beta_t}; на последнем шаге шум не добавляют). Свежий шум делает генерацию случайной: из одного и того же xTx_T получаются разные картинки — как сэмплирование с температурой в LLM.

Возьмём данные попроще картинок: точки на окружности радиуса 1. «Картинка» здесь — всего 2 числа (x,y)(x, y), зато весь алгоритм ровно тот же. Модель должна научиться из гауссова шума делать точки на кольце.

import math
import torch
import 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 = 200
betas = torch.linspace(1e-4, 0.05, T)
alphas = 1 - betas
alpha_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, почти незаметно. Такие ошибки числами не ловятся, поэтому сверяйте код с формулой построчно;
  • убран свежий шум σtz\sigma_t z — точки сжимаются внутрь кольца (средний радиус ≈0.89): без шума генерация «усредняет» и теряет разброс.

Обратите внимание и на расписание: у нас αˉ200≈0.006\bar\alpha_{200} \approx 0.006, то есть в xTx_T осталось лишь ≈8% амплитуды данных — почти чистый шум, с которого и стартует генерация. Если уменьшить последний β\beta до 0.010.01, получится αˉ200≈0.36\bar\alpha_{200} \approx 0.36: в xTx_T останется 60% сигнала. Генерация же стартует с чистого N(0,I)\mathcal{N}(0, I), то есть модель получит на вход не то, на чём училась, — и кольцо выйдет хуже.

От игрушки к Stable Diffusion: латентная диффузия

Заголовок раздела «От игрушки к Stable Diffusion: латентная диффузия»

Картинка 512×512 — это 512⋅512⋅3=786 432512 \cdot 512 \cdot 3 = 786\,432 числа, и тысяча шагов денойзера по такому массиву стоит дорого. При этом большая часть пикселей — мелкая текстура, которую нет смысла «придумывать» диффузией.

Латентная диффузиялатентная диффузияEN latent diffusionдиффузия идёт не по пикселям, а в сжатом пространстве VAE (Stable Diffusion) (Ромбах и соавторы, 2021 — это Stable Diffusion) делит работу на две части:

  1. Вариационный автокодировщик (VAE)вариационный автокодировщик (VAE)EN variational autoencoder (VAE) учится сжимать картинку в латент и разжимать обратно. В Stable Diffusion 512×512×3 → 64×64×4: 16 38416\,384 чисел, в 48 раз меньше. Как устроен VAE и почему его латент удобен для диффузии — в уроке 9.1.3.
  2. Диффузия работает в латенте. В конце декодер VAE превращает латент в пиксели.

Текстовый промпт подаётся в денойзер через перекрёстное вниманиеперекрёстное внимание (cross-attention)EN cross-attentionЗапросы из одной последовательности, ключи и значения — из другой (декодер смотрит на выход энкодера). Ср. self-attention, где все три из одной последовательности. (урок 4.5.1): запросы — из латента, ключи и значения — из эмбеддингов текстового энкодера. В первых версиях Stable Diffusion это был текстовый энкодер CLIP из прошлого урока — вот где он пригодился.

Если просто учить модель с условием, картинки получаются «вялыми» — промпт учитывается слабо. Classifier-free guidanceguidance без классификатора (CFG)EN classifier-free guidance (CFG) (Хо и Салиманс, 2022) решает это простым трюком:

  • при обучении в части примеров (обычно 10–20%) условие выбрасывают — модель учится и без промпта;
  • при генерации делают два прогона и экстраполируют:
ε~=εθ(xt,∅)+w⋅(εθ(xt,c)−εθ(xt,∅))\tilde\varepsilon = \varepsilon_\theta(x_t, \varnothing) + w \cdot \big(\varepsilon_\theta(x_t, c) - \varepsilon_\theta(x_t, \varnothing)\big)

Разность εc−ε∅\varepsilon_c - \varepsilon_\varnothing — это «что промпт добавляет к картинке». При w=1w = 1 получаем обычную условную модель, при w>1w > 1 усиливаем влияние промпта. Пример на одном числе: ε∅=0.2\varepsilon_\varnothing = 0.2, εc=0.5\varepsilon_c = 0.5, w=6w = 6 → ε~=0.2+6⋅0.3=2.0\tilde\varepsilon = 0.2 + 6 \cdot 0.3 = 2.0. Цена — два прогона денойзера на шаг и риск пересвеченных, «пластиковых» картинок при слишком большом ww. В README Wan2.1 пример команды использует --sample_guide_scale 6.

Долгое время денойзером служила U-NetU-NetEN U-Netсвёрточная сеть «сжать — разжать» с обходными связями; классический денойзер диффузии до DiT — свёрточная сеть «сжать — разжать» со skip-связями (её устройство и код — в уроке 3.5.2). В 2022 году Уильям Пиблс и Се Сайнин показали, что её можно заменить трансформером, — так появился DiT (Diffusion Transformer)диффузионный трансформер (DiT)EN Diffusion Transformer (DiT). Устройство вам уже знакомо по ViT:

  1. Латент 32×32×4 (картинка 256×256 после VAE) режется на патчи 2×2 → 16⋅16=25616 \cdot 16 = 256 токенов.
  2. Токены идут через блоки трансформера.
  3. Номер шага tt и класс (или текст) подмешиваются через 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 * W
lat_t, lat_h, lat_w = (frames - 1) // 4 + 1, H // 8, W // 8
latent = lat_t * 16 * lat_h * lat_w
tokens = lat_t * (lat_h // 2) * (lat_w // 2) # DiT режет латент на кубики 1×2×2
print(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 * 4
print(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+10
Stable Diffusion: 786,432 → 16,384 (в 48 раз)

Пять секунд видео — это ≈112 тысяч токенов, и каждый из десятков шагов денойзера гоняет по ним полное внимание. Поэтому видео-генерация — один из главных потребителей разреженного внимания (урок 5.5.2) и параллелизма по последовательности: длинную последовательность токенов делят между несколькими GPU, а для внимания карты обмениваются нужными кусками данных (родственник видов параллелизма из урока 5.4.3). Кубик 1×2×2 — типичный выбор, но точная нарезка у разных моделей своя.

МодельКтоКогдаЧто важно
U-ViT → ViduУниверситет Цинхуа, затем ShengShu Technology2022 → 04.2024U-ViT — один из первых ViT-денойзеров (одновременно с DiT): все входы, включая время и условие, — токены, плюс длинные обходные связи. По словам разработчиков, на этой архитектуре построена видеомодель Vidu
KlingKuaishou06.2024одна из первых сильных видеомоделей после Sora; закрытая, доступна через сервис
CogVideoXZhipu и Цинхуа08.20243D-VAE + «экспертный» трансформер; 10-секундные ролики 768×1360 при 16 к/с; открытые веса
HunyuanVideoTencent12.2024более 13 млрд параметров — на момент выхода крупнейшая открытая видеомодель
Wan 2.1Alibaba02.202514B и 1.3B; младшей нужно 8.19 ГБ видеопамяти; Apache-2.0
Qwen-ImageAlibaba08.202520B MMDiT, Apache-2.0; сильная сторона — рендеринг текста в картинке, особенно китайских иероглифов
Janus-ProDeepSeek01.2025не диффузия: одна авторегрессионная модель и понимает, и генерирует картинки
Seedream, SeedanceByteDance Seed2025–2026закрытые модели картинок и видео; доступны через продукты и API ByteDance

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

Проверь себя6 вопросов
1 В DDPM при t=500t = 500 имеем αˉt≈0.0786\bar\alpha_t \approx 0.0786. Какой коэффициент стоит перед x0x_0 в формуле прыжка? Округлите до двух знаков.
2 Что предсказывает нейросеть в DDPM и с каким loss её учат?
3 Почему в прямом процессе xt−1x_{t-1} умножают на 1−βt\sqrt{1-\beta_t}, а не оставляют как есть?
4 Какие утверждения верны?Можно выбрать несколько ответов
5 CFG: ε∅=0.1\varepsilon_\varnothing = 0.1, εc=0.3\varepsilon_c = 0.3, w=5w = 5. Чему равно ε~\tilde\varepsilon?
6 Видео-DiT получает латент 31×90×160 (время × высота × ширина) и режет его на кубики 1×2×2. Сколько это токенов?
  • Диффузия разбивает генерацию на много простых шагов: прямой процесс постепенно превращает данные в шум, нейросеть учится убирать шум по шагу. Формула прыжка позволяет учиться на любом шаге сразу.
  • Обучение 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, на котором учат многие современные генераторы.