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,252 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
from typing import Optional, Union, Dict, Any, List, Callable, Type, Tuple
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
import torch.nn as nn
|
|
10
|
+
import torch.optim as optim
|
|
11
|
+
from torch.utils.data import DataLoader, TensorDataset
|
|
12
|
+
|
|
13
|
+
from .model import Transformer
|
|
14
|
+
from ..lstm.modeling_lstm import LSTMMolecularPredictor
|
|
15
|
+
from ...utils.search import (
|
|
16
|
+
suggest_parameter,
|
|
17
|
+
ParameterSpec,
|
|
18
|
+
ParameterType,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Dictionary mapping parameter names to their types and ranges
|
|
22
|
+
DEFAULT_TRANSFORMER_SEARCH_SPACES: Dict[str, ParameterSpec] = {
|
|
23
|
+
# Integer-valued parameters
|
|
24
|
+
"hidden_size": ParameterSpec(ParameterType.INTEGER, (64, 256)),
|
|
25
|
+
"n_heads": ParameterSpec(ParameterType.INTEGER, (2, 8)),
|
|
26
|
+
"num_layers": ParameterSpec(ParameterType.INTEGER, (2, 6)),
|
|
27
|
+
"dim_feedforward": ParameterSpec(ParameterType.INTEGER, (128, 512)),
|
|
28
|
+
# Float-valued parameters with log scale
|
|
29
|
+
"learning_rate": ParameterSpec(ParameterType.LOG_FLOAT, (1e-5, 1e-2)),
|
|
30
|
+
"weight_decay": ParameterSpec(ParameterType.LOG_FLOAT, (1e-8, 1e-3)),
|
|
31
|
+
"dropout": ParameterSpec(ParameterType.FLOAT, (0.0, 0.5)),
|
|
32
|
+
"scheduler_factor": ParameterSpec(ParameterType.FLOAT, (0.1, 0.5)),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SMILESTransformerMolecularPredictor(LSTMMolecularPredictor):
|
|
37
|
+
"""This predictor implements a Transformer model for SMILES-based molecular property predictions.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
This implementation uses a transformer encoder architecture to learn
|
|
42
|
+
representations of molecular structures from SMILES strings.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
num_task : int, default=1
|
|
47
|
+
Number of prediction tasks.
|
|
48
|
+
task_type : str, default="regression"
|
|
49
|
+
Type of prediction task, either "regression" or "classification".
|
|
50
|
+
input_dim : int, default=54
|
|
51
|
+
Size of vocabulary for SMILES tokenization.
|
|
52
|
+
hidden_size : int, default=128
|
|
53
|
+
Dimension of embedding vectors.
|
|
54
|
+
n_heads : int, default=4
|
|
55
|
+
Number of attention heads in transformer layers.
|
|
56
|
+
num_layers : int, default=3
|
|
57
|
+
Number of transformer encoder layers.
|
|
58
|
+
dim_feedforward : int, default=256
|
|
59
|
+
Dimension of the feedforward network in transformer layers.
|
|
60
|
+
max_input_len : int, default=200
|
|
61
|
+
Maximum length of input sequences. Shorter sequences will be padded.
|
|
62
|
+
dropout : float, default=0.1
|
|
63
|
+
Dropout rate for transformer layers.
|
|
64
|
+
batch_size : int, default=64
|
|
65
|
+
Number of samples per batch for training.
|
|
66
|
+
epochs : int, default=200
|
|
67
|
+
Maximum number of training epochs.
|
|
68
|
+
loss_criterion : callable, optional
|
|
69
|
+
Loss function for training. Defaults to MSELoss for regression.
|
|
70
|
+
evaluate_criterion : str or callable, optional
|
|
71
|
+
Metric for model evaluation.
|
|
72
|
+
evaluate_higher_better : bool, optional
|
|
73
|
+
Whether higher values of the evaluation metric are better.
|
|
74
|
+
learning_rate : float, default=0.0001
|
|
75
|
+
Learning rate for optimizer.
|
|
76
|
+
weight_decay : float, default=0.0
|
|
77
|
+
L2 regularization strength.
|
|
78
|
+
patience : int, default=20
|
|
79
|
+
Number of epochs to wait for improvement before early stopping.
|
|
80
|
+
use_lr_scheduler : bool, default=True
|
|
81
|
+
Whether to use learning rate scheduler.
|
|
82
|
+
scheduler_factor : float, default=0.5
|
|
83
|
+
Factor by which to reduce learning rate when plateau is reached.
|
|
84
|
+
scheduler_patience : int, default=5
|
|
85
|
+
Number of epochs with no improvement after which learning rate will be reduced.
|
|
86
|
+
verbose : bool, default=False
|
|
87
|
+
Whether to print progress information during training.
|
|
88
|
+
"""
|
|
89
|
+
# Model parameters
|
|
90
|
+
num_task: int = 1
|
|
91
|
+
task_type: str = "regression"
|
|
92
|
+
input_dim: int = 54 # vocabulary size
|
|
93
|
+
hidden_size: int = 128
|
|
94
|
+
n_heads: int = 4
|
|
95
|
+
num_layers: int = 3
|
|
96
|
+
dim_feedforward: Optional[int] = 256
|
|
97
|
+
max_input_len: int = 200 # max token length
|
|
98
|
+
dropout: float = 0.1
|
|
99
|
+
|
|
100
|
+
# Training parameters
|
|
101
|
+
batch_size: int = 64
|
|
102
|
+
epochs: int = 200
|
|
103
|
+
loss_criterion: Optional[Callable] = None
|
|
104
|
+
evaluate_criterion: Optional[Union[str, Callable]] = None
|
|
105
|
+
evaluate_higher_better: Optional[bool] = None
|
|
106
|
+
learning_rate: float = 0.0001
|
|
107
|
+
weight_decay: float = 0.0
|
|
108
|
+
patience: int = 20
|
|
109
|
+
|
|
110
|
+
# Scheduler parameters
|
|
111
|
+
use_lr_scheduler: bool = True
|
|
112
|
+
scheduler_factor: float = 0.5
|
|
113
|
+
scheduler_patience: int = 5
|
|
114
|
+
|
|
115
|
+
# Other parameters
|
|
116
|
+
verbose: bool = False
|
|
117
|
+
model_name: str = "SMILESTransformerMolecularPredictor"
|
|
118
|
+
|
|
119
|
+
# Non-init fields
|
|
120
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
121
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
122
|
+
model_class: Type[Transformer] = field(default=Transformer, init=False)
|
|
123
|
+
|
|
124
|
+
def __post_init__(self):
|
|
125
|
+
"""Initialize after dataclass initialization."""
|
|
126
|
+
super().__post_init__()
|
|
127
|
+
# Validate n_heads is compatible with hidden_size
|
|
128
|
+
if self.hidden_size % self.n_heads != 0:
|
|
129
|
+
raise ValueError(f"hidden_size ({self.hidden_size}) must be divisible by n_heads ({self.n_heads})")
|
|
130
|
+
|
|
131
|
+
# Setup loss criterion and evaluation
|
|
132
|
+
if self.loss_criterion is None:
|
|
133
|
+
self.loss_criterion = nn.MSELoss()
|
|
134
|
+
self._setup_evaluation(self.evaluate_criterion, self.evaluate_higher_better)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _get_param_names() -> List[str]:
|
|
138
|
+
"""Get parameter names for the estimator.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
List[str]
|
|
143
|
+
List of parameter names that can be used for model configuration.
|
|
144
|
+
"""
|
|
145
|
+
return [
|
|
146
|
+
# Model Hyperparameters
|
|
147
|
+
"num_task",
|
|
148
|
+
"task_type",
|
|
149
|
+
"input_dim",
|
|
150
|
+
"hidden_size",
|
|
151
|
+
"n_heads",
|
|
152
|
+
"num_layers",
|
|
153
|
+
"dim_feedforward",
|
|
154
|
+
"max_input_len",
|
|
155
|
+
"dropout",
|
|
156
|
+
# Training Parameters
|
|
157
|
+
"batch_size",
|
|
158
|
+
"epochs",
|
|
159
|
+
"learning_rate",
|
|
160
|
+
"weight_decay",
|
|
161
|
+
"patience",
|
|
162
|
+
"loss_criterion",
|
|
163
|
+
# Evaluation Parameters
|
|
164
|
+
"evaluate_name",
|
|
165
|
+
"evaluate_criterion",
|
|
166
|
+
"evaluate_higher_better",
|
|
167
|
+
# Scheduler Parameters
|
|
168
|
+
"use_lr_scheduler",
|
|
169
|
+
"scheduler_factor",
|
|
170
|
+
"scheduler_patience",
|
|
171
|
+
# Other Parameters
|
|
172
|
+
"fitting_epoch",
|
|
173
|
+
"fitting_loss",
|
|
174
|
+
"device",
|
|
175
|
+
"verbose"
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
179
|
+
"""Get model parameters from checkpoint or current instance.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
checkpoint : Optional[Dict], default=None
|
|
184
|
+
Optional dictionary containing model checkpoint data
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
Dict[str, Any]
|
|
189
|
+
Dictionary of model parameters
|
|
190
|
+
"""
|
|
191
|
+
if checkpoint is not None:
|
|
192
|
+
if "hyperparameters" not in checkpoint:
|
|
193
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
194
|
+
|
|
195
|
+
hyperparameters = checkpoint["hyperparameters"]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
"num_task": hyperparameters.get("num_task", self.num_task),
|
|
200
|
+
"input_dim": hyperparameters.get("input_dim", self.input_dim),
|
|
201
|
+
"hidden_size": hyperparameters.get("hidden_size", self.hidden_size),
|
|
202
|
+
"n_heads": hyperparameters.get("n_heads", self.n_heads),
|
|
203
|
+
"num_layers": hyperparameters.get("num_layers", self.num_layers),
|
|
204
|
+
"dim_feedforward": hyperparameters.get("dim_feedforward", self.dim_feedforward),
|
|
205
|
+
"max_input_len": hyperparameters.get("max_input_len", self.max_input_len),
|
|
206
|
+
"dropout": hyperparameters.get("dropout", self.dropout),
|
|
207
|
+
}
|
|
208
|
+
else:
|
|
209
|
+
return {
|
|
210
|
+
"num_task": self.num_task,
|
|
211
|
+
"input_dim": self.input_dim,
|
|
212
|
+
"hidden_size": self.hidden_size,
|
|
213
|
+
"n_heads": self.n_heads,
|
|
214
|
+
"num_layers": self.num_layers,
|
|
215
|
+
"dim_feedforward": self.dim_feedforward,
|
|
216
|
+
"max_input_len": self.max_input_len,
|
|
217
|
+
"dropout": self.dropout,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
221
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
Tuple[optim.Optimizer, Optional[Any]]
|
|
226
|
+
A tuple containing:
|
|
227
|
+
- The configured optimizer
|
|
228
|
+
- The learning rate scheduler (if enabled, else None)
|
|
229
|
+
"""
|
|
230
|
+
optimizer = torch.optim.AdamW(
|
|
231
|
+
self.model.parameters(),
|
|
232
|
+
lr=self.learning_rate,
|
|
233
|
+
weight_decay=self.weight_decay
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
scheduler = None
|
|
237
|
+
if self.use_lr_scheduler:
|
|
238
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
239
|
+
optimizer,
|
|
240
|
+
mode='min' if not self.evaluate_higher_better else 'max',
|
|
241
|
+
factor=self.scheduler_factor,
|
|
242
|
+
patience=self.scheduler_patience,
|
|
243
|
+
verbose=self.verbose
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return optimizer, scheduler
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _get_default_search_space(self):
|
|
250
|
+
"""Get the default hyperparameter search space.
|
|
251
|
+
"""
|
|
252
|
+
return DEFAULT_TRANSFORMER_SEARCH_SPACES
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
import copy
|
|
5
|
+
# from torch_scatter import scatter
|
|
6
|
+
from .utils import scatter
|
|
7
|
+
|
|
8
|
+
from ...nn import GNN_node, GNN_node_Virtualnode, MLP
|
|
9
|
+
from ...utils import init_weights
|
|
10
|
+
|
|
11
|
+
class SSR(nn.Module):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
num_task,
|
|
15
|
+
num_layer,
|
|
16
|
+
coarse_pool='mean',
|
|
17
|
+
hidden_size=300,
|
|
18
|
+
gnn_type="gin-virtual",
|
|
19
|
+
drop_ratio=0.5,
|
|
20
|
+
norm_layer="batch_norm",
|
|
21
|
+
graph_pooling="mean",
|
|
22
|
+
augmented_feature=['maccs', 'morgan']
|
|
23
|
+
):
|
|
24
|
+
|
|
25
|
+
super(SSR, self).__init__()
|
|
26
|
+
gnn_name = gnn_type.split("-")[0]
|
|
27
|
+
self.num_task = num_task
|
|
28
|
+
self.hidden_size = hidden_size
|
|
29
|
+
self.coarse_pool = coarse_pool
|
|
30
|
+
|
|
31
|
+
if "virtual" in gnn_type:
|
|
32
|
+
self.graph_encoder = GNN_node_Virtualnode(
|
|
33
|
+
num_layer,
|
|
34
|
+
hidden_size,
|
|
35
|
+
JK="last",
|
|
36
|
+
drop_ratio=drop_ratio,
|
|
37
|
+
residual=True,
|
|
38
|
+
gnn_name=gnn_name,
|
|
39
|
+
norm_layer=norm_layer,
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
self.graph_encoder = GNN_node(
|
|
43
|
+
num_layer,
|
|
44
|
+
hidden_size,
|
|
45
|
+
JK="last",
|
|
46
|
+
drop_ratio=drop_ratio,
|
|
47
|
+
residual=True,
|
|
48
|
+
gnn_name=gnn_name,
|
|
49
|
+
norm_layer=norm_layer,
|
|
50
|
+
)
|
|
51
|
+
if graph_pooling == "sum":
|
|
52
|
+
self.pool = global_add_pool
|
|
53
|
+
elif graph_pooling == "mean":
|
|
54
|
+
self.pool = global_mean_pool
|
|
55
|
+
elif graph_pooling == "max":
|
|
56
|
+
self.pool = global_max_pool
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"Invalid graph pooling type {graph_pooling}.")
|
|
59
|
+
|
|
60
|
+
graph_dim = hidden_size
|
|
61
|
+
self.augmented_feature = augmented_feature
|
|
62
|
+
if augmented_feature:
|
|
63
|
+
if "morgan" in augmented_feature:
|
|
64
|
+
graph_dim += 1024
|
|
65
|
+
if "maccs" in augmented_feature:
|
|
66
|
+
graph_dim += 167
|
|
67
|
+
self.predictor = MLP(graph_dim, hidden_features=2 * hidden_size, out_features=num_task)
|
|
68
|
+
|
|
69
|
+
def l2diff(self, x1, x2):
|
|
70
|
+
"""Compute L2 difference between tensors"""
|
|
71
|
+
return torch.sqrt(torch.sum((x1 - x2) ** 2, dim=1))
|
|
72
|
+
|
|
73
|
+
def moment_diff(self, sx1, sx2, k, og_batch, coarse_batch):
|
|
74
|
+
"""Compute difference between k-th moments"""
|
|
75
|
+
ss1 = scatter(sx1**k, og_batch, dim=0, reduce='mean')
|
|
76
|
+
ss2 = scatter(sx2**k, coarse_batch, dim=0, reduce='mean')
|
|
77
|
+
return self.l2diff(ss1, ss2)
|
|
78
|
+
|
|
79
|
+
def cmd(self, x1, x2, og_batch, coarse_batch,n_moments):
|
|
80
|
+
"""Central Moment Discrepancy between x1 and x2 distributions"""
|
|
81
|
+
# Compute means
|
|
82
|
+
mx1 = scatter(x1, og_batch, dim=0, reduce='mean')
|
|
83
|
+
mx2 = scatter(x2, coarse_batch, dim=0, reduce='mean')
|
|
84
|
+
# Center features
|
|
85
|
+
sx1 = x1 - mx1[og_batch]
|
|
86
|
+
sx2 = x2 - mx2[coarse_batch]
|
|
87
|
+
|
|
88
|
+
# First moment difference
|
|
89
|
+
dm = self.l2diff(mx1, mx2)
|
|
90
|
+
scms = dm
|
|
91
|
+
|
|
92
|
+
# Higher order moments
|
|
93
|
+
for i in range(n_moments-1):
|
|
94
|
+
scms = scms + self.moment_diff(sx1, sx2, i+2, og_batch, coarse_batch)
|
|
95
|
+
|
|
96
|
+
return scms.mean()
|
|
97
|
+
|
|
98
|
+
def pool_batch_by_graph(self, clusters, original_batch):
|
|
99
|
+
"""
|
|
100
|
+
Build a new batch vector for coarse nodes so that all coarse nodes derived from the same
|
|
101
|
+
original graph get the same graph label. Here, we iterate graph-by-graph.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
clusters (Tensor): Cluster assignments for each original node, shape [num_original_nodes].
|
|
105
|
+
original_batch (Tensor): Original batch vector (graph assignments) for each node.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Tensor: New batch vector for coarse nodes, of shape [num_coarse_nodes].
|
|
109
|
+
"""
|
|
110
|
+
unique_graphs = torch.unique(original_batch)
|
|
111
|
+
new_batch_list = []
|
|
112
|
+
for g in unique_graphs:
|
|
113
|
+
# Get indices for nodes belonging to graph g.
|
|
114
|
+
mask = (original_batch == g)
|
|
115
|
+
# For these nodes, get their cluster ids.
|
|
116
|
+
clusters_in_g = clusters[mask]
|
|
117
|
+
# Determine the unique clusters in this graph.
|
|
118
|
+
unique_clusters_in_g = torch.unique(clusters_in_g)
|
|
119
|
+
# For each coarse node from graph g, assign the same graph id (g.item())
|
|
120
|
+
new_batch_list.extend([g.item()] * unique_clusters_in_g.numel())
|
|
121
|
+
return torch.tensor(new_batch_list, device=original_batch.device, dtype=torch.long)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def prepare_coarsened_batch(self, batch, ratio):
|
|
125
|
+
"""
|
|
126
|
+
Create a coarsened version of batch for forward pass with edge attributes,
|
|
127
|
+
ensuring that the new batch assignment preserves the original graph labels.
|
|
128
|
+
"""
|
|
129
|
+
new_batch = copy.deepcopy(batch)
|
|
130
|
+
coarse_ratio_postfix = str(int(ratio * 100))
|
|
131
|
+
|
|
132
|
+
# 1. Get clusters (one per original node) and ensure long type.
|
|
133
|
+
clusters = getattr(batch, f"clusters_{coarse_ratio_postfix}").long() # shape: [num_nodes]
|
|
134
|
+
|
|
135
|
+
# Compute a factor to disambiguate clusters from different graphs.
|
|
136
|
+
factor = clusters.max().item() + 1 # All clusters are in [0, factor-1]
|
|
137
|
+
# Create a global key that combines the original batch assignment and clusters.
|
|
138
|
+
global_coarse_key = batch.batch * factor + clusters # shape: [num_nodes]
|
|
139
|
+
|
|
140
|
+
# Get the unique global keys and the inverse mapping.
|
|
141
|
+
unique_global_keys, inverse_indices = torch.unique(global_coarse_key, return_inverse=True)
|
|
142
|
+
num_coarse_nodes = unique_global_keys.numel()
|
|
143
|
+
|
|
144
|
+
# 2. Pool node features using the global coarse key.
|
|
145
|
+
new_x = scatter(batch.x, inverse_indices, dim=0, reduce=self.coarse_pool, dim_size=num_coarse_nodes)
|
|
146
|
+
new_batch.x = new_x
|
|
147
|
+
|
|
148
|
+
# 3. Build the new batch vector for coarse nodes:
|
|
149
|
+
# The graph assignment for each coarse node is simply:
|
|
150
|
+
# graph_id = unique_global_key // factor
|
|
151
|
+
new_batch_vec = unique_global_keys // factor
|
|
152
|
+
new_batch.batch = new_batch_vec
|
|
153
|
+
|
|
154
|
+
# 4. Process the coarsened edge index.
|
|
155
|
+
# Assume that the stored coarsened edge index is based on original node indices.
|
|
156
|
+
# Map each node index to its global coarse key.
|
|
157
|
+
coarse_edge_index = getattr(batch, f"coarsened_edge_index_{coarse_ratio_postfix}")
|
|
158
|
+
# For each index in coarse_edge_index, replace with its global coarse key.
|
|
159
|
+
temp_edge_index = torch.stack([
|
|
160
|
+
global_coarse_key[coarse_edge_index[0]],
|
|
161
|
+
global_coarse_key[coarse_edge_index[1]]
|
|
162
|
+
])
|
|
163
|
+
# Build a mapping from global key to new index from unique_global_keys.
|
|
164
|
+
mapping = {old.item(): new for new, old in enumerate(unique_global_keys)}
|
|
165
|
+
new_edge_index = torch.zeros_like(temp_edge_index)
|
|
166
|
+
for i in range(temp_edge_index.size(1)):
|
|
167
|
+
new_edge_index[0, i] = mapping[temp_edge_index[0, i].item()]
|
|
168
|
+
new_edge_index[1, i] = mapping[temp_edge_index[1, i].item()]
|
|
169
|
+
new_batch.edge_index = new_edge_index
|
|
170
|
+
|
|
171
|
+
# 5. Process edge attributes, if available.
|
|
172
|
+
if hasattr(batch, f"coarsened_edge_attr_{coarse_ratio_postfix}"):
|
|
173
|
+
edge_attr = getattr(batch, f"coarsened_edge_attr_{coarse_ratio_postfix}")
|
|
174
|
+
new_batch.edge_attr = torch.round(edge_attr).long()
|
|
175
|
+
|
|
176
|
+
new_batch.num_nodes = num_coarse_nodes
|
|
177
|
+
'''
|
|
178
|
+
# Debug prints:
|
|
179
|
+
print("\n=== Feature and Cluster Info ===")
|
|
180
|
+
print(f"Original x shape: {batch.x.shape}")
|
|
181
|
+
print(f"Original batch shape: {batch.batch.shape}")
|
|
182
|
+
print(f"Number of unique clusters (globally disambiguated): {num_coarse_nodes}")
|
|
183
|
+
print("\n=== Final Dimensions ===")
|
|
184
|
+
print(f"New x shape: {new_batch.x.shape}") # Should be [num_coarse_nodes, feature_dim]
|
|
185
|
+
print(f"New batch shape: {new_batch.batch.shape}") # Should be [num_coarse_nodes]
|
|
186
|
+
print(f"New edge_index shape: {new_batch.edge_index.shape}")
|
|
187
|
+
'''
|
|
188
|
+
# Validate dimensions.
|
|
189
|
+
assert new_batch.batch.numel() == num_coarse_nodes, \
|
|
190
|
+
f"Batch size {new_batch.batch.numel()} doesn't match number of coarse nodes {num_coarse_nodes}"
|
|
191
|
+
assert new_batch.x.shape[0] == num_coarse_nodes, \
|
|
192
|
+
f"Number of node features {new_batch.x.shape[0]} doesn't match number of coarse nodes {num_coarse_nodes}"
|
|
193
|
+
|
|
194
|
+
return new_batch
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def initialize_parameters(self, seed=None):
|
|
199
|
+
"""
|
|
200
|
+
Randomly initialize all model parameters using the init_weights function.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
seed (int, optional): Random seed for reproducibility. Defaults to None.
|
|
204
|
+
"""
|
|
205
|
+
if seed is not None:
|
|
206
|
+
torch.manual_seed(seed)
|
|
207
|
+
|
|
208
|
+
# Initialize the main components
|
|
209
|
+
init_weights(self.graph_encoder)
|
|
210
|
+
init_weights(self.predictor)
|
|
211
|
+
|
|
212
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
213
|
+
def reset_parameters(module):
|
|
214
|
+
if hasattr(module, 'reset_parameters'):
|
|
215
|
+
module.reset_parameters()
|
|
216
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
217
|
+
init_weights(module)
|
|
218
|
+
|
|
219
|
+
self.apply(reset_parameters)
|
|
220
|
+
|
|
221
|
+
def _augmented_graph_features(self, batched_data, h_rep):
|
|
222
|
+
if self.augmented_feature:
|
|
223
|
+
if 'morgan' in self.augmented_feature:
|
|
224
|
+
morgan = batched_data.morgan.type_as(h_rep)
|
|
225
|
+
h_rep = torch.cat((h_rep, morgan), dim=1)
|
|
226
|
+
if 'maccs' in self.augmented_feature:
|
|
227
|
+
maccs = batched_data.maccs.type_as(h_rep)
|
|
228
|
+
h_rep = torch.cat((h_rep, maccs), dim=1)
|
|
229
|
+
return h_rep
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def compute_loss(self, batched_data, criterion, coarse_ratios=[0.8, 0.9],cmd_coeff=0.1,fine_grained=True,n_moments=5):
|
|
233
|
+
"""Compute loss with SSR regularization"""
|
|
234
|
+
# Original forward pass
|
|
235
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
236
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
237
|
+
h_rep = self._augmented_graph_features(batched_data, h_rep)
|
|
238
|
+
prediction = self.predictor(h_rep)
|
|
239
|
+
|
|
240
|
+
# Compute prediction loss
|
|
241
|
+
target = batched_data.y.to(torch.float32)
|
|
242
|
+
is_labeled = batched_data.y == batched_data.y # Check for NaN
|
|
243
|
+
pred_loss = criterion(prediction.to(torch.float32)[is_labeled], target[is_labeled]).mean()
|
|
244
|
+
|
|
245
|
+
# Initialize SSR loss
|
|
246
|
+
ssr_loss = torch.tensor(0.0, device=batched_data.x.device)
|
|
247
|
+
|
|
248
|
+
# Process each coarsening ratio for SSR
|
|
249
|
+
for ratio in coarse_ratios:
|
|
250
|
+
# Skip if coarsened data wasn't pre-computed
|
|
251
|
+
if not hasattr(batched_data, f"coarsened_edge_index_{str(int(ratio*100))}"):
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Prepare coarsened batch
|
|
255
|
+
coarse_batch = self.prepare_coarsened_batch(batched_data, ratio)
|
|
256
|
+
|
|
257
|
+
# Get node embeddings for coarsened graph
|
|
258
|
+
coarse_h_node, _ = self.graph_encoder(coarse_batch)
|
|
259
|
+
|
|
260
|
+
# Compute CMD loss
|
|
261
|
+
if fine_grained:
|
|
262
|
+
# Node-level CMD
|
|
263
|
+
ssr_loss = ssr_loss + self.cmd(h_node,
|
|
264
|
+
coarse_h_node,
|
|
265
|
+
batched_data.batch,
|
|
266
|
+
coarse_batch.batch,n_moments)
|
|
267
|
+
else:
|
|
268
|
+
# Graph-level CMD
|
|
269
|
+
coarse_h_rep = self.pool(coarse_h_node, coarse_batch.batch)
|
|
270
|
+
ssr_loss = ssr_loss + torch.norm(h_rep - coarse_h_rep, dim=1).mean()
|
|
271
|
+
|
|
272
|
+
# Compute total loss
|
|
273
|
+
total_loss = pred_loss + cmd_coeff * ssr_loss
|
|
274
|
+
|
|
275
|
+
return total_loss, pred_loss, ssr_loss
|
|
276
|
+
|
|
277
|
+
def forward(self, batched_data):
|
|
278
|
+
"""Standard forward pass (for inference without SSR)"""
|
|
279
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
280
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
281
|
+
h_rep = self._augmented_graph_features(batched_data, h_rep)
|
|
282
|
+
prediction = self.predictor(h_rep)
|
|
283
|
+
return {"prediction": prediction}
|