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