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.
Files changed (56) hide show
  1. medfl-2.0.10.dev0/MEDfl/LearningManager/client.py +169 -0
  2. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/model.py +74 -139
  3. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/params.yaml +4 -4
  4. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/plot.py +105 -0
  5. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/server.py +65 -24
  6. medfl-2.0.10.dev0/MEDfl/LearningManager/strategy.py +348 -0
  7. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/flsetup.py +185 -74
  8. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/net_helper.py +73 -46
  9. medfl-2.0.10.dev0/MEDfl/rw/xgboost/__init__.py +16 -0
  10. medfl-2.0.10.dev0/MEDfl/rw/xgboost/client.py +379 -0
  11. medfl-2.0.10.dev0/MEDfl/rw/xgboost/config.py +84 -0
  12. medfl-2.0.10.dev0/MEDfl/rw/xgboost/data.py +253 -0
  13. medfl-2.0.10.dev0/MEDfl/rw/xgboost/metrics.py +176 -0
  14. medfl-2.0.10.dev0/MEDfl/rw/xgboost/serialization.py +56 -0
  15. medfl-2.0.10.dev0/MEDfl/rw/xgboost/strategy.py +380 -0
  16. {medfl-2.0.9/medfl.egg-info → medfl-2.0.10.dev0}/PKG-INFO +1 -1
  17. {medfl-2.0.9 → medfl-2.0.10.dev0/medfl.egg-info}/PKG-INFO +1 -1
  18. {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/SOURCES.txt +7 -0
  19. {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/requires.txt +1 -0
  20. {medfl-2.0.9 → medfl-2.0.10.dev0}/setup.py +1 -1
  21. medfl-2.0.9/MEDfl/LearningManager/client.py +0 -173
  22. medfl-2.0.9/MEDfl/LearningManager/strategy.py +0 -83
  23. {medfl-2.0.9 → medfl-2.0.10.dev0}/LICENSE +0 -0
  24. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/__init__.py +0 -0
  25. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/dynamicModal.py +0 -0
  26. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/federated_dataset.py +0 -0
  27. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/flpipeline.py +0 -0
  28. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/params_optimiser.py +0 -0
  29. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/LearningManager/utils.py +0 -0
  30. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/__init__.py +0 -0
  31. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/database_connector.py +0 -0
  32. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/dataset.py +0 -0
  33. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/net_manager_queries.py +0 -0
  34. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/network.py +0 -0
  35. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/NetManager/node.py +0 -0
  36. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/__init__.py +0 -0
  37. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/__init__.py +0 -0
  38. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/client.py +0 -0
  39. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/model.py +0 -0
  40. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/server.py +0 -0
  41. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/rw/strategy.py +0 -0
  42. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/scripts/__init__.py +0 -0
  43. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/scripts/base.py +0 -0
  44. {medfl-2.0.9 → medfl-2.0.10.dev0}/MEDfl/scripts/create_db.py +0 -0
  45. {medfl-2.0.9 → medfl-2.0.10.dev0}/README.md +0 -0
  46. {medfl-2.0.9 → medfl-2.0.10.dev0}/alembic/__init__.py +0 -0
  47. {medfl-2.0.9 → medfl-2.0.10.dev0}/alembic/env.py +0 -0
  48. {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/dependency_links.txt +0 -0
  49. {medfl-2.0.9 → medfl-2.0.10.dev0}/medfl.egg-info/top_level.txt +0 -0
  50. {medfl-2.0.9 → medfl-2.0.10.dev0}/setup.cfg +0 -0
  51. {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_dataset.py +0 -0
  52. {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_federated_dataset.py +0 -0
  53. {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_flpipeline.py +0 -0
  54. {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_flsetup.py +0 -0
  55. {medfl-2.0.9 → medfl-2.0.10.dev0}/tests/test_network.py +0 -0
  56. {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 accuracy_score, roc_auc_score, mean_squared_error, mean_absolute_error, r2_score
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] # reference tensor for dtype/shape
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) # ensure (batch,)
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
- print(f"❌ Train batch {i}: X_train has NaN/Inf -> skipping batch")
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
- print(f"❌ Train batch {i}: y_train has NaN/Inf -> skipping batch")
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
- print("Train scale check | X min/max:", X_train.min().item(), X_train.max().item(),
108
- "| y min/max:", y_train.min().item(), y_train.max().item())
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
- print(f"❌ Train batch {i}: y_hat has NaN/Inf -> skipping batch")
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
- print(f"❌ Train batch {i}: loss is NaN/Inf -> skipping batch")
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
- print(
149
- f"❌ Train batch {i}: grad NaN/Inf in {name} "
150
- f"(NaN={torch.isnan(p.grad).sum().item()}, Inf={torch.isinf(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
- print(
192
- f"\tTrain Epoch {epoch} | "
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
- print(
199
- f"\tTrain Epoch {epoch} | "
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
- return epsilon, mean_train_loss, mean_train_metric
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) # force 1D targets
221
- y_hat_t = self.model(X_test).view(-1) # force 1D preds
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
- print(f"❌ Eval batch {i}: y_hat NaN/Inf -> skipping")
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
- print(f"❌ Eval batch {i}: y_test NaN/Inf -> skipping")
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
- print(f"❌ Eval batch {i}: loss NaN/Inf -> skipping")
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
- "rmse": float("nan"),
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 , model_name:str):
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 , **torch_kwargs)
277
+ loaded_model = torch.load(model_path, **torch_kwargs)
341
278
  else:
342
- loaded_model = torch.load(model_path, map_location=torch.device('cpu') , **torch_kwargs)
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: true
5
- lr: 0.001
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: 32
14
- train_epochs: 20
13
+ train_batch_size: 6
14
+ train_epochs: 1