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,493 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
from typing import Optional, Union, Dict, Any, Tuple, List
|
|
5
|
+
import warnings
|
|
6
|
+
import datetime
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch_geometric.loader import DataLoader
|
|
10
|
+
from torch_geometric.data import Data
|
|
11
|
+
from .gnn import GREANet
|
|
12
|
+
from .features import smiles_to_graph
|
|
13
|
+
|
|
14
|
+
from ...base import BaseMolecularPredictor
|
|
15
|
+
|
|
16
|
+
class GREAMolecularPredictor(BaseMolecularPredictor):
|
|
17
|
+
"""This predictor implements a GNN model based on Graph Rationalization
|
|
18
|
+
for molecular property prediction tasks.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
num_tasks: int = 1,
|
|
24
|
+
task_type: str = "classification",
|
|
25
|
+
gamma: float = 0.4,
|
|
26
|
+
num_layer: int = 5,
|
|
27
|
+
emb_dim: int = 300,
|
|
28
|
+
gnn_type: str = "gin-virtual",
|
|
29
|
+
drop_ratio: float = 0.5,
|
|
30
|
+
norm_layer: str = "batch_norm",
|
|
31
|
+
batch_size: int = 32,
|
|
32
|
+
epochs: int = 100,
|
|
33
|
+
learning_rate: float = 0.001,
|
|
34
|
+
weight_decay: float = 0.0,
|
|
35
|
+
patience: int = 50,
|
|
36
|
+
device: Optional[str] = None,
|
|
37
|
+
use_lr_scheduler: bool = True,
|
|
38
|
+
scheduler_factor: float = 0.5,
|
|
39
|
+
scheduler_patience: int = 5,
|
|
40
|
+
grad_clip_value: Optional[float] = None,
|
|
41
|
+
verbose: bool = False,
|
|
42
|
+
model_name: str = "GREApredictor",
|
|
43
|
+
criterion = None,
|
|
44
|
+
):
|
|
45
|
+
super().__init__()
|
|
46
|
+
|
|
47
|
+
# Model hyperparameters
|
|
48
|
+
self.num_tasks = num_tasks
|
|
49
|
+
self.task_type = task_type
|
|
50
|
+
self.num_layer = num_layer
|
|
51
|
+
self.gamma = gamma
|
|
52
|
+
self.emb_dim = emb_dim
|
|
53
|
+
self.gnn_type = gnn_type
|
|
54
|
+
self.drop_ratio = drop_ratio
|
|
55
|
+
self.norm_layer = norm_layer
|
|
56
|
+
|
|
57
|
+
# Training parameters
|
|
58
|
+
self.batch_size = batch_size
|
|
59
|
+
self.epochs = epochs
|
|
60
|
+
self.learning_rate = learning_rate
|
|
61
|
+
self.weight_decay = weight_decay
|
|
62
|
+
self.patience = patience
|
|
63
|
+
self.use_lr_scheduler = use_lr_scheduler
|
|
64
|
+
self.scheduler_factor = scheduler_factor
|
|
65
|
+
self.scheduler_patience = scheduler_patience
|
|
66
|
+
self.grad_clip_value = grad_clip_value
|
|
67
|
+
self.criterion = criterion if criterion is not None else self._load_default_criterion()
|
|
68
|
+
|
|
69
|
+
# Set device
|
|
70
|
+
self.device = (
|
|
71
|
+
torch.device(device)
|
|
72
|
+
if device
|
|
73
|
+
else torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Initialize model
|
|
77
|
+
self.model = GREANet(
|
|
78
|
+
num_tasks=self.num_tasks,
|
|
79
|
+
num_layer=self.num_layer,
|
|
80
|
+
gamma=self.gamma,
|
|
81
|
+
emb_dim=self.emb_dim,
|
|
82
|
+
gnn_type=self.gnn_type,
|
|
83
|
+
drop_ratio=self.drop_ratio,
|
|
84
|
+
norm_layer=self.norm_layer,
|
|
85
|
+
).to(self.device)
|
|
86
|
+
|
|
87
|
+
self.fitting_loss = []
|
|
88
|
+
self.fitting_loss_mean = float("inf")
|
|
89
|
+
self.fitting_epoch = 0
|
|
90
|
+
self.verbose = verbose
|
|
91
|
+
self.model_name = model_name
|
|
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
|
+
"gamma",
|
|
108
|
+
"emb_dim",
|
|
109
|
+
"gnn_type",
|
|
110
|
+
"drop_ratio",
|
|
111
|
+
"norm_layer",
|
|
112
|
+
# Training parameters
|
|
113
|
+
"batch_size",
|
|
114
|
+
"epochs",
|
|
115
|
+
"learning_rate",
|
|
116
|
+
"weight_decay",
|
|
117
|
+
"patience",
|
|
118
|
+
"device",
|
|
119
|
+
"grad_clip_value",
|
|
120
|
+
"criterion",
|
|
121
|
+
# Scheduler parameters
|
|
122
|
+
"use_lr_scheduler",
|
|
123
|
+
"scheduler_factor",
|
|
124
|
+
"scheduler_patience",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
128
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
Tuple[optim.Optimizer, Optional[Any]]
|
|
133
|
+
A tuple containing:
|
|
134
|
+
- The configured optimizer
|
|
135
|
+
- The learning rate scheduler (if enabled, else None)
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
The scheduler returned can be either _LRScheduler or ReduceLROnPlateau,
|
|
140
|
+
hence we use Any as the type hint instead of optim.lr_scheduler._LRScheduler
|
|
141
|
+
"""
|
|
142
|
+
optimizer = torch.optim.Adam(
|
|
143
|
+
self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
|
|
144
|
+
)
|
|
145
|
+
if self.grad_clip_value is not None:
|
|
146
|
+
for group in optimizer.param_groups:
|
|
147
|
+
group.setdefault("max_norm", self.grad_clip_value)
|
|
148
|
+
group.setdefault("norm_type", 2.0)
|
|
149
|
+
|
|
150
|
+
scheduler = None
|
|
151
|
+
if self.use_lr_scheduler:
|
|
152
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
153
|
+
optimizer,
|
|
154
|
+
mode="min",
|
|
155
|
+
factor=self.scheduler_factor,
|
|
156
|
+
patience=self.scheduler_patience,
|
|
157
|
+
min_lr=1e-6,
|
|
158
|
+
cooldown=0,
|
|
159
|
+
eps=1e-8,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return optimizer, scheduler
|
|
163
|
+
|
|
164
|
+
def _convert_to_pytorch_data(self, X, y=None):
|
|
165
|
+
"""Convert numpy arrays to PyTorch data format.
|
|
166
|
+
|
|
167
|
+
Should be implemented by child classes to handle specific molecular
|
|
168
|
+
representations (e.g., SMILES to graphs, conformers to graphs, etc.)
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
pyg_graph_list = []
|
|
172
|
+
for idx, smiles in enumerate(tqdm(X, desc="Converting molecules to graphs")):
|
|
173
|
+
if y is not None:
|
|
174
|
+
properties = y[idx]
|
|
175
|
+
else:
|
|
176
|
+
properties = None
|
|
177
|
+
graph = smiles_to_graph(smiles, properties)
|
|
178
|
+
g = Data()
|
|
179
|
+
g.num_nodes = graph["num_nodes"]
|
|
180
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
181
|
+
|
|
182
|
+
del graph["num_nodes"]
|
|
183
|
+
del graph["edge_index"]
|
|
184
|
+
|
|
185
|
+
if graph["edge_feat"] is not None:
|
|
186
|
+
g.edge_attr = torch.from_numpy(graph["edge_feat"])
|
|
187
|
+
del graph["edge_feat"]
|
|
188
|
+
|
|
189
|
+
if graph["node_feat"] is not None:
|
|
190
|
+
g.x = torch.from_numpy(graph["node_feat"])
|
|
191
|
+
del graph["node_feat"]
|
|
192
|
+
|
|
193
|
+
if graph["y"] is not None:
|
|
194
|
+
g.y = torch.from_numpy(graph["y"])
|
|
195
|
+
del graph["y"]
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
g.fp = torch.tensor(graph["fp"], dtype=torch.int8).view(1, -1)
|
|
199
|
+
del graph["fp"]
|
|
200
|
+
except:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
pyg_graph_list.append(g)
|
|
204
|
+
|
|
205
|
+
return pyg_graph_list
|
|
206
|
+
|
|
207
|
+
def fit(self, X: List[str], y: Optional[Union[List, np.ndarray]]) -> "GREAMolecularPredictor":
|
|
208
|
+
"""Fit the model to the data.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
X : list of SMILES strings
|
|
213
|
+
Input molecular structures as SMILES strings
|
|
214
|
+
y : array-like,
|
|
215
|
+
Target values for property prediction
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
self : object
|
|
219
|
+
Fitted estimator
|
|
220
|
+
"""
|
|
221
|
+
self.model = self.model.to(self.device)
|
|
222
|
+
# Setup training
|
|
223
|
+
optimizer, scheduler = self.setup_optimizers()
|
|
224
|
+
cnt_wait = 0
|
|
225
|
+
|
|
226
|
+
# Convert molecular data to PyTorch Geometric format
|
|
227
|
+
X, y = self._validate_inputs(X, y)
|
|
228
|
+
dataset = self._convert_to_pytorch_data(X, y)
|
|
229
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
230
|
+
|
|
231
|
+
# Initialize best model state
|
|
232
|
+
self.fitting_loss = []
|
|
233
|
+
self.fitting_loss_mean = float("inf")
|
|
234
|
+
self.fitting_epoch = 0
|
|
235
|
+
best_param_vector = None
|
|
236
|
+
|
|
237
|
+
# Training loop
|
|
238
|
+
for epoch in range(self.epochs):
|
|
239
|
+
# Train for one epoch
|
|
240
|
+
losses = self.train_epoch(loader, optimizer)
|
|
241
|
+
loss_mean = np.mean(losses)
|
|
242
|
+
if scheduler:
|
|
243
|
+
scheduler.step()
|
|
244
|
+
|
|
245
|
+
# Update best model
|
|
246
|
+
if loss_mean < self.fitting_loss_mean:
|
|
247
|
+
self.fitting_epoch = epoch
|
|
248
|
+
self.fitting_loss = losses
|
|
249
|
+
self.fitting_loss_mean = loss_mean
|
|
250
|
+
best_param_vector = torch.nn.utils.parameters_to_vector(self.model.parameters())
|
|
251
|
+
cnt_wait = 0
|
|
252
|
+
else:
|
|
253
|
+
cnt_wait += 1
|
|
254
|
+
if cnt_wait > self.patience:
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
if best_param_vector is not None:
|
|
258
|
+
torch.nn.utils.vector_to_parameters(best_param_vector, self.model.parameters())
|
|
259
|
+
|
|
260
|
+
self.is_fitted_ = True
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
def predict(self, X: List[str]) -> Dict[str, np.ndarray]:
|
|
264
|
+
"""Make predictions using the fitted model.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
X : List[str]
|
|
269
|
+
List of SMILES strings to make predictions for
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
Dict[str, np.ndarray]
|
|
274
|
+
Dictionary containing:
|
|
275
|
+
- 'prediction': Model predictions (shape: [n_samples, n_tasks])
|
|
276
|
+
- 'confidence': Prediction confidences (shape: [n_samples, n_tasks])
|
|
277
|
+
- 'variance': Prediction variances (shape: [n_samples, n_tasks])
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
self._check_is_fitted()
|
|
281
|
+
|
|
282
|
+
# Convert to PyTorch Geometric format and create loader
|
|
283
|
+
X, _ = self._validate_inputs(X)
|
|
284
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
285
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
286
|
+
|
|
287
|
+
# Make predictions
|
|
288
|
+
self.model.eval()
|
|
289
|
+
predictions = []
|
|
290
|
+
confidences = []
|
|
291
|
+
variances = []
|
|
292
|
+
with torch.no_grad():
|
|
293
|
+
for batch in loader:
|
|
294
|
+
batch = batch.to(self.device)
|
|
295
|
+
out = self.model(batch)
|
|
296
|
+
predictions.append(out["prediction"].cpu().numpy())
|
|
297
|
+
confidences.append(out["confidence"].cpu().numpy())
|
|
298
|
+
variances.append(out["variance"].cpu().numpy())
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"prediction": np.concatenate(predictions, axis=0),
|
|
302
|
+
"confidence": np.concatenate(confidences, axis=0),
|
|
303
|
+
"variance": np.concatenate(variances, axis=0),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
def train_epoch(self, train_loader, optimizer):
|
|
307
|
+
"""Training logic for one epoch.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
train_loader: DataLoader containing training data
|
|
311
|
+
optimizer: Optimizer instance for model parameter updates
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
list: List of loss values for each training step
|
|
315
|
+
"""
|
|
316
|
+
self.model.train()
|
|
317
|
+
losses = []
|
|
318
|
+
|
|
319
|
+
iterator = (
|
|
320
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
321
|
+
if self.verbose
|
|
322
|
+
else train_loader
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
for batch in iterator:
|
|
326
|
+
batch = batch.to(self.device)
|
|
327
|
+
optimizer.zero_grad()
|
|
328
|
+
|
|
329
|
+
# Forward pass and loss computation
|
|
330
|
+
loss = self.model.compute_loss(batch, self.criterion)
|
|
331
|
+
|
|
332
|
+
# Backward pass
|
|
333
|
+
loss.backward()
|
|
334
|
+
|
|
335
|
+
# Compute gradient norm if gradient clipping is enabled
|
|
336
|
+
if self.grad_clip_value is not None:
|
|
337
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
338
|
+
|
|
339
|
+
optimizer.step()
|
|
340
|
+
|
|
341
|
+
losses.append(loss.item())
|
|
342
|
+
|
|
343
|
+
# Update progress bar if using tqdm
|
|
344
|
+
if self.verbose:
|
|
345
|
+
iterator.set_postfix({"loss": f"{loss.item():.4f}"})
|
|
346
|
+
|
|
347
|
+
return losses
|
|
348
|
+
|
|
349
|
+
def save_model(self, path: str) -> None:
|
|
350
|
+
"""Save the model to disk.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
path : str
|
|
355
|
+
Path where to save the model
|
|
356
|
+
"""
|
|
357
|
+
if not self.is_fitted_:
|
|
358
|
+
raise ValueError("Model must be fitted before saving")
|
|
359
|
+
|
|
360
|
+
if not path.endswith((".pt", ".pth")):
|
|
361
|
+
raise ValueError("Save path should end with '.pt' or '.pth'")
|
|
362
|
+
|
|
363
|
+
# Create directory if it doesn't exist
|
|
364
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|
365
|
+
|
|
366
|
+
model_name = os.path.splitext(os.path.basename(path))[0]
|
|
367
|
+
save_dict = {
|
|
368
|
+
"model_state_dict": self.model.state_dict(),
|
|
369
|
+
"hyperparameters": self.get_params(),
|
|
370
|
+
"fitting_epoch": self.fitting_epoch,
|
|
371
|
+
"fitting_loss": self.fitting_loss,
|
|
372
|
+
"fitting_loss_mean": self.fitting_loss_mean,
|
|
373
|
+
"model_name": model_name,
|
|
374
|
+
"date_saved": datetime.datetime.now().isoformat(),
|
|
375
|
+
"version": getattr(self, "__version__", "1.0.0"),
|
|
376
|
+
}
|
|
377
|
+
torch.save(save_dict, path)
|
|
378
|
+
|
|
379
|
+
def load_model(self, path: str, repo: Optional[str] = None) -> "GREAMolecularPredictor":
|
|
380
|
+
"""Load a saved model from disk or download from HuggingFace hub.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
path : str
|
|
385
|
+
Path to the saved model file or desired local path for downloaded model
|
|
386
|
+
repo : str, optional
|
|
387
|
+
HuggingFace model repository ID (e.g., 'username/model-name')
|
|
388
|
+
If provided and local path doesn't exist, will attempt to download from hub
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
self : GREAMolecularPredictor
|
|
393
|
+
Updated model instance with loaded weights and parameters
|
|
394
|
+
|
|
395
|
+
Raises
|
|
396
|
+
------
|
|
397
|
+
FileNotFoundError
|
|
398
|
+
If the model file doesn't exist locally and no repo is provided,
|
|
399
|
+
or if download from repo fails
|
|
400
|
+
ValueError
|
|
401
|
+
If the saved file is corrupted or incompatible
|
|
402
|
+
"""
|
|
403
|
+
# First try to load from local path
|
|
404
|
+
if os.path.exists(path):
|
|
405
|
+
try:
|
|
406
|
+
checkpoint = torch.load(path, map_location=self.device)
|
|
407
|
+
except Exception as e:
|
|
408
|
+
raise ValueError(f"Error loading model from {path}: {str(e)}")
|
|
409
|
+
|
|
410
|
+
# If local file doesn't exist and repo is provided, try downloading
|
|
411
|
+
elif repo is not None:
|
|
412
|
+
try:
|
|
413
|
+
from huggingface_hub import hf_hub_download
|
|
414
|
+
# Create directory if it doesn't exist
|
|
415
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|
416
|
+
|
|
417
|
+
# Download from HuggingFace using specified parameters
|
|
418
|
+
model_name = os.path.splitext(os.path.basename(path))[0]
|
|
419
|
+
downloaded_path = hf_hub_download(
|
|
420
|
+
repo_id=repo,
|
|
421
|
+
filename=model_name,
|
|
422
|
+
local_dir=os.path.dirname(path),
|
|
423
|
+
local_dir_use_symlinks=False,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Load the downloaded model
|
|
427
|
+
checkpoint = torch.load(downloaded_path, map_location=self.device)
|
|
428
|
+
|
|
429
|
+
except Exception as e:
|
|
430
|
+
if os.path.exists(path):
|
|
431
|
+
os.remove(path) # Clean up partial downloads
|
|
432
|
+
raise FileNotFoundError(
|
|
433
|
+
f"Failed to download model from repository '{repo}': {str(e)}"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# If neither local file exists nor repo provided
|
|
437
|
+
else:
|
|
438
|
+
raise FileNotFoundError(f"No model file found at '{path}' and no repository provided")
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
# Validate checkpoint contents
|
|
442
|
+
required_keys = {
|
|
443
|
+
"model_state_dict",
|
|
444
|
+
"hyperparameters",
|
|
445
|
+
"fitting_loss",
|
|
446
|
+
"fitting_epoch",
|
|
447
|
+
"model_name",
|
|
448
|
+
}
|
|
449
|
+
if not all(key in checkpoint for key in required_keys):
|
|
450
|
+
missing_keys = required_keys - set(checkpoint.keys())
|
|
451
|
+
raise ValueError(f"Checkpoint missing required keys: {missing_keys}")
|
|
452
|
+
|
|
453
|
+
# Validate compatibility of hyperparameters
|
|
454
|
+
for key, value in checkpoint["hyperparameters"].items():
|
|
455
|
+
if hasattr(self, key) and getattr(self, key) != value:
|
|
456
|
+
warnings.warn(
|
|
457
|
+
f"Loaded model has different {key} ({value}) "
|
|
458
|
+
f"than current instance ({getattr(self, key)}). "
|
|
459
|
+
"Updating to match loaded model."
|
|
460
|
+
)
|
|
461
|
+
setattr(self, key, value)
|
|
462
|
+
|
|
463
|
+
# Update model state
|
|
464
|
+
if not hasattr(self, 'model') or self.model is None:
|
|
465
|
+
raise ValueError("Model not initialized. Please initialize model before loading weights.")
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
469
|
+
except Exception as e:
|
|
470
|
+
raise ValueError(f"Failed to load model state dict: {str(e)}")
|
|
471
|
+
|
|
472
|
+
# Update training state
|
|
473
|
+
self.fitting_epoch = checkpoint.get("fitting_epoch", 0)
|
|
474
|
+
self.fitting_loss = checkpoint.get("fitting_loss", float("inf"))
|
|
475
|
+
self.model_name = checkpoint['model_name']
|
|
476
|
+
self.is_fitted_ = True
|
|
477
|
+
|
|
478
|
+
# Move model to correct device
|
|
479
|
+
self.model = self.model.to(self.device)
|
|
480
|
+
|
|
481
|
+
print(f"Model successfully loaded from {'repository' if repo else 'local path'}")
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
raise ValueError(f"Error loading model checkpoint: {str(e)}")
|
|
486
|
+
|
|
487
|
+
finally:
|
|
488
|
+
# Clean up any temporary files
|
|
489
|
+
if repo is not None and os.path.exists(downloaded_path) and downloaded_path != path:
|
|
490
|
+
try:
|
|
491
|
+
os.remove(downloaded_path)
|
|
492
|
+
except Exception:
|
|
493
|
+
pass
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# The code is from torch_scatter: https://github.com/rusty1s/pytorch_scatter/blob/1.3.0/torch_scatter/add.py
|
|
2
|
+
|
|
3
|
+
from itertools import repeat
|
|
4
|
+
|
|
5
|
+
def maybe_dim_size(index, dim_size=None):
|
|
6
|
+
if dim_size is not None:
|
|
7
|
+
return dim_size
|
|
8
|
+
return index.max().item() + 1 if index.numel() > 0 else 0
|
|
9
|
+
|
|
10
|
+
def gen(src, index, dim=-1, out=None, dim_size=None, fill_value=0):
|
|
11
|
+
dim = range(src.dim())[dim] # Get real dim value.
|
|
12
|
+
|
|
13
|
+
# Automatically expand index tensor to the right dimensions.
|
|
14
|
+
if index.dim() == 1:
|
|
15
|
+
index_size = list(repeat(1, src.dim()))
|
|
16
|
+
index_size[dim] = src.size(dim)
|
|
17
|
+
index = index.view(index_size).expand_as(src)
|
|
18
|
+
|
|
19
|
+
# Generate output tensor if not given.
|
|
20
|
+
if out is None:
|
|
21
|
+
out_size = list(src.size())
|
|
22
|
+
dim_size = maybe_dim_size(index, dim_size)
|
|
23
|
+
out_size[dim] = dim_size
|
|
24
|
+
out = src.new_full(out_size, fill_value)
|
|
25
|
+
|
|
26
|
+
return src, out, index, dim
|
|
27
|
+
|
|
28
|
+
def scatter_add(src, index, dim=-1, out=None, dim_size=None, fill_value=0):
|
|
29
|
+
r"""
|
|
30
|
+
|
|
|
31
|
+
|
|
32
|
+
.. image:: https://raw.githubusercontent.com/rusty1s/pytorch_scatter/
|
|
33
|
+
master/docs/source/_figures/add.svg?sanitize=true
|
|
34
|
+
:align: center
|
|
35
|
+
:width: 400px
|
|
36
|
+
|
|
37
|
+
|
|
|
38
|
+
|
|
39
|
+
Sums all values from the :attr:`src` tensor into :attr:`out` at the indices
|
|
40
|
+
specified in the :attr:`index` tensor along a given axis :attr:`dim`. For
|
|
41
|
+
each value in :attr:`src`, its output index is specified by its index in
|
|
42
|
+
:attr:`input` for dimensions outside of :attr:`dim` and by the
|
|
43
|
+
corresponding value in :attr:`index` for dimension :attr:`dim`. If
|
|
44
|
+
multiple indices reference the same location, their **contributions add**.
|
|
45
|
+
|
|
46
|
+
Formally, if :attr:`src` and :attr:`index` are n-dimensional tensors with
|
|
47
|
+
size :math:`(x_0, ..., x_{i-1}, x_i, x_{i+1}, ..., x_{n-1})` and
|
|
48
|
+
:attr:`dim` = `i`, then :attr:`out` must be an n-dimensional tensor with
|
|
49
|
+
size :math:`(x_0, ..., x_{i-1}, y, x_{i+1}, ..., x_{n-1})`. Moreover, the
|
|
50
|
+
values of :attr:`index` must be between `0` and `out.size(dim) - 1`.
|
|
51
|
+
|
|
52
|
+
For one-dimensional tensors, the operation computes
|
|
53
|
+
|
|
54
|
+
.. math::
|
|
55
|
+
\mathrm{out}_i = \mathrm{out}_i + \sum_j \mathrm{src}_j
|
|
56
|
+
|
|
57
|
+
where :math:`\sum_j` is over :math:`j` such that
|
|
58
|
+
:math:`\mathrm{index}_j = i`.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
src (Tensor): The source tensor.
|
|
62
|
+
index (LongTensor): The indices of elements to scatter.
|
|
63
|
+
dim (int, optional): The axis along which to index.
|
|
64
|
+
(default: :obj:`-1`)
|
|
65
|
+
out (Tensor, optional): The destination tensor. (default: :obj:`None`)
|
|
66
|
+
dim_size (int, optional): If :attr:`out` is not given, automatically
|
|
67
|
+
create output with size :attr:`dim_size` at dimension :attr:`dim`.
|
|
68
|
+
If :attr:`dim_size` is not given, a minimal sized output tensor is
|
|
69
|
+
returned. (default: :obj:`None`)
|
|
70
|
+
fill_value (int, optional): If :attr:`out` is not given, automatically
|
|
71
|
+
fill output tensor with :attr:`fill_value`. (default: :obj:`0`)
|
|
72
|
+
|
|
73
|
+
:rtype: :class:`Tensor`
|
|
74
|
+
|
|
75
|
+
.. testsetup::
|
|
76
|
+
|
|
77
|
+
import torch
|
|
78
|
+
|
|
79
|
+
.. testcode::
|
|
80
|
+
|
|
81
|
+
from torch_scatter import scatter_add
|
|
82
|
+
|
|
83
|
+
src = torch.Tensor([[2, 0, 1, 4, 3], [0, 2, 1, 3, 4]])
|
|
84
|
+
index = torch.tensor([[4, 5, 4, 2, 3], [0, 0, 2, 2, 1]])
|
|
85
|
+
out = src.new_zeros((2, 6))
|
|
86
|
+
|
|
87
|
+
out = scatter_add(src, index, out=out)
|
|
88
|
+
|
|
89
|
+
print(out)
|
|
90
|
+
|
|
91
|
+
.. testoutput::
|
|
92
|
+
|
|
93
|
+
tensor([[0., 0., 4., 3., 3., 0.],
|
|
94
|
+
[2., 4., 4., 0., 0., 0.]])
|
|
95
|
+
"""
|
|
96
|
+
src, out, index, dim = gen(src, index, dim, out, dim_size, fill_value)
|
|
97
|
+
return out.scatter_add_(dim, index, src)
|