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.
Files changed (148) hide show
  1. torch_molecule/__init__.py +70 -0
  2. torch_molecule/base/__init__.py +8 -0
  3. torch_molecule/base/base.py +396 -0
  4. torch_molecule/base/encoder.py +44 -0
  5. torch_molecule/base/generator.py +24 -0
  6. torch_molecule/base/old_encoder.py +322 -0
  7. torch_molecule/base/old_predictor.py +904 -0
  8. torch_molecule/base/old_predictor2.py +436 -0
  9. torch_molecule/base/predictor.py +112 -0
  10. torch_molecule/encoder/attrmask/__init__.py +3 -0
  11. torch_molecule/encoder/attrmask/model.py +113 -0
  12. torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
  13. torch_molecule/encoder/constant.py +13 -0
  14. torch_molecule/encoder/contextpred/__init__.py +3 -0
  15. torch_molecule/encoder/contextpred/model.py +207 -0
  16. torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
  17. torch_molecule/encoder/contextpred/utils.py +206 -0
  18. torch_molecule/encoder/edgepred/__init__.py +3 -0
  19. torch_molecule/encoder/edgepred/model.py +104 -0
  20. torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
  21. torch_molecule/encoder/graphmae/__init__.py +3 -0
  22. torch_molecule/encoder/graphmae/dataloader.py +206 -0
  23. torch_molecule/encoder/graphmae/model.py +137 -0
  24. torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
  25. torch_molecule/encoder/infograph/__init__.py +3 -0
  26. torch_molecule/encoder/infograph/loss_function.py +128 -0
  27. torch_molecule/encoder/infograph/model.py +92 -0
  28. torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
  29. torch_molecule/encoder/infograph/module.py +31 -0
  30. torch_molecule/encoder/moama/__init__.py +3 -0
  31. torch_molecule/encoder/moama/model.py +131 -0
  32. torch_molecule/encoder/moama/modeling_moama.py +307 -0
  33. torch_molecule/encoder/moama/utils.py +143 -0
  34. torch_molecule/encoder/pretrained/__init__.py +3 -0
  35. torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
  36. torch_molecule/encoder/supervised/__init__.py +3 -0
  37. torch_molecule/encoder/supervised/model.py +97 -0
  38. torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
  39. torch_molecule/generator/digress/__init__.py +3 -0
  40. torch_molecule/generator/digress/diffusion.py +194 -0
  41. torch_molecule/generator/digress/modeling_digress.py +503 -0
  42. torch_molecule/generator/digress/transformer.py +360 -0
  43. torch_molecule/generator/digress/utils.py +196 -0
  44. torch_molecule/generator/gdss/__init__.py +3 -0
  45. torch_molecule/generator/gdss/layers.py +257 -0
  46. torch_molecule/generator/gdss/model.py +204 -0
  47. torch_molecule/generator/gdss/modeling_gdss.py +474 -0
  48. torch_molecule/generator/gdss/sde.py +334 -0
  49. torch_molecule/generator/gdss/solver.py +227 -0
  50. torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
  51. torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
  52. torch_molecule/generator/gdss/utils.py +240 -0
  53. torch_molecule/generator/graph_dit/__init__.py +3 -0
  54. torch_molecule/generator/graph_dit/diffusion.py +188 -0
  55. torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
  56. torch_molecule/generator/graph_dit/transformer.py +207 -0
  57. torch_molecule/generator/graph_dit/utils.py +220 -0
  58. torch_molecule/generator/graph_ga/__init__.py +3 -0
  59. torch_molecule/generator/graph_ga/crossover.py +182 -0
  60. torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
  61. torch_molecule/generator/graph_ga/mutate.py +136 -0
  62. torch_molecule/generator/graph_ga/oracle.py +95 -0
  63. torch_molecule/generator/jtvae/__init__.py +3 -0
  64. torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
  65. torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
  66. torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
  67. torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
  68. torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
  69. torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
  70. torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
  71. torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
  72. torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
  73. torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
  74. torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
  75. torch_molecule/generator/lstm/__init__.py +3 -0
  76. torch_molecule/generator/lstm/action_sampler.py +91 -0
  77. torch_molecule/generator/lstm/lstm.py +59 -0
  78. torch_molecule/generator/lstm/modeling_lstm.py +263 -0
  79. torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
  80. torch_molecule/generator/lstm/utils.py +62 -0
  81. torch_molecule/generator/molgpt/__init__.py +3 -0
  82. torch_molecule/generator/molgpt/dataset.py +87 -0
  83. torch_molecule/generator/molgpt/gpt.py +251 -0
  84. torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
  85. torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
  86. torch_molecule/nn/__init__.py +16 -0
  87. torch_molecule/nn/attention.py +63 -0
  88. torch_molecule/nn/embedder.py +183 -0
  89. torch_molecule/nn/gnn.py +631 -0
  90. torch_molecule/nn/mlp.py +34 -0
  91. torch_molecule/predictor/bfgnn/__init__.py +3 -0
  92. torch_molecule/predictor/bfgnn/model.py +116 -0
  93. torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
  94. torch_molecule/predictor/dir/__init__.py +3 -0
  95. torch_molecule/predictor/dir/model.py +235 -0
  96. torch_molecule/predictor/dir/modeling_dir.py +161 -0
  97. torch_molecule/predictor/dir/utils.py +40 -0
  98. torch_molecule/predictor/gnn/__init__.py +3 -0
  99. torch_molecule/predictor/gnn/model.py +114 -0
  100. torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
  101. torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
  102. torch_molecule/predictor/grea/__init__.py +3 -0
  103. torch_molecule/predictor/grea/model.py +190 -0
  104. torch_molecule/predictor/grea/modeling_grea.py +107 -0
  105. torch_molecule/predictor/grea/old1_features.py +229 -0
  106. torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
  107. torch_molecule/predictor/grea/utils.py +97 -0
  108. torch_molecule/predictor/grin/__init__.py +3 -0
  109. torch_molecule/predictor/grin/model.py +116 -0
  110. torch_molecule/predictor/grin/modeling_grin.py +91 -0
  111. torch_molecule/predictor/irm/__init__.py +3 -0
  112. torch_molecule/predictor/irm/model.py +127 -0
  113. torch_molecule/predictor/irm/modeling_irm.py +179 -0
  114. torch_molecule/predictor/lstm/__init__.py +3 -0
  115. torch_molecule/predictor/lstm/model.py +145 -0
  116. torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
  117. torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
  118. torch_molecule/predictor/old_modeling_graph.py +650 -0
  119. torch_molecule/predictor/rpgnn/__init__.py +3 -0
  120. torch_molecule/predictor/rpgnn/model.py +185 -0
  121. torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
  122. torch_molecule/predictor/sgir/__init__.py +3 -0
  123. torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
  124. torch_molecule/predictor/sgir/strategy.py +266 -0
  125. torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
  126. torch_molecule/predictor/smiles_transformer/model.py +424 -0
  127. torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
  128. torch_molecule/predictor/ssr/__init__.py +3 -0
  129. torch_molecule/predictor/ssr/model.py +283 -0
  130. torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
  131. torch_molecule/predictor/ssr/utils.py +185 -0
  132. torch_molecule/utils/__init__.py +31 -0
  133. torch_molecule/utils/checker.py +158 -0
  134. torch_molecule/utils/checkpoint.py +294 -0
  135. torch_molecule/utils/format.py +140 -0
  136. torch_molecule/utils/generic/metrics.py +530 -0
  137. torch_molecule/utils/generic/pseudo_tasks.py +11 -0
  138. torch_molecule/utils/generic/weights.py +30 -0
  139. torch_molecule/utils/graph/features.py +178 -0
  140. torch_molecule/utils/graph/graph_from_smiles.py +134 -0
  141. torch_molecule/utils/graph/graph_to_smiles.py +312 -0
  142. torch_molecule/utils/hf.py +232 -0
  143. torch_molecule/utils/search.py +64 -0
  144. torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
  145. torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
  146. torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
  147. torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
  148. 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,3 @@
1
+ from .modeling_dir import DIRMolecularPredictor
2
+
3
+ __all__ = ['DIRMolecularPredictor']
@@ -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
@@ -0,0 +1,3 @@
1
+ from .modeling_gnn import GNNMolecularPredictor
2
+
3
+ __all__ = ['GNNMolecularPredictor']