Compare commits

...
26 Commits
Author SHA1 Message Date
Phil Wang cee30ce304 optimizer should be saved and loaded 2022-07-08 17:07:32 -07:00
Phil Wang 662172851b add convert_image_to keyword argument, for forcing images being loaded to be converted to some format, greyscale, rgb, rgba, whatever 2022-07-08 09:22:59 -07:00
Phil Wang 0248b5e4d3 also make sure grad scaler actually exists in the saved pt file 2022-07-06 11:48:04 -07:00
Phil Wang 6b56af08a2 support multi-gpu training using huggingface accelerate, addressing https://github.com/lucidrains/denoising-diffusion-pytorch/pull/54 2022-07-06 11:45:56 -07:00
Phil Wang d4420248f1 tqdm auto instead 2022-06-29 19:48:49 -07:00
Phil Wang a536e5bee9 reorganize elucidating code 2022-06-29 08:49:38 -07:00
Phil Wang 1b85379d3a add clamping option to elucidated diffusion 2022-06-29 08:32:10 -07:00
Phil Wang 8859864f63 patch 2022-06-29 07:55:11 -07:00
Phil Wang 32657f035f Merge pull request #52 from AryaAftab/patch-1
Solve issue #26
2022-06-29 07:54:48 -07:00
Arya Aftab d97bc0278c Solve issue #26 2022-06-29 11:42:54 +04:30
Phil Wang 8408775cfc fix bug in elucidating sampling 2022-06-28 17:52:02 -07:00
Phil Wang 86fcb6785b release elucidating diffusion 2022-06-28 17:39:48 -07:00
Phil Wang c535d31fc5 Merge pull request #51 from lucidrains/pw/elucidating-ddpm
elucidating diffusion, first pass
2022-06-28 17:29:28 -07:00
Phil Wang 5db64fec4b refactor sigmas and gamma generation 2022-06-28 17:26:58 -07:00
Phil Wang b87ea27781 fix off by one 2022-06-28 16:05:21 -07:00
Phil Wang a8403b83fe no clamping when training from sigmas drawn from log normal distribution, clamp final images being sampled 2022-06-28 16:00:16 -07:00
Phil Wang f4b1d7a67c complete a first pass of elucidated ddpm 2022-06-28 15:15:21 -07:00
Phil Wang 618493714f clamp the sigma coming out of the log normal distribution 2022-06-28 14:34:39 -07:00
Phil Wang 76b79aa847 take care of equation 7 in the paper 2022-06-28 14:16:05 -07:00
Phil Wang be2bd8d320 cleanup again 2022-06-28 13:42:41 -07:00
Phil Wang c3d1607019 cleanup 2022-06-28 13:38:37 -07:00
Phil Wang 06b2e52645 get training working 2022-06-28 13:35:06 -07:00
Phil Wang 09b8a1c805 some basic scaffold for elucidating diffusion and derived values 2022-06-28 13:11:56 -07:00
Phil Wang d26acbcae6 more skip connections, as in guided diffusion 2022-06-27 13:23:32 -07:00
Phil Wang 9939a48139 make sure all versions of torch supported 2022-06-23 12:28:09 -07:00
Phil Wang 75ea49a7ef pass parameter for Trainer to EMA properly 2022-06-21 07:37:43 -07:00
7 changed files with 392 additions and 69 deletions
+26
View File
@@ -80,6 +80,22 @@ trainer.train()
Samples and model checkpoints will be logged to `./results` periodically
## Multi-GPU Training
The `Trainer` class is now equipped with <a href="https://huggingface.co/docs/accelerate/accelerator">🤗 Accelerator</a>. You can easily do multi-gpu training in two steps using their `accelerate` CLI
At the project root directory, where the training script is, run
```python
$ accelerate config
```
Then, in the same directory
```python
$ accelerate launch train.py
```
## Citations
```bibtex
@@ -133,3 +149,13 @@ Samples and model checkpoints will be logged to `./results` periodically
volume = {abs/2204.00227}
}
```
```bibtex
@article{Karras2022ElucidatingTD,
title = {Elucidating the Design Space of Diffusion-Based Generative Models},
author = {Tero Karras and Miika Aittala and Timo Aila and Samuli Laine},
journal = {ArXiv},
year = {2022},
volume = {abs/2206.00364}
}
```
+1
View File
@@ -3,3 +3,4 @@ from denoising_diffusion_pytorch.denoising_diffusion_pytorch import GaussianDiff
from denoising_diffusion_pytorch.learned_gaussian_diffusion import LearnedGaussianDiffusion
from denoising_diffusion_pytorch.continuous_time_gaussian_diffusion import ContinuousTimeGaussianDiffusion
from denoising_diffusion_pytorch.weighted_objective_gaussian_diffusion import WeightedObjectiveGaussianDiffusion
from denoising_diffusion_pytorch.elucidated_diffusion import ElucidatedDiffusion
@@ -1,3 +1,4 @@
import math
import torch
from torch import sqrt
from torch import nn, einsum
@@ -66,7 +67,7 @@ def beta_linear_log_snr(t):
return -log(expm1(1e-4 + 10 * (t ** 2)))
def alpha_cosine_log_snr(t, s = 0.008):
return -log((torch.cos((t + s) / (1 + s) * torch.pi * 0.5) ** -2) - 1, eps = 1e-5)
return -log((torch.cos((t + s) / (1 + s) * math.pi * 0.5) ** -2) - 1, eps = 1e-5)
class learned_noise_schedule(nn.Module):
""" described in section H and then I.2 of the supplementary material for variational ddpm paper """
@@ -6,21 +6,22 @@ import torch.nn.functional as F
from inspect import isfunction
from functools import partial
from torch.utils import data
from torch.utils.data import Dataset, DataLoader
from multiprocessing import cpu_count
from torch.cuda.amp import autocast, GradScaler
from pathlib import Path
from torch.optim import Adam
from torchvision import transforms, utils
from torchvision import transforms as T, utils
from PIL import Image
from tqdm import tqdm
from einops import rearrange, reduce
from einops.layers.torch import Rearrange
from tqdm.auto import tqdm
from ema_pytorch import EMA
from accelerate import Accelerator
# helpers functions
def exists(x):
@@ -36,6 +37,9 @@ def cycle(dl):
for data in dl:
yield data
def has_int_squareroot(num):
return (math.sqrt(num) ** 2) == num
def num_to_groups(num, divisor):
groups = num // divisor
remainder = num % divisor
@@ -44,6 +48,13 @@ def num_to_groups(num, divisor):
arr.append(remainder)
return arr
def convert_image_to(img_type, image):
if image.mode != img_type:
return image.convert(img_type)
return image
# normalization functions
def normalize_to_neg_one_to_one(img):
return img * 2 - 1
@@ -60,11 +71,14 @@ class Residual(nn.Module):
def forward(self, x, *args, **kwargs):
return self.fn(x, *args, **kwargs) + x
def Upsample(dim):
return nn.ConvTranspose2d(dim, dim, 4, 2, 1)
def Upsample(dim, dim_out = None):
return nn.Sequential(
nn.Upsample(scale_factor = 2, mode = 'nearest'),
nn.Conv2d(dim, default(dim_out, dim), 3, padding = 1)
)
def Downsample(dim):
return nn.Conv2d(dim, dim, 4, 2, 1)
def Downsample(dim, dim_out = None):
return nn.Conv2d(dim, default(dim_out, dim), 4, 2, 1)
class LayerNorm(nn.Module):
def __init__(self, dim, eps = 1e-5):
@@ -277,10 +291,10 @@ class Unet(nn.Module):
is_last = ind >= (num_resolutions - 1)
self.downs.append(nn.ModuleList([
block_klass(dim_in, dim_out, time_emb_dim = time_dim),
block_klass(dim_out, dim_out, time_emb_dim = time_dim),
Residual(PreNorm(dim_out, LinearAttention(dim_out))),
Downsample(dim_out) if not is_last else nn.Identity()
block_klass(dim_in, dim_in, time_emb_dim = time_dim),
block_klass(dim_in, dim_in, time_emb_dim = time_dim),
Residual(PreNorm(dim_in, LinearAttention(dim_in))),
Downsample(dim_in, dim_out) if not is_last else nn.Conv2d(dim_in, dim_out, 3, padding = 1)
]))
mid_dim = dims[-1]
@@ -292,10 +306,10 @@ class Unet(nn.Module):
is_last = ind == (len(in_out) - 1)
self.ups.append(nn.ModuleList([
block_klass(dim_out * 2, dim_in, time_emb_dim = time_dim),
block_klass(dim_in, dim_in, time_emb_dim = time_dim),
Residual(PreNorm(dim_in, LinearAttention(dim_in))),
Upsample(dim_in) if not is_last else nn.Identity()
block_klass(dim_out + dim_in, dim_out, time_emb_dim = time_dim),
block_klass(dim_out + dim_in, dim_out, time_emb_dim = time_dim),
Residual(PreNorm(dim_out, LinearAttention(dim_out))),
Upsample(dim_out, dim_in) if not is_last else nn.Conv2d(dim_out, dim_in, 3, padding = 1)
]))
default_out_dim = channels * (1 if not learned_variance else 2)
@@ -314,9 +328,12 @@ class Unet(nn.Module):
for block1, block2, attn, downsample in self.downs:
x = block1(x, t)
h.append(x)
x = block2(x, t)
x = attn(x)
h.append(x)
x = downsample(x)
x = self.mid_block1(x, t)
@@ -326,8 +343,11 @@ class Unet(nn.Module):
for block1, block2, attn, upsample in self.ups:
x = torch.cat((x, h.pop()), dim = 1)
x = block1(x, t)
x = torch.cat((x, h.pop()), dim = 1)
x = block2(x, t)
x = attn(x)
x = upsample(x)
x = torch.cat((x, r), dim = 1)
@@ -355,7 +375,7 @@ def cosine_beta_schedule(timesteps, s = 0.008):
"""
steps = timesteps + 1
x = torch.linspace(0, timesteps, steps, dtype = torch.float64)
alphas_cumprod = torch.cos(((x / timesteps) + s) / (1 + s) * torch.pi * 0.5) ** 2
alphas_cumprod = torch.cos(((x / timesteps) + s) / (1 + s) * math.pi * 0.5) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
return torch.clip(betas, 0, 0.999)
@@ -553,18 +573,28 @@ class GaussianDiffusion(nn.Module):
# dataset classes
class Dataset(data.Dataset):
def __init__(self, folder, image_size, exts = ['jpg', 'jpeg', 'png'], augment_horizontal_flip = False):
class Dataset(Dataset):
def __init__(
self,
folder,
image_size,
exts = ['jpg', 'jpeg', 'png', 'tiff'],
augment_horizontal_flip = False,
convert_image_to = None
):
super().__init__()
self.folder = folder
self.image_size = image_size
self.paths = [p for ext in exts for p in Path(f'{folder}').glob(f'**/*.{ext}')]
self.transform = transforms.Compose([
transforms.Resize(image_size),
transforms.RandomHorizontalFlip() if augment_horizontal_flip else nn.Identity(),
transforms.CenterCrop(image_size),
transforms.ToTensor()
maybe_convert_fn = partial(convert_image_to, convert_image_to) if exists(convert_image_to) else nn.Identity()
self.transform = T.Compose([
T.Lambda(maybe_convert_fn),
T.Resize(image_size),
T.RandomHorizontalFlip() if augment_horizontal_flip else nn.Identity(),
T.CenterCrop(image_size),
T.ToTensor()
])
def __len__(self):
@@ -583,93 +613,137 @@ class Trainer(object):
diffusion_model,
folder,
*,
ema_decay = 0.995,
train_batch_size = 32,
train_batch_size = 16,
gradient_accumulate_every = 1,
augment_horizontal_flip = True,
train_lr = 1e-4,
train_num_steps = 100000,
gradient_accumulate_every = 2,
amp = False,
step_start_ema = 2000,
update_ema_every = 10,
ema_update_every = 10,
ema_decay = 0.995,
save_and_sample_every = 1000,
num_samples = 25,
results_folder = './results',
augment_horizontal_flip = True
amp = False,
fp16 = False,
split_batches = True,
convert_image_to = None
):
super().__init__()
self.image_size = diffusion_model.image_size
self.accelerator = Accelerator(
split_batches = split_batches,
mixed_precision = 'fp16' if fp16 else 'no'
)
self.accelerator.native_amp = amp
self.model = diffusion_model
self.ema = EMA(diffusion_model, beta = ema_decay)
self.update_ema_every = update_ema_every
self.step_start_ema = step_start_ema
assert has_int_squareroot(num_samples), 'number of samples must have an integer square root'
self.num_samples = num_samples
self.save_and_sample_every = save_and_sample_every
self.batch_size = train_batch_size
self.image_size = diffusion_model.image_size
self.gradient_accumulate_every = gradient_accumulate_every
self.train_num_steps = train_num_steps
self.ds = Dataset(folder, self.image_size, augment_horizontal_flip = augment_horizontal_flip)
self.dl = cycle(data.DataLoader(self.ds, batch_size = train_batch_size, shuffle = True, pin_memory = True, num_workers = cpu_count()))
self.train_num_steps = train_num_steps
self.image_size = diffusion_model.image_size
# dataset and dataloader
self.ds = Dataset(folder, self.image_size, augment_horizontal_flip = augment_horizontal_flip, convert_image_to = convert_image_to)
dl = DataLoader(self.ds, batch_size = train_batch_size, shuffle = True, pin_memory = True, num_workers = cpu_count())
self.dl = cycle(dl)
# optimizer
self.opt = Adam(diffusion_model.parameters(), lr = train_lr)
# for logging results in a folder periodically
if self.accelerator.is_main_process:
self.ema = EMA(diffusion_model, beta = ema_decay, update_every = ema_update_every)
self.results_folder = Path(results_folder)
self.results_folder.mkdir(exist_ok = True)
# step counter state
self.step = 0
self.amp = amp
self.scaler = GradScaler(enabled = amp)
# prepare model, dataloader, optimizer with accelerator
self.results_folder = Path(results_folder)
self.results_folder.mkdir(exist_ok = True)
self.model, self.dl, self.opt = self.accelerator.prepare(self.model, self.dl, self.opt)
def save(self, milestone):
if not self.accelerator.is_main_process:
return
data = {
'step': self.step,
'model': self.model.state_dict(),
'model': self.accelerator.get_state_dict(self.model),
'opt': self.opt.state_dict(),
'ema': self.ema.state_dict(),
'scaler': self.scaler.state_dict()
'scaler': self.accelerator.scaler.state_dict() if exists(self.accelerator.scaler) else None
}
torch.save(data, str(self.results_folder / f'model-{milestone}.pt'))
def load(self, milestone):
data = torch.load(str(self.results_folder / f'model-{milestone}.pt'))
model = self.accelerator.unwrap_model(self.model)
model.load_state_dict(data['model'])
self.step = data['step']
self.model.load_state_dict(data['model'])
self.opt.load_state_dict(data['opt'])
self.ema.load_state_dict(data['ema'])
self.scaler.load_state_dict(data['scaler'])
if exists(self.accelerator.scaler) and exists(data['scaler']):
self.accelerator.scaler.load_state_dict(data['scaler'])
def train(self):
with tqdm(initial = self.step, total = self.train_num_steps) as pbar:
accelerator = self.accelerator
device = accelerator.device
with tqdm(initial = self.step, total = self.train_num_steps, disable = not accelerator.is_main_process) as pbar:
while self.step < self.train_num_steps:
for i in range(self.gradient_accumulate_every):
data = next(self.dl).cuda()
with autocast(enabled = self.amp):
for _ in range(self.gradient_accumulate_every):
data = next(self.dl).to(device)
with self.accelerator.autocast():
loss = self.model(data)
self.scaler.scale(loss / self.gradient_accumulate_every).backward()
self.accelerator.backward(loss / self.gradient_accumulate_every)
pbar.set_description(f'loss: {loss.item():.4f}')
pbar.set_description(f'loss: {loss.item():.4f}')
self.scaler.step(self.opt)
self.scaler.update()
accelerator.wait_for_everyone()
self.opt.step()
self.opt.zero_grad()
self.ema.update()
accelerator.wait_for_everyone()
if self.step != 0 and self.step % self.save_and_sample_every == 0:
self.ema.ema_model.eval()
with torch.no_grad():
milestone = self.step // self.save_and_sample_every
batches = num_to_groups(36, self.batch_size)
all_images_list = list(map(lambda n: self.ema.ema_model.sample(batch_size=n), batches))
if accelerator.is_main_process:
self.ema.to(device)
self.ema.update()
all_images = torch.cat(all_images_list, dim=0)
utils.save_image(all_images, str(self.results_folder / f'sample-{milestone}.png'), nrow = 6)
self.save(milestone)
if self.step != 0 and self.step % self.save_and_sample_every == 0:
self.ema.ema_model.eval()
with torch.no_grad():
milestone = self.step // self.save_and_sample_every
batches = num_to_groups(self.num_samples, self.batch_size)
all_images_list = list(map(lambda n: self.ema.ema_model.sample(batch_size=n), batches))
all_images = torch.cat(all_images_list, dim = 0)
utils.save_image(all_images, str(self.results_folder / f'sample-{milestone}.png'), nrow = int(math.sqrt(self.num_samples)))
self.save(milestone)
self.step += 1
pbar.update(1)
print('training complete')
accelerator.print('training complete')
@@ -0,0 +1,220 @@
from math import sqrt
import torch
from torch import nn, einsum
import torch.nn.functional as F
from tqdm import tqdm
from einops import rearrange, repeat, reduce
# helpers
def exists(val):
return val is not None
def default(val, d):
if exists(val):
return val
return d() if callable(d) else d
# tensor helpers
def log(t, eps = 1e-20):
return torch.log(t.clamp(min = eps))
# normalization functions
def normalize_to_neg_one_to_one(img):
return img * 2 - 1
def unnormalize_to_zero_to_one(t):
return (t + 1) * 0.5
# main class
class ElucidatedDiffusion(nn.Module):
def __init__(
self,
net,
*,
image_size,
channels = 3,
num_sample_steps = 32, # number of sampling steps
sigma_min = 0.002, # min noise level
sigma_max = 80, # max noise level
sigma_data = 0.5, # standard deviation of data distribution
rho = 7, # controls the sampling schedule
P_mean = -1.2, # mean of log-normal distribution from which noise is drawn for training
P_std = 1.2, # standard deviation of log-normal distribution from which noise is drawn for training
S_churn = 80, # parameters for stochastic sampling - depends on dataset, Table 5 in apper
S_tmin = 0.05,
S_tmax = 50,
S_noise = 1.003,
):
super().__init__()
assert net.learned_sinusoidal_cond
self.net = net
# image dimensions
self.channels = channels
self.image_size = image_size
# parameters
self.sigma_min = sigma_min
self.sigma_max = sigma_max
self.sigma_data = sigma_data
self.rho = rho
self.P_mean = P_mean
self.P_std = P_std
self.num_sample_steps = num_sample_steps # otherwise known as N in the paper
self.S_churn = S_churn
self.S_tmin = S_tmin
self.S_tmax = S_tmax
self.S_noise = S_noise
@property
def device(self):
return next(self.net.parameters()).device
# derived preconditioning params - Table 1
def c_skip(self, sigma):
return (self.sigma_data ** 2) / (sigma ** 2 + self.sigma_data ** 2)
def c_out(self, sigma):
return sigma * self.sigma_data * (self.sigma_data ** 2 + sigma ** 2) ** -0.5
def c_in(self, sigma):
return 1 * (sigma ** 2 + self.sigma_data ** 2) ** -0.5
def c_noise(self, sigma):
return log(sigma) * 0.25
# preconditioned network output
# equation (7) in the paper
def preconditioned_network_forward(self, noised_images, sigma, clamp = False):
batch, device = noised_images.shape[0], noised_images.device
if isinstance(sigma, float):
sigma = torch.full((batch,), sigma, device = device)
padded_sigma = rearrange(sigma, 'b -> b 1 1 1')
net_out = self.net(
self.c_in(padded_sigma) * noised_images,
self.c_noise(sigma)
)
out = self.c_skip(padded_sigma) * noised_images + self.c_out(padded_sigma) * net_out
if clamp:
out = out.clamp(-1., 1.)
return out
# sampling
# sample schedule
# equation (5) in the paper
def sample_schedule(self, num_sample_steps = None):
num_sample_steps = default(num_sample_steps, self.num_sample_steps)
N = num_sample_steps
inv_rho = 1 / self.rho
steps = torch.arange(num_sample_steps, device = self.device, dtype = torch.float32)
sigmas = (self.sigma_max ** inv_rho + steps / (N - 1) * (self.sigma_min ** inv_rho - self.sigma_max ** inv_rho)) ** self.rho
sigmas = F.pad(sigmas, (0, 1), value = 0.) # last step is sigma value of 0.
return sigmas
@torch.no_grad()
def sample(self, batch_size = 16, num_sample_steps = None, clamp = True):
num_sample_steps = default(num_sample_steps, self.num_sample_steps)
shape = (batch_size, self.channels, self.image_size, self.image_size)
# get the schedule, which is returned as (sigma, gamma) tuple, and pair up with the next sigma and gamma
sigmas = self.sample_schedule(num_sample_steps)
gammas = torch.where(
(sigmas >= self.S_tmin) & (sigmas <= self.S_tmax),
min(self.S_churn / num_sample_steps, sqrt(2) - 1),
0.
)
sigmas_and_gammas = list(zip(sigmas[:-1], sigmas[1:], gammas[:-1]))
# images is noise at the beginning
init_sigma = sigmas[0]
images = init_sigma * torch.randn(shape, device = self.device)
# gradually denoise
for sigma, sigma_next, gamma in tqdm(sigmas_and_gammas, desc = 'sampling time step'):
sigma, sigma_next, gamma = map(lambda t: t.item(), (sigma, sigma_next, gamma))
eps = self.S_noise * torch.randn(shape, device = self.device) # stochastic sampling
sigma_hat = sigma + gamma * sigma
images_hat = images + sqrt(sigma_hat ** 2 - sigma ** 2) * eps
model_output = self.preconditioned_network_forward(images_hat, sigma_hat, clamp = clamp)
denoised_over_sigma = (images_hat - model_output) / sigma_hat
images_next = images_hat + (sigma_next - sigma_hat) * denoised_over_sigma
# second order correction, if not the last timestep
if sigma_next != 0:
model_output_next = self.preconditioned_network_forward(images_next, sigma_next, clamp = clamp)
denoised_prime_over_sigma = (images_next - model_output_next) / sigma_next
images_next = images_hat + 0.5 * (sigma_next - sigma_hat) * (denoised_over_sigma + denoised_prime_over_sigma)
images = images_next
images = images.clamp(-1., 1.)
return unnormalize_to_zero_to_one(images)
# training
def loss_weight(self, sigma):
return (sigma ** 2 + self.sigma_data ** 2) * (sigma * self.sigma_data) ** -2
def noise_distribution(self, batch_size):
return (self.P_mean + self.P_std * torch.randn((batch_size,), device = self.device)).exp()
def forward(self, images):
batch_size, c, h, w, device, image_size, channels = *images.shape, images.device, self.image_size, self.channels
assert h == image_size and w == image_size, f'height and width of image must be {image_size}'
assert c == channels, 'mismatch of image channels'
images = normalize_to_neg_one_to_one(images)
sigmas = self.noise_distribution(batch_size)
padded_sigmas = rearrange(sigmas, 'b -> b 1 1 1')
noise = torch.randn_like(images)
noised_images = images + padded_sigmas * noise # alphas are 1. in the paper
denoised = self.preconditioned_network_forward(noised_images, sigmas)
losses = F.mse_loss(denoised, images, reduction = 'none')
losses = reduce(losses, 'b ... -> b', 'mean')
losses = losses * self.loss_weight(sigmas)
return losses.mean()
@@ -22,7 +22,7 @@ def default(val, d):
# tensor helpers
def log(t, eps = 1e-12):
def log(t, eps = 1e-15):
return torch.log(t.clamp(min = eps))
def meanflat(x):
+2 -1
View File
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name = 'denoising-diffusion-pytorch',
packages = find_packages(),
version = '0.21.0',
version = '0.24.3',
license='MIT',
description = 'Denoising Diffusion Probabilistic Models - Pytorch',
author = 'Phil Wang',
@@ -15,6 +15,7 @@ setup(
'generative models'
],
install_requires=[
'accelerate',
'einops',
'ema-pytorch',
'pillow',