Tiny Universe, часть 6: TinyDiffusion — DDPM с нуля на CIFAR-10
Полный перевод раздела TinyDiffusion из tiny-universe: формулы DDPM (прямой зашумляющий и обратный очищающий процесс), простая U-Net с временным эмбеддингом, расписание шума, обучение на MSE, выборка изображений и оценка по IS и FID.
Tiny DDPM
Заголовок раздела «Tiny DDPM»Как пользоваться (Usage)
Заголовок раздела «Как пользоваться (Usage)»pip install -r requirements.txt- Скачайте датасет CIFAR-10 в папку
datasets/, чтобы получилась структураdatasets/cifar-10-batches-py. - Обучить модель:
python ddpm/train.py - Сгенерировать изображения:
python ddpm/sample.py - Посчитать IS и FID:
python ddpm/metrics.py
Напоминание: формулы DDPM
Заголовок раздела «Напоминание: формулы DDPM»Рисунок в оригинале (fig/fig1.png) — марковская цепь DDPM: , переходы назад задаёт модель , вперёд — зашумление .
DDPM — генеративная модель на основе марковской цепи. Как показано на рисунке, прямой процесс (Forward Process) постепенно добавляет к данным гауссов шум, пока не останется чистый шум, а обратный процесс (Reverse Process) постепенно восстанавливает данные из шума.
Прямое зашумление устроено просто. А из формул прямого процесса нужно вывести формулу обратного, очищающего процесса — она и станет целью обучения модели. При самом обучении достаточно формулы обратного процесса.
Обозначения
Заголовок раздела «Обозначения»— распределение вероятностей процесса очистки, которое оценивает сеть с параметрами
— истинное распределение изображений
— исходное изображение
— изображение после шагов зашумления
Прямой процесс: зашумление
Заголовок раздела «Прямой процесс: зашумление»В прямом процессе к добавляется небольшой гауссов шум, и получается значение в следующий момент:
Здесь — заранее заданный коэффициент дисперсии, он управляет зашумлением и выбирается вручную.
Это формула (2) статьи (там она записана как распределение):
Обозначим и продолжим подстановку дальше; по свойству сложения гауссовых величин получаем формулу перехода сразу от к :
где .
Это формула (4) статьи:
Когда достаточно велико (), почти распределено как стандартное нормальное: . То есть после достаточного числа шагов зашумления изображение полностью превращается в случайный шум. Поэтому при генерации в качестве стартовой точки можно просто взять выборку из стандартного нормального распределения.
Обратный процесс: очистка от шума
Заголовок раздела «Обратный процесс: очистка от шума»При очистке нужно по текущему и с помощью модели предсказать значение в предыдущий момент. На практике это делается так: модель предсказывает шум, добавленный в прямом процессе при переходе от к :
где , а — шум прямого процесса, который модель предсказывает по и .
Это формулы (6)–(7) статьи (если подставить и воспользоваться ):
где , .
Хотя вывод DDPM довольно сложен, на деле, чтобы при инференсе получить изображение, нужна только одна формула — формула для из обратного процесса. Её можно упрощённо записать так:
где , , — известные, вычислимые константы, — шум, предсказанный моделью, — шум из стандартного нормального распределения.
Цель обучения — чтобы предсказанный моделью шум был как можно ближе к настоящему. Поэтому достаточно минимизировать среднеквадратичную ошибку (MSE) между предсказанным и настоящим шумом.
Реализация
Заголовок раздела «Реализация»1. Загрузка данных
Заголовок раздела «1. Загрузка данных»Обучаем на датасете CIFAR-10. Сначала скачайте данные в
папку datasets/ так, чтобы получилось datasets/cifar-10-batches-py.
Затем определяем преобразования данных. Для обучающей выборки — случайное отражение по горизонтали и приведение к размеру . Чтобы было удобно добавлять гауссов шум и убирать его, данные нужно перевести в диапазон . Для тестовой выборки — просто приводим к и переводим в .
train_data_transform = transforms.Compose([ transforms.Resize((img_size, img_size)), transforms.RandomHorizontalFlip(), # случайное отражение по горизонтали transforms.ToTensor(), # переводим данные в диапазон [0, 1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # переводим данные в диапазон [-1, 1]])test_data_transform = transforms.Compose([ transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),])Затем загружаем данные через torchvision.datasets.CIFAR10 и создаём DataLoader — подробности в ddpm/dataloader.py.
После этого можно написать функцию визуализации. Преобразованные данные переводим из диапазона в , меняем порядок каналов и превращаем в картинку PIL; если на входе батч — берём первую картинку.
def show_tensor_image(image): reverse_transforms = transforms.Compose([ transforms.Lambda(lambda t: (t + 1) / 2), # из [-1, 1] в [0, 1] transforms.Lambda(lambda t: t.permute(1, 2, 0)), # порядок каналов CHW → HWC transforms.Lambda(lambda t: t * 255.), # в диапазон [0, 255] transforms.Lambda(lambda t: t.numpy().astype(np.uint8)), # в тип uint8 transforms.ToPILImage(), # в картинку PIL ])
# Если на входе батч — берём первую картинку if len(image.shape) == 4: image = image[0, :, :, :] return reverse_transforms(image)Проверить, что данные загружаются правильно, можно командой python ddpm/dataloader.py.
2. Модель
Заголовок раздела «2. Модель»На вход модели DDPM подаются зашумлённое изображение и шаг времени , на выходе — предсказанный шум .
Сначала определим слой временного эмбеддинга: он вносит информацию о времени в признаки, отображая шаг в вектор большой размерности. По аналогии с позиционным кодированием в Transformer используем синусы и косинусы:
где — размерность эмбеддинга, — номер координаты.
class SinusoidalPositionEmbeddings(nn.Module): def __init__(self, dim): super().__init__() self.dim = dim
def forward(self, time): device = time.device # делим размерность пополам: одна половина для sin, другая для cos half_dim = self.dim // 2 # шаг экспоненциального убывания частот embeddings = math.log(10000) / (half_dim - 1) # последовательность частот embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings) # умножаем шаг времени на частоты embeddings = time[:, None] * embeddings[None, :] # склеиваем sin и cos — получаем итоговый эмбеддинг embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1) return embeddingsЗатем определим базовый модуль U-Net — Block — с временным эмбеддингом и повышением/понижением разрешения. Первая свёртка увеличивает число каналов, затем добавляется временной эмбеддинг, затем вторая свёртка смешивает признаки, и в конце — повышение или понижение разрешения. Мы используем упрощённую U-Net, без механизма внимания, который есть в модели исходной статьи.
class Block(nn.Module): def __init__(self, in_channels, out_channels, time_emb_dim, up=False): super().__init__() self.time_mlp = nn.Linear(time_emb_dim, out_channels) if up: self.conv1 = nn.Conv2d(2 * in_channels, out_channels, kernel_size=3, padding=1) # из-за остаточных связей U-Net при повышении разрешения приклеиваются (concat) прежние признаки, поэтому входных каналов вдвое больше self.transform = nn.ConvTranspose2d(out_channels, out_channels, kernel_size=4, stride=2, padding=1) else: self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1) self.transform = nn.Conv2d(out_channels, out_channels, kernel_size=4, stride=2, padding=1)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1) self.bnorm1 = nn.BatchNorm2d(out_channels) self.bnorm2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU()
def forward(self, x, t): # первая свёртка h = self.bnorm1(self.relu(self.conv1(x))) # временной эмбеддинг time_emb = self.relu(self.time_mlp(t)) # вносим информацию о времени в карту признаков h = h + time_emb[..., None, None] # вторая свёртка h = self.bnorm2(self.relu(self.conv2(h))) # повышение или понижение разрешения return self.transform(h)Наконец, собираем из нескольких Block модель U-Net. Сначала модель понижает разрешение, меняя число каналов 3 → 64 → 128 → 256 → 512 → 1024: каналов становится больше, карты признаков — меньше. Затем повышает разрешение с каналами 1024 → 512 → 256 → 128 → 64 → 3 (это выходные каналы; входных из-за остаточных связей вдвое больше): каналов становится меньше, карты признаков — больше. В каждый слой вносится информация о шаге времени, а на выходе получается предсказанный шум того же размера, что и входное изображение.
class SimpleUnet(nn.Module): def __init__(self): super().__init__() image_channels = 3 down_channels = (64, 128, 256, 512, 1024) up_channels = (1024, 512, 256, 128, 64) out_dim = 3 time_emb_dim = 32
# слой временного эмбеддинга self.time_embed = nn.Sequential( SinusoidalPositionEmbeddings(time_emb_dim), nn.Linear(time_emb_dim, time_emb_dim), )
# входной слой, слои понижения и повышения разрешения, выходной слой self.input = nn.Conv2d(image_channels, down_channels[0], kernel_size=3, padding=1) self.downs = nn.ModuleList([Block(down_channels[i], down_channels[i + 1], time_emb_dim) for i in range(len(down_channels) - 1)]) self.ups = nn.ModuleList([Block(up_channels[i], up_channels[i + 1], time_emb_dim, up=True) for i in range(len(up_channels) - 1)]) self.output = nn.Conv2d(up_channels[-1], out_dim, kernel_size=3, padding=1)
def forward(self, x, time_step): # эмбеддинг шага времени t = self.time_embed(time_step) # начальная свёртка x = self.input(x) # прямой проход U-Net: сначала понижаем разрешение и собираем признаки, затем повышаем разрешение residual_stack = [] for down in self.downs: x = down(x, t) residual_stack.append(x) for up in self.ups: residual_x = residual_stack.pop() x = torch.cat((x, residual_x), dim=1) x = up(x, t) return self.output(x)Командой python ddpm/unet.py можно напечатать формы входа и выхода каждого слоя U-Net и проверить, что модель
собрана правильно:
Input shape: torch.Size([1, 3, 32, 32])Time embedding shape: torch.Size([1, 32])After input conv shape: torch.Size([1, 64, 32, 32])
Downsampling process:Down block 1 output shape: torch.Size([1, 128, 16, 16])Down block 2 output shape: torch.Size([1, 256, 8, 8])Down block 3 output shape: torch.Size([1, 512, 4, 4])Down block 4 output shape: torch.Size([1, 1024, 2, 2])
Upsampling process:Concatenated input shape before up block 1: torch.Size([1, 2048, 2, 2])Up block 1 output shape: torch.Size([1, 512, 4, 4])Concatenated input shape before up block 2: torch.Size([1, 1024, 4, 4])Up block 2 output shape: torch.Size([1, 256, 8, 8])Concatenated input shape before up block 3: torch.Size([1, 512, 8, 8])Up block 3 output shape: torch.Size([1, 128, 16, 16])Concatenated input shape before up block 4: torch.Size([1, 256, 16, 16])Up block 4 output shape: torch.Size([1, 64, 32, 32])
Final output shape: torch.Size([1, 3, 32, 32])flowchart LR I["x_t<br/>3×32×32"] --> C0["conv 64×32×32"] C0 --> D1["down 128×16×16"] --> D2["down 256×8×8"] --> D3["down 512×4×4"] --> D4["down 1024×2×2"] D4 --> U1["up 512×4×4"] --> U2["up 256×8×8"] --> U3["up 128×16×16"] --> U4["up 64×32×32"] --> O["ε̂: 3×32×32"] D4 -.->|concat| U1 D3 -.->|concat| U2 D2 -.->|concat| U3 D1 -.->|concat| U4 T["шаг t → эмбеддинг 32"] --> D1 T --> U1
3. Обучение
Заголовок раздела «3. Обучение»Сначала нужно определить планировщик шума (noise scheduler): он управляет зашумлением и даёт зашумлённые
изображения для разных шагов. По формулам выше реализуем его в коде. Для прямого процесса нужны ,
, , , . Задаём их через register_buffer —
тогда они автоматически сохраняются и загружаются вместе с параметрами модели.
class NoiseScheduler(nn.Module): def __init__(self, beta_start=0.0001, beta_end=0.02, num_steps=1000): super().__init__() self.beta_start = beta_start self.beta_end = beta_end self.num_steps = num_steps
# β_t: линейное расписание шума self.register_buffer('betas', torch.linspace(beta_start, beta_end, num_steps)) # α_t = 1 - β_t self.register_buffer('alphas', 1.0 - self.betas) # α_bar_t = ∏(1-β_i) для i от 1 до t self.register_buffer('alpha_bar', torch.cumprod(self.alphas, dim=0)) # sqrt(α_bar_t) self.register_buffer('sqrt_alpha_bar', torch.sqrt(self.alpha_bar)) # sqrt(1-α_bar_t) self.register_buffer('sqrt_one_minus_alpha_bar', torch.sqrt(1.0 - self.alpha_bar))Мы обучаем на батчах изображений, и эти величины нужно комбинировать с тензором картинок, а сейчас все они —
одномерные тензоры. Поэтому их форму надо подогнать под тензоры другой размерности. Для этого в классе
NoiseScheduler есть метод get: он берёт значение величины для заданных шагов и меняет форму; var — тензор
величины, t — шаги, x_shape — целевая форма.
def get(self, var, t, x_shape): # берём значения величины для заданных шагов out = var[t] # меняем форму на [batch_size, 1, 1, 1] для трансляции (broadcasting) return out.view([t.shape[0]] + [1] * (len(x_shape) - 1))Теперь можно реализовать зашумление по формуле ,
, где и берутся методом get.
def add_noise(self, x, t): # sqrt(α_bar_t) для шага t sqrt_alpha_bar = self.get(self.sqrt_alpha_bar, t, x.shape) # sqrt(1-α_bar_t) для шага t sqrt_one_minus_alpha_bar = self.get(self.sqrt_one_minus_alpha_bar, t, x.shape) # шум из стандартного нормального распределения ε ~ N(0,I) noise = torch.randn_like(x) # прямой процесс диффузии: x_t = sqrt(α_bar_t) * x_0 + sqrt(1-α_bar_t) * ε return sqrt_alpha_bar * x + sqrt_one_minus_alpha_bar * noise, noiseКомандой python ddpm/diffusion.py можно нарисовать, как изображение шаг за шагом зашумляется.
Рисунок в оригинале (fig/fig2.png): одна и та же картинка CIFAR-10 на нескольких шагах — от чёткой до сплошного шума.
Наконец, можно написать весь цикл обучения. Шаги такие:
- Случайно выбрать шаг времени
t. - Зашумить изображения — получить зашумлённые картинки и сам шум.
- Предсказать шум моделью.
- Посчитать MSE между предсказанным и настоящим шумом.
- Обратное распространение и шаг оптимизатора.
Командой python ddpm/train.py модель обучается и сохраняет параметры; 200 эпох на RTX-4090 занимают около 2 часов.
4. Генерация (сэмплирование)
Заголовок раздела «4. Генерация (сэмплирование)»Идея генерации: берём начальный шум из стандартного нормального распределения, затем шаг за шагом убираем шум от
до и в конце обрезаем результат до диапазона . Для очистки нужны коэффициенты
, , , .
Определяем их в классе NoiseScheduler:
# α_bar_(t-1)self.register_buffer('alpha_bar_prev', torch.cat([torch.tensor([1.0]), self.alpha_bar[:-1]], dim=0))# 1/sqrt(α_t)self.register_buffer('sqrt_recip_alphas', torch.sqrt(1.0 / self.alphas))
# 1/sqrt(α_bar_t)self.register_buffer('sqrt_recip_alphas_bar', torch.sqrt(1.0 / self.alpha_bar))# sqrt(1/α_bar_t - 1)self.register_buffer('sqrt_recipm1_alphas_bar', torch.sqrt(1.0 / self.alpha_bar - 1))# дисперсия апостериорного распределения σ_t^2self.register_buffer('posterior_var', self.betas * (1.0 - self.alpha_bar_prev) / (1.0 - self.alpha_bar))# коэффициент 1 среднего апостериорного распределения: β_t * sqrt(α_bar_(t-1))/(1-α_bar_t)self.register_buffer('posterior_mean_coef1', self.betas * torch.sqrt(self.alpha_bar_prev) / (1.0 - self.alpha_bar))# коэффициент 2 среднего апостериорного распределения: (1-α_bar_(t-1)) * sqrt(α_t)/(1-α_bar_t)self.register_buffer('posterior_mean_coef2', (1.0 - self.alpha_bar_prev) * torch.sqrt(self.alphas) / (1.0 - self.alpha_bar))Порядок генерации (по формулам статьи):
- Взять начальный шум из стандартного нормального распределения: .
- Повторять в цикле от до следующие шаги:
- По текущему шагу и текущему изображению вычислить моделью предсказанный шум .
- Вычислить предсказание : .
- Вычислить среднее апостериорного распределения: .
- Вычислить логарифм дисперсии апостериорного распределения: .
- Если , сделать выборку из апостериорного распределения: , .
- Если , взять среднее как результат генерации: .
def sample(model, scheduler, num_samples, size, device="cpu"): model.eval() with torch.no_grad(): # начальный шум из стандартного нормального распределения x_T ~ N(0,I) x_t = torch.randn(num_samples, *size).to(device)
# шаг за шагом убираем шум, от t=T до t=0 for t in tqdm(reversed(range(scheduler.num_steps)), desc="Sampling"): # батч из номеров шага t_batch = torch.tensor([t] * num_samples).to(device)
# коэффициенты для генерации sqrt_recip_alpha_bar = scheduler.get(scheduler.sqrt_recip_alphas_bar, t_batch, x_t.shape) sqrt_recipm1_alpha_bar = scheduler.get(scheduler.sqrt_recipm1_alphas_bar, t_batch, x_t.shape) posterior_mean_coef1 = scheduler.get(scheduler.posterior_mean_coef1, t_batch, x_t.shape) posterior_mean_coef2 = scheduler.get(scheduler.posterior_mean_coef2, t_batch, x_t.shape)
# предсказываем шум ε_θ(x_t,t) predicted_noise = model(x_t, t_batch)
# предсказание x_0: x_0 = 1/sqrt(α_bar_t) * x_t - sqrt(1/α_bar_t-1) * ε_θ(x_t,t) _x_0 = sqrt_recip_alpha_bar * x_t - sqrt_recipm1_alpha_bar * predicted_noise # среднее апостериорного распределения μ_θ(x_t,t) model_mean = posterior_mean_coef1 * _x_0 + posterior_mean_coef2 * x_t # логарифм дисперсии log(σ_t^2) model_log_var = scheduler.get(torch.log(torch.cat([scheduler.posterior_var[1:2], scheduler.betas[1:]])), t_batch, x_t.shape)
if t > 0: # при t>0 делаем выборку из апостериорного распределения: x_t-1 = μ_θ(x_t,t) + σ_t * z, z~N(0,I) noise = torch.randn_like(x_t).to(device) x_t = model_mean + torch.exp(0.5 * model_log_var) * noise else: # при t=0 берём среднее как результат x_t = model_mean # обрезаем результат до диапазона [-1,1] x_0 = torch.clamp(x_t, -1.0, 1.0) return x_0Командой python ddpm/sample.py можно загрузить обученную модель, сгенерировать изображения и сохранить их.
Рисунок в оригинале (fig/fig3.png): сетка сгенерированных картинок 32×32.
5. Оценка
Заголовок раздела «5. Оценка»Inception Score (IS) и Fréchet Inception Distance (FID) — две важные метрики качества сгенерированных изображений; по ним мы и оцениваем качество генерации.
Сначала загружаем предобученную модель Inception и получаем признаки настоящих и сгенерированных изображений.
class InceptionStatistics: def __init__(self, device='cuda'): self.device = device # загружаем предобученную модель Inception v3 self.model = models.inception_v3(weights=models.Inception_V3_Weights.IMAGENET1K_V1, transform_input=False) self.model.fc = nn.Identity() # убираем последний полносвязный слой self.model = self.model.to(device) self.model.eval()
# предобработка изображений self.preprocess = transforms.Compose([ transforms.Resize(299), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])
@torch.no_grad() def get_features(self, images): """Получить признаки Inception""" features = [] probs = []
# приводим изображения к размеру 299x299 images = self.preprocess(images)
# обрабатываем изображения батчами dataset = TensorDataset(images) dataloader = DataLoader(dataset, batch_size=32)
for (batch,) in tqdm(dataloader): batch = batch.to(self.device)
# получаем признаки и логиты feature = self.model(batch) prob = F.softmax(feature, dim=1)
features.append(feature.cpu().numpy()) probs.append(prob.cpu().numpy())
features = np.concatenate(features, axis=0) probs = np.concatenate(probs, axis=0)
return features, probsInception Score (IS)
Заголовок раздела «Inception Score (IS)»IS оценивает качество и разнообразие сгенерированных изображений с помощью предобученной сети Inception v3. Формула:
где:
- — распределение генератора;
- — вероятности классов, которые модель Inception предсказывает для изображения ;
- — среднее распределение классов по всем сгенерированным изображениям;
- KL — расхождение Кульбака — Лейблера, мера различия двух распределений вероятностей: .
Чем выше IS, тем:
- увереннее предсказание класса для каждого изображения (качество выше);
- шире разброс классов между изображениями (разнообразие выше).
Шаги:
- Разбить все изображения на группы.
- Для каждой группы:
- посчитать маргинальное распределение — среднее по группе;
- посчитать KL-расхождение;
- взять экспоненту.
- Вернуть среднее и стандартное отклонение оценок по группам.
def calculate_inception_score(probs, splits=10): # оценки IS для каждой группы scores = [] # размер группы split_size = probs.shape[0] // splits
# считаем для каждой группы for i in tqdm(range(splits)): # распределения вероятностей текущей группы part = probs[i * split_size:(i + 1) * split_size] # KL-расхождение: KL(p(y|x) || p(y)) kl = part * (np.log(part) - np.log(np.expand_dims(np.mean(part, axis=0), 0))) # усредняем KL по объектам kl = np.mean(np.sum(kl, axis=1)) # exp(KL) — в список оценок scores.append(np.exp(kl))
# среднее и стандартное отклонение IS по группам return np.mean(scores), np.std(scores)Fréchet Inception Distance (FID)
Заголовок раздела «Fréchet Inception Distance (FID)»FID оценивает качество генерации, сравнивая распределения настоящих и сгенерированных изображений в пространстве признаков Inception. Формула:
где:
- — средние признаков настоящих и сгенерированных изображений;
- — ковариационные матрицы признаков настоящих и сгенерированных изображений;
- — след матрицы.
Чем ниже FID, тем ближе распределение признаков сгенерированных изображений к настоящему и тем выше качество.
Шаги:
- Для настоящих и для сгенерированных изображений по отдельности:
- извлечь признаки моделью Inception;
- посчитать вектор средних и ковариационную матрицу признаков.
- Посчитать евклидово расстояние между векторами средних.
- Посчитать квадратный корень из произведения ковариационных матриц.
- Посчитать итоговый FID.
def calculate_fid(real_features, fake_features): # векторы средних и ковариационные матрицы признаков настоящих и сгенерированных изображений mu1, sigma1 = real_features.mean(axis=0), np.cov(real_features, rowvar=False) mu2, sigma2 = fake_features.mean(axis=0), np.cov(fake_features, rowvar=False)
# квадрат евклидова расстояния между векторами средних ssdiff = np.sum((mu1 - mu2) ** 2) # квадратный корень из произведения ковариаций: (Σ_r Σ_f)^(1/2) covmean = linalg.sqrtm(sigma1.dot(sigma2)) # считается долго # если в результате есть комплексные числа — берём действительную часть if np.iscomplexobj(covmean): covmean = covmean.real
# итоговый FID fid = ssdiff + np.trace(sigma1 + sigma2 - 2 * covmean) return fidКомандой python ddpm/metrics.py можно посчитать IS и FID: мы генерируем обученной диффузионной моделью 10 000
изображений и сравниваем их с датасетом CIFAR-10 как набором настоящих изображений.
По одной картинке IS и FID не посчитать, но можно наглядно посмотреть, как выглядят изображения двух моделей с разными IS и FID. Ниже — изображения упрощённой U-Net этого проекта (epochs=2000, IS=1.12, FID=41.63) и U-Net со слоями внимания (Attention) из статьи DDPM (epochs=2000, IS=1.17, FID=14.10), обученных в нашем фреймворке 200 и 2000 эпох.
Рисунки в оригинале — четыре сетки сгенерированных картинок: SimpleUnet, 200 эпох, AttentionUnet, 200 эпох, SimpleUnet, 2000 эпох, AttentionUnet, 2000 эпох.
Видно, что по мере обучения и с ростом качества модели изображения модели с более высоким IS и более низким FID лучше узнаются по классу, а детали ближе к настоящим картинкам CIFAR-10.
От переводчика: как запустить на своей машине
Заголовок раздела «От переводчика: как запустить на своей машине»Что сделать руками:
- Повторите таблицу из примечания в NumPy и постройте график по для линейного расписания. Сравните с косинусным расписанием (Improved DDPM).
- Обучите SimpleUnet 20 эпох и сгенерируйте 16 картинок с 1000 шагами. Затем сократите генерацию до 100 шагов, просто пропуская шаги, — и посмотрите, почему так делать нельзя без DDIM.
- Исправьте подсчёт IS (верните
fc, а признаки для FID берите отдельно) и пересчитайте метрики.
Словарик
Заголовок раздела «Словарик»| Русский | Пояснение |
|---|---|
| диффузионная модель (diffusion model) | генерация через постепенное удаление шума |
| прямой процесс, зашумление (forward process) | |
| обратный процесс, очистка от шума (reverse process) | |
| марковская цепь (Markov chain) | следующее состояние зависит только от текущего |
| расписание шума (noise schedule) | значения по шагам |
| апостериорное распределение (posterior) | |
| временной эмбеддинг (time embedding) | номер шага → вектор |
| повышение / понижение разрешения | upsampling / downsampling |
| генерация, сэмплирование (sampling) | получение новых картинок |
| расхождение Кульбака — Лейблера (KL divergence) | урок 1.3.3 |