D4CMPP2 0.4.0__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.
- D4CMPP2/_Data/AGENTS.md +24 -0
- D4CMPP2/_Data/Aqsoldb.csv +9291 -0
- D4CMPP2/_Data/BradleyMP.csv +3042 -0
- D4CMPP2/_Data/Lipophilicity.csv +1131 -0
- D4CMPP2/_Data/README.md +8 -0
- D4CMPP2/_Data/__init__.py +26 -0
- D4CMPP2/_Data/optical.csv +20237 -0
- D4CMPP2/_Data/test.csv +190 -0
- D4CMPP2/__init__.py +16 -0
- D4CMPP2/__main__.py +5 -0
- D4CMPP2/_main.py +500 -0
- D4CMPP2/cli.py +7 -0
- D4CMPP2/exceptions.py +53 -0
- D4CMPP2/grid_search.py +259 -0
- D4CMPP2/network_refer.yaml +160 -0
- D4CMPP2/networks/AFP_model.py +72 -0
- D4CMPP2/networks/AFPwithSolv_model.py +72 -0
- D4CMPP2/networks/DMPNN_model.py +90 -0
- D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
- D4CMPP2/networks/GAT_model.py +48 -0
- D4CMPP2/networks/GATwithSolv_model.py +63 -0
- D4CMPP2/networks/GCN_model.py +113 -0
- D4CMPP2/networks/GCNwithSolv_model.py +103 -0
- D4CMPP2/networks/GC_model.py +122 -0
- D4CMPP2/networks/ISATPM_model.py +14 -0
- D4CMPP2/networks/ISATPN_model.py +199 -0
- D4CMPP2/networks/ISAT_model.py +90 -0
- D4CMPP2/networks/MPNN_model.py +56 -0
- D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
- D4CMPP2/networks/__init__.py +25 -0
- D4CMPP2/networks/base.py +250 -0
- D4CMPP2/networks/registry.py +187 -0
- D4CMPP2/networks/src/AFP.py +118 -0
- D4CMPP2/networks/src/BiDropout.py +29 -0
- D4CMPP2/networks/src/DMPNN.py +35 -0
- D4CMPP2/networks/src/GAT.py +69 -0
- D4CMPP2/networks/src/GC.py +85 -0
- D4CMPP2/networks/src/GCN.py +71 -0
- D4CMPP2/networks/src/ISAT.py +153 -0
- D4CMPP2/networks/src/Linear.py +49 -0
- D4CMPP2/networks/src/MPNN.py +56 -0
- D4CMPP2/networks/src/SolventLayer.py +62 -0
- D4CMPP2/networks/src/__init__.py +0 -0
- D4CMPP2/networks/src/distGCN.py +21 -0
- D4CMPP2/networks/src/pyg_hetero.py +24 -0
- D4CMPP2/optimize.py +472 -0
- D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
- D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
- D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
- D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
- D4CMPP2/src/Analyzer/__init__.py +54 -0
- D4CMPP2/src/Analyzer/core.py +480 -0
- D4CMPP2/src/Analyzer/factory.py +166 -0
- D4CMPP2/src/Analyzer/interpretation.py +232 -0
- D4CMPP2/src/Analyzer/results.py +101 -0
- D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
- D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
- D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
- D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
- D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
- D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
- D4CMPP2/src/DataManager/ISADataManager.py +67 -0
- D4CMPP2/src/DataManager/MolDataManager.py +735 -0
- D4CMPP2/src/DataManager/__init__.py +14 -0
- D4CMPP2/src/DataManager/contracts.py +179 -0
- D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
- D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
- D4CMPP2/src/NetworkManager/__init__.py +14 -0
- D4CMPP2/src/PostProcessor.py +160 -0
- D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
- D4CMPP2/src/TrainManager/TrainManager.py +254 -0
- D4CMPP2/src/TrainManager/__init__.py +14 -0
- D4CMPP2/src/TrainManager/callbacks.py +119 -0
- D4CMPP2/src/__init__.py +0 -0
- D4CMPP2/src/utils/PATH.py +246 -0
- D4CMPP2/src/utils/__init__.py +0 -0
- D4CMPP2/src/utils/argparser.py +56 -0
- D4CMPP2/src/utils/checkpointing.py +90 -0
- D4CMPP2/src/utils/config_resolution.py +123 -0
- D4CMPP2/src/utils/config_validation.py +370 -0
- D4CMPP2/src/utils/csv_validation.py +105 -0
- D4CMPP2/src/utils/data_quality.py +181 -0
- D4CMPP2/src/utils/featureizer.py +202 -0
- D4CMPP2/src/utils/functional_group.csv +169 -0
- D4CMPP2/src/utils/graph_cache.py +213 -0
- D4CMPP2/src/utils/leaderboard.py +212 -0
- D4CMPP2/src/utils/metrics.py +31 -0
- D4CMPP2/src/utils/module_loader.py +147 -0
- D4CMPP2/src/utils/output.py +80 -0
- D4CMPP2/src/utils/reproducibility.py +70 -0
- D4CMPP2/src/utils/run_manifest.py +175 -0
- D4CMPP2/src/utils/scaler.py +40 -0
- D4CMPP2/src/utils/sculptor.py +713 -0
- D4CMPP2/src/utils/splitting.py +250 -0
- D4CMPP2/src/utils/supportfile_saver.py +94 -0
- D4CMPP2/src/utils/tools.py +156 -0
- D4CMPP2/src/utils/transfer_learning.py +111 -0
- d4cmpp2-0.4.0.dist-info/METADATA +420 -0
- d4cmpp2-0.4.0.dist-info/RECORD +103 -0
- d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
- d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
- d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
- d4cmpp2-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os, sys
|
|
4
|
+
import yaml
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
from D4CMPP2.src.utils.config_validation import validate_runtime_environment
|
|
8
|
+
from D4CMPP2.src.utils.checkpointing import (
|
|
9
|
+
CHECKPOINT_SCHEMA_VERSION,
|
|
10
|
+
atomic_torch_save,
|
|
11
|
+
capture_rng_state,
|
|
12
|
+
load_checkpoint,
|
|
13
|
+
resolve_resume_checkpoint,
|
|
14
|
+
restore_rng_state,
|
|
15
|
+
)
|
|
16
|
+
from D4CMPP2.src.utils.transfer_learning import (
|
|
17
|
+
file_sha256,
|
|
18
|
+
select_compatible_state,
|
|
19
|
+
write_transfer_report,
|
|
20
|
+
)
|
|
21
|
+
from D4CMPP2.src.utils.output import get_output
|
|
22
|
+
|
|
23
|
+
class NetworkManager:
|
|
24
|
+
|
|
25
|
+
def __init__(self, config, tf_path=None, unwrapper=None, temp=False, resume_path=None):
|
|
26
|
+
|
|
27
|
+
self.config = config
|
|
28
|
+
self.output = get_output(config)
|
|
29
|
+
self.device = config.get('device', 'cpu')
|
|
30
|
+
validate_runtime_environment(config, backend="pyg", torch_module=torch)
|
|
31
|
+
self.output.info(f"[Runtime] Compute device: {self.device!r}.")
|
|
32
|
+
self.last_lr = config.get('learning_rate',0.001)
|
|
33
|
+
self.unwrapper = unwrapper
|
|
34
|
+
self.best_loss = float('inf')
|
|
35
|
+
self.es_patience = config.get('early_stopping_patience',50)
|
|
36
|
+
self.es_counter = 0
|
|
37
|
+
self.state= "train"
|
|
38
|
+
self.tf_path = tf_path
|
|
39
|
+
self.schedulers = []
|
|
40
|
+
self.temp = temp
|
|
41
|
+
self.resume_path = resume_path
|
|
42
|
+
self.completed_epoch = -1
|
|
43
|
+
self.next_epoch = 0
|
|
44
|
+
self.best_epoch = None
|
|
45
|
+
self.run_id = config.get("run_id")
|
|
46
|
+
|
|
47
|
+
if self.tf_path:
|
|
48
|
+
self.transferlearn_network()
|
|
49
|
+
else:
|
|
50
|
+
self.init_network(load_existing=not bool(self.resume_path))
|
|
51
|
+
|
|
52
|
+
self.init_optimizer(config.get('lr_dict',{}) )
|
|
53
|
+
self.init_scheduler()
|
|
54
|
+
if self.resume_path:
|
|
55
|
+
self.load_training_checkpoint(self.resume_path)
|
|
56
|
+
|
|
57
|
+
def set_unwrapper(self, unwrapper):
|
|
58
|
+
self.unwrapper = unwrapper
|
|
59
|
+
|
|
60
|
+
def _output(self):
|
|
61
|
+
"""Return the configured adapter, including for legacy test fixtures."""
|
|
62
|
+
|
|
63
|
+
return getattr(self, "output", get_output(getattr(self, "config", None)))
|
|
64
|
+
|
|
65
|
+
def get_net_module(self,model_path=None):
|
|
66
|
+
if model_path is None:
|
|
67
|
+
model_path = self.config['MODEL_PATH']
|
|
68
|
+
if os.path.exists(os.path.join(model_path,'network.py')):
|
|
69
|
+
spec1 = importlib.util.spec_from_file_location("network", os.path.join(model_path,"network.py"))
|
|
70
|
+
module = importlib.util.module_from_spec(spec1)
|
|
71
|
+
spec1.loader.exec_module(module)
|
|
72
|
+
net = getattr(module, 'network')
|
|
73
|
+
return net
|
|
74
|
+
network_name = self.config.get("network_id")
|
|
75
|
+
if network_name is not None:
|
|
76
|
+
try:
|
|
77
|
+
from D4CMPP2.networks.registry import get_model
|
|
78
|
+
|
|
79
|
+
return get_model(network_name).network
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
raise FileNotFoundError(
|
|
83
|
+
model_path + "/network.py not found and network_id is not registered"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Initialize the network
|
|
87
|
+
def init_network(self, load_existing=True):
|
|
88
|
+
module = self.get_net_module()
|
|
89
|
+
self.network = module(self.config)
|
|
90
|
+
self.network.to(device = self.device)
|
|
91
|
+
parameter_count = sum(
|
|
92
|
+
p.numel() for p in self.network.parameters() if p.requires_grad
|
|
93
|
+
)
|
|
94
|
+
self._output().info(
|
|
95
|
+
f"[Model] Initialized {type(self.network).__name__} with "
|
|
96
|
+
f"{parameter_count:,} trainable parameters."
|
|
97
|
+
)
|
|
98
|
+
from D4CMPP2.networks.base import MolecularNetwork
|
|
99
|
+
|
|
100
|
+
if isinstance(self.network, MolecularNetwork):
|
|
101
|
+
self.loss_fn = self.network.compute_loss
|
|
102
|
+
else:
|
|
103
|
+
self.loss_fn = self.network.loss_fn
|
|
104
|
+
|
|
105
|
+
if os.path.exists(os.path.join(self.config['MODEL_PATH'],'result','learning_curve.csv')):
|
|
106
|
+
self.learning_curve = pd.read_csv(os.path.join(self.config['MODEL_PATH'],'result','learning_curve.csv'))
|
|
107
|
+
elif os.path.exists(os.path.join(self.config['MODEL_PATH'],'learning_curve.csv')):
|
|
108
|
+
self.learning_curve = pd.read_csv(os.path.join(self.config['MODEL_PATH'],'learning_curve.csv'))
|
|
109
|
+
else:
|
|
110
|
+
self.learning_curve = None
|
|
111
|
+
|
|
112
|
+
if not load_existing:
|
|
113
|
+
return
|
|
114
|
+
if os.path.exists(os.path.join(self.config['MODEL_PATH'],'final.pth')):
|
|
115
|
+
self.load_params(os.path.join(self.config['MODEL_PATH'],'final.pth'))
|
|
116
|
+
else:
|
|
117
|
+
losses = []
|
|
118
|
+
for file in os.listdir(self.config['MODEL_PATH']):
|
|
119
|
+
if file.startswith("param_") and file.endswith(".pth"):
|
|
120
|
+
losses.append(float(file.split("_")[1].replace(".pth","")))
|
|
121
|
+
if losses:
|
|
122
|
+
self.best_loss = min(losses)
|
|
123
|
+
self.load_params(os.path.join(self.config['MODEL_PATH'],'param_'+str(self.best_loss)+'.pth')
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Transfer learning
|
|
128
|
+
def transferlearn_network(self):
|
|
129
|
+
self.init_network()
|
|
130
|
+
self.load_params_transfer_learn(self.tf_path)
|
|
131
|
+
|
|
132
|
+
# Initialize the optimizer
|
|
133
|
+
def init_optimizer(self,lr_dict=None):
|
|
134
|
+
lr_dict = {} if lr_dict is None else lr_dict
|
|
135
|
+
if not isinstance(lr_dict, dict):
|
|
136
|
+
raise TypeError(
|
|
137
|
+
f"lr_dict must be a mapping of layer-name components to learning rates, "
|
|
138
|
+
f"got {type(lr_dict).__name__}. Example: lr_dict={{'GCNs': 0.0001}}."
|
|
139
|
+
)
|
|
140
|
+
invalid = {
|
|
141
|
+
key: value for key, value in lr_dict.items()
|
|
142
|
+
if (
|
|
143
|
+
not isinstance(key, str)
|
|
144
|
+
or not key.strip()
|
|
145
|
+
or isinstance(value, bool)
|
|
146
|
+
or not isinstance(value, (int, float))
|
|
147
|
+
or value <= 0
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
if invalid:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"lr_dict keys must be non-empty layer-name strings and values must be "
|
|
153
|
+
f"positive learning rates; invalid entries: {invalid!r}."
|
|
154
|
+
)
|
|
155
|
+
params = []
|
|
156
|
+
matched_lr_keys = set()
|
|
157
|
+
for name, param in self.network.named_parameters():
|
|
158
|
+
matching_keys = [key for key in lr_dict if key in name.split(".")]
|
|
159
|
+
if len(matching_keys) > 1:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"lr_dict entries {matching_keys!r} both match parameter {name!r}. "
|
|
162
|
+
"Use one unambiguous layer-name component for this parameter."
|
|
163
|
+
)
|
|
164
|
+
if matching_keys:
|
|
165
|
+
key = matching_keys[0]
|
|
166
|
+
matched_lr_keys.add(key)
|
|
167
|
+
lr = lr_dict[key]
|
|
168
|
+
self._output().info(
|
|
169
|
+
f"[Optimizer] Parameter {name!r} uses learning rate {lr:g}."
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
lr = self.config.get('learning_rate',0.001)
|
|
173
|
+
|
|
174
|
+
params.append({'params': param, 'lr': lr})
|
|
175
|
+
|
|
176
|
+
unmatched = sorted(set(lr_dict) - matched_lr_keys)
|
|
177
|
+
if unmatched:
|
|
178
|
+
available = sorted(
|
|
179
|
+
{component for name, _ in self.network.named_parameters() for component in name.split(".")}
|
|
180
|
+
)
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"lr_dict layer names {unmatched!r} did not match any trainable parameter. "
|
|
183
|
+
f"Available name components include: {available!r}."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
self.optimizer = getattr(torch.optim, self.config['optimizer'])(params,
|
|
187
|
+
lr=self.config.get('learning_rate',0.001),
|
|
188
|
+
weight_decay=self.config.get('weight_decay',0.0005)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Initialize the scheduler
|
|
192
|
+
def init_scheduler(self):
|
|
193
|
+
self.schedulers.append( torch.optim.lr_scheduler.ReduceLROnPlateau(self.optimizer,
|
|
194
|
+
patience=self.config.get('lr_patience',10),
|
|
195
|
+
min_lr=self.config.get('min_lr',1e-7),
|
|
196
|
+
factor=self.config.get('lr_plateau_decay',0.1),
|
|
197
|
+
))
|
|
198
|
+
self.schedulers.append( torch.optim.lr_scheduler.StepLR(self.optimizer,
|
|
199
|
+
step_size=self.config.get('lr_step',40),
|
|
200
|
+
gamma=self.config.get('lr_step_decay',0.98)))
|
|
201
|
+
|
|
202
|
+
"----------------------------------------------------------------------------------------------------------------------"
|
|
203
|
+
"Below are the functions to manage the network during the training, and called by the Trainer class"
|
|
204
|
+
|
|
205
|
+
def train(self):
|
|
206
|
+
self.state = "train"
|
|
207
|
+
self.network.train()
|
|
208
|
+
|
|
209
|
+
def eval(self):
|
|
210
|
+
self.state = "eval"
|
|
211
|
+
self.network.eval()
|
|
212
|
+
|
|
213
|
+
def get_lr(self):
|
|
214
|
+
lrs = []
|
|
215
|
+
for param_group in self.optimizer.param_groups:
|
|
216
|
+
lrs.append(param_group['lr'])
|
|
217
|
+
return np.mean(lrs)
|
|
218
|
+
|
|
219
|
+
# One step of the training including forward and backward
|
|
220
|
+
def step(self, loader, flag= False, **kargs):
|
|
221
|
+
self.optimizer.zero_grad()
|
|
222
|
+
if type(loader) is dict:
|
|
223
|
+
loader.update({'device': self.device})
|
|
224
|
+
x = self.unwrapper(**loader)
|
|
225
|
+
else:
|
|
226
|
+
x= self.unwrapper(*loader,device=self.device)
|
|
227
|
+
y= x['target']
|
|
228
|
+
x.update(kargs)
|
|
229
|
+
self._validate_network_input(x)
|
|
230
|
+
y_pred = self.network(**x)
|
|
231
|
+
if kargs.get('get_score',False) or kargs.get('get_feature',False):
|
|
232
|
+
return y_pred
|
|
233
|
+
|
|
234
|
+
loss = self.loss_fn(y_pred, y)
|
|
235
|
+
if self.state == "train":
|
|
236
|
+
loss.backward()
|
|
237
|
+
self.optimizer.step()
|
|
238
|
+
if flag:
|
|
239
|
+
return y, y_pred.detach(), loss.detach().item(), x
|
|
240
|
+
return y, y_pred.detach(), loss.detach().item()
|
|
241
|
+
|
|
242
|
+
# One step of the prediction including forward
|
|
243
|
+
def predict(self,loader):
|
|
244
|
+
self.optimizer.zero_grad()
|
|
245
|
+
if type(loader) is dict:
|
|
246
|
+
x = self.unwrapper(**loader, device=self.device)
|
|
247
|
+
else:
|
|
248
|
+
x= self.unwrapper(*loader,device=self.device)
|
|
249
|
+
y= x['target']
|
|
250
|
+
self._validate_network_input(x)
|
|
251
|
+
y_pred = self.network(**x)
|
|
252
|
+
return y_pred, y
|
|
253
|
+
|
|
254
|
+
def _validate_network_input(self, batch):
|
|
255
|
+
from D4CMPP2.networks.base import MolecularNetwork
|
|
256
|
+
|
|
257
|
+
if isinstance(self.network, MolecularNetwork):
|
|
258
|
+
self.network.validate_input(batch)
|
|
259
|
+
|
|
260
|
+
def scheduler_step(self, val_loss=None, completed_epoch=None):
|
|
261
|
+
if completed_epoch is None:
|
|
262
|
+
completed_epoch = getattr(self, "completed_epoch", -1) + 1
|
|
263
|
+
for scheduler in self.schedulers:
|
|
264
|
+
if isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
265
|
+
scheduler.step(val_loss)
|
|
266
|
+
else:
|
|
267
|
+
scheduler.step()
|
|
268
|
+
|
|
269
|
+
# save the best model and reset the early stopping counter
|
|
270
|
+
if val_loss < self.best_loss:
|
|
271
|
+
self.best_loss = val_loss
|
|
272
|
+
self.best_epoch = completed_epoch
|
|
273
|
+
self.save_checkpoint(val_loss)
|
|
274
|
+
self.es_counter = 0
|
|
275
|
+
self.save_training_checkpoint("best", completed_epoch)
|
|
276
|
+
else:
|
|
277
|
+
self.es_counter += 1
|
|
278
|
+
|
|
279
|
+
# early stopping
|
|
280
|
+
if self.es_counter > self.es_patience:
|
|
281
|
+
self.load_best_checkpoint()
|
|
282
|
+
self.completed_epoch = completed_epoch
|
|
283
|
+
self.next_epoch = completed_epoch + 1
|
|
284
|
+
self.save_training_checkpoint("latest", completed_epoch)
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
# load the best model if the learning rate is reduced
|
|
288
|
+
current_lr = self.get_lr()
|
|
289
|
+
if current_lr<self.last_lr/2:
|
|
290
|
+
self.last_lr=current_lr
|
|
291
|
+
self.load_best_checkpoint()
|
|
292
|
+
self._output().info(
|
|
293
|
+
f"[Training] Learning rate reduced to {self.last_lr:g}; "
|
|
294
|
+
"restored the best validation checkpoint."
|
|
295
|
+
)
|
|
296
|
+
self.completed_epoch = completed_epoch
|
|
297
|
+
self.next_epoch = completed_epoch + 1
|
|
298
|
+
self.save_training_checkpoint("latest", completed_epoch)
|
|
299
|
+
|
|
300
|
+
def save_checkpoint(self, val_loss):
|
|
301
|
+
self.save_params(os.path.join(self.config['MODEL_PATH'],"param_"+str(val_loss)+".pth"))
|
|
302
|
+
|
|
303
|
+
def load_best_checkpoint(self):
|
|
304
|
+
path = os.path.join(self.config['MODEL_PATH'],"param_"+str(self.best_loss)+".pth")
|
|
305
|
+
if os.path.exists(path):
|
|
306
|
+
self.load_params(path)
|
|
307
|
+
return path
|
|
308
|
+
else:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
def load_params(self, path):
|
|
312
|
+
self._output().info(f"[Checkpoint] Loading model weights from {path!r}.")
|
|
313
|
+
self.network.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
|
|
314
|
+
|
|
315
|
+
def load_params_transfer_learn(self, tf_path):
|
|
316
|
+
self._output().info(
|
|
317
|
+
f"[Transfer] Loading compatible parameters from {tf_path!r}."
|
|
318
|
+
)
|
|
319
|
+
config_path = os.path.join(tf_path, "config.yaml")
|
|
320
|
+
weights_path = os.path.join(tf_path, "final.pth")
|
|
321
|
+
if not os.path.isfile(config_path):
|
|
322
|
+
raise FileNotFoundError(
|
|
323
|
+
f"Transfer source config {config_path!r} was not found. "
|
|
324
|
+
"Select a completed saved-model directory containing config.yaml."
|
|
325
|
+
)
|
|
326
|
+
if not os.path.isfile(weights_path):
|
|
327
|
+
raise FileNotFoundError(
|
|
328
|
+
f"Transfer source weights {weights_path!r} were not found. "
|
|
329
|
+
"Select a completed saved-model directory containing final.pth."
|
|
330
|
+
)
|
|
331
|
+
try:
|
|
332
|
+
with open(config_path, "r", encoding="utf-8") as stream:
|
|
333
|
+
source_config = yaml.safe_load(stream)
|
|
334
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"Transfer source config {config_path!r} could not be read as YAML: {exc}."
|
|
337
|
+
) from exc
|
|
338
|
+
if not isinstance(source_config, dict):
|
|
339
|
+
raise TypeError(
|
|
340
|
+
f"Transfer source config {config_path!r} must contain a YAML mapping, "
|
|
341
|
+
f"got {type(source_config).__name__}."
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
module = self.get_net_module(tf_path)
|
|
346
|
+
pretrained_network = module(source_config)
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
raise RuntimeError(
|
|
349
|
+
f"Transfer source network snapshot in {tf_path!r} could not be constructed "
|
|
350
|
+
f"from config.yaml: {exc}"
|
|
351
|
+
) from exc
|
|
352
|
+
try:
|
|
353
|
+
params = torch.load(
|
|
354
|
+
weights_path,
|
|
355
|
+
weights_only=True,
|
|
356
|
+
map_location=self.device,
|
|
357
|
+
)
|
|
358
|
+
pretrained_network.load_state_dict(params, strict=True)
|
|
359
|
+
except (OSError, RuntimeError, TypeError, ValueError) as exc:
|
|
360
|
+
raise RuntimeError(
|
|
361
|
+
f"Transfer source weights {weights_path!r} are incompatible with its "
|
|
362
|
+
f"saved network.py/config.yaml snapshot: {exc}"
|
|
363
|
+
) from exc
|
|
364
|
+
|
|
365
|
+
selected, report = select_compatible_state(
|
|
366
|
+
pretrained_network.state_dict(),
|
|
367
|
+
self.network.state_dict(),
|
|
368
|
+
)
|
|
369
|
+
if not selected:
|
|
370
|
+
raise ValueError(
|
|
371
|
+
f"Transfer source {tf_path!r} has no name-and-shape-compatible state "
|
|
372
|
+
"entries for the target network. Check the selected source and target architectures."
|
|
373
|
+
)
|
|
374
|
+
result = self.network.load_state_dict(selected, strict=False)
|
|
375
|
+
report.update(
|
|
376
|
+
{
|
|
377
|
+
"source_model_path": os.path.abspath(tf_path),
|
|
378
|
+
"source_weights": os.path.abspath(weights_path),
|
|
379
|
+
"source_weights_sha256": file_sha256(weights_path),
|
|
380
|
+
"source_network": source_config.get(
|
|
381
|
+
"network_id", source_config.get("network")
|
|
382
|
+
),
|
|
383
|
+
"target_network": self.config.get(
|
|
384
|
+
"network_id", self.config.get("network")
|
|
385
|
+
),
|
|
386
|
+
"load_state_missing_keys": list(result.missing_keys),
|
|
387
|
+
"load_state_unexpected_keys": list(result.unexpected_keys),
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
report["report_path"] = write_transfer_report(
|
|
391
|
+
report, self.config["MODEL_PATH"]
|
|
392
|
+
)
|
|
393
|
+
self.transfer_report = report
|
|
394
|
+
counts = report["counts"]
|
|
395
|
+
self._output().info(
|
|
396
|
+
"[Transfer] Parameter selection complete: "
|
|
397
|
+
f"loaded={counts['loaded']}, "
|
|
398
|
+
f"shape_mismatch={counts['shape_mismatch']}, "
|
|
399
|
+
f"source_only={counts['source_only']}, "
|
|
400
|
+
f"target_only={counts['target_only']}. "
|
|
401
|
+
f"Report: {report['report_path']!r}."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def save_params(self, path):
|
|
405
|
+
torch.save(self.network.state_dict(), path)
|
|
406
|
+
|
|
407
|
+
def _checkpoint_identity(self):
|
|
408
|
+
return {
|
|
409
|
+
"network_id": self.config.get("network_id", self.config.get("network")),
|
|
410
|
+
"network_module": self.config.get("network"),
|
|
411
|
+
"target": list(self.config.get("target", [])),
|
|
412
|
+
"target_dim": self.config.get("target_dim"),
|
|
413
|
+
"optimizer": self.config.get("optimizer"),
|
|
414
|
+
"scheduler_policy": self.config.get("scheduler_policy", "legacy_dual"),
|
|
415
|
+
"graph_backend": "pyg",
|
|
416
|
+
"graph_schema_version": 1,
|
|
417
|
+
"random_seed": self.config.get("random_seed"),
|
|
418
|
+
"deterministic_algorithms": self.config.get("deterministic_algorithms", False),
|
|
419
|
+
"split_strategy": self.config.get("split_strategy", "auto"),
|
|
420
|
+
"scaffold_column": self.config.get("scaffold_column"),
|
|
421
|
+
"scaffold_include_chirality": self.config.get(
|
|
422
|
+
"scaffold_include_chirality", False
|
|
423
|
+
),
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
def save_training_checkpoint(self, kind, completed_epoch):
|
|
427
|
+
if completed_epoch is None:
|
|
428
|
+
return None
|
|
429
|
+
path = os.path.join(self.config["MODEL_PATH"], "checkpoints", f"{kind}.ckpt")
|
|
430
|
+
payload = {
|
|
431
|
+
"checkpoint_schema_version": CHECKPOINT_SCHEMA_VERSION,
|
|
432
|
+
"kind": kind,
|
|
433
|
+
"run_id": getattr(self, "run_id", None),
|
|
434
|
+
"completed_epoch": int(completed_epoch),
|
|
435
|
+
"next_epoch": int(completed_epoch) + 1,
|
|
436
|
+
"model_state_dict": self.network.state_dict(),
|
|
437
|
+
"optimizer_state_dict": self.optimizer.state_dict(),
|
|
438
|
+
"schedulers": [
|
|
439
|
+
{"class": scheduler.__class__.__name__, "state_dict": scheduler.state_dict()}
|
|
440
|
+
for scheduler in self.schedulers
|
|
441
|
+
],
|
|
442
|
+
"best_epoch": getattr(self, "best_epoch", None),
|
|
443
|
+
"best_metric": self.best_loss,
|
|
444
|
+
"metric_name": "val_loss",
|
|
445
|
+
"early_stopping_counter": self.es_counter,
|
|
446
|
+
"last_lr": self.last_lr,
|
|
447
|
+
"rng_state": capture_rng_state(),
|
|
448
|
+
"identity": self._checkpoint_identity(),
|
|
449
|
+
}
|
|
450
|
+
atomic_torch_save(payload, path)
|
|
451
|
+
return path
|
|
452
|
+
|
|
453
|
+
def load_training_checkpoint(self, value):
|
|
454
|
+
path = resolve_resume_checkpoint(value)
|
|
455
|
+
payload = load_checkpoint(path, self.device)
|
|
456
|
+
expected = self._checkpoint_identity()
|
|
457
|
+
actual = payload.get("identity", {})
|
|
458
|
+
for key in (
|
|
459
|
+
"network_module",
|
|
460
|
+
"target",
|
|
461
|
+
"target_dim",
|
|
462
|
+
"optimizer",
|
|
463
|
+
"scheduler_policy",
|
|
464
|
+
"graph_backend",
|
|
465
|
+
"graph_schema_version",
|
|
466
|
+
):
|
|
467
|
+
if actual.get(key) != expected.get(key):
|
|
468
|
+
raise ValueError(
|
|
469
|
+
f"Resume checkpoint {str(path)!r} is incompatible for {key}: "
|
|
470
|
+
f"checkpoint={actual.get(key)!r}, current={expected.get(key)!r}. "
|
|
471
|
+
"Use LOAD_PATH for weight-only continuation or TRANSFER_PATH for a different model/target."
|
|
472
|
+
)
|
|
473
|
+
for key in ("random_seed", "deterministic_algorithms"):
|
|
474
|
+
if key in actual and actual.get(key) != expected.get(key):
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"Resume checkpoint {str(path)!r} is incompatible for {key}: "
|
|
477
|
+
f"checkpoint={actual.get(key)!r}, current={expected.get(key)!r}. "
|
|
478
|
+
"Use the saved reproducibility policy for full resume."
|
|
479
|
+
)
|
|
480
|
+
for key in (
|
|
481
|
+
"split_strategy",
|
|
482
|
+
"scaffold_column",
|
|
483
|
+
"scaffold_include_chirality",
|
|
484
|
+
):
|
|
485
|
+
if key in actual and actual.get(key) != expected.get(key):
|
|
486
|
+
raise ValueError(
|
|
487
|
+
f"Resume checkpoint {str(path)!r} is incompatible for {key}: "
|
|
488
|
+
f"checkpoint={actual.get(key)!r}, current={expected.get(key)!r}. "
|
|
489
|
+
"Use the saved split policy for full resume, or start a new run."
|
|
490
|
+
)
|
|
491
|
+
scheduler_states = payload.get("schedulers", [])
|
|
492
|
+
current_names = [scheduler.__class__.__name__ for scheduler in self.schedulers]
|
|
493
|
+
saved_names = [item.get("class") for item in scheduler_states]
|
|
494
|
+
if saved_names != current_names:
|
|
495
|
+
raise ValueError(
|
|
496
|
+
f"Resume checkpoint {str(path)!r} scheduler classes {saved_names!r} "
|
|
497
|
+
f"do not match current classes {current_names!r}."
|
|
498
|
+
)
|
|
499
|
+
self.network.load_state_dict(payload["model_state_dict"])
|
|
500
|
+
self.optimizer.load_state_dict(payload["optimizer_state_dict"])
|
|
501
|
+
for scheduler, item in zip(self.schedulers, scheduler_states):
|
|
502
|
+
scheduler.load_state_dict(item["state_dict"])
|
|
503
|
+
self.best_epoch = payload.get("best_epoch")
|
|
504
|
+
self.best_loss = payload["best_metric"]
|
|
505
|
+
self.es_counter = payload["early_stopping_counter"]
|
|
506
|
+
self.last_lr = payload["last_lr"]
|
|
507
|
+
self.completed_epoch = payload["completed_epoch"]
|
|
508
|
+
self.next_epoch = payload["next_epoch"]
|
|
509
|
+
self.parent_run_id = payload.get("run_id")
|
|
510
|
+
restore_rng_state(payload["rng_state"])
|
|
511
|
+
self._output().info(
|
|
512
|
+
f"[Checkpoint] Restored full training state from {str(path)!r}; "
|
|
513
|
+
f"next epoch: {self.next_epoch}."
|
|
514
|
+
)
|
|
515
|
+
return str(path)
|
|
516
|
+
|
|
517
|
+
def dropout_on(self):
|
|
518
|
+
for m in self.network.modules():
|
|
519
|
+
if m.__class__.__name__.startswith('Dropout'):
|
|
520
|
+
m.train()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Lazy exports for built-in network-manager modules."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
|
|
6
|
+
__all__ = ["NetworkManager", "ISANetworkManager"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def __getattr__(name: str) -> ModuleType:
|
|
10
|
+
if name not in __all__:
|
|
11
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
12
|
+
module = import_module(f"{__name__}.{name}")
|
|
13
|
+
globals()[name] = module
|
|
14
|
+
return module
|