LudwigO's picture
add chemprop files
5716801
Raw
History Blame Contribute Delete
9.12 kB
from __future__ import annotations
from typing import Iterable
from lightning import pytorch as pl
import torch
from torch import nn, Tensor, optim
from chemprop.data import TrainingBatch, BatchMolGraph
from chemprop.nn.metrics import Metric
from chemprop.nn import MessagePassing, Aggregation, Predictor, LossFunction
from chemprop.schedulers import NoamLR
from chemprop.nn.transforms import ScaleTransform
class MPNN(pl.LightningModule):
r"""An :class:`MPNN` is a sequence of message passing layers, an aggregation routine, and a
predictor routine.
The first two modules calculate learned fingerprints from an input molecule
reaction graph, and the final module takes these learned fingerprints as input to calculate a
final prediction. I.e., the following operation:
.. math::
\mathtt{MPNN}(\mathcal{G}) =
\mathtt{predictor}(\mathtt{agg}(\mathtt{message\_passing}(\mathcal{G})))
The full model is trained end-to-end.
Parameters
----------
message_passing : MessagePassing
the message passing block to use to calculate learned fingerprints
agg : Aggregation
the aggregation operation to use during molecule-level predictor
predictor : Predictor
the function to use to calculate the final prediction
batch_norm : bool, default=True
if `True`, apply batch normalization to the output of the aggregation operation
metrics : Iterable[Metric] | None, default=None
the metrics to use to evaluate the model during training and evaluation
warmup_epochs : int, default=2
the number of epochs to use for the learning rate warmup
init_lr : int, default=1e-4
the initial learning rate
max_lr : float, default=1e-3
the maximum learning rate
final_lr : float, default=1e-4
the final learning rate
Raises
------
ValueError
if the output dimension of the message passing block does not match the input dimension of
the predictor function
"""
def __init__(
self,
message_passing: MessagePassing,
agg: Aggregation,
predictor: Predictor,
batch_norm: bool = True,
metrics: Iterable[Metric] | None = None,
warmup_epochs: int = 2,
init_lr: float = 1e-4,
max_lr: float = 1e-3,
final_lr: float = 1e-4,
X_d_transform: ScaleTransform | None = None,
):
super().__init__()
self.save_hyperparameters(ignore=["message_passing", "agg", "predictor"])
self.hparams.update(
{
"message_passing": message_passing.hparams,
"agg": agg.hparams,
"predictor": predictor.hparams,
}
)
self.message_passing = message_passing
self.agg = agg
self.bn = nn.BatchNorm1d(self.message_passing.output_dim) if batch_norm else nn.Identity()
self.predictor = predictor
self.X_d_transform = X_d_transform if X_d_transform is not None else nn.Identity()
self.metrics = (
[*metrics, self.criterion]
if metrics
else [self.predictor._T_default_metric(), self.criterion]
)
self.warmup_epochs = warmup_epochs
self.init_lr = init_lr
self.max_lr = max_lr
self.final_lr = final_lr
@property
def output_dim(self) -> int:
return self.predictor.output_dim
@property
def n_tasks(self) -> int:
return self.predictor.n_tasks
@property
def n_targets(self) -> int:
return self.predictor.n_targets
@property
def criterion(self) -> LossFunction:
return self.predictor.criterion
def fingerprint(
self, bmg: BatchMolGraph, V_d: Tensor | None = None, X_d: Tensor | None = None
) -> Tensor:
"""the learned fingerprints for the input molecules"""
H_v = self.message_passing(bmg, V_d)
H = self.agg(H_v, bmg.batch)
H = self.bn(H)
return H if X_d is None else torch.cat((H, self.X_d_transform(X_d)), 1)
def encoding(
self, bmg: BatchMolGraph, V_d: Tensor | None = None, X_d: Tensor | None = None, i: int = -1
) -> Tensor:
"""Calculate the :attr:`i`-th hidden representation"""
return self.predictor.encode(self.fingerprint(bmg, V_d, X_d), i)
def forward(
self, bmg: BatchMolGraph, V_d: Tensor | None = None, X_d: Tensor | None = None
) -> Tensor:
"""Generate predictions for the input molecules/reactions"""
return self.predictor(self.fingerprint(bmg, V_d, X_d))
def training_step(self, batch: TrainingBatch, batch_idx):
bmg, V_d, X_d, targets, weights, lt_mask, gt_mask = batch
mask = targets.isfinite()
targets = targets.nan_to_num(nan=0.0)
Z = self.fingerprint(bmg, V_d, X_d)
preds = self.predictor.train_step(Z)
l = self.criterion(preds, targets, mask, weights, lt_mask, gt_mask)
self.log("train_loss", l, prog_bar=True)
return l
def on_validation_model_eval(self) -> None:
self.eval()
self.predictor.output_transform.train()
def validation_step(self, batch: TrainingBatch, batch_idx: int = 0):
losses = self._evaluate_batch(batch)
metric2loss = {f"val/{m.alias}": l for m, l in zip(self.metrics, losses)}
self.log_dict(metric2loss, batch_size=len(batch[0]))
self.log("val_loss", losses[0], batch_size=len(batch[0]), prog_bar=True)
def test_step(self, batch: TrainingBatch, batch_idx: int = 0):
losses = self._evaluate_batch(batch)
metric2loss = {f"batch_averaged_test/{m.alias}": l for m, l in zip(self.metrics, losses)}
self.log_dict(metric2loss, batch_size=len(batch[0]))
def _evaluate_batch(self, batch) -> list[Tensor]:
bmg, V_d, X_d, targets, _, lt_mask, gt_mask = batch
mask = targets.isfinite()
targets = targets.nan_to_num(nan=0.0)
preds = self(bmg, V_d, X_d)
return [
metric(preds, targets, mask, None, lt_mask, gt_mask) for metric in self.metrics[:-1]
]
def predict_step(self, batch: TrainingBatch, batch_idx: int, dataloader_idx: int = 0) -> Tensor:
"""Return the predictions of the input batch
Parameters
----------
batch : TrainingBatch
the input batch
Returns
-------
Tensor
a tensor of varying shape depending on the task type:
* regression/binary classification: ``n x (t * s)``, where ``n`` is the number of input
molecules/reactions, ``t`` is the number of tasks, and ``s`` is the number of targets
per task. The final dimension is flattened, so that the targets for each task are
grouped. I.e., the first ``t`` elements are the first target for each task, the second
``t`` elements the second target, etc.
* multiclass classification: ``n x t x c``, where ``c`` is the number of classes
"""
bmg, X_vd, X_d, *_ = batch
return self(bmg, X_vd, X_d)
def configure_optimizers(self):
opt = optim.Adam(self.parameters(), self.init_lr)
lr_sched = NoamLR(
opt,
self.warmup_epochs,
self.trainer.max_epochs,
self.trainer.estimated_stepping_batches // self.trainer.max_epochs,
self.init_lr,
self.max_lr,
self.final_lr,
)
lr_sched_config = {
"scheduler": lr_sched,
"interval": "step" if isinstance(lr_sched, NoamLR) else "batch",
}
return {"optimizer": opt, "lr_scheduler": lr_sched_config}
@classmethod
def load_submodules(cls, checkpoint_path, **kwargs):
hparams = torch.load(checkpoint_path)["hyper_parameters"]
kwargs |= {
key: hparams[key].pop("cls")(**hparams[key])
for key in ("message_passing", "agg", "predictor")
if key not in kwargs
}
return kwargs
@classmethod
def load_from_checkpoint(
cls, checkpoint_path, map_location=None, hparams_file=None, strict=True, **kwargs
) -> MPNN:
kwargs = cls.load_submodules(checkpoint_path, **kwargs)
return super().load_from_checkpoint(
checkpoint_path, map_location, hparams_file, strict, **kwargs
)
@classmethod
def load_from_file(cls, model_path, map_location=None, strict=True) -> MPNN:
d = torch.load(model_path, map_location=map_location)
try:
hparams = d["hyper_parameters"]
state_dict = d["state_dict"]
except KeyError:
raise KeyError(f"Could not find hyper parameters and/or state dict in {model_path}. ")
for key in ["message_passing", "agg", "predictor"]:
hparam_kwargs = hparams[key]
hparam_cls = hparam_kwargs.pop("cls")
hparams[key] = hparam_cls(**hparam_kwargs)
model = cls(**hparams)
model.load_state_dict(state_dict, strict=strict)
return model