MEDfl 2.0.9__tar.gz → 2.0.10.dev0__tar.gz
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.
- medfl-2.0.10.dev0/MEDfl/LearningManager/client.py +169 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/model.py +74 -139
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/params.yaml +4 -4
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/plot.py +105 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/server.py +65 -24
- medfl-2.0.10.dev0/MEDfl/LearningManager/strategy.py +348 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/flsetup.py +185 -74
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/net_helper.py +73 -46
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/__init__.py +16 -0
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/client.py +379 -0
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/config.py +84 -0
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/data.py +253 -0
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/metrics.py +176 -0
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/serialization.py +56 -0
- medfl-2.0.10.dev0/MEDfl/rw/xgboost/strategy.py +380 -0
- {medfl-2.0.9/medfl.egg-info → medfl-2.0.10.dev0}/PKG-INFO +1 -1
- {medfl-2.0.9 → medfl-2.0.10.dev0/medfl.egg-info}/PKG-INFO +1 -1
- {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/SOURCES.txt +7 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/requires.txt +1 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/setup.py +1 -1
- medfl-2.0.9/MEDfl/LearningManager/client.py +0 -173
- medfl-2.0.9/MEDfl/LearningManager/strategy.py +0 -83
- {medfl-2.0.9 → medfl-2.0.10.dev0}/LICENSE +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/__init__.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/dynamicModal.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/federated_dataset.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/flpipeline.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/params_optimiser.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/utils.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/__init__.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/database_connector.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/dataset.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/net_manager_queries.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/network.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/node.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/__init__.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/__init__.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/client.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/model.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/server.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/strategy.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/scripts/__init__.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/scripts/base.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/scripts/create_db.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/README.md +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/alembic/__init__.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/alembic/env.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/dependency_links.txt +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/top_level.txt +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/setup.cfg +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_dataset.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_federated_dataset.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_flpipeline.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_flsetup.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_network.py +0 -0
- {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_node.py +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import flwr as fl
|
|
6
|
+
from opacus import PrivacyEngine
|
|
7
|
+
from torch.utils.data import DataLoader
|
|
8
|
+
|
|
9
|
+
from .model import Model
|
|
10
|
+
from .utils import params
|
|
11
|
+
import torch
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Per-worker logger setup
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Ray workers are separate processes. We configure this logger inside the
|
|
19
|
+
# worker so that messages go to stdout — Ray then forwards stdout to the
|
|
20
|
+
# driver when log_to_driver=True is set in ray_init_args.
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
def _get_logger() -> logging.Logger:
|
|
23
|
+
logger = logging.getLogger("MEDfl.client")
|
|
24
|
+
if not logger.handlers:
|
|
25
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
26
|
+
handler.setFormatter(
|
|
27
|
+
logging.Formatter(
|
|
28
|
+
"[%(asctime)s][%(name)s][%(levelname)s] %(message)s",
|
|
29
|
+
datefmt="%H:%M:%S",
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
logger.addHandler(handler)
|
|
33
|
+
logger.setLevel(logging.DEBUG)
|
|
34
|
+
logger.propagate = False # avoid double-printing on driver
|
|
35
|
+
return logger
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FlowerClient(fl.client.NumPyClient):
|
|
39
|
+
"""
|
|
40
|
+
FlowerClient class for creating MEDfl clients.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
cid: str,
|
|
46
|
+
local_model: Model,
|
|
47
|
+
trainloader: DataLoader,
|
|
48
|
+
valloader: DataLoader,
|
|
49
|
+
diff_priv: bool = params["diff_privacy"],
|
|
50
|
+
):
|
|
51
|
+
self.cid = cid
|
|
52
|
+
self.local_model = local_model
|
|
53
|
+
self.trainloader = trainloader
|
|
54
|
+
self.valloader = valloader
|
|
55
|
+
self.log = _get_logger()
|
|
56
|
+
|
|
57
|
+
self.device = torch.device("cpu")
|
|
58
|
+
self.local_model.model.to(self.device)
|
|
59
|
+
|
|
60
|
+
self.privacy_engine = PrivacyEngine(secure_mode=False)
|
|
61
|
+
self.diff_priv = diff_priv
|
|
62
|
+
self.epsilons = []
|
|
63
|
+
self.accuracies = []
|
|
64
|
+
self.losses = []
|
|
65
|
+
self.train_round_losses = []
|
|
66
|
+
self.train_round_metrics = []
|
|
67
|
+
|
|
68
|
+
if self.diff_priv:
|
|
69
|
+
model, optimizer, self.trainloader = self.privacy_engine.make_private_with_epsilon(
|
|
70
|
+
module=self.local_model.model.train(),
|
|
71
|
+
optimizer=self.local_model.optimizer,
|
|
72
|
+
data_loader=self.trainloader,
|
|
73
|
+
epochs=params["train_epochs"],
|
|
74
|
+
target_epsilon=float(params["EPSILON"]),
|
|
75
|
+
target_delta=float(params["DELTA"]),
|
|
76
|
+
max_grad_norm=params["MAX_GRAD_NORM"],
|
|
77
|
+
)
|
|
78
|
+
setattr(self.local_model, "model", model)
|
|
79
|
+
setattr(self.local_model, "optimizer", optimizer)
|
|
80
|
+
self.validate()
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
def validate(self):
|
|
84
|
+
"""Validates cid, local_model, trainloader, valloader."""
|
|
85
|
+
if not isinstance(self.cid, str):
|
|
86
|
+
raise TypeError("cid argument must be a string")
|
|
87
|
+
if not isinstance(self.local_model, Model):
|
|
88
|
+
raise TypeError("local_model argument must be a MEDfl.LearningManager.model.Model")
|
|
89
|
+
if not isinstance(self.trainloader, DataLoader):
|
|
90
|
+
raise TypeError("trainloader argument must be a torch.utils.data.dataloader")
|
|
91
|
+
if not isinstance(self.valloader, DataLoader):
|
|
92
|
+
raise TypeError("valloader argument must be a torch.utils.data.dataloader")
|
|
93
|
+
if not isinstance(self.diff_priv, bool):
|
|
94
|
+
raise TypeError("diff_priv argument must be a bool")
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
def get_parameters(self, config):
|
|
98
|
+
self.log.info(f"[Client {self.cid}] get_parameters")
|
|
99
|
+
return self.local_model.get_parameters()
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
def fit(self, parameters, config):
|
|
103
|
+
self.log.info(f"[Client {self.cid}] fit | config: {config}")
|
|
104
|
+
|
|
105
|
+
self.local_model.set_parameters(parameters)
|
|
106
|
+
|
|
107
|
+
epoch_losses, epoch_metrics = [], []
|
|
108
|
+
|
|
109
|
+
for ep in range(params["train_epochs"]):
|
|
110
|
+
epsilon, tr_loss, tr_metric = self.local_model.train(
|
|
111
|
+
self.trainloader,
|
|
112
|
+
epoch=ep,
|
|
113
|
+
device=self.device,
|
|
114
|
+
privacy_engine=self.privacy_engine,
|
|
115
|
+
diff_priv=self.diff_priv,
|
|
116
|
+
)
|
|
117
|
+
self.epsilons.append(epsilon)
|
|
118
|
+
epoch_losses.append(tr_loss)
|
|
119
|
+
epoch_metrics.append(tr_metric)
|
|
120
|
+
|
|
121
|
+
avg_grad_norm = float(np.nanmean(self.local_model.grad_norms)) \
|
|
122
|
+
if hasattr(self.local_model, "grad_norms") and self.local_model.grad_norms \
|
|
123
|
+
else float("nan")
|
|
124
|
+
self.local_model.grad_norms = [] # reset for next round
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
self.log.info(
|
|
128
|
+
f"[Client {self.cid}] fit done | "
|
|
129
|
+
f"epsilon={epsilon:.4f} | "
|
|
130
|
+
f"avg_loss={float(np.nanmean(epoch_losses)):.4f}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
round_train_loss = float(np.nanmean(epoch_losses)) if len(epoch_losses) else float("nan")
|
|
134
|
+
round_train_metric = float(np.nanmean(epoch_metrics)) if len(epoch_metrics) else float("nan")
|
|
135
|
+
|
|
136
|
+
self.train_round_losses.append(round_train_loss)
|
|
137
|
+
self.train_round_metrics.append(round_train_metric)
|
|
138
|
+
|
|
139
|
+
metric_key = "train_accuracy" if self.local_model.task_type == "binary" else "train_rmse"
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
self.local_model.get_parameters(),
|
|
143
|
+
len(self.trainloader.dataset),
|
|
144
|
+
{
|
|
145
|
+
"epsilon": float(epsilon),
|
|
146
|
+
"train_loss": float(round_train_loss),
|
|
147
|
+
metric_key: float(round_train_metric),
|
|
148
|
+
"grad_norm": avg_grad_norm, # <-- NEW
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
def evaluate(self, parameters, config):
|
|
154
|
+
self.log.info(f"[Client {self.cid}] evaluate | config: {config}")
|
|
155
|
+
self.local_model.set_parameters(parameters)
|
|
156
|
+
|
|
157
|
+
loss, metrics = self.local_model.evaluate(self.valloader, device=self.device)
|
|
158
|
+
|
|
159
|
+
self.losses.append(loss)
|
|
160
|
+
|
|
161
|
+
if isinstance(metrics, dict) and "accuracy" in metrics:
|
|
162
|
+
self.accuracies.append(metrics["accuracy"])
|
|
163
|
+
|
|
164
|
+
self.log.info(
|
|
165
|
+
f"[Client {self.cid}] evaluate done | "
|
|
166
|
+
f"loss={loss:.4f} | metrics={metrics}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return float(loss), len(self.valloader.dataset), {k: float(v) for k, v in (metrics or {}).items()}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# froked from https://github.com/pythonlessons/mltu/blob/main/mltu/torch/model.py
|
|
3
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
4
5
|
import typing
|
|
5
6
|
from collections import OrderedDict
|
|
6
7
|
from typing import Dict, List, Optional, Tuple
|
|
@@ -8,20 +9,39 @@ from typing import Dict, List, Optional, Tuple
|
|
|
8
9
|
import numpy as np
|
|
9
10
|
import torch
|
|
10
11
|
import torch.nn as nn
|
|
11
|
-
from sklearn.metrics import
|
|
12
|
-
|
|
12
|
+
from sklearn.metrics import (
|
|
13
|
+
accuracy_score, roc_auc_score,
|
|
14
|
+
mean_squared_error, mean_absolute_error, r2_score,
|
|
15
|
+
)
|
|
13
16
|
|
|
14
17
|
from .utils import params
|
|
15
18
|
|
|
16
19
|
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Per-worker logger — same pattern as client.py so Ray forwards to notebook
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
def _get_logger() -> logging.Logger:
|
|
24
|
+
logger = logging.getLogger("MEDfl.model")
|
|
25
|
+
if not logger.handlers:
|
|
26
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
27
|
+
handler.setFormatter(
|
|
28
|
+
logging.Formatter(
|
|
29
|
+
"[%(asctime)s][%(name)s][%(levelname)s] %(message)s",
|
|
30
|
+
datefmt="%H:%M:%S",
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
logger.addHandler(handler)
|
|
34
|
+
logger.setLevel(logging.DEBUG)
|
|
35
|
+
logger.propagate = False
|
|
36
|
+
return logger
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_log = _get_logger()
|
|
40
|
+
|
|
41
|
+
|
|
17
42
|
class Model:
|
|
18
43
|
"""
|
|
19
44
|
Model class for training and testing PyTorch neural networks.
|
|
20
|
-
|
|
21
|
-
Attributes:
|
|
22
|
-
model (torch.nn.Module): PyTorch neural network.
|
|
23
|
-
optimizer (torch.optim.Optimizer): PyTorch optimizer.
|
|
24
|
-
criterion (typing.Callable): Loss function.
|
|
25
45
|
"""
|
|
26
46
|
|
|
27
47
|
def __init__(
|
|
@@ -29,56 +49,31 @@ class Model:
|
|
|
29
49
|
model: torch.nn.Module,
|
|
30
50
|
optimizer: torch.optim.Optimizer,
|
|
31
51
|
criterion: typing.Callable,
|
|
32
|
-
task_type: str = "binary"
|
|
52
|
+
task_type: str = "binary",
|
|
33
53
|
) -> None:
|
|
34
|
-
"""
|
|
35
|
-
Initialize Model class with the specified model, optimizer, and criterion.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
model (torch.nn.Module): PyTorch neural network.
|
|
39
|
-
optimizer (torch.optim.Optimizer): PyTorch optimizer.
|
|
40
|
-
criterion (typing.Callable): Loss function.
|
|
41
|
-
"""
|
|
42
54
|
self.model = model
|
|
43
55
|
self.optimizer = optimizer
|
|
44
56
|
self.criterion = criterion
|
|
45
57
|
self.task_type = task_type
|
|
46
|
-
# Get device on which model is running
|
|
47
58
|
self.validate()
|
|
48
59
|
|
|
49
60
|
def validate(self) -> None:
|
|
50
|
-
"""
|
|
51
|
-
Validate model and optimizer.
|
|
52
|
-
"""
|
|
53
61
|
if not isinstance(self.model, torch.nn.Module):
|
|
54
62
|
raise TypeError("model argument must be a torch.nn.Module")
|
|
55
|
-
|
|
56
63
|
if not isinstance(self.optimizer, torch.optim.Optimizer):
|
|
57
|
-
raise TypeError(
|
|
58
|
-
"optimizer argument must be a torch.optim.Optimizer"
|
|
59
|
-
)
|
|
64
|
+
raise TypeError("optimizer argument must be a torch.optim.Optimizer")
|
|
60
65
|
|
|
61
66
|
def get_parameters(self) -> List[np.ndarray]:
|
|
62
|
-
"""
|
|
63
|
-
Get the parameters of the model as a list of NumPy arrays.
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
List[np.ndarray]: The parameters of the model as a list of NumPy arrays.
|
|
67
|
-
"""
|
|
68
67
|
return [val.detach().cpu().numpy() for val in self.model.state_dict().values()]
|
|
69
68
|
|
|
70
69
|
def set_parameters(self, parameters: List[np.ndarray]) -> None:
|
|
71
70
|
state_dict = self.model.state_dict()
|
|
72
71
|
keys = list(state_dict.keys())
|
|
73
|
-
|
|
74
|
-
device = next(self.model.parameters()).device # model device
|
|
72
|
+
device = next(self.model.parameters()).device
|
|
75
73
|
new_state_dict = OrderedDict()
|
|
76
|
-
|
|
77
74
|
for k, v in zip(keys, parameters):
|
|
78
|
-
ref = state_dict[k]
|
|
79
|
-
# v is numpy array -> tensor on correct device/dtype
|
|
75
|
+
ref = state_dict[k]
|
|
80
76
|
new_state_dict[k] = torch.as_tensor(v, device=device, dtype=ref.dtype)
|
|
81
|
-
|
|
82
77
|
self.model.load_state_dict(new_state_dict, strict=True)
|
|
83
78
|
|
|
84
79
|
def train(self, train_loader, epoch, device, privacy_engine, diff_priv=False) -> float:
|
|
@@ -89,66 +84,51 @@ class Model:
|
|
|
89
84
|
|
|
90
85
|
for i, (X_train, y_train) in enumerate(train_loader):
|
|
91
86
|
X_train = X_train.to(device)
|
|
92
|
-
y_train = y_train.to(device).view(-1)
|
|
87
|
+
y_train = y_train.to(device).view(-1)
|
|
93
88
|
|
|
94
|
-
# -------------------------
|
|
95
|
-
# HARD INPUT SANITY CHECKS
|
|
96
|
-
# -------------------------
|
|
97
89
|
if torch.isnan(X_train).any() or torch.isinf(X_train).any():
|
|
98
|
-
|
|
90
|
+
_log.warning(f"Train batch {i}: X_train has NaN/Inf -> skipping batch")
|
|
99
91
|
continue
|
|
100
92
|
if torch.isnan(y_train).any() or torch.isinf(y_train).any():
|
|
101
|
-
|
|
93
|
+
_log.warning(f"Train batch {i}: y_train has NaN/Inf -> skipping batch")
|
|
102
94
|
continue
|
|
103
95
|
|
|
104
|
-
# Optional: print scales once (helps diagnose exploding grads)
|
|
105
96
|
if i == 0 and epoch == 0:
|
|
106
97
|
try:
|
|
107
|
-
|
|
108
|
-
"
|
|
98
|
+
_log.debug(
|
|
99
|
+
f"Train scale check | "
|
|
100
|
+
f"X min/max: {X_train.min().item():.4f} / {X_train.max().item():.4f} | "
|
|
101
|
+
f"y min/max: {y_train.min().item():.4f} / {y_train.max().item():.4f}"
|
|
102
|
+
)
|
|
109
103
|
except Exception:
|
|
110
104
|
pass
|
|
111
105
|
|
|
112
106
|
self.optimizer.zero_grad(set_to_none=True)
|
|
113
107
|
|
|
114
|
-
|
|
115
|
-
# FORWARD
|
|
116
|
-
# -------------------------
|
|
117
|
-
y_hat = self.model(X_train).view(-1) # ensure (batch,)
|
|
108
|
+
y_hat = self.model(X_train).view(-1)
|
|
118
109
|
|
|
119
110
|
if torch.isnan(y_hat).any() or torch.isinf(y_hat).any():
|
|
120
|
-
|
|
111
|
+
_log.warning(f"Train batch {i}: y_hat has NaN/Inf -> skipping batch")
|
|
121
112
|
continue
|
|
122
113
|
|
|
123
|
-
# -------------------------
|
|
124
|
-
# LOSS
|
|
125
|
-
# -------------------------
|
|
126
114
|
loss = self.criterion(y_hat, y_train)
|
|
127
115
|
|
|
128
116
|
if torch.isnan(loss) or torch.isinf(loss):
|
|
129
|
-
|
|
117
|
+
_log.warning(f"Train batch {i}: loss is NaN/Inf -> skipping batch")
|
|
130
118
|
continue
|
|
131
119
|
|
|
132
120
|
losses.append(float(loss.item()))
|
|
133
|
-
|
|
134
|
-
# -------------------------
|
|
135
|
-
# BACKWARD
|
|
136
|
-
# -------------------------
|
|
137
121
|
loss.backward()
|
|
138
122
|
|
|
139
|
-
# -------------------------
|
|
140
|
-
# GRADIENT SANITY CHECK
|
|
141
|
-
# If grads are NaN/Inf, DO NOT UPDATE WEIGHTS
|
|
142
|
-
# -------------------------
|
|
143
123
|
bad_grad = False
|
|
144
124
|
for name, p in self.model.named_parameters():
|
|
145
125
|
if p.grad is None:
|
|
146
126
|
continue
|
|
147
127
|
if torch.isnan(p.grad).any() or torch.isinf(p.grad).any():
|
|
148
|
-
|
|
149
|
-
f"
|
|
150
|
-
f"(NaN={torch.isnan(p.grad).sum().item()},
|
|
151
|
-
f"-> skipping update"
|
|
128
|
+
_log.warning(
|
|
129
|
+
f"Train batch {i}: grad NaN/Inf in {name} "
|
|
130
|
+
f"(NaN={torch.isnan(p.grad).sum().item()}, "
|
|
131
|
+
f"Inf={torch.isinf(p.grad).sum().item()}) -> skipping update"
|
|
152
132
|
)
|
|
153
133
|
bad_grad = True
|
|
154
134
|
break
|
|
@@ -157,46 +137,32 @@ class Model:
|
|
|
157
137
|
self.optimizer.zero_grad(set_to_none=True)
|
|
158
138
|
continue
|
|
159
139
|
|
|
160
|
-
# -------------------------
|
|
161
|
-
# CLIP + STEP
|
|
162
|
-
# -------------------------
|
|
163
140
|
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
|
|
164
141
|
self.optimizer.step()
|
|
165
142
|
|
|
166
|
-
# -------------------------
|
|
167
|
-
# METRICS
|
|
168
|
-
# -------------------------
|
|
169
143
|
if self.task_type == "binary":
|
|
170
|
-
# Assumes y_hat is probability (Sigmoid model + BCELoss)
|
|
171
144
|
y_prob = y_hat.detach().cpu().numpy()
|
|
172
145
|
y_true = y_train.detach().cpu().numpy()
|
|
173
146
|
acc = accuracy_score(y_true, (y_prob >= 0.5).astype(int))
|
|
174
147
|
metrics.append(float(acc))
|
|
175
|
-
|
|
176
148
|
elif self.task_type == "regression":
|
|
177
149
|
rmse = torch.sqrt(torch.mean((y_hat - y_train) ** 2)).item()
|
|
178
150
|
metrics.append(float(rmse))
|
|
179
151
|
|
|
180
|
-
# -------------------------
|
|
181
|
-
# DP EPSILON
|
|
182
|
-
# -------------------------
|
|
183
152
|
if diff_priv:
|
|
184
153
|
epsilon = float(privacy_engine.get_epsilon(float(params["DELTA"])))
|
|
185
154
|
|
|
186
|
-
# -------------------------
|
|
187
|
-
# LOGGING
|
|
188
|
-
# -------------------------
|
|
189
155
|
if (i + 1) % 10 == 0 and len(losses) > 0:
|
|
190
156
|
if self.task_type == "binary" and len(metrics) > 0:
|
|
191
|
-
|
|
192
|
-
f"
|
|
157
|
+
_log.info(
|
|
158
|
+
f"Train Epoch {epoch} | batch {i+1} | "
|
|
193
159
|
f"Loss {np.mean(losses):.6f} | "
|
|
194
160
|
f"Acc {np.mean(metrics) * 100:.2f}%"
|
|
195
161
|
+ (f" | ε={epsilon:.2f}" if diff_priv else "")
|
|
196
162
|
)
|
|
197
163
|
elif self.task_type == "regression" and len(metrics) > 0:
|
|
198
|
-
|
|
199
|
-
f"
|
|
164
|
+
_log.info(
|
|
165
|
+
f"Train Epoch {epoch} | batch {i+1} | "
|
|
200
166
|
f"Loss {np.mean(losses):.6f} | "
|
|
201
167
|
f"RMSE {np.mean(metrics):.6f}"
|
|
202
168
|
+ (f" | ε={epsilon:.2f}" if diff_priv else "")
|
|
@@ -205,9 +171,17 @@ class Model:
|
|
|
205
171
|
mean_train_loss = float(np.mean(losses)) if len(losses) else float("nan")
|
|
206
172
|
mean_train_metric = float(np.mean(metrics)) if len(metrics) else float("nan")
|
|
207
173
|
|
|
208
|
-
|
|
209
|
-
|
|
174
|
+
total_grad_norm = 0.0
|
|
175
|
+
for p in self.model.parameters():
|
|
176
|
+
if p.grad is not None:
|
|
177
|
+
total_grad_norm += p.grad.detach().norm(2).item() ** 2
|
|
178
|
+
total_grad_norm = total_grad_norm ** 0.5
|
|
179
|
+
# store it
|
|
180
|
+
if not hasattr(self, "grad_norms"):
|
|
181
|
+
self.grad_norms = []
|
|
182
|
+
self.grad_norms.append(total_grad_norm)
|
|
210
183
|
|
|
184
|
+
return epsilon, mean_train_loss, mean_train_metric
|
|
211
185
|
|
|
212
186
|
def evaluate(self, val_loader, device=torch.device("cpu")):
|
|
213
187
|
self.model.eval()
|
|
@@ -217,26 +191,24 @@ class Model:
|
|
|
217
191
|
with torch.no_grad():
|
|
218
192
|
for i, (X_test, y_test) in enumerate(val_loader):
|
|
219
193
|
X_test = X_test.to(device)
|
|
220
|
-
y_test_t = y_test.to(device).view(-1)
|
|
221
|
-
y_hat_t = self.model(X_test).view(-1)
|
|
194
|
+
y_test_t = y_test.to(device).view(-1)
|
|
195
|
+
y_hat_t = self.model(X_test).view(-1)
|
|
222
196
|
|
|
223
|
-
# Skip bad batches
|
|
224
197
|
if torch.isnan(y_hat_t).any() or torch.isinf(y_hat_t).any():
|
|
225
|
-
|
|
198
|
+
_log.warning(f"Eval batch {i}: y_hat NaN/Inf -> skipping")
|
|
226
199
|
continue
|
|
227
200
|
if torch.isnan(y_test_t).any() or torch.isinf(y_test_t).any():
|
|
228
|
-
|
|
201
|
+
_log.warning(f"Eval batch {i}: y_test NaN/Inf -> skipping")
|
|
229
202
|
continue
|
|
230
203
|
|
|
231
204
|
crit = self.criterion.to(y_hat_t.device) if hasattr(self.criterion, "to") else self.criterion
|
|
232
205
|
loss_t = crit(y_hat_t, y_test_t)
|
|
233
206
|
|
|
234
207
|
if torch.isnan(loss_t) or torch.isinf(loss_t):
|
|
235
|
-
|
|
208
|
+
_log.warning(f"Eval batch {i}: loss NaN/Inf -> skipping")
|
|
236
209
|
continue
|
|
237
210
|
|
|
238
211
|
losses.append(float(loss_t.item()))
|
|
239
|
-
# IMPORTANT: append 1D numpy arrays (never scalars)
|
|
240
212
|
y_true_all.append(np.atleast_1d(y_test_t.detach().cpu().numpy()))
|
|
241
213
|
y_pred_all.append(np.atleast_1d(y_hat_t.detach().cpu().numpy()))
|
|
242
214
|
|
|
@@ -245,7 +217,6 @@ class Model:
|
|
|
245
217
|
|
|
246
218
|
y_true = np.concatenate(y_true_all, axis=0)
|
|
247
219
|
y_pred = np.concatenate(y_pred_all, axis=0)
|
|
248
|
-
|
|
249
220
|
mean_loss = float(np.mean(losses))
|
|
250
221
|
|
|
251
222
|
if self.task_type == "binary":
|
|
@@ -256,28 +227,19 @@ class Model:
|
|
|
256
227
|
if self.task_type == "regression":
|
|
257
228
|
mask = np.isfinite(y_true) & np.isfinite(y_pred)
|
|
258
229
|
if mask.sum() == 0:
|
|
259
|
-
return mean_loss, {
|
|
260
|
-
|
|
261
|
-
"mae": float("nan"),
|
|
262
|
-
"r2": float("nan"),
|
|
263
|
-
"adj_r2": float("nan"),
|
|
264
|
-
}
|
|
230
|
+
return mean_loss, {"rmse": float("nan"), "mae": float("nan"),
|
|
231
|
+
"r2": float("nan"), "adj_r2": float("nan")}
|
|
265
232
|
|
|
266
233
|
y_true_c = y_true[mask]
|
|
267
234
|
y_pred_c = y_pred[mask]
|
|
268
|
-
|
|
269
235
|
mse = mean_squared_error(y_true_c, y_pred_c)
|
|
270
236
|
mae = mean_absolute_error(y_true_c, y_pred_c)
|
|
271
237
|
r2 = r2_score(y_true_c, y_pred_c)
|
|
272
238
|
rmse = float(np.sqrt(mse))
|
|
273
|
-
|
|
274
|
-
# ---- Adjusted R² ----
|
|
275
239
|
n = int(len(y_true_c))
|
|
276
240
|
|
|
277
|
-
# Determine p (number of predictors)
|
|
278
241
|
p = None
|
|
279
242
|
try:
|
|
280
|
-
# Works for Linear Regressor and most MLPs (first Linear layer)
|
|
281
243
|
for m in self.model.modules():
|
|
282
244
|
if isinstance(m, torch.nn.Linear):
|
|
283
245
|
p = int(m.in_features)
|
|
@@ -289,9 +251,8 @@ class Model:
|
|
|
289
251
|
adj_r2 = float("nan")
|
|
290
252
|
else:
|
|
291
253
|
adj_r2 = 1.0 - (1.0 - float(r2)) * (n - 1) / (n - p - 1)
|
|
292
|
-
|
|
293
|
-
print("adj_r2 check | n =", n, "| p =", p, "| condition n>p+1 =", n > p + 1)
|
|
294
254
|
|
|
255
|
+
_log.debug(f"adj_r2 check | n={n} | p={p} | condition n>p+1={n > p + 1}")
|
|
295
256
|
|
|
296
257
|
return mean_loss, {
|
|
297
258
|
"rmse": float(rmse),
|
|
@@ -302,44 +263,18 @@ class Model:
|
|
|
302
263
|
|
|
303
264
|
return mean_loss, {}
|
|
304
265
|
|
|
305
|
-
|
|
306
266
|
@staticmethod
|
|
307
|
-
def save_model(model
|
|
308
|
-
"""
|
|
309
|
-
Saves a PyTorch model to a file.
|
|
310
|
-
|
|
311
|
-
Args:
|
|
312
|
-
model (torch.nn.Module): PyTorch model to be saved.
|
|
313
|
-
model_name (str): Name of the model file.
|
|
314
|
-
|
|
315
|
-
Raises:
|
|
316
|
-
Exception: If there is an issue during the saving process.
|
|
317
|
-
|
|
318
|
-
Returns:
|
|
319
|
-
None
|
|
320
|
-
"""
|
|
267
|
+
def save_model(model, model_name: str):
|
|
321
268
|
try:
|
|
322
269
|
torch.save(model, '../../notebooks/.ipynb_checkpoints/trainedModels/' + model_name + ".pth")
|
|
323
270
|
except Exception as e:
|
|
324
271
|
raise Exception(f"Error saving the model: {str(e)}")
|
|
325
|
-
|
|
272
|
+
|
|
326
273
|
@staticmethod
|
|
327
274
|
def load_model(model_path: str):
|
|
328
|
-
"""
|
|
329
|
-
Loads a PyTorch model from a file.
|
|
330
|
-
|
|
331
|
-
Args:
|
|
332
|
-
model_path (str): Path to the model file to be loaded.
|
|
333
|
-
|
|
334
|
-
Returns:
|
|
335
|
-
torch.nn.Module: Loaded PyTorch model.
|
|
336
|
-
"""
|
|
337
|
-
# Ensure models are loaded onto the CPU when CUDA is not available
|
|
338
275
|
torch_kwargs = {"weights_only": False}
|
|
339
276
|
if torch.cuda.is_available():
|
|
340
|
-
loaded_model = torch.load(model_path
|
|
277
|
+
loaded_model = torch.load(model_path, **torch_kwargs)
|
|
341
278
|
else:
|
|
342
|
-
loaded_model = torch.load(model_path, map_location=torch.device('cpu')
|
|
343
|
-
return loaded_model
|
|
344
|
-
|
|
345
|
-
|
|
279
|
+
loaded_model = torch.load(model_path, map_location=torch.device('cpu'), **torch_kwargs)
|
|
280
|
+
return loaded_model
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
DELTA: 1.0e-05
|
|
2
2
|
EPSILON: 5.0
|
|
3
3
|
MAX_GRAD_NORM: 1.0
|
|
4
|
-
diff_privacy:
|
|
5
|
-
lr: 0.
|
|
4
|
+
diff_privacy: false
|
|
5
|
+
lr: 0.01
|
|
6
6
|
min_evalclient: 2
|
|
7
7
|
num_rounds: 20
|
|
8
8
|
optimizer: SGD
|
|
@@ -10,5 +10,5 @@ path_to_master_csv: /home/local/USHERBROOKE/saho6810/MEDfl/code/MEDfl/notebooks/
|
|
|
10
10
|
path_to_test_csv: /home/local/USHERBROOKE/saho6810/MEDfl/code/MEDfl/notebooks/data/masterDataSet/Mimic_train.csv
|
|
11
11
|
task: BinaryClassification
|
|
12
12
|
test_batch_size: 1
|
|
13
|
-
train_batch_size:
|
|
14
|
-
train_epochs:
|
|
13
|
+
train_batch_size: 6
|
|
14
|
+
train_epochs: 1
|