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,627 @@
1
+ import os
2
+ import numpy as np
3
+ from tqdm import tqdm
4
+ from typing import Optional, Union, Dict, Any, List, Callable, Type, Tuple
5
+ import warnings
6
+ from dataclasses import dataclass, field
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.optim as optim
11
+ from torch.utils.data import DataLoader, TensorDataset
12
+
13
+ from .model import LSTM
14
+ from .token_from_smiles import create_tensor_dataset
15
+ from ...base import BaseMolecularPredictor
16
+ from ...utils.search import (
17
+ suggest_parameter,
18
+ ParameterSpec,
19
+ ParameterType,
20
+ )
21
+
22
+ # Dictionary mapping parameter names to their types and ranges
23
+ DEFAULT_LSTM_SEARCH_SPACES: Dict[str, ParameterSpec] = {
24
+ # Integer-valued parameters
25
+ "output_dim": ParameterSpec(ParameterType.INTEGER, (8, 32)),
26
+ "LSTMunits": ParameterSpec(ParameterType.INTEGER, (30, 120)),
27
+ # Float-valued parameters with log scale
28
+ "learning_rate": ParameterSpec(ParameterType.LOG_FLOAT, (1e-4, 1e-2)),
29
+ "weight_decay": ParameterSpec(ParameterType.LOG_FLOAT, (1e-8, 1e-3)),
30
+ "scheduler_factor": ParameterSpec(ParameterType.FLOAT, (0.1, 0.5)),
31
+ }
32
+
33
+ @dataclass
34
+ class LSTMMolecularPredictor(BaseMolecularPredictor):
35
+ """This predictor implements a LSTM model for molecular property prediction tasks.
36
+
37
+ References
38
+ ----------
39
+ - Predicting Polymers' Glass Transition Temperature by a Chemical Language Processing Model.
40
+ https://www.semanticscholar.org/reader/f43ed533b2520567be2d8c24f6396f4e63e96430
41
+
42
+ Parameters
43
+ ----------
44
+ num_task : int, default=1
45
+ Number of prediction tasks.
46
+ task_type : str, default="regression"
47
+ Type of prediction task, either "regression" or "classification".
48
+ input_dim : int, default=54
49
+ Size of vocabulary for SMILES tokenization.
50
+ output_dim : int, default=15
51
+ Dimension of embedding vectors.
52
+ LSTMunits : int, default=60
53
+ Number of hidden units in LSTM layers.
54
+ max_input_len : int, default=200
55
+ Maximum length of input sequences. Longer sequences will be truncated.
56
+ batch_size : int, default=128
57
+ Number of samples per batch for training.
58
+ epochs : int, default=500
59
+ Maximum number of training epochs.
60
+ loss_criterion : callable, optional
61
+ Loss function for training. Defaults to MSELoss for regression.
62
+ evaluate_criterion : str or callable, optional
63
+ Metric for model evaluation.
64
+ evaluate_higher_better : bool, optional
65
+ Whether higher values of the evaluation metric are better.
66
+ learning_rate : float, default=0.001
67
+ Learning rate for optimizer.
68
+ weight_decay : float, default=0.0
69
+ L2 regularization strength.
70
+ patience : int, default=50
71
+ Number of epochs to wait for improvement before early stopping.
72
+ use_lr_scheduler : bool, default=False
73
+ Whether to use learning rate scheduler.
74
+ scheduler_factor : float, default=0.5
75
+ Factor by which to reduce learning rate when plateau is reached.
76
+ scheduler_patience : int, default=5
77
+ Number of epochs with no improvement after which learning rate will be reduced.
78
+ verbose : bool, default=False
79
+ Whether to print progress information during training.
80
+
81
+ """
82
+ # Model parameters
83
+ num_task: int = 1
84
+ task_type: str = "regression"
85
+ input_dim: int = 54 # vocabulary size
86
+ output_dim: int = 15
87
+ LSTMunits: int = 60
88
+ max_input_len: int = 200 # max token length
89
+
90
+ # Training parameters
91
+ batch_size: int = 128
92
+ epochs: int = 500
93
+ loss_criterion: Optional[Callable] = None
94
+ evaluate_criterion: Optional[Union[str, Callable]] = None
95
+ evaluate_higher_better: Optional[bool] = None
96
+ learning_rate: float = 0.001
97
+ weight_decay: float = 0.0
98
+ patience: int = 50
99
+
100
+ # Scheduler parameters
101
+ use_lr_scheduler: bool = False
102
+ scheduler_factor: float = 0.5
103
+ scheduler_patience: int = 5
104
+
105
+ # Other parameters
106
+ verbose: bool = False
107
+ model_name: str = "LSTMMolecularPredictor"
108
+
109
+ # Non-init fields
110
+ fitting_loss: List[float] = field(default_factory=list, init=False)
111
+ fitting_epoch: int = field(default=0, init=False)
112
+ model_class: Type[LSTM] = field(default=LSTM, init=False)
113
+
114
+ def __post_init__(self):
115
+ super().__post_init__()
116
+ # Setup loss criterion and evaluation
117
+ if self.loss_criterion is None:
118
+ self.loss_criterion = nn.MSELoss()
119
+ self._setup_evaluation(self.evaluate_criterion, self.evaluate_higher_better)
120
+
121
+ @staticmethod
122
+ def _get_param_names() -> List[str]:
123
+ """Get parameter names for the estimator.
124
+
125
+ Returns
126
+ -------
127
+ List[str]
128
+ List of parameter names that can be used for model configuration.
129
+ """
130
+ return [
131
+ # Model Hyperparameters
132
+ "num_task",
133
+ "task_type",
134
+ "input_dim",
135
+ "output_dim",
136
+ "LSTMunits",
137
+ "max_input_len",
138
+ # Training Parameters
139
+ "batch_size",
140
+ "epochs",
141
+ "learning_rate",
142
+ "weight_decay",
143
+ "patience",
144
+ "loss_criterion",
145
+ # Evaluation Parameters
146
+ "evaluate_name",
147
+ "evaluate_criterion",
148
+ "evaluate_higher_better",
149
+ # Scheduler Parameters
150
+ "use_lr_scheduler",
151
+ "scheduler_factor",
152
+ "scheduler_patience",
153
+ # Other Parameters
154
+ "fitting_epoch",
155
+ "fitting_loss",
156
+ "device",
157
+ "verbose"
158
+ ]
159
+
160
+ def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
161
+ if checkpoint is not None:
162
+ if "hyperparameters" not in checkpoint:
163
+ raise ValueError("Checkpoint missing 'hyperparameters' key")
164
+
165
+ hyperparameters = checkpoint["hyperparameters"]
166
+
167
+ return {
168
+ "num_task": hyperparameters.get("num_task", self.num_task),
169
+ "input_dim": hyperparameters.get("input_dim", self.input_dim),
170
+ "output_dim": hyperparameters.get("output_dim", self.output_dim),
171
+ "LSTMunits": hyperparameters.get("LSTMunits", self.LSTMunits),
172
+ "max_input_len": hyperparameters.get("max_input_len", self.max_input_len),
173
+ }
174
+ else:
175
+ return {
176
+ "num_task": self.num_task,
177
+ "input_dim": self.input_dim,
178
+ "output_dim": self.output_dim,
179
+ "LSTMunits": self.LSTMunits,
180
+ "max_input_len": self.max_input_len,
181
+ }
182
+
183
+ def _convert_to_pytorch_data(self, X, y=None):
184
+ """Convert numpy arrays to PyTorch data format.
185
+ """
186
+ if self.verbose:
187
+ iterator = tqdm(enumerate(X), desc="Converting lists of data to tensordataset", total=len(X))
188
+ else:
189
+ iterator = enumerate(X)
190
+ tokenized_X = create_tensor_dataset(X, self.max_input_len)
191
+ if y is not None and y.size > 0:
192
+ if len(y) != len(X):
193
+ raise ValueError(f"The number of smiles {len(X)} is incompatible with the number of labels {len(y)}!")
194
+ return TensorDataset(torch.tensor(tokenized_X, dtype=torch.long),
195
+ torch.tensor(y, dtype=torch.float32))
196
+ return TensorDataset(torch.tensor(tokenized_X, dtype=torch.long),
197
+ torch.zeros(len(tokenized_X), dtype=torch.float32))
198
+
199
+
200
+ def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
201
+ """Setup optimization components including optimizer and learning rate scheduler.
202
+
203
+ Returns
204
+ -------
205
+ Tuple[optim.Optimizer, Optional[Any]]
206
+ A tuple containing:
207
+ - The configured optimizer
208
+ - The learning rate scheduler (if enabled, else None)
209
+ """
210
+ optimizer = torch.optim.Adam(
211
+ self.model.parameters(),
212
+ lr=self.learning_rate,
213
+ weight_decay=self.weight_decay
214
+ )
215
+
216
+ scheduler = None
217
+ if self.use_lr_scheduler:
218
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
219
+ optimizer,
220
+ mode='min' if not self.evaluate_higher_better else 'max',
221
+ factor=self.scheduler_factor,
222
+ patience=self.scheduler_patience,
223
+ verbose=self.verbose
224
+ )
225
+
226
+ return optimizer, scheduler
227
+
228
+ def _get_default_search_space(self):
229
+ """Get the default hyperparameter search space.
230
+ """
231
+ return DEFAULT_LSTM_SEARCH_SPACES
232
+
233
+ def autofit(
234
+ self,
235
+ X_train: List[str],
236
+ y_train: Optional[Union[List, np.ndarray]],
237
+ X_val: Optional[List[str]] = None,
238
+ y_val: Optional[Union[List, np.ndarray]] = None,
239
+ search_parameters: Optional[Dict[str, ParameterSpec]] = None,
240
+ n_trials: int = 10,
241
+ ) -> "LSTMMolecularPredictor":
242
+ import optuna
243
+ # Default search parameters
244
+ default_search_parameters = self._get_default_search_space()
245
+ if search_parameters is None:
246
+ search_parameters = default_search_parameters
247
+ else:
248
+ # Validate search parameter keys
249
+ invalid_params = set(search_parameters.keys()) - set(default_search_parameters.keys())
250
+ if invalid_params:
251
+ raise ValueError(
252
+ f"Invalid search parameters: {invalid_params}. "
253
+ f"Valid parameters are: {list(default_search_parameters.keys())}"
254
+ )
255
+
256
+ if self.verbose:
257
+ all_params = set(self._get_param_names())
258
+ searched_params = set(search_parameters.keys())
259
+ non_searched_params = all_params - searched_params
260
+
261
+ print("\nParameter Search Configuration:")
262
+ print("-" * 50)
263
+
264
+ print("\n Parameters being searched:")
265
+ for param in sorted(searched_params):
266
+ spec = search_parameters[param]
267
+ if spec.param_type == ParameterType.CATEGORICAL:
268
+ print(f" • {param}: {spec.value_range}")
269
+ else:
270
+ print(f" • {param}: [{spec.value_range[0]}, {spec.value_range[1]}]")
271
+
272
+ print("\n Fixed parameters (not being searched):")
273
+ for param in sorted(non_searched_params):
274
+ value = getattr(self, param, "N/A")
275
+ print(f" • {param}: {value}")
276
+
277
+ print("\n" + "-" * 50)
278
+
279
+ print(f"\nStarting hyperparameter optimization using {self.evaluate_name} metric")
280
+ print(f"Direction: {'maximize' if self.evaluate_higher_better else 'minimize'}")
281
+ print(f"Number of trials: {n_trials}")
282
+
283
+ # Variables to track best state
284
+ best_score = float('-inf') if self.evaluate_higher_better else float('inf')
285
+ best_state_dict = None
286
+ best_trial_params = None
287
+ best_loss = None
288
+ best_epoch = None
289
+
290
+ def objective(trial):
291
+ nonlocal best_score, best_state_dict, best_trial_params, best_loss, best_epoch
292
+
293
+ # Define hyperparameters to optimize using the parameter specifications
294
+ params = {}
295
+ for param_name, param_spec in search_parameters.items():
296
+ try:
297
+ params[param_name] = suggest_parameter(trial, param_name, param_spec)
298
+ except Exception as e:
299
+ print(f"Error suggesting parameter {param_name}: {str(e)}")
300
+ return float('inf')
301
+
302
+
303
+ self.set_params(**params)
304
+ self.fit(X_train, y_train, X_val, y_val)
305
+
306
+ # Get evaluation score
307
+ eval_data = (X_val if X_val is not None else X_train)
308
+ eval_labels = (y_val if y_val is not None else y_train)
309
+ eval_results = self.predict(eval_data)
310
+ score = float(self.evaluate_criterion(eval_labels, eval_results['prediction']))
311
+
312
+ # Update best state if current score is better
313
+ is_better = (
314
+ score > best_score if self.evaluate_higher_better
315
+ else score < best_score
316
+ )
317
+
318
+ if is_better:
319
+ best_score = score
320
+ best_state_dict = {
321
+ 'model': self.model.state_dict(),
322
+ 'architecture': self._get_model_params()
323
+ }
324
+ best_trial_params = params.copy()
325
+ best_loss = self.fitting_loss.copy()
326
+ best_epoch = self.fitting_epoch
327
+
328
+ if self.verbose:
329
+ print(
330
+ f"Trial {trial.number}: {self.evaluate_name} = {score:.4f} "
331
+ f"({'better' if is_better else 'worse'} than best = {best_score:.4f})"
332
+ )
333
+ print("Current parameters:")
334
+ for param_name, value in params.items():
335
+ print(f" {param_name}: {value}")
336
+
337
+ # Return score (negated if higher is better, since Optuna minimizes)
338
+ return -score if self.evaluate_higher_better else score
339
+
340
+ # Create study with optional output control
341
+ optuna.logging.set_verbosity(
342
+ optuna.logging.INFO if self.verbose else optuna.logging.WARNING
343
+ )
344
+
345
+ # Create study with optional output control
346
+ study = optuna.create_study(
347
+ direction="minimize",
348
+ study_name=f"{self.model_name}_optimization"
349
+ )
350
+
351
+ study.optimize(
352
+ objective,
353
+ n_trials=n_trials,
354
+ catch=(Exception,),
355
+ show_progress_bar=self.verbose
356
+ )
357
+
358
+ if best_state_dict is not None:
359
+ self.set_params(**best_trial_params)
360
+ self._initialize_model(self.model_class)
361
+ self.model.load_state_dict(best_state_dict['model'])
362
+ self.fitting_loss = best_loss
363
+ self.fitting_epoch = best_epoch
364
+ self.is_fitted_ = True
365
+
366
+ if self.verbose:
367
+ print(f"\nOptimization completed successfully:")
368
+ print(f"Best {self.evaluate_name}: {best_score:.4f}")
369
+
370
+ eval_data = (X_val if X_val is not None else X_train)
371
+ eval_labels = (y_val if y_val is not None else y_train)
372
+ eval_results = self.predict(eval_data)
373
+ score = float(self.evaluate_criterion(eval_labels, eval_results['prediction']))
374
+ print('post score is: ', score)
375
+
376
+ print("\nBest parameters:")
377
+ for param, value in best_trial_params.items():
378
+ param_spec = search_parameters[param]
379
+ print(f" {param}: {value} (type: {param_spec.param_type.value})")
380
+
381
+ print("\nOptimization statistics:")
382
+ print(f" Number of completed trials: {len(study.trials)}")
383
+ print(f" Number of pruned trials: {len(study.get_trials(states=[optuna.trial.TrialState.PRUNED]))}")
384
+ print(f" Number of failed trials: {len(study.get_trials(states=[optuna.trial.TrialState.FAIL]))}")
385
+
386
+ return self
387
+
388
+ def fit(
389
+ self,
390
+ X_train: List[str],
391
+ y_train: Optional[Union[List, np.ndarray]],
392
+ X_val: Optional[List[str]] = None,
393
+ y_val: Optional[Union[List, np.ndarray]] = None,
394
+ ) -> "LSTMMolecularPredictor":
395
+ """Fit the model to the training data with optional validation set.
396
+
397
+ Parameters
398
+ ----------
399
+ X_train : List[str]
400
+ Training set input molecular structures as SMILES strings
401
+ y_train : Union[List, np.ndarray]
402
+ Training set target values for property prediction
403
+ X_val : List[str], optional
404
+ Validation set input molecular structures as SMILES strings.
405
+ If None, training data will be used for validation
406
+ y_val : Union[List, np.ndarray], optional
407
+ Validation set target values. Required if X_val is provided
408
+ """
409
+ if (X_val is None) != (y_val is None):
410
+ raise ValueError(
411
+ "Both X_val and y_val must be provided for validation. "
412
+ f"Got X_val={X_val is not None}, y_val={y_val is not None}"
413
+ )
414
+
415
+ self._initialize_model(self.model_class)
416
+ self.model.initialize_parameters()
417
+ optimizer, scheduler = self._setup_optimizers()
418
+
419
+ # Prepare datasets and loaders
420
+ if not isinstance(X_train, list) or not all(isinstance(item, str) for item in X_train):
421
+ raise TypeError(f"Expected train data to be a list of strings, but got {type(X_train)} with elements {X_train}")
422
+
423
+ train_dataset = self._convert_to_pytorch_data(X_train, y_train)
424
+ train_loader = DataLoader(
425
+ train_dataset,
426
+ batch_size=self.batch_size,
427
+ shuffle=True,
428
+ num_workers=0
429
+ )
430
+
431
+ if X_val is None or y_val is None:
432
+ val_loader = train_loader
433
+ warnings.warn(
434
+ "No validation set provided. Using training set for validation. "
435
+ "This may lead to overfitting.",
436
+ UserWarning
437
+ )
438
+ else:
439
+ if not isinstance(X_val, list) or not all(isinstance(item, str) for item in X_val):
440
+ raise TypeError(f"Expected valid data to be a list of strings, but got {type(X_val)} with elements {X_val}")
441
+
442
+ val_dataset = self._convert_to_pytorch_data(X_val, y_val)
443
+ val_loader = DataLoader(
444
+ val_dataset,
445
+ batch_size=self.batch_size,
446
+ shuffle=False,
447
+ num_workers=0
448
+ )
449
+
450
+ # Initialize training state
451
+ self.fitting_loss = []
452
+ self.fitting_epoch = 0
453
+ best_state_dict = None
454
+ best_eval = float('-inf') if self.evaluate_higher_better else float('inf')
455
+ cnt_wait = 0
456
+
457
+ for epoch in range(self.epochs):
458
+ # Training phase
459
+ train_losses = self._train_epoch(train_loader, optimizer)
460
+ self.fitting_loss.append(np.mean(train_losses))
461
+
462
+ # Validation phase
463
+ current_eval = self._evaluation_epoch(val_loader)
464
+
465
+ if scheduler:
466
+ scheduler.step(current_eval)
467
+
468
+ # Model selection (check if current evaluation is better)
469
+ is_better = (
470
+ current_eval > best_eval if self.evaluate_higher_better
471
+ else current_eval < best_eval
472
+ )
473
+
474
+ if is_better:
475
+ self.fitting_epoch = epoch
476
+ best_eval = current_eval
477
+ best_state_dict = self.model.state_dict()
478
+ cnt_wait = 0
479
+ else:
480
+ cnt_wait += 1
481
+ if cnt_wait > self.patience:
482
+ if self.verbose:
483
+ print(f"Early stopping triggered after {epoch} epochs")
484
+ break
485
+
486
+ if self.verbose and epoch % 10 == 0:
487
+ print(
488
+ f"Epoch {epoch}: Loss = {np.mean(train_losses):.4f}, "
489
+ f"{self.evaluate_name} = {current_eval:.4f}, "
490
+ f"Best {self.evaluate_name} = {best_eval:.4f}"
491
+ )
492
+
493
+ # Restore best model
494
+ if best_state_dict is not None:
495
+ self.model.load_state_dict(best_state_dict)
496
+ else:
497
+ warnings.warn(
498
+ "No improvement was achieved during training. "
499
+ "The model may not be fitted properly.",
500
+ UserWarning
501
+ )
502
+
503
+ self.is_fitted_ = True
504
+ return self
505
+
506
+ def predict(self, X: List[str]) -> Dict[str, np.ndarray]:
507
+ """Make predictions using the fitted model.
508
+
509
+ Parameters
510
+ ----------
511
+ X : List[str]
512
+ List of SMILES strings to make predictions for
513
+
514
+ Returns
515
+ -------
516
+ Dict[str, np.ndarray]
517
+ Dictionary containing:
518
+ - 'prediction': Model predictions (shape: [n_samples, n_tasks])
519
+
520
+ """
521
+ self._check_is_fitted()
522
+
523
+ # Convert to token format and create loader
524
+ if not isinstance(X, list) or not all(isinstance(item, str) for item in X):
525
+ raise TypeError(f"Expected X to be a list of strings, but got {type(X)} with elements {X}")
526
+ dataset = self._convert_to_pytorch_data(X)
527
+ loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
528
+
529
+ if self.model is None:
530
+ raise RuntimeError("Model not initialized")
531
+ # Make predictions
532
+ self.model = self.model.to(self.device)
533
+ self.model.eval()
534
+ predictions = []
535
+ with torch.no_grad():
536
+ for batch in tqdm(loader, disable=not self.verbose):
537
+ batched_input, batched_label = batch
538
+ batched_input = batched_input.to(self.device)
539
+
540
+ out = self.model(batched_input)
541
+ predictions.append(out["prediction"].cpu().numpy())
542
+ return {
543
+ "prediction": np.concatenate(predictions, axis=0),
544
+ }
545
+
546
+ def _evaluation_epoch(
547
+ self,
548
+ loader: DataLoader,
549
+ ) -> float:
550
+ """Evaluate the model on given data.
551
+
552
+ Parameters
553
+ ----------
554
+ loader : DataLoader
555
+ DataLoader containing evaluation data
556
+ train_losses : List[float]
557
+ Training losses from current epoch
558
+
559
+ Returns
560
+ -------
561
+ float
562
+ Evaluation metric value (adjusted for higher/lower better)
563
+ """
564
+ self.model.eval()
565
+ y_pred_list = []
566
+ y_true_list = []
567
+
568
+ with torch.no_grad():
569
+ for batch in loader:
570
+ batched_input, batched_label = batch
571
+ batched_input = batched_input.to(self.device)
572
+
573
+ out = self.model(batched_input)
574
+ y_pred_list.append(out["prediction"].detach().cpu().numpy()) # ensuring NumPy format
575
+ y_true_list.append(batched_label.cpu().numpy())
576
+
577
+ y_pred = np.concatenate(y_pred_list, axis=0)
578
+ y_pred = y_pred.reshape(-1, 1)
579
+ y_true = np.concatenate(y_true_list, axis=0)
580
+
581
+ # Compute metric
582
+ metric_value = float(self.evaluate_criterion(y_true, y_pred))
583
+
584
+ # Adjust metric value based on higher/lower better
585
+ return metric_value
586
+
587
+ def _train_epoch(self, train_loader, optimizer):
588
+ """Training logic for one epoch.
589
+
590
+ Args:
591
+ train_loader: DataLoader containing training data
592
+ optimizer: Optimizer instance for model parameter updates
593
+
594
+ Returns:
595
+ list: List of loss values for each training step
596
+ """
597
+ self.model.train()
598
+ losses = []
599
+
600
+ iterator = (
601
+ tqdm(train_loader, desc="Training", leave=False)
602
+ if self.verbose
603
+ else train_loader
604
+ )
605
+
606
+ for batch in iterator:
607
+ batched_input, batched_label = batch
608
+ batched_input = batched_input.to(self.device)
609
+ batched_label = batched_label.to(self.device).view(-1, 1)
610
+ optimizer.zero_grad()
611
+
612
+ # Forward pass and loss computation
613
+ loss = self.model.compute_loss(batched_input, batched_label, self.loss_criterion)
614
+
615
+ # Backward pass
616
+ loss.backward()
617
+
618
+ optimizer.step()
619
+
620
+ losses.append(loss.item())
621
+
622
+ # Update progress bar if using tqdm
623
+ if self.verbose:
624
+ iterator.set_postfix({"loss": f"{loss.item():.4f}"})
625
+
626
+ return losses
627
+