texttoimage / sr_diffusion.py
Murali Chandran
Add CIFAR-10 T2I + SR pipeline
97f6b95
Raw History Blame Contribute Delete
14.7 kB
# sr_diffusion.py
# -----------------------------------------------------------
# Super-Resolution Diffusion for CIFAR-10 (32×32 → 128×128)
# Stage-2 after your text-to-image model
# -----------------------------------------------------------
import os, math, argparse, random, glob
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, utils as vutils
from tqdm import tqdm
# ------------------ utils ------------------
def set_seed(seed: int = 0):
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def _raw_module(m):
return getattr(m, "_orig_mod", m)
# ------------------ time embedding ------------------
class SinusoidalTimeEmbedding(nn.Module):
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, t):
half = self.dim // 2
emb = torch.log(torch.tensor(10000.0)) / (half - 1)
emb = torch.exp(torch.arange(half, device=t.device) * -emb)
emb = t[:, None].float() * emb[None, :]
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=1)
if self.dim % 2 == 1:
emb = F.pad(emb, (0,1))
return emb
# ------------------ building blocks ------------------
class ResBlock(nn.Module):
def __init__(self, dim_in, dim_out, cond_dim):
super().__init__()
self.norm1 = nn.GroupNorm(32, dim_in)
self.conv1 = nn.Conv2d(dim_in, dim_out, 3, padding=1)
self.norm2 = nn.GroupNorm(32, dim_out)
self.conv2 = nn.Conv2d(dim_out, dim_out, 3, padding=1)
self.emb = nn.Sequential(
nn.SiLU(),
nn.Linear(cond_dim, dim_out * 2)
)
self.act = nn.SiLU()
self.skip = nn.Conv2d(dim_in, dim_out, 1) if dim_in != dim_out else nn.Identity()
def forward(self, x, cond):
h = self.conv1(self.act(self.norm1(x)))
scale, shift = self.emb(cond).chunk(2, dim=1)
scale, shift = scale[:, :, None, None], shift[:, :, None, None]
h = h * (1 + scale) + shift
h = self.conv2(self.act(self.norm2(h)))
return h + self.skip(x)
class Down(nn.Module):
def __init__(self, dim_in, dim_out, cond_dim):
super().__init__()
self.block1 = ResBlock(dim_in, dim_out, cond_dim)
self.block2 = ResBlock(dim_out, dim_out, cond_dim)
self.down = nn.Conv2d(dim_out, dim_out, 4, stride=2, padding=1)
def forward(self, x, cond):
x = self.block1(x, cond)
x = self.block2(x, cond)
x = self.down(x)
return x
class Up(nn.Module):
def __init__(self, dim_in, dim_out, cond_dim):
super().__init__()
self.block1 = ResBlock(dim_in, dim_out, cond_dim)
self.block2 = ResBlock(dim_out, dim_out, cond_dim)
self.up = nn.ConvTranspose2d(dim_out, dim_out, 4, stride=2, padding=1)
def forward(self, x, cond):
x = self.block1(x, cond)
x = self.block2(x, cond)
x = self.up(x)
return x
class UNetSR(nn.Module):
def __init__(self, in_ch=3, base=64, cond_dim=256):
super().__init__()
self.in_conv = nn.Conv2d(in_ch, base, 3, padding=1)
self.down1 = Down(base, base*2, cond_dim)
self.down2 = Down(base*2, base*4, cond_dim)
self.mid1 = ResBlock(base*4, base*4, cond_dim)
self.mid2 = ResBlock(base*4, base*4, cond_dim)
self.up2 = Up(base*4, base*2, cond_dim)
self.up1 = Up(base*2, base, cond_dim)
self.out_norm = nn.GroupNorm(8, base)
self.out_conv = nn.Conv2d(base, in_ch, 3, padding=1)
self.act = nn.SiLU()
def forward(self, x, cond):
x0 = self.in_conv(x)
d1 = self.down1(x0, cond)
d2 = self.down2(d1, cond)
m = self.mid1(d2, cond)
m = self.mid2(m, cond)
u2 = self.up2(m + d2, cond)
u1 = self.up1(u2 + d1, cond)
h = self.act(self.out_norm(u1))
return self.out_conv(h)
# ------------------ diffusion ------------------
class Diffusion(nn.Module):
def __init__(self, model: nn.Module, T=1000, schedule="cosine"):
super().__init__()
self.model = model
self.T = T
self.register_buffer("betas", self.make_betas(T, schedule))
alphas = 1.0 - self.betas
alphas_cumprod = torch.cumprod(alphas, dim=0)
self.register_buffer("alphas_cumprod", alphas_cumprod)
self.register_buffer("sqrt_alphas_cumprod", torch.sqrt(alphas_cumprod))
self.register_buffer("sqrt_one_minus_alphas_cumprod", torch.sqrt(1.0 - alphas_cumprod))
from diffusers.schedulers.scheduling_ddpm import DDPMScheduler
self.scheduler = DDPMScheduler(num_train_timesteps=self.T, beta_schedule="squaredcos_cap_v2")
@staticmethod
def make_betas(T, schedule):
if schedule == "linear":
return torch.linspace(1e-4, 0.02, T)
s = 0.008
steps = T + 1
x = torch.linspace(0, T, steps)
alphas_cum = torch.cos(((x / T) + s) / (1 + s) * math.pi * 0.5) ** 2
alphas_cum = alphas_cum / alphas_cum[0]
betas = 1 - (alphas_cum[1:] / alphas_cum[:-1])
return betas.clamp(max=0.999)
@torch.no_grad()
def sample(self, shape, steps=1000, cond=None):
device = next(self.parameters()).device
x = torch.randn(shape, device=device)
self.scheduler.set_timesteps(steps)
for t in self.scheduler.timesteps:
eps_pred = self.model(x, torch.full((shape[0],), t, device=device, dtype=torch.long), cond)
x = self.scheduler.step(eps_pred, t, x).prev_sample
return x
# ------------------ Wrapper ------------------
class CondUNetSR(nn.Module):
def __init__(self, base=64, time_dim=128, cond_dim=256):
super().__init__()
self.time_emb = SinusoidalTimeEmbedding(time_dim)
self.time_mlp = nn.Sequential(
nn.Linear(time_dim, cond_dim), nn.SiLU(), nn.Linear(cond_dim, cond_dim)
)
# learnable projection for conditioning image
self.cond_proj = nn.Sequential(
nn.Conv2d(3, 16, 3, stride=2, padding=1),
nn.SiLU(),
nn.Conv2d(16, 32, 3, stride=2, padding=1),
nn.SiLU(),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(32, cond_dim)
)
self.net = UNetSR(in_ch=3, base=base, cond_dim=cond_dim)
def forward(self, x, t, cond_img=None):
t_emb = self.time_mlp(self.time_emb(t)) # [B, 256]
if cond_img is not None:
cond_emb = self.cond_proj(cond_img) # [B, 256]
cond = t_emb + cond_emb
else:
cond = t_emb
return self.net(x, cond)
# ------------------ Trainer ------------------
class SRTrainer:
def __init__(self, args):
self.args = args
set_seed(args.seed)
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tfm_hr = transforms.Compose([
transforms.Resize(args.hr_size),
transforms.ToTensor(),
transforms.Lambda(lambda x: x * 2 - 1),
])
self.ds = datasets.CIFAR10(args.data_root, train=True, transform=tfm_hr, download=True)
self.loader = DataLoader(self.ds, batch_size=args.bs, shuffle=True,
num_workers=args.workers, drop_last=True)
self.model = CondUNetSR(base=args.base, time_dim=128, cond_dim=args.cond_dim).to(self.device)
self.diff = Diffusion(self.model, T=args.T, schedule=args.schedule).to(self.device)
self.opt = torch.optim.AdamW(self.model.parameters(), lr=args.lr)
os.makedirs(args.outdir, exist_ok=True)
# preview set
test_ds = datasets.CIFAR10(args.data_root, train=False,
transform=transforms.ToTensor(), download=True)
self.sample_imgs = torch.stack([test_ds[i][0] for i in range(16)]).to(self.device) * 2 - 1
# auto resume
self.start_epoch = 0
if args.resume:
self.start_epoch = self.try_resume()
# ---- checkpoints ----
def save_ckpt(self, epoch):
ckpt = {
"model": _raw_module(self.model).state_dict(),
"opt": self.opt.state_dict(),
"epoch": epoch
}
path = os.path.join(self.args.outdir, f"ckpt_epoch{epoch}.pt")
torch.save(ckpt, path)
print(f"[ckpt] saved {path}")
def load_ckpt(self, path):
ckpt = torch.load(path, map_location=self.device)
self.model.load_state_dict(ckpt["model"])
self.opt.load_state_dict(ckpt["opt"])
print(f"[ckpt] loaded {path} (epoch {ckpt['epoch']})")
return ckpt["epoch"]
def try_resume(self):
ckpts = sorted(glob.glob(os.path.join(self.args.outdir, "ckpt_epoch*.pt")))
if len(ckpts) == 0:
print("[resume] no checkpoints found")
return 0
last_ckpt = ckpts[-1]
epoch = self.load_ckpt(last_ckpt)
return epoch
# ---- training ----
def train(self):
self.model.train()
for epoch in range(self.start_epoch, self.args.epochs):
losses = []
pbar = tqdm(self.loader, desc=f"Epoch {epoch+1}/{self.args.epochs}")
for hr, _ in pbar:
hr = hr.to(self.device)
lr = F.interpolate(hr, size=32, mode="bilinear") # low-res input
t = torch.randint(0, self.args.T, (hr.size(0),), device=self.device)
noise = torch.randn_like(hr)
a_t = self.diff.sqrt_alphas_cumprod[t][:, None, None, None]
am1_t = self.diff.sqrt_one_minus_alphas_cumprod[t][:, None, None, None]
hr_noisy = a_t * hr + am1_t * noise
pred = self.model(hr_noisy, t, cond_img=lr)
loss = F.mse_loss(pred, noise)
self.opt.zero_grad(set_to_none=True)
loss.backward()
nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
self.opt.step()
losses.append(loss.item())
pbar.set_postfix(loss=loss.item())
avg_loss = sum(losses) / len(losses)
print(f"Epoch {epoch+1}/{self.args.epochs} | Avg Loss: {avg_loss:.4f}")
# ---- checkpoint + sample ----
if (epoch + 1) % self.args.sample_every == 0:
self.save_ckpt(epoch + 1)
self.model.eval()
with torch.no_grad():
sr = self.super_resolve(self.sample_imgs, steps=self.args.steps)
grid = vutils.make_grid(sr, nrow=4)
save_path = os.path.join(self.args.outdir, f"sr_epoch{epoch+1}.png")
vutils.save_image(grid, save_path)
print(f"[sample] saved {save_path}")
self.model.train()
@torch.no_grad()
def super_resolve(self, lr_imgs, steps=1000):
self.model.eval()
sr_imgs = self.diff.sample((lr_imgs.size(0), 3, self.args.hr_size, self.args.hr_size),
steps=steps, cond=lr_imgs)
return (sr_imgs.clamp(-1,1)+1)*0.5
# ------------------ CLI ------------------
def build_parser():
p = argparse.ArgumentParser(description="Super-Resolution Diffusion (CIFAR 32→HR)")
p.add_argument("--train", action="store_true")
p.add_argument("--sample", action="store_true")
p.add_argument("--data_root", type=str, default="./data")
p.add_argument("--epochs", type=int, default=20)
p.add_argument("--bs", type=int, default=128)
p.add_argument("--lr", type=float, default=1e-4)
p.add_argument("--T", type=int, default=1000)
p.add_argument("--schedule", type=str, default="cosine", choices=["cosine", "linear"])
p.add_argument("--base", type=int, default=64)
p.add_argument("--cond_dim", type=int, default=256)
p.add_argument("--workers", type=int, default=4)
p.add_argument("--seed", type=int, default=0)
p.add_argument("--outdir", type=str, default="outputs_sr")
p.add_argument("--hr_size", type=int, default=128)
p.add_argument("--steps", type=int, default=50)
p.add_argument("--sample_every", type=int, default=10,
help="Save checkpoint + preview samples every N epochs")
p.add_argument("--resume", action="store_true",
help="Auto-resume from the latest checkpoint in outdir")
p.add_argument("--prompts", nargs="+", type=str, default=None,
help="Text prompts for text→image→super-res pipeline")
return p
def main():
args = build_parser().parse_args()
trainer = SRTrainer(args)
if args.train:
trainer.train()
if args.sample:
# ---- load checkpoint before sampling ----
ckpts = sorted(glob.glob(os.path.join(args.outdir, "ckpt_epoch*.pt")))
if len(ckpts) == 0:
print("[sample] No checkpoints found in outdir, cannot sample")
return
last_ckpt = ckpts[-1]
trainer.load_ckpt(last_ckpt)
if args.prompts:
# -------- Stage-1: text-to-image (32x32) --------
from t2i_text import Trainer as T2ITrainer, build_parser as t2i_parser
# load stage-1 checkpoint
t2i_args = t2i_parser().parse_args([])
t2i_trainer = T2ITrainer(t2i_args)
t2i_trainer.load_ckpt("outputs/ckpt_latest.pt") # adjust path if needed
print(f"[stage-1] generating low-res 32x32 from prompts: {args.prompts}")
lr_imgs = t2i_trainer.sample(prompts=args.prompts, steps=50, n_per_prompt=1)
lr_imgs = lr_imgs.to(trainer.device) * 2 - 1 # normalize to [-1,1]
else:
# -------- fallback: use CIFAR test images --------
ds = datasets.CIFAR10(args.data_root, train=False,
transform=transforms.ToTensor(), download=True)
lr_imgs = torch.stack([ds[i][0] for i in range(16)])
lr_imgs = (lr_imgs*2-1).to(trainer.device)
# -------- Stage-2: super-resolution (32→128) --------
sr = trainer.super_resolve(lr_imgs, steps=args.steps)
grid = vutils.make_grid(sr, nrow=4)
os.makedirs(args.outdir, exist_ok=True)
if args.prompts:
save_path = os.path.join(args.outdir, "sr_from_prompt.png")
else:
save_path = os.path.join(args.outdir, "sr_samples.png")
vutils.save_image(grid, save_path)
print(f"[sample] saved {save_path}")
if __name__ == "__main__":
main()