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,247 @@
|
|
|
1
|
+
from tqdm import tqdm
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Optional, Dict, Any, Tuple, List, Type
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from .jtnn_vae import JTNNVAE
|
|
9
|
+
from .jtnn.mol_tree import MolTree
|
|
10
|
+
from .jtnn.vocab import Vocab
|
|
11
|
+
from .jtnn.datautils import MolTreeFolder
|
|
12
|
+
from ...base import BaseMolecularGenerator
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class JTVAEMolecularGenerator(BaseMolecularGenerator):
|
|
16
|
+
"""
|
|
17
|
+
JT-VAE-based molecular generator. Implemented for unconditional moleculargeneration.
|
|
18
|
+
|
|
19
|
+
References
|
|
20
|
+
----------
|
|
21
|
+
- Junction Tree Variational Autoencoder for Molecular Graph Generation. ICML 2018. https://arxiv.org/pdf/1802.04364
|
|
22
|
+
- Code: https://github.com/kamikaze0923/jtvae
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
hidden_size : int, default=450
|
|
27
|
+
Dimension of hidden layers in the model.
|
|
28
|
+
latent_size : int, default=56
|
|
29
|
+
Dimension of the latent space.
|
|
30
|
+
depthT : int, default=20
|
|
31
|
+
Depth of the tree encoder.
|
|
32
|
+
depthG : int, default=3
|
|
33
|
+
Depth of the graph decoder.
|
|
34
|
+
batch_size : int, default=32
|
|
35
|
+
Number of samples per batch during training.
|
|
36
|
+
epochs : int, default=20
|
|
37
|
+
Number of epochs to train the model.
|
|
38
|
+
learning_rate : float, default=0.003
|
|
39
|
+
Initial learning rate for the optimizer.
|
|
40
|
+
weight_decay : float, default=0.0
|
|
41
|
+
L2 regularization factor.
|
|
42
|
+
grad_norm_clip : Optional[float], default=None
|
|
43
|
+
Maximum norm for gradient clipping. None means no clipping.
|
|
44
|
+
beta : float, default=0.0
|
|
45
|
+
Initial KL divergence weight for VAE training.
|
|
46
|
+
step_beta : float, default=0.002
|
|
47
|
+
Step size for KL annealing.
|
|
48
|
+
max_beta : float, default=1.0
|
|
49
|
+
Maximum value for KL weight.
|
|
50
|
+
warmup : int, default=40000
|
|
51
|
+
Number of steps for KL annealing warmup.
|
|
52
|
+
use_lr_scheduler : bool, default=True
|
|
53
|
+
Whether to use learning rate scheduling.
|
|
54
|
+
anneal_rate : float, default=0.9
|
|
55
|
+
Learning rate annealing factor.
|
|
56
|
+
anneal_iter : int, default=40000
|
|
57
|
+
Number of iterations between learning rate updates.
|
|
58
|
+
kl_anneal_iter : int, default=2000
|
|
59
|
+
Number of iterations between KL weight updates.
|
|
60
|
+
verbose : bool, default=False
|
|
61
|
+
Whether to print detailed training information.
|
|
62
|
+
"""
|
|
63
|
+
# Model parameters
|
|
64
|
+
hidden_size: int = 450
|
|
65
|
+
latent_size: int = 56
|
|
66
|
+
depthT: int = 20
|
|
67
|
+
depthG: int = 3
|
|
68
|
+
|
|
69
|
+
# Training parameters
|
|
70
|
+
batch_size: int = 32
|
|
71
|
+
epochs: int = 20
|
|
72
|
+
learning_rate: float = 0.003
|
|
73
|
+
weight_decay: float = 0.0
|
|
74
|
+
grad_norm_clip: Optional[float] = None
|
|
75
|
+
|
|
76
|
+
# KL annealing parameters
|
|
77
|
+
beta: float = 0.0
|
|
78
|
+
step_beta: float = 0.002
|
|
79
|
+
max_beta: float = 1.0
|
|
80
|
+
warmup: int = 40000
|
|
81
|
+
|
|
82
|
+
# Learning rate scheduling
|
|
83
|
+
use_lr_scheduler: bool = True
|
|
84
|
+
anneal_rate: float = 0.9
|
|
85
|
+
anneal_iter: int = 40000
|
|
86
|
+
kl_anneal_iter: int = 2000
|
|
87
|
+
|
|
88
|
+
# Other parameters
|
|
89
|
+
verbose: bool = False
|
|
90
|
+
|
|
91
|
+
# attributes
|
|
92
|
+
model_name: str = "JTVAEMolecularGenerator"
|
|
93
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
94
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
95
|
+
model_class: Type[JTNNVAE] = field(default=JTNNVAE, init=False)
|
|
96
|
+
|
|
97
|
+
def __post_init__(self):
|
|
98
|
+
super().__post_init__()
|
|
99
|
+
self.vocab = None
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _get_param_names() -> List[str]:
|
|
103
|
+
return [
|
|
104
|
+
"hidden_size", "latent_size", "depthT", "depthG",
|
|
105
|
+
"batch_size", "epochs", "learning_rate", "weight_decay",
|
|
106
|
+
"beta", "step_beta", "max_beta", "warmup",
|
|
107
|
+
"use_lr_scheduler", "anneal_rate", "anneal_iter", "kl_anneal_iter",
|
|
108
|
+
"verbose", 'model_name', 'vocab'
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
112
|
+
params = [
|
|
113
|
+
"vocab", "hidden_size", "latent_size", "depthT", "depthG"
|
|
114
|
+
]
|
|
115
|
+
if checkpoint is not None:
|
|
116
|
+
if "hyperparameters" not in checkpoint:
|
|
117
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
118
|
+
return {k: checkpoint["hyperparameters"][k] for k in params}
|
|
119
|
+
|
|
120
|
+
return {k: getattr(self, k) for k in params}
|
|
121
|
+
|
|
122
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
123
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
124
|
+
"""
|
|
125
|
+
optimizer = torch.optim.Adam(
|
|
126
|
+
self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
|
|
127
|
+
)
|
|
128
|
+
scheduler = None
|
|
129
|
+
if self.use_lr_scheduler:
|
|
130
|
+
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, self.anneal_rate)
|
|
131
|
+
|
|
132
|
+
return optimizer, scheduler
|
|
133
|
+
|
|
134
|
+
def _extract_vocab(self, X):
|
|
135
|
+
cset = set()
|
|
136
|
+
for ii, smiles in enumerate(X):
|
|
137
|
+
try:
|
|
138
|
+
mol = MolTree(smiles)
|
|
139
|
+
for c in mol.nodes:
|
|
140
|
+
cset.add(c.smiles)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f'Error {e} in extracting vocab for smiles: {smiles}')
|
|
143
|
+
pass
|
|
144
|
+
vocab = list(cset)
|
|
145
|
+
return vocab
|
|
146
|
+
|
|
147
|
+
def _convert_to_tensor(self, X):
|
|
148
|
+
all_data = []
|
|
149
|
+
for smiles in X:
|
|
150
|
+
try:
|
|
151
|
+
mol_tree = MolTree(smiles)
|
|
152
|
+
mol_tree.recover()
|
|
153
|
+
mol_tree.assemble()
|
|
154
|
+
for node in mol_tree.nodes:
|
|
155
|
+
if node.label not in node.cands:
|
|
156
|
+
node.cands.append(node.label)
|
|
157
|
+
|
|
158
|
+
del mol_tree.mol
|
|
159
|
+
for node in mol_tree.nodes:
|
|
160
|
+
del node.mol
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f'Error {e} in tensorizing smiles: {smiles}')
|
|
163
|
+
mol_tree = None
|
|
164
|
+
all_data.append(mol_tree)
|
|
165
|
+
return all_data
|
|
166
|
+
|
|
167
|
+
def fit(
|
|
168
|
+
self,
|
|
169
|
+
X_train: List[str],
|
|
170
|
+
) -> "JTVAEMolecularGenerator":
|
|
171
|
+
|
|
172
|
+
X_train, _ = self._validate_inputs(X_train, None, num_task=0, return_rdkit_mol=False)
|
|
173
|
+
vocab = self._extract_vocab(X_train)
|
|
174
|
+
vocab = Vocab(vocab)
|
|
175
|
+
self.vocab = vocab
|
|
176
|
+
|
|
177
|
+
self._initialize_model(self.model_class)
|
|
178
|
+
self.model.initialize_parameters()
|
|
179
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
180
|
+
|
|
181
|
+
train_dataset = self._convert_to_tensor(X_train)
|
|
182
|
+
train_dataset = list(filter(lambda x: x is not None, train_dataset))
|
|
183
|
+
train_loader = MolTreeFolder(train_dataset, vocab, self.batch_size, num_workers=0)
|
|
184
|
+
step_len = len(X_train) // self.batch_size
|
|
185
|
+
self.fitting_loss = []
|
|
186
|
+
self.fitting_epoch = 0
|
|
187
|
+
total_step = 0
|
|
188
|
+
for epoch in range(self.epochs):
|
|
189
|
+
train_losses, total_step = self._train_epoch(train_loader, optimizer, scheduler, epoch, total_step, step_len)
|
|
190
|
+
self.fitting_loss.append(np.mean(train_losses).item())
|
|
191
|
+
|
|
192
|
+
self.fitting_epoch = epoch
|
|
193
|
+
self.is_fitted_ = True
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
def _train_epoch(self, train_loader, optimizer, scheduler, epoch, total_step, step_len):
|
|
197
|
+
self.model.train()
|
|
198
|
+
losses = []
|
|
199
|
+
iterator = (
|
|
200
|
+
tqdm(train_loader, desc="Training", leave=False, total=step_len)
|
|
201
|
+
if self.verbose
|
|
202
|
+
else train_loader
|
|
203
|
+
)
|
|
204
|
+
for step, batched_data in enumerate(iterator):
|
|
205
|
+
total_step += 1
|
|
206
|
+
optimizer.zero_grad()
|
|
207
|
+
|
|
208
|
+
word_loss, topo_loss, assm_loss, kl_div = self.model.compute_loss(batched_data)
|
|
209
|
+
total_loss = word_loss + topo_loss + assm_loss + kl_div * self.beta
|
|
210
|
+
total_loss.backward()
|
|
211
|
+
if self.grad_norm_clip is not None:
|
|
212
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_norm_clip)
|
|
213
|
+
optimizer.step()
|
|
214
|
+
losses.append(total_loss.item())
|
|
215
|
+
|
|
216
|
+
if total_step % self.anneal_iter == 0:
|
|
217
|
+
scheduler.step()
|
|
218
|
+
|
|
219
|
+
if total_step % self.kl_anneal_iter == 0 and total_step >= self.warmup:
|
|
220
|
+
self.beta = min(self.max_beta, self.beta + self.step_beta)
|
|
221
|
+
|
|
222
|
+
if self.verbose:
|
|
223
|
+
iterator.set_postfix({"Epoch": epoch, "Loss": f"{total_loss.item():.4f}, word_loss: {word_loss.item():.4f}, topo_loss: {topo_loss.item():.4f}, assm_loss: {assm_loss.item():.4f}, kl_div: {kl_div.item():.4f}"})
|
|
224
|
+
|
|
225
|
+
return losses, total_step
|
|
226
|
+
|
|
227
|
+
def generate(
|
|
228
|
+
self,
|
|
229
|
+
batch_size: int = 32
|
|
230
|
+
) -> List[str]:
|
|
231
|
+
"""Generate molecules using JT-VAE.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
batch_size : int
|
|
236
|
+
Number of molecules to generate.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
List[str]
|
|
241
|
+
Generated molecules as SMILES strings.
|
|
242
|
+
"""
|
|
243
|
+
if not self.is_fitted_:
|
|
244
|
+
raise ValueError("Model must be fitted before generating molecules.")
|
|
245
|
+
|
|
246
|
+
return self.model.sample_prior(self.device)
|
|
247
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn.functional as F
|
|
5
|
+
from torch.distributions import Categorical, Distribution
|
|
6
|
+
|
|
7
|
+
from .lstm import LSTM
|
|
8
|
+
from .utils import rnn_start_token_vector
|
|
9
|
+
|
|
10
|
+
class ActionSampler:
|
|
11
|
+
"""
|
|
12
|
+
Sampler for a SmilesRNN models.
|
|
13
|
+
|
|
14
|
+
Does not return SMILES strings directly, but instead the actions (i.e. which SMILES character to select).
|
|
15
|
+
Those values are more general and are for instance necessary for other RL algorithms.
|
|
16
|
+
|
|
17
|
+
The class will sample the RNN model multiple times if the number of desired samples is larger than the
|
|
18
|
+
maximal allowed batch size.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, max_batch_size, max_seq_length, device,
|
|
22
|
+
distribution_cls: Type[Distribution] = Categorical) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Args:
|
|
25
|
+
max_batch_size: maximal batch size for the RNN model
|
|
26
|
+
max_seq_length: max length for a sampled SMILES string
|
|
27
|
+
device: cuda | cpu
|
|
28
|
+
distribution_cls: distribution type to sample from. If None, will be a multinomial distribution. Useful for testing purposes.
|
|
29
|
+
"""
|
|
30
|
+
self.max_batch_size = max_batch_size
|
|
31
|
+
self.max_seq_length = max_seq_length
|
|
32
|
+
self.device = device
|
|
33
|
+
self.distribution_cls = distribution_cls
|
|
34
|
+
|
|
35
|
+
def sample(self, model: LSTM, num_samples: int, target: torch.Tensor) -> torch.Tensor:
|
|
36
|
+
"""
|
|
37
|
+
Samples a specified number of actions from an RNN model based on a multinomial distribution.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
model: Smiles RNN model to sample from
|
|
41
|
+
num_samples: Number of samples to generate
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
tensor of actions (num_samples x max_seq_length)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Round up division to get the number of batches that are necessary:
|
|
48
|
+
number_batches = (num_samples + self.max_batch_size - 1) // self.max_batch_size
|
|
49
|
+
remaining_samples = num_samples
|
|
50
|
+
|
|
51
|
+
actions = torch.LongTensor(num_samples, self.max_seq_length).to(self.device)
|
|
52
|
+
|
|
53
|
+
batch_start = 0
|
|
54
|
+
|
|
55
|
+
for i in range(number_batches):
|
|
56
|
+
batch_size = min(self.max_batch_size, remaining_samples)
|
|
57
|
+
batch_end = batch_start + batch_size
|
|
58
|
+
|
|
59
|
+
actions[batch_start:batch_end, :] = self._sample_batch(model, batch_size, target)
|
|
60
|
+
|
|
61
|
+
batch_start += batch_size
|
|
62
|
+
remaining_samples -= batch_size
|
|
63
|
+
|
|
64
|
+
return actions
|
|
65
|
+
|
|
66
|
+
def _sample_batch(self, model: LSTM, batch_size: int, target: torch.Tensor) -> torch.Tensor:
|
|
67
|
+
"""
|
|
68
|
+
Samples a batch of actions based on a multinomial distribution.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
model: Smiles RNN model to sample from
|
|
72
|
+
num_samples: Number of samples to generate
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
tensor of actions (batch_size x max_seq_length)
|
|
76
|
+
"""
|
|
77
|
+
hidden, cell = model.init_hidden(batch_size, target)
|
|
78
|
+
inp = rnn_start_token_vector(batch_size, self.device)
|
|
79
|
+
actions = torch.zeros((batch_size, self.max_seq_length), dtype=torch.long).to(self.device)
|
|
80
|
+
|
|
81
|
+
for char in range(self.max_seq_length):
|
|
82
|
+
output, hidden, cell = model(inp, hidden, cell)
|
|
83
|
+
prob = F.softmax(output, dim=2)
|
|
84
|
+
distribution = self.distribution_cls(probs=prob)
|
|
85
|
+
action = distribution.sample()
|
|
86
|
+
|
|
87
|
+
actions[:, char] = action.squeeze()
|
|
88
|
+
|
|
89
|
+
inp = action
|
|
90
|
+
|
|
91
|
+
return actions
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
class LSTM(nn.Module):
|
|
5
|
+
def __init__(self, num_task, input_size, hidden_size, output_size, num_layer, dropout):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.input_size = input_size
|
|
8
|
+
self.hidden_size = hidden_size
|
|
9
|
+
self.output_size = output_size
|
|
10
|
+
self.num_layer = num_layer
|
|
11
|
+
self.dropout = dropout
|
|
12
|
+
if num_task == 0:
|
|
13
|
+
self.input_dim = 1
|
|
14
|
+
else:
|
|
15
|
+
self.input_dim = num_task
|
|
16
|
+
self.hidden_transform = nn.Linear(self.input_dim, num_layer * hidden_size)
|
|
17
|
+
self.cell_transform = nn.Linear(self.input_dim, num_layer * hidden_size)
|
|
18
|
+
self.encoder = nn.Embedding(input_size, hidden_size)
|
|
19
|
+
self.decoder = nn.Linear(hidden_size, output_size)
|
|
20
|
+
|
|
21
|
+
self.rnn = nn.LSTM(hidden_size, hidden_size, batch_first=True, num_layers=num_layer, dropout=dropout)
|
|
22
|
+
self.initialize_parameters()
|
|
23
|
+
|
|
24
|
+
def initialize_parameters(self):
|
|
25
|
+
# encoder / decoder
|
|
26
|
+
nn.init.xavier_uniform_(self.encoder.weight)
|
|
27
|
+
nn.init.xavier_uniform_(self.decoder.weight)
|
|
28
|
+
nn.init.constant_(self.decoder.bias, 0)
|
|
29
|
+
# RNN
|
|
30
|
+
for name, param in self.rnn.named_parameters():
|
|
31
|
+
if 'weight' in name:
|
|
32
|
+
nn.init.orthogonal_(param)
|
|
33
|
+
elif 'bias' in name:
|
|
34
|
+
nn.init.constant_(param, 0)
|
|
35
|
+
# LSTM remember gate bias should be initialised to 1
|
|
36
|
+
# https://github.com/pytorch/pytorch/issues/750
|
|
37
|
+
r_gate = param[int(0.25 * len(param)):int(0.5 * len(param))]
|
|
38
|
+
nn.init.constant_(r_gate, 1)
|
|
39
|
+
|
|
40
|
+
def forward(self, input, hidden, cell):
|
|
41
|
+
embeds = self.encoder(input)
|
|
42
|
+
output, (hidden, cell) = self.rnn(embeds, (hidden, cell))
|
|
43
|
+
output = self.decoder(output)
|
|
44
|
+
return output, hidden, cell
|
|
45
|
+
|
|
46
|
+
def init_hidden(self, bsz, target):
|
|
47
|
+
hidden = self.hidden_transform(target)
|
|
48
|
+
cell = self.cell_transform(target)
|
|
49
|
+
hidden = hidden.view(self.num_layer, bsz, self.hidden_size)
|
|
50
|
+
cell = cell.view(self.num_layer, bsz, self.hidden_size)
|
|
51
|
+
return hidden, cell
|
|
52
|
+
|
|
53
|
+
def compute_loss(self, batch_data, criterion):
|
|
54
|
+
ipt, tgt, y = batch_data
|
|
55
|
+
hidden, cell = self.init_hidden(ipt.size(0), y)
|
|
56
|
+
output, hidden, cell = self.forward(ipt, hidden, cell)
|
|
57
|
+
output = output.view(output.size(0) * output.size(1), -1)
|
|
58
|
+
loss = criterion(output, tgt.view(-1))
|
|
59
|
+
return loss
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from tqdm import tqdm
|
|
2
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Callable, Type
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch.utils.data import DataLoader, TensorDataset
|
|
8
|
+
|
|
9
|
+
from .lstm import LSTM
|
|
10
|
+
from .utils import canonicalize
|
|
11
|
+
from .action_sampler import ActionSampler
|
|
12
|
+
from .smiles_char_dict import SmilesCharDictionary
|
|
13
|
+
from ...base import BaseMolecularGenerator
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LSTMMolecularGenerator(BaseMolecularGenerator):
|
|
17
|
+
"""LSTM-based molecular generator.
|
|
18
|
+
|
|
19
|
+
This generator implements an LSTM architecture for molecular generation.
|
|
20
|
+
When conditions (y values) are provided during training, they are used to initialize the hidden
|
|
21
|
+
and cell states of the LSTM.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
num_task : int, default=0
|
|
26
|
+
Number of tasks for conditional generation (0 means unconditional generation).
|
|
27
|
+
max_len : int, default=100
|
|
28
|
+
Maximum length of the SMILES strings.
|
|
29
|
+
num_layer : int, default=3
|
|
30
|
+
Number of LSTM layers.
|
|
31
|
+
hidden_size : int, default=512
|
|
32
|
+
Dimension of hidden states in LSTM.
|
|
33
|
+
dropout : float, default=0.2
|
|
34
|
+
Dropout probability for regularization.
|
|
35
|
+
batch_size : int, default=128
|
|
36
|
+
Batch size for training.
|
|
37
|
+
epochs : int, default=10000
|
|
38
|
+
Maximum number of training epochs.
|
|
39
|
+
learning_rate : float, default=0.0002
|
|
40
|
+
Learning rate for optimizer.
|
|
41
|
+
weight_decay : float, default=0.0
|
|
42
|
+
L2 regularization factor.
|
|
43
|
+
use_lr_scheduler : bool, default=False
|
|
44
|
+
Whether to use learning rate scheduler.
|
|
45
|
+
scheduler_factor : float, default=0.5
|
|
46
|
+
Factor by which to reduce learning rate when using scheduler (if True).
|
|
47
|
+
scheduler_patience : int, default=5
|
|
48
|
+
Number of epochs with no improvement after which learning rate will be reduced (if True).
|
|
49
|
+
grad_norm_clip : Optional[float], default=None
|
|
50
|
+
Maximum norm for gradient clipping. None means no clipping.
|
|
51
|
+
verbose : bool, default=False
|
|
52
|
+
Whether to print progress during training.
|
|
53
|
+
"""
|
|
54
|
+
num_task: int = 0
|
|
55
|
+
# LSTM parameters
|
|
56
|
+
max_len: int = 100
|
|
57
|
+
num_layer: int = 3
|
|
58
|
+
hidden_size: int = 512
|
|
59
|
+
dropout: float = 0.2
|
|
60
|
+
|
|
61
|
+
# Training parameters
|
|
62
|
+
batch_size: int = 128
|
|
63
|
+
epochs: int = 10000
|
|
64
|
+
learning_rate: float = 0.0002
|
|
65
|
+
weight_decay: float = 0.0
|
|
66
|
+
# Scheduler parameters
|
|
67
|
+
use_lr_scheduler: bool = False
|
|
68
|
+
scheduler_factor: float = 0.5
|
|
69
|
+
scheduler_patience: int = 5
|
|
70
|
+
grad_norm_clip: Optional[float] = None
|
|
71
|
+
# Other parameters
|
|
72
|
+
verbose: bool = False
|
|
73
|
+
|
|
74
|
+
# attributes
|
|
75
|
+
model_name: str = "LSTMMolecularGenerator"
|
|
76
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
77
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
78
|
+
input_size: int = field(init=False, default=None)
|
|
79
|
+
output_size: int = field(init=False, default=None)
|
|
80
|
+
model_class: Type[LSTM] = field(default=LSTM, init=False)
|
|
81
|
+
|
|
82
|
+
def __post_init__(self):
|
|
83
|
+
super().__post_init__()
|
|
84
|
+
self.tokenizer = SmilesCharDictionary()
|
|
85
|
+
self.input_size = len(self.tokenizer.char_idx)
|
|
86
|
+
self.output_size = len(self.tokenizer.char_idx)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _get_param_names() -> List[str]:
|
|
90
|
+
return [
|
|
91
|
+
"num_task", "max_len", "num_layer", "hidden_size", "dropout",
|
|
92
|
+
"batch_size", "epochs", "learning_rate", "weight_decay",
|
|
93
|
+
"use_lr_scheduler", "scheduler_factor", "scheduler_patience",
|
|
94
|
+
"grad_norm_clip", "verbose", "input_size", "output_size", 'model_name'
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
98
|
+
params = [
|
|
99
|
+
"num_task", "input_size", "hidden_size", "output_size", "num_layer", "dropout",
|
|
100
|
+
]
|
|
101
|
+
if checkpoint is not None:
|
|
102
|
+
if "hyperparameters" not in checkpoint:
|
|
103
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
104
|
+
return {k: checkpoint["hyperparameters"][k] for k in params}
|
|
105
|
+
|
|
106
|
+
return {k: getattr(self, k) for k in params}
|
|
107
|
+
|
|
108
|
+
def _convert_to_pytorch_data(self, X, y=None):
|
|
109
|
+
# filter valid smiles strings
|
|
110
|
+
valid_smiles = []
|
|
111
|
+
for s in X:
|
|
112
|
+
s = s.strip()
|
|
113
|
+
if self.tokenizer.allowed(s) and len(s) <= self.max_len:
|
|
114
|
+
valid_smiles.append(s)
|
|
115
|
+
else:
|
|
116
|
+
valid_smiles.append('C') # default placeholder
|
|
117
|
+
|
|
118
|
+
# max len + two chars for start token 'Q' and stop token '\n'
|
|
119
|
+
max_seq_len = self.max_len + 2
|
|
120
|
+
# allocate the zero matrix to be filled
|
|
121
|
+
seqs = np.zeros((len(valid_smiles), max_seq_len), dtype=np.int32)
|
|
122
|
+
|
|
123
|
+
for i, mol in enumerate(valid_smiles):
|
|
124
|
+
enc_smi = self.tokenizer.BEGIN + self.tokenizer.encode(mol) + self.tokenizer.END
|
|
125
|
+
for c in range(len(enc_smi)):
|
|
126
|
+
seqs[i, c] = self.tokenizer.char_idx[enc_smi[c]]
|
|
127
|
+
|
|
128
|
+
seqs_tensor = torch.from_numpy(seqs).long()
|
|
129
|
+
inp = seqs_tensor[:, :-1]
|
|
130
|
+
target = seqs_tensor[:, 1:]
|
|
131
|
+
bsz = inp.size(0)
|
|
132
|
+
if y is not None:
|
|
133
|
+
assert len(y) == bsz
|
|
134
|
+
y = torch.tensor(y)
|
|
135
|
+
return TensorDataset(inp, target, y)
|
|
136
|
+
else:
|
|
137
|
+
target_y = torch.zeros(bsz, 1)
|
|
138
|
+
return TensorDataset(inp, target, target_y)
|
|
139
|
+
|
|
140
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
141
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
142
|
+
"""
|
|
143
|
+
optimizer = torch.optim.Adam(
|
|
144
|
+
self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
|
|
145
|
+
)
|
|
146
|
+
scheduler = None
|
|
147
|
+
if self.use_lr_scheduler:
|
|
148
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
149
|
+
optimizer,
|
|
150
|
+
mode="min",
|
|
151
|
+
factor=self.scheduler_factor,
|
|
152
|
+
patience=self.scheduler_patience,
|
|
153
|
+
min_lr=1e-6,
|
|
154
|
+
cooldown=0,
|
|
155
|
+
eps=1e-8,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return optimizer, scheduler
|
|
159
|
+
|
|
160
|
+
def fit(
|
|
161
|
+
self,
|
|
162
|
+
X_train: List[str],
|
|
163
|
+
y_train: Optional[Union[List, np.ndarray]] = None,
|
|
164
|
+
) -> "LSTMMolecularGenerator":
|
|
165
|
+
X_train, y_train = self._validate_inputs(X_train, y_train, num_task=self.num_task, return_rdkit_mol=False)
|
|
166
|
+
self._initialize_model(self.model_class)
|
|
167
|
+
self.model.initialize_parameters()
|
|
168
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
169
|
+
|
|
170
|
+
train_dataset = self._convert_to_pytorch_data(X_train, y_train)
|
|
171
|
+
train_loader = DataLoader(
|
|
172
|
+
train_dataset,
|
|
173
|
+
batch_size=self.batch_size,
|
|
174
|
+
shuffle=True,
|
|
175
|
+
num_workers=0
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
self.fitting_loss = []
|
|
179
|
+
self.fitting_epoch = 0
|
|
180
|
+
criterion = torch.nn.CrossEntropyLoss()
|
|
181
|
+
for epoch in range(self.epochs):
|
|
182
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch, criterion)
|
|
183
|
+
self.fitting_loss.append(np.mean(train_losses).item())
|
|
184
|
+
if scheduler:
|
|
185
|
+
scheduler.step(np.mean(train_losses).item())
|
|
186
|
+
|
|
187
|
+
self.fitting_epoch = epoch
|
|
188
|
+
self.is_fitted_ = True
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def _train_epoch(self, train_loader, optimizer, epoch, criterion):
|
|
192
|
+
self.model.train()
|
|
193
|
+
losses = []
|
|
194
|
+
iterator = (
|
|
195
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
196
|
+
if self.verbose
|
|
197
|
+
else train_loader
|
|
198
|
+
)
|
|
199
|
+
for step, batched_data in enumerate(iterator):
|
|
200
|
+
for i in range(len(batched_data)):
|
|
201
|
+
batched_data[i] = batched_data[i].to(self.device)
|
|
202
|
+
optimizer.zero_grad()
|
|
203
|
+
|
|
204
|
+
loss = self.model.compute_loss(batched_data, criterion)
|
|
205
|
+
loss.backward()
|
|
206
|
+
if self.grad_norm_clip is not None:
|
|
207
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_norm_clip)
|
|
208
|
+
optimizer.step()
|
|
209
|
+
losses.append(loss.item())
|
|
210
|
+
|
|
211
|
+
if self.verbose:
|
|
212
|
+
iterator.set_postfix({"Epoch": epoch, "Loss": f"{loss.item():.4f}"})
|
|
213
|
+
|
|
214
|
+
return losses
|
|
215
|
+
|
|
216
|
+
def generate(
|
|
217
|
+
self,
|
|
218
|
+
labels: Optional[Union[List[List], np.ndarray]] = None,
|
|
219
|
+
batch_size: int = 32
|
|
220
|
+
) -> List[str]:
|
|
221
|
+
"""Generate molecules using LSTM.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
labels : Optional[Union[List[List], np.ndarray]]
|
|
226
|
+
Target property values for conditional generation.
|
|
227
|
+
batch_size : int
|
|
228
|
+
Number of molecules to generate.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
List[str]
|
|
233
|
+
Generated molecules as SMILES strings.
|
|
234
|
+
"""
|
|
235
|
+
if not self.is_fitted_:
|
|
236
|
+
raise ValueError("Model must be fitted before generating molecules.")
|
|
237
|
+
|
|
238
|
+
if labels is not None and batch_size != len(labels):
|
|
239
|
+
print(f"batch_size is not equal to the length of labels, using the length of labels: {len(labels)}")
|
|
240
|
+
batch_size = len(labels)
|
|
241
|
+
|
|
242
|
+
# Convert properties to 2D tensor if needed
|
|
243
|
+
if isinstance(labels, list):
|
|
244
|
+
labels = torch.tensor(labels)
|
|
245
|
+
elif isinstance(labels, np.ndarray):
|
|
246
|
+
labels = torch.from_numpy(labels)
|
|
247
|
+
if labels is not None and labels.dim() == 1:
|
|
248
|
+
labels = labels.unsqueeze(-1)
|
|
249
|
+
elif labels is None:
|
|
250
|
+
labels = torch.zeros(batch_size, 1)
|
|
251
|
+
labels = labels.to(self.device)
|
|
252
|
+
|
|
253
|
+
all_generated_mols = []
|
|
254
|
+
sampler = ActionSampler(max_batch_size=batch_size, max_seq_length=self.max_len, device=self.device)
|
|
255
|
+
self.model.eval()
|
|
256
|
+
with torch.no_grad():
|
|
257
|
+
indices = sampler.sample(self.model, num_samples=batch_size, target=labels)
|
|
258
|
+
samples = self.tokenizer.matrix_to_smiles(indices)
|
|
259
|
+
canonicalized_smiles = [canonicalize(smiles, include_stereocenters=True) for smiles in samples]
|
|
260
|
+
all_generated_mols.extend(canonicalized_smiles)
|
|
261
|
+
|
|
262
|
+
return all_generated_mols
|
|
263
|
+
|