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,427 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from typing import Optional, Dict, Any, Tuple, List, Type
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
import torch.nn.functional as F
|
|
9
|
+
from torch.utils.data import DataLoader
|
|
10
|
+
from torch.cuda.amp import GradScaler
|
|
11
|
+
|
|
12
|
+
from .gpt import GPT
|
|
13
|
+
from .dataset import SmilesDataset
|
|
14
|
+
from ...base import BaseMolecularGenerator
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MolGPTMolecularGenerator(BaseMolecularGenerator):
|
|
18
|
+
"""
|
|
19
|
+
This generator implements the molecular GPT model for generating molecules.
|
|
20
|
+
|
|
21
|
+
The model uses a GPT-like architecture to learn the distribution of SMILES strings
|
|
22
|
+
and generate new molecules. It supports conditional generation based on properties
|
|
23
|
+
and/or molecular scaffolds.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
- MolGPT: Molecular Generation Using a Transformer-Decoder Model. Journal of Chemical
|
|
28
|
+
Information and Modeling. https://pubs.acs.org/doi/10.1021/acs.jcim.1c00600
|
|
29
|
+
- Code: https://github.com/devalab/molgpt
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
num_layer : int, default=8
|
|
34
|
+
Number of transformer layers in the model.
|
|
35
|
+
num_head : int, default=8
|
|
36
|
+
Number of attention heads in each transformer layer.
|
|
37
|
+
hidden_size : int, default=256
|
|
38
|
+
Dimension of the hidden representations.
|
|
39
|
+
max_len : int, default=128
|
|
40
|
+
Maximum length of SMILES strings.
|
|
41
|
+
num_task : int, default=0
|
|
42
|
+
Number of property prediction tasks for conditional generation. O for unconditional generation.
|
|
43
|
+
use_scaffold : bool, default=False
|
|
44
|
+
Whether to use scaffold conditioning.
|
|
45
|
+
use_lstm : bool, default=False
|
|
46
|
+
Whether to use LSTM for encoding scaffold.
|
|
47
|
+
lstm_layers : int, default=0
|
|
48
|
+
Number of LSTM layers if use_lstm is True.
|
|
49
|
+
batch_size : int, default=64
|
|
50
|
+
Batch size for training.
|
|
51
|
+
epochs : int, default=1000
|
|
52
|
+
Number of training epochs.
|
|
53
|
+
learning_rate : float, default=3e-4
|
|
54
|
+
Learning rate for optimizer.
|
|
55
|
+
adamw_betas : Tuple[float, float], default=(0.9, 0.95)
|
|
56
|
+
Beta parameters for AdamW optimizer.
|
|
57
|
+
weight_decay : float, default=0.1
|
|
58
|
+
Weight decay for optimizer.
|
|
59
|
+
grad_norm_clip : float, default=1.0
|
|
60
|
+
Gradient norm clipping value.
|
|
61
|
+
verbose : bool, default=False
|
|
62
|
+
Whether to display progress bars during training.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# Model parameters
|
|
66
|
+
num_layer: int = 8
|
|
67
|
+
num_head: int = 8
|
|
68
|
+
hidden_size: int = 256
|
|
69
|
+
max_len: int = 128
|
|
70
|
+
|
|
71
|
+
# Conditioning parameters
|
|
72
|
+
num_task: int = 0
|
|
73
|
+
use_scaffold: bool = False
|
|
74
|
+
use_lstm: bool = False
|
|
75
|
+
lstm_layers: int = 0
|
|
76
|
+
|
|
77
|
+
# Training parameters
|
|
78
|
+
batch_size: int = 64
|
|
79
|
+
epochs: int = 1000
|
|
80
|
+
learning_rate: float = 3e-4
|
|
81
|
+
adamw_betas: Tuple[float, float] = (0.9, 0.95)
|
|
82
|
+
weight_decay: float = 0.1
|
|
83
|
+
grad_norm_clip: float = 1.0
|
|
84
|
+
|
|
85
|
+
verbose: bool = False
|
|
86
|
+
# Attributes
|
|
87
|
+
model_name: str = "MolGPTMolecularGenerator"
|
|
88
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
89
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
90
|
+
model_class: Type[GPT] = field(default=GPT, init=False)
|
|
91
|
+
|
|
92
|
+
def __post_init__(self):
|
|
93
|
+
"""Initialize the model after dataclass initialization."""
|
|
94
|
+
super().__post_init__()
|
|
95
|
+
self.vocab_size = None
|
|
96
|
+
self.token_to_id = None
|
|
97
|
+
self.id_to_token = None
|
|
98
|
+
self.scaffold_maxlen = None
|
|
99
|
+
self.pattern = "(\[[^\]]+]|<|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9])"
|
|
100
|
+
self.regex = re.compile(self.pattern)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _get_param_names() -> List[str]:
|
|
104
|
+
"""Get parameter names for the estimator.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
List[str]
|
|
109
|
+
List of parameter names that can be used for model configuration.
|
|
110
|
+
"""
|
|
111
|
+
return [
|
|
112
|
+
# Model Hyperparameters
|
|
113
|
+
"num_layer", "num_head", "hidden_size",
|
|
114
|
+
# Conditioning parameters
|
|
115
|
+
"num_task", "use_scaffold", "use_lstm", "lstm_layers",
|
|
116
|
+
# post-initialization parameters
|
|
117
|
+
"vocab_size", "token_to_id", "id_to_token", "scaffold_maxlen", "max_len",
|
|
118
|
+
# Training Parameters
|
|
119
|
+
"batch_size", "epochs", "learning_rate", "weight_decay", "grad_norm_clip",
|
|
120
|
+
# Other Parameters
|
|
121
|
+
"verbose", "model_name", "fitting_epoch", "fitting_loss", "device"
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
def _initialize_model(
|
|
125
|
+
self,
|
|
126
|
+
model_class: Type[torch.nn.Module],
|
|
127
|
+
checkpoint: Optional[Dict] = None
|
|
128
|
+
) -> torch.nn.Module:
|
|
129
|
+
"""Initialize the model with parameters or a checkpoint.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
model_class : Type[torch.nn.Module]
|
|
134
|
+
PyTorch module class to instantiate
|
|
135
|
+
checkpoint : Optional[Dict], default=None
|
|
136
|
+
Optional dictionary containing model checkpoint data
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
torch.nn.Module
|
|
141
|
+
Initialized PyTorch model
|
|
142
|
+
"""
|
|
143
|
+
model_params = self._get_model_params(checkpoint)
|
|
144
|
+
self.model = model_class(**model_params)
|
|
145
|
+
self.model = self.model.to(self.device)
|
|
146
|
+
|
|
147
|
+
if checkpoint is not None:
|
|
148
|
+
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
149
|
+
# get other params in checkpoint["hyperparameters"] but NOT in model_params
|
|
150
|
+
other_params = {k: checkpoint["hyperparameters"][k] for k in checkpoint["hyperparameters"] if k not in model_params}
|
|
151
|
+
# set other params in self
|
|
152
|
+
for k, v in other_params.items():
|
|
153
|
+
setattr(self, k, v)
|
|
154
|
+
return self.model
|
|
155
|
+
|
|
156
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
157
|
+
params = [
|
|
158
|
+
"vocab_size", "max_len", "num_task", "num_layer", "num_head", "hidden_size", "use_scaffold", "scaffold_maxlen", "use_lstm", "lstm_layers",
|
|
159
|
+
]
|
|
160
|
+
if checkpoint is not None:
|
|
161
|
+
if "hyperparameters" not in checkpoint:
|
|
162
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
163
|
+
return {k: checkpoint["hyperparameters"][k] for k in params}
|
|
164
|
+
|
|
165
|
+
return {k: getattr(self, k) for k in params}
|
|
166
|
+
|
|
167
|
+
def _setup_optimizers(self):
|
|
168
|
+
train_config = {
|
|
169
|
+
"learning_rate": self.learning_rate,
|
|
170
|
+
"weight_decay": self.weight_decay,
|
|
171
|
+
"betas": self.adamw_betas
|
|
172
|
+
}
|
|
173
|
+
return self.model.configure_optimizers(train_config)
|
|
174
|
+
|
|
175
|
+
def fit(self, X_train, y_train=None, X_scaffold=None):
|
|
176
|
+
"""
|
|
177
|
+
Train the MolGPT model on SMILES strings.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
X_train : List[str]
|
|
182
|
+
List of SMILES strings for training
|
|
183
|
+
y_train : Optional[List[float]]
|
|
184
|
+
Optional list of property values for conditional generation
|
|
185
|
+
X_scaffold : Optional[List[str]]
|
|
186
|
+
Optional list of scaffold SMILES strings for conditional generation
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
self : MolGPTGenerator
|
|
191
|
+
The fitted model
|
|
192
|
+
"""
|
|
193
|
+
X_train, y_train = self._validate_inputs(X_train, y_train, num_task=self.num_task, return_rdkit_mol=False)
|
|
194
|
+
|
|
195
|
+
# Calculate max length for padding
|
|
196
|
+
lens = [len(self.regex.findall(i.strip())) for i in X_train]
|
|
197
|
+
max_len = max(lens)
|
|
198
|
+
|
|
199
|
+
if X_scaffold is not None:
|
|
200
|
+
assert len(X_scaffold) == len(X_train), "X_scaffold and X_train must have the same length"
|
|
201
|
+
assert self.use_scaffold, "use_scaffold must be True"
|
|
202
|
+
X_scaffold, _ = self._validate_inputs(X_scaffold, num_task=self.num_task, return_rdkit_mol=False)
|
|
203
|
+
scaffold_maxlen = max([len(self.regex.findall(i.strip())) for i in X_scaffold])
|
|
204
|
+
else:
|
|
205
|
+
scaffold_maxlen = 0
|
|
206
|
+
|
|
207
|
+
self.scaffold_maxlen = scaffold_maxlen
|
|
208
|
+
self.max_len = max_len
|
|
209
|
+
|
|
210
|
+
# Create dataset
|
|
211
|
+
train_dataset = SmilesDataset(
|
|
212
|
+
X_train,
|
|
213
|
+
self.regex,
|
|
214
|
+
max_len,
|
|
215
|
+
properties=y_train,
|
|
216
|
+
scaffolds=X_scaffold,
|
|
217
|
+
scaffold_maxlen=scaffold_maxlen,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Save vocabulary
|
|
221
|
+
self.vocab_size = train_dataset.vocab_size
|
|
222
|
+
self.token_to_id = train_dataset.stoi
|
|
223
|
+
self.id_to_token = train_dataset.itos
|
|
224
|
+
|
|
225
|
+
# Initialize model
|
|
226
|
+
self._initialize_model(self.model_class)
|
|
227
|
+
self.model.initialize_parameters()
|
|
228
|
+
optimizer = self._setup_optimizers()
|
|
229
|
+
|
|
230
|
+
# Create data loader
|
|
231
|
+
train_loader = DataLoader(
|
|
232
|
+
train_dataset,
|
|
233
|
+
batch_size=self.batch_size,
|
|
234
|
+
shuffle=True,
|
|
235
|
+
num_workers=0
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Training loop
|
|
239
|
+
self.fitting_loss = []
|
|
240
|
+
self.fitting_epoch = 0
|
|
241
|
+
|
|
242
|
+
scaler = GradScaler()
|
|
243
|
+
for epoch in range(self.epochs):
|
|
244
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch, scaler)
|
|
245
|
+
self.fitting_loss.append(np.mean(train_losses))
|
|
246
|
+
|
|
247
|
+
self.fitting_epoch = epoch
|
|
248
|
+
self.is_fitted_ = True
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def _train_epoch(self, train_loader, optimizer, epoch, scaler):
|
|
252
|
+
self.model.train()
|
|
253
|
+
losses = []
|
|
254
|
+
|
|
255
|
+
iterator = (
|
|
256
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
257
|
+
if self.verbose
|
|
258
|
+
else train_loader
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
for step, (x, y, prop, scaffold) in enumerate(iterator):
|
|
262
|
+
x = x.to(self.device)
|
|
263
|
+
y = y.to(self.device)
|
|
264
|
+
prop = prop.to(self.device) if prop.numel() > 0 else None
|
|
265
|
+
scaffold = scaffold.to(self.device) if scaffold.numel() > 0 else None
|
|
266
|
+
|
|
267
|
+
optimizer.zero_grad()
|
|
268
|
+
loss = self.model.compute_loss(x, targets=y, prop=prop, scaffold=scaffold)
|
|
269
|
+
|
|
270
|
+
scaler.scale(loss).backward()
|
|
271
|
+
scaler.unscale_(optimizer)
|
|
272
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_norm_clip)
|
|
273
|
+
scaler.step(optimizer)
|
|
274
|
+
scaler.update()
|
|
275
|
+
losses.append(loss.item())
|
|
276
|
+
|
|
277
|
+
if self.verbose:
|
|
278
|
+
iterator.set_postfix({"Epoch": epoch, "Loss": f"{loss.item():.4f}"})
|
|
279
|
+
|
|
280
|
+
return losses
|
|
281
|
+
|
|
282
|
+
@torch.no_grad()
|
|
283
|
+
def sample(self, x, steps, temperature=1.0, top_k=None, prop=None, scaffold=None):
|
|
284
|
+
"""
|
|
285
|
+
Sample from the model given a context.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
x : torch.Tensor
|
|
290
|
+
Context tensor of shape (batch_size, seq_len)
|
|
291
|
+
steps : int
|
|
292
|
+
Number of steps to sample
|
|
293
|
+
temperature : float
|
|
294
|
+
Sampling temperature
|
|
295
|
+
top_k : int
|
|
296
|
+
Top-k sampling parameter
|
|
297
|
+
prop : torch.Tensor
|
|
298
|
+
Property conditioning tensor
|
|
299
|
+
scaffold : torch.Tensor
|
|
300
|
+
Scaffold conditioning tensor
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
torch.Tensor
|
|
305
|
+
Generated sequences
|
|
306
|
+
"""
|
|
307
|
+
model = self.model
|
|
308
|
+
model.eval()
|
|
309
|
+
|
|
310
|
+
for k in range(steps):
|
|
311
|
+
# Get block size from model
|
|
312
|
+
max_len = model.get_max_len()
|
|
313
|
+
|
|
314
|
+
# Crop context if needed
|
|
315
|
+
x_cond = x if x.size(1) <= max_len else x[:, -max_len:]
|
|
316
|
+
|
|
317
|
+
# Forward pass
|
|
318
|
+
logits, _ = model(x_cond, prop=prop, scaffold=scaffold)
|
|
319
|
+
|
|
320
|
+
# Get logits for the next token and apply temperature
|
|
321
|
+
logits = logits[:, -1, :] / temperature
|
|
322
|
+
|
|
323
|
+
# Apply top-k sampling if specified
|
|
324
|
+
if top_k is not None:
|
|
325
|
+
v, _ = torch.topk(logits, top_k)
|
|
326
|
+
logits[logits < v[:, [-1]]] = -float('Inf')
|
|
327
|
+
|
|
328
|
+
# Apply softmax to get probabilities
|
|
329
|
+
probs = F.softmax(logits, dim=-1)
|
|
330
|
+
|
|
331
|
+
# Sample from the distribution
|
|
332
|
+
next_token = torch.multinomial(probs, num_samples=1)
|
|
333
|
+
|
|
334
|
+
# Append to the sequence
|
|
335
|
+
x = torch.cat((x, next_token), dim=1)
|
|
336
|
+
|
|
337
|
+
return x
|
|
338
|
+
|
|
339
|
+
def generate(self, n_samples=10, properties=None, scaffolds=None, max_len=None, temperature=1.0, top_k=10, starting_token='C'):
|
|
340
|
+
"""
|
|
341
|
+
Generate molecules using the trained model.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
n_samples : int, default=10
|
|
346
|
+
Number of molecules to generate
|
|
347
|
+
properties : Optional[List[List[float]]]
|
|
348
|
+
Property values for conditional generation
|
|
349
|
+
scaffolds : Optional[List[str]]
|
|
350
|
+
Scaffold SMILES for conditional generation
|
|
351
|
+
max_len : Optional[int]
|
|
352
|
+
Maximum length of generated SMILES
|
|
353
|
+
temperature : float, default=1.0
|
|
354
|
+
Sampling temperature
|
|
355
|
+
top_k : int, default=10
|
|
356
|
+
Top-k sampling parameter
|
|
357
|
+
starting_token : Optional[str]
|
|
358
|
+
Starting token for generation (default is 'C')
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
List[str]
|
|
363
|
+
List of generated SMILES strings
|
|
364
|
+
"""
|
|
365
|
+
if not self.is_fitted_:
|
|
366
|
+
raise ValueError("Model must be fitted before generating molecules")
|
|
367
|
+
|
|
368
|
+
if max_len is None:
|
|
369
|
+
max_len = self.max_len
|
|
370
|
+
|
|
371
|
+
# Prepare property conditioning if provided
|
|
372
|
+
if properties is not None:
|
|
373
|
+
if len(properties) != n_samples:
|
|
374
|
+
raise ValueError(f"Number of property values ({len(properties)}) must match n_samples ({n_samples})")
|
|
375
|
+
prop_tensor = torch.tensor(properties, dtype=torch.float).to(self.device)
|
|
376
|
+
else:
|
|
377
|
+
prop_tensor = None
|
|
378
|
+
|
|
379
|
+
# Prepare scaffold conditioning if provided
|
|
380
|
+
if scaffolds is not None:
|
|
381
|
+
if len(scaffolds) != n_samples:
|
|
382
|
+
raise ValueError(f"Number of scaffolds ({len(scaffolds)}) must match n_samples ({n_samples})")
|
|
383
|
+
|
|
384
|
+
# Tokenize scaffolds
|
|
385
|
+
regex = re.compile(self.pattern)
|
|
386
|
+
|
|
387
|
+
scaffold_tokens = []
|
|
388
|
+
for scaffold in scaffolds:
|
|
389
|
+
tokens = regex.findall(scaffold.strip())
|
|
390
|
+
# Pad with '<' if needed
|
|
391
|
+
tokens += ['<'] * (self.scaffold_maxlen - len(tokens))
|
|
392
|
+
# Convert to indices
|
|
393
|
+
scaffold_tokens.append([self.token_to_id.get(t, 0) for t in tokens])
|
|
394
|
+
|
|
395
|
+
scaffold_tensor = torch.tensor(scaffold_tokens, dtype=torch.long).to(self.device)
|
|
396
|
+
else:
|
|
397
|
+
scaffold_tensor = None
|
|
398
|
+
|
|
399
|
+
if starting_token in self.token_to_id:
|
|
400
|
+
start_token = self.token_to_id[starting_token]
|
|
401
|
+
else:
|
|
402
|
+
warnings.warn(f"Starting token {starting_token} not found in vocabulary, using first token instead")
|
|
403
|
+
start_token = 0
|
|
404
|
+
|
|
405
|
+
context = torch.tensor([[start_token]] * n_samples, dtype=torch.long).to(self.device)
|
|
406
|
+
|
|
407
|
+
# Sample from the model
|
|
408
|
+
generated = self.sample(
|
|
409
|
+
context,
|
|
410
|
+
steps=max_len-1, # -1 because we already have one token
|
|
411
|
+
temperature=temperature,
|
|
412
|
+
top_k=top_k,
|
|
413
|
+
prop=prop_tensor,
|
|
414
|
+
scaffold=scaffold_tensor
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Convert to SMILES strings
|
|
418
|
+
smiles_list = []
|
|
419
|
+
for i in range(n_samples):
|
|
420
|
+
# Convert indices to tokens
|
|
421
|
+
tokens = [self.id_to_token[idx.item()] for idx in generated[i]]
|
|
422
|
+
# Join tokens and remove padding
|
|
423
|
+
smiles = ''.join(tokens).replace('<', '')
|
|
424
|
+
smiles_list.append(smiles)
|
|
425
|
+
|
|
426
|
+
return smiles_list
|
|
427
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# TODO
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .gnn import GNN_node, GNN_node_Virtualnode, GCNConv, GINConv
|
|
2
|
+
from .mlp import MLP
|
|
3
|
+
from .attention import AttentionWithNodeMask
|
|
4
|
+
from .embedder import TimestepEmbedder, CategoricalEmbedder, ClusterContinuousEmbedder
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MLP",
|
|
8
|
+
"GCNConv",
|
|
9
|
+
"GINConv",
|
|
10
|
+
"GNN_node",
|
|
11
|
+
"GNN_node_Virtualnode",
|
|
12
|
+
"AttentionWithNodeMask",
|
|
13
|
+
"TimestepEmbedder",
|
|
14
|
+
"CategoricalEmbedder",
|
|
15
|
+
"ClusterContinuousEmbedder",
|
|
16
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from torch.jit import Final
|
|
2
|
+
import torch.nn.functional as F
|
|
3
|
+
from itertools import repeat
|
|
4
|
+
import collections.abc
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
import torch.nn as nn
|
|
8
|
+
|
|
9
|
+
class AttentionWithNodeMask(nn.Module):
|
|
10
|
+
fast_attn: Final[bool]
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
dim,
|
|
15
|
+
num_head=8,
|
|
16
|
+
qkv_bias=False,
|
|
17
|
+
qk_norm=False,
|
|
18
|
+
attn_drop=0.,
|
|
19
|
+
proj_drop=0.,
|
|
20
|
+
norm_layer=nn.LayerNorm,
|
|
21
|
+
):
|
|
22
|
+
super().__init__()
|
|
23
|
+
assert dim % num_head == 0, 'dim should be divisible by num_head'
|
|
24
|
+
self.num_head = num_head
|
|
25
|
+
self.head_dim = dim // num_head
|
|
26
|
+
|
|
27
|
+
self.scale = self.head_dim ** -0.5
|
|
28
|
+
self.fast_attn = hasattr(torch.nn.functional, 'scaled_dot_product_attention') # FIXME
|
|
29
|
+
assert self.fast_attn, "scaled_dot_product_attention Not implemented"
|
|
30
|
+
|
|
31
|
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
|
32
|
+
|
|
33
|
+
self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
|
|
34
|
+
self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
|
|
35
|
+
self.attn_drop = nn.Dropout(attn_drop)
|
|
36
|
+
|
|
37
|
+
self.proj = nn.Linear(dim, dim)
|
|
38
|
+
self.proj_drop = nn.Dropout(proj_drop)
|
|
39
|
+
|
|
40
|
+
def forward(self, x, node_mask):
|
|
41
|
+
B, N, D = x.shape
|
|
42
|
+
|
|
43
|
+
# B, head, N, head_dim
|
|
44
|
+
qkv = self.qkv(x).reshape(B, N, 3, self.num_head, self.head_dim).permute(2, 0, 3, 1, 4)
|
|
45
|
+
q, k, v = qkv.unbind(0) # B, head, N, head_dim
|
|
46
|
+
q, k = self.q_norm(q), self.k_norm(k)
|
|
47
|
+
|
|
48
|
+
attn_mask = (node_mask[:, None, :, None] & node_mask[:, None, None, :]).expand(-1, self.num_head, N, N)
|
|
49
|
+
attn_mask = attn_mask.clone()
|
|
50
|
+
attn_mask[attn_mask.sum(-1) == 0] = True
|
|
51
|
+
|
|
52
|
+
x = F.scaled_dot_product_attention(
|
|
53
|
+
q, k, v,
|
|
54
|
+
dropout_p=self.attn_drop.p,
|
|
55
|
+
attn_mask=attn_mask,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
x = x.transpose(1, 2).reshape(B, N, -1)
|
|
59
|
+
|
|
60
|
+
x = self.proj(x)
|
|
61
|
+
x = self.proj_drop(x)
|
|
62
|
+
|
|
63
|
+
return x
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
class TimestepEmbedder(nn.Module):
|
|
6
|
+
"""
|
|
7
|
+
Embeds scalar timesteps into vector representations using a sinusoidal embedding
|
|
8
|
+
followed by a multilayer perceptron (MLP).
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
hidden_size : int
|
|
13
|
+
Output dimension of the MLP embedding.
|
|
14
|
+
frequency_embedding_size : int, optional
|
|
15
|
+
Size of the input frequency embedding, by default 256.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, hidden_size, frequency_embedding_size=256):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.mlp = nn.Sequential(
|
|
21
|
+
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
|
|
22
|
+
nn.SiLU(),
|
|
23
|
+
nn.Linear(hidden_size, hidden_size, bias=True),
|
|
24
|
+
)
|
|
25
|
+
self.frequency_embedding_size = frequency_embedding_size
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def timestep_embedding(t, dim, max_period=10000):
|
|
29
|
+
"""
|
|
30
|
+
Create sinusoidal timestep embeddings.
|
|
31
|
+
|
|
32
|
+
:param t: a 1-D Tensor of N indices, one per batch element.
|
|
33
|
+
These may be fractional.
|
|
34
|
+
:param dim: the dimension of the output.
|
|
35
|
+
:param max_period: controls the minimum frequency of the embeddings.
|
|
36
|
+
:return: an (N, D) Tensor of positional embeddings.
|
|
37
|
+
"""
|
|
38
|
+
# https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
|
|
39
|
+
half = dim // 2
|
|
40
|
+
freqs = torch.exp(
|
|
41
|
+
-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
|
|
42
|
+
).to(device=t.device)
|
|
43
|
+
args = t[:, None].float() * freqs[None]
|
|
44
|
+
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
|
45
|
+
if dim % 2:
|
|
46
|
+
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
|
47
|
+
return embedding
|
|
48
|
+
|
|
49
|
+
def forward(self, t):
|
|
50
|
+
"""
|
|
51
|
+
Forward pass for timestep embedding.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
t : torch.Tensor
|
|
56
|
+
1D tensor of scalar timesteps.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
torch.Tensor
|
|
61
|
+
The final embedded representation of shape (N, hidden_size).
|
|
62
|
+
"""
|
|
63
|
+
t = t.view(-1)
|
|
64
|
+
t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
|
|
65
|
+
t_emb = self.mlp(t_freq)
|
|
66
|
+
return t_emb
|
|
67
|
+
|
|
68
|
+
class CategoricalEmbedder(nn.Module):
|
|
69
|
+
"""
|
|
70
|
+
Embeds categorical conditions (e.g., data source labels) into vector representations.
|
|
71
|
+
Supports label dropout for classifier-free guidance.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
num_classes : int
|
|
76
|
+
Number of distinct label categories.
|
|
77
|
+
hidden_size : int
|
|
78
|
+
Size of the embedding vectors.
|
|
79
|
+
dropout_prob : float
|
|
80
|
+
Probability of label dropout.
|
|
81
|
+
"""
|
|
82
|
+
def __init__(self, num_classes, hidden_size, dropout_prob):
|
|
83
|
+
super().__init__()
|
|
84
|
+
use_cfg_embedding = dropout_prob > 0
|
|
85
|
+
self.embedding_table = nn.Embedding(num_classes + use_cfg_embedding, hidden_size)
|
|
86
|
+
self.num_classes = num_classes
|
|
87
|
+
self.dropout_prob = dropout_prob
|
|
88
|
+
|
|
89
|
+
def forward(self, labels, train, force_drop_ids=None):
|
|
90
|
+
"""
|
|
91
|
+
Forward pass for categorical embedding with optional label dropout.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
labels : torch.Tensor
|
|
96
|
+
Tensor of categorical labels.
|
|
97
|
+
train : bool
|
|
98
|
+
Whether the model is in training mode.
|
|
99
|
+
force_drop_ids : torch.Tensor or None, optional
|
|
100
|
+
Explicit mask for which labels to drop.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
torch.Tensor
|
|
105
|
+
Embedded label representations, with optional noise added during training.
|
|
106
|
+
"""
|
|
107
|
+
labels = labels.long().view(-1)
|
|
108
|
+
|
|
109
|
+
use_dropout = self.dropout_prob > 0
|
|
110
|
+
drop_ids = force_drop_ids == 1
|
|
111
|
+
|
|
112
|
+
if (train and use_dropout):
|
|
113
|
+
drop_ids_rand = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob
|
|
114
|
+
if force_drop_ids is not None:
|
|
115
|
+
drop_ids = torch.logical_or(drop_ids, drop_ids_rand)
|
|
116
|
+
else:
|
|
117
|
+
drop_ids = drop_ids_rand
|
|
118
|
+
|
|
119
|
+
if use_dropout:
|
|
120
|
+
labels = torch.where(drop_ids, self.num_classes, labels)
|
|
121
|
+
embeddings = self.embedding_table(labels)
|
|
122
|
+
if train:
|
|
123
|
+
noise = torch.randn_like(embeddings)
|
|
124
|
+
embeddings = embeddings + noise
|
|
125
|
+
return embeddings
|
|
126
|
+
|
|
127
|
+
class ClusterContinuousEmbedder(nn.Module):
|
|
128
|
+
"""
|
|
129
|
+
Embeds continuous input features into vector representations using a multilayer perceptron (MLP).
|
|
130
|
+
Supports optional embedding dropout for classifier-free guidance.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
input_size : int
|
|
135
|
+
The size of the input features.
|
|
136
|
+
hidden_size : int
|
|
137
|
+
The size of the output embedding vectors.
|
|
138
|
+
dropout_prob : float
|
|
139
|
+
Probability of embedding dropout, used for classifier-free guidance.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
def __init__(self, input_size, hidden_size, dropout_prob):
|
|
143
|
+
super().__init__()
|
|
144
|
+
use_cfg_embedding = dropout_prob > 0
|
|
145
|
+
|
|
146
|
+
if use_cfg_embedding:
|
|
147
|
+
self.embedding_drop = nn.Embedding(1, hidden_size)
|
|
148
|
+
|
|
149
|
+
self.mlp = nn.Sequential(
|
|
150
|
+
nn.Linear(input_size, hidden_size, bias=True),
|
|
151
|
+
nn.Softmax(dim=1),
|
|
152
|
+
nn.Linear(hidden_size, hidden_size, bias=False)
|
|
153
|
+
)
|
|
154
|
+
self.hidden_size = hidden_size
|
|
155
|
+
self.dropout_prob = dropout_prob
|
|
156
|
+
|
|
157
|
+
def forward(self, labels, train, force_drop_ids=None):
|
|
158
|
+
use_dropout = self.dropout_prob > 0
|
|
159
|
+
if force_drop_ids is not None:
|
|
160
|
+
drop_ids = force_drop_ids == 1
|
|
161
|
+
else:
|
|
162
|
+
drop_ids = None
|
|
163
|
+
if not use_dropout:
|
|
164
|
+
drop_ids = None
|
|
165
|
+
|
|
166
|
+
if (train and use_dropout):
|
|
167
|
+
drop_ids_rand = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob
|
|
168
|
+
if force_drop_ids is not None:
|
|
169
|
+
drop_ids = torch.logical_or(drop_ids, drop_ids_rand)
|
|
170
|
+
else:
|
|
171
|
+
drop_ids = drop_ids_rand
|
|
172
|
+
|
|
173
|
+
if drop_ids is not None:
|
|
174
|
+
embeddings = torch.zeros((labels.shape[0], self.hidden_size), device=labels.device)
|
|
175
|
+
embeddings[~drop_ids] = self.mlp(labels[~drop_ids])
|
|
176
|
+
embeddings[drop_ids] += self.embedding_drop.weight[0]
|
|
177
|
+
else:
|
|
178
|
+
embeddings = self.mlp(labels)
|
|
179
|
+
|
|
180
|
+
if train:
|
|
181
|
+
noise = torch.randn_like(embeddings)
|
|
182
|
+
embeddings = embeddings + noise
|
|
183
|
+
return embeddings
|