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,530 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Union, Optional
|
|
3
|
+
import numpy as np
|
|
4
|
+
from sklearn.metrics import roc_auc_score as sk_roc_auc_score
|
|
5
|
+
from sklearn.exceptions import UndefinedMetricWarning
|
|
6
|
+
from sklearn.metrics import mean_absolute_error as sk_mae
|
|
7
|
+
from sklearn.metrics import mean_squared_error as sk_mse
|
|
8
|
+
from sklearn.metrics import r2_score as sk_r2_score
|
|
9
|
+
|
|
10
|
+
def sigmoid(x):
|
|
11
|
+
"""Numerically stable sigmoid function."""
|
|
12
|
+
return np.where(x >= 0,
|
|
13
|
+
1 / (1 + np.exp(-x)),
|
|
14
|
+
np.exp(x) / (1 + np.exp(x)))
|
|
15
|
+
|
|
16
|
+
def roc_auc_score(
|
|
17
|
+
y_true: Union[np.ndarray, list],
|
|
18
|
+
y_pred: Union[np.ndarray, list],
|
|
19
|
+
average: bool = True,
|
|
20
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
21
|
+
) -> Union[float, np.ndarray]:
|
|
22
|
+
"""Calculate ROC AUC scores for multi-task binary classification, handling NaN values.
|
|
23
|
+
|
|
24
|
+
For each task dimension, computes AUC score using only the non-NaN samples.
|
|
25
|
+
Tasks with insufficient valid samples or unique labels are masked in the output.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
y_true : Union[np.ndarray, list]
|
|
30
|
+
True binary labels. Shape should be (n_samples, n_tasks)
|
|
31
|
+
y_pred : Union[np.ndarray, list]
|
|
32
|
+
Predicted probabilities. Shape should be (n_samples, n_tasks)
|
|
33
|
+
average : bool, default=True
|
|
34
|
+
If True, return the average ROC AUC score across all valid tasks.
|
|
35
|
+
If False, return individual scores for each task (NaN for invalid tasks).
|
|
36
|
+
sample_weight : Optional[np.ndarray], default=None
|
|
37
|
+
Sample weights for each instance. Shape should be (n_samples,)
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
Union[float, np.ndarray]
|
|
42
|
+
If average=True, returns mean ROC AUC score across all valid tasks.
|
|
43
|
+
If average=False, returns array of ROC AUC scores with NaN for invalid tasks.
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
ValueError
|
|
48
|
+
If input shapes don't match or no valid tasks are found
|
|
49
|
+
TypeError
|
|
50
|
+
If inputs are not of correct type
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> y_true = np.array([[0, 1, np.nan], [1, 0, 1], [1, np.nan, 0], [0, 0, 1]])
|
|
55
|
+
>>> y_pred = np.array([[0.1, 0.8, 0.7], [0.9, 0.2, 0.8], [0.8, 0.7, 0.3], [0.2, 0.1, 0.9]])
|
|
56
|
+
>>> score = roc_auc_score(y_true, y_pred)
|
|
57
|
+
>>> print(f"Average ROC AUC across valid tasks: {score:.3f}")
|
|
58
|
+
"""
|
|
59
|
+
# Convert inputs to numpy arrays if needed
|
|
60
|
+
try:
|
|
61
|
+
y_true = np.asarray(y_true)
|
|
62
|
+
y_pred = np.asarray(y_pred)
|
|
63
|
+
if len(y_true.shape) == 1:
|
|
64
|
+
y_true = y_true.reshape(-1, 1)
|
|
65
|
+
except (ValueError, TypeError) as e:
|
|
66
|
+
raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
|
|
67
|
+
|
|
68
|
+
# Validate input shapes
|
|
69
|
+
if y_true.shape != y_pred.shape:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if y_true.ndim != 2:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if sample_weight is not None:
|
|
80
|
+
sample_weight = np.asarray(sample_weight)
|
|
81
|
+
if sample_weight.shape[0] != y_true.shape[0]:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
n_tasks = y_true.shape[1]
|
|
87
|
+
auc_scores = np.full(n_tasks, np.nan) # Initialize with NaN
|
|
88
|
+
valid_task_mask = np.zeros(n_tasks, dtype=bool)
|
|
89
|
+
|
|
90
|
+
# Calculate AUC for each task
|
|
91
|
+
for task_idx in range(n_tasks):
|
|
92
|
+
# Get valid sample mask for this task
|
|
93
|
+
valid_samples = ~np.isnan(y_true[:, task_idx])
|
|
94
|
+
|
|
95
|
+
if not np.any(valid_samples):
|
|
96
|
+
continue # Skip if no valid samples
|
|
97
|
+
|
|
98
|
+
task_true = y_true[valid_samples, task_idx]
|
|
99
|
+
task_pred = y_pred[valid_samples, task_idx]
|
|
100
|
+
|
|
101
|
+
# Get task-specific sample weights if provided
|
|
102
|
+
task_weights = None
|
|
103
|
+
if sample_weight is not None:
|
|
104
|
+
task_weights = sample_weight[valid_samples]
|
|
105
|
+
|
|
106
|
+
# Check for valid binary labels
|
|
107
|
+
unique_labels = np.unique(task_true)
|
|
108
|
+
if len(unique_labels) < 2:
|
|
109
|
+
continue # Skip if not enough unique labels
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with warnings.catch_warnings():
|
|
113
|
+
warnings.filterwarnings('ignore', category=UndefinedMetricWarning)
|
|
114
|
+
auc_scores[task_idx] = sk_roc_auc_score(
|
|
115
|
+
task_true,
|
|
116
|
+
task_pred,
|
|
117
|
+
sample_weight=task_weights
|
|
118
|
+
)
|
|
119
|
+
valid_task_mask[task_idx] = True
|
|
120
|
+
except Exception:
|
|
121
|
+
continue # Skip if AUC calculation fails
|
|
122
|
+
|
|
123
|
+
# Check if any valid tasks remain
|
|
124
|
+
if not np.any(valid_task_mask):
|
|
125
|
+
raise ValueError("No valid tasks found for AUC calculation")
|
|
126
|
+
|
|
127
|
+
# Return results based on averaging preference
|
|
128
|
+
if average:
|
|
129
|
+
return float(np.nanmean(auc_scores))
|
|
130
|
+
else:
|
|
131
|
+
return auc_scores
|
|
132
|
+
|
|
133
|
+
def accuracy_score(y_true, logits, avergae=None, thresholds=None, task_weights=None, task_types=None):
|
|
134
|
+
"""
|
|
135
|
+
Calculate accuracy for multiple tasks from prediction logits.
|
|
136
|
+
|
|
137
|
+
Parameters:
|
|
138
|
+
-----------
|
|
139
|
+
y_true : numpy.ndarray
|
|
140
|
+
Ground truth labels with shape (n_samples, n_tasks)
|
|
141
|
+
logits : numpy.ndarray
|
|
142
|
+
Prediction logits with shape (n_samples, n_tasks)
|
|
143
|
+
task_types : list or None, optional
|
|
144
|
+
List of task types ('binary' or 'multiclass') for each task
|
|
145
|
+
If None, assumes all tasks are binary
|
|
146
|
+
thresholds : numpy.ndarray or None, optional
|
|
147
|
+
Classification thresholds for binary tasks with shape (n_tasks,)
|
|
148
|
+
If None, uses 0.5 for all binary tasks
|
|
149
|
+
task_weights : numpy.ndarray or None, optional
|
|
150
|
+
Weights for each task with shape (n_tasks,)
|
|
151
|
+
If None, all tasks are weighted equally
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
--------
|
|
155
|
+
dict
|
|
156
|
+
A dictionary containing:
|
|
157
|
+
- 'task_accuracies': Accuracy for each individual task
|
|
158
|
+
- 'weighted_accuracy': Overall weighted accuracy across all tasks
|
|
159
|
+
- 'macro_accuracy': Simple average of all task accuracies
|
|
160
|
+
- 'predictions': Binary predictions after applying activation and thresholds
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
-------
|
|
164
|
+
ValueError
|
|
165
|
+
If input shapes don't match or dimensions are incorrect
|
|
166
|
+
"""
|
|
167
|
+
# Convert inputs to numpy arrays if needed
|
|
168
|
+
try:
|
|
169
|
+
y_true = np.asarray(y_true)
|
|
170
|
+
y_pred = np.asarray(y_pred)
|
|
171
|
+
if len(y_true.shape) == 1:
|
|
172
|
+
y_true = y_true.reshape(-1, 1)
|
|
173
|
+
except (ValueError, TypeError) as e:
|
|
174
|
+
raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
|
|
175
|
+
|
|
176
|
+
# Input validation
|
|
177
|
+
if y_true.shape != logits.shape:
|
|
178
|
+
raise ValueError(f"Shape mismatch: y_true {y_true.shape} != logits {logits.shape}")
|
|
179
|
+
|
|
180
|
+
if len(y_true.shape) != 2:
|
|
181
|
+
raise ValueError(f"Expected 2D arrays, got shape {y_true.shape}")
|
|
182
|
+
|
|
183
|
+
n_samples, n_tasks = y_true.shape
|
|
184
|
+
|
|
185
|
+
# Set default task types if none provided
|
|
186
|
+
if task_types is None:
|
|
187
|
+
task_types = ['binary'] * n_tasks
|
|
188
|
+
|
|
189
|
+
if len(task_types) != n_tasks:
|
|
190
|
+
raise ValueError(f"Task types length {len(task_types)} != number of tasks {n_tasks}")
|
|
191
|
+
|
|
192
|
+
# Set default thresholds if none provided
|
|
193
|
+
if thresholds is None:
|
|
194
|
+
thresholds = np.array([0.5] * n_tasks)
|
|
195
|
+
else:
|
|
196
|
+
thresholds = np.array(thresholds)
|
|
197
|
+
if len(thresholds) != n_tasks:
|
|
198
|
+
raise ValueError(f"Thresholds length {len(thresholds)} != number of tasks {n_tasks}")
|
|
199
|
+
|
|
200
|
+
# Set default weights if none provided
|
|
201
|
+
if task_weights is None:
|
|
202
|
+
task_weights = np.ones(n_tasks) / n_tasks
|
|
203
|
+
else:
|
|
204
|
+
if len(task_weights) != n_tasks:
|
|
205
|
+
raise ValueError(f"Task weights length {len(task_weights)} != number of tasks {n_tasks}")
|
|
206
|
+
# Normalize weights to sum to 1
|
|
207
|
+
task_weights = np.array(task_weights) / np.sum(task_weights)
|
|
208
|
+
|
|
209
|
+
# Initialize predictions array
|
|
210
|
+
y_pred = np.zeros_like(logits)
|
|
211
|
+
|
|
212
|
+
# Process each task
|
|
213
|
+
for task_idx in range(n_tasks):
|
|
214
|
+
task_type = task_types[task_idx]
|
|
215
|
+
task_logits = logits[:, task_idx]
|
|
216
|
+
|
|
217
|
+
if task_type == 'binary':
|
|
218
|
+
# Apply sigmoid activation
|
|
219
|
+
probabilities = 1 / (1 + np.exp(-task_logits))
|
|
220
|
+
# Apply threshold
|
|
221
|
+
y_pred[:, task_idx] = (probabilities >= thresholds[task_idx]).astype(int)
|
|
222
|
+
elif task_type == 'multiclass':
|
|
223
|
+
# For multiclass, assume logits are already proper shape and just take argmax
|
|
224
|
+
y_pred[:, task_idx] = np.argmax(task_logits, axis=-1)
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError(f"Unknown task type: {task_type}")
|
|
227
|
+
|
|
228
|
+
# Calculate accuracy for each task
|
|
229
|
+
task_accuracies = np.mean(y_true == y_pred, axis=0)
|
|
230
|
+
|
|
231
|
+
# Calculate weighted average accuracy
|
|
232
|
+
weighted_accuracy = np.sum(task_accuracies * task_weights)
|
|
233
|
+
|
|
234
|
+
# Calculate macro accuracy (simple average)
|
|
235
|
+
if avergae:
|
|
236
|
+
return float(np.mean(task_accuracies))
|
|
237
|
+
else:
|
|
238
|
+
return weighted_accuracy
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def mean_absolute_error(
|
|
242
|
+
y_true: Union[np.ndarray, list],
|
|
243
|
+
y_pred: Union[np.ndarray, list],
|
|
244
|
+
average: bool = True,
|
|
245
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
246
|
+
) -> Union[float, np.ndarray]:
|
|
247
|
+
"""Calculate Mean Absolute Error for multi-task regression, handling NaN values.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
y_true : Union[np.ndarray, list]
|
|
252
|
+
Ground truth values. Shape should be (n_samples, n_tasks)
|
|
253
|
+
y_pred : Union[np.ndarray, list]
|
|
254
|
+
Predicted values. Shape should be (n_samples, n_tasks)
|
|
255
|
+
average : bool, default=True
|
|
256
|
+
If True, return the average MAE across all valid tasks.
|
|
257
|
+
If False, return individual MAE for each task (NaN for invalid tasks).
|
|
258
|
+
sample_weight : Optional[np.ndarray], default=None
|
|
259
|
+
Sample weights. Shape should be (n_samples,)
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
Union[float, np.ndarray]
|
|
264
|
+
If average=True, returns mean MAE across all valid tasks.
|
|
265
|
+
If average=False, returns array of MAE scores with NaN for invalid tasks.
|
|
266
|
+
"""
|
|
267
|
+
# Convert inputs to numpy arrays if needed
|
|
268
|
+
try:
|
|
269
|
+
y_true = np.asarray(y_true)
|
|
270
|
+
y_pred = np.asarray(y_pred)
|
|
271
|
+
if len(y_true.shape) == 1:
|
|
272
|
+
y_true = y_true.reshape(-1, 1)
|
|
273
|
+
except (ValueError, TypeError) as e:
|
|
274
|
+
raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
|
|
275
|
+
|
|
276
|
+
# Validate input shapes
|
|
277
|
+
if y_true.shape != y_pred.shape:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if y_true.ndim != 2:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if sample_weight is not None:
|
|
288
|
+
sample_weight = np.asarray(sample_weight)
|
|
289
|
+
if sample_weight.shape[0] != y_true.shape[0]:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
n_tasks = y_true.shape[1]
|
|
295
|
+
mae_scores = np.full(n_tasks, np.nan)
|
|
296
|
+
valid_task_mask = np.zeros(n_tasks, dtype=bool)
|
|
297
|
+
|
|
298
|
+
# Calculate MAE for each task
|
|
299
|
+
for task_idx in range(n_tasks):
|
|
300
|
+
# Get valid sample mask for this task
|
|
301
|
+
valid_samples = ~np.isnan(y_true[:, task_idx]) & ~np.isnan(y_pred[:, task_idx])
|
|
302
|
+
|
|
303
|
+
if not np.any(valid_samples):
|
|
304
|
+
continue # Skip if no valid samples
|
|
305
|
+
|
|
306
|
+
task_true = y_true[valid_samples, task_idx]
|
|
307
|
+
task_pred = y_pred[valid_samples, task_idx]
|
|
308
|
+
|
|
309
|
+
# Get task-specific sample weights if provided
|
|
310
|
+
task_weights = None
|
|
311
|
+
if sample_weight is not None:
|
|
312
|
+
task_weights = sample_weight[valid_samples]
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
mae_scores[task_idx] = sk_mae(
|
|
316
|
+
task_true,
|
|
317
|
+
task_pred,
|
|
318
|
+
sample_weight=task_weights
|
|
319
|
+
)
|
|
320
|
+
valid_task_mask[task_idx] = True
|
|
321
|
+
except Exception:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# Check if any valid tasks remain
|
|
325
|
+
if not np.any(valid_task_mask):
|
|
326
|
+
raise ValueError("No valid tasks found for MAE calculation")
|
|
327
|
+
|
|
328
|
+
# Return results based on averaging preference
|
|
329
|
+
if average:
|
|
330
|
+
return float(np.nanmean(mae_scores))
|
|
331
|
+
else:
|
|
332
|
+
return mae_scores
|
|
333
|
+
|
|
334
|
+
def root_mean_squared_error(y_true, y_pred, average, sample_weight):
|
|
335
|
+
return mean_squared_error(y_true, y_pred, average, sample_weight, squared=False)
|
|
336
|
+
|
|
337
|
+
def mean_squared_error(
|
|
338
|
+
y_true: Union[np.ndarray, list],
|
|
339
|
+
y_pred: Union[np.ndarray, list],
|
|
340
|
+
average: bool = True,
|
|
341
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
342
|
+
squared: bool = True
|
|
343
|
+
) -> Union[float, np.ndarray]:
|
|
344
|
+
"""Calculate Mean Squared Error for multi-task regression, handling NaN values.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
y_true : Union[np.ndarray, list]
|
|
349
|
+
Ground truth values. Shape should be (n_samples, n_tasks)
|
|
350
|
+
y_pred : Union[np.ndarray, list]
|
|
351
|
+
Predicted values. Shape should be (n_samples, n_tasks)
|
|
352
|
+
average : bool, default=True
|
|
353
|
+
If True, return the average MSE across all valid tasks.
|
|
354
|
+
If False, return individual MSE for each task (NaN for invalid tasks).
|
|
355
|
+
sample_weight : Optional[np.ndarray], default=None
|
|
356
|
+
Sample weights. Shape should be (n_samples,)
|
|
357
|
+
squared : bool, default=True
|
|
358
|
+
If True, returns MSE value.
|
|
359
|
+
If False, returns RMSE value.
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
Union[float, np.ndarray]
|
|
364
|
+
If average=True, returns mean MSE/RMSE across all valid tasks.
|
|
365
|
+
If average=False, returns array of MSE/RMSE scores with NaN for invalid tasks.
|
|
366
|
+
"""
|
|
367
|
+
# Convert inputs to numpy arrays if needed
|
|
368
|
+
try:
|
|
369
|
+
y_true = np.asarray(y_true)
|
|
370
|
+
y_pred = np.asarray(y_pred)
|
|
371
|
+
if len(y_true.shape) == 1:
|
|
372
|
+
y_true = y_true.reshape(-1, 1)
|
|
373
|
+
except (ValueError, TypeError) as e:
|
|
374
|
+
raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
|
|
375
|
+
|
|
376
|
+
# Validate input shapes
|
|
377
|
+
if y_true.shape != y_pred.shape:
|
|
378
|
+
raise ValueError(
|
|
379
|
+
f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if y_true.ndim != 2:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if sample_weight is not None:
|
|
388
|
+
sample_weight = np.asarray(sample_weight)
|
|
389
|
+
if sample_weight.shape[0] != y_true.shape[0]:
|
|
390
|
+
raise ValueError(
|
|
391
|
+
f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
n_tasks = y_true.shape[1]
|
|
395
|
+
mse_scores = np.full(n_tasks, np.nan)
|
|
396
|
+
valid_task_mask = np.zeros(n_tasks, dtype=bool)
|
|
397
|
+
|
|
398
|
+
# Calculate MSE for each task
|
|
399
|
+
for task_idx in range(n_tasks):
|
|
400
|
+
# Get valid sample mask for this task
|
|
401
|
+
valid_samples = ~np.isnan(y_true[:, task_idx]) & ~np.isnan(y_pred[:, task_idx])
|
|
402
|
+
|
|
403
|
+
if not np.any(valid_samples):
|
|
404
|
+
continue # Skip if no valid samples
|
|
405
|
+
|
|
406
|
+
task_true = y_true[valid_samples, task_idx]
|
|
407
|
+
task_pred = y_pred[valid_samples, task_idx]
|
|
408
|
+
|
|
409
|
+
# Get task-specific sample weights if provided
|
|
410
|
+
task_weights = None
|
|
411
|
+
if sample_weight is not None:
|
|
412
|
+
task_weights = sample_weight[valid_samples]
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
mse_scores[task_idx] = sk_mse(
|
|
416
|
+
task_true,
|
|
417
|
+
task_pred,
|
|
418
|
+
sample_weight=task_weights,
|
|
419
|
+
)
|
|
420
|
+
valid_task_mask[task_idx] = True
|
|
421
|
+
except Exception:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
# Check if any valid tasks remain
|
|
425
|
+
if not np.any(valid_task_mask):
|
|
426
|
+
raise ValueError("No valid tasks found for MSE calculation")
|
|
427
|
+
|
|
428
|
+
# Convert to RMSE if requested
|
|
429
|
+
if not squared:
|
|
430
|
+
mse_scores = np.sqrt(mse_scores)
|
|
431
|
+
|
|
432
|
+
# Return results based on averaging preference
|
|
433
|
+
if average:
|
|
434
|
+
return float(np.nanmean(mse_scores))
|
|
435
|
+
else:
|
|
436
|
+
return mse_scores
|
|
437
|
+
|
|
438
|
+
def r2_score(
|
|
439
|
+
y_true: Union[np.ndarray, list],
|
|
440
|
+
y_pred: Union[np.ndarray, list],
|
|
441
|
+
average: bool = True,
|
|
442
|
+
sample_weight: Optional[np.ndarray] = None,
|
|
443
|
+
) -> Union[float, np.ndarray]:
|
|
444
|
+
"""Calculate R² Score for multi-task regression, handling NaN values.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
y_true : Union[np.ndarray, list]
|
|
449
|
+
Ground truth values. Shape should be (n_samples, n_tasks)
|
|
450
|
+
y_pred : Union[np.ndarray, list]
|
|
451
|
+
Predicted values. Shape should be (n_samples, n_tasks)
|
|
452
|
+
average : bool, default=True
|
|
453
|
+
If True, return the average R² across all valid tasks.
|
|
454
|
+
If False, return individual R² for each task (NaN for invalid tasks).
|
|
455
|
+
sample_weight : Optional[np.ndarray], default=None
|
|
456
|
+
Sample weights. Shape should be (n_samples,)
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
Union[float, np.ndarray]
|
|
461
|
+
If average=True, returns mean R² across all valid tasks.
|
|
462
|
+
If average=False, returns array of R² scores with NaN for invalid tasks.
|
|
463
|
+
"""
|
|
464
|
+
# Convert inputs to numpy arrays
|
|
465
|
+
try:
|
|
466
|
+
y_true = np.asarray(y_true)
|
|
467
|
+
y_pred = np.asarray(y_pred)
|
|
468
|
+
if len(y_true.shape) == 1:
|
|
469
|
+
y_true = y_true.reshape(-1, 1)
|
|
470
|
+
except (ValueError, TypeError) as e:
|
|
471
|
+
raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
|
|
472
|
+
|
|
473
|
+
# Validate input shapes
|
|
474
|
+
if y_true.shape != y_pred.shape:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if y_true.ndim != 2:
|
|
480
|
+
raise ValueError(
|
|
481
|
+
f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if sample_weight is not None:
|
|
485
|
+
sample_weight = np.asarray(sample_weight)
|
|
486
|
+
if sample_weight.shape[0] != y_true.shape[0]:
|
|
487
|
+
raise ValueError(
|
|
488
|
+
f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
n_tasks = y_true.shape[1]
|
|
492
|
+
r2_scores = np.full(n_tasks, np.nan)
|
|
493
|
+
valid_task_mask = np.zeros(n_tasks, dtype=bool)
|
|
494
|
+
|
|
495
|
+
# Calculate R² for each task
|
|
496
|
+
for task_idx in range(n_tasks):
|
|
497
|
+
# Get valid sample mask for this task
|
|
498
|
+
valid_samples = ~np.isnan(y_true[:, task_idx]) & ~np.isnan(y_pred[:, task_idx])
|
|
499
|
+
|
|
500
|
+
if not np.any(valid_samples):
|
|
501
|
+
continue # Skip if no valid samples
|
|
502
|
+
|
|
503
|
+
task_true = y_true[valid_samples, task_idx]
|
|
504
|
+
task_pred = y_pred[valid_samples, task_idx]
|
|
505
|
+
|
|
506
|
+
# Get task-specific sample weights if provided
|
|
507
|
+
task_weights = None
|
|
508
|
+
if sample_weight is not None:
|
|
509
|
+
task_weights = sample_weight[valid_samples]
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
r2_scores[task_idx] = sk_r2_score(
|
|
513
|
+
task_true,
|
|
514
|
+
task_pred,
|
|
515
|
+
sample_weight=task_weights
|
|
516
|
+
)
|
|
517
|
+
valid_task_mask[task_idx] = True
|
|
518
|
+
except Exception:
|
|
519
|
+
continue
|
|
520
|
+
|
|
521
|
+
# Check if any valid tasks remain
|
|
522
|
+
if not np.any(valid_task_mask):
|
|
523
|
+
raise ValueError("No valid tasks found for R² calculation")
|
|
524
|
+
|
|
525
|
+
# Return results based on averaging preference
|
|
526
|
+
if average:
|
|
527
|
+
return float(np.nanmean(r2_scores))
|
|
528
|
+
else:
|
|
529
|
+
return r2_scores
|
|
530
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
PSEUDOTASK = {
|
|
2
|
+
"morgan": (1024, "classification"),
|
|
3
|
+
"maccs": (167, "classification"),
|
|
4
|
+
"logP": (1, "regression"),
|
|
5
|
+
# "SAS": (1, "regression"),
|
|
6
|
+
# "TPSA": (1, "regression"),
|
|
7
|
+
# "QED": (1, "regression"),
|
|
8
|
+
# "cycle_score": (1, "regression"),
|
|
9
|
+
# "SA_score": (1, "regression"),
|
|
10
|
+
# "weight": (1, "regression"),
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
def init_weights(net, init_type='xavier', init_gain=0.02, verbose=False):
|
|
4
|
+
"""Initialize network weights.
|
|
5
|
+
Parameters:
|
|
6
|
+
net (network) -- network to be initialized
|
|
7
|
+
init_type (str) -- the name of an initialization method: normal | xavier | kaiming | orthogonal
|
|
8
|
+
init_gain (float) -- scaling factor for normal, xavier and orthogonal.
|
|
9
|
+
"""
|
|
10
|
+
def init_func(m): # define the initialization function
|
|
11
|
+
classname = m.__class__.__name__
|
|
12
|
+
if hasattr(m, 'weight') and (classname.find('Conv') != -1 or classname.find('Linear') != -1):
|
|
13
|
+
if init_type == 'normal':
|
|
14
|
+
torch.nn.init.normal_(m.weight.data, 0.0, init_gain)
|
|
15
|
+
elif init_type == 'xavier':
|
|
16
|
+
torch.nn.init.xavier_normal_(m.weight.data, gain=init_gain)
|
|
17
|
+
elif init_type == 'kaiming':
|
|
18
|
+
torch.nn.init.kaiming_normal_(m.weight.data, a=0, mode='fan_in')
|
|
19
|
+
elif init_type == 'orthogonal':
|
|
20
|
+
torch.nn.init.orthogonal_(m.weight.data)
|
|
21
|
+
else:
|
|
22
|
+
raise NotImplementedError('initialization method [%s] is not implemented' % init_type)
|
|
23
|
+
if hasattr(m, 'bias') and m.bias is not None:
|
|
24
|
+
torch.nn.init.constant_(m.bias.data, 0.0)
|
|
25
|
+
elif classname.find('BatchNorm2d') != -1: # BatchNorm Layer's weight is not a matrix
|
|
26
|
+
torch.nn.init.normal_(m.weight.data, 1.0, init_gain)
|
|
27
|
+
torch.nn.init.constant_(m.bias.data, 0.0)
|
|
28
|
+
if verbose:
|
|
29
|
+
print('initialize network with %s' % init_type)
|
|
30
|
+
net.apply(init_func) # apply the initialization function
|