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,503 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Type
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
import torch.nn.functional as F
|
|
8
|
+
from torch_geometric.loader import DataLoader
|
|
9
|
+
from torch_geometric.data import Data
|
|
10
|
+
|
|
11
|
+
from .transformer import GraphTransformer
|
|
12
|
+
from .utils import PlaceHolder, to_dense, compute_dataset_info
|
|
13
|
+
from .diffusion import NoiseScheduleDiscrete, MarginalTransition, sample_discrete_features, sample_discrete_feature_noise, compute_batched_over0_posterior_distribution
|
|
14
|
+
|
|
15
|
+
from ...base import BaseMolecularGenerator
|
|
16
|
+
from ...utils import graph_from_smiles, graph_to_smiles
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DigressMolecularGenerator(BaseMolecularGenerator):
|
|
20
|
+
"""
|
|
21
|
+
This generator implements the DiGress for unconditional molecular generation.
|
|
22
|
+
|
|
23
|
+
References
|
|
24
|
+
----------
|
|
25
|
+
- DiGress: Discrete Denoising diffusion for graph generation. International Conference on
|
|
26
|
+
Learning Representations (ICLR) 2023. https://openreview.net/forum?id=UaAD-Nu86WX
|
|
27
|
+
- Code: https://github.com/cvignac/DiGress
|
|
28
|
+
|
|
29
|
+
:param hidden_size_X: Hidden dimension size for node features, defaults to 256
|
|
30
|
+
:type hidden_size_X: int, optional
|
|
31
|
+
:param hidden_size_E: Hidden dimension size for edge features, defaults to 128
|
|
32
|
+
:type hidden_size_E: int, optional
|
|
33
|
+
:param num_layer: Number of transformer layers, defaults to 5
|
|
34
|
+
:type num_layer: int, optional
|
|
35
|
+
:param n_head: Number of attention heads, defaults to 8
|
|
36
|
+
:type n_head: int, optional
|
|
37
|
+
:param dropout: Dropout rate for transformer layers, defaults to 0.1
|
|
38
|
+
:type dropout: float, optional
|
|
39
|
+
:param timesteps: Number of diffusion timesteps, defaults to 500
|
|
40
|
+
:type timesteps: int, optional
|
|
41
|
+
:param batch_size: Batch size for training, defaults to 128
|
|
42
|
+
:type batch_size: int, optional
|
|
43
|
+
:param epochs: Number of training epochs, defaults to 10000
|
|
44
|
+
:type epochs: int, optional
|
|
45
|
+
:param learning_rate: Learning rate for optimization, defaults to 0.0002
|
|
46
|
+
:type learning_rate: float, optional
|
|
47
|
+
:param grad_clip_value: Value for gradient clipping (None = no clipping), defaults to None
|
|
48
|
+
:type grad_clip_value: Optional[float], optional
|
|
49
|
+
:param weight_decay: Weight decay for optimization, defaults to 0.0
|
|
50
|
+
:type weight_decay: float, optional
|
|
51
|
+
:param lw_X: Loss weight for node reconstruction, defaults to 1
|
|
52
|
+
:type lw_X: float, optional
|
|
53
|
+
:param lw_E: Loss weight for edge reconstruction, defaults to 10
|
|
54
|
+
:type lw_E: float, optional
|
|
55
|
+
:param use_lr_scheduler: Whether to use learning rate scheduler, defaults to False
|
|
56
|
+
:type use_lr_scheduler: bool, optional
|
|
57
|
+
:param scheduler_factor: Factor for learning rate scheduler (if use_lr_scheduler is True), defaults to 0.5
|
|
58
|
+
:type scheduler_factor: float, optional
|
|
59
|
+
:param scheduler_patience: Patience for learning rate scheduler (if use_lr_scheduler is True), defaults to 5
|
|
60
|
+
:type scheduler_patience: int, optional
|
|
61
|
+
:param verbose: Whether to display progress bars and logs. Default is False.
|
|
62
|
+
:type verbose: bool
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# Model parameters
|
|
66
|
+
hidden_size_X: int = 256
|
|
67
|
+
hidden_size_E: int = 128
|
|
68
|
+
num_layer: int = 5
|
|
69
|
+
n_head: int = 8
|
|
70
|
+
dropout: float = 0.1
|
|
71
|
+
|
|
72
|
+
# Diffusion parameters
|
|
73
|
+
timesteps: int = 500
|
|
74
|
+
|
|
75
|
+
# Training parameters
|
|
76
|
+
batch_size: int = 512
|
|
77
|
+
epochs: int = 1000
|
|
78
|
+
learning_rate: float = 0.0002
|
|
79
|
+
grad_clip_value: Optional[float] = None
|
|
80
|
+
weight_decay: float = 1e-12
|
|
81
|
+
lw_X: float = 1
|
|
82
|
+
lw_E: float = 5
|
|
83
|
+
|
|
84
|
+
# Scheduler parameters
|
|
85
|
+
use_lr_scheduler: bool = False
|
|
86
|
+
scheduler_factor: float = 0.5
|
|
87
|
+
scheduler_patience: int = 5
|
|
88
|
+
verbose: bool = False
|
|
89
|
+
|
|
90
|
+
# attributes
|
|
91
|
+
model_name: str = "DigressMolecularGenerator"
|
|
92
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
93
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
94
|
+
model_class: Type[GraphTransformer] = field(default=GraphTransformer, init=False)
|
|
95
|
+
dataset_info: Dict[str, Any] = field(default_factory=dict, init=False)
|
|
96
|
+
|
|
97
|
+
def __post_init__(self):
|
|
98
|
+
"""Initialize the model after dataclass initialization."""
|
|
99
|
+
super().__post_init__()
|
|
100
|
+
self.input_dim_X = None
|
|
101
|
+
self.input_dim_E = None
|
|
102
|
+
self.input_dim_y = 1
|
|
103
|
+
self.hidden_size_y: int = 128
|
|
104
|
+
self.max_node: int = None
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _get_param_names() -> List[str]:
|
|
108
|
+
return [
|
|
109
|
+
# Model Hyperparameters
|
|
110
|
+
"hidden_size_X", "hidden_size_E", "hidden_size_y", "num_layer", "n_head", "dropout",
|
|
111
|
+
"input_dim_X", "input_dim_E", "input_dim_y", "max_node",
|
|
112
|
+
# Diffusion parameters
|
|
113
|
+
"timesteps", "dataset_info",
|
|
114
|
+
# Training Parameters
|
|
115
|
+
"batch_size", "epochs", "learning_rate", "grad_clip_value",
|
|
116
|
+
"weight_decay", "lw_X", "lw_E",
|
|
117
|
+
# Scheduler Parameters
|
|
118
|
+
"use_lr_scheduler", "scheduler_factor", "scheduler_patience",
|
|
119
|
+
# Other Parameters
|
|
120
|
+
"fitting_epoch", "fitting_loss", "device", "verbose", "model_name"
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
124
|
+
params = ["num_layer", "input_dim_X", "input_dim_E", "input_dim_y",
|
|
125
|
+
"hidden_size_X", "hidden_size_E", "hidden_size_y", "n_head", "dropout"]
|
|
126
|
+
|
|
127
|
+
if checkpoint is not None:
|
|
128
|
+
if "hyperparameters" not in checkpoint:
|
|
129
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
130
|
+
return {k: checkpoint["hyperparameters"][k] for k in params}
|
|
131
|
+
|
|
132
|
+
return {k: getattr(self, k) for k in params}
|
|
133
|
+
|
|
134
|
+
def _convert_to_pytorch_data(self, X, y=None):
|
|
135
|
+
"""Convert numpy arrays to PyTorch Geometric data format.
|
|
136
|
+
"""
|
|
137
|
+
if self.verbose:
|
|
138
|
+
iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
|
|
139
|
+
else:
|
|
140
|
+
iterator = enumerate(X)
|
|
141
|
+
|
|
142
|
+
pyg_graph_list = []
|
|
143
|
+
for idx, smiles_or_mol in iterator:
|
|
144
|
+
if y is not None:
|
|
145
|
+
properties = y[idx]
|
|
146
|
+
else:
|
|
147
|
+
properties = None
|
|
148
|
+
graph = graph_from_smiles(smiles_or_mol, properties)
|
|
149
|
+
g = Data()
|
|
150
|
+
|
|
151
|
+
# No H, first heavy atom has type 0
|
|
152
|
+
node_type = torch.from_numpy(graph['node_feat'][:, 0] - 1)
|
|
153
|
+
|
|
154
|
+
# Filter out invalid node types (< 0)
|
|
155
|
+
valid_mask = node_type >= 0
|
|
156
|
+
if not valid_mask.all():
|
|
157
|
+
# Get valid nodes and adjust edge indices
|
|
158
|
+
valid_indices = torch.where(valid_mask)[0]
|
|
159
|
+
index_map = -torch.ones(node_type.size(0), dtype=torch.long)
|
|
160
|
+
index_map[valid_indices] = torch.arange(valid_indices.size(0))
|
|
161
|
+
|
|
162
|
+
# Filter edges that connect to invalid nodes
|
|
163
|
+
edge_index = torch.from_numpy(graph["edge_index"])
|
|
164
|
+
valid_edges_mask = valid_mask[edge_index[0]] & valid_mask[edge_index[1]]
|
|
165
|
+
valid_edge_index = edge_index[:, valid_edges_mask]
|
|
166
|
+
|
|
167
|
+
# Remap edge indices to account for removed nodes
|
|
168
|
+
remapped_edge_index = index_map[valid_edge_index]
|
|
169
|
+
|
|
170
|
+
# Filter edge attributes
|
|
171
|
+
edge_attr = torch.from_numpy(graph["edge_feat"])[:, 0] + 1
|
|
172
|
+
valid_edge_attr = edge_attr[valid_edges_mask]
|
|
173
|
+
|
|
174
|
+
# Update node and edge data
|
|
175
|
+
node_type = node_type[valid_mask]
|
|
176
|
+
g.edge_index = remapped_edge_index
|
|
177
|
+
g.edge_attr = valid_edge_attr.long().squeeze(-1)
|
|
178
|
+
else:
|
|
179
|
+
# No invalid nodes, proceed normally
|
|
180
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
181
|
+
edge_attr = torch.from_numpy(graph["edge_feat"])[:, 0] + 1
|
|
182
|
+
g.edge_attr = edge_attr.long().squeeze(-1)
|
|
183
|
+
|
|
184
|
+
# * is encoded as "misc" which is 119 - 1 and should be 117
|
|
185
|
+
node_type[node_type == 118] = 117
|
|
186
|
+
g.x = node_type.long().squeeze(-1)
|
|
187
|
+
# g.y = torch.from_numpy(graph["y"])
|
|
188
|
+
g.y = torch.zeros(1, 1)
|
|
189
|
+
del graph["node_feat"]
|
|
190
|
+
del graph["edge_index"]
|
|
191
|
+
del graph["edge_feat"]
|
|
192
|
+
del graph["y"]
|
|
193
|
+
|
|
194
|
+
pyg_graph_list.append(g)
|
|
195
|
+
|
|
196
|
+
return pyg_graph_list
|
|
197
|
+
|
|
198
|
+
def _setup_diffusion_params(self, X: Union[List, Dict]) -> None:
|
|
199
|
+
# Extract dataset info from X if it's a dict (from checkpoint), otherwise compute it
|
|
200
|
+
if isinstance(X, dict):
|
|
201
|
+
dataset_info = X["hyperparameters"]["dataset_info"]
|
|
202
|
+
timesteps = X["hyperparameters"]["timesteps"]
|
|
203
|
+
max_node = X["hyperparameters"]["max_node"]
|
|
204
|
+
else:
|
|
205
|
+
assert isinstance(X, list), "X must be a list of SMILES strings, but got {}".format(type(X))
|
|
206
|
+
dataset_info = compute_dataset_info(X)
|
|
207
|
+
timesteps = self.timesteps
|
|
208
|
+
max_node = dataset_info["max_node"]
|
|
209
|
+
|
|
210
|
+
self.input_dim_X = dataset_info["x_margins"].shape[0]
|
|
211
|
+
self.input_dim_E = dataset_info["e_margins"].shape[0]
|
|
212
|
+
self.dataset_info = dataset_info
|
|
213
|
+
self.timesteps = timesteps
|
|
214
|
+
self.max_node = max_node
|
|
215
|
+
|
|
216
|
+
x_limit = dataset_info["x_margins"].to(self.device)
|
|
217
|
+
e_limit = dataset_info["e_margins"].to(self.device)
|
|
218
|
+
self.transition_model = MarginalTransition(x_limit, e_limit, y_classes=self.input_dim_y)
|
|
219
|
+
self.limit_dist = PlaceHolder(X=x_limit, E=e_limit, y=None)
|
|
220
|
+
self.noise_schedule = NoiseScheduleDiscrete(timesteps=self.timesteps).to(self.device)
|
|
221
|
+
|
|
222
|
+
def _initialize_model(
|
|
223
|
+
self,
|
|
224
|
+
model_class: Type[torch.nn.Module],
|
|
225
|
+
checkpoint: Optional[Dict] = None
|
|
226
|
+
) -> torch.nn.Module:
|
|
227
|
+
"""Initialize the model with parameters or a checkpoint."""
|
|
228
|
+
model_params = self._get_model_params(checkpoint)
|
|
229
|
+
self.model = model_class(**model_params)
|
|
230
|
+
self.model = self.model.to(self.device)
|
|
231
|
+
|
|
232
|
+
if checkpoint is not None:
|
|
233
|
+
self._setup_diffusion_params(checkpoint)
|
|
234
|
+
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
235
|
+
return self.model
|
|
236
|
+
|
|
237
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
238
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
239
|
+
"""
|
|
240
|
+
optimizer = torch.optim.Adam(
|
|
241
|
+
self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
|
|
242
|
+
)
|
|
243
|
+
scheduler = None
|
|
244
|
+
if self.use_lr_scheduler:
|
|
245
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
246
|
+
optimizer,
|
|
247
|
+
mode="min",
|
|
248
|
+
factor=self.scheduler_factor,
|
|
249
|
+
patience=self.scheduler_patience,
|
|
250
|
+
min_lr=1e-6,
|
|
251
|
+
cooldown=0,
|
|
252
|
+
eps=1e-8,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return optimizer, scheduler
|
|
256
|
+
|
|
257
|
+
def fit(self, X_train: List[str]) -> "DigressMolecularGenerator":
|
|
258
|
+
num_task = 0 if self.input_dim_y is None else self.input_dim_y
|
|
259
|
+
X_train, _ = self._validate_inputs(X_train, num_task=num_task)
|
|
260
|
+
self._setup_diffusion_params(X_train)
|
|
261
|
+
self._initialize_model(self.model_class)
|
|
262
|
+
self.model.initialize_parameters()
|
|
263
|
+
|
|
264
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
265
|
+
train_dataset = self._convert_to_pytorch_data(X_train)
|
|
266
|
+
train_loader = DataLoader(
|
|
267
|
+
train_dataset,
|
|
268
|
+
batch_size=self.batch_size,
|
|
269
|
+
shuffle=True,
|
|
270
|
+
num_workers=0
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
self.fitting_loss = []
|
|
274
|
+
self.fitting_epoch = 0
|
|
275
|
+
for epoch in range(self.epochs):
|
|
276
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch)
|
|
277
|
+
self.fitting_loss.append(np.mean(train_losses).item())
|
|
278
|
+
if scheduler:
|
|
279
|
+
scheduler.step(np.mean(train_losses).item())
|
|
280
|
+
|
|
281
|
+
self.fitting_epoch = epoch
|
|
282
|
+
self.is_fitted_ = True
|
|
283
|
+
return self
|
|
284
|
+
|
|
285
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
286
|
+
self.model.train()
|
|
287
|
+
losses = []
|
|
288
|
+
iterator = (
|
|
289
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
290
|
+
if self.verbose
|
|
291
|
+
else train_loader
|
|
292
|
+
)
|
|
293
|
+
active_index = self.dataset_info["active_index"]
|
|
294
|
+
for step, batched_data in enumerate(iterator):
|
|
295
|
+
batched_data = batched_data.to(self.device)
|
|
296
|
+
optimizer.zero_grad()
|
|
297
|
+
|
|
298
|
+
data_x = F.one_hot(batched_data.x, num_classes=118).float()[:, active_index]
|
|
299
|
+
data_edge_attr = F.one_hot(batched_data.edge_attr, num_classes=5).float()
|
|
300
|
+
dense_data, node_mask = to_dense(data_x, batched_data.edge_index, data_edge_attr, batched_data.batch, self.max_node)
|
|
301
|
+
dense_data = dense_data.mask(node_mask)
|
|
302
|
+
X, E = dense_data.X, dense_data.E
|
|
303
|
+
noisy_data = self.apply_noise(X, E, batched_data.y, node_mask)
|
|
304
|
+
|
|
305
|
+
loss, loss_X, loss_E = self.model.compute_loss(noisy_data, true_X=X, true_E=E, lw_X=self.lw_X, lw_E=self.lw_E)
|
|
306
|
+
if self.grad_clip_value is not None:
|
|
307
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
308
|
+
loss.backward()
|
|
309
|
+
optimizer.step()
|
|
310
|
+
losses.append(loss.item())
|
|
311
|
+
|
|
312
|
+
if self.verbose:
|
|
313
|
+
iterator.set_postfix({"Epoch": epoch, "Loss": f"{loss.item():.4f}", "Loss_X": f"{loss_X.item():.4f}", "Loss_E": f"{loss_E.item():.4f}"})
|
|
314
|
+
|
|
315
|
+
return losses
|
|
316
|
+
|
|
317
|
+
def apply_noise(self, X, E, y, node_mask) -> Dict[str, Any]:
|
|
318
|
+
t_int = torch.randint(0, self.timesteps + 1, size=(X.size(0), 1), device=X.device).float() # (bs, 1)
|
|
319
|
+
s_int = t_int - 1
|
|
320
|
+
|
|
321
|
+
t_float = t_int / self.timesteps
|
|
322
|
+
s_float = s_int / self.timesteps
|
|
323
|
+
|
|
324
|
+
# beta_t and alpha_s_bar are used for denoising/loss computation
|
|
325
|
+
beta_t = self.noise_schedule(t_normalized=t_float) # (bs, 1)
|
|
326
|
+
alpha_s_bar = self.noise_schedule.get_alpha_bar(t_normalized=s_float) # (bs, 1)
|
|
327
|
+
alpha_t_bar = self.noise_schedule.get_alpha_bar(t_normalized=t_float) # (bs, 1)
|
|
328
|
+
|
|
329
|
+
Qtb = self.transition_model.get_Qt_bar(alpha_t_bar, device=self.device) # (bs, dx_in, dx_out), (bs, de_in, de_out)
|
|
330
|
+
assert (abs(Qtb.X.sum(dim=2) - 1.) < 1e-4).all(), Qtb.X.sum(dim=2) - 1
|
|
331
|
+
assert (abs(Qtb.E.sum(dim=2) - 1.) < 1e-4).all()
|
|
332
|
+
|
|
333
|
+
probX = X @ Qtb.X # (bs, n, dx_out)
|
|
334
|
+
probE = E @ Qtb.E.unsqueeze(1) # (bs, n, n, de_out)
|
|
335
|
+
|
|
336
|
+
sampled_t = sample_discrete_features(probX=probX, probE=probE, node_mask=node_mask)
|
|
337
|
+
|
|
338
|
+
X_t = F.one_hot(sampled_t.X, num_classes=self.input_dim_X)
|
|
339
|
+
E_t = F.one_hot(sampled_t.E, num_classes=self.input_dim_E)
|
|
340
|
+
assert (X.shape == X_t.shape) and (E.shape == E_t.shape)
|
|
341
|
+
|
|
342
|
+
z_t = PlaceHolder(X=X_t, E=E_t, y=y).type_as(X_t).mask(node_mask)
|
|
343
|
+
|
|
344
|
+
noisy_data = {'t': t_float * self.timesteps, 'beta_t': beta_t, 'alpha_s_bar': alpha_s_bar,
|
|
345
|
+
'alpha_t_bar': alpha_t_bar, 'X_t': z_t.X, 'E_t': z_t.E, 'y_t': z_t.y, 'node_mask': node_mask}
|
|
346
|
+
|
|
347
|
+
return noisy_data
|
|
348
|
+
|
|
349
|
+
@torch.no_grad()
|
|
350
|
+
def generate(self, num_nodes: Optional[Union[List[List], np.ndarray, torch.Tensor]] = None, batch_size: int = 32) -> List[str]:
|
|
351
|
+
"""Randomly generate molecules with specified node counts.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
num_nodes : Optional[Union[List[List], np.ndarray, torch.Tensor]], default=None
|
|
356
|
+
Number of nodes for each molecule in the batch. If None, samples from
|
|
357
|
+
the training distribution. Can be provided as:
|
|
358
|
+
- A list of lists
|
|
359
|
+
- A numpy array of shape (batch_size, 1)
|
|
360
|
+
- A torch tensor of shape (batch_size, 1)
|
|
361
|
+
|
|
362
|
+
batch_size : int, default=32
|
|
363
|
+
Number of molecules to generate.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
List[str]
|
|
368
|
+
List of generated molecules in SMILES format.
|
|
369
|
+
"""
|
|
370
|
+
if not self.is_fitted_:
|
|
371
|
+
raise ValueError("Model must be fitted before generating molecules.")
|
|
372
|
+
if self.input_dim_y is None or self.input_dim_X is None or self.input_dim_E is None or self.max_node is None:
|
|
373
|
+
raise ValueError(f"Model may not be fitted correctly as one of below attributes is not set: input_dim_y={self.input_dim_y}, input_dim_X={self.input_dim_X}, input_dim_E={self.input_dim_E}, max_node={self.max_node}")
|
|
374
|
+
|
|
375
|
+
if num_nodes is not None:
|
|
376
|
+
batch_size = len(num_nodes)
|
|
377
|
+
|
|
378
|
+
if num_nodes is None:
|
|
379
|
+
num_nodes_dist = self.dataset_info["num_nodes_dist"]
|
|
380
|
+
num_nodes = num_nodes_dist.sample_n(batch_size, self.device)
|
|
381
|
+
elif isinstance(num_nodes, list):
|
|
382
|
+
num_nodes = torch.tensor(num_nodes).to(self.device)
|
|
383
|
+
elif isinstance(num_nodes, np.ndarray):
|
|
384
|
+
num_nodes = torch.from_numpy(num_nodes).to(self.device)
|
|
385
|
+
if num_nodes.dim() == 1:
|
|
386
|
+
num_nodes = num_nodes.unsqueeze(-1)
|
|
387
|
+
|
|
388
|
+
assert num_nodes.size(0) == batch_size
|
|
389
|
+
arange = (
|
|
390
|
+
torch.arange(self.max_node).to(self.device)
|
|
391
|
+
.unsqueeze(0)
|
|
392
|
+
.expand(batch_size, -1)
|
|
393
|
+
)
|
|
394
|
+
node_mask = arange < num_nodes
|
|
395
|
+
|
|
396
|
+
if not hasattr(self, 'limit_dist') or self.limit_dist is None:
|
|
397
|
+
raise ValueError("Limit distribution not found. Please call setup_diffusion_params first.")
|
|
398
|
+
if not hasattr(self, 'dataset_info') or self.dataset_info is None:
|
|
399
|
+
raise ValueError("Dataset info not found. Please call setup_diffusion_params first.")
|
|
400
|
+
|
|
401
|
+
z_T = sample_discrete_feature_noise(
|
|
402
|
+
limit_dist=self.limit_dist, node_mask=node_mask
|
|
403
|
+
)
|
|
404
|
+
X, E = z_T.X, z_T.E
|
|
405
|
+
y = torch.zeros(batch_size, 1).to(self.device)
|
|
406
|
+
|
|
407
|
+
assert (E == torch.transpose(E, 1, 2)).all()
|
|
408
|
+
|
|
409
|
+
# Iteratively sample p(z_s | z_t) for t = 1, ..., T, with s = t - 1.
|
|
410
|
+
for s_int in reversed(range(0, self.timesteps)):
|
|
411
|
+
s_array = s_int * torch.ones((batch_size, 1)).float().to(self.device)
|
|
412
|
+
t_array = s_array + 1
|
|
413
|
+
s_norm = s_array / self.timesteps
|
|
414
|
+
t_norm = t_array / self.timesteps
|
|
415
|
+
|
|
416
|
+
# Sample z_s
|
|
417
|
+
sampled_s = self.sample_p_zs_given_zt(s_norm, t_norm, X, E, y, node_mask)
|
|
418
|
+
X, E, y = sampled_s.X, sampled_s.E, sampled_s.y
|
|
419
|
+
|
|
420
|
+
# Sample
|
|
421
|
+
sampled_s = sampled_s.mask(node_mask, collapse=True)
|
|
422
|
+
X, E, y = sampled_s.X, sampled_s.E, sampled_s.y
|
|
423
|
+
|
|
424
|
+
molecule_list = []
|
|
425
|
+
for i in range(batch_size):
|
|
426
|
+
n = num_nodes[i][0].item()
|
|
427
|
+
atom_types = X[i, :n].cpu()
|
|
428
|
+
edge_types = E[i, :n, :n].cpu()
|
|
429
|
+
molecule_list.append([atom_types, edge_types])
|
|
430
|
+
|
|
431
|
+
smiles_list = graph_to_smiles(molecule_list, self.dataset_info["atom_decoder"])
|
|
432
|
+
return smiles_list
|
|
433
|
+
|
|
434
|
+
def sample_p_zs_given_zt(
|
|
435
|
+
self, s, t, X_t, E_t, y, node_mask
|
|
436
|
+
):
|
|
437
|
+
"""Samples from zs ~ p(zs | zt). Only used during sampling.
|
|
438
|
+
if last_step, return the graph prediction as well"""
|
|
439
|
+
bs, n, _ = X_t.shape
|
|
440
|
+
beta_t = self.noise_schedule(t_normalized=t) # (bs, 1)
|
|
441
|
+
alpha_s_bar = self.noise_schedule.get_alpha_bar(t_normalized=s)
|
|
442
|
+
alpha_t_bar = self.noise_schedule.get_alpha_bar(t_normalized=t)
|
|
443
|
+
|
|
444
|
+
# Neural net predictions
|
|
445
|
+
noisy_data = {
|
|
446
|
+
"X_t": X_t,
|
|
447
|
+
"E_t": E_t,
|
|
448
|
+
"y_t": y,
|
|
449
|
+
"t": t,
|
|
450
|
+
"node_mask": node_mask,
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pred = self.model(noisy_data)
|
|
454
|
+
|
|
455
|
+
# Normalize predictions
|
|
456
|
+
pred_X = F.softmax(pred.X, dim=-1) # bs, n, d0
|
|
457
|
+
pred_E = F.softmax(pred.E, dim=-1) # bs, n, n, d0
|
|
458
|
+
|
|
459
|
+
device = pred_X.device
|
|
460
|
+
# Retrieve transitions matrix
|
|
461
|
+
Qtb = self.transition_model.get_Qt_bar(alpha_t_bar, device)
|
|
462
|
+
Qsb = self.transition_model.get_Qt_bar(alpha_s_bar, device)
|
|
463
|
+
Qt = self.transition_model.get_Qt(beta_t, device)
|
|
464
|
+
|
|
465
|
+
p_s_and_t_given_0_X = compute_batched_over0_posterior_distribution(
|
|
466
|
+
X_t=X_t,
|
|
467
|
+
Qt=Qt.X,
|
|
468
|
+
Qsb=Qsb.X,
|
|
469
|
+
Qtb=Qtb.X
|
|
470
|
+
)
|
|
471
|
+
p_s_and_t_given_0_E = compute_batched_over0_posterior_distribution(
|
|
472
|
+
X_t=E_t,
|
|
473
|
+
Qt=Qt.E,
|
|
474
|
+
Qsb=Qsb.E,
|
|
475
|
+
Qtb=Qtb.E
|
|
476
|
+
)
|
|
477
|
+
# Dim of these two tensors: bs, N, d0, d_t-1
|
|
478
|
+
weighted_X = pred_X.unsqueeze(-1) * p_s_and_t_given_0_X # bs, n, d0, d_t-1
|
|
479
|
+
unnormalized_prob_X = weighted_X.sum(dim=2) # bs, n, d_t-1
|
|
480
|
+
unnormalized_prob_X[torch.sum(unnormalized_prob_X, dim=-1) == 0] = 1e-5
|
|
481
|
+
prob_X = unnormalized_prob_X / torch.sum(unnormalized_prob_X, dim=-1, keepdim=True) # bs, n, d_t-1
|
|
482
|
+
|
|
483
|
+
pred_E = pred_E.reshape((bs, -1, pred_E.shape[-1]))
|
|
484
|
+
weighted_E = pred_E.unsqueeze(-1) * p_s_and_t_given_0_E # bs, N, d0, d_t-1
|
|
485
|
+
unnormalized_prob_E = weighted_E.sum(dim=-2)
|
|
486
|
+
unnormalized_prob_E[torch.sum(unnormalized_prob_E, dim=-1) == 0] = 1e-5
|
|
487
|
+
prob_E = unnormalized_prob_E / torch.sum(unnormalized_prob_E, dim=-1, keepdim=True)
|
|
488
|
+
prob_E = prob_E.reshape(bs, n, n, pred_E.shape[-1])
|
|
489
|
+
|
|
490
|
+
assert ((prob_X.sum(dim=-1) - 1).abs() < 1e-4).all()
|
|
491
|
+
assert ((prob_E.sum(dim=-1) - 1).abs() < 1e-4).all()
|
|
492
|
+
|
|
493
|
+
sampled_s = sample_discrete_features(prob_X, prob_E, node_mask=node_mask)
|
|
494
|
+
|
|
495
|
+
X_s = F.one_hot(sampled_s.X, num_classes=self.input_dim_X).to(self.device).float()
|
|
496
|
+
E_s = F.one_hot(sampled_s.E, num_classes=self.input_dim_E).to(self.device).float()
|
|
497
|
+
|
|
498
|
+
assert (E_s == torch.transpose(E_s, 1, 2)).all()
|
|
499
|
+
assert (X_t.shape == X_s.shape) and (E_t.shape == E_s.shape)
|
|
500
|
+
|
|
501
|
+
out_one_hot = PlaceHolder(X=X_s, E=E_s, y=y)
|
|
502
|
+
|
|
503
|
+
return out_one_hot.mask(node_mask)
|