MatterGen / model /diffusion /training /field_loss.py
dadadaxi's picture
Upload folder using huggingface_hub
f15d29e verified
Raw
History Blame Contribute Delete
6.63 kB
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from typing import Literal, Protocol
import torch
from torch_scatter import scatter
from ...diffusion.corruption.corruption import Corruption
from ...diffusion.corruption.sde_lib import maybe_expand
from ...diffusion.d3pm.d3pm import compute_kl_reverse_process
from ...diffusion.data.batched_data import BatchedData
from ...diffusion.discrete_time import to_discrete_time
from ...diffusion.model_target import ModelTarget
def compute_noise_given_sample_and_corruption(
x: torch.Tensor,
x_noisy: torch.Tensor,
corruption: Corruption,
t: torch.Tensor,
batch_idx: torch.LongTensor | None,
batch: BatchedData,
) -> torch.Tensor:
"""
Recover the (unit-Gaussian-distributed) raw noise that was used to corrupt a batch of samples.
We first obtain the mean and std of the noisy samples from the corruption via `t` and the clean batch.
Then we solve:
x_noisy = x_mean + noise * std w.r.t. `noise`:
noise = (x_noisy - x_mean) / std
"""
x_mean, std = corruption.marginal_prob(
x,
t=t,
batch_idx=batch_idx,
batch=batch,
)
return (x_noisy - x_mean) / std
class FieldLoss(Protocol):
"""Loss function for a single field. Because loss functions are defined different ways in different papers,
we pass loads of keyword arguments. Each loss function will only use a subset of these arguments.
"""
def __call__(
self,
*,
corruption: Corruption,
score_model_output: torch.Tensor,
t: torch.Tensor,
batch_idx: torch.LongTensor | None,
batch_size: int,
x: torch.Tensor,
noisy_x: torch.Tensor,
reduce: Literal["sum", "mean"],
batch: BatchedData,
) -> torch.Tensor:
"""Calculate loss per sample for a single field. Returns a loss tensor of shape (batch_size,)."""
pass
def denoising_score_matching(
*,
corruption: Corruption,
score_model_output: torch.Tensor,
t: torch.Tensor,
batch_idx: torch.LongTensor | None,
batch_size: int,
x: torch.Tensor,
noisy_x: torch.Tensor,
reduce: Literal["sum", "mean"],
batch: BatchedData,
model_target: ModelTarget,
node_is_unmasked: torch.LongTensor | None = None,
**_,
) -> torch.Tensor:
"""Mean square error in predicting raw noise, optionally reweighted."""
assert score_model_output.ndim >= 2
model_target = ModelTarget(model_target) # in case str was passed
losses = get_losses(
corruption=corruption,
score_model_output=score_model_output,
t=t,
batch_idx=batch_idx,
x=x,
noisy_x=noisy_x,
batch=batch,
model_target=model_target,
)
if node_is_unmasked is not None:
losses = node_is_unmasked.unsqueeze(-1) * losses # Apply masking.
original_reduce = reduce
reduce = "sum" # We sum first and handle the division by nodes_per_sample for the mean manually later.
loss_per_sample = aggregate_per_sample(losses, batch_idx, reduce=reduce, batch_size=batch_size)
if (node_is_unmasked is not None) and (original_reduce == "mean"):
nodes_per_sample = scatter(node_is_unmasked, batch_idx, dim=0, reduce="sum")
loss_per_sample /= nodes_per_sample
return loss_per_sample
def get_losses(
corruption: Corruption,
score_model_output: torch.Tensor,
t: torch.Tensor,
batch_idx: torch.LongTensor | None,
x: torch.Tensor,
noisy_x: torch.Tensor,
batch: BatchedData,
model_target: ModelTarget,
) -> torch.Tensor:
if model_target == ModelTarget.score_times_std:
raw_noise = compute_noise_given_sample_and_corruption(
x=x, x_noisy=noisy_x, corruption=corruption, t=t, batch_idx=batch_idx, batch=batch
)
target = -raw_noise
losses = (score_model_output - target).square()
else:
raise ValueError(f"Unknown model_target {model_target}")
return losses
def aggregate_per_sample(
loss_per_row: torch.Tensor,
batch_idx: torch.Tensor | None,
reduce: Literal["sum", "mean"],
batch_size: int,
):
"""
Aggregate (potentially) batched input tensor to get a scalar for each sample in the batch.
E.g., (num_atoms, d1, d2, ..., dn) -> (batch_size, d1, d2, ..., dn) -> (batch_size,),
where the first aggregation only happens when batch_idx is provided.
Args:
loss_per_row: shape (num_nodes, any_more_dims). May contain multiple nodes per sample.
batch_idx: shape (num_nodes,). Indicates which sample each row belongs to. If not provided,
then we assume the first dimension is the batch dimension.
reduce: determines how to aggregate over nodes within each sample. (Aggregation over samples
and within dims for one node is always mean.)
batch_size: number of samples in the batch.
Returns:
Scalar for each sample, shape (batch_size,).
"""
# Sum over all but 0th dimension.
loss_per_row = torch.mean(loss_per_row.reshape(loss_per_row.shape[0], -1), dim=1)
if batch_idx is None:
# First dimension is batch dimension. In this case 'reduce' is ignored.
loss_per_sample = loss_per_row
else:
# Aggregate over nodes within each sample.
loss_per_sample = scatter(
src=loss_per_row,
index=batch_idx,
dim_size=batch_size,
reduce=reduce,
)
return loss_per_sample
def d3pm_loss(
*,
corruption: Corruption,
score_model_output: torch.Tensor,
t: torch.Tensor,
batch_idx: torch.LongTensor | None,
batch_size: int,
x: torch.Tensor,
noisy_x: torch.Tensor,
reduce: Literal["sum", "mean"],
d3pm_hybrid_lambda: float = 0.0,
**_,
) -> torch.Tensor:
assert hasattr(corruption, "N") # mypy
assert hasattr(corruption, "_to_zero_based") # mypy
assert hasattr(corruption, "d3pm") # mypy
t = maybe_expand(to_discrete_time(t, N=corruption.N, T=corruption.T), batch_idx)
metrics_dict = compute_kl_reverse_process(
corruption._to_zero_based(x.long()),
t,
diffusion=corruption.d3pm,
log_space=True,
denoise_fn=lambda targets, timestep: score_model_output,
hybrid_lambda=d3pm_hybrid_lambda,
x_t_plus_1=corruption._to_zero_based(noisy_x.long()),
)
loss = metrics_dict.pop("loss")
loss_per_structure = aggregate_per_sample(
loss, batch_idx=batch_idx, reduce=reduce, batch_size=batch_size
)
return loss_per_structure