Skip to content

Commit ba70bda

Browse files
authored
Merge pull request #160 from chairc/dev
Update samples
2 parents 7c8c413 + d462457 commit ba70bda

7 files changed

Lines changed: 279 additions & 125 deletions

File tree

iddm/config/choices.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# Choice settings
1717
# Support option
1818
bool_choices = [True, False]
19-
sample_choices = ["ddpm", "ddim", "plms"]
19+
sample_choices = ["ddpm", "ddim", "plms", "dpm2"]
2020
network_choices = ["unet", "cspdarkunet", "unetv2", "unet-slim", "unet-cross-attn", "unet-flash-self-attn",]
2121
optim_choices = ["adam", "adamw", "sgd"]
2222
act_choices = ["gelu", "silu", "relu", "relu6", "lrelu"]

iddm/model/samples/base.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
"""
88
import math
99
from typing import Optional, List, Union, Tuple
10+
import logging
11+
import coloredlogs
1012

1113
import torch
1214
from torch import nn
1315

1416
from iddm.config.setting import DEFAULT_IMAGE_SIZE, IMAGE_CHANNEL
1517

18+
logger = logging.getLogger(__name__)
19+
coloredlogs.install(level="INFO")
20+
1621

1722
class BaseDiffusion:
1823
"""
@@ -176,15 +181,42 @@ def sample(
176181
cfg_scale: Optional[float] = None
177182
) -> torch.Tensor:
178183
"""
179-
Sample method, this method should be implemented in the subclass
184+
Sample method, it is inherited by subclasses
180185
:param model: Model
181186
:param x: Input image tensor, if provided, will be used as the starting point for sampling
182187
:param n: Number of sample images, x priority is greater than n
183188
:param labels: Labels
184189
:param cfg_scale: classifier-free guidance interpolation weight, users can better generate model effect.
185190
:return: Sample images
186191
"""
187-
pass
192+
x, n = self._get_input_image(n=n, x=x)
193+
logger.info(msg=f"{self.__class__.__name__} Sampling {n} images...")
194+
model.eval()
195+
with torch.no_grad():
196+
# Subclasses implement specific iterative logic
197+
x = self._sample_loop(model, x, n, labels, cfg_scale)
198+
x = self.post_process(x)
199+
model.train()
200+
return x
201+
202+
def _sample_loop(
203+
self,
204+
model: nn.Module,
205+
x: Optional[torch.Tensor] = None,
206+
n: int = 1,
207+
labels: Optional[torch.Tensor] = None,
208+
cfg_scale: Optional[float] = None
209+
) -> torch.Tensor:
210+
"""
211+
Sampling iteration logic, to be implemented in subclasses
212+
:param model: Model
213+
:param x: Input image tensor
214+
:param n: Number of sample images
215+
:param labels: Labels
216+
:param cfg_scale: Classifier-free guidance scale
217+
:return: None
218+
"""
219+
raise NotImplementedError("Subclasses need to implement sampling iteration logic")
188220

189221
def _get_input_image(
190222
self,

iddm/model/samples/ddim.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def _init_time_step(self):
7373
self.time_step = reversed(torch.cat((torch.tensor([0], dtype=torch.long), self.time_step)))
7474
self.time_step = list(zip(self.time_step[:-1], self.time_step[1:]))
7575

76-
def sample(
76+
def _sample_loop(
7777
self,
7878
model: nn.Module,
7979
x: Optional[torch.Tensor] = None,
@@ -82,7 +82,7 @@ def sample(
8282
cfg_scale: Optional[float] = None
8383
) -> torch.Tensor:
8484
"""
85-
DDIM sample method
85+
DDPM sample loop method
8686
:param model: Model
8787
:param x: Input image tensor, if provided, will be used as the starting point for sampling
8888
:param n: Number of sample images, x priority is greater than n
@@ -91,35 +91,28 @@ def sample(
9191
Avoiding the posterior collapse problem, Reference paper: 'Classifier-Free Diffusion Guidance'
9292
:return: Sample images
9393
"""
94-
# Input dim: [n, img_channel, img_size_h, img_size_w]
95-
x, n = self._get_input_image(n=n, x=x)
96-
logger.info(msg=f"DDIM Sampling {n} new images....")
97-
model.eval()
98-
with torch.no_grad():
99-
# The list of current time and previous time
100-
for i, p_i in tqdm(self.time_step):
101-
# Time step, creating a tensor of size n
102-
t = (torch.ones(n) * i).long().to(self.device)
103-
# Previous time step, creating a tensor of size n
104-
p_t = (torch.ones(n) * p_i).long().to(self.device)
105-
# Expand to a 4-dimensional tensor, and get the value according to the time step t
106-
alpha_t = self.alpha_hat[t][:, None, None, None]
107-
alpha_prev = self.alpha_hat[p_t][:, None, None, None]
108-
noise = torch.randn_like(x) if i > 1 else torch.zeros_like(x)
94+
# The list of current time and previous time
95+
for i, p_i in tqdm(self.time_step):
96+
# Time step, creating a tensor of size n
97+
t = (torch.ones(n) * i).long().to(self.device)
98+
# Previous time step, creating a tensor of size n
99+
p_t = (torch.ones(n) * p_i).long().to(self.device)
100+
# Expand to a 4-dimensional tensor, and get the value according to the time step t
101+
alpha_t = self.alpha_hat[t][:, None, None, None]
102+
alpha_prev = self.alpha_hat[p_t][:, None, None, None]
103+
noise = torch.randn_like(x) if i > 1 else torch.zeros_like(x)
109104

110-
# Predict noise
111-
predicted_noise = self._get_predicted_noise(model, x, t, labels, cfg_scale)
105+
# Predict noise
106+
predicted_noise = self._get_predicted_noise(model, x, t, labels, cfg_scale)
107+
108+
# Calculation formula
109+
# Division would cause the value to be too large or too small, and it needs to be constrained
110+
# https://github.com/ermongroup/ddim/blob/main/functions/denoising.py#L54C12-L54C54
111+
x0_t = torch.clamp((x - (predicted_noise * torch.sqrt((1 - alpha_t)))) / torch.sqrt(alpha_t), -1, 1)
112+
# Sigma
113+
c1 = self.eta * torch.sqrt((1 - alpha_t / alpha_prev) * (1 - alpha_prev) / (1 - alpha_t))
114+
c2 = torch.sqrt((1 - alpha_prev) - c1 ** 2)
115+
# Predicted x0 + direction pointing to xt + sigma * predicted noise
116+
x = torch.sqrt(alpha_prev) * x0_t + c2 * predicted_noise + c1 * noise
112117

113-
# Calculation formula
114-
# Division would cause the value to be too large or too small, and it needs to be constrained
115-
# https://github.com/ermongroup/ddim/blob/main/functions/denoising.py#L54C12-L54C54
116-
x0_t = torch.clamp((x - (predicted_noise * torch.sqrt((1 - alpha_t)))) / torch.sqrt(alpha_t), -1, 1)
117-
# Sigma
118-
c1 = self.eta * torch.sqrt((1 - alpha_t / alpha_prev) * (1 - alpha_prev) / (1 - alpha_t))
119-
c2 = torch.sqrt((1 - alpha_prev) - c1 ** 2)
120-
# Predicted x0 + direction pointing to xt + sigma * predicted noise
121-
x = torch.sqrt(alpha_prev) * x0_t + c2 * predicted_noise + c1 * noise
122-
# Post process
123-
x = self.post_process(x=x)
124-
model.train()
125118
return x

iddm/model/samples/ddpm.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656
device=device, schedule_name=schedule_name, latent=latent, latent_channel=latent_channel,
5757
autoencoder=autoencoder)
5858

59-
def sample(
59+
def _sample_loop(
6060
self,
6161
model: nn.Module,
6262
x: Optional[torch.Tensor] = None,
@@ -65,7 +65,7 @@ def sample(
6565
cfg_scale: Optional[float] = None
6666
) -> torch.Tensor:
6767
"""
68-
DDPM sample method
68+
DDPM sample loop method
6969
:param model: Model
7070
:param x: Input image tensor, if provided, will be used as the starting point for sampling
7171
:param n: Number of sample images, x priority is greater than n
@@ -74,42 +74,32 @@ def sample(
7474
Avoiding the posterior collapse problem, Reference paper: 'Classifier-Free Diffusion Guidance'
7575
:return: Sample images
7676
"""
77-
# Input dim: [n, img_channel, img_size_h, img_size_w]
78-
x, n = self._get_input_image(n=n, x=x)
79-
logger.info(msg=f"DDPM Sampling {n} new images....")
80-
model.eval()
81-
with torch.no_grad():
82-
# 'reversed(range(1, self.noise_steps)' iterates over a sequence of integers in reverse
83-
for i in tqdm(reversed(range(1, self.noise_steps)), position=0, total=self.noise_steps - 1):
84-
# Time step, creating a tensor of size n
85-
t = (torch.ones(n) * i).long().to(self.device)
77+
# 'reversed(range(1, self.noise_steps)' iterates over a sequence of integers in reverse
78+
for i in tqdm(reversed(range(1, self.noise_steps)), position=0, total=self.noise_steps - 1):
79+
# Time step, creating a tensor of size n
80+
t = (torch.ones(n) * i).long().to(self.device)
8681

87-
# Whether the network has conditional input, such as multiple category input
88-
# Predict noise
89-
predicted_noise = self._get_predicted_noise(model, x, t, labels, cfg_scale)
82+
# Whether the network has conditional input, such as multiple category input
83+
# Predict noise
84+
predicted_noise = self._get_predicted_noise(model, x, t, labels, cfg_scale)
9085

91-
# Expand to a 4-dimensional tensor, and get the value according to the time step t
92-
alpha = self.alpha[t][:, None, None, None]
93-
alpha_hat = self.alpha_hat[t][:, None, None, None]
94-
beta = self.beta[t][:, None, None, None]
95-
# Only noise with a step size greater than 1 is required.
96-
# For details, refer to line 3 of Algorithm 2 on page 4 of the paper
97-
noise = torch.randn_like(x) if i > 1 else torch.zeros_like(x)
86+
# Expand to a 4-dimensional tensor, and get the value according to the time step t
87+
alpha = self.alpha[t][:, None, None, None]
88+
alpha_hat = self.alpha_hat[t][:, None, None, None]
89+
beta = self.beta[t][:, None, None, None]
90+
# Only noise with a step size greater than 1 is required.
91+
# For details, refer to line 3 of Algorithm 2 on page 4 of the paper
92+
noise = torch.randn_like(x) if i > 1 else torch.zeros_like(x)
9893

99-
# Fix latent diffusion explosion problem
100-
if self.latent:
101-
x = x.clamp(-1, 1)
102-
103-
# In each epoch, use x to calculate t - 1 of x
104-
# For details, refer to line 4 of Algorithm 2 on page 4 of the paper
105-
x = 1 / torch.sqrt(alpha) * (
106-
x - ((1 - alpha) / (torch.sqrt(1 - alpha_hat))) * predicted_noise) + torch.sqrt(
107-
beta) * noise
108-
# Post process, output of constraint x
94+
# Fix latent diffusion explosion problem
10995
if self.latent:
110-
x = self.post_process(x=x)
111-
else:
112-
x = self.post_process(x=x.clamp(-1, 1))
96+
x = x.clamp(-1, 1)
97+
98+
# In each epoch, use x to calculate t - 1 of x
99+
# For details, refer to line 4 of Algorithm 2 on page 4 of the paper
100+
x = 1 / torch.sqrt(alpha) * (
101+
x - ((1 - alpha) / (torch.sqrt(1 - alpha_hat))) * predicted_noise) + torch.sqrt(
102+
beta) * noise
113103

114-
model.train()
104+
x = x if self.latent else x.clamp(-1, 1)
115105
return x

iddm/model/samples/dpm2.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python
2+
# -*- coding:utf-8 -*-
3+
"""
4+
@Date : 2025/9/2 16:18
5+
@Author : Elizabeth
6+
@Site : https://github.com/RachelElizaUK
7+
"""
8+
from typing import Optional, List, Union
9+
10+
import torch
11+
import logging
12+
import coloredlogs
13+
from torch import nn
14+
15+
from tqdm import tqdm
16+
17+
from iddm.model.samples.base import BaseDiffusion
18+
19+
logger = logging.getLogger(__name__)
20+
coloredlogs.install(level="INFO")
21+
22+
23+
class DPM2Diffusion(BaseDiffusion):
24+
"""
25+
DPM2 sampler class
26+
"""
27+
28+
def __init__(
29+
self,
30+
noise_steps: int = 1000,
31+
sample_steps: int = 50,
32+
beta_start: float = 1e-4,
33+
beta_end: float = 2e-2,
34+
img_size: Optional[List[int]] = None,
35+
device: Union[str, torch.device] = "cpu",
36+
schedule_name: str = "linear",
37+
latent: bool = False,
38+
latent_channel: int = 8,
39+
autoencoder: Optional[nn.Module] = None
40+
):
41+
"""
42+
The implement of DPM2
43+
:param noise_steps: Total noise steps
44+
:param sample_steps: Sampling steps (DPM2 typically uses fewer steps than full noise steps)
45+
:param beta_start: β start value
46+
:param beta_end: β end value
47+
:param img_size: Image size
48+
:param device: Device type
49+
:param schedule_name: Noise schedule name
50+
:param latent: Whether to use latent diffusion
51+
:param latent_channel: Latent channel size
52+
:param autoencoder: Autoencoder model for latent diffusion
53+
"""
54+
super().__init__(
55+
noise_steps=noise_steps,
56+
beta_start=beta_start,
57+
beta_end=beta_end,
58+
img_size=img_size,
59+
device=device,
60+
schedule_name=schedule_name,
61+
latent=latent,
62+
latent_channel=latent_channel,
63+
autoencoder=autoencoder
64+
)
65+
self.sample_steps = sample_steps
66+
self.eta = 0.0 # DPM2 uses deterministic path by default
67+
self._init_time_steps()
68+
69+
def _init_time_steps(self):
70+
"""
71+
Initialize time steps for DPM2 sampling
72+
Creates a sequence of time steps from T down to 0 with equal intervals
73+
"""
74+
step_ratio = self.noise_steps // self.sample_steps
75+
self.time_steps = torch.arange(0, self.noise_steps, step_ratio).long() + 1
76+
self.time_steps = reversed(torch.cat((torch.tensor([0], dtype=torch.long), self.time_steps)))
77+
self.time_steps = list(zip(self.time_steps[:-1], self.time_steps[1:]))
78+
79+
def sample(
80+
self,
81+
model: nn.Module,
82+
x: Optional[torch.Tensor] = None,
83+
n: int = 1,
84+
labels: Optional[torch.Tensor] = None,
85+
cfg_scale: Optional[float] = None
86+
) -> torch.Tensor:
87+
"""
88+
DPM2 sampling method
89+
:param model: Diffusion model
90+
:param x: Initial input tensor (optional)
91+
:param n: Number of samples to generate
92+
:param labels: Conditional labels (optional)
93+
:param cfg_scale: Classifier-free guidance scale (optional)
94+
:return: Generated images tensor
95+
"""
96+
# Get initial input image
97+
x, n = self._get_input_image(n=n, x=x)
98+
logger.info(msg=f"DPM2 Sampling {n} new images....")
99+
model.eval()
100+
101+
with torch.no_grad():
102+
for i, p_i in tqdm(self.time_steps, position=0, total=len(self.time_steps)):
103+
# Current and previous time steps
104+
t = (torch.ones(n) * i).long().to(self.device)
105+
p_t = (torch.ones(n) * p_i).long().to(self.device)
106+
107+
# Get alpha values for current and previous steps
108+
alpha_curr = self.alpha_hat[t][:, None, None, None]
109+
alpha_prev = self.alpha_hat[p_t][:, None, None, None]
110+
111+
# Step 1: Predict noise at current time step
112+
predicted_noise = self._get_predicted_noise(model, x, t, labels, cfg_scale)
113+
114+
# Step 2: Compute x0 from current x and predicted noise
115+
x0 = (x - torch.sqrt(1 - alpha_curr) * predicted_noise) / torch.sqrt(alpha_curr)
116+
x0 = torch.clamp(x0, -1.0, 1.0) # Stabilize x0 prediction
117+
118+
# Step 3: Midpoint prediction (DPM2 uses 2nd-order method)
119+
t_mid = (t + p_t) // 2
120+
alpha_mid = self.alpha_hat[t_mid][:, None, None, None]
121+
122+
# Compute midpoint x
123+
x_mid = torch.sqrt(alpha_mid) * x0 + torch.sqrt(1 - alpha_mid) * predicted_noise
124+
125+
# Predict noise at midpoint
126+
predicted_noise_mid = self._get_predicted_noise(model, x_mid, t_mid, labels, cfg_scale)
127+
128+
# Step 4: Update x using midpoint correction
129+
x = torch.sqrt(alpha_prev) * x0 + torch.sqrt(1 - alpha_prev) * predicted_noise_mid
130+
131+
# Add noise if using stochastic sampling (eta > 0)
132+
if self.eta > 0 and i > 1:
133+
sigma = self.eta * torch.sqrt(
134+
(1 - alpha_prev) / (1 - alpha_curr) * (1 - alpha_curr / alpha_prev)
135+
)
136+
x += sigma * torch.randn_like(x)
137+
138+
x = self.post_process(x=x)
139+
140+
model.train()
141+
return x

0 commit comments

Comments
 (0)