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,323 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Literal, Type
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch_geometric.loader import DataLoader
|
|
8
|
+
from torch_geometric.data import Data
|
|
9
|
+
|
|
10
|
+
from .model import GNN
|
|
11
|
+
from .dataloader import DataLoaderMaskingPred
|
|
12
|
+
from ..constant import GNN_ENCODER_MODELS, GNN_ENCODER_READOUTS, GNN_ENCODER_PARAMS
|
|
13
|
+
from ...base import BaseMolecularEncoder
|
|
14
|
+
from ...utils import graph_from_smiles
|
|
15
|
+
|
|
16
|
+
ALLOWABLE_ENCODER_MODELS = GNN_ENCODER_MODELS
|
|
17
|
+
ALLOWABLE_ENCODER_READOUTS = GNN_ENCODER_READOUTS
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class GraphMAEMolecularEncoder(BaseMolecularEncoder):
|
|
21
|
+
"""GraphMAE: Self-Supervised Masked Graph Autoencoders
|
|
22
|
+
|
|
23
|
+
References
|
|
24
|
+
----------
|
|
25
|
+
- Paper: https://arxiv.org/abs/2205.10803
|
|
26
|
+
- Code: https://github.com/THUDM/GraphMAE/tree/main/chem
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
mask_rate : float, default=0.15
|
|
31
|
+
Fraction of nodes to mask during training.
|
|
32
|
+
mask_edge : bool, default=False
|
|
33
|
+
Whether to mask edges in addition to nodes.
|
|
34
|
+
predictor_type : str, default="gin"
|
|
35
|
+
Type of predictor network to use for reconstruction.
|
|
36
|
+
Options: ["gin", "gcn", "linear"]
|
|
37
|
+
|
|
38
|
+
num_layer : int, default=5
|
|
39
|
+
Number of message passing layers in the GNN.
|
|
40
|
+
hidden_size : int, default=300
|
|
41
|
+
Dimension of hidden node representations.
|
|
42
|
+
drop_ratio : float, default=0.5
|
|
43
|
+
Dropout probability.
|
|
44
|
+
norm_layer : str, default="batch_norm"
|
|
45
|
+
Type of normalization to use.
|
|
46
|
+
Options: ["batch_norm", "layer_norm", "instance_norm", "graph_norm", "size_norm", "pair_norm"]
|
|
47
|
+
|
|
48
|
+
encoder_type : str, default="gin-virtual"
|
|
49
|
+
Type of GNN encoder to use.
|
|
50
|
+
Options: ["gin-virtual", "gcn-virtual", "gin", "gcn"]
|
|
51
|
+
readout : str, default="sum"
|
|
52
|
+
Pooling method to use for graph-level representations.
|
|
53
|
+
Options: ["sum", "mean", "max"]
|
|
54
|
+
|
|
55
|
+
batch_size : int, default=128
|
|
56
|
+
Batch size for training and inference.
|
|
57
|
+
epochs : int, default=500
|
|
58
|
+
Number of training epochs.
|
|
59
|
+
learning_rate : float, default=0.001
|
|
60
|
+
Learning rate for optimizer.
|
|
61
|
+
grad_clip_value : Optional[float], default=None
|
|
62
|
+
Maximum norm of gradients for gradient clipping. No clipping if None.
|
|
63
|
+
weight_decay : float, default=0.0
|
|
64
|
+
L2 regularization factor.
|
|
65
|
+
|
|
66
|
+
use_lr_scheduler : bool, default=False
|
|
67
|
+
Whether to use a learning rate scheduler.
|
|
68
|
+
scheduler_factor : float, default=0.5
|
|
69
|
+
Factor by which to reduce learning rate when using scheduler.
|
|
70
|
+
scheduler_patience : int, default=5
|
|
71
|
+
Number of epochs with no improvement after which learning rate will be reduced.
|
|
72
|
+
|
|
73
|
+
verbose : bool, default=False
|
|
74
|
+
Whether to display progress bars and logs.
|
|
75
|
+
model_name : str, default="GraphMAEMolecularEncoder"
|
|
76
|
+
Name of the model.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
>>> from torch_molecule import GraphMAEMolecularEncoder
|
|
81
|
+
>>> encoder = GraphMAEMolecularEncoder(hidden_size=128, epochs=100)
|
|
82
|
+
>>> encoder.fit(["CC(=O)OC1=CC=CC=C1C(=O)O", "CCO", "C1=CC=CC=C1"])
|
|
83
|
+
>>> representations = encoder.encode(["CCO"])
|
|
84
|
+
"""
|
|
85
|
+
# Task related parameters
|
|
86
|
+
mask_rate: float = 0.15
|
|
87
|
+
mask_edge: bool = False # whether to mask edges
|
|
88
|
+
predictor_type: str = "gin" # one of ["gin", "gcn", "linear"]
|
|
89
|
+
|
|
90
|
+
# Model parameters
|
|
91
|
+
num_layer: int = 5
|
|
92
|
+
hidden_size: int = 300
|
|
93
|
+
drop_ratio: float = 0.5
|
|
94
|
+
norm_layer: str = "batch_norm" # one of ["batch_norm", "layer_norm", "instance_norm", "graph_norm", "size_norm", "pair_norm"]
|
|
95
|
+
|
|
96
|
+
encoder_type: str = "gin-virtual" # one of ["gin-virtual", "gcn-virtual", "gin", "gcn"]
|
|
97
|
+
readout: str = "sum" # one of ["sum", "mean", "max"]
|
|
98
|
+
|
|
99
|
+
# Training parameters
|
|
100
|
+
batch_size: int = 128
|
|
101
|
+
epochs: int = 500
|
|
102
|
+
learning_rate: float = 0.001
|
|
103
|
+
grad_clip_value: Optional[float] = None
|
|
104
|
+
weight_decay: float = 0.0
|
|
105
|
+
|
|
106
|
+
# Scheduler parameters
|
|
107
|
+
use_lr_scheduler: bool = False
|
|
108
|
+
scheduler_factor: float = 0.5 # if use_lr_scheduler is True
|
|
109
|
+
scheduler_patience: int = 5 # if use_lr_scheduler is True
|
|
110
|
+
|
|
111
|
+
# Other parameters
|
|
112
|
+
verbose: bool = False
|
|
113
|
+
model_name: str = "GraphMAEMolecularEncoder"
|
|
114
|
+
|
|
115
|
+
# Non-init fields
|
|
116
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
117
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
118
|
+
model_class: Type[GNN] = field(default=GNN, init=False)
|
|
119
|
+
|
|
120
|
+
def __post_init__(self):
|
|
121
|
+
"""Initialize the model after dataclass initialization."""
|
|
122
|
+
super().__post_init__()
|
|
123
|
+
if self.encoder_type not in ALLOWABLE_ENCODER_MODELS:
|
|
124
|
+
raise ValueError(f"Invalid encoder_model: {self.encoder_type}. Currently only {ALLOWABLE_ENCODER_MODELS} are supported.")
|
|
125
|
+
if self.readout not in ALLOWABLE_ENCODER_READOUTS:
|
|
126
|
+
raise ValueError(f"Invalid encoder_readout: {self.readout}. Currently only {ALLOWABLE_ENCODER_READOUTS} are supported.")
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _get_param_names() -> List[str]:
|
|
130
|
+
"""Get parameter names for the estimator.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
List[str]
|
|
135
|
+
List of parameter names that can be used for model configuration.
|
|
136
|
+
"""
|
|
137
|
+
return ["mask_rate", "mask_edge", "predictor_type"] + GNN_ENCODER_PARAMS.copy()
|
|
138
|
+
|
|
139
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
140
|
+
params = {
|
|
141
|
+
"num_layer": self.num_layer,
|
|
142
|
+
"hidden_size": self.hidden_size,
|
|
143
|
+
"drop_ratio": self.drop_ratio,
|
|
144
|
+
"norm_layer": self.norm_layer,
|
|
145
|
+
"readout": self.readout,
|
|
146
|
+
"encoder_type": self.encoder_type,
|
|
147
|
+
"predictor_type": self.predictor_type,
|
|
148
|
+
"mask_edge": self.mask_edge
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if checkpoint is not None:
|
|
152
|
+
if "hyperparameters" not in checkpoint:
|
|
153
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
154
|
+
hyperparameters = checkpoint["hyperparameters"]
|
|
155
|
+
params = {k: hyperparameters.get(k, v) for k, v in params.items()}
|
|
156
|
+
|
|
157
|
+
return params
|
|
158
|
+
|
|
159
|
+
def _convert_to_pytorch_data(self, X):
|
|
160
|
+
"""Convert numpy arrays to PyTorch Geometric data format.
|
|
161
|
+
"""
|
|
162
|
+
if self.verbose:
|
|
163
|
+
iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
|
|
164
|
+
else:
|
|
165
|
+
iterator = enumerate(X)
|
|
166
|
+
|
|
167
|
+
pyg_graph_list = []
|
|
168
|
+
for idx, smiles_or_mol in iterator:
|
|
169
|
+
graph = graph_from_smiles(smiles_or_mol, None)
|
|
170
|
+
g = Data()
|
|
171
|
+
# g.num_nodes = graph["num_nodes"]
|
|
172
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
173
|
+
|
|
174
|
+
del graph["num_nodes"]
|
|
175
|
+
del graph["edge_index"]
|
|
176
|
+
|
|
177
|
+
if graph["edge_feat"] is not None:
|
|
178
|
+
g.edge_attr = torch.from_numpy(graph["edge_feat"])
|
|
179
|
+
del graph["edge_feat"]
|
|
180
|
+
|
|
181
|
+
if graph["node_feat"] is not None:
|
|
182
|
+
g.x = torch.from_numpy(graph["node_feat"])
|
|
183
|
+
del graph["node_feat"]
|
|
184
|
+
|
|
185
|
+
pyg_graph_list.append(g)
|
|
186
|
+
|
|
187
|
+
return pyg_graph_list
|
|
188
|
+
|
|
189
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
190
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
191
|
+
"""
|
|
192
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
|
|
193
|
+
scheduler = None
|
|
194
|
+
if self.use_lr_scheduler:
|
|
195
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
196
|
+
optimizer,
|
|
197
|
+
mode="min",
|
|
198
|
+
factor=self.scheduler_factor,
|
|
199
|
+
patience=self.scheduler_patience,
|
|
200
|
+
min_lr=1e-6,
|
|
201
|
+
cooldown=0,
|
|
202
|
+
eps=1e-8,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return optimizer, scheduler
|
|
206
|
+
|
|
207
|
+
def fit(
|
|
208
|
+
self,
|
|
209
|
+
X_train: List[str],
|
|
210
|
+
) -> "GraphMAEMolecularEncoder":
|
|
211
|
+
"""Fit the model to the training data with optional validation set.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
X_train : List[str]
|
|
216
|
+
Training set input molecular structures as SMILES strings
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
self : GraphMAEMolecularEncoder
|
|
220
|
+
Fitted estimator
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
self._initialize_model(self.model_class)
|
|
224
|
+
self.model.initialize_parameters()
|
|
225
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
226
|
+
|
|
227
|
+
# Prepare datasets and loaders
|
|
228
|
+
X_train, _ = self._validate_inputs(X_train, return_rdkit_mol=True)
|
|
229
|
+
train_dataset = self._convert_to_pytorch_data(X_train)
|
|
230
|
+
|
|
231
|
+
train_loader = DataLoaderMaskingPred(
|
|
232
|
+
train_dataset,
|
|
233
|
+
batch_size=self.batch_size,
|
|
234
|
+
shuffle=True, num_workers = 0,
|
|
235
|
+
mask_rate=self.mask_rate,
|
|
236
|
+
mask_edge=self.mask_edge)
|
|
237
|
+
|
|
238
|
+
self.fitting_loss = []
|
|
239
|
+
|
|
240
|
+
for epoch in range(self.epochs):
|
|
241
|
+
# Training phase
|
|
242
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch)
|
|
243
|
+
self.fitting_loss.append(np.mean(train_losses))
|
|
244
|
+
if scheduler:
|
|
245
|
+
scheduler.step(np.mean(train_losses))
|
|
246
|
+
|
|
247
|
+
self.fitting_epoch = epoch
|
|
248
|
+
self.is_fitted_ = True
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
252
|
+
"""Training logic for one epoch.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
train_loader: DataLoader containing training data
|
|
256
|
+
optimizer: Optimizer instance for model parameter updates
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
list: List of loss values for each training step
|
|
260
|
+
"""
|
|
261
|
+
self.model.train()
|
|
262
|
+
losses = []
|
|
263
|
+
|
|
264
|
+
iterator = (
|
|
265
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
266
|
+
if self.verbose
|
|
267
|
+
else train_loader
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
for batch in iterator:
|
|
271
|
+
batch = batch.to(self.device)
|
|
272
|
+
optimizer.zero_grad()
|
|
273
|
+
loss_atom, loss_edge = self.model.compute_loss(batch)
|
|
274
|
+
loss = loss_atom + loss_edge
|
|
275
|
+
loss.backward()
|
|
276
|
+
if self.grad_clip_value is not None:
|
|
277
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
278
|
+
optimizer.step()
|
|
279
|
+
losses.append(loss.item())
|
|
280
|
+
|
|
281
|
+
if self.verbose:
|
|
282
|
+
iterator.set_postfix({"Epoch": f"{epoch}", "Loss": f"{loss.item():.4f}", "Loss_atom": f"{loss_atom.item():.4f}", "Loss_edge": f"{loss_edge.item():.4f}"})
|
|
283
|
+
|
|
284
|
+
return losses
|
|
285
|
+
|
|
286
|
+
def encode(self, X: List[str], return_type: Literal["np", "pt"] = "pt") -> Union[np.ndarray, torch.Tensor]:
|
|
287
|
+
"""Encode molecules into vector representations.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
X : List[str]
|
|
292
|
+
List of SMILES strings
|
|
293
|
+
return_type : Literal["np", "pt"], default="pt"
|
|
294
|
+
Return type of the representations
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
representations : ndarray or torch.Tensor
|
|
299
|
+
Molecular representations
|
|
300
|
+
"""
|
|
301
|
+
self._check_is_fitted()
|
|
302
|
+
|
|
303
|
+
# Convert to PyTorch Geometric format and create loader
|
|
304
|
+
X, _ = self._validate_inputs(X, return_rdkit_mol=True)
|
|
305
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
306
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
307
|
+
|
|
308
|
+
if self.model is None:
|
|
309
|
+
raise RuntimeError("Model not initialized")
|
|
310
|
+
|
|
311
|
+
# Generate encodings
|
|
312
|
+
self.model = self.model.to(self.device)
|
|
313
|
+
self.model.eval()
|
|
314
|
+
encodings = []
|
|
315
|
+
with torch.no_grad():
|
|
316
|
+
for batch in tqdm(loader, disable=not self.verbose):
|
|
317
|
+
batch = batch.to(self.device)
|
|
318
|
+
out = self.model(batch)
|
|
319
|
+
encodings.append(out["graph"].cpu())
|
|
320
|
+
|
|
321
|
+
# Concatenate and convert to requested format
|
|
322
|
+
encodings = torch.cat(encodings, dim=0)
|
|
323
|
+
return encodings if return_type == "pt" else encodings.numpy()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
def local_global_loss_(l_enc, g_enc, batch, measure):
|
|
7
|
+
'''
|
|
8
|
+
Args:
|
|
9
|
+
l: Local feature map.
|
|
10
|
+
g: Global features.
|
|
11
|
+
measure: Type of f-divergence. For use with mode `fd`
|
|
12
|
+
Returns:
|
|
13
|
+
torch.Tensor: Loss.
|
|
14
|
+
'''
|
|
15
|
+
num_graphs = g_enc.shape[0]
|
|
16
|
+
num_nodes = l_enc.shape[0]
|
|
17
|
+
|
|
18
|
+
pos_mask = torch.zeros((num_nodes, num_graphs)).cuda()
|
|
19
|
+
neg_mask = torch.ones((num_nodes, num_graphs)).cuda()
|
|
20
|
+
for nodeidx, graphidx in enumerate(batch):
|
|
21
|
+
pos_mask[nodeidx][graphidx] = 1.
|
|
22
|
+
neg_mask[nodeidx][graphidx] = 0.
|
|
23
|
+
|
|
24
|
+
res = torch.mm(l_enc, g_enc.t())
|
|
25
|
+
|
|
26
|
+
E_pos = get_positive_expectation(res * pos_mask, measure, average=False).sum()
|
|
27
|
+
E_pos = E_pos / num_nodes
|
|
28
|
+
E_neg = get_negative_expectation(res * neg_mask, measure, average=False).sum()
|
|
29
|
+
E_neg = E_neg / (num_nodes * (num_graphs - 1))
|
|
30
|
+
|
|
31
|
+
return E_neg - E_pos
|
|
32
|
+
|
|
33
|
+
def raise_measure_error(measure):
|
|
34
|
+
supported_measures = ['GAN', 'JSD', 'X2', 'KL', 'RKL', 'DV', 'H2', 'W1']
|
|
35
|
+
raise NotImplementedError(
|
|
36
|
+
'Measure `{}` not supported. Supported: {}'.format(measure,
|
|
37
|
+
supported_measures))
|
|
38
|
+
|
|
39
|
+
def get_positive_expectation(p_samples, measure, average=True):
|
|
40
|
+
"""Computes the positive part of a divergence / difference.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
p_samples: Positive samples.
|
|
44
|
+
measure: Measure to compute for.
|
|
45
|
+
average: Average the result over samples.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
torch.Tensor
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
log_2 = math.log(2.)
|
|
52
|
+
|
|
53
|
+
if measure == 'GAN':
|
|
54
|
+
Ep = - F.softplus(-p_samples)
|
|
55
|
+
elif measure == 'JSD':
|
|
56
|
+
Ep = log_2 - F.softplus(- p_samples)
|
|
57
|
+
elif measure == 'X2':
|
|
58
|
+
Ep = p_samples ** 2
|
|
59
|
+
elif measure == 'KL':
|
|
60
|
+
Ep = p_samples + 1.
|
|
61
|
+
elif measure == 'RKL':
|
|
62
|
+
Ep = -torch.exp(-p_samples)
|
|
63
|
+
elif measure == 'DV':
|
|
64
|
+
Ep = p_samples
|
|
65
|
+
elif measure == 'H2':
|
|
66
|
+
Ep = 1. - torch.exp(-p_samples)
|
|
67
|
+
elif measure == 'W1':
|
|
68
|
+
Ep = p_samples
|
|
69
|
+
else:
|
|
70
|
+
raise_measure_error(measure)
|
|
71
|
+
|
|
72
|
+
if average:
|
|
73
|
+
return Ep.mean()
|
|
74
|
+
else:
|
|
75
|
+
return Ep
|
|
76
|
+
|
|
77
|
+
def get_negative_expectation(q_samples, measure, average=True):
|
|
78
|
+
"""Computes the negative part of a divergence / difference.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
q_samples: Negative samples.
|
|
82
|
+
measure: Measure to compute for.
|
|
83
|
+
average: Average the result over samples.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
torch.Tensor
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
log_2 = math.log(2.)
|
|
90
|
+
|
|
91
|
+
if measure == 'GAN':
|
|
92
|
+
Eq = F.softplus(-q_samples) + q_samples
|
|
93
|
+
elif measure == 'JSD':
|
|
94
|
+
Eq = F.softplus(-q_samples) + q_samples - log_2
|
|
95
|
+
elif measure == 'X2':
|
|
96
|
+
Eq = -0.5 * ((torch.sqrt(q_samples ** 2) + 1.) ** 2)
|
|
97
|
+
elif measure == 'KL':
|
|
98
|
+
Eq = torch.exp(q_samples)
|
|
99
|
+
elif measure == 'RKL':
|
|
100
|
+
Eq = q_samples - 1.
|
|
101
|
+
elif measure == 'DV':
|
|
102
|
+
Eq = log_sum_exp(q_samples, 0) - math.log(q_samples.size(0))
|
|
103
|
+
elif measure == 'H2':
|
|
104
|
+
Eq = torch.exp(q_samples) - 1.
|
|
105
|
+
elif measure == 'W1':
|
|
106
|
+
Eq = q_samples
|
|
107
|
+
else:
|
|
108
|
+
raise_measure_error(measure)
|
|
109
|
+
|
|
110
|
+
if average:
|
|
111
|
+
return Eq.mean()
|
|
112
|
+
else:
|
|
113
|
+
return Eq
|
|
114
|
+
|
|
115
|
+
def log_sum_exp(x, axis=None):
|
|
116
|
+
"""Log sum exp function
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
x: Input.
|
|
120
|
+
axis: Axis over which to perform sum.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
torch.Tensor: log sum exp
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
x_max = torch.max(x, axis)[0]
|
|
127
|
+
y = torch.log((torch.exp(x - x_max)).sum(axis)) + x_max
|
|
128
|
+
return y
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
|
6
|
+
|
|
7
|
+
from .module import PriorDiscriminator, FF
|
|
8
|
+
from .loss_function import local_global_loss_
|
|
9
|
+
|
|
10
|
+
class GNN(nn.Module):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
num_layer,
|
|
14
|
+
embedding_dim,
|
|
15
|
+
drop_ratio=0.5,
|
|
16
|
+
norm_layer="batch_norm",
|
|
17
|
+
encoder_type="gin-virtual",
|
|
18
|
+
readout="add",
|
|
19
|
+
use_prior=False,
|
|
20
|
+
):
|
|
21
|
+
super(GNN, self).__init__()
|
|
22
|
+
gnn_name = encoder_type.split("-")[0]
|
|
23
|
+
|
|
24
|
+
self.hidden_size = embedding_dim // num_layer
|
|
25
|
+
self.embedding_dim = embedding_dim
|
|
26
|
+
|
|
27
|
+
encoder_params = {
|
|
28
|
+
"num_layer": num_layer,
|
|
29
|
+
"hidden_size": self.hidden_size,
|
|
30
|
+
"JK": "last",
|
|
31
|
+
"drop_ratio": drop_ratio,
|
|
32
|
+
"residual": False,
|
|
33
|
+
"gnn_name": gnn_name,
|
|
34
|
+
"norm_layer": norm_layer
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Choose encoder type based on encoder_model
|
|
38
|
+
encoder_class = GNN_node_Virtualnode if "virtual" in encoder_type else GNN_node
|
|
39
|
+
self.graph_encoder = encoder_class(**encoder_params)
|
|
40
|
+
pooling_funcs = {
|
|
41
|
+
"sum": global_add_pool,
|
|
42
|
+
"mean": global_mean_pool,
|
|
43
|
+
"max": global_max_pool
|
|
44
|
+
}
|
|
45
|
+
self.pool = pooling_funcs.get(readout)
|
|
46
|
+
if self.pool is None:
|
|
47
|
+
raise ValueError(f"Invalid graph pooling type {readout}.")
|
|
48
|
+
|
|
49
|
+
self.local_d = FF(self.embedding_dim)
|
|
50
|
+
self.global_d = FF(self.embedding_dim)
|
|
51
|
+
self.use_prior = use_prior
|
|
52
|
+
if self.use_prior:
|
|
53
|
+
self.prior_d = PriorDiscriminator(self.embedding_dim)
|
|
54
|
+
|
|
55
|
+
self.initialize_parameters()
|
|
56
|
+
|
|
57
|
+
def initialize_parameters(self):
|
|
58
|
+
for m in self.modules():
|
|
59
|
+
if isinstance(m, nn.Linear):
|
|
60
|
+
torch.nn.init.xavier_uniform_(m.weight.data)
|
|
61
|
+
if m.bias is not None:
|
|
62
|
+
m.bias.data.fill_(0.0)
|
|
63
|
+
|
|
64
|
+
def compute_loss(self, batched_data, lw_prior=0.1):
|
|
65
|
+
_, h_node_list = self.graph_encoder(batched_data)
|
|
66
|
+
h_node_list = h_node_list[1:]
|
|
67
|
+
hpool = [self.pool(h_node, batched_data.batch) for h_node in h_node_list]
|
|
68
|
+
h_g = torch.cat(hpool, dim=1)
|
|
69
|
+
h_l = torch.cat(h_node_list, dim=1)
|
|
70
|
+
|
|
71
|
+
g_enc = self.global_d(h_g)
|
|
72
|
+
l_enc = self.local_d(h_l)
|
|
73
|
+
|
|
74
|
+
local_global_loss = local_global_loss_(l_enc, g_enc, batched_data.batch, "JSD")
|
|
75
|
+
|
|
76
|
+
if self.use_prior:
|
|
77
|
+
prior = torch.rand_like(h_g)
|
|
78
|
+
term_a = torch.log(self.prior_d(prior)).mean()
|
|
79
|
+
term_b = torch.log(1.0 - self.prior_d(h_g)).mean()
|
|
80
|
+
PRIOR = - (term_a + term_b) * lw_prior
|
|
81
|
+
else:
|
|
82
|
+
PRIOR = torch.tensor(0.0)
|
|
83
|
+
|
|
84
|
+
return local_global_loss, PRIOR
|
|
85
|
+
|
|
86
|
+
def forward(self, batched_data):
|
|
87
|
+
_, h_node_list = self.graph_encoder(batched_data)
|
|
88
|
+
h_node_list = h_node_list[1:]
|
|
89
|
+
hpool = [self.pool(h_node, batched_data.batch) for h_node in h_node_list]
|
|
90
|
+
h_g = torch.cat(hpool, dim=1)
|
|
91
|
+
|
|
92
|
+
return {"graph": h_g}
|