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,436 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import torch
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ..utils import (
|
|
5
|
+
roc_auc_score,
|
|
6
|
+
accuracy_score,
|
|
7
|
+
mean_absolute_error,
|
|
8
|
+
mean_squared_error,
|
|
9
|
+
root_mean_squared_error,
|
|
10
|
+
r2_score,
|
|
11
|
+
)
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import Optional, ClassVar, Union, List, Dict, Any, Tuple, Callable, Type
|
|
15
|
+
from ..base.base import BaseModel
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BaseMolecularPredictor(BaseModel, ABC):
|
|
19
|
+
"""Base class for molecular discovery estimators."""
|
|
20
|
+
|
|
21
|
+
num_task: int
|
|
22
|
+
task_type: str
|
|
23
|
+
model_name: str = field(default="BaseMolecularPredictor")
|
|
24
|
+
|
|
25
|
+
DEFAULT_METRICS: ClassVar[Dict] = {
|
|
26
|
+
"classification": {"default": ("roc_auc", roc_auc_score, True)},
|
|
27
|
+
"regression": {"default": ("mae", mean_absolute_error, False)},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
super().__post_init__()
|
|
32
|
+
if self.task_type not in ["classification", "regression"]:
|
|
33
|
+
raise ValueError(f"Invalid task_type: {self.task_type}")
|
|
34
|
+
if self.num_task <= 0:
|
|
35
|
+
raise ValueError(f"num_task must be positive, got {self.num_task}")
|
|
36
|
+
|
|
37
|
+
def _get_param_names(self) -> List[str]:
|
|
38
|
+
return super()._get_param_names() + ["num_task", "task_type"]
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def fit(self, X_train, y_train, X_val=None, y_val=None, X_unlbl=None) -> "BaseMolecularPredictor":
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def autofit(self, X_train, y_train, X_val=None, y_val=None, search_parameters: Optional[dict] = None, n_trials: int = 10) -> "BaseMolecularPredictor":
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _train_epoch(self, train_loader, optimizer):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def _evaluation_epoch(self, evaluate_loader):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def _setup_evaluation(
|
|
65
|
+
self,
|
|
66
|
+
evaluate_criterion: Optional[Union[str, Callable]],
|
|
67
|
+
evaluate_higher_better: Optional[bool],
|
|
68
|
+
) -> None:
|
|
69
|
+
if evaluate_criterion is None:
|
|
70
|
+
default_metric = self.DEFAULT_METRICS[self.task_type]["default"]
|
|
71
|
+
self.evaluate_name = default_metric[0]
|
|
72
|
+
self.evaluate_criterion = default_metric[1]
|
|
73
|
+
self.evaluate_higher_better = default_metric[2]
|
|
74
|
+
else:
|
|
75
|
+
if isinstance(evaluate_criterion, str):
|
|
76
|
+
metric_map = {
|
|
77
|
+
"accuracy": (accuracy_score, True),
|
|
78
|
+
"roc_auc": (roc_auc_score, True),
|
|
79
|
+
"rmse": (root_mean_squared_error, False),
|
|
80
|
+
"mse": (mean_squared_error, False),
|
|
81
|
+
"mae": (mean_absolute_error, False),
|
|
82
|
+
"r2": (r2_score, True),
|
|
83
|
+
}
|
|
84
|
+
if evaluate_criterion not in metric_map:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Unknown metric: {evaluate_criterion}. "
|
|
87
|
+
f"Available metrics: {list(metric_map.keys())}"
|
|
88
|
+
)
|
|
89
|
+
self.evaluate_name = evaluate_criterion
|
|
90
|
+
self.evaluate_criterion = metric_map[evaluate_criterion][0]
|
|
91
|
+
self.evaluate_higher_better = (
|
|
92
|
+
metric_map[evaluate_criterion][1]
|
|
93
|
+
if evaluate_higher_better is None
|
|
94
|
+
else evaluate_higher_better
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
if evaluate_higher_better is None:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"evaluate_higher_better must be specified for a custom function."
|
|
100
|
+
)
|
|
101
|
+
self.evaluate_name = "custom"
|
|
102
|
+
self.evaluate_criterion = evaluate_criterion
|
|
103
|
+
self.evaluate_higher_better = evaluate_higher_better
|
|
104
|
+
|
|
105
|
+
def _load_default_criterion(self):
|
|
106
|
+
if self.task_type == "regression":
|
|
107
|
+
return torch.nn.L1Loss()
|
|
108
|
+
elif self.task_type == "classification":
|
|
109
|
+
return torch.nn.BCEWithLogitsLoss()
|
|
110
|
+
else:
|
|
111
|
+
warnings.warn(
|
|
112
|
+
"Unknown task type. Using L1 Loss as default. "
|
|
113
|
+
"Please specify 'regression' or 'classification' for better results."
|
|
114
|
+
)
|
|
115
|
+
return torch.nn.L1Loss()
|
|
116
|
+
|
|
117
|
+
# import os
|
|
118
|
+
# import json
|
|
119
|
+
# import datetime
|
|
120
|
+
# import tempfile
|
|
121
|
+
# import warnings
|
|
122
|
+
# import torch
|
|
123
|
+
# import numpy as np
|
|
124
|
+
# from rdkit import Chem
|
|
125
|
+
# from ..utils import (
|
|
126
|
+
# roc_auc_score,
|
|
127
|
+
# accuracy_score,
|
|
128
|
+
# mean_absolute_error,
|
|
129
|
+
# mean_squared_error,
|
|
130
|
+
# root_mean_squared_error,
|
|
131
|
+
# r2_score,
|
|
132
|
+
# )
|
|
133
|
+
# from ..utils.checkpoint import LocalCheckpointManager, HuggingFaceCheckpointManager
|
|
134
|
+
# from ..utils.checker import MolecularInputChecker
|
|
135
|
+
# from dataclasses import dataclass, field
|
|
136
|
+
# from abc import ABC, abstractmethod
|
|
137
|
+
# from typing import Optional, ClassVar, Union, List, Dict, Any, Tuple, Callable, Type
|
|
138
|
+
|
|
139
|
+
# @dataclass
|
|
140
|
+
# class BaseMolecularPredictor(ABC):
|
|
141
|
+
# """Base class for molecular discovery estimators.
|
|
142
|
+
|
|
143
|
+
# This class provides a structure for implementing custom molecular discovery
|
|
144
|
+
# models. It includes basic functionality for parameter management, fitting,
|
|
145
|
+
# prediction, and validation.
|
|
146
|
+
# """
|
|
147
|
+
|
|
148
|
+
# num_task: int
|
|
149
|
+
# task_type: str
|
|
150
|
+
# model_name: str = field(default="BaseMolecularPredictor")
|
|
151
|
+
# device: Optional[torch.device] = field(default=None)
|
|
152
|
+
# model: Optional[torch.nn.Module] = field(default=None, init=False)
|
|
153
|
+
|
|
154
|
+
# DEFAULT_METRICS: ClassVar[Dict] = {
|
|
155
|
+
# "classification": {
|
|
156
|
+
# "default": ("roc_auc", roc_auc_score, True),
|
|
157
|
+
# },
|
|
158
|
+
# "regression": {"default": ("mae", mean_absolute_error, False)},
|
|
159
|
+
# }
|
|
160
|
+
|
|
161
|
+
# is_fitted_: bool = field(default=False, init=False)
|
|
162
|
+
# model_class: Optional[Type[torch.nn.Module]] = field(default=None, init=False)
|
|
163
|
+
|
|
164
|
+
# def __post_init__(self):
|
|
165
|
+
# if self.task_type not in ["classification", "regression"]:
|
|
166
|
+
# raise ValueError(
|
|
167
|
+
# f"task_type must be one of ['classification', 'regression'], got {self.task_type}"
|
|
168
|
+
# )
|
|
169
|
+
|
|
170
|
+
# if not isinstance(self.num_task, (int, np.integer)) or self.num_task <= 0:
|
|
171
|
+
# raise ValueError(f"num_task must be a positive integer, got {self.num_task}")
|
|
172
|
+
|
|
173
|
+
# if self.device is None:
|
|
174
|
+
# self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
|
175
|
+
# elif isinstance(self.device, str):
|
|
176
|
+
# self.device = torch.device(self.device)
|
|
177
|
+
|
|
178
|
+
# def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
179
|
+
# out = dict()
|
|
180
|
+
# for key in self._get_param_names():
|
|
181
|
+
# value = getattr(self, key)
|
|
182
|
+
# if deep and hasattr(value, "get_params"):
|
|
183
|
+
# deep_items = value.get_params().items()
|
|
184
|
+
# out.update((key + "__" + k, val) for k, val in deep_items)
|
|
185
|
+
# out[key] = value
|
|
186
|
+
# return out
|
|
187
|
+
|
|
188
|
+
# def set_params(self, **params) -> "BaseMolecularPredictor":
|
|
189
|
+
# if not params:
|
|
190
|
+
# return self
|
|
191
|
+
|
|
192
|
+
# valid_params = self.get_params(deep=True)
|
|
193
|
+
# for key, value in params.items():
|
|
194
|
+
# if key not in valid_params:
|
|
195
|
+
# raise ValueError(f"Invalid parameter {key} for predictor {self}")
|
|
196
|
+
# setattr(self, key, value)
|
|
197
|
+
# return self
|
|
198
|
+
|
|
199
|
+
# @abstractmethod
|
|
200
|
+
# def fit(self, X_train, y_train, X_val=None, y_val=None, X_unlbl=None) -> "BaseMolecularPredictor":
|
|
201
|
+
# pass
|
|
202
|
+
|
|
203
|
+
# @abstractmethod
|
|
204
|
+
# def autofit(
|
|
205
|
+
# self,
|
|
206
|
+
# X_train,
|
|
207
|
+
# y_train,
|
|
208
|
+
# X_val=None,
|
|
209
|
+
# y_val=None,
|
|
210
|
+
# search_parameters: Optional[dict] = None,
|
|
211
|
+
# n_trials: int = 10,
|
|
212
|
+
# ) -> "BaseMolecularPredictor":
|
|
213
|
+
# pass
|
|
214
|
+
|
|
215
|
+
# @abstractmethod
|
|
216
|
+
# def predict(self, X: np.ndarray) -> np.ndarray:
|
|
217
|
+
# pass
|
|
218
|
+
|
|
219
|
+
# def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
220
|
+
# raise NotImplementedError("Child class must implement optimizer and scheduler setup.")
|
|
221
|
+
|
|
222
|
+
# def _setup_evaluation(
|
|
223
|
+
# self,
|
|
224
|
+
# evaluate_criterion: Optional[Union[str, Callable]],
|
|
225
|
+
# evaluate_higher_better: Optional[bool],
|
|
226
|
+
# ) -> None:
|
|
227
|
+
# if evaluate_criterion is None:
|
|
228
|
+
# default_metric = self.DEFAULT_METRICS[self.task_type]["default"]
|
|
229
|
+
# self.evaluate_name = default_metric[0]
|
|
230
|
+
# self.evaluate_criterion = default_metric[1]
|
|
231
|
+
# self.evaluate_higher_better = default_metric[2]
|
|
232
|
+
# else:
|
|
233
|
+
# if isinstance(evaluate_criterion, str):
|
|
234
|
+
# metric_map = {
|
|
235
|
+
# "accuracy": (accuracy_score, True),
|
|
236
|
+
# "roc_auc": (roc_auc_score, True),
|
|
237
|
+
# "rmse": (root_mean_squared_error, False),
|
|
238
|
+
# "mse": (mean_squared_error, False),
|
|
239
|
+
# "mae": (mean_absolute_error, False),
|
|
240
|
+
# "r2": (r2_score, True),
|
|
241
|
+
# }
|
|
242
|
+
# if evaluate_criterion not in metric_map:
|
|
243
|
+
# raise ValueError(
|
|
244
|
+
# f"Unknown metric: {evaluate_criterion}. "
|
|
245
|
+
# f"Available metrics: {list(metric_map.keys())}"
|
|
246
|
+
# )
|
|
247
|
+
# self.evaluate_name = evaluate_criterion
|
|
248
|
+
# self.evaluate_criterion = metric_map[evaluate_criterion][0]
|
|
249
|
+
# self.evaluate_higher_better = (
|
|
250
|
+
# metric_map[evaluate_criterion][1]
|
|
251
|
+
# if evaluate_higher_better is None
|
|
252
|
+
# else evaluate_higher_better
|
|
253
|
+
# )
|
|
254
|
+
# else:
|
|
255
|
+
# if evaluate_higher_better is None:
|
|
256
|
+
# raise ValueError(
|
|
257
|
+
# "evaluate_higher_better must be specified for a custom function."
|
|
258
|
+
# )
|
|
259
|
+
# self.evaluate_name = "custom"
|
|
260
|
+
# self.evaluate_criterion = evaluate_criterion
|
|
261
|
+
# self.evaluate_higher_better = evaluate_higher_better
|
|
262
|
+
|
|
263
|
+
# def _train_epoch(self, train_loader, optimizer):
|
|
264
|
+
# raise NotImplementedError("Child class must implement training logic.")
|
|
265
|
+
|
|
266
|
+
# def _evaluation_epoch(self, evaluate_loader):
|
|
267
|
+
# raise NotImplementedError("Child class must implement evaluation logic.")
|
|
268
|
+
|
|
269
|
+
# def _check_is_fitted(self) -> None:
|
|
270
|
+
# if not self.is_fitted_:
|
|
271
|
+
# raise AttributeError(
|
|
272
|
+
# "This instance is not fitted yet. "
|
|
273
|
+
# "Call 'fit' before using this estimator."
|
|
274
|
+
# )
|
|
275
|
+
|
|
276
|
+
# def __repr__(self, N_CHAR_MAX=700):
|
|
277
|
+
# attributes = {
|
|
278
|
+
# name: value
|
|
279
|
+
# for name, value in sorted(self.__dict__.items())
|
|
280
|
+
# if not name.startswith("_") and not callable(value)
|
|
281
|
+
# }
|
|
282
|
+
|
|
283
|
+
# def format_value(v):
|
|
284
|
+
# if isinstance(v, (float, np.float32, np.float64)):
|
|
285
|
+
# return f"{v:.3g}"
|
|
286
|
+
# elif isinstance(v, (list, tuple, np.ndarray)) and len(v) > 6:
|
|
287
|
+
# return f"{str(v[:3])[:-1]}, ..., {str(v[-3:])[1:]}"
|
|
288
|
+
# elif isinstance(v, str) and len(v) > 50:
|
|
289
|
+
# return f"'{v[:25]}...{v[-22:]}'"
|
|
290
|
+
# elif isinstance(v, dict) and len(v) > 6:
|
|
291
|
+
# items = list(v.items())
|
|
292
|
+
# return f"{dict(items[:3])}...{dict(items[-3:])}"
|
|
293
|
+
# elif isinstance(v, torch.nn.Module):
|
|
294
|
+
# return f"{v.__class__.__name__}(...)"
|
|
295
|
+
# elif v is None:
|
|
296
|
+
# return "None"
|
|
297
|
+
# else:
|
|
298
|
+
# return repr(v)
|
|
299
|
+
|
|
300
|
+
# class_name = self.__class__.__name__
|
|
301
|
+
# attributes_str = []
|
|
302
|
+
|
|
303
|
+
# important_attrs = ["num_task", "task_type", "model_name", "is_fitted_"]
|
|
304
|
+
# for attr in important_attrs:
|
|
305
|
+
# if attr in attributes:
|
|
306
|
+
# value = attributes.pop(attr)
|
|
307
|
+
# attributes_str.append(f"{attr}={format_value(value)}")
|
|
308
|
+
|
|
309
|
+
# sorted_attrs = sorted(attributes.items())
|
|
310
|
+
# attributes_str.extend(f"{name}={format_value(value)}" for name, value in sorted_attrs)
|
|
311
|
+
|
|
312
|
+
# content = ",\n ".join(attributes_str)
|
|
313
|
+
# repr_ = f"{class_name}(\n {content}\n)"
|
|
314
|
+
|
|
315
|
+
# if len(repr_) > N_CHAR_MAX:
|
|
316
|
+
# lines = repr_.split("\n")
|
|
317
|
+
# result = [lines[0]]
|
|
318
|
+
# current_length = len(lines[0])
|
|
319
|
+
# for line in lines[1:-1]:
|
|
320
|
+
# if current_length + len(line) + 5 > N_CHAR_MAX:
|
|
321
|
+
# result.append(" ...")
|
|
322
|
+
# break
|
|
323
|
+
# result.append(line)
|
|
324
|
+
# current_length += len(line) + 1
|
|
325
|
+
# result.append(lines[-1])
|
|
326
|
+
# repr_ = "\n".join(result)
|
|
327
|
+
|
|
328
|
+
# return repr_
|
|
329
|
+
|
|
330
|
+
# @abstractmethod
|
|
331
|
+
# def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
332
|
+
# raise NotImplementedError("Child class must implement _get_model_params.")
|
|
333
|
+
|
|
334
|
+
# def _initialize_model(
|
|
335
|
+
# self, model_class: Type[torch.nn.Module], device, checkpoint: Optional[Dict] = None
|
|
336
|
+
# ) -> None:
|
|
337
|
+
# """Initialize the model with parameters or a checkpoint."""
|
|
338
|
+
# try:
|
|
339
|
+
# model_params = self._get_model_params(checkpoint)
|
|
340
|
+
# self.model = model_class(**model_params)
|
|
341
|
+
# try:
|
|
342
|
+
# self.model = self.model.to(device)
|
|
343
|
+
# except Exception as e:
|
|
344
|
+
# raise RuntimeError(f"Failed to move model to device {device}: {str(e)}")
|
|
345
|
+
|
|
346
|
+
# if checkpoint is not None:
|
|
347
|
+
# try:
|
|
348
|
+
# self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
349
|
+
# except Exception as e:
|
|
350
|
+
# raise ValueError(f"Failed to load model state dictionary: {str(e)}")
|
|
351
|
+
|
|
352
|
+
# except Exception as e:
|
|
353
|
+
# raise RuntimeError(f"Model initialization failed: {str(e)}")
|
|
354
|
+
|
|
355
|
+
# def _load_default_criterion(self):
|
|
356
|
+
# if self.task_type == "regression":
|
|
357
|
+
# return torch.nn.L1Loss()
|
|
358
|
+
# elif self.task_type == "classification":
|
|
359
|
+
# return torch.nn.BCEWithLogitsLoss()
|
|
360
|
+
# else:
|
|
361
|
+
# warnings.warn(
|
|
362
|
+
# "Unknown task type. Using L1 Loss as default. "
|
|
363
|
+
# "Please specify 'regression' or 'classification' for better results."
|
|
364
|
+
# )
|
|
365
|
+
# return torch.nn.L1Loss()
|
|
366
|
+
|
|
367
|
+
# def _validate_inputs(
|
|
368
|
+
# self, X: List[str], y: Optional[Union[List, np.ndarray]] = None, return_rdkit_mol: bool = True
|
|
369
|
+
# ) -> Tuple[Union[List[str], List["Chem.Mol"]], Optional[np.ndarray]]:
|
|
370
|
+
# return MolecularInputChecker.validate_inputs(X, y, self.num_task, 0, return_rdkit_mol)
|
|
371
|
+
|
|
372
|
+
# # def _validate_smiles(self, smiles: str, idx: int) -> Tuple[bool, Optional[str], Optional[Chem.Mol]]:
|
|
373
|
+
# # return MolecularInputChecker.validate_smiles(smiles, idx)
|
|
374
|
+
|
|
375
|
+
# @staticmethod
|
|
376
|
+
# def _get_param_names() -> List[str]:
|
|
377
|
+
# return ["num_task", "task_type", "is_fitted_"]
|
|
378
|
+
|
|
379
|
+
# def save_to_local(self, path: str) -> None:
|
|
380
|
+
# """Save to local disk using LocalCheckpointManager."""
|
|
381
|
+
# LocalCheckpointManager.save_model_to_local(self, path)
|
|
382
|
+
|
|
383
|
+
# def load_from_local(self, path: str) -> None:
|
|
384
|
+
# """Load from local disk using LocalCheckpointManager."""
|
|
385
|
+
# LocalCheckpointManager.load_model_from_local(self, path)
|
|
386
|
+
|
|
387
|
+
# def save_to_hf(
|
|
388
|
+
# self,
|
|
389
|
+
# repo_id: str,
|
|
390
|
+
# task_id: str = "default",
|
|
391
|
+
# metadata_dict: Optional[Dict[str, Any]] = None,
|
|
392
|
+
# metrics: Optional[Dict[str, float]] = None,
|
|
393
|
+
# commit_message: str = "Update model",
|
|
394
|
+
# token: Optional[str] = None,
|
|
395
|
+
# private: bool = False,
|
|
396
|
+
# ) -> None:
|
|
397
|
+
# HuggingFaceCheckpointManager.push_to_huggingface(
|
|
398
|
+
# model_instance=self,
|
|
399
|
+
# repo_id=repo_id,
|
|
400
|
+
# task_id=task_id,
|
|
401
|
+
# metadata_dict=metadata_dict,
|
|
402
|
+
# metrics=metrics,
|
|
403
|
+
# commit_message=commit_message,
|
|
404
|
+
# token=token,
|
|
405
|
+
# private=private,
|
|
406
|
+
# )
|
|
407
|
+
|
|
408
|
+
# def load_from_hf(self, repo_id: str, path: str) -> None:
|
|
409
|
+
# """Download and load model from Hugging Face Hub."""
|
|
410
|
+
# HuggingFaceCheckpointManager.load_model_from_hf(self, repo_id, path)
|
|
411
|
+
|
|
412
|
+
# def save(self, path: Optional[str] = None, repo_id: Optional[str] = None, **kwargs) -> None:
|
|
413
|
+
# """
|
|
414
|
+
# Automatic save. If `repo_id` is given, push to Hugging Face Hub.
|
|
415
|
+
# Otherwise, save to local path.
|
|
416
|
+
# """
|
|
417
|
+
# if repo_id is not None:
|
|
418
|
+
# self.save_to_hf(repo_id=repo_id, **kwargs)
|
|
419
|
+
# else:
|
|
420
|
+
# if path is None:
|
|
421
|
+
# raise ValueError("path must be provided if repo_id is not given.")
|
|
422
|
+
# self.save_to_local(path)
|
|
423
|
+
|
|
424
|
+
# def load(self, path: str, repo_id: Optional[str] = None) -> None:
|
|
425
|
+
# """
|
|
426
|
+
# Automatic load. If local `path` exists, load from there.
|
|
427
|
+
# Otherwise, try loading from Hugging Face Hub if `repo_id` is given.
|
|
428
|
+
# """
|
|
429
|
+
# if os.path.exists(path):
|
|
430
|
+
# self.load_from_local(path)
|
|
431
|
+
# else:
|
|
432
|
+
# if repo_id is None:
|
|
433
|
+
# raise FileNotFoundError(
|
|
434
|
+
# f"No local file found at '{path}' and no repo_id provided."
|
|
435
|
+
# )
|
|
436
|
+
# self.load_from_hf(repo_id, path)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import torch
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ..utils import (
|
|
5
|
+
roc_auc_score,
|
|
6
|
+
accuracy_score,
|
|
7
|
+
mean_absolute_error,
|
|
8
|
+
mean_squared_error,
|
|
9
|
+
root_mean_squared_error,
|
|
10
|
+
r2_score,
|
|
11
|
+
)
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import Optional, ClassVar, Union, List, Dict, Any, Tuple, Callable, Type
|
|
15
|
+
from ..base.base import BaseModel
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BaseMolecularPredictor(BaseModel, ABC):
|
|
19
|
+
"""Base class for molecular discovery estimators."""
|
|
20
|
+
|
|
21
|
+
model_name: str = field(default="BaseMolecularPredictor")
|
|
22
|
+
num_task: int = field(default=0)
|
|
23
|
+
task_type: str = field(default=None)
|
|
24
|
+
DEFAULT_METRICS: ClassVar[Dict] = {
|
|
25
|
+
"classification": {"default": ("roc_auc", roc_auc_score, True)},
|
|
26
|
+
"regression": {"default": ("mae", mean_absolute_error, False)},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
super().__post_init__()
|
|
31
|
+
if self.task_type not in ["classification", "regression"]:
|
|
32
|
+
raise ValueError(f"Invalid task_type: {self.task_type}")
|
|
33
|
+
if self.num_task <= 0:
|
|
34
|
+
raise ValueError(f"num_task must be positive, got {self.num_task}")
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _get_param_names(self) -> List[str]:
|
|
38
|
+
return super()._get_param_names() + ["num_task", "task_type"]
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def autofit(self, X_train, y_train, X_val=None, y_val=None, search_parameters: Optional[dict] = None, n_trials: int = 10) -> "BaseMolecularPredictor":
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def fit(self, X_train, y_train, X_val=None, y_val=None, search_parameters: Optional[dict] = None, n_trials: int = 10) -> "BaseMolecularPredictor":
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def _evaluation_epoch(self, evaluate_loader):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def _setup_evaluation(
|
|
57
|
+
self,
|
|
58
|
+
evaluate_criterion: Optional[Union[str, Callable]],
|
|
59
|
+
evaluate_higher_better: Optional[bool],
|
|
60
|
+
) -> None:
|
|
61
|
+
if evaluate_criterion is None:
|
|
62
|
+
default_metric = self.DEFAULT_METRICS[self.task_type]["default"]
|
|
63
|
+
self.evaluate_name = default_metric[0]
|
|
64
|
+
self.evaluate_criterion = default_metric[1]
|
|
65
|
+
self.evaluate_higher_better = default_metric[2]
|
|
66
|
+
else:
|
|
67
|
+
if isinstance(evaluate_criterion, str):
|
|
68
|
+
metric_map = {
|
|
69
|
+
"accuracy": (accuracy_score, True),
|
|
70
|
+
"roc_auc": (roc_auc_score, True),
|
|
71
|
+
"rmse": (root_mean_squared_error, False),
|
|
72
|
+
"mse": (mean_squared_error, False),
|
|
73
|
+
"mae": (mean_absolute_error, False),
|
|
74
|
+
"r2": (r2_score, True),
|
|
75
|
+
}
|
|
76
|
+
if evaluate_criterion not in metric_map:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Unknown metric: {evaluate_criterion}. "
|
|
79
|
+
f"Available metrics: {list(metric_map.keys())}"
|
|
80
|
+
)
|
|
81
|
+
self.evaluate_name = evaluate_criterion
|
|
82
|
+
self.evaluate_criterion = metric_map[evaluate_criterion][0]
|
|
83
|
+
self.evaluate_higher_better = (
|
|
84
|
+
metric_map[evaluate_criterion][1]
|
|
85
|
+
if evaluate_higher_better is None
|
|
86
|
+
else evaluate_higher_better
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
if evaluate_higher_better is None:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"evaluate_higher_better must be specified for a custom function."
|
|
92
|
+
)
|
|
93
|
+
self.evaluate_name = "custom"
|
|
94
|
+
self.evaluate_criterion = evaluate_criterion
|
|
95
|
+
self.evaluate_higher_better = evaluate_higher_better
|
|
96
|
+
|
|
97
|
+
def _load_default_criterion(self):
|
|
98
|
+
if self.task_type == "regression":
|
|
99
|
+
return torch.nn.L1Loss(reduction='none')
|
|
100
|
+
elif self.task_type == "classification":
|
|
101
|
+
return torch.nn.BCEWithLogitsLoss(reduction='none')
|
|
102
|
+
else:
|
|
103
|
+
warnings.warn(
|
|
104
|
+
"Unknown task type. Using L1 Loss as default. "
|
|
105
|
+
"Please specify 'regression' or 'classification' for better results."
|
|
106
|
+
)
|
|
107
|
+
return torch.nn.L1Loss(reduction='none')
|
|
108
|
+
|
|
109
|
+
def _validate_inputs(
|
|
110
|
+
self, X: List[str], y: Optional[Union[List, np.ndarray]] = None, num_task: int = 0, num_pretask: int = 0, return_rdkit_mol: bool = True
|
|
111
|
+
) -> Tuple[Union[List[str], List["Chem.Mol"]], Optional[np.ndarray]]:
|
|
112
|
+
return super()._validate_inputs(X, y, self.num_task, 0, return_rdkit_mol)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
|
|
4
|
+
|
|
5
|
+
from ...nn import GNN_node, GNN_node_Virtualnode, MLP
|
|
6
|
+
from ...utils import init_weights
|
|
7
|
+
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
class_criterion = torch.nn.CrossEntropyLoss()
|
|
11
|
+
|
|
12
|
+
class GNN(nn.Module):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
num_layer,
|
|
16
|
+
hidden_size,
|
|
17
|
+
drop_ratio=0.5,
|
|
18
|
+
norm_layer="batch_norm",
|
|
19
|
+
encoder_type="gin-virtual",
|
|
20
|
+
readout="max",
|
|
21
|
+
mask_num=0,
|
|
22
|
+
mask_rate=0.15
|
|
23
|
+
):
|
|
24
|
+
super(GNN, self).__init__()
|
|
25
|
+
gnn_name = encoder_type.split("-")[0]
|
|
26
|
+
self.num_atom_type = 119
|
|
27
|
+
self.hidden_size = hidden_size
|
|
28
|
+
self.mask_num = mask_num
|
|
29
|
+
self.mask_rate = mask_rate
|
|
30
|
+
|
|
31
|
+
encoder_params = {
|
|
32
|
+
"num_layer": num_layer,
|
|
33
|
+
"hidden_size": hidden_size,
|
|
34
|
+
"JK": "last",
|
|
35
|
+
"drop_ratio": drop_ratio,
|
|
36
|
+
"residual": True,
|
|
37
|
+
"gnn_name": gnn_name,
|
|
38
|
+
"norm_layer": norm_layer
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Choose encoder type based on encoder_model
|
|
42
|
+
encoder_class = GNN_node_Virtualnode if "virtual" in encoder_type else GNN_node
|
|
43
|
+
self.graph_encoder = encoder_class(**encoder_params)
|
|
44
|
+
pooling_funcs = {
|
|
45
|
+
"sum": global_add_pool,
|
|
46
|
+
"mean": global_mean_pool,
|
|
47
|
+
"max": global_max_pool
|
|
48
|
+
}
|
|
49
|
+
self.pool = pooling_funcs.get(readout)
|
|
50
|
+
if self.pool is None:
|
|
51
|
+
raise ValueError(f"Invalid graph pooling type {readout}.")
|
|
52
|
+
|
|
53
|
+
self.predictor = MLP(hidden_size, hidden_features=2 * hidden_size, out_features=self.num_atom_type)
|
|
54
|
+
|
|
55
|
+
def initialize_parameters(self, seed=None):
|
|
56
|
+
"""
|
|
57
|
+
Randomly initialize all model parameters using the init_weights function.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
seed (int, optional): Random seed for reproducibility. Defaults to None.
|
|
61
|
+
"""
|
|
62
|
+
if seed is not None:
|
|
63
|
+
torch.manual_seed(seed)
|
|
64
|
+
|
|
65
|
+
# Initialize the main components
|
|
66
|
+
init_weights(self.graph_encoder)
|
|
67
|
+
init_weights(self.predictor)
|
|
68
|
+
|
|
69
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
70
|
+
def reset_parameters(module):
|
|
71
|
+
if hasattr(module, 'reset_parameters'):
|
|
72
|
+
module.reset_parameters()
|
|
73
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
74
|
+
init_weights(module)
|
|
75
|
+
|
|
76
|
+
self.apply(reset_parameters)
|
|
77
|
+
|
|
78
|
+
def compute_loss(self, batched_data):
|
|
79
|
+
masked_node_indices = list()
|
|
80
|
+
|
|
81
|
+
# select indices of masked nodes
|
|
82
|
+
for i in range(batched_data.batch[-1] + 1):
|
|
83
|
+
idx = torch.nonzero((batched_data.batch == i).float()).squeeze(-1)
|
|
84
|
+
num_node = idx.shape[0]
|
|
85
|
+
if self.mask_num == 0:
|
|
86
|
+
sample_size = int(num_node * self.mask_rate + 1)
|
|
87
|
+
else:
|
|
88
|
+
sample_size = min(self.mask_num, int(num_node * 0.5))
|
|
89
|
+
masked_node_idx = random.sample(idx.tolist(), sample_size)
|
|
90
|
+
masked_node_idx.sort()
|
|
91
|
+
masked_node_indices += masked_node_idx
|
|
92
|
+
|
|
93
|
+
batched_data.masked_node_indices = torch.tensor(masked_node_indices)
|
|
94
|
+
|
|
95
|
+
batched_data.y = batched_data.x[masked_node_indices][:, 0]
|
|
96
|
+
|
|
97
|
+
# mask nodes' features
|
|
98
|
+
for node_idx in masked_node_indices:
|
|
99
|
+
batched_data.x[node_idx] = torch.tensor([self.num_atom_type - 1] + [0] * (batched_data.x.shape[1] - 1))
|
|
100
|
+
|
|
101
|
+
# generate predictions
|
|
102
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
103
|
+
prediction_class = self.predictor(h_node[masked_node_indices])
|
|
104
|
+
|
|
105
|
+
target_class = batched_data.y.to(torch.float32)
|
|
106
|
+
loss_class = class_criterion(prediction_class.to(torch.float32), target_class.long())
|
|
107
|
+
|
|
108
|
+
return loss_class
|
|
109
|
+
|
|
110
|
+
def forward(self, batched_data):
|
|
111
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
112
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
113
|
+
return {"graph": h_rep, "node": h_node}
|