mirror of
https://github.com/wassname/denoising-diffusion-pytorch.git
synced 2026-09-10 12:01:08 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d59acdfd | ||
|
|
689593a579 | ||
|
|
eba44498d1 | ||
|
|
12f95b33d8 | ||
|
|
6b504c4ae9 | ||
|
|
37334ae824 | ||
|
|
6eba6cdd50 | ||
|
|
555566c188 | ||
|
|
2b742dd2cc | ||
|
|
1345a8a41d | ||
|
|
931a5af2c3 | ||
|
|
a0c3443eaa | ||
|
|
662172851b | ||
|
|
0248b5e4d3 | ||
|
|
6b56af08a2 | ||
|
|
d4420248f1 | ||
|
|
a536e5bee9 | ||
|
|
1b85379d3a | ||
|
|
8859864f63 | ||
|
|
32657f035f | ||
|
|
d97bc0278c | ||
|
|
8408775cfc | ||
|
|
86fcb6785b | ||
|
|
c535d31fc5 | ||
|
|
5db64fec4b | ||
|
|
b87ea27781 | ||
|
|
a8403b83fe | ||
|
|
f4b1d7a67c | ||
|
|
618493714f | ||
|
|
76b79aa847 | ||
|
|
be2bd8d320 | ||
|
|
c3d1607019 | ||
|
|
06b2e52645 | ||
|
|
09b8a1c805 | ||
|
|
d26acbcae6 | ||
|
|
9939a48139 | ||
|
|
75ea49a7ef | ||
|
|
8c3609a6e3 | ||
|
|
1586d1a8a0 | ||
|
|
b4fb8804d2 | ||
|
|
9fd05f1b1f | ||
|
|
ec2397f0ba | ||
|
|
844e557dfb | ||
|
|
8b30be8042 | ||
|
|
f2f3994b92 | ||
|
|
8ec4ea56a5 | ||
|
|
99cf9b5b96 | ||
|
|
ecc6f30901 | ||
|
|
f900f40f14 | ||
|
|
479f60c178 | ||
|
|
96bb2ff310 | ||
|
|
582bfe275b | ||
|
|
4284c8840d | ||
|
|
d4ffa3fced | ||
|
|
c44d3ea01d | ||
|
|
c4991f576f | ||
|
|
a19331aa59 | ||
|
|
94eabaca1a | ||
|
|
eaf9d9fdc4 | ||
|
|
3bf5e768c2 | ||
|
|
532178a6a3 | ||
|
|
3bbb6ebf16 | ||
|
|
6b93fa48f6 | ||
|
|
a291da5098 | ||
|
|
e5a18bb25c | ||
|
|
fc8e4547aa | ||
|
|
cae9f4a71f | ||
|
|
91f03fb88b | ||
|
|
60128257c5 | ||
|
|
cf6db71985 | ||
|
|
84ebb9ad13 | ||
|
|
caa5af170d | ||
|
|
55c658b967 | ||
|
|
e0f26677d6 | ||
|
|
e147839d74 | ||
|
|
62e8490385 | ||
|
|
d412d8816b | ||
|
|
402b7c26df | ||
|
|
09613a40f3 | ||
|
|
c6966ae95a | ||
|
|
73591cf1ad | ||
|
|
989f0fcb8e | ||
|
|
84731bb03d | ||
|
|
c6ecca555b | ||
|
|
1f5c233072 | ||
|
|
de378158e5 | ||
|
|
e274fb305a | ||
|
|
f39b3b1d3f | ||
|
|
782c904d3b | ||
|
|
71953ebd22 | ||
|
|
0b8cdb4c8b | ||
|
|
e504e0e554 | ||
|
|
bd1e3b676e | ||
|
|
f4615599bc | ||
|
|
eb6e1b508e | ||
|
|
91cff45939 | ||
|
|
7b51e30da7 | ||
|
|
dadbf20154 | ||
|
|
7706bdfc6f | ||
|
|
183e5f3cc5 | ||
|
|
16c9ae7bb3 | ||
|
|
f5916111f8 | ||
|
|
ad9e303ff3 | ||
|
|
ae42f48f6a | ||
|
|
5989f4c77e | ||
|
|
2082046888 | ||
|
|
3c5b7e2d56 | ||
|
|
d4ce9f6c38 | ||
|
|
ff451f697e | ||
|
|
3d96532c60 | ||
|
|
ef2ca0b625 | ||
|
|
9f95a03c07 | ||
|
|
a4c68d3569 | ||
|
|
b33a48e342 | ||
|
|
8e5fb17063 | ||
|
|
4bf28914bc | ||
|
|
88f83d0ff2 | ||
|
|
26b5cab6c8 |
@@ -1,3 +1,6 @@
|
||||
# Generation results
|
||||
results/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<img src="./denoising-diffusion.png" width="500px"></img>
|
||||
<img src="./images/denoising-diffusion.png" width="500px"></img>
|
||||
|
||||
## Denoising Diffusion Probabilistic Model, in Pytorch
|
||||
|
||||
Implementation of <a href="https://arxiv.org/abs/2006.11239">Denoising Diffusion Probabilistic Model</a> in Pytorch. It is a new approach to generative modeling that may <a href="https://ajolicoeur.wordpress.com/the-new-contender-to-gans-score-matching-with-langevin-sampling/">have the potential</a> to rival GANs. It uses denoising score matching to estimate the gradient of the data distribution, followed by Langevin sampling to sample from the true distribution. This implementation was transcribed from the official Tensorflow version <a href="https://github.com/hojonathanho/diffusion">here</a>.
|
||||
Implementation of <a href="https://arxiv.org/abs/2006.11239">Denoising Diffusion Probabilistic Model</a> in Pytorch. It is a new approach to generative modeling that may <a href="https://ajolicoeur.wordpress.com/the-new-contender-to-gans-score-matching-with-langevin-sampling/">have the potential</a> to rival GANs. It uses denoising score matching to estimate the gradient of the data distribution, followed by Langevin sampling to sample from the true distribution.
|
||||
|
||||
<img src="./sample.png" width="500px"><img>
|
||||
This implementation was transcribed from the official Tensorflow version <a href="https://github.com/hojonathanho/diffusion">here</a>
|
||||
|
||||
Youtube AI Educators - <a href="https://www.youtube.com/watch?v=W-O7AZNzbzQ">Yannic Kilcher</a> | <a href="https://www.youtube.com/watch?v=344w5h24-h8">AI Coffeebreak with Letitia</a> | <a href="https://www.youtube.com/watch?v=HoKDTa5jHvg">Outlier</a>
|
||||
|
||||
<a href="https://huggingface.co/blog/annotated-diffusion">Annotated code</a> by Research Scientists / Engineers from <a href="https://huggingface.co/">🤗 Huggingface</a>
|
||||
|
||||
<img src="./images/sample.png" width="500px"><img>
|
||||
|
||||
[](https://badge.fury.io/py/denoising-diffusion-pytorch)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -25,18 +33,17 @@ model = Unet(
|
||||
|
||||
diffusion = GaussianDiffusion(
|
||||
model,
|
||||
beta_start = 0.0001,
|
||||
beta_end = 0.02,
|
||||
num_diffusion_timesteps = 1000, # number of steps
|
||||
loss_type = 'l1' # L1 or L2 (wavegrad paper claims l1 is better?)
|
||||
image_size = 128,
|
||||
timesteps = 1000, # number of steps
|
||||
loss_type = 'l1' # L1 or L2
|
||||
)
|
||||
|
||||
training_images = torch.randn(8, 3, 128, 128)
|
||||
training_images = torch.randn(8, 3, 128, 128) # images are normalized from 0 to 1
|
||||
loss = diffusion(training_images)
|
||||
loss.backward()
|
||||
# after a lot of training
|
||||
|
||||
sampled_images = diffusion.sample(128, batch_size = 4)
|
||||
sampled_images = diffusion.sample(batch_size = 4)
|
||||
sampled_images.shape # (4, 3, 128, 128)
|
||||
```
|
||||
|
||||
@@ -52,37 +59,125 @@ model = Unet(
|
||||
|
||||
diffusion = GaussianDiffusion(
|
||||
model,
|
||||
beta_start = 0.0001,
|
||||
beta_end = 0.02,
|
||||
num_diffusion_timesteps = 1000, # number of steps
|
||||
loss_type = 'l1' # L1 or L2
|
||||
image_size = 128,
|
||||
timesteps = 1000, # number of steps
|
||||
sampling_timesteps = 250, # number of sampling timesteps (using ddim for faster inference [see citation for ddim paper])
|
||||
loss_type = 'l1' # L1 or L2
|
||||
).cuda()
|
||||
|
||||
trainer = Trainer(
|
||||
diffusion,
|
||||
'path/to/your/images',
|
||||
image_size = 128,
|
||||
train_batch_size = 32,
|
||||
train_lr = 2e-5,
|
||||
train_num_steps = 100000, # total training steps
|
||||
train_lr = 8e-5,
|
||||
train_num_steps = 700000, # total training steps
|
||||
gradient_accumulate_every = 2, # gradient accumulation steps
|
||||
ema_decay = 0.995 # exponential moving average decay
|
||||
ema_decay = 0.995, # exponential moving average decay
|
||||
amp = True # turn on mixed precision
|
||||
)
|
||||
|
||||
trainer.train()
|
||||
```
|
||||
|
||||
Todo: Command line tool for one-line training
|
||||
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
|
||||
@misc{ho2020denoising,
|
||||
title={Denoising Diffusion Probabilistic Models},
|
||||
author={Jonathan Ho and Ajay Jain and Pieter Abbeel},
|
||||
year={2020},
|
||||
eprint={2006.11239},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.LG}
|
||||
@inproceedings{NEURIPS2020_4c5bcfec,
|
||||
author = {Ho, Jonathan and Jain, Ajay and Abbeel, Pieter},
|
||||
booktitle = {Advances in Neural Information Processing Systems},
|
||||
editor = {H. Larochelle and M. Ranzato and R. Hadsell and M.F. Balcan and H. Lin},
|
||||
pages = {6840--6851},
|
||||
publisher = {Curran Associates, Inc.},
|
||||
title = {Denoising Diffusion Probabilistic Models},
|
||||
url = {https://proceedings.neurips.cc/paper/2020/file/4c5bcfec8584af0d967f1ab10179ca4b-Paper.pdf},
|
||||
volume = {33},
|
||||
year = {2020}
|
||||
}
|
||||
```
|
||||
|
||||
```bibtex
|
||||
@InProceedings{pmlr-v139-nichol21a,
|
||||
title = {Improved Denoising Diffusion Probabilistic Models},
|
||||
author = {Nichol, Alexander Quinn and Dhariwal, Prafulla},
|
||||
booktitle = {Proceedings of the 38th International Conference on Machine Learning},
|
||||
pages = {8162--8171},
|
||||
year = {2021},
|
||||
editor = {Meila, Marina and Zhang, Tong},
|
||||
volume = {139},
|
||||
series = {Proceedings of Machine Learning Research},
|
||||
month = {18--24 Jul},
|
||||
publisher = {PMLR},
|
||||
pdf = {http://proceedings.mlr.press/v139/nichol21a/nichol21a.pdf},
|
||||
url = {https://proceedings.mlr.press/v139/nichol21a.html},
|
||||
}
|
||||
```
|
||||
|
||||
```bibtex
|
||||
@inproceedings{kingma2021on,
|
||||
title = {On Density Estimation with Diffusion Models},
|
||||
author = {Diederik P Kingma and Tim Salimans and Ben Poole and Jonathan Ho},
|
||||
booktitle = {Advances in Neural Information Processing Systems},
|
||||
editor = {A. Beygelzimer and Y. Dauphin and P. Liang and J. Wortman Vaughan},
|
||||
year = {2021},
|
||||
url = {https://openreview.net/forum?id=2LdBqxc1Yv}
|
||||
}
|
||||
```
|
||||
|
||||
```bibtex
|
||||
@article{Choi2022PerceptionPT,
|
||||
title = {Perception Prioritized Training of Diffusion Models},
|
||||
author = {Jooyoung Choi and Jungbeom Lee and Chaehun Shin and Sungwon Kim and Hyunwoo J. Kim and Sung-Hoon Yoon},
|
||||
journal = {ArXiv},
|
||||
year = {2022},
|
||||
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}
|
||||
}
|
||||
```
|
||||
|
||||
```bibtex
|
||||
@article{Song2021DenoisingDI,
|
||||
title = {Denoising Diffusion Implicit Models},
|
||||
author = {Jiaming Song and Chenlin Meng and Stefano Ermon},
|
||||
journal = {ArXiv},
|
||||
year = {2021},
|
||||
volume = {abs/2010.02502}
|
||||
}
|
||||
```
|
||||
|
||||
```bibtex
|
||||
@misc{chen2022analog,
|
||||
title = {Analog Bits: Generating Discrete Data using Diffusion Models with Self-Conditioning},
|
||||
author = {Ting Chen and Ruixiang Zhang and Geoffrey Hinton},
|
||||
year = {2022},
|
||||
eprint = {2208.04202},
|
||||
archivePrefix = {arXiv},
|
||||
primaryClass = {cs.CV}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
from denoising_diffusion_pytorch.denoising_diffusion_pytorch import GaussianDiffusion, Unet, Trainer
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import math
|
||||
import torch
|
||||
from torch import sqrt
|
||||
from torch import nn, einsum
|
||||
import torch.nn.functional as F
|
||||
from torch.special import expm1
|
||||
|
||||
from tqdm import tqdm
|
||||
from einops import rearrange, repeat, reduce
|
||||
from einops.layers.torch import Rearrange
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# diffusion helpers
|
||||
|
||||
def right_pad_dims_to(x, t):
|
||||
padding_dims = x.ndim - t.ndim
|
||||
if padding_dims <= 0:
|
||||
return t
|
||||
return t.view(*t.shape, *((1,) * padding_dims))
|
||||
|
||||
# neural net helpers
|
||||
|
||||
class Residual(nn.Module):
|
||||
def __init__(self, fn):
|
||||
super().__init__()
|
||||
self.fn = fn
|
||||
|
||||
def forward(self, x):
|
||||
return x + self.fn(x)
|
||||
|
||||
class MonotonicLinear(nn.Module):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
self.net = nn.Linear(*args, **kwargs)
|
||||
|
||||
def forward(self, x):
|
||||
return F.linear(x, self.net.weight.abs(), self.net.bias.abs())
|
||||
|
||||
# continuous schedules
|
||||
|
||||
# equations are taken from https://openreview.net/attachment?id=2LdBqxc1Yv&name=supplementary_material
|
||||
# @crowsonkb Katherine's repository also helped here https://github.com/crowsonkb/v-diffusion-jax/blob/master/diffusion/utils.py
|
||||
|
||||
# log(snr) that approximates the original linear schedule
|
||||
|
||||
def log(t, eps = 1e-20):
|
||||
return torch.log(t.clamp(min = eps))
|
||||
|
||||
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) * 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 """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
log_snr_max,
|
||||
log_snr_min,
|
||||
hidden_dim = 1024,
|
||||
frac_gradient = 1.
|
||||
):
|
||||
super().__init__()
|
||||
self.slope = log_snr_min - log_snr_max
|
||||
self.intercept = log_snr_max
|
||||
|
||||
self.net = nn.Sequential(
|
||||
Rearrange('... -> ... 1'),
|
||||
MonotonicLinear(1, 1),
|
||||
Residual(nn.Sequential(
|
||||
MonotonicLinear(1, hidden_dim),
|
||||
nn.Sigmoid(),
|
||||
MonotonicLinear(hidden_dim, 1)
|
||||
)),
|
||||
Rearrange('... 1 -> ...'),
|
||||
)
|
||||
|
||||
self.frac_gradient = frac_gradient
|
||||
|
||||
def forward(self, x):
|
||||
frac_gradient = self.frac_gradient
|
||||
device = x.device
|
||||
|
||||
out_zero = self.net(torch.zeros_like(x))
|
||||
out_one = self.net(torch.ones_like(x))
|
||||
|
||||
x = self.net(x)
|
||||
|
||||
normed = self.slope * ((x - out_zero) / (out_one - out_zero)) + self.intercept
|
||||
return normed * frac_gradient + normed.detach() * (1 - frac_gradient)
|
||||
|
||||
class ContinuousTimeGaussianDiffusion(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
*,
|
||||
image_size,
|
||||
channels = 3,
|
||||
loss_type = 'l1',
|
||||
noise_schedule = 'linear',
|
||||
num_sample_steps = 500,
|
||||
clip_sample_denoised = True,
|
||||
learned_schedule_net_hidden_dim = 1024,
|
||||
learned_noise_schedule_frac_gradient = 1., # between 0 and 1, determines what percentage of gradients go back, so one can update the learned noise schedule more slowly
|
||||
p2_loss_weight_gamma = 0., # p2 loss weight, from https://arxiv.org/abs/2204.00227 - 0 is equivalent to weight of 1 across time
|
||||
p2_loss_weight_k = 1
|
||||
):
|
||||
super().__init__()
|
||||
assert model.learned_sinusoidal_cond
|
||||
assert not model.self_condition, 'not supported yet'
|
||||
|
||||
self.model = model
|
||||
|
||||
# image dimensions
|
||||
|
||||
self.channels = channels
|
||||
self.image_size = image_size
|
||||
|
||||
# continuous noise schedule related stuff
|
||||
|
||||
self.loss_type = loss_type
|
||||
|
||||
if noise_schedule == 'linear':
|
||||
self.log_snr = beta_linear_log_snr
|
||||
elif noise_schedule == 'cosine':
|
||||
self.log_snr = alpha_cosine_log_snr
|
||||
elif noise_schedule == 'learned':
|
||||
log_snr_max, log_snr_min = [beta_linear_log_snr(torch.tensor([time])).item() for time in (0., 1.)]
|
||||
|
||||
self.log_snr = learned_noise_schedule(
|
||||
log_snr_max = log_snr_max,
|
||||
log_snr_min = log_snr_min,
|
||||
hidden_dim = learned_schedule_net_hidden_dim,
|
||||
frac_gradient = learned_noise_schedule_frac_gradient
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'unknown noise schedule {noise_schedule}')
|
||||
|
||||
# sampling
|
||||
|
||||
self.num_sample_steps = num_sample_steps
|
||||
self.clip_sample_denoised = clip_sample_denoised
|
||||
|
||||
# p2 loss weight
|
||||
# proposed https://arxiv.org/abs/2204.00227
|
||||
|
||||
assert p2_loss_weight_gamma <= 2, 'in paper, they noticed any gamma greater than 2 is harmful'
|
||||
|
||||
self.p2_loss_weight_gamma = p2_loss_weight_gamma # recommended to be 0.5 or 1
|
||||
self.p2_loss_weight_k = p2_loss_weight_k
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return next(self.model.parameters()).device
|
||||
|
||||
@property
|
||||
def loss_fn(self):
|
||||
if self.loss_type == 'l1':
|
||||
return F.l1_loss
|
||||
elif self.loss_type == 'l2':
|
||||
return F.mse_loss
|
||||
else:
|
||||
raise ValueError(f'invalid loss type {self.loss_type}')
|
||||
|
||||
def p_mean_variance(self, x, time, time_next):
|
||||
# reviewer found an error in the equation in the paper (missing sigma)
|
||||
# following - https://openreview.net/forum?id=2LdBqxc1Yv¬eId=rIQgH0zKsRt
|
||||
|
||||
log_snr = self.log_snr(time)
|
||||
log_snr_next = self.log_snr(time_next)
|
||||
c = -expm1(log_snr - log_snr_next)
|
||||
|
||||
squared_alpha, squared_alpha_next = log_snr.sigmoid(), log_snr_next.sigmoid()
|
||||
squared_sigma, squared_sigma_next = (-log_snr).sigmoid(), (-log_snr_next).sigmoid()
|
||||
|
||||
alpha, sigma, alpha_next = map(sqrt, (squared_alpha, squared_sigma, squared_alpha_next))
|
||||
|
||||
batch_log_snr = repeat(log_snr, ' -> b', b = x.shape[0])
|
||||
pred_noise = self.model(x, batch_log_snr)
|
||||
|
||||
if self.clip_sample_denoised:
|
||||
x_start = (x - sigma * pred_noise) / alpha
|
||||
|
||||
# in Imagen, this was changed to dynamic thresholding
|
||||
x_start.clamp_(-1., 1.)
|
||||
|
||||
model_mean = alpha_next * (x * (1 - c) / alpha + c * x_start)
|
||||
else:
|
||||
model_mean = alpha_next / alpha * (x - c * sigma * pred_noise)
|
||||
|
||||
posterior_variance = squared_sigma_next * c
|
||||
|
||||
return model_mean, posterior_variance
|
||||
|
||||
# sampling related functions
|
||||
|
||||
@torch.no_grad()
|
||||
def p_sample(self, x, time, time_next):
|
||||
batch, *_, device = *x.shape, x.device
|
||||
|
||||
model_mean, model_variance = self.p_mean_variance(x = x, time = time, time_next = time_next)
|
||||
|
||||
if time_next == 0:
|
||||
return model_mean
|
||||
|
||||
noise = torch.randn_like(x)
|
||||
return model_mean + sqrt(model_variance) * noise
|
||||
|
||||
@torch.no_grad()
|
||||
def p_sample_loop(self, shape):
|
||||
batch = shape[0]
|
||||
|
||||
img = torch.randn(shape, device = self.device)
|
||||
steps = torch.linspace(1., 0., self.num_sample_steps + 1, device = self.device)
|
||||
|
||||
for i in tqdm(range(self.num_sample_steps), desc = 'sampling loop time step', total = self.num_sample_steps):
|
||||
times = steps[i]
|
||||
times_next = steps[i + 1]
|
||||
img = self.p_sample(img, times, times_next)
|
||||
|
||||
img.clamp_(-1., 1.)
|
||||
img = unnormalize_to_zero_to_one(img)
|
||||
return img
|
||||
|
||||
@torch.no_grad()
|
||||
def sample(self, batch_size = 16):
|
||||
return self.p_sample_loop((batch_size, self.channels, self.image_size, self.image_size))
|
||||
|
||||
# training related functions - noise prediction
|
||||
|
||||
def q_sample(self, x_start, times, noise = None):
|
||||
noise = default(noise, lambda: torch.randn_like(x_start))
|
||||
|
||||
log_snr = self.log_snr(times)
|
||||
|
||||
log_snr_padded = right_pad_dims_to(x_start, log_snr)
|
||||
alpha, sigma = sqrt(log_snr_padded.sigmoid()), sqrt((-log_snr_padded).sigmoid())
|
||||
x_noised = x_start * alpha + noise * sigma
|
||||
|
||||
return x_noised, log_snr
|
||||
|
||||
def random_times(self, batch_size):
|
||||
# times are now uniform from 0 to 1
|
||||
return torch.zeros((batch_size,), device = self.device).float().uniform_(0, 1)
|
||||
|
||||
def p_losses(self, x_start, times, noise = None):
|
||||
noise = default(noise, lambda: torch.randn_like(x_start))
|
||||
|
||||
x, log_snr = self.q_sample(x_start = x_start, times = times, noise = noise)
|
||||
model_out = self.model(x, log_snr)
|
||||
|
||||
losses = self.loss_fn(model_out, noise, reduction = 'none')
|
||||
losses = reduce(losses, 'b ... -> b', 'mean')
|
||||
|
||||
if self.p2_loss_weight_gamma >= 0:
|
||||
# following eq 8. in https://arxiv.org/abs/2204.00227
|
||||
loss_weight = (self.p2_loss_weight_k + log_snr.exp()) ** -self.p2_loss_weight_gamma
|
||||
losses = losses * loss_weight
|
||||
|
||||
return losses.mean()
|
||||
|
||||
def forward(self, img, *args, **kwargs):
|
||||
b, c, h, w, device, img_size, = *img.shape, img.device, self.image_size
|
||||
assert h == img_size and w == img_size, f'height and width of image must be {img_size}'
|
||||
|
||||
times = self.random_times(b)
|
||||
img = normalize_to_neg_one_to_one(img)
|
||||
return self.p_losses(img, times, *args, **kwargs)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,221 @@
|
||||
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
|
||||
assert not net.self_condition, 'not supported yet'
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,151 @@
|
||||
import torch
|
||||
from collections import namedtuple
|
||||
from math import pi, sqrt, log as ln
|
||||
from inspect import isfunction
|
||||
from torch import nn, einsum
|
||||
from einops import rearrange
|
||||
|
||||
from denoising_diffusion_pytorch.denoising_diffusion_pytorch import GaussianDiffusion, extract, unnormalize_to_zero_to_one
|
||||
|
||||
# constants
|
||||
|
||||
NAT = 1. / ln(2)
|
||||
|
||||
ModelPrediction = namedtuple('ModelPrediction', ['pred_noise', 'pred_x_start', 'pred_variance'])
|
||||
|
||||
# helper functions
|
||||
|
||||
def exists(x):
|
||||
return x is not None
|
||||
|
||||
def default(val, d):
|
||||
if exists(val):
|
||||
return val
|
||||
return d() if isfunction(d) else d
|
||||
|
||||
# tensor helpers
|
||||
|
||||
def log(t, eps = 1e-15):
|
||||
return torch.log(t.clamp(min = eps))
|
||||
|
||||
def meanflat(x):
|
||||
return x.mean(dim = tuple(range(1, len(x.shape))))
|
||||
|
||||
def normal_kl(mean1, logvar1, mean2, logvar2):
|
||||
"""
|
||||
KL divergence between normal distributions parameterized by mean and log-variance.
|
||||
"""
|
||||
return 0.5 * (-1.0 + logvar2 - logvar1 + torch.exp(logvar1 - logvar2) + ((mean1 - mean2) ** 2) * torch.exp(-logvar2))
|
||||
|
||||
def approx_standard_normal_cdf(x):
|
||||
return 0.5 * (1.0 + torch.tanh(sqrt(2.0 / pi) * (x + 0.044715 * (x ** 3))))
|
||||
|
||||
def discretized_gaussian_log_likelihood(x, *, means, log_scales, thres = 0.999):
|
||||
assert x.shape == means.shape == log_scales.shape
|
||||
|
||||
centered_x = x - means
|
||||
inv_stdv = torch.exp(-log_scales)
|
||||
plus_in = inv_stdv * (centered_x + 1. / 255.)
|
||||
cdf_plus = approx_standard_normal_cdf(plus_in)
|
||||
min_in = inv_stdv * (centered_x - 1. / 255.)
|
||||
cdf_min = approx_standard_normal_cdf(min_in)
|
||||
log_cdf_plus = log(cdf_plus)
|
||||
log_one_minus_cdf_min = log(1. - cdf_min)
|
||||
cdf_delta = cdf_plus - cdf_min
|
||||
|
||||
log_probs = torch.where(x < -thres,
|
||||
log_cdf_plus,
|
||||
torch.where(x > thres,
|
||||
log_one_minus_cdf_min,
|
||||
log(cdf_delta)))
|
||||
|
||||
return log_probs
|
||||
|
||||
# https://arxiv.org/abs/2102.09672
|
||||
|
||||
# i thought the results were questionable, if one were to focus only on FID
|
||||
# but may as well get this in here for others to try, as GLIDE is using it (and DALL-E2 first stage of cascade)
|
||||
# gaussian diffusion for learned variance + hybrid eps simple + vb loss
|
||||
|
||||
class LearnedGaussianDiffusion(GaussianDiffusion):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
vb_loss_weight = 0.001, # lambda was 0.001 in the paper
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(model, *args, **kwargs)
|
||||
assert model.out_dim == (model.channels * 2), 'dimension out of unet must be twice the number of channels for learned variance - you can also set the `learned_variance` keyword argument on the Unet to be `True`'
|
||||
assert not model.self_condition, 'not supported yet'
|
||||
|
||||
self.vb_loss_weight = vb_loss_weight
|
||||
|
||||
def model_predictions(self, x, t):
|
||||
model_output = self.model(x, t)
|
||||
model_output, pred_variance = model_output.chunk(2, dim = 1)
|
||||
|
||||
if self.objective == 'pred_noise':
|
||||
pred_noise = model_output
|
||||
x_start = self.predict_start_from_noise(x, t, model_output)
|
||||
|
||||
elif self.objective == 'pred_x0':
|
||||
pred_noise = self.predict_noise_from_start(x, t, model_output)
|
||||
x_start = model_output
|
||||
|
||||
return ModelPrediction(pred_noise, x_start, pred_variance)
|
||||
|
||||
def p_mean_variance(self, *, x, t, clip_denoised, model_output = None):
|
||||
model_output = default(model_output, lambda: self.model(x, t))
|
||||
pred_noise, var_interp_frac_unnormalized = model_output.chunk(2, dim = 1)
|
||||
|
||||
min_log = extract(self.posterior_log_variance_clipped, t, x.shape)
|
||||
max_log = extract(torch.log(self.betas), t, x.shape)
|
||||
var_interp_frac = unnormalize_to_zero_to_one(var_interp_frac_unnormalized)
|
||||
|
||||
model_log_variance = var_interp_frac * max_log + (1 - var_interp_frac) * min_log
|
||||
model_variance = model_log_variance.exp()
|
||||
|
||||
x_start = self.predict_start_from_noise(x, t, pred_noise)
|
||||
|
||||
if clip_denoised:
|
||||
x_start.clamp_(-1., 1.)
|
||||
|
||||
model_mean, _, _ = self.q_posterior(x_start, x, t)
|
||||
|
||||
return model_mean, model_variance, model_log_variance
|
||||
|
||||
def p_losses(self, x_start, t, noise = None, clip_denoised = False):
|
||||
noise = default(noise, lambda: torch.randn_like(x_start))
|
||||
x_t = self.q_sample(x_start = x_start, t = t, noise = noise)
|
||||
|
||||
# model output
|
||||
|
||||
model_output = self.model(x_t, t)
|
||||
|
||||
# calculating kl loss for learned variance (interpolation)
|
||||
|
||||
true_mean, _, true_log_variance_clipped = self.q_posterior(x_start = x_start, x_t = x_t, t = t)
|
||||
model_mean, _, model_log_variance = self.p_mean_variance(x = x_t, t = t, clip_denoised = clip_denoised, model_output = model_output)
|
||||
|
||||
# kl loss with detached model predicted mean, for stability reasons as in paper
|
||||
|
||||
detached_model_mean = model_mean.detach()
|
||||
|
||||
kl = normal_kl(true_mean, true_log_variance_clipped, detached_model_mean, model_log_variance)
|
||||
kl = meanflat(kl) * NAT
|
||||
|
||||
decoder_nll = -discretized_gaussian_log_likelihood(x_start, means = detached_model_mean, log_scales = 0.5 * model_log_variance)
|
||||
decoder_nll = meanflat(decoder_nll) * NAT
|
||||
|
||||
# at the first timestep return the decoder NLL, otherwise return KL(q(x_{t-1}|x_t,x_0) || p(x_{t-1}|x_t))
|
||||
|
||||
vb_losses = torch.where(t == 0, decoder_nll, kl)
|
||||
|
||||
# simple loss - predicting noise, x0, or x_prev
|
||||
|
||||
pred_noise, _ = model_output.chunk(2, dim = 1)
|
||||
|
||||
simple_losses = self.loss_fn(pred_noise, noise)
|
||||
|
||||
return simple_losses + vb_losses.mean() * self.vb_loss_weight
|
||||
@@ -0,0 +1,82 @@
|
||||
import torch
|
||||
from inspect import isfunction
|
||||
from torch import nn, einsum
|
||||
from einops import rearrange
|
||||
|
||||
from denoising_diffusion_pytorch.denoising_diffusion_pytorch import GaussianDiffusion
|
||||
|
||||
# helper functions
|
||||
|
||||
def exists(x):
|
||||
return x is not None
|
||||
|
||||
def default(val, d):
|
||||
if exists(val):
|
||||
return val
|
||||
return d() if isfunction(d) else d
|
||||
|
||||
# some improvisation on my end
|
||||
# where i have the model learn to both predict noise and x0
|
||||
# and learn the weighted sum for each depending on time step
|
||||
|
||||
class WeightedObjectiveGaussianDiffusion(GaussianDiffusion):
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
*args,
|
||||
pred_noise_loss_weight = 0.1,
|
||||
pred_x_start_loss_weight = 0.1,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(model, *args, **kwargs)
|
||||
channels = model.channels
|
||||
assert model.out_dim == (channels * 2 + 2), 'dimension out (out_dim) of unet must be twice the number of channels + 2 (for the softmax weighted sum) - for channels of 3, this should be (3 * 2) + 2 = 8'
|
||||
assert not model.self_condition, 'not supported yet'
|
||||
assert not self.is_ddim_sampling, 'ddim sampling cannot be used'
|
||||
|
||||
self.split_dims = (channels, channels, 2)
|
||||
self.pred_noise_loss_weight = pred_noise_loss_weight
|
||||
self.pred_x_start_loss_weight = pred_x_start_loss_weight
|
||||
|
||||
def p_mean_variance(self, *, x, t, clip_denoised, model_output = None):
|
||||
model_output = self.model(x, t)
|
||||
|
||||
pred_noise, pred_x_start, weights = model_output.split(self.split_dims, dim = 1)
|
||||
normalized_weights = weights.softmax(dim = 1)
|
||||
|
||||
x_start_from_noise = self.predict_start_from_noise(x, t = t, noise = pred_noise)
|
||||
|
||||
x_starts = torch.stack((x_start_from_noise, pred_x_start), dim = 1)
|
||||
weighted_x_start = einsum('b j h w, b j c h w -> b c h w', normalized_weights, x_starts)
|
||||
|
||||
if clip_denoised:
|
||||
weighted_x_start.clamp_(-1., 1.)
|
||||
|
||||
model_mean, model_variance, model_log_variance = self.q_posterior(weighted_x_start, x, t)
|
||||
|
||||
return model_mean, model_variance, model_log_variance
|
||||
|
||||
def p_losses(self, x_start, t, noise = None, clip_denoised = False):
|
||||
noise = default(noise, lambda: torch.randn_like(x_start))
|
||||
x_t = self.q_sample(x_start = x_start, t = t, noise = noise)
|
||||
|
||||
model_output = self.model(x_t, t)
|
||||
pred_noise, pred_x_start, weights = model_output.split(self.split_dims, dim = 1)
|
||||
|
||||
# get loss for predicted noise and x_start
|
||||
# with the loss weight given at initialization
|
||||
|
||||
noise_loss = self.loss_fn(noise, pred_noise) * self.pred_noise_loss_weight
|
||||
x_start_loss = self.loss_fn(x_start, pred_x_start) * self.pred_x_start_loss_weight
|
||||
|
||||
# calculate x_start from predicted noise
|
||||
# then do a weighted sum of the x_start prediction, weights also predicted by the model (softmax normalized)
|
||||
|
||||
x_start_from_pred_noise = self.predict_start_from_noise(x_t, t, pred_noise)
|
||||
x_start_from_pred_noise = x_start_from_pred_noise.clamp(-2., 2.)
|
||||
weighted_x_start = einsum('b j h w, b j c h w -> b c h w', weights.softmax(dim = 1), torch.stack((x_start_from_pred_noise, pred_x_start), dim = 1))
|
||||
|
||||
# main loss to x_start with the weighted one
|
||||
|
||||
weighted_x_start_loss = self.loss_fn(x_start, weighted_x_start)
|
||||
return weighted_x_start_loss + x_start_loss + noise_loss
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 842 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -3,19 +3,21 @@ from setuptools import setup, find_packages
|
||||
setup(
|
||||
name = 'denoising-diffusion-pytorch',
|
||||
packages = find_packages(),
|
||||
version = '0.2.0',
|
||||
version = '0.27.1',
|
||||
license='MIT',
|
||||
description = 'Denoising Diffusion Probabilistic Models - Pytorch',
|
||||
author = 'Phil Wang',
|
||||
author_email = 'lucidrains@gmail.com',
|
||||
url = 'https://github.com/lucidrains/denoising-diffusion-pytorch',
|
||||
long_description_content_type = 'text/markdown',
|
||||
keywords = [
|
||||
'artificial intelligence',
|
||||
'generative models'
|
||||
],
|
||||
install_requires=[
|
||||
'accelerate',
|
||||
'einops',
|
||||
'numpy',
|
||||
'ema-pytorch',
|
||||
'pillow',
|
||||
'torch',
|
||||
'torchvision',
|
||||
|
||||
Reference in New Issue
Block a user