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