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,285 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Callable, Literal, Type
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch_geometric.loader import DataLoader
|
|
10
|
+
from torch_geometric.data import Data
|
|
11
|
+
|
|
12
|
+
from .model import GNN
|
|
13
|
+
from ..constant import GNN_ENCODER_MODELS, GNN_ENCODER_READOUTS, GNN_ENCODER_PARAMS
|
|
14
|
+
from ...base import BaseMolecularEncoder
|
|
15
|
+
from ...utils import graph_from_smiles
|
|
16
|
+
|
|
17
|
+
ALLOWABLE_ENCODER_MODELS = GNN_ENCODER_MODELS
|
|
18
|
+
ALLOWABLE_ENCODER_READOUTS = GNN_ENCODER_READOUTS
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class EdgePredMolecularEncoder(BaseMolecularEncoder):
|
|
22
|
+
"""This encoder implements a GNN-based model for molecular representation learning
|
|
23
|
+
using the edge prediction.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
- Paper: Strategies for Pre-training Graph Neural Networks (ICLR 2020) https://arxiv.org/abs/1905.12265
|
|
28
|
+
- Code: https://github.com/snap-stanford/pretrain-gnns/tree/master/chem
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
num_layer : int, default=5
|
|
33
|
+
Number of GNN layers.
|
|
34
|
+
hidden_size : int, default=300
|
|
35
|
+
Dimension of hidden node features.
|
|
36
|
+
drop_ratio : float, default=0.5
|
|
37
|
+
Dropout probability.
|
|
38
|
+
norm_layer : str, default="batch_norm"
|
|
39
|
+
Type of normalization layer to use. One of ["batch_norm", "layer_norm", "instance_norm", "graph_norm", "size_norm", "pair_norm"].
|
|
40
|
+
encoder_type : str, default="gin-virtual"
|
|
41
|
+
Type of GNN architecture to use.
|
|
42
|
+
readout : str, default="sum"
|
|
43
|
+
Method for aggregating node features to obtain graph-level representations.
|
|
44
|
+
batch_size : int, default=128
|
|
45
|
+
Number of samples per batch for training.
|
|
46
|
+
epochs : int, default=500
|
|
47
|
+
Maximum number of training epochs.
|
|
48
|
+
learning_rate : float, default=0.001
|
|
49
|
+
Learning rate for optimizer.
|
|
50
|
+
grad_clip_value : float, optional
|
|
51
|
+
Maximum norm of gradients for gradient clipping.
|
|
52
|
+
weight_decay : float, default=0.0
|
|
53
|
+
L2 regularization strength.
|
|
54
|
+
use_lr_scheduler : bool, default=False
|
|
55
|
+
Whether to use learning rate scheduler.
|
|
56
|
+
scheduler_factor : float, default=0.5
|
|
57
|
+
Factor by which to reduce learning rate when plateau is reached.
|
|
58
|
+
scheduler_patience : int, default=5
|
|
59
|
+
Number of epochs with no improvement after which learning rate will be reduced.
|
|
60
|
+
verbose : bool, default=False
|
|
61
|
+
Whether to print progress information during training.
|
|
62
|
+
"""
|
|
63
|
+
# Model parameters
|
|
64
|
+
num_layer: int = 5
|
|
65
|
+
hidden_size: int = 300
|
|
66
|
+
drop_ratio: float = 0.5
|
|
67
|
+
norm_layer: str = "batch_norm"
|
|
68
|
+
|
|
69
|
+
encoder_type: str = "gin-virtual"
|
|
70
|
+
readout: str = "sum"
|
|
71
|
+
|
|
72
|
+
# Training parameters
|
|
73
|
+
batch_size: int = 128
|
|
74
|
+
epochs: int = 500
|
|
75
|
+
learning_rate: float = 0.001
|
|
76
|
+
grad_clip_value: Optional[float] = None
|
|
77
|
+
weight_decay: float = 0.0
|
|
78
|
+
|
|
79
|
+
# Scheduler parameters
|
|
80
|
+
use_lr_scheduler: bool = False
|
|
81
|
+
scheduler_factor: float = 0.5
|
|
82
|
+
scheduler_patience: int = 5
|
|
83
|
+
|
|
84
|
+
# Other parameters
|
|
85
|
+
verbose: bool = False
|
|
86
|
+
model_name: str = "EdgePredMolecularEncoder"
|
|
87
|
+
|
|
88
|
+
# Non-init fields
|
|
89
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
90
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
91
|
+
model_class: Type[GNN] = field(default=GNN, init=False)
|
|
92
|
+
|
|
93
|
+
def __post_init__(self):
|
|
94
|
+
"""Initialize the model after dataclass initialization."""
|
|
95
|
+
super().__post_init__()
|
|
96
|
+
if self.encoder_type not in ALLOWABLE_ENCODER_MODELS:
|
|
97
|
+
raise ValueError(f"Invalid encoder_model: {self.encoder_type}. Currently only {ALLOWABLE_ENCODER_MODELS} are supported.")
|
|
98
|
+
if self.readout not in ALLOWABLE_ENCODER_READOUTS:
|
|
99
|
+
raise ValueError(f"Invalid encoder_readout: {self.readout}. Currently only {ALLOWABLE_ENCODER_READOUTS} are supported.")
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _get_param_names() -> List[str]:
|
|
103
|
+
return GNN_ENCODER_PARAMS.copy()
|
|
104
|
+
|
|
105
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
106
|
+
params = {
|
|
107
|
+
"num_layer": self.num_layer,
|
|
108
|
+
"hidden_size": self.hidden_size,
|
|
109
|
+
"encoder_type": self.encoder_type,
|
|
110
|
+
"drop_ratio": self.drop_ratio,
|
|
111
|
+
"norm_layer": self.norm_layer,
|
|
112
|
+
"readout": self.readout,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if checkpoint is not None:
|
|
116
|
+
if "hyperparameters" not in checkpoint:
|
|
117
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
118
|
+
params = {k: checkpoint["hyperparameters"].get(k, v) for k, v in params.items()}
|
|
119
|
+
|
|
120
|
+
return params
|
|
121
|
+
|
|
122
|
+
def _convert_to_pytorch_data(self, X):
|
|
123
|
+
"""Convert numpy arrays to PyTorch Geometric data format.
|
|
124
|
+
"""
|
|
125
|
+
if self.verbose:
|
|
126
|
+
iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
|
|
127
|
+
else:
|
|
128
|
+
iterator = enumerate(X)
|
|
129
|
+
|
|
130
|
+
pyg_graph_list = []
|
|
131
|
+
for idx, smiles_or_mol in iterator:
|
|
132
|
+
graph = graph_from_smiles(smiles_or_mol, None)
|
|
133
|
+
g = Data()
|
|
134
|
+
g.num_nodes = graph["num_nodes"]
|
|
135
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
136
|
+
|
|
137
|
+
del graph["num_nodes"]
|
|
138
|
+
del graph["edge_index"]
|
|
139
|
+
|
|
140
|
+
if graph["edge_feat"] is not None:
|
|
141
|
+
g.edge_attr = torch.from_numpy(graph["edge_feat"])
|
|
142
|
+
del graph["edge_feat"]
|
|
143
|
+
|
|
144
|
+
if graph["node_feat"] is not None:
|
|
145
|
+
g.x = torch.from_numpy(graph["node_feat"])
|
|
146
|
+
del graph["node_feat"]
|
|
147
|
+
|
|
148
|
+
pyg_graph_list.append(g)
|
|
149
|
+
|
|
150
|
+
return pyg_graph_list
|
|
151
|
+
|
|
152
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
153
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
|
|
157
|
+
scheduler = None
|
|
158
|
+
if self.use_lr_scheduler:
|
|
159
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
160
|
+
optimizer,
|
|
161
|
+
mode="min",
|
|
162
|
+
factor=self.scheduler_factor,
|
|
163
|
+
patience=self.scheduler_patience,
|
|
164
|
+
min_lr=1e-6,
|
|
165
|
+
cooldown=0,
|
|
166
|
+
eps=1e-8,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return optimizer, scheduler
|
|
170
|
+
|
|
171
|
+
def fit(
|
|
172
|
+
self,
|
|
173
|
+
X_train: List[str],
|
|
174
|
+
) -> "EdgePredMolecularEncoder":
|
|
175
|
+
"""Fit the model to the training data with optional validation set.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
X_train : List[str]
|
|
180
|
+
Training set input molecular structures as SMILES strings
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
self : EdgePredMolecularEncoder
|
|
184
|
+
Fitted estimator
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
self._initialize_model(self.model_class)
|
|
188
|
+
self.model.initialize_parameters()
|
|
189
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
190
|
+
|
|
191
|
+
# Prepare datasets and loaders
|
|
192
|
+
X_train, _ = self._validate_inputs(X_train, return_rdkit_mol=True)
|
|
193
|
+
train_dataset = self._convert_to_pytorch_data(X_train)
|
|
194
|
+
train_loader = DataLoader(
|
|
195
|
+
train_dataset,
|
|
196
|
+
batch_size=self.batch_size,
|
|
197
|
+
shuffle=True,
|
|
198
|
+
num_workers=0
|
|
199
|
+
)
|
|
200
|
+
self.fitting_loss = []
|
|
201
|
+
|
|
202
|
+
for epoch in range(self.epochs):
|
|
203
|
+
# Training phase
|
|
204
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch)
|
|
205
|
+
self.fitting_loss.append(np.mean(train_losses))
|
|
206
|
+
if scheduler:
|
|
207
|
+
scheduler.step(np.mean(train_losses))
|
|
208
|
+
|
|
209
|
+
self.fitting_epoch = epoch
|
|
210
|
+
self.is_fitted_ = True
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
214
|
+
"""Training logic for one epoch.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
train_loader: DataLoader containing training data
|
|
218
|
+
optimizer: Optimizer instance for model parameter updates
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
list: List of loss values for each training step
|
|
222
|
+
"""
|
|
223
|
+
self.model.train()
|
|
224
|
+
losses = []
|
|
225
|
+
|
|
226
|
+
iterator = (
|
|
227
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
228
|
+
if self.verbose
|
|
229
|
+
else train_loader
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
for batch in iterator:
|
|
233
|
+
batch = batch.to(self.device)
|
|
234
|
+
optimizer.zero_grad()
|
|
235
|
+
loss = self.model.compute_loss(batch)
|
|
236
|
+
loss.backward()
|
|
237
|
+
if self.grad_clip_value is not None:
|
|
238
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
239
|
+
optimizer.step()
|
|
240
|
+
losses.append(loss.item())
|
|
241
|
+
|
|
242
|
+
# Update progress bar if using tqdm
|
|
243
|
+
if self.verbose:
|
|
244
|
+
iterator.set_postfix({"Epoch": f"{epoch}", "Loss": f"{loss.item():.4f}"})
|
|
245
|
+
|
|
246
|
+
return losses
|
|
247
|
+
|
|
248
|
+
def encode(self, X: List[str], return_type: Literal["np", "pt"] = "pt") -> Union[np.ndarray, torch.Tensor]:
|
|
249
|
+
"""Encode molecules into vector representations.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
X : List[str]
|
|
254
|
+
List of SMILES strings
|
|
255
|
+
return_type : Literal["np", "pt"], default="pt"
|
|
256
|
+
Return type of the representations
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
representations : ndarray or torch.Tensor
|
|
261
|
+
Molecular representations
|
|
262
|
+
"""
|
|
263
|
+
self._check_is_fitted()
|
|
264
|
+
|
|
265
|
+
# Convert to PyTorch Geometric format and create loader
|
|
266
|
+
X, _ = self._validate_inputs(X, return_rdkit_mol=True)
|
|
267
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
268
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
269
|
+
|
|
270
|
+
if self.model is None:
|
|
271
|
+
raise RuntimeError("Model not initialized")
|
|
272
|
+
|
|
273
|
+
# Generate encodings
|
|
274
|
+
self.model = self.model.to(self.device)
|
|
275
|
+
self.model.eval()
|
|
276
|
+
encodings = []
|
|
277
|
+
with torch.no_grad():
|
|
278
|
+
for batch in tqdm(loader, disable=not self.verbose):
|
|
279
|
+
batch = batch.to(self.device)
|
|
280
|
+
out = self.model(batch)
|
|
281
|
+
encodings.append(out["graph"].cpu())
|
|
282
|
+
|
|
283
|
+
# Concatenate and convert to requested format
|
|
284
|
+
encodings = torch.cat(encodings, dim=0)
|
|
285
|
+
return encodings if return_type == "pt" else encodings.numpy()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import copy
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn.functional as F
|
|
5
|
+
from torch_geometric.data import Data
|
|
6
|
+
|
|
7
|
+
class DataLoaderMaskingPred(torch.utils.data.DataLoader):
|
|
8
|
+
r"""Data loader which merges data objects from a
|
|
9
|
+
:class:`torch_geometric.data.dataset` to a mini-batch.
|
|
10
|
+
Args:
|
|
11
|
+
dataset (Dataset): The dataset from which to load the data.
|
|
12
|
+
batch_size (int, optional): How may samples per batch to load.
|
|
13
|
+
(default: :obj:`1`)
|
|
14
|
+
shuffle (bool, optional): If set to :obj:`True`, the data will be
|
|
15
|
+
reshuffled at every epoch (default: :obj:`True`)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, dataset, batch_size=1, shuffle=True, mask_rate=0.0, mask_edge=False, **kwargs):
|
|
19
|
+
self._transform = MaskAtom(num_atom_type = 119, num_edge_type = 5, mask_rate = mask_rate, mask_edge=mask_edge)
|
|
20
|
+
super(DataLoaderMaskingPred, self).__init__(
|
|
21
|
+
dataset,
|
|
22
|
+
batch_size,
|
|
23
|
+
shuffle,
|
|
24
|
+
collate_fn=self.collate_fn,
|
|
25
|
+
**kwargs)
|
|
26
|
+
|
|
27
|
+
def collate_fn(self, batches):
|
|
28
|
+
batchs = [self._transform(x) for x in batches]
|
|
29
|
+
return BatchMasking.from_data_list(batchs)
|
|
30
|
+
|
|
31
|
+
class MaskAtom:
|
|
32
|
+
def __init__(self, num_atom_type, num_edge_type, mask_rate, mask_edge=True):
|
|
33
|
+
"""
|
|
34
|
+
Randomly masks an atom, and optionally masks edges connecting to it.
|
|
35
|
+
The mask atom type index is num_possible_atom_type
|
|
36
|
+
The mask edge type index in num_possible_edge_type
|
|
37
|
+
:param num_atom_type:
|
|
38
|
+
:param num_edge_type:
|
|
39
|
+
:param mask_rate: % of atoms to be masked
|
|
40
|
+
:param mask_edge: If True, also mask the edges that connect to the
|
|
41
|
+
masked atoms
|
|
42
|
+
"""
|
|
43
|
+
self.num_atom_type = num_atom_type
|
|
44
|
+
self.num_edge_type = num_edge_type
|
|
45
|
+
self.mask_rate = mask_rate
|
|
46
|
+
self.mask_edge = mask_edge
|
|
47
|
+
# self.num_chirality_tag = 3
|
|
48
|
+
# self.num_bond_direction = 3
|
|
49
|
+
|
|
50
|
+
def __call__(self, input_data, masked_atom_indices=None):
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
:param data: pytorch geometric data object. Assume that the edge
|
|
54
|
+
ordering is the default pytorch geometric ordering, where the two
|
|
55
|
+
directions of a single edge occur in pairs.
|
|
56
|
+
Eg. data.edge_index = tensor([[0, 1, 1, 2, 2, 3],
|
|
57
|
+
[1, 0, 2, 1, 3, 2]])
|
|
58
|
+
:param masked_atom_indices: If None, then randomly samples num_atoms
|
|
59
|
+
* mask rate number of atom indices
|
|
60
|
+
Otherwise a list of atom idx that sets the atoms to be masked (for
|
|
61
|
+
debugging only)
|
|
62
|
+
:return: None, Creates new attributes in original data object:
|
|
63
|
+
data.mask_node_idx
|
|
64
|
+
data.mask_node_label
|
|
65
|
+
data.mask_edge_idx
|
|
66
|
+
data.mask_edge_label
|
|
67
|
+
"""
|
|
68
|
+
data = copy.deepcopy(input_data)
|
|
69
|
+
if masked_atom_indices == None:
|
|
70
|
+
# sample x distinct atoms to be masked, based on mask rate. But
|
|
71
|
+
# will sample at least 1 atom
|
|
72
|
+
num_atoms = data.x.size()[0]
|
|
73
|
+
sample_size = int(num_atoms * self.mask_rate + 1)
|
|
74
|
+
masked_atom_indices = random.sample(range(num_atoms), sample_size)
|
|
75
|
+
|
|
76
|
+
# create mask node label by copying atom feature of mask atom
|
|
77
|
+
mask_node_labels_list = []
|
|
78
|
+
for atom_idx in masked_atom_indices:
|
|
79
|
+
mask_node_labels_list.append(data.x[atom_idx].view(1, -1))
|
|
80
|
+
data.mask_node_label = torch.cat(mask_node_labels_list, dim=0)
|
|
81
|
+
data.masked_atom_indices = torch.tensor(masked_atom_indices)
|
|
82
|
+
|
|
83
|
+
# ----------- graphMAE -----------
|
|
84
|
+
atom_type = F.one_hot(data.mask_node_label[:, 0], num_classes=self.num_atom_type).float()
|
|
85
|
+
# atom_chirality = F.one_hot(data.mask_node_label[:, 1], num_classes=self.num_chirality_tag).float()
|
|
86
|
+
# data.node_attr_label = torch.cat((atom_type,atom_chirality), dim=1)
|
|
87
|
+
data.node_attr_label = atom_type
|
|
88
|
+
|
|
89
|
+
# modify the original node feature of the masked node
|
|
90
|
+
for atom_idx in masked_atom_indices:
|
|
91
|
+
feat_dim = data.x[atom_idx].shape[0]
|
|
92
|
+
data.x[atom_idx] = torch.zeros(feat_dim)
|
|
93
|
+
data.x[atom_idx][0] = self.num_atom_type - 1
|
|
94
|
+
|
|
95
|
+
if self.mask_edge:
|
|
96
|
+
# create mask edge labels by copying edge features of edges that are bonded to
|
|
97
|
+
# mask atoms
|
|
98
|
+
connected_edge_indices = []
|
|
99
|
+
for bond_idx, (u, v) in enumerate(data.edge_index.cpu().numpy().T):
|
|
100
|
+
for atom_idx in masked_atom_indices:
|
|
101
|
+
if atom_idx in set((u, v)) and \
|
|
102
|
+
bond_idx not in connected_edge_indices:
|
|
103
|
+
connected_edge_indices.append(bond_idx)
|
|
104
|
+
|
|
105
|
+
if len(connected_edge_indices) > 0:
|
|
106
|
+
# create mask edge labels by copying bond features of the bonds connected to
|
|
107
|
+
# the mask atoms
|
|
108
|
+
mask_edge_labels_list = []
|
|
109
|
+
for bond_idx in connected_edge_indices[::2]: # because the
|
|
110
|
+
# edge ordering is such that two directions of a single
|
|
111
|
+
# edge occur in pairs, so to get the unique undirected
|
|
112
|
+
# edge indices, we take every 2nd edge index from list
|
|
113
|
+
mask_edge_labels_list.append(
|
|
114
|
+
data.edge_attr[bond_idx].view(1, -1))
|
|
115
|
+
|
|
116
|
+
data.mask_edge_label = torch.cat(mask_edge_labels_list, dim=0)
|
|
117
|
+
# modify the original bond features of the bonds connected to the mask atoms
|
|
118
|
+
for bond_idx in connected_edge_indices:
|
|
119
|
+
feat_dim = data.edge_attr[bond_idx].shape[0]
|
|
120
|
+
data.edge_attr[bond_idx] = torch.zeros(feat_dim)
|
|
121
|
+
data.edge_attr[bond_idx][0] = self.num_edge_type - 1
|
|
122
|
+
# print('feat_dim', feat_dim, 'data.edge_attr[bond_idx].shape', data.edge_attr[bond_idx].shape)
|
|
123
|
+
|
|
124
|
+
data.connected_edge_indices = torch.tensor(
|
|
125
|
+
connected_edge_indices[::2])
|
|
126
|
+
else:
|
|
127
|
+
data.mask_edge_label = torch.empty((0, 2)).to(torch.int64)
|
|
128
|
+
data.connected_edge_indices = torch.tensor(
|
|
129
|
+
connected_edge_indices).to(torch.int64)
|
|
130
|
+
|
|
131
|
+
edge_type = F.one_hot(data.mask_edge_label[:, 0], num_classes=self.num_edge_type).float()
|
|
132
|
+
# bond_direction = F.one_hot(data.mask_edge_label[:, 1], num_classes=self.num_bond_direction).float()
|
|
133
|
+
data.edge_attr_label = edge_type
|
|
134
|
+
# data.edge_attr_label = edge_type
|
|
135
|
+
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
def __repr__(self):
|
|
139
|
+
return '{}(num_atom_type={}, num_edge_type={}, mask_rate={}, mask_edge={})'.format(
|
|
140
|
+
self.__class__.__name__, self.num_atom_type, self.num_edge_type,
|
|
141
|
+
self.mask_rate, self.mask_edge)
|
|
142
|
+
|
|
143
|
+
class BatchMasking(Data):
|
|
144
|
+
r"""A plain old python object modeling a batch of graphs as one big
|
|
145
|
+
(dicconnected) graph. With :class:`torch_geometric.data.Data` being the
|
|
146
|
+
base class, all its methods can also be used here.
|
|
147
|
+
In addition, single graphs can be reconstructed via the assignment vector
|
|
148
|
+
:obj:`batch`, which maps each node to its respective graph identifier.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, batch=None, **kwargs):
|
|
152
|
+
super(BatchMasking, self).__init__(**kwargs)
|
|
153
|
+
self.batch = batch
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def from_data_list(data_list):
|
|
157
|
+
r"""Constructs a batch object from a python list holding
|
|
158
|
+
:class:`torch_geometric.data.Data` objects.
|
|
159
|
+
The assignment vector :obj:`batch` is created on the fly."""
|
|
160
|
+
keys = [set(data.keys()) for data in data_list]
|
|
161
|
+
keys = list(set.union(*keys))
|
|
162
|
+
assert 'batch' not in keys
|
|
163
|
+
|
|
164
|
+
batch = BatchMasking()
|
|
165
|
+
|
|
166
|
+
for key in keys:
|
|
167
|
+
batch[key] = []
|
|
168
|
+
batch.batch = []
|
|
169
|
+
|
|
170
|
+
cumsum_node = 0
|
|
171
|
+
cumsum_edge = 0
|
|
172
|
+
|
|
173
|
+
for i, data in enumerate(data_list):
|
|
174
|
+
num_nodes = data.num_nodes
|
|
175
|
+
batch.batch.append(torch.full((num_nodes, ), i, dtype=torch.long))
|
|
176
|
+
for key in data.keys():
|
|
177
|
+
item = data[key]
|
|
178
|
+
if key in ['edge_index', 'masked_atom_indices']:
|
|
179
|
+
item = item + cumsum_node
|
|
180
|
+
elif key == 'connected_edge_indices':
|
|
181
|
+
item = item + cumsum_edge
|
|
182
|
+
batch[key].append(item)
|
|
183
|
+
|
|
184
|
+
cumsum_node += num_nodes
|
|
185
|
+
cumsum_edge += data.edge_index.shape[1]
|
|
186
|
+
|
|
187
|
+
for key in keys:
|
|
188
|
+
batch[key] = torch.cat(
|
|
189
|
+
batch[key], dim=data_list[0].__cat_dim__(key, batch[key][0]))
|
|
190
|
+
batch.batch = torch.cat(batch.batch, dim=-1)
|
|
191
|
+
return batch.contiguous()
|
|
192
|
+
|
|
193
|
+
def cumsum(self, key, item):
|
|
194
|
+
r"""If :obj:`True`, the attribute :obj:`key` with content :obj:`item`
|
|
195
|
+
should be added up cumulatively before concatenated together.
|
|
196
|
+
.. note::
|
|
197
|
+
This method is for internal use only, and should only be overridden
|
|
198
|
+
if the batch concatenation process is corrupted for a specific data
|
|
199
|
+
attribute.
|
|
200
|
+
"""
|
|
201
|
+
return key in ['edge_index', 'face', 'masked_atom_indices', 'connected_edge_indices']
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def num_graphs(self):
|
|
205
|
+
"""Returns the number of graphs in the batch."""
|
|
206
|
+
return self.batch[-1].item() + 1
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn as nn
|
|
5
|
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
|
|
6
|
+
import torch.nn.functional as F
|
|
7
|
+
|
|
8
|
+
from ...nn import GNN_node, GNN_node_Virtualnode
|
|
9
|
+
from ...nn.gnn import GINConv, GCNConv
|
|
10
|
+
from ...utils import init_weights
|
|
11
|
+
|
|
12
|
+
def sce_loss(x, y, alpha=1):
|
|
13
|
+
x = F.normalize(x, p=2, dim=-1)
|
|
14
|
+
y = F.normalize(y, p=2, dim=-1)
|
|
15
|
+
loss = (1 - (x * y).sum(dim=-1)).pow_(alpha)
|
|
16
|
+
loss = loss.mean()
|
|
17
|
+
return loss
|
|
18
|
+
|
|
19
|
+
class GNN(nn.Module):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
num_layer,
|
|
23
|
+
hidden_size,
|
|
24
|
+
drop_ratio=0.5,
|
|
25
|
+
norm_layer="batch_norm",
|
|
26
|
+
encoder_type="gin-virtual",
|
|
27
|
+
readout="sum",
|
|
28
|
+
mask_edge=False,
|
|
29
|
+
predictor_type="gin",
|
|
30
|
+
):
|
|
31
|
+
super(GNN, self).__init__()
|
|
32
|
+
gnn_name = encoder_type.split("-")[0]
|
|
33
|
+
self.num_atom_type = 119
|
|
34
|
+
self.num_edge_type = 5
|
|
35
|
+
self.hidden_size = hidden_size
|
|
36
|
+
|
|
37
|
+
encoder_params = {
|
|
38
|
+
"num_layer": num_layer,
|
|
39
|
+
"hidden_size": hidden_size,
|
|
40
|
+
"JK": "last",
|
|
41
|
+
"drop_ratio": drop_ratio,
|
|
42
|
+
"residual": True,
|
|
43
|
+
"gnn_name": gnn_name,
|
|
44
|
+
"norm_layer": norm_layer
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Choose encoder type based on encoder_model
|
|
48
|
+
encoder_class = GNN_node_Virtualnode if "virtual" in encoder_type else GNN_node
|
|
49
|
+
self.graph_encoder = encoder_class(**encoder_params)
|
|
50
|
+
pooling_funcs = {
|
|
51
|
+
"sum": global_add_pool,
|
|
52
|
+
"mean": global_mean_pool,
|
|
53
|
+
"max": global_max_pool
|
|
54
|
+
}
|
|
55
|
+
self.pool = pooling_funcs.get(readout)
|
|
56
|
+
if self.pool is None:
|
|
57
|
+
raise ValueError(f"Invalid graph pooling type {readout}.")
|
|
58
|
+
|
|
59
|
+
self.atom_predictor = Predictor(hidden_size, self.num_atom_type, nn_type=predictor_type)
|
|
60
|
+
self.mask_edge = mask_edge
|
|
61
|
+
if mask_edge:
|
|
62
|
+
self.bond_predictor = Predictor(hidden_size, self.num_edge_type, nn_type="linear")
|
|
63
|
+
|
|
64
|
+
def initialize_parameters(self, seed=None):
|
|
65
|
+
"""
|
|
66
|
+
Randomly initialize all model parameters using the init_weights function.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
seed (int, optional): Random seed for reproducibility. Defaults to None.
|
|
70
|
+
"""
|
|
71
|
+
if seed is not None:
|
|
72
|
+
torch.manual_seed(seed)
|
|
73
|
+
|
|
74
|
+
# Initialize the main components
|
|
75
|
+
init_weights(self.graph_encoder)
|
|
76
|
+
init_weights(self.atom_predictor)
|
|
77
|
+
if self.mask_edge:
|
|
78
|
+
init_weights(self.bond_predictor)
|
|
79
|
+
|
|
80
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
81
|
+
def reset_parameters(module):
|
|
82
|
+
if hasattr(module, 'reset_parameters'):
|
|
83
|
+
module.reset_parameters()
|
|
84
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
85
|
+
init_weights(module)
|
|
86
|
+
|
|
87
|
+
self.apply(reset_parameters)
|
|
88
|
+
|
|
89
|
+
def compute_loss(self, batched_data, alpha_l=1):
|
|
90
|
+
criterion = partial(sce_loss, alpha=alpha_l)
|
|
91
|
+
|
|
92
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
93
|
+
|
|
94
|
+
node_attr_label = batched_data.node_attr_label
|
|
95
|
+
masked_node_indices = batched_data.masked_atom_indices
|
|
96
|
+
pred_node = self.atom_predictor(h_node, batched_data.edge_index, batched_data.edge_attr, masked_node_indices)
|
|
97
|
+
loss_atom = criterion(pred_node[masked_node_indices], node_attr_label.to(pred_node.dtype))
|
|
98
|
+
if self.mask_edge:
|
|
99
|
+
masked_edge_index = batched_data.edge_index[:, batched_data.connected_edge_indices]
|
|
100
|
+
edge_rep = h_node[masked_edge_index[0]] + h_node[masked_edge_index[1]]
|
|
101
|
+
pred_edge = self.bond_predictor(edge_rep)
|
|
102
|
+
loss_edge = criterion(pred_edge, batched_data.edge_attr_label.to(pred_edge.dtype))
|
|
103
|
+
else:
|
|
104
|
+
loss_edge = torch.tensor(0.0)
|
|
105
|
+
return loss_atom, loss_edge
|
|
106
|
+
|
|
107
|
+
def forward(self, batched_data):
|
|
108
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
109
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
110
|
+
return {"graph": h_rep, "node": h_node}
|
|
111
|
+
|
|
112
|
+
class Predictor(torch.nn.Module):
|
|
113
|
+
def __init__(self, hidden_size, output_size, nn_type = "gin"):
|
|
114
|
+
super().__init__()
|
|
115
|
+
self._dec_type = nn_type
|
|
116
|
+
if "gin" in nn_type:
|
|
117
|
+
self.conv = GINConv(hidden_size, output_size)
|
|
118
|
+
elif "gcn" in nn_type:
|
|
119
|
+
self.conv = GCNConv(hidden_size, output_size)
|
|
120
|
+
elif "linear" in nn_type:
|
|
121
|
+
self.dec = torch.nn.Linear(hidden_size, output_size)
|
|
122
|
+
else:
|
|
123
|
+
raise NotImplementedError(f"{nn_type}")
|
|
124
|
+
if "linear" not in nn_type:
|
|
125
|
+
self.activation = torch.nn.PReLU()
|
|
126
|
+
self.enc_to_dec = torch.nn.Linear(hidden_size, hidden_size, bias=False)
|
|
127
|
+
|
|
128
|
+
def forward(self, x, edge_index=None, edge_attr=None, mask_node_indices=None):
|
|
129
|
+
if self._dec_type == "linear":
|
|
130
|
+
out = self.dec(x)
|
|
131
|
+
else:
|
|
132
|
+
assert edge_index is not None and edge_attr is not None and mask_node_indices is not None
|
|
133
|
+
x = self.activation(x)
|
|
134
|
+
x = self.enc_to_dec(x)
|
|
135
|
+
x[mask_node_indices] = 0
|
|
136
|
+
out = self.conv(x, edge_index, edge_attr)
|
|
137
|
+
return out
|