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.
Files changed (148) hide show
  1. torch_molecule/__init__.py +70 -0
  2. torch_molecule/base/__init__.py +8 -0
  3. torch_molecule/base/base.py +396 -0
  4. torch_molecule/base/encoder.py +44 -0
  5. torch_molecule/base/generator.py +24 -0
  6. torch_molecule/base/old_encoder.py +322 -0
  7. torch_molecule/base/old_predictor.py +904 -0
  8. torch_molecule/base/old_predictor2.py +436 -0
  9. torch_molecule/base/predictor.py +112 -0
  10. torch_molecule/encoder/attrmask/__init__.py +3 -0
  11. torch_molecule/encoder/attrmask/model.py +113 -0
  12. torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
  13. torch_molecule/encoder/constant.py +13 -0
  14. torch_molecule/encoder/contextpred/__init__.py +3 -0
  15. torch_molecule/encoder/contextpred/model.py +207 -0
  16. torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
  17. torch_molecule/encoder/contextpred/utils.py +206 -0
  18. torch_molecule/encoder/edgepred/__init__.py +3 -0
  19. torch_molecule/encoder/edgepred/model.py +104 -0
  20. torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
  21. torch_molecule/encoder/graphmae/__init__.py +3 -0
  22. torch_molecule/encoder/graphmae/dataloader.py +206 -0
  23. torch_molecule/encoder/graphmae/model.py +137 -0
  24. torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
  25. torch_molecule/encoder/infograph/__init__.py +3 -0
  26. torch_molecule/encoder/infograph/loss_function.py +128 -0
  27. torch_molecule/encoder/infograph/model.py +92 -0
  28. torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
  29. torch_molecule/encoder/infograph/module.py +31 -0
  30. torch_molecule/encoder/moama/__init__.py +3 -0
  31. torch_molecule/encoder/moama/model.py +131 -0
  32. torch_molecule/encoder/moama/modeling_moama.py +307 -0
  33. torch_molecule/encoder/moama/utils.py +143 -0
  34. torch_molecule/encoder/pretrained/__init__.py +3 -0
  35. torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
  36. torch_molecule/encoder/supervised/__init__.py +3 -0
  37. torch_molecule/encoder/supervised/model.py +97 -0
  38. torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
  39. torch_molecule/generator/digress/__init__.py +3 -0
  40. torch_molecule/generator/digress/diffusion.py +194 -0
  41. torch_molecule/generator/digress/modeling_digress.py +503 -0
  42. torch_molecule/generator/digress/transformer.py +360 -0
  43. torch_molecule/generator/digress/utils.py +196 -0
  44. torch_molecule/generator/gdss/__init__.py +3 -0
  45. torch_molecule/generator/gdss/layers.py +257 -0
  46. torch_molecule/generator/gdss/model.py +204 -0
  47. torch_molecule/generator/gdss/modeling_gdss.py +474 -0
  48. torch_molecule/generator/gdss/sde.py +334 -0
  49. torch_molecule/generator/gdss/solver.py +227 -0
  50. torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
  51. torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
  52. torch_molecule/generator/gdss/utils.py +240 -0
  53. torch_molecule/generator/graph_dit/__init__.py +3 -0
  54. torch_molecule/generator/graph_dit/diffusion.py +188 -0
  55. torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
  56. torch_molecule/generator/graph_dit/transformer.py +207 -0
  57. torch_molecule/generator/graph_dit/utils.py +220 -0
  58. torch_molecule/generator/graph_ga/__init__.py +3 -0
  59. torch_molecule/generator/graph_ga/crossover.py +182 -0
  60. torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
  61. torch_molecule/generator/graph_ga/mutate.py +136 -0
  62. torch_molecule/generator/graph_ga/oracle.py +95 -0
  63. torch_molecule/generator/jtvae/__init__.py +3 -0
  64. torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
  65. torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
  66. torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
  67. torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
  68. torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
  69. torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
  70. torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
  71. torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
  72. torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
  73. torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
  74. torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
  75. torch_molecule/generator/lstm/__init__.py +3 -0
  76. torch_molecule/generator/lstm/action_sampler.py +91 -0
  77. torch_molecule/generator/lstm/lstm.py +59 -0
  78. torch_molecule/generator/lstm/modeling_lstm.py +263 -0
  79. torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
  80. torch_molecule/generator/lstm/utils.py +62 -0
  81. torch_molecule/generator/molgpt/__init__.py +3 -0
  82. torch_molecule/generator/molgpt/dataset.py +87 -0
  83. torch_molecule/generator/molgpt/gpt.py +251 -0
  84. torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
  85. torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
  86. torch_molecule/nn/__init__.py +16 -0
  87. torch_molecule/nn/attention.py +63 -0
  88. torch_molecule/nn/embedder.py +183 -0
  89. torch_molecule/nn/gnn.py +631 -0
  90. torch_molecule/nn/mlp.py +34 -0
  91. torch_molecule/predictor/bfgnn/__init__.py +3 -0
  92. torch_molecule/predictor/bfgnn/model.py +116 -0
  93. torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
  94. torch_molecule/predictor/dir/__init__.py +3 -0
  95. torch_molecule/predictor/dir/model.py +235 -0
  96. torch_molecule/predictor/dir/modeling_dir.py +161 -0
  97. torch_molecule/predictor/dir/utils.py +40 -0
  98. torch_molecule/predictor/gnn/__init__.py +3 -0
  99. torch_molecule/predictor/gnn/model.py +114 -0
  100. torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
  101. torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
  102. torch_molecule/predictor/grea/__init__.py +3 -0
  103. torch_molecule/predictor/grea/model.py +190 -0
  104. torch_molecule/predictor/grea/modeling_grea.py +107 -0
  105. torch_molecule/predictor/grea/old1_features.py +229 -0
  106. torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
  107. torch_molecule/predictor/grea/utils.py +97 -0
  108. torch_molecule/predictor/grin/__init__.py +3 -0
  109. torch_molecule/predictor/grin/model.py +116 -0
  110. torch_molecule/predictor/grin/modeling_grin.py +91 -0
  111. torch_molecule/predictor/irm/__init__.py +3 -0
  112. torch_molecule/predictor/irm/model.py +127 -0
  113. torch_molecule/predictor/irm/modeling_irm.py +179 -0
  114. torch_molecule/predictor/lstm/__init__.py +3 -0
  115. torch_molecule/predictor/lstm/model.py +145 -0
  116. torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
  117. torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
  118. torch_molecule/predictor/old_modeling_graph.py +650 -0
  119. torch_molecule/predictor/rpgnn/__init__.py +3 -0
  120. torch_molecule/predictor/rpgnn/model.py +185 -0
  121. torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
  122. torch_molecule/predictor/sgir/__init__.py +3 -0
  123. torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
  124. torch_molecule/predictor/sgir/strategy.py +266 -0
  125. torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
  126. torch_molecule/predictor/smiles_transformer/model.py +424 -0
  127. torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
  128. torch_molecule/predictor/ssr/__init__.py +3 -0
  129. torch_molecule/predictor/ssr/model.py +283 -0
  130. torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
  131. torch_molecule/predictor/ssr/utils.py +185 -0
  132. torch_molecule/utils/__init__.py +31 -0
  133. torch_molecule/utils/checker.py +158 -0
  134. torch_molecule/utils/checkpoint.py +294 -0
  135. torch_molecule/utils/format.py +140 -0
  136. torch_molecule/utils/generic/metrics.py +530 -0
  137. torch_molecule/utils/generic/pseudo_tasks.py +11 -0
  138. torch_molecule/utils/generic/weights.py +30 -0
  139. torch_molecule/utils/graph/features.py +178 -0
  140. torch_molecule/utils/graph/graph_from_smiles.py +134 -0
  141. torch_molecule/utils/graph/graph_to_smiles.py +312 -0
  142. torch_molecule/utils/hf.py +232 -0
  143. torch_molecule/utils/search.py +64 -0
  144. torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
  145. torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
  146. torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
  147. torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
  148. 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,3 @@
1
+ from .modeling_lstm import LSTMMolecularGenerator
2
+
3
+ __all__ = ['LSTMMolecularGenerator']
@@ -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
+