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,116 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
|
|
4
|
+
|
|
5
|
+
from ...nn import GNN_node, GNN_node_Virtualnode, MLP
|
|
6
|
+
from ...utils import init_weights
|
|
7
|
+
|
|
8
|
+
class BFGNN(nn.Module):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
num_task,
|
|
12
|
+
num_layer,
|
|
13
|
+
hidden_size=300,
|
|
14
|
+
gnn_type="gin-virtual",
|
|
15
|
+
drop_ratio=0.5,
|
|
16
|
+
norm_layer="batch_norm",
|
|
17
|
+
graph_pooling="max",
|
|
18
|
+
augmented_feature=['maccs', 'morgan'],
|
|
19
|
+
algorithm_aligned = 'bf'
|
|
20
|
+
):
|
|
21
|
+
super(BFGNN, self).__init__()
|
|
22
|
+
gnn_name = gnn_type.split("-")[0]
|
|
23
|
+
self.algorithm_aligned = algorithm_aligned
|
|
24
|
+
self.num_task = num_task
|
|
25
|
+
self.hidden_size = hidden_size
|
|
26
|
+
|
|
27
|
+
if "virtual" in gnn_type:
|
|
28
|
+
self.graph_encoder = GNN_node_Virtualnode(
|
|
29
|
+
num_layer,
|
|
30
|
+
hidden_size,
|
|
31
|
+
JK="last",
|
|
32
|
+
drop_ratio=drop_ratio,
|
|
33
|
+
residual=True,
|
|
34
|
+
gnn_name=gnn_name,
|
|
35
|
+
norm_layer=norm_layer,
|
|
36
|
+
algorithm_aligned=algorithm_aligned
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
self.graph_encoder = GNN_node(
|
|
40
|
+
num_layer,
|
|
41
|
+
hidden_size,
|
|
42
|
+
JK="last",
|
|
43
|
+
drop_ratio=drop_ratio,
|
|
44
|
+
residual=True,
|
|
45
|
+
gnn_name=gnn_name,
|
|
46
|
+
norm_layer=norm_layer,
|
|
47
|
+
algorithm_aligned=algorithm_aligned
|
|
48
|
+
)
|
|
49
|
+
if graph_pooling == "sum":
|
|
50
|
+
self.pool = global_add_pool
|
|
51
|
+
elif graph_pooling == "mean":
|
|
52
|
+
self.pool = global_mean_pool
|
|
53
|
+
elif graph_pooling == "max":
|
|
54
|
+
self.pool = global_max_pool
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"Invalid graph pooling type {graph_pooling}.")
|
|
57
|
+
|
|
58
|
+
graph_dim = hidden_size
|
|
59
|
+
self.augmented_feature = augmented_feature
|
|
60
|
+
if augmented_feature:
|
|
61
|
+
if "morgan" in augmented_feature:
|
|
62
|
+
graph_dim += 1024
|
|
63
|
+
if "maccs" in augmented_feature:
|
|
64
|
+
graph_dim += 167
|
|
65
|
+
self.predictor = MLP(graph_dim, hidden_features=2 * hidden_size, out_features=num_task)
|
|
66
|
+
|
|
67
|
+
def initialize_parameters(self, seed=None):
|
|
68
|
+
"""
|
|
69
|
+
Randomly initialize all model parameters using the init_weights function.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
seed (int, optional): Random seed for reproducibility. Defaults to None.
|
|
73
|
+
"""
|
|
74
|
+
if seed is not None:
|
|
75
|
+
torch.manual_seed(seed)
|
|
76
|
+
|
|
77
|
+
# Initialize the main components
|
|
78
|
+
init_weights(self.graph_encoder)
|
|
79
|
+
init_weights(self.predictor)
|
|
80
|
+
|
|
81
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
82
|
+
def reset_parameters(module):
|
|
83
|
+
if hasattr(module, 'reset_parameters'):
|
|
84
|
+
module.reset_parameters()
|
|
85
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
86
|
+
init_weights(module)
|
|
87
|
+
|
|
88
|
+
self.apply(reset_parameters)
|
|
89
|
+
|
|
90
|
+
def _augmented_graph_features(self, batched_data, h_rep):
|
|
91
|
+
if self.augmented_feature:
|
|
92
|
+
if 'morgan' in self.augmented_feature:
|
|
93
|
+
morgan = batched_data.morgan.type_as(h_rep)
|
|
94
|
+
h_rep = torch.cat((h_rep, morgan), dim=1)
|
|
95
|
+
if 'maccs' in self.augmented_feature:
|
|
96
|
+
maccs = batched_data.maccs.type_as(h_rep)
|
|
97
|
+
h_rep = torch.cat((h_rep, maccs), dim=1)
|
|
98
|
+
return h_rep
|
|
99
|
+
|
|
100
|
+
def compute_loss(self, batched_data, criterion, l1_penalty=1e-3):
|
|
101
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
102
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
103
|
+
h_rep = self._augmented_graph_features(batched_data, h_rep)
|
|
104
|
+
prediction = self.predictor(h_rep)
|
|
105
|
+
target = batched_data.y.to(torch.float32)
|
|
106
|
+
is_labeled = batched_data.y == batched_data.y
|
|
107
|
+
loss = criterion(prediction.to(torch.float32)[is_labeled], target[is_labeled]).mean()
|
|
108
|
+
l1_loss = sum(p.abs().sum() for p in self.graph_encoder.parameters()) + sum(p.abs().sum() for p in self.predictor.parameters())
|
|
109
|
+
return loss + l1_penalty * l1_loss
|
|
110
|
+
|
|
111
|
+
def forward(self, batched_data):
|
|
112
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
113
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
114
|
+
h_rep = self._augmented_graph_features(batched_data, h_rep)
|
|
115
|
+
prediction = self.predictor(h_rep)
|
|
116
|
+
return {"prediction": prediction}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import warnings
|
|
4
|
+
import datetime
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
from typing import Optional, Union, Dict, Any, List, Type
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
from torch_geometric.data import Data
|
|
11
|
+
|
|
12
|
+
from .model import BFGNN
|
|
13
|
+
from ...utils import graph_from_smiles
|
|
14
|
+
from ..gnn.modeling_gnn import GNNMolecularPredictor
|
|
15
|
+
from ...utils.search import (
|
|
16
|
+
ParameterSpec,
|
|
17
|
+
ParameterType,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class BFGNNMolecularPredictor(GNNMolecularPredictor):
|
|
22
|
+
"""
|
|
23
|
+
This predictor implements algorithm alignment of Bellman-Ford algorithm with GNN.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
- Graph neural networks extrapolate out-of-distribution for shortest paths.
|
|
28
|
+
https://arxiv.org/abs/2503.19173
|
|
29
|
+
|
|
30
|
+
:param l1_penalty: Weight for the L1 penalty
|
|
31
|
+
:type l1_penalty: float, default=1e-3
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
l1_penalty: float = 1e-3
|
|
36
|
+
# Other Non-init fields
|
|
37
|
+
model_name: str = "BFGNNMolecularPredictor"
|
|
38
|
+
model_class: Type[BFGNN] = field(default=BFGNN, init=False)
|
|
39
|
+
|
|
40
|
+
def __post_init__(self):
|
|
41
|
+
super().__post_init__()
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _get_param_names() -> List[str]:
|
|
45
|
+
return GNNMolecularPredictor._get_param_names() + [
|
|
46
|
+
"l1_penalty",
|
|
47
|
+
]
|
|
48
|
+
def _get_default_search_space(self):
|
|
49
|
+
search_space = super()._get_default_search_space().copy()
|
|
50
|
+
search_space["l1_penalty"] = ParameterSpec(ParameterType.LOG_FLOAT, (1e-6, 1))
|
|
51
|
+
return search_space
|
|
52
|
+
|
|
53
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
54
|
+
base_params = super()._get_model_params(checkpoint)
|
|
55
|
+
return base_params
|
|
56
|
+
|
|
57
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
58
|
+
self.model.train()
|
|
59
|
+
losses = []
|
|
60
|
+
|
|
61
|
+
iterator = (
|
|
62
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
63
|
+
if self.verbose
|
|
64
|
+
else train_loader
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
for step, batch in enumerate(iterator):
|
|
68
|
+
batch = batch.to(self.device)
|
|
69
|
+
optimizer.zero_grad()
|
|
70
|
+
|
|
71
|
+
loss = self.model.compute_loss(batch, self.loss_criterion, self.l1_penalty)
|
|
72
|
+
loss.backward()
|
|
73
|
+
if self.grad_clip_value is not None:
|
|
74
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
75
|
+
optimizer.step()
|
|
76
|
+
|
|
77
|
+
if self.verbose:
|
|
78
|
+
iterator.set_postfix({"Epoch": epoch, "Total Loss": f"{loss.item():.4f}"})
|
|
79
|
+
losses.append(loss.item())
|
|
80
|
+
|
|
81
|
+
return losses
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import numpy as np
|
|
4
|
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
|
|
5
|
+
|
|
6
|
+
from .utils import split_batch, relabel, set_masks, clear_masks
|
|
7
|
+
from ...nn import GNN_node, GNN_node_Virtualnode
|
|
8
|
+
from ...utils import init_weights
|
|
9
|
+
|
|
10
|
+
class DIR(nn.Module):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
num_task,
|
|
14
|
+
num_layer,
|
|
15
|
+
causal_ratio=0.8,
|
|
16
|
+
hidden_size=300,
|
|
17
|
+
gnn_type="gin-virtual",
|
|
18
|
+
drop_ratio=0.5,
|
|
19
|
+
norm_layer="batch_norm",
|
|
20
|
+
graph_pooling="sum",
|
|
21
|
+
augmented_feature=['maccs', 'morgan']
|
|
22
|
+
):
|
|
23
|
+
super(DIR, self).__init__()
|
|
24
|
+
gnn_name = gnn_type.split("-")[0]
|
|
25
|
+
self.num_task = num_task
|
|
26
|
+
self.hidden_size = hidden_size
|
|
27
|
+
self.causal_ratio = causal_ratio
|
|
28
|
+
if "virtual" in gnn_type:
|
|
29
|
+
self.graph_encoder = GNN_node_Virtualnode(
|
|
30
|
+
num_layer,
|
|
31
|
+
hidden_size,
|
|
32
|
+
JK="last",
|
|
33
|
+
drop_ratio=drop_ratio,
|
|
34
|
+
residual=True,
|
|
35
|
+
gnn_name=gnn_name,
|
|
36
|
+
norm_layer=norm_layer,
|
|
37
|
+
encode_atom=False
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
self.graph_encoder = GNN_node(
|
|
41
|
+
num_layer,
|
|
42
|
+
hidden_size,
|
|
43
|
+
JK="last",
|
|
44
|
+
drop_ratio=drop_ratio,
|
|
45
|
+
residual=True,
|
|
46
|
+
gnn_name=gnn_name,
|
|
47
|
+
norm_layer=norm_layer,
|
|
48
|
+
encode_atom=False
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.causal_att_net = CausalAttNet(causal_ratio=causal_ratio, hidden_size=hidden_size)
|
|
52
|
+
|
|
53
|
+
graph_dim = hidden_size
|
|
54
|
+
self.augmented_feature = augmented_feature
|
|
55
|
+
if graph_pooling == "sum":
|
|
56
|
+
self.pool = global_add_pool
|
|
57
|
+
elif graph_pooling == "mean":
|
|
58
|
+
self.pool = global_mean_pool
|
|
59
|
+
elif graph_pooling == "max":
|
|
60
|
+
self.pool = global_max_pool
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Invalid graph pooling type {graph_pooling}.")
|
|
63
|
+
|
|
64
|
+
if augmented_feature:
|
|
65
|
+
if "morgan" in augmented_feature:
|
|
66
|
+
graph_dim += 1024
|
|
67
|
+
if "maccs" in augmented_feature:
|
|
68
|
+
graph_dim += 167
|
|
69
|
+
|
|
70
|
+
self.causal_lin = torch.nn.Linear(graph_dim, num_task)
|
|
71
|
+
self.conf_lin = torch.nn.Linear(hidden_size, num_task)
|
|
72
|
+
|
|
73
|
+
def get_graph_rep(self, x, edge_index, edge_attr, batch):
|
|
74
|
+
h_node, _ = self.graph_encoder(x, edge_index, edge_attr, batch)
|
|
75
|
+
h_graph = self.pool(h_node, batch)
|
|
76
|
+
return h_graph
|
|
77
|
+
|
|
78
|
+
def get_causal_pred(self, batched_data, h_graph):
|
|
79
|
+
h_graph = self._augment_graph_features(batched_data, h_graph)
|
|
80
|
+
return self.causal_lin(h_graph)
|
|
81
|
+
|
|
82
|
+
def get_conf_pred(self, conf_graph_x):
|
|
83
|
+
return self.conf_lin(conf_graph_x)
|
|
84
|
+
|
|
85
|
+
def get_comb_pred(self, batched_data, causal_graph_x, conf_graph_x):
|
|
86
|
+
causal_graph_x = self._augment_graph_features(batched_data, causal_graph_x)
|
|
87
|
+
causal_pred = self.causal_lin(causal_graph_x)
|
|
88
|
+
conf_pred = self.conf_lin(conf_graph_x).detach()
|
|
89
|
+
return torch.sigmoid(conf_pred) * causal_pred
|
|
90
|
+
|
|
91
|
+
def initialize_parameters(self, seed=None):
|
|
92
|
+
if seed is not None:
|
|
93
|
+
torch.manual_seed(seed)
|
|
94
|
+
|
|
95
|
+
# Initialize the main components
|
|
96
|
+
init_weights(self.graph_encoder)
|
|
97
|
+
init_weights(self.causal_att_net)
|
|
98
|
+
init_weights(self.causal_lin)
|
|
99
|
+
init_weights(self.conf_lin)
|
|
100
|
+
|
|
101
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
102
|
+
def reset_parameters(module):
|
|
103
|
+
if hasattr(module, 'reset_parameters'):
|
|
104
|
+
module.reset_parameters()
|
|
105
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
106
|
+
init_weights(module)
|
|
107
|
+
|
|
108
|
+
self.apply(reset_parameters)
|
|
109
|
+
|
|
110
|
+
def _augment_graph_features(self, batched_data, h):
|
|
111
|
+
if self.augmented_feature:
|
|
112
|
+
if 'morgan' in self.augmented_feature:
|
|
113
|
+
morgan = batched_data.morgan.type_as(h)
|
|
114
|
+
h = torch.cat((h, morgan), dim=1)
|
|
115
|
+
if 'maccs' in self.augmented_feature:
|
|
116
|
+
maccs = batched_data.maccs.type_as(h)
|
|
117
|
+
h = torch.cat((h, maccs), dim=1)
|
|
118
|
+
return h
|
|
119
|
+
|
|
120
|
+
def compute_loss(self, batched_data, criterion, alpha_prime):
|
|
121
|
+
(causal_x, causal_edge_index, causal_edge_attr, causal_edge_weight, causal_batch),\
|
|
122
|
+
(conf_x, conf_edge_index, conf_edge_attr, conf_edge_weight, conf_batch), pred_edge_weight = self.causal_att_net(batched_data)
|
|
123
|
+
|
|
124
|
+
set_masks(causal_edge_weight, self.graph_encoder)
|
|
125
|
+
set_masks(causal_edge_weight, self.causal_lin)
|
|
126
|
+
set_masks(causal_edge_weight, self.conf_lin)
|
|
127
|
+
causal_rep = self.get_graph_rep(x=causal_x, edge_index=causal_edge_index, edge_attr=causal_edge_attr, batch=causal_batch)
|
|
128
|
+
causal_out = self.get_causal_pred(batched_data, causal_rep)
|
|
129
|
+
clear_masks(self.graph_encoder)
|
|
130
|
+
clear_masks(self.causal_lin)
|
|
131
|
+
clear_masks(self.conf_lin)
|
|
132
|
+
|
|
133
|
+
set_masks(conf_edge_weight, self.graph_encoder)
|
|
134
|
+
set_masks(conf_edge_weight, self.causal_lin)
|
|
135
|
+
set_masks(conf_edge_weight, self.conf_lin)
|
|
136
|
+
conf_rep = self.get_graph_rep(x=conf_x, edge_index=conf_edge_index, edge_attr=conf_edge_attr, batch=conf_batch).detach()
|
|
137
|
+
conf_out = self.get_conf_pred(conf_rep)
|
|
138
|
+
clear_masks(self.graph_encoder)
|
|
139
|
+
clear_masks(self.causal_lin)
|
|
140
|
+
clear_masks(self.conf_lin)
|
|
141
|
+
|
|
142
|
+
batched_data.y = batched_data.y[0:causal_out.size(0),:]
|
|
143
|
+
is_labeled = batched_data.y == batched_data.y
|
|
144
|
+
|
|
145
|
+
causal_loss = criterion(
|
|
146
|
+
causal_out.to(torch.float32)[is_labeled],
|
|
147
|
+
batched_data.y.to(torch.float32)[is_labeled]
|
|
148
|
+
).mean()
|
|
149
|
+
conf_loss = criterion(
|
|
150
|
+
conf_out.to(torch.float32)[is_labeled],
|
|
151
|
+
batched_data.y.to(torch.float32)[is_labeled]
|
|
152
|
+
).mean()
|
|
153
|
+
|
|
154
|
+
env_loss = torch.tensor([]).to(batched_data.y.device)
|
|
155
|
+
for conf in conf_rep:
|
|
156
|
+
rep_out = self.get_comb_pred(batched_data, causal_rep, conf)
|
|
157
|
+
tmp = criterion(rep_out.to(torch.float32)[is_labeled], batched_data.y.to(torch.float32)[is_labeled]).mean()
|
|
158
|
+
env_loss = torch.cat([env_loss, tmp.unsqueeze(0)])
|
|
159
|
+
causal_loss += alpha_prime * env_loss.mean()
|
|
160
|
+
env_loss = alpha_prime * torch.var(env_loss * conf_rep.size(0))
|
|
161
|
+
|
|
162
|
+
return causal_loss, conf_loss, env_loss
|
|
163
|
+
|
|
164
|
+
def forward(self, batched_data):
|
|
165
|
+
(causal_x, causal_edge_index, causal_edge_attr, causal_edge_weight, causal_batch), _, _ = self.causal_att_net(batched_data)
|
|
166
|
+
set_masks(causal_edge_weight, self.graph_encoder)
|
|
167
|
+
set_masks(causal_edge_weight, self.causal_lin)
|
|
168
|
+
set_masks(causal_edge_weight, self.conf_lin)
|
|
169
|
+
|
|
170
|
+
causal_rep = self.get_graph_rep(x=causal_x, edge_index=causal_edge_index, edge_attr=causal_edge_attr, batch=causal_batch)
|
|
171
|
+
causal_out = self.get_causal_pred(batched_data, causal_rep)
|
|
172
|
+
clear_masks(self.graph_encoder)
|
|
173
|
+
clear_masks(self.causal_lin)
|
|
174
|
+
clear_masks(self.conf_lin)
|
|
175
|
+
|
|
176
|
+
return {"prediction": causal_out, "representation": causal_rep}
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _disable_batchnorm_tracking(model):
|
|
180
|
+
def fn(module):
|
|
181
|
+
if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):
|
|
182
|
+
module.track_running_stats = False
|
|
183
|
+
|
|
184
|
+
model.apply(fn)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _enable_batchnorm_tracking(model):
|
|
188
|
+
def fn(module):
|
|
189
|
+
if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):
|
|
190
|
+
module.track_running_stats = True
|
|
191
|
+
|
|
192
|
+
model.apply(fn)
|
|
193
|
+
|
|
194
|
+
class CausalAttNet(nn.Module):
|
|
195
|
+
def __init__(self, causal_ratio, hidden_size):
|
|
196
|
+
super(CausalAttNet, self).__init__()
|
|
197
|
+
self.gnn_node = GNN_node_Virtualnode(2, hidden_size, JK="last", drop_ratio=0., residual=True, gnn_name="gin", norm_layer="batch_norm")
|
|
198
|
+
self.linear = nn.Linear(hidden_size*2, 1)
|
|
199
|
+
self.ratio = causal_ratio
|
|
200
|
+
|
|
201
|
+
def forward(self, data):
|
|
202
|
+
x, _ = self.gnn_node(data.x, data.edge_index, data.edge_attr, data.batch)
|
|
203
|
+
|
|
204
|
+
row, col = data.edge_index
|
|
205
|
+
edge_rep = torch.cat([x[row], x[col]], dim=-1)
|
|
206
|
+
pred_edge_weight = self.linear(edge_rep).view(-1)
|
|
207
|
+
causal_edge_index = torch.LongTensor([[],[]]).to(x.device)
|
|
208
|
+
causal_edge_weight = torch.tensor([]).to(x.device)
|
|
209
|
+
causal_edge_attr = torch.LongTensor([]).to(x.device)
|
|
210
|
+
conf_edge_index = torch.LongTensor([[],[]]).to(x.device)
|
|
211
|
+
conf_edge_weight = torch.tensor([]).to(x.device)
|
|
212
|
+
conf_edge_attr = torch.LongTensor([]).to(x.device)
|
|
213
|
+
|
|
214
|
+
edge_indices, _, _, num_edges, cum_edges = split_batch(data)
|
|
215
|
+
for edge_index, N, C in zip(edge_indices, num_edges, cum_edges):
|
|
216
|
+
n_reserve = int(self.ratio * N)
|
|
217
|
+
edge_attr = data.edge_attr[C:C+N]
|
|
218
|
+
single_mask = pred_edge_weight[C:C+N]
|
|
219
|
+
single_mask_detach = pred_edge_weight[C:C+N].detach().cpu().numpy()
|
|
220
|
+
rank = np.argpartition(-single_mask_detach, n_reserve)
|
|
221
|
+
idx_reserve, idx_drop = rank[:n_reserve], rank[n_reserve:]
|
|
222
|
+
|
|
223
|
+
causal_edge_index = torch.cat([causal_edge_index, edge_index[:, idx_reserve]], dim=1)
|
|
224
|
+
conf_edge_index = torch.cat([conf_edge_index, edge_index[:, idx_drop]], dim=1)
|
|
225
|
+
causal_edge_weight = torch.cat([causal_edge_weight, single_mask[idx_reserve]])
|
|
226
|
+
conf_edge_weight = torch.cat([conf_edge_weight, -1 * single_mask[idx_drop]])
|
|
227
|
+
causal_edge_attr = torch.cat([causal_edge_attr, edge_attr[idx_reserve]])
|
|
228
|
+
conf_edge_attr = torch.cat([conf_edge_attr, edge_attr[idx_drop]])
|
|
229
|
+
|
|
230
|
+
causal_x, causal_edge_index, causal_batch, _ = relabel(x, causal_edge_index, data.batch)
|
|
231
|
+
conf_x, conf_edge_index, conf_batch, _ = relabel(x, conf_edge_index, data.batch)
|
|
232
|
+
|
|
233
|
+
return (causal_x, causal_edge_index, causal_edge_attr, causal_edge_weight, causal_batch),\
|
|
234
|
+
(conf_x, conf_edge_index, conf_edge_attr, conf_edge_weight, conf_batch),\
|
|
235
|
+
pred_edge_weight
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import warnings
|
|
4
|
+
import datetime
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Callable, Literal, Type
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
from torch_geometric.loader import DataLoader
|
|
11
|
+
|
|
12
|
+
from .model import DIR
|
|
13
|
+
from ..gnn.modeling_gnn import GNNMolecularPredictor
|
|
14
|
+
from ...utils.search import (
|
|
15
|
+
ParameterSpec,
|
|
16
|
+
ParameterType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DIRMolecularPredictor(GNNMolecularPredictor):
|
|
21
|
+
"""This predictor implements the DIR for molecular property prediction tasks.
|
|
22
|
+
|
|
23
|
+
The full name of DIR is Discovering Invariant Rationales.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
- Discovering Invariant Rationales for Graph Neural Networks.
|
|
28
|
+
https://openreview.net/forum?id=hGXij5rfiHw
|
|
29
|
+
- Code: https://github.com/Wuyxin/DIR-GNN
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
causal_ratio : float, default=0.8
|
|
34
|
+
The ratio of causal edges to keep during training. A higher ratio means more edges
|
|
35
|
+
are considered causal/important for the prediction. This controls the sparsity of
|
|
36
|
+
the learned rationales.
|
|
37
|
+
|
|
38
|
+
lw_invariant : float, default=1e-4
|
|
39
|
+
The weight of the invariance loss term. This loss encourages the model to learn
|
|
40
|
+
rationales that are invariant across different environments/perturbations. A higher
|
|
41
|
+
value puts more emphasis on learning invariant features.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# DIR-specific parameters
|
|
45
|
+
causal_ratio: float = 0.8
|
|
46
|
+
lw_invariant: float = 1e-4
|
|
47
|
+
# Override parent defaults
|
|
48
|
+
model_name: str = "DIRMolecularPredictor"
|
|
49
|
+
model_class: Type[DIR] = field(default=DIR, init=False)
|
|
50
|
+
|
|
51
|
+
def __post_init__(self):
|
|
52
|
+
super().__post_init__()
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _get_param_names() -> List[str]:
|
|
56
|
+
return ["causal_ratio", "lw_invariant"] + GNNMolecularPredictor._get_param_names()
|
|
57
|
+
|
|
58
|
+
def _get_default_search_space(self):
|
|
59
|
+
search_space = super()._get_default_search_space().copy()
|
|
60
|
+
search_space["causal_ratio"] = ParameterSpec(ParameterType.FLOAT, (0.1, 0.9))
|
|
61
|
+
search_space["lw_invariant"] = ParameterSpec(ParameterType.FLOAT, (1e-5, 1e-2))
|
|
62
|
+
return search_space
|
|
63
|
+
|
|
64
|
+
def _setup_optimizers(self) -> Tuple[Dict[str, torch.optim.Optimizer], Optional[Any]]:
|
|
65
|
+
model_optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
|
|
66
|
+
conf_optimizer = torch.optim.Adam(self.model.conf_lin.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
|
|
67
|
+
if self.grad_clip_value is not None:
|
|
68
|
+
for group in model_optimizer.param_groups:
|
|
69
|
+
group.setdefault("max_norm", self.grad_clip_value)
|
|
70
|
+
group.setdefault("norm_type", 2.0)
|
|
71
|
+
|
|
72
|
+
scheduler = None
|
|
73
|
+
if self.use_lr_scheduler:
|
|
74
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
75
|
+
model_optimizer,
|
|
76
|
+
mode="min",
|
|
77
|
+
factor=self.scheduler_factor,
|
|
78
|
+
patience=self.scheduler_patience,
|
|
79
|
+
min_lr=1e-6,
|
|
80
|
+
cooldown=0,
|
|
81
|
+
eps=1e-8,
|
|
82
|
+
)
|
|
83
|
+
optimizer = {"model": model_optimizer, "conf": conf_optimizer}
|
|
84
|
+
|
|
85
|
+
return optimizer, scheduler
|
|
86
|
+
|
|
87
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
88
|
+
base_params = super()._get_model_params(checkpoint)
|
|
89
|
+
if checkpoint and "hyperparameters" in checkpoint:
|
|
90
|
+
base_params["causal_ratio"] = checkpoint["hyperparameters"]["causal_ratio"]
|
|
91
|
+
else:
|
|
92
|
+
base_params["causal_ratio"] = self.causal_ratio
|
|
93
|
+
return base_params
|
|
94
|
+
|
|
95
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
96
|
+
self.model.train()
|
|
97
|
+
losses = []
|
|
98
|
+
|
|
99
|
+
iterator = (
|
|
100
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
101
|
+
if self.verbose
|
|
102
|
+
else train_loader
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
alpha_prime = self.lw_invariant * (epoch ** 1.6)
|
|
106
|
+
conf_opt = optimizer["conf"]
|
|
107
|
+
model_optimizer = optimizer["model"]
|
|
108
|
+
|
|
109
|
+
for batch in iterator:
|
|
110
|
+
|
|
111
|
+
batch = batch.to(self.device)
|
|
112
|
+
|
|
113
|
+
# Forward pass and loss computation
|
|
114
|
+
causal_loss, conf_loss, env_loss = self.model.compute_loss(batch, self.loss_criterion, alpha_prime)
|
|
115
|
+
|
|
116
|
+
conf_opt.zero_grad()
|
|
117
|
+
conf_loss.backward()
|
|
118
|
+
conf_opt.step()
|
|
119
|
+
|
|
120
|
+
model_optimizer.zero_grad()
|
|
121
|
+
(causal_loss + env_loss).backward()
|
|
122
|
+
model_optimizer.step()
|
|
123
|
+
|
|
124
|
+
loss = causal_loss + env_loss + conf_loss
|
|
125
|
+
losses.append(loss.item())
|
|
126
|
+
|
|
127
|
+
# Update progress bar if using tqdm
|
|
128
|
+
if self.verbose:
|
|
129
|
+
iterator.set_postfix({"Epoch": epoch, "Causal Loss": f"{causal_loss.item():.4f}", "Conf Loss": f"{conf_loss.item():.4f}", "Env Loss": f"{env_loss.item():.4f}", "Total Loss": f"{loss.item():.4f}"})
|
|
130
|
+
|
|
131
|
+
return losses
|
|
132
|
+
|
|
133
|
+
def predict(self, X: List[str]) -> Dict[str, Union[np.ndarray, List[List]]]:
|
|
134
|
+
self._check_is_fitted()
|
|
135
|
+
|
|
136
|
+
# Convert to PyTorch Geometric format and create loader
|
|
137
|
+
X, _ = self._validate_inputs(X)
|
|
138
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
139
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
140
|
+
|
|
141
|
+
# Make predictions
|
|
142
|
+
self.model = self.model.to(self.device)
|
|
143
|
+
self.model.eval()
|
|
144
|
+
predictions = []
|
|
145
|
+
with torch.no_grad():
|
|
146
|
+
for batch in tqdm(loader, disable=not self.verbose):
|
|
147
|
+
batch = batch.to(self.device)
|
|
148
|
+
out = self.model(batch)
|
|
149
|
+
predictions.append(out["prediction"].cpu().numpy())
|
|
150
|
+
|
|
151
|
+
if predictions:
|
|
152
|
+
return {
|
|
153
|
+
"prediction": np.concatenate(predictions, axis=0),
|
|
154
|
+
}
|
|
155
|
+
else:
|
|
156
|
+
warnings.warn(
|
|
157
|
+
"No valid predictions could be made from the input data. Returning empty results."
|
|
158
|
+
)
|
|
159
|
+
return {"prediction": np.array([])}
|
|
160
|
+
|
|
161
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch_geometric.utils import degree
|
|
3
|
+
from torch_geometric.nn.conv import MessagePassing
|
|
4
|
+
|
|
5
|
+
def split_batch(g):
|
|
6
|
+
split = degree(g.batch[g.edge_index[0]], dtype=torch.long).tolist()
|
|
7
|
+
edge_indices = torch.split(g.edge_index, split, dim=1)
|
|
8
|
+
num_nodes = degree(g.batch, dtype=torch.long)
|
|
9
|
+
cum_nodes = torch.cat([g.batch.new_zeros(1), num_nodes.cumsum(dim=0)[:-1]])
|
|
10
|
+
num_edges = torch.tensor([e.size(1) for e in edge_indices], dtype=torch.long).to(g.x.device)
|
|
11
|
+
cum_edges = torch.cat([g.batch.new_zeros(1), num_edges.cumsum(dim=0)[:-1]])
|
|
12
|
+
|
|
13
|
+
return edge_indices, num_nodes, cum_nodes, num_edges, cum_edges
|
|
14
|
+
|
|
15
|
+
def relabel(x, edge_index, batch, pos=None):
|
|
16
|
+
num_nodes = x.size(0)
|
|
17
|
+
sub_nodes = torch.unique(edge_index)
|
|
18
|
+
x = x[sub_nodes]
|
|
19
|
+
batch = batch[sub_nodes]
|
|
20
|
+
row, col = edge_index
|
|
21
|
+
# remapping the nodes in the explanatory subgraph to new ids.
|
|
22
|
+
node_idx = row.new_full((num_nodes,), -1)
|
|
23
|
+
node_idx[sub_nodes] = torch.arange(sub_nodes.size(0), device=row.device)
|
|
24
|
+
edge_index = node_idx[edge_index]
|
|
25
|
+
if pos is not None:
|
|
26
|
+
pos = pos[sub_nodes]
|
|
27
|
+
return x, edge_index, batch, pos
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_masks(mask: torch.Tensor, model: torch.nn.Module):
|
|
31
|
+
for module in model.modules():
|
|
32
|
+
if isinstance(module, MessagePassing):
|
|
33
|
+
module.explain = True
|
|
34
|
+
module._edge_mask = mask
|
|
35
|
+
|
|
36
|
+
def clear_masks(model: torch.nn.Module):
|
|
37
|
+
for module in model.modules():
|
|
38
|
+
if isinstance(module, MessagePassing):
|
|
39
|
+
module.explain = None
|
|
40
|
+
module._edge_mask = None
|