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,185 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
|
|
5
|
+
from ...nn import GNN_node, GNN_node_Virtualnode, MLP
|
|
6
|
+
from ...utils import init_weights
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RPGNN(nn.Module):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
num_task,
|
|
13
|
+
num_layer,
|
|
14
|
+
num_node_feature,
|
|
15
|
+
fixed_size=32,
|
|
16
|
+
num_perm=3,
|
|
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
|
+
Random Permutation Graph Neural Network (RPGNN)
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
num_task: Number of prediction tasks
|
|
29
|
+
num_layer: Number of GNN layers
|
|
30
|
+
num_node_feature: Number of node features
|
|
31
|
+
fixed_size: Size of position encoding vectors
|
|
32
|
+
num_perm: Number of random permutations to average over
|
|
33
|
+
hidden_size: Hidden dimension size
|
|
34
|
+
gnn_type: Type of GNN to use (gin, gin-virtual, gcn, gcn-virtual)
|
|
35
|
+
drop_ratio: Dropout ratio
|
|
36
|
+
norm_layer: Type of normalization layer
|
|
37
|
+
graph_pooling: Type of graph pooling (mean, sum, max)
|
|
38
|
+
augmented_feature: List of additional molecular features to use
|
|
39
|
+
"""
|
|
40
|
+
super(RPGNN, self).__init__()
|
|
41
|
+
|
|
42
|
+
gnn_name = gnn_type.split("-")[0]
|
|
43
|
+
self.num_task = num_task
|
|
44
|
+
self.hidden_size = hidden_size
|
|
45
|
+
self.fixed_size = fixed_size
|
|
46
|
+
self.num_perm = num_perm
|
|
47
|
+
self.augmented_feature = augmented_feature
|
|
48
|
+
self.num_node_feature = num_node_feature
|
|
49
|
+
# Register fixed-size node IDs (position encoding)
|
|
50
|
+
self.register_buffer("node_ids", torch.eye(self.fixed_size))
|
|
51
|
+
|
|
52
|
+
# GNN selection
|
|
53
|
+
if "virtual" in gnn_type:
|
|
54
|
+
self.gnn = GNN_node_Virtualnode(
|
|
55
|
+
num_layer=num_layer,
|
|
56
|
+
hidden_size=hidden_size,
|
|
57
|
+
JK="last",
|
|
58
|
+
drop_ratio=drop_ratio,
|
|
59
|
+
residual=True,
|
|
60
|
+
gnn_name=gnn_name,
|
|
61
|
+
norm_layer=norm_layer
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
self.gnn = GNN_node(
|
|
65
|
+
num_layer=num_layer,
|
|
66
|
+
hidden_size=hidden_size,
|
|
67
|
+
JK="last",
|
|
68
|
+
drop_ratio=drop_ratio,
|
|
69
|
+
residual=True,
|
|
70
|
+
gnn_name=gnn_name,
|
|
71
|
+
norm_layer=norm_layer
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Graph pooling selection
|
|
75
|
+
if graph_pooling == "sum":
|
|
76
|
+
self.pool = global_add_pool
|
|
77
|
+
elif graph_pooling == "mean":
|
|
78
|
+
self.pool = global_mean_pool
|
|
79
|
+
elif graph_pooling == "max":
|
|
80
|
+
self.pool = global_max_pool
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(f"Invalid graph pooling type: {graph_pooling}")
|
|
83
|
+
|
|
84
|
+
# Calculate final prediction dimension
|
|
85
|
+
graph_dim = hidden_size+self.fixed_size
|
|
86
|
+
if augmented_feature:
|
|
87
|
+
if "morgan" in augmented_feature:
|
|
88
|
+
graph_dim += 1024
|
|
89
|
+
if "maccs" in augmented_feature:
|
|
90
|
+
graph_dim += 167
|
|
91
|
+
|
|
92
|
+
# Final predictor MLP
|
|
93
|
+
self.predictor = MLP(
|
|
94
|
+
in_features=graph_dim,
|
|
95
|
+
hidden_features=2 * hidden_size,
|
|
96
|
+
out_features=num_task
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def initialize_parameters(self, seed=None):
|
|
100
|
+
"""Initialize all model parameters."""
|
|
101
|
+
if seed is not None:
|
|
102
|
+
torch.manual_seed(seed)
|
|
103
|
+
|
|
104
|
+
# Initialize GNN and predictor
|
|
105
|
+
init_weights(self.gnn)
|
|
106
|
+
init_weights(self.predictor)
|
|
107
|
+
|
|
108
|
+
# Apply reset_parameters to all modules that have it
|
|
109
|
+
def reset_parameters(module):
|
|
110
|
+
if hasattr(module, 'reset_parameters'):
|
|
111
|
+
module.reset_parameters()
|
|
112
|
+
|
|
113
|
+
self.apply(reset_parameters)
|
|
114
|
+
|
|
115
|
+
def _augmented_graph_features(self, batched_data, h_rep):
|
|
116
|
+
"""Add augmented molecular features to the graph representation."""
|
|
117
|
+
if not self.augmented_feature:
|
|
118
|
+
return h_rep
|
|
119
|
+
|
|
120
|
+
if "morgan" in self.augmented_feature:
|
|
121
|
+
morgan = batched_data.morgan.type_as(h_rep)
|
|
122
|
+
h_rep = torch.cat((h_rep, morgan), dim=1)
|
|
123
|
+
if "maccs" in self.augmented_feature:
|
|
124
|
+
maccs = batched_data.maccs.type_as(h_rep)
|
|
125
|
+
h_rep = torch.cat((h_rep, maccs), dim=1)
|
|
126
|
+
|
|
127
|
+
return h_rep
|
|
128
|
+
|
|
129
|
+
def forward(self, batched_data):
|
|
130
|
+
"""Forward pass of RPGNN."""
|
|
131
|
+
# Extract original data
|
|
132
|
+
x, edge_index, batch = batched_data.x, batched_data.edge_index, batched_data.batch
|
|
133
|
+
|
|
134
|
+
# Initialize final output
|
|
135
|
+
out = None
|
|
136
|
+
|
|
137
|
+
# Average over multiple permutations
|
|
138
|
+
for _ in range(self.num_perm):
|
|
139
|
+
temp_data = batched_data.clone()
|
|
140
|
+
# Create new_x with same dtype as x
|
|
141
|
+
new_x = torch.empty(x.size(0), x.size(1) + self.fixed_size, dtype=x.dtype, device=x.device)
|
|
142
|
+
|
|
143
|
+
for graph_idx in range(batch.max().item() + 1):
|
|
144
|
+
node_indices = (batch == graph_idx).nonzero().squeeze(1)
|
|
145
|
+
graph_size = node_indices.size(0)
|
|
146
|
+
perm = torch.randperm(graph_size)
|
|
147
|
+
|
|
148
|
+
node_ids = self.node_ids.repeat(
|
|
149
|
+
graph_size // self.fixed_size + 1, 1
|
|
150
|
+
)[:graph_size]
|
|
151
|
+
permuted_node_ids = node_ids[perm, :]
|
|
152
|
+
|
|
153
|
+
# Convert both tensors to the same type as x before concatenation
|
|
154
|
+
new_x[node_indices] = torch.cat([x[node_indices].to(x.dtype), permuted_node_ids.to(x.dtype)], dim=1)
|
|
155
|
+
temp_data.x = new_x[:,:self.num_node_feature]
|
|
156
|
+
# Process through GNN
|
|
157
|
+
h_v, _ = self.gnn(temp_data)
|
|
158
|
+
|
|
159
|
+
fused_h_v = torch.cat([h_v, new_x[:,self.num_node_feature:]], dim=1)
|
|
160
|
+
|
|
161
|
+
# Pool node representations to graph representations
|
|
162
|
+
h_rep = self.pool(fused_h_v, temp_data.batch)
|
|
163
|
+
|
|
164
|
+
# Add augmented features
|
|
165
|
+
h_rep = self._augmented_graph_features(temp_data, h_rep)
|
|
166
|
+
|
|
167
|
+
# Accumulate output
|
|
168
|
+
if out is None:
|
|
169
|
+
out = h_rep / self.num_perm
|
|
170
|
+
else:
|
|
171
|
+
out += h_rep / self.num_perm
|
|
172
|
+
|
|
173
|
+
# Final prediction
|
|
174
|
+
prediction = self.predictor(out)
|
|
175
|
+
|
|
176
|
+
return {"prediction": prediction}
|
|
177
|
+
|
|
178
|
+
def compute_loss(self, batched_data, criterion):
|
|
179
|
+
out = self.forward(batched_data)
|
|
180
|
+
prediction = out['prediction']
|
|
181
|
+
target = batched_data.y.to(torch.float32)
|
|
182
|
+
is_labeled = batched_data.y == batched_data.y
|
|
183
|
+
loss = criterion(prediction.to(torch.float32)[is_labeled], target[is_labeled]).mean()
|
|
184
|
+
|
|
185
|
+
return loss
|
|
@@ -0,0 +1,76 @@
|
|
|
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 RPGNN
|
|
13
|
+
from ..gnn.modeling_gnn import GNNMolecularPredictor
|
|
14
|
+
from ...utils.search import (
|
|
15
|
+
ParameterSpec,
|
|
16
|
+
ParameterType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RPGNNMolecularPredictor(GNNMolecularPredictor):
|
|
21
|
+
"""This predictor implements a GNN model based on Relational pooling.
|
|
22
|
+
|
|
23
|
+
The full name of RPGNN is Relational Pooling for Graph Representations.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
- Relational Pooling for Graph Representations.
|
|
28
|
+
https://arxiv.org/abs/1903.02541
|
|
29
|
+
- Reference Code: https://github.com/PurdueMINDS/RelationalPooling/tree/master?tab=readme-ov-file
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
num_perm : int, default=3
|
|
34
|
+
Number of random permutations to use for relational pooling.
|
|
35
|
+
fixed_size : int, default=10
|
|
36
|
+
Maximum number of nodes to consider in the graph.
|
|
37
|
+
num_node_feature : int, default=9
|
|
38
|
+
Dimension of the input node features. This should match the number of atomic features used to represent
|
|
39
|
+
each node in the molecular graph (e.g., atomic number, degree, hybridization, etc.).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# RPGNN-specific parameter
|
|
43
|
+
num_perm: int = 3
|
|
44
|
+
fixed_size: int = 10
|
|
45
|
+
num_node_feature: int = 9
|
|
46
|
+
# Override parent defaults
|
|
47
|
+
model_name: str = "RPGNNMolecularPredictor"
|
|
48
|
+
model_class: Type[RPGNN] = field(default=RPGNN, init=False)
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
super().__post_init__()
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _get_param_names() -> List[str]:
|
|
55
|
+
return ["num_perm", "fixed_size", "num_node_feature"] + GNNMolecularPredictor._get_param_names()
|
|
56
|
+
|
|
57
|
+
def _get_default_search_space(self):
|
|
58
|
+
search_space = super()._get_default_search_space().copy()
|
|
59
|
+
search_space["num_perm"] = ParameterSpec(ParameterType.INTEGER, (1, 10))
|
|
60
|
+
search_space["fixed_size"] = ParameterSpec(ParameterType.INTEGER, (1, 10))
|
|
61
|
+
return search_space
|
|
62
|
+
|
|
63
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
64
|
+
base_params = super()._get_model_params(checkpoint)
|
|
65
|
+
if checkpoint and "hyperparameters" in checkpoint:
|
|
66
|
+
base_params["num_perm"] = checkpoint["hyperparameters"]["num_perm"]
|
|
67
|
+
base_params["fixed_size"] = checkpoint["hyperparameters"]["fixed_size"]
|
|
68
|
+
base_params["num_node_feature"] = checkpoint["hyperparameters"]["num_node_feature"]
|
|
69
|
+
else:
|
|
70
|
+
base_params["num_perm"] = self.num_perm
|
|
71
|
+
base_params["fixed_size"] = self.fixed_size
|
|
72
|
+
base_params["num_node_feature"] = self.num_node_feature
|
|
73
|
+
base_params.pop("graph_pooling", None)
|
|
74
|
+
return base_params
|
|
75
|
+
|
|
76
|
+
|
|
@@ -0,0 +1,260 @@
|
|
|
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
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
from torch_geometric.loader import DataLoader
|
|
11
|
+
|
|
12
|
+
from .strategy import build_selection_dataset, build_augmentation_dataset
|
|
13
|
+
from ..grea.modeling_grea import GREAMolecularPredictor
|
|
14
|
+
from ..grea.model import GREA
|
|
15
|
+
|
|
16
|
+
from ...utils.search import (
|
|
17
|
+
ParameterSpec,
|
|
18
|
+
ParameterType,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SGIRMolecularPredictor(GREAMolecularPredictor):
|
|
23
|
+
"""
|
|
24
|
+
This predictor implements SGIR for semi-supervised graph imbalanced regression.
|
|
25
|
+
|
|
26
|
+
It trains the GREA model based on pseudo-labeling and data augmentation.
|
|
27
|
+
|
|
28
|
+
References
|
|
29
|
+
----------
|
|
30
|
+
- Semi-Supervised Graph Imbalanced Regression.
|
|
31
|
+
https://dl.acm.org/doi/10.1145/3580305.3599497
|
|
32
|
+
- Code: https://github.com/liugangcode/SGIR
|
|
33
|
+
|
|
34
|
+
:param num_anchor: Number of anchor points used to split the label space during pseudo-labeling
|
|
35
|
+
:type num_anchor: int, default=10
|
|
36
|
+
:param warmup_epoch: Number of epochs to train before starting pseudo-labeling and data augmentation
|
|
37
|
+
:type warmup_epoch: int, default=20
|
|
38
|
+
:param labeling_interval: Interval (in epochs) between pseudo-labeling steps
|
|
39
|
+
:type labeling_interval: int, default=5
|
|
40
|
+
:param augmentation_interval: Interval (in epochs) between data augmentation steps
|
|
41
|
+
:type augmentation_interval: int, default=5
|
|
42
|
+
:param top_quantile: Quantile threshold for selecting high confidence predictions during pseudo-labeling
|
|
43
|
+
:type top_quantile: float, default=0.1
|
|
44
|
+
:param label_logscale: Whether to use log scale for the label space during pseudo-labeling and data augmentation
|
|
45
|
+
:type label_logscale: bool, default=False
|
|
46
|
+
:param lw_aug: Weight for the data augmentation loss
|
|
47
|
+
:type lw_aug: float, default=1
|
|
48
|
+
"""
|
|
49
|
+
# SGIR-specific parameters
|
|
50
|
+
num_anchor: int = 10
|
|
51
|
+
warmup_epoch: int = 20
|
|
52
|
+
labeling_interval: int = 5
|
|
53
|
+
augmentation_interval: int = 5
|
|
54
|
+
top_quantile: float = 0.1
|
|
55
|
+
label_logscale: bool = False
|
|
56
|
+
lw_aug: float = 1
|
|
57
|
+
# Override parent defaults
|
|
58
|
+
task_type: str = "regression"
|
|
59
|
+
model_name: str = "SGIRMolecularPredictor"
|
|
60
|
+
|
|
61
|
+
def __post_init__(self):
|
|
62
|
+
super().__post_init__()
|
|
63
|
+
|
|
64
|
+
if self.task_type != "regression" or self.num_task != 1:
|
|
65
|
+
raise ValueError("SGIR only supports regression tasks with 1 task")
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _get_param_names():
|
|
69
|
+
grea_params = [
|
|
70
|
+
"num_anchor", "warmup_epoch", "labeling_interval",
|
|
71
|
+
"augmentation_interval", "top_quantile", "label_logscale", "lw_aug"
|
|
72
|
+
]
|
|
73
|
+
return grea_params + GREAMolecularPredictor._get_param_names()
|
|
74
|
+
|
|
75
|
+
def _get_default_search_space(self):
|
|
76
|
+
search_space = super()._get_default_search_space().copy()
|
|
77
|
+
search_space["num_anchor"] = ParameterSpec(ParameterType.INTEGER, (10, 100))
|
|
78
|
+
search_space["labeling_interval"] = ParameterSpec(ParameterType.INTEGER, (10, 20))
|
|
79
|
+
search_space["augmentation_interval"] = ParameterSpec(ParameterType.INTEGER, (10, 20))
|
|
80
|
+
search_space["top_quantile"] = ParameterSpec(ParameterType.LOG_FLOAT, (0.01, 0.5))
|
|
81
|
+
search_space["lw_aug"] = ParameterSpec(ParameterType.FLOAT, (0.1, 1))
|
|
82
|
+
return search_space
|
|
83
|
+
|
|
84
|
+
def fit(
|
|
85
|
+
self,
|
|
86
|
+
X_train: List[str],
|
|
87
|
+
y_train: Optional[Union[List, np.ndarray]],
|
|
88
|
+
X_val: Optional[List[str]] = None,
|
|
89
|
+
y_val: Optional[Union[List, np.ndarray]] = None,
|
|
90
|
+
X_unlbl: Optional[List[str]] = None,
|
|
91
|
+
) -> "SGIRMolecularPredictor":
|
|
92
|
+
"""Fit the model to training data with optional validation set.
|
|
93
|
+
"""
|
|
94
|
+
if (X_val is None) != (y_val is None):
|
|
95
|
+
raise ValueError("X_val and y_val must both be provided for validation")
|
|
96
|
+
if X_unlbl is None:
|
|
97
|
+
raise ValueError("X_unlbl (unlabeled SMILES strings) must be provided in SGIR")
|
|
98
|
+
if len(X_unlbl) == 0:
|
|
99
|
+
raise ValueError("X_unlbl (unlabeled SMILES strings) must not be empty")
|
|
100
|
+
|
|
101
|
+
# Initialize model and optimization
|
|
102
|
+
self._initialize_model(self.model_class)
|
|
103
|
+
self.model.initialize_parameters()
|
|
104
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
105
|
+
|
|
106
|
+
# Prepare datasets
|
|
107
|
+
X_train, y_train = self._validate_inputs(X_train, y_train)
|
|
108
|
+
train_dataset = self._convert_to_pytorch_data(X_train, y_train)
|
|
109
|
+
train_loader = DataLoader(
|
|
110
|
+
train_dataset,
|
|
111
|
+
batch_size=self.batch_size,
|
|
112
|
+
shuffle=True,
|
|
113
|
+
num_workers=0
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
X_unlbl, _ = self._validate_inputs(X_unlbl, None)
|
|
117
|
+
unlbl_dataset = self._convert_to_pytorch_data(X_unlbl)
|
|
118
|
+
|
|
119
|
+
if X_val is None:
|
|
120
|
+
val_loader = train_loader
|
|
121
|
+
warnings.warn(
|
|
122
|
+
"No validation set provided. Using training set for validation.",
|
|
123
|
+
UserWarning
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
X_val, y_val = self._validate_inputs(X_val, y_val)
|
|
127
|
+
val_dataset = self._convert_to_pytorch_data(X_val, y_val)
|
|
128
|
+
val_loader = DataLoader(
|
|
129
|
+
val_dataset,
|
|
130
|
+
batch_size=self.batch_size,
|
|
131
|
+
shuffle=False,
|
|
132
|
+
num_workers=0
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Training loop
|
|
136
|
+
augmented_dataset = None
|
|
137
|
+
self.fitting_loss = []
|
|
138
|
+
self.fitting_epoch = 0
|
|
139
|
+
best_state_dict = None
|
|
140
|
+
best_eval = float('-inf') if self.evaluate_higher_better else float('inf')
|
|
141
|
+
cnt_wait = 0
|
|
142
|
+
|
|
143
|
+
self.model.train()
|
|
144
|
+
for epoch in range(self.epochs):
|
|
145
|
+
# Training phase
|
|
146
|
+
train_losses = self._train_epoch(train_loader, augmented_dataset, optimizer, epoch)
|
|
147
|
+
|
|
148
|
+
# Update datasets after warmup
|
|
149
|
+
if epoch > self.warmup_epoch:
|
|
150
|
+
if epoch % self.labeling_interval == 0:
|
|
151
|
+
train_loader = build_selection_dataset(
|
|
152
|
+
self.model, train_dataset, unlbl_dataset,
|
|
153
|
+
self.batch_size, self.num_anchor, self.top_quantile,
|
|
154
|
+
self.device, self.label_logscale
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if epoch % self.augmentation_interval == 0:
|
|
158
|
+
augmented_dataset = build_augmentation_dataset(
|
|
159
|
+
self.model, train_dataset, unlbl_dataset,
|
|
160
|
+
self.batch_size, self.num_anchor, self.device,
|
|
161
|
+
self.label_logscale
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self.fitting_loss.append(np.mean(train_losses))
|
|
165
|
+
|
|
166
|
+
# Validation and model selection
|
|
167
|
+
current_eval = self._evaluation_epoch(val_loader)
|
|
168
|
+
if scheduler:
|
|
169
|
+
scheduler.step(current_eval)
|
|
170
|
+
|
|
171
|
+
is_better = (
|
|
172
|
+
current_eval > best_eval if self.evaluate_higher_better
|
|
173
|
+
else current_eval < best_eval
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if is_better:
|
|
177
|
+
self.fitting_epoch = epoch
|
|
178
|
+
best_eval = current_eval
|
|
179
|
+
best_state_dict = self.model.state_dict()
|
|
180
|
+
cnt_wait = 0
|
|
181
|
+
else:
|
|
182
|
+
cnt_wait += 1
|
|
183
|
+
if cnt_wait > self.patience:
|
|
184
|
+
if self.verbose:
|
|
185
|
+
print(f"Early stopping at epoch {epoch}")
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
if self.verbose and epoch % 10 == 0:
|
|
189
|
+
print(
|
|
190
|
+
f"Epoch {epoch}: Loss = {np.mean(train_losses):.4f}, "
|
|
191
|
+
f"{self.evaluate_name} = {current_eval:.4f}, "
|
|
192
|
+
f"Best {self.evaluate_name} = {best_eval:.4f}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Restore best model
|
|
196
|
+
if best_state_dict is not None:
|
|
197
|
+
self.model.load_state_dict(best_state_dict)
|
|
198
|
+
else:
|
|
199
|
+
warnings.warn(
|
|
200
|
+
"No improvement achieved during training.",
|
|
201
|
+
UserWarning
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self.is_fitted_ = True
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def _train_epoch(self, train_loader, augmented_dataset, optimizer, epoch):
|
|
208
|
+
losses = []
|
|
209
|
+
|
|
210
|
+
if augmented_dataset is not None and self.lw_aug != 0:
|
|
211
|
+
aug_reps = augmented_dataset['representations']
|
|
212
|
+
aug_targets = augmented_dataset['labels']
|
|
213
|
+
random_inds = torch.randperm(aug_reps.size(0))
|
|
214
|
+
aug_reps = aug_reps[random_inds]
|
|
215
|
+
aug_targets = aug_targets[random_inds]
|
|
216
|
+
num_step = len(train_loader)
|
|
217
|
+
aug_batch_size = aug_reps.size(0) // max(1, num_step)
|
|
218
|
+
aug_inputs = list(torch.split(aug_reps, aug_batch_size))
|
|
219
|
+
aug_outputs = list(torch.split(aug_targets, aug_batch_size))
|
|
220
|
+
else:
|
|
221
|
+
aug_inputs = None
|
|
222
|
+
aug_outputs = None
|
|
223
|
+
|
|
224
|
+
iterator = (
|
|
225
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
226
|
+
if self.verbose
|
|
227
|
+
else train_loader
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
for batch_idx, batch in enumerate(iterator):
|
|
231
|
+
batch = batch.to(self.device)
|
|
232
|
+
optimizer.zero_grad()
|
|
233
|
+
|
|
234
|
+
# augmentation loss
|
|
235
|
+
if aug_inputs is not None and aug_outputs is not None and aug_inputs[batch_idx].size(0) != 1:
|
|
236
|
+
self.model._disable_batchnorm_tracking(self.model)
|
|
237
|
+
pred_aug = self.model.predictor(aug_inputs[batch_idx])
|
|
238
|
+
self.model._enable_batchnorm_tracking(self.model)
|
|
239
|
+
targets_aug = aug_outputs[batch_idx]
|
|
240
|
+
Laug = self.loss_criterion(pred_aug.view(targets_aug.size()).to(torch.float32), targets_aug).mean()
|
|
241
|
+
else:
|
|
242
|
+
Laug = torch.tensor(0.)
|
|
243
|
+
Lx = self.model.compute_loss(batch, self.loss_criterion)
|
|
244
|
+
loss = Lx + Laug * self.lw_aug
|
|
245
|
+
|
|
246
|
+
loss.backward()
|
|
247
|
+
|
|
248
|
+
# Compute gradient norm if gradient clipping is enabled
|
|
249
|
+
if self.grad_clip_value is not None:
|
|
250
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
251
|
+
|
|
252
|
+
optimizer.step()
|
|
253
|
+
|
|
254
|
+
losses.append(loss.item())
|
|
255
|
+
|
|
256
|
+
# Update progress bar if using tqdm
|
|
257
|
+
if self.verbose:
|
|
258
|
+
iterator.set_postfix({"Epoch": epoch, "Total Loss": f"{loss.item():.4f}", "Lbls Loss": f"{Lx.item():.4f}", "Aug Loss": f"{Laug.item():.4f}",})
|
|
259
|
+
|
|
260
|
+
return losses
|