| |
| |
|
|
| """Adapted from https://github.com/yang-song/score_sde_pytorch which is released under Apache license. |
| |
| Key changes: |
| - Introduced batch_idx argument to work with graph-like data (e.g. molecules) |
| - Introduced `..._given_score` methods so that multiple fields can be sampled at once using a shared score model. See PredictorCorrector for how this is used. |
| """ |
|
|
| import abc |
| import logging |
|
|
| import torch |
|
|
| from ...diffusion.corruption.corruption import Corruption |
| from ...diffusion.corruption.sde_lib import SDE, ScoreFunction, check_score_fn_defined |
| from ...diffusion.data.batched_data import BatchedData |
| from ...diffusion.sampling.predictors_correctors import SampleAndMean, Sampler |
| from ...diffusion.wrapped.wrapped_sde import WrappedSDEMixin |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class Predictor(Sampler): |
| """The abstract class for something that takes x_t and predicts x_{t-dt}, |
| where t is diffusion timestep.""" |
|
|
| def __init__( |
| self, |
| corruption: Corruption, |
| score_fn: ScoreFunction | None, |
| ): |
| super().__init__(corruption, score_fn=score_fn) |
|
|
| def update_fn( |
| self, |
| *, |
| x: torch.Tensor, |
| t: torch.Tensor, |
| dt: torch.Tensor, |
| batch_idx: torch.LongTensor, |
| batch: BatchedData | None, |
| ) -> SampleAndMean: |
| """One update of the predictor. |
| |
| Args: |
| x: current state |
| t: timesteps |
| batch_idx: indicates which sample each row of x belongs to |
| |
| Returns: |
| (sampled next state, mean next state) |
| """ |
| check_score_fn_defined(self.score_fn, "update_given_score") |
| assert self.score_fn is not None |
| score = self.score_fn(x=x, t=t, batch_idx=batch_idx) |
| return self.update_given_score( |
| x=x, t=t, dt=dt, batch_idx=batch_idx, score=score, batch=batch |
| ) |
|
|
| @abc.abstractmethod |
| def update_given_score( |
| self, |
| *, |
| x: torch.Tensor, |
| t: torch.Tensor, |
| dt: torch.Tensor, |
| batch_idx: torch.LongTensor, |
| score: torch.Tensor, |
| batch: BatchedData | None, |
| ) -> SampleAndMean: |
| pass |
|
|
|
|
| class AncestralSamplingPredictor(Predictor): |
| """Suitable for all linear SDEs. |
| |
| This predictor is derived by converting the score prediction to a prediction of x_0 given x_t, and then |
| sampling from the conditional distribution of x_{t-dt} given x_0 and x_t according to the corruption process. |
| It corresponds to equation (47) in Song et al. for VESDE (https://openreview.net/forum?id=PxTIG12RRHS) |
| and equation (7) in Ho et al. for VPSDE (https://arxiv.org/abs/2006.11239) |
| |
| In more detail: suppose the SDE has marginals x_t ~ N(alpha_t *x_0, sigma_t**2) |
| |
| We estimate x_0 as follows: |
| x_0 \approx (x_t + sigma_t^2 * score) / alpha_t |
| |
| For any s < t, the forward corruption process implies that |
| x_t| x_s ~ N(alpha_t/alpha_s * x_s, sigma_t^2 - sigma_s^2 * alpha_t^2 / alpha_s^2) |
| |
| Now go away and do some algebra to get the mean and variance of x_s given x_t |
| and x_0, and you will get the coefficients in the `update_given_score` method below. |
| |
| """ |
|
|
| def update_given_score( |
| self, |
| *, |
| x: torch.Tensor, |
| t: torch.Tensor, |
| dt: torch.Tensor, |
| batch_idx: torch.LongTensor, |
| score: torch.Tensor, |
| batch: BatchedData | None, |
| ) -> SampleAndMean: |
| x_coeff, score_coeff, std = self._get_coeffs( |
| x=x, |
| t=t, |
| dt=dt, |
| batch_idx=batch_idx, |
| batch=batch, |
| ) |
| |
| z = torch.randn_like(x_coeff) |
|
|
| mean = x_coeff * x + score_coeff * score |
| sample = mean + std * z |
|
|
| return sample, mean |
|
|
| def _get_coeffs(self, x, t, dt, batch_idx, batch): |
| """ |
| Compute coefficients for ancestral sampling. |
| This is in a separate method to make it easier to test.""" |
| sde = self.corruption |
| assert isinstance(sde, SDE) |
|
|
| |
| s = t + dt |
|
|
| alpha_t, sigma_t = sde.mean_coeff_and_std(x=x, t=t, batch_idx=batch_idx, batch=batch) |
| if batch_idx is None: |
| is_time_zero = s <= 0 |
| else: |
| is_time_zero = s[batch_idx] <= 0 |
| alpha_s, sigma_s = sde.mean_coeff_and_std(x=x, t=s, batch_idx=batch_idx, batch=batch) |
| sigma_s[is_time_zero] = 0 |
|
|
| |
| |
| |
| sigma2_t_given_s = sigma_t**2 - sigma_s**2 * alpha_t**2 / alpha_s**2 |
| sigma_t_given_s = torch.sqrt(sigma2_t_given_s) |
| std = sigma_t_given_s * sigma_s / sigma_t |
|
|
| |
| min_alpha_t_given_s = 0.001 |
| alpha_t_given_s = alpha_t / alpha_s |
| if torch.any(alpha_t_given_s < min_alpha_t_given_s): |
| |
| |
| |
| logger.warning( |
| f"Clipping alpha_t_given_s to {min_alpha_t_given_s} to avoid divide-by-zero. You should probably change something else to avoid this." |
| ) |
| alpha_t_given_s = torch.clip(alpha_t_given_s, min_alpha_t_given_s, 1) |
|
|
| score_coeff = sigma2_t_given_s / alpha_t_given_s |
|
|
| x_coeff = 1.0 / alpha_t_given_s |
|
|
| std[is_time_zero] = 0 |
|
|
| return x_coeff, score_coeff, std |
|
|
| @classmethod |
| def is_compatible(cls, corruption: Corruption) -> bool: |
| return super().is_compatible(corruption) and not isinstance(corruption, WrappedSDEMixin) |
|
|