torch-molecule 0.1.0.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torch_molecule/__init__.py +70 -0
- torch_molecule/base/__init__.py +8 -0
- torch_molecule/base/base.py +396 -0
- torch_molecule/base/encoder.py +44 -0
- torch_molecule/base/generator.py +24 -0
- torch_molecule/base/old_encoder.py +322 -0
- torch_molecule/base/old_predictor.py +904 -0
- torch_molecule/base/old_predictor2.py +436 -0
- torch_molecule/base/predictor.py +112 -0
- torch_molecule/encoder/attrmask/__init__.py +3 -0
- torch_molecule/encoder/attrmask/model.py +113 -0
- torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
- torch_molecule/encoder/constant.py +13 -0
- torch_molecule/encoder/contextpred/__init__.py +3 -0
- torch_molecule/encoder/contextpred/model.py +207 -0
- torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
- torch_molecule/encoder/contextpred/utils.py +206 -0
- torch_molecule/encoder/edgepred/__init__.py +3 -0
- torch_molecule/encoder/edgepred/model.py +104 -0
- torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
- torch_molecule/encoder/graphmae/__init__.py +3 -0
- torch_molecule/encoder/graphmae/dataloader.py +206 -0
- torch_molecule/encoder/graphmae/model.py +137 -0
- torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
- torch_molecule/encoder/infograph/__init__.py +3 -0
- torch_molecule/encoder/infograph/loss_function.py +128 -0
- torch_molecule/encoder/infograph/model.py +92 -0
- torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
- torch_molecule/encoder/infograph/module.py +31 -0
- torch_molecule/encoder/moama/__init__.py +3 -0
- torch_molecule/encoder/moama/model.py +131 -0
- torch_molecule/encoder/moama/modeling_moama.py +307 -0
- torch_molecule/encoder/moama/utils.py +143 -0
- torch_molecule/encoder/pretrained/__init__.py +3 -0
- torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
- torch_molecule/encoder/supervised/__init__.py +3 -0
- torch_molecule/encoder/supervised/model.py +97 -0
- torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
- torch_molecule/generator/digress/__init__.py +3 -0
- torch_molecule/generator/digress/diffusion.py +194 -0
- torch_molecule/generator/digress/modeling_digress.py +503 -0
- torch_molecule/generator/digress/transformer.py +360 -0
- torch_molecule/generator/digress/utils.py +196 -0
- torch_molecule/generator/gdss/__init__.py +3 -0
- torch_molecule/generator/gdss/layers.py +257 -0
- torch_molecule/generator/gdss/model.py +204 -0
- torch_molecule/generator/gdss/modeling_gdss.py +474 -0
- torch_molecule/generator/gdss/sde.py +334 -0
- torch_molecule/generator/gdss/solver.py +227 -0
- torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
- torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
- torch_molecule/generator/gdss/utils.py +240 -0
- torch_molecule/generator/graph_dit/__init__.py +3 -0
- torch_molecule/generator/graph_dit/diffusion.py +188 -0
- torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
- torch_molecule/generator/graph_dit/transformer.py +207 -0
- torch_molecule/generator/graph_dit/utils.py +220 -0
- torch_molecule/generator/graph_ga/__init__.py +3 -0
- torch_molecule/generator/graph_ga/crossover.py +182 -0
- torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
- torch_molecule/generator/graph_ga/mutate.py +136 -0
- torch_molecule/generator/graph_ga/oracle.py +95 -0
- torch_molecule/generator/jtvae/__init__.py +3 -0
- torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
- torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
- torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
- torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
- torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
- torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
- torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
- torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
- torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
- torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
- torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
- torch_molecule/generator/lstm/__init__.py +3 -0
- torch_molecule/generator/lstm/action_sampler.py +91 -0
- torch_molecule/generator/lstm/lstm.py +59 -0
- torch_molecule/generator/lstm/modeling_lstm.py +263 -0
- torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
- torch_molecule/generator/lstm/utils.py +62 -0
- torch_molecule/generator/molgpt/__init__.py +3 -0
- torch_molecule/generator/molgpt/dataset.py +87 -0
- torch_molecule/generator/molgpt/gpt.py +251 -0
- torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
- torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
- torch_molecule/nn/__init__.py +16 -0
- torch_molecule/nn/attention.py +63 -0
- torch_molecule/nn/embedder.py +183 -0
- torch_molecule/nn/gnn.py +631 -0
- torch_molecule/nn/mlp.py +34 -0
- torch_molecule/predictor/bfgnn/__init__.py +3 -0
- torch_molecule/predictor/bfgnn/model.py +116 -0
- torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
- torch_molecule/predictor/dir/__init__.py +3 -0
- torch_molecule/predictor/dir/model.py +235 -0
- torch_molecule/predictor/dir/modeling_dir.py +161 -0
- torch_molecule/predictor/dir/utils.py +40 -0
- torch_molecule/predictor/gnn/__init__.py +3 -0
- torch_molecule/predictor/gnn/model.py +114 -0
- torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
- torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
- torch_molecule/predictor/grea/__init__.py +3 -0
- torch_molecule/predictor/grea/model.py +190 -0
- torch_molecule/predictor/grea/modeling_grea.py +107 -0
- torch_molecule/predictor/grea/old1_features.py +229 -0
- torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
- torch_molecule/predictor/grea/utils.py +97 -0
- torch_molecule/predictor/grin/__init__.py +3 -0
- torch_molecule/predictor/grin/model.py +116 -0
- torch_molecule/predictor/grin/modeling_grin.py +91 -0
- torch_molecule/predictor/irm/__init__.py +3 -0
- torch_molecule/predictor/irm/model.py +127 -0
- torch_molecule/predictor/irm/modeling_irm.py +179 -0
- torch_molecule/predictor/lstm/__init__.py +3 -0
- torch_molecule/predictor/lstm/model.py +145 -0
- torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
- torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
- torch_molecule/predictor/old_modeling_graph.py +650 -0
- torch_molecule/predictor/rpgnn/__init__.py +3 -0
- torch_molecule/predictor/rpgnn/model.py +185 -0
- torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
- torch_molecule/predictor/sgir/__init__.py +3 -0
- torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
- torch_molecule/predictor/sgir/strategy.py +266 -0
- torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
- torch_molecule/predictor/smiles_transformer/model.py +424 -0
- torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
- torch_molecule/predictor/ssr/__init__.py +3 -0
- torch_molecule/predictor/ssr/model.py +283 -0
- torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
- torch_molecule/predictor/ssr/utils.py +185 -0
- torch_molecule/utils/__init__.py +31 -0
- torch_molecule/utils/checker.py +158 -0
- torch_molecule/utils/checkpoint.py +294 -0
- torch_molecule/utils/format.py +140 -0
- torch_molecule/utils/generic/metrics.py +530 -0
- torch_molecule/utils/generic/pseudo_tasks.py +11 -0
- torch_molecule/utils/generic/weights.py +30 -0
- torch_molecule/utils/graph/features.py +178 -0
- torch_molecule/utils/graph/graph_from_smiles.py +134 -0
- torch_molecule/utils/graph/graph_to_smiles.py +312 -0
- torch_molecule/utils/hf.py +232 -0
- torch_molecule/utils/search.py +64 -0
- torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
- torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
- torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
- torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
- torch_molecule-0.1.0.dev2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Literal, Type
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch_geometric.loader import DataLoader
|
|
8
|
+
from torch_geometric.data import Data
|
|
9
|
+
|
|
10
|
+
from .model import GNN
|
|
11
|
+
from ..constant import GNN_ENCODER_MODELS, GNN_ENCODER_READOUTS, GNN_ENCODER_PARAMS
|
|
12
|
+
from ...base import BaseMolecularEncoder
|
|
13
|
+
from ...utils import graph_from_smiles
|
|
14
|
+
from ...utils import PSEUDOTASK
|
|
15
|
+
|
|
16
|
+
ALLOWABLE_ENCODER_MODELS = GNN_ENCODER_MODELS
|
|
17
|
+
ALLOWABLE_ENCODER_READOUTS = GNN_ENCODER_READOUTS
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SupervisedMolecularEncoder(BaseMolecularEncoder):
|
|
21
|
+
"""This encoder implements a GNN model for supervised molecular representation learning with user-defined or predefined fingerprint/calculated property tasks.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
num_task : int, optional
|
|
26
|
+
Number of user-defined tasks for supervised pretraining. If it is specified, user must provide y_train in the fit function.
|
|
27
|
+
predefined_task : List[str], optional
|
|
28
|
+
List of predefined tasks to use. Must be from the supported task list ["morgan", "maccs", "logP"]. If None and num_task is None, all predefined tasks will be used.
|
|
29
|
+
encoder_type : str, default="gin-virtual"
|
|
30
|
+
Type of GNN architecture to use. One of ["gin-virtual", "gcn-virtual", "gin", "gcn"].
|
|
31
|
+
readout : str, default="sum"
|
|
32
|
+
Method for aggregating node features to obtain graph-level representations. One of ["sum", "mean", "max"].
|
|
33
|
+
num_layer : int, default=5
|
|
34
|
+
Number of GNN layers.
|
|
35
|
+
hidden_size : int, default=300
|
|
36
|
+
Dimension of hidden node features.
|
|
37
|
+
drop_ratio : float, default=0.5
|
|
38
|
+
Dropout probability.
|
|
39
|
+
norm_layer : str, default="batch_norm"
|
|
40
|
+
Type of normalization layer to use. One of ["batch_norm", "layer_norm", "instance_norm", "graph_norm", "size_norm", "pair_norm"].
|
|
41
|
+
batch_size : int, default=128
|
|
42
|
+
Number of samples per batch for training.
|
|
43
|
+
epochs : int, default=500
|
|
44
|
+
Maximum number of training epochs.
|
|
45
|
+
learning_rate : float, default=0.001
|
|
46
|
+
Learning rate for optimizer.
|
|
47
|
+
grad_clip_value : float, optional
|
|
48
|
+
Maximum norm of gradients for gradient clipping.
|
|
49
|
+
weight_decay : float, default=0.0
|
|
50
|
+
L2 regularization strength.
|
|
51
|
+
use_lr_scheduler : bool, default=False
|
|
52
|
+
Whether to use a learning rate scheduler.
|
|
53
|
+
scheduler_factor : float, default=0.5
|
|
54
|
+
Factor by which to reduce the learning rate when plateau is detected.
|
|
55
|
+
scheduler_patience : int, default=5
|
|
56
|
+
Number of epochs with no improvement after which learning rate will be reduced.
|
|
57
|
+
verbose : bool, default=False
|
|
58
|
+
Whether to print progress information during training.
|
|
59
|
+
"""
|
|
60
|
+
# pretraining task
|
|
61
|
+
num_task: Optional[int] = None
|
|
62
|
+
predefined_task: Optional[List[str]] = None
|
|
63
|
+
# Model parameters
|
|
64
|
+
encoder_type: str = "gin-virtual"
|
|
65
|
+
readout: str = "sum"
|
|
66
|
+
num_layer: int = 5
|
|
67
|
+
hidden_size: int = 300
|
|
68
|
+
drop_ratio: float = 0.5
|
|
69
|
+
norm_layer: str = "batch_norm"
|
|
70
|
+
|
|
71
|
+
# Training parameters
|
|
72
|
+
batch_size: int = 128
|
|
73
|
+
epochs: int = 500
|
|
74
|
+
learning_rate: float = 0.001
|
|
75
|
+
grad_clip_value: Optional[float] = None
|
|
76
|
+
weight_decay: float = 0.0
|
|
77
|
+
|
|
78
|
+
# Scheduler parameters
|
|
79
|
+
use_lr_scheduler: bool = False
|
|
80
|
+
scheduler_factor: float = 0.5
|
|
81
|
+
scheduler_patience: int = 5
|
|
82
|
+
|
|
83
|
+
# Other parameters
|
|
84
|
+
verbose: bool = False
|
|
85
|
+
model_name: str = "SupervisedMolecularEncoder"
|
|
86
|
+
|
|
87
|
+
# Non-init fields
|
|
88
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
89
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
90
|
+
model_class: Type[GNN] = field(default=GNN, init=False)
|
|
91
|
+
|
|
92
|
+
def __post_init__(self):
|
|
93
|
+
"""Initialize the model after dataclass initialization."""
|
|
94
|
+
super().__post_init__()
|
|
95
|
+
self.num_pretask = None
|
|
96
|
+
|
|
97
|
+
if self.encoder_type not in ALLOWABLE_ENCODER_MODELS:
|
|
98
|
+
raise ValueError(f"Invalid encoder: {self.encoder_type}. Currently only {ALLOWABLE_ENCODER_MODELS} are supported.")
|
|
99
|
+
if self.readout not in ALLOWABLE_ENCODER_READOUTS:
|
|
100
|
+
raise ValueError(f"Invalid readout: {self.readout}. Currently only {ALLOWABLE_ENCODER_READOUTS} are supported.")
|
|
101
|
+
if self.predefined_task is not None:
|
|
102
|
+
for task in self.predefined_task:
|
|
103
|
+
if task not in PSEUDOTASK.keys():
|
|
104
|
+
raise ValueError(f"Invalid predefined_task: {task}. Currently only {PSEUDOTASK.keys()} are supported.")
|
|
105
|
+
|
|
106
|
+
# Calculate number of predefined tasks if any are specified
|
|
107
|
+
num_pretask = 0
|
|
108
|
+
if self.predefined_task is not None:
|
|
109
|
+
num_pretask = sum(PSEUDOTASK[task][0] for task in self.predefined_task)
|
|
110
|
+
elif self.predefined_task is None and self.num_task is None:
|
|
111
|
+
# Use all predefined tasks if none specified
|
|
112
|
+
self.predefined_task = list(PSEUDOTASK.keys())
|
|
113
|
+
num_pretask = sum(task[0] for task in PSEUDOTASK.values())
|
|
114
|
+
|
|
115
|
+
self.num_pretask = num_pretask
|
|
116
|
+
self.num_task = (self.num_task or 0) + num_pretask
|
|
117
|
+
|
|
118
|
+
if self.verbose:
|
|
119
|
+
if self.predefined_task is None:
|
|
120
|
+
print(f"Using {self.num_task} user-defined tasks.")
|
|
121
|
+
elif self.num_task == num_pretask:
|
|
122
|
+
print(f"Using {num_pretask} predefined tasks from: {self.predefined_task}")
|
|
123
|
+
else:
|
|
124
|
+
print(f"Using {num_pretask} predefined tasks and {self.num_task - num_pretask} user-defined tasks.")
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _get_param_names() -> List[str]:
|
|
128
|
+
"""Get parameter names for the estimator.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
List[str]
|
|
133
|
+
List of parameter names that can be used for model configuration.
|
|
134
|
+
"""
|
|
135
|
+
return ["num_task", "predefined_task", "num_pretask"] + GNN_ENCODER_PARAMS.copy()
|
|
136
|
+
|
|
137
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
138
|
+
params = {
|
|
139
|
+
"num_layer": self.num_layer,
|
|
140
|
+
"hidden_size": self.hidden_size,
|
|
141
|
+
"num_task": self.num_task,
|
|
142
|
+
"encoder_type": self.encoder_type,
|
|
143
|
+
"drop_ratio": self.drop_ratio,
|
|
144
|
+
"norm_layer": self.norm_layer,
|
|
145
|
+
"readout": self.readout,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if checkpoint is not None:
|
|
149
|
+
if "hyperparameters" not in checkpoint:
|
|
150
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
151
|
+
hyperparameters = checkpoint["hyperparameters"]
|
|
152
|
+
params = {k: hyperparameters.get(k, v) for k, v in params.items()}
|
|
153
|
+
|
|
154
|
+
return params
|
|
155
|
+
def _convert_to_pytorch_data(self, X, y=None):
|
|
156
|
+
"""Convert numpy arrays to PyTorch Geometric data format.
|
|
157
|
+
"""
|
|
158
|
+
if self.verbose:
|
|
159
|
+
iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
|
|
160
|
+
else:
|
|
161
|
+
iterator = enumerate(X)
|
|
162
|
+
|
|
163
|
+
pyg_graph_list = []
|
|
164
|
+
for idx, smiles_or_mol in iterator:
|
|
165
|
+
if y is not None:
|
|
166
|
+
properties = y[idx]
|
|
167
|
+
else:
|
|
168
|
+
properties = None
|
|
169
|
+
graph = graph_from_smiles(smiles_or_mol, properties, augmented_properties = self.predefined_task)
|
|
170
|
+
g = Data()
|
|
171
|
+
g.num_nodes = graph["num_nodes"]
|
|
172
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
173
|
+
|
|
174
|
+
del graph["num_nodes"]
|
|
175
|
+
del graph["edge_index"]
|
|
176
|
+
|
|
177
|
+
if graph["edge_feat"] is not None:
|
|
178
|
+
g.edge_attr = torch.from_numpy(graph["edge_feat"])
|
|
179
|
+
del graph["edge_feat"]
|
|
180
|
+
|
|
181
|
+
if graph["node_feat"] is not None:
|
|
182
|
+
g.x = torch.from_numpy(graph["node_feat"])
|
|
183
|
+
del graph["node_feat"]
|
|
184
|
+
|
|
185
|
+
if graph["y"] is not None:
|
|
186
|
+
g.y = torch.from_numpy(graph["y"])
|
|
187
|
+
del graph["y"]
|
|
188
|
+
|
|
189
|
+
if graph["morgan"] is not None:
|
|
190
|
+
g.morgan = torch.tensor(graph["morgan"], dtype=torch.int8).view(1, -1)
|
|
191
|
+
del graph["morgan"]
|
|
192
|
+
|
|
193
|
+
if graph["maccs"] is not None:
|
|
194
|
+
g.maccs = torch.tensor(graph["maccs"], dtype=torch.int8).view(1, -1)
|
|
195
|
+
del graph["maccs"]
|
|
196
|
+
|
|
197
|
+
pyg_graph_list.append(g)
|
|
198
|
+
|
|
199
|
+
return pyg_graph_list
|
|
200
|
+
|
|
201
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
202
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
203
|
+
"""
|
|
204
|
+
optimizer = torch.optim.Adam(
|
|
205
|
+
self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
|
|
206
|
+
)
|
|
207
|
+
scheduler = None
|
|
208
|
+
if self.use_lr_scheduler:
|
|
209
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
210
|
+
optimizer,
|
|
211
|
+
mode="min",
|
|
212
|
+
factor=self.scheduler_factor,
|
|
213
|
+
patience=self.scheduler_patience,
|
|
214
|
+
min_lr=1e-6,
|
|
215
|
+
cooldown=0,
|
|
216
|
+
eps=1e-8,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return optimizer, scheduler
|
|
220
|
+
|
|
221
|
+
def fit(
|
|
222
|
+
self,
|
|
223
|
+
X_train: List[str],
|
|
224
|
+
y_train: Optional[Union[List, np.ndarray]] = None,
|
|
225
|
+
) -> "SupervisedMolecularEncoder":
|
|
226
|
+
"""Fit the model to the training data with optional validation set.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
X_train : List[str]
|
|
231
|
+
Training set input molecular structures as SMILES strings
|
|
232
|
+
y_train : Union[List, np.ndarray]
|
|
233
|
+
Training set target values for representation learning
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
self : SupervisedMolecularEncoder
|
|
237
|
+
Fitted estimator
|
|
238
|
+
"""
|
|
239
|
+
user_defined_task = self.num_task - self.num_pretask
|
|
240
|
+
if user_defined_task > 0:
|
|
241
|
+
if y_train is None:
|
|
242
|
+
raise ValueError("User-defined tasks require target values but y_train is None.")
|
|
243
|
+
if y_train.shape[1] != user_defined_task:
|
|
244
|
+
raise ValueError(f"Number of user-defined tasks ({user_defined_task}) must match the number of target values in y_train ({y_train.shape[1]}).")
|
|
245
|
+
|
|
246
|
+
self._initialize_model(self.model_class)
|
|
247
|
+
self.model.initialize_parameters()
|
|
248
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
249
|
+
|
|
250
|
+
# Prepare datasets and loaders
|
|
251
|
+
X_train, y_train = self._validate_inputs(X_train, y_train, return_rdkit_mol=True, num_task=self.num_task, num_pretask=self.num_pretask)
|
|
252
|
+
train_dataset = self._convert_to_pytorch_data(X_train, y_train)
|
|
253
|
+
train_loader = DataLoader(
|
|
254
|
+
train_dataset,
|
|
255
|
+
batch_size=self.batch_size,
|
|
256
|
+
shuffle=True,
|
|
257
|
+
num_workers=0
|
|
258
|
+
)
|
|
259
|
+
self.fitting_loss = []
|
|
260
|
+
if user_defined_task > 0:
|
|
261
|
+
is_class_user = self._inspect_task_types(y_train, return_type="pt")
|
|
262
|
+
else:
|
|
263
|
+
is_class_user = torch.tensor([], dtype=torch.bool)
|
|
264
|
+
|
|
265
|
+
if self.predefined_task is not None:
|
|
266
|
+
is_class_predefined = torch.cat([torch.full((PSEUDOTASK[task][0],), PSEUDOTASK[task][1] == "classification", dtype=torch.bool) for task in self.predefined_task])
|
|
267
|
+
else:
|
|
268
|
+
is_class_predefined = torch.tensor([], dtype=torch.bool)
|
|
269
|
+
|
|
270
|
+
is_class = torch.cat([is_class_user, is_class_predefined])
|
|
271
|
+
for epoch in range(self.epochs):
|
|
272
|
+
# Training phase
|
|
273
|
+
train_losses = self._train_epoch(train_loader, optimizer, is_class, epoch)
|
|
274
|
+
self.fitting_loss.append(np.mean(train_losses).item())
|
|
275
|
+
if scheduler:
|
|
276
|
+
scheduler.step(np.mean(train_losses).item())
|
|
277
|
+
|
|
278
|
+
self.fitting_epoch = epoch
|
|
279
|
+
self.is_fitted_ = True
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
def _train_epoch(self, train_loader, optimizer, is_class, epoch):
|
|
283
|
+
"""Training logic for one epoch.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
train_loader: DataLoader containing training data
|
|
287
|
+
optimizer: Optimizer instance for model parameter updates
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
list: List of loss values for each training step
|
|
291
|
+
"""
|
|
292
|
+
self.model.train()
|
|
293
|
+
losses = []
|
|
294
|
+
|
|
295
|
+
iterator = (
|
|
296
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
297
|
+
if self.verbose
|
|
298
|
+
else train_loader
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for batch in iterator:
|
|
302
|
+
batch = batch.to(self.device)
|
|
303
|
+
optimizer.zero_grad()
|
|
304
|
+
loss = self.model.compute_loss(batch, is_class)
|
|
305
|
+
loss.backward()
|
|
306
|
+
if self.grad_clip_value is not None:
|
|
307
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
308
|
+
optimizer.step()
|
|
309
|
+
losses.append(loss.item())
|
|
310
|
+
|
|
311
|
+
# Update progress bar if using tqdm
|
|
312
|
+
if self.verbose:
|
|
313
|
+
iterator.set_postfix({"Epoch": f"{epoch}", "Loss": f"{loss.item():.4f}"})
|
|
314
|
+
|
|
315
|
+
return losses
|
|
316
|
+
|
|
317
|
+
def encode(self, X: List[str], return_type: Literal["np", "pt"] = "pt") -> Union[np.ndarray, torch.Tensor]:
|
|
318
|
+
"""Encode molecules into vector representations.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
X : List[str]
|
|
323
|
+
List of SMILES strings
|
|
324
|
+
return_type : Literal["np", "pt"], default="pt"
|
|
325
|
+
Return type of the representations
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
representations : ndarray or torch.Tensor
|
|
330
|
+
Molecular representations
|
|
331
|
+
"""
|
|
332
|
+
self._check_is_fitted()
|
|
333
|
+
|
|
334
|
+
# Convert to PyTorch Geometric format and create loader
|
|
335
|
+
X, _ = self._validate_inputs(X, return_rdkit_mol=True)
|
|
336
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
337
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
338
|
+
|
|
339
|
+
if self.model is None:
|
|
340
|
+
raise RuntimeError("Model not initialized")
|
|
341
|
+
|
|
342
|
+
# Generate encodings
|
|
343
|
+
self.model = self.model.to(self.device)
|
|
344
|
+
self.model.eval()
|
|
345
|
+
encodings = []
|
|
346
|
+
with torch.no_grad():
|
|
347
|
+
for batch in tqdm(loader, disable=not self.verbose):
|
|
348
|
+
batch = batch.to(self.device)
|
|
349
|
+
out = self.model(batch)
|
|
350
|
+
encodings.append(out["graph"].cpu())
|
|
351
|
+
|
|
352
|
+
# Concatenate and convert to requested format
|
|
353
|
+
encodings = torch.cat(encodings, dim=0)
|
|
354
|
+
return encodings if return_type == "pt" else encodings.numpy()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import numpy as np
|
|
3
|
+
from torch.nn import functional as F
|
|
4
|
+
from .utils import PlaceHolder
|
|
5
|
+
|
|
6
|
+
def cosine_beta_schedule_discrete(timesteps, s=0.008):
|
|
7
|
+
""" Cosine schedule as proposed in https://openreview.net/forum?id=-NEXDKk8gZ. """
|
|
8
|
+
steps = timesteps + 2
|
|
9
|
+
x = np.linspace(0, steps, steps)
|
|
10
|
+
|
|
11
|
+
alphas_cumprod = np.cos(0.5 * np.pi * ((x / steps) + s) / (1 + s)) ** 2
|
|
12
|
+
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
|
|
13
|
+
alphas = (alphas_cumprod[1:] / alphas_cumprod[:-1])
|
|
14
|
+
betas = 1 - alphas
|
|
15
|
+
return betas.squeeze()
|
|
16
|
+
|
|
17
|
+
class NoiseScheduleDiscrete(torch.nn.Module):
|
|
18
|
+
def __init__(self, timesteps):
|
|
19
|
+
super(NoiseScheduleDiscrete, self).__init__()
|
|
20
|
+
self.timesteps = timesteps
|
|
21
|
+
|
|
22
|
+
betas = cosine_beta_schedule_discrete(timesteps)
|
|
23
|
+
self.register_buffer('betas', torch.from_numpy(betas).float())
|
|
24
|
+
|
|
25
|
+
# 0.9999
|
|
26
|
+
self.alphas = 1 - torch.clamp(self.betas, min=0, max=1)
|
|
27
|
+
|
|
28
|
+
log_alpha = torch.log(self.alphas)
|
|
29
|
+
log_alpha_bar = torch.cumsum(log_alpha, dim=0)
|
|
30
|
+
self.alphas_bar = torch.exp(log_alpha_bar)
|
|
31
|
+
|
|
32
|
+
def forward(self, t_normalized=None, t_int=None):
|
|
33
|
+
assert int(t_normalized is None) + int(t_int is None) == 1
|
|
34
|
+
if t_int is None:
|
|
35
|
+
t_int = torch.round(t_normalized * self.timesteps)
|
|
36
|
+
return self.betas[t_int.long()]
|
|
37
|
+
|
|
38
|
+
def get_alpha_bar(self, t_normalized=None, t_int=None):
|
|
39
|
+
assert int(t_normalized is None) + int(t_int is None) == 1
|
|
40
|
+
if t_int is None:
|
|
41
|
+
t_int = torch.round(t_normalized * self.timesteps)
|
|
42
|
+
self.alphas_bar = self.alphas_bar.to(t_int.device)
|
|
43
|
+
return self.alphas_bar[t_int.long()]
|
|
44
|
+
|
|
45
|
+
class MarginalTransition:
|
|
46
|
+
def __init__(self, x_marginals, e_marginals, y_classes):
|
|
47
|
+
self.X_classes = len(x_marginals)
|
|
48
|
+
self.E_classes = len(e_marginals)
|
|
49
|
+
self.y_classes = y_classes
|
|
50
|
+
self.x_marginals = x_marginals
|
|
51
|
+
self.e_marginals = e_marginals
|
|
52
|
+
|
|
53
|
+
self.u_x = x_marginals.unsqueeze(0).expand(self.X_classes, -1).unsqueeze(0)
|
|
54
|
+
self.u_e = e_marginals.unsqueeze(0).expand(self.E_classes, -1).unsqueeze(0)
|
|
55
|
+
self.u_y = torch.ones(1, self.y_classes, self.y_classes)
|
|
56
|
+
if self.y_classes > 0:
|
|
57
|
+
self.u_y = self.u_y / self.y_classes
|
|
58
|
+
|
|
59
|
+
def get_Qt(self, beta_t, device):
|
|
60
|
+
""" Returns one-step transition matrices for X and E, from step t - 1 to step t.
|
|
61
|
+
Qt = (1 - beta_t) * I + beta_t / K
|
|
62
|
+
|
|
63
|
+
beta_t: (bs) noise level between 0 and 1
|
|
64
|
+
returns: qx (bs, dx, dx), qe (bs, de, de), qy (bs, dy, dy). """
|
|
65
|
+
beta_t = beta_t.unsqueeze(1)
|
|
66
|
+
beta_t = beta_t.to(device)
|
|
67
|
+
self.u_x = self.u_x.to(device)
|
|
68
|
+
self.u_e = self.u_e.to(device)
|
|
69
|
+
self.u_y = self.u_y.to(device)
|
|
70
|
+
|
|
71
|
+
q_x = beta_t * self.u_x + (1 - beta_t) * torch.eye(self.X_classes, device=device).unsqueeze(0)
|
|
72
|
+
q_e = beta_t * self.u_e + (1 - beta_t) * torch.eye(self.E_classes, device=device).unsqueeze(0)
|
|
73
|
+
q_y = beta_t * self.u_y + (1 - beta_t) * torch.eye(self.y_classes, device=device).unsqueeze(0)
|
|
74
|
+
|
|
75
|
+
return PlaceHolder(X=q_x, E=q_e, y=q_y)
|
|
76
|
+
|
|
77
|
+
def get_Qt_bar(self, alpha_bar_t, device):
|
|
78
|
+
""" Returns t-step transition matrices for X and E, from step 0 to step t.
|
|
79
|
+
Qt = prod(1 - beta_t) * I + (1 - prod(1 - beta_t)) * K
|
|
80
|
+
|
|
81
|
+
alpha_bar_t: (bs) Product of the (1 - beta_t) for each time step from 0 to t.
|
|
82
|
+
returns: qx (bs, dx, dx), qe (bs, de, de), qy (bs, dy, dy).
|
|
83
|
+
"""
|
|
84
|
+
alpha_bar_t = alpha_bar_t.unsqueeze(1)
|
|
85
|
+
alpha_bar_t = alpha_bar_t.to(device)
|
|
86
|
+
self.u_x = self.u_x.to(device)
|
|
87
|
+
self.u_e = self.u_e.to(device)
|
|
88
|
+
self.u_y = self.u_y.to(device)
|
|
89
|
+
|
|
90
|
+
q_x = alpha_bar_t * torch.eye(self.X_classes, device=device).unsqueeze(0) + (1 - alpha_bar_t) * self.u_x
|
|
91
|
+
q_e = alpha_bar_t * torch.eye(self.E_classes, device=device).unsqueeze(0) + (1 - alpha_bar_t) * self.u_e
|
|
92
|
+
q_y = alpha_bar_t * torch.eye(self.y_classes, device=device).unsqueeze(0) + (1 - alpha_bar_t) * self.u_y
|
|
93
|
+
|
|
94
|
+
return PlaceHolder(X=q_x, E=q_e, y=q_y)
|
|
95
|
+
|
|
96
|
+
def sample_discrete_features(probX, probE, node_mask):
|
|
97
|
+
""" Sample features from multinomial distribution with given probabilities (probX, probE, proby)
|
|
98
|
+
:param probX: bs, n, dx_out node features
|
|
99
|
+
:param probE: bs, n, n, de_out edge features
|
|
100
|
+
:param proby: bs, dy_out global features.
|
|
101
|
+
"""
|
|
102
|
+
bs, n, _ = probX.shape
|
|
103
|
+
|
|
104
|
+
# Noise X
|
|
105
|
+
# The masked rows should define probability distributions as well
|
|
106
|
+
probX[~node_mask] = 1 / probX.shape[-1]
|
|
107
|
+
|
|
108
|
+
# Flatten the probability tensor to sample with multinomial
|
|
109
|
+
probX = probX.reshape(bs * n, -1) # (bs * n, dx_out)
|
|
110
|
+
|
|
111
|
+
# Sample X
|
|
112
|
+
probX = probX + 1e-12
|
|
113
|
+
probX = probX / probX.sum(dim=-1, keepdim=True)
|
|
114
|
+
X_t = probX.multinomial(1) # (bs * n, 1)
|
|
115
|
+
X_t = X_t.reshape(bs, n) # (bs, n)
|
|
116
|
+
|
|
117
|
+
# Noise E
|
|
118
|
+
# The masked rows should define probability distributions as well
|
|
119
|
+
inverse_edge_mask = ~(node_mask.unsqueeze(1) * node_mask.unsqueeze(2))
|
|
120
|
+
diag_mask = torch.eye(n).unsqueeze(0).expand(bs, -1, -1)
|
|
121
|
+
|
|
122
|
+
probE[inverse_edge_mask] = 1 / probE.shape[-1]
|
|
123
|
+
probE[diag_mask.bool()] = 1 / probE.shape[-1]
|
|
124
|
+
probE = probE.reshape(bs * n * n, -1) # (bs * n * n, de_out)
|
|
125
|
+
probE = probE + 1e-12
|
|
126
|
+
probE = probE / probE.sum(dim=-1, keepdim=True)
|
|
127
|
+
|
|
128
|
+
# Sample E
|
|
129
|
+
E_t = probE.multinomial(1).reshape(bs, n, n) # (bs, n, n)
|
|
130
|
+
E_t = torch.triu(E_t, diagonal=1)
|
|
131
|
+
E_t = (E_t + torch.transpose(E_t, 1, 2))
|
|
132
|
+
|
|
133
|
+
return PlaceHolder(X=X_t, E=E_t, y=None)
|
|
134
|
+
|
|
135
|
+
def sample_discrete_feature_noise(limit_dist, node_mask):
|
|
136
|
+
"""Sample from the limit distribution of the diffusion process"""
|
|
137
|
+
bs, n_max = node_mask.shape
|
|
138
|
+
x_limit = limit_dist.X[None, None, :].expand(bs, n_max, -1)
|
|
139
|
+
U_X = x_limit.flatten(end_dim=-2).multinomial(1).reshape(bs, n_max)
|
|
140
|
+
U_X = F.one_hot(U_X.long(), num_classes=x_limit.shape[-1]).type_as(x_limit)
|
|
141
|
+
|
|
142
|
+
e_limit = limit_dist.E[None, None, None, :].expand(bs, n_max, n_max, -1)
|
|
143
|
+
U_E = e_limit.flatten(end_dim=-2).multinomial(1).reshape(bs, n_max, n_max)
|
|
144
|
+
U_E = F.one_hot(U_E.long(), num_classes=e_limit.shape[-1]).type_as(x_limit)
|
|
145
|
+
|
|
146
|
+
U_X = U_X.to(node_mask.device)
|
|
147
|
+
U_E = U_E.to(node_mask.device)
|
|
148
|
+
|
|
149
|
+
# Get upper triangular part of edge noise, without main diagonal
|
|
150
|
+
upper_triangular_mask = torch.zeros_like(U_E)
|
|
151
|
+
indices = torch.triu_indices(row=U_E.size(1), col=U_E.size(2), offset=1)
|
|
152
|
+
upper_triangular_mask[:, indices[0], indices[1], :] = 1
|
|
153
|
+
|
|
154
|
+
U_E = U_E * upper_triangular_mask
|
|
155
|
+
U_E = U_E + torch.transpose(U_E, 1, 2)
|
|
156
|
+
|
|
157
|
+
assert (U_E == torch.transpose(U_E, 1, 2)).all()
|
|
158
|
+
return PlaceHolder(X=U_X, E=U_E, y=None).mask(node_mask)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def assert_correctly_masked(variable, node_mask):
|
|
162
|
+
assert (variable * (1 - node_mask.long())).abs().max().item() < 1e-4, \
|
|
163
|
+
'Variables not masked properly.'
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def compute_batched_over0_posterior_distribution(X_t, Qt, Qsb, Qtb):
|
|
167
|
+
""" M: X or E
|
|
168
|
+
Compute xt @ Qt.T * x0 @ Qsb / x0 @ Qtb @ xt.T for each possible value of x0
|
|
169
|
+
X_t: bs, n, dt or bs, n, n, dt
|
|
170
|
+
Qt: bs, d_t-1, dt
|
|
171
|
+
Qsb: bs, d0, d_t-1
|
|
172
|
+
Qtb: bs, d0, dt.
|
|
173
|
+
"""
|
|
174
|
+
# Flatten feature tensors
|
|
175
|
+
# Careful with this line. It does nothing if X is a node feature. If X is an edge features it maps to
|
|
176
|
+
# bs x (n ** 2) x d
|
|
177
|
+
X_t = X_t.flatten(start_dim=1, end_dim=-2).to(torch.float32) # bs x N x dt
|
|
178
|
+
|
|
179
|
+
Qt_T = Qt.transpose(-1, -2) # bs, dt, d_t-1
|
|
180
|
+
left_term = X_t @ Qt_T # bs, N, d_t-1
|
|
181
|
+
left_term = left_term.unsqueeze(dim=2) # bs, N, 1, d_t-1
|
|
182
|
+
|
|
183
|
+
right_term = Qsb.unsqueeze(1) # bs, 1, d0, d_t-1
|
|
184
|
+
numerator = left_term * right_term # bs, N, d0, d_t-1
|
|
185
|
+
|
|
186
|
+
X_t_transposed = X_t.transpose(-1, -2) # bs, dt, N
|
|
187
|
+
|
|
188
|
+
prod = Qtb @ X_t_transposed # bs, d0, N
|
|
189
|
+
prod = prod.transpose(-1, -2) # bs, N, d0
|
|
190
|
+
denominator = prod.unsqueeze(-1) # bs, N, d0, 1
|
|
191
|
+
denominator[denominator == 0] = 1e-6
|
|
192
|
+
|
|
193
|
+
out = numerator / denominator
|
|
194
|
+
return out
|