MEDfl 2.0.10.dev9__tar.gz → 2.0.10.dev11__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.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/client.py +29 -14
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/holdout.py +68 -38
- medfl-2.0.10.dev11/MEDfl/rw/identity.py +12 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/client.py +22 -14
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/PKG-INFO +1 -1
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/medfl.egg-info/PKG-INFO +1 -1
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/medfl.egg-info/SOURCES.txt +1 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/setup.py +1 -1
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/LICENSE +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/client.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/dynamicModal.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/federated_dataset.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/flpipeline.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/model.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/params.yaml +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/params_optimiser.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/plot.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/server.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/aggregation.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/config.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/explainer.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/xgboost_explainer.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/strategy.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/utils.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/client.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/config.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/data.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/metrics.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/serialization.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/server.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/strategy.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/database_connector.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/dataset.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/flsetup.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/net_helper.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/net_manager_queries.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/network.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/NetManager/node.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/model.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/server.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/strategy.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/config.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/data.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/metrics.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/serialization.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/strategy.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/scripts/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/scripts/base.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/MEDfl/scripts/create_db.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/README.md +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/alembic/__init__.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/alembic/env.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/medfl.egg-info/dependency_links.txt +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/medfl.egg-info/requires.txt +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/medfl.egg-info/top_level.txt +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/setup.cfg +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_dataset.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_federated_dataset.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_flpipeline.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_flsetup.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_network.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_node.py +0 -0
- {medfl-2.0.10.dev9 → medfl-2.0.10.dev11}/tests/test_rw_holdout.py +0 -0
|
@@ -13,6 +13,7 @@ from sklearn.model_selection import train_test_split
|
|
|
13
13
|
from MEDfl.rw.model import Net # votre définition de modèle
|
|
14
14
|
from MEDfl.LearningManager.shap import LocalPyTorchSHAPExplainer
|
|
15
15
|
from MEDfl.rw.holdout import run_local_holdout
|
|
16
|
+
from MEDfl.rw.identity import client_name
|
|
16
17
|
import socket
|
|
17
18
|
import platform
|
|
18
19
|
import psutil
|
|
@@ -262,6 +263,14 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
262
263
|
X_train, y_train = X_trval, y_trval
|
|
263
264
|
X_val, y_val = np.empty((0, X_all.shape[1])), np.empty((0,))
|
|
264
265
|
|
|
266
|
+
# the whole learning set, for the optimized holdout threshold. Kept as arrays: with DP, Opacus replaces
|
|
267
|
+
# the train loader by a Poisson-sampling one that does not go over every training row
|
|
268
|
+
self._learning_splits = {
|
|
269
|
+
"train": (X_train, y_train),
|
|
270
|
+
"validation": (X_val, y_val),
|
|
271
|
+
"test": (X_test, y_test),
|
|
272
|
+
}
|
|
273
|
+
|
|
265
274
|
# ---------- build loaders
|
|
266
275
|
batch_size = self.dp_config.batch_size if self.dp_config else 32
|
|
267
276
|
self.train_loader = self._mk_loader(X_train, y_train, batch_size, shuffle=True)
|
|
@@ -426,7 +435,7 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
426
435
|
|
|
427
436
|
# Convert NumPy arrays to lists before JSON serialization.
|
|
428
437
|
serializable_result = {
|
|
429
|
-
"client_id":
|
|
438
|
+
"client_id": client_name(),
|
|
430
439
|
"feature_names": feature_names,
|
|
431
440
|
"available_samples": int(len(loader.dataset)),
|
|
432
441
|
"sample_count": explained_samples,
|
|
@@ -502,7 +511,7 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
502
511
|
auc = float("nan")
|
|
503
512
|
acc = accuracy_score(y_true, binary_preds)
|
|
504
513
|
|
|
505
|
-
hostname =
|
|
514
|
+
hostname = client_name()
|
|
506
515
|
os_type = platform.system()
|
|
507
516
|
metrics = {
|
|
508
517
|
"hostname": hostname,
|
|
@@ -608,9 +617,9 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
608
617
|
features=self.effective_features,
|
|
609
618
|
target=self.effective_target,
|
|
610
619
|
predict=self._predict_probabilities,
|
|
611
|
-
client_id=
|
|
620
|
+
client_id=client_name(),
|
|
612
621
|
drop_missing_features=True,
|
|
613
|
-
validation=self.
|
|
622
|
+
validation=self._learning_set_predictions,
|
|
614
623
|
)
|
|
615
624
|
)
|
|
616
625
|
|
|
@@ -625,18 +634,24 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
625
634
|
logits = self.model(torch.tensor(X, dtype=torch.float32))
|
|
626
635
|
return torch.sigmoid(logits.reshape(-1)).cpu().numpy()
|
|
627
636
|
|
|
628
|
-
def
|
|
629
|
-
"""
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
+
def _learning_set_predictions(self):
|
|
638
|
+
"""
|
|
639
|
+
(y_true, y_prob, rows per split) of the whole learning set (train + validation + test splits),
|
|
640
|
+
for the optimized holdout threshold.
|
|
641
|
+
"""
|
|
642
|
+
y_parts, prob_parts, splits = [], [], {}
|
|
643
|
+
for split, (X, y) in getattr(self, "_learning_splits", {}).items():
|
|
644
|
+
y = np.asarray(y, dtype=float).reshape(-1)
|
|
645
|
+
splits[split] = int(len(y))
|
|
646
|
+
if len(y):
|
|
647
|
+
y_parts.append(y)
|
|
648
|
+
prob_parts.append(self._predict_probabilities(np.asarray(X, dtype=np.float32)))
|
|
649
|
+
if not y_parts:
|
|
650
|
+
return np.empty((0,)), np.empty((0,)), splits
|
|
651
|
+
return np.concatenate(y_parts), np.concatenate(prob_parts), splits
|
|
637
652
|
|
|
638
653
|
def get_properties(self, config):
|
|
639
|
-
hostname =
|
|
654
|
+
hostname = client_name()
|
|
640
655
|
os_type = platform.system()
|
|
641
656
|
|
|
642
657
|
if self._initialized:
|
|
@@ -19,10 +19,12 @@ data: the server features in the server order, the raw values (the
|
|
|
19
19
|
real-world clients do not impute or scale), and a binary 0/1 target.
|
|
20
20
|
|
|
21
21
|
Optimized threshold (threshold_source="optimized"): in the same final
|
|
22
|
-
round, every client also sends the two score histograms of its
|
|
23
|
-
|
|
24
|
-
threshold among the histogram bin edges (Youden's
|
|
25
|
-
sensitivity).
|
|
22
|
+
round, every client also sends the two score histograms of its whole
|
|
23
|
+
learning set (train + validation + test splits, LEARNING_SET). The server
|
|
24
|
+
sums them and picks the threshold among the histogram bin edges (Youden's
|
|
25
|
+
J or a target sensitivity). The training rows were used to fit the model,
|
|
26
|
+
so the learning-set metrics at that threshold are optimistic: the holdout
|
|
27
|
+
gives the unbiased estimate. At a bin edge, the histograms give exact confusion matrices,
|
|
26
28
|
so the holdout results are then recomputed from the holdout histograms
|
|
27
29
|
at that threshold. The holdout histograms are never used to choose it.
|
|
28
30
|
"""
|
|
@@ -46,6 +48,10 @@ HOLDOUT_FAILED = "failed"
|
|
|
46
48
|
HOLDOUT_UNSUPPORTED = "unsupported_task"
|
|
47
49
|
|
|
48
50
|
THRESHOLD_SOURCES = ("manual", "optimized")
|
|
51
|
+
# data the clients use for the optimized threshold (sent back in their summary, so a client running an
|
|
52
|
+
# older MEDfl that only sends its validation split is detected)
|
|
53
|
+
LEARNING_SET = "learning_set"
|
|
54
|
+
LEARNING_SET_SPLITS = ("train", "validation", "test")
|
|
49
55
|
# criterion key -> label shown in the results
|
|
50
56
|
THRESHOLD_CRITERIA = {"youden": "Youden's J", "target_sensitivity": "Target sensitivity"}
|
|
51
57
|
|
|
@@ -70,13 +76,14 @@ class HoldoutConfig:
|
|
|
70
76
|
include_client_results: bool = True
|
|
71
77
|
|
|
72
78
|
# "manual": the threshold above. "optimized": chosen on the clients'
|
|
73
|
-
# validation
|
|
79
|
+
# learning sets (train + validation + test), at a histogram bin edge
|
|
80
|
+
# (resolution 1 / num_bins)
|
|
74
81
|
threshold_source: str = "manual"
|
|
75
82
|
|
|
76
83
|
# "youden" or "target_sensitivity" (optimized threshold only)
|
|
77
84
|
threshold_criterion: str = "youden"
|
|
78
85
|
|
|
79
|
-
# minimal pooled
|
|
86
|
+
# minimal pooled learning-set sensitivity of the target_sensitivity criterion
|
|
80
87
|
target_sensitivity: float = 0.9
|
|
81
88
|
|
|
82
89
|
def validate(self) -> None:
|
|
@@ -115,7 +122,7 @@ class HoldoutConfig:
|
|
|
115
122
|
"holdout_threshold": float(self.threshold),
|
|
116
123
|
"holdout_num_bins": int(self.num_bins),
|
|
117
124
|
"holdout_minimum_samples": int(self.minimum_samples),
|
|
118
|
-
# the clients also send their
|
|
125
|
+
# the clients also send their learning-set histograms
|
|
119
126
|
"threshold_optimization_enabled": self.threshold_source == "optimized",
|
|
120
127
|
}
|
|
121
128
|
|
|
@@ -285,11 +292,13 @@ def local_validation_summary(
|
|
|
285
292
|
num_bins: int,
|
|
286
293
|
minimum_samples: int,
|
|
287
294
|
client_id: str,
|
|
295
|
+
splits: Optional[Dict[str, int]] = None,
|
|
288
296
|
) -> Dict[str, Any]:
|
|
289
297
|
"""
|
|
290
|
-
Summary of one client's validation
|
|
291
|
-
safe to send to the server: counts and the two score
|
|
292
|
-
bins as the holdout). Below minimum_samples rows, only
|
|
298
|
+
Summary of one client's learning set (train + validation + test) for the
|
|
299
|
+
optimized threshold, safe to send to the server: counts and the two score
|
|
300
|
+
histograms (same bins as the holdout). Below minimum_samples rows, only
|
|
301
|
+
the counts are sent. splits gives the number of rows of each split.
|
|
293
302
|
"""
|
|
294
303
|
y_true = np.asarray(y_true, dtype=float).reshape(-1)
|
|
295
304
|
y_prob = np.asarray(y_prob, dtype=float).reshape(-1)
|
|
@@ -301,7 +310,7 @@ def local_validation_summary(
|
|
|
301
310
|
classes = set(np.unique(y_true).tolist())
|
|
302
311
|
if not classes.issubset({0.0, 1.0}):
|
|
303
312
|
raise ValueError(
|
|
304
|
-
f"The
|
|
313
|
+
f"The learning-set target must be binary (0/1), found {len(classes)} "
|
|
305
314
|
f"values: {sorted(classes)[:10]}"
|
|
306
315
|
)
|
|
307
316
|
|
|
@@ -309,11 +318,13 @@ def local_validation_summary(
|
|
|
309
318
|
positives = int(y_true.sum())
|
|
310
319
|
summary = {
|
|
311
320
|
"client_id": client_id,
|
|
321
|
+
"data": LEARNING_SET,
|
|
312
322
|
"dataset": {
|
|
313
323
|
"rows": rows,
|
|
314
324
|
"positives": positives,
|
|
315
325
|
"negatives": rows - positives,
|
|
316
326
|
"rows_without_prediction": int((~predicted).sum()),
|
|
327
|
+
"splits": {key: int(value) for key, value in (splits or {}).items()},
|
|
317
328
|
},
|
|
318
329
|
}
|
|
319
330
|
|
|
@@ -321,7 +332,7 @@ def local_validation_summary(
|
|
|
321
332
|
return {
|
|
322
333
|
**summary,
|
|
323
334
|
"status": HOLDOUT_MISSING,
|
|
324
|
-
"reason": "No
|
|
335
|
+
"reason": "No learning-set rows",
|
|
325
336
|
}
|
|
326
337
|
|
|
327
338
|
if rows < int(minimum_samples):
|
|
@@ -329,7 +340,7 @@ def local_validation_summary(
|
|
|
329
340
|
**summary,
|
|
330
341
|
"status": HOLDOUT_SKIPPED,
|
|
331
342
|
"reason": (
|
|
332
|
-
f"{rows}
|
|
343
|
+
f"{rows} learning-set row(s), fewer than the minimum of "
|
|
333
344
|
f"{int(minimum_samples)}"
|
|
334
345
|
),
|
|
335
346
|
}
|
|
@@ -347,35 +358,38 @@ def local_validation_summary(
|
|
|
347
358
|
|
|
348
359
|
def run_local_validation(
|
|
349
360
|
config: Dict[str, Any],
|
|
350
|
-
validation: Optional[Callable[[], Tuple[np.ndarray, np.ndarray]]],
|
|
361
|
+
validation: Optional[Callable[[], Tuple[np.ndarray, np.ndarray, Dict[str, int]]]],
|
|
351
362
|
client_id: str,
|
|
352
363
|
) -> Dict[str, Any]:
|
|
353
364
|
"""
|
|
354
365
|
Entries to add to the Flower evaluate metrics for the optimized
|
|
355
366
|
threshold: threshold_validation_status, then threshold_validation
|
|
356
|
-
(JSON) or threshold_validation_error.
|
|
367
|
+
(JSON) or threshold_validation_error. (The metric names are kept from
|
|
368
|
+
the validation-only versions; the data is the learning set.)
|
|
357
369
|
|
|
358
|
-
validation returns (y_true, y_prob) of the client's
|
|
359
|
-
with the final global model
|
|
370
|
+
validation returns (y_true, y_prob, splits) of the client's whole
|
|
371
|
+
learning set (train + validation + test) with the final global model,
|
|
372
|
+
splits being the number of rows of each split.
|
|
360
373
|
"""
|
|
361
374
|
if validation is None:
|
|
362
375
|
return {
|
|
363
376
|
"threshold_validation_status": HOLDOUT_MISSING,
|
|
364
|
-
"threshold_validation_error": "This client cannot send
|
|
377
|
+
"threshold_validation_error": "This client cannot send learning-set data",
|
|
365
378
|
}
|
|
366
379
|
|
|
367
380
|
try:
|
|
368
|
-
y_true, y_prob = validation()
|
|
381
|
+
y_true, y_prob, splits = validation()
|
|
369
382
|
summary = local_validation_summary(
|
|
370
383
|
y_true=y_true,
|
|
371
384
|
y_prob=y_prob,
|
|
372
385
|
num_bins=int(config.get("holdout_num_bins", 100)),
|
|
373
386
|
minimum_samples=int(config.get("holdout_minimum_samples", 10)),
|
|
374
387
|
client_id=client_id,
|
|
388
|
+
splits=splits,
|
|
375
389
|
)
|
|
376
390
|
print(
|
|
377
|
-
f"[Client/Holdout]
|
|
378
|
-
f"client={client_id} | rows={summary['dataset']['rows']}",
|
|
391
|
+
f"[Client/Holdout] Learning set for the threshold: {summary['status']} | "
|
|
392
|
+
f"client={client_id} | rows={summary['dataset']['rows']} | splits={splits}",
|
|
379
393
|
flush=True,
|
|
380
394
|
)
|
|
381
395
|
return {
|
|
@@ -383,7 +397,7 @@ def run_local_validation(
|
|
|
383
397
|
"threshold_validation": json.dumps(summary, allow_nan=False),
|
|
384
398
|
}
|
|
385
399
|
except Exception as exc:
|
|
386
|
-
print(f"[Client/Holdout]
|
|
400
|
+
print(f"[Client/Holdout] Learning set for the threshold failed: {exc}", flush=True)
|
|
387
401
|
return {
|
|
388
402
|
"threshold_validation_status": HOLDOUT_FAILED,
|
|
389
403
|
"threshold_validation_error": str(exc),
|
|
@@ -398,7 +412,7 @@ def run_local_holdout(
|
|
|
398
412
|
predict: Callable[[np.ndarray], np.ndarray],
|
|
399
413
|
client_id: str,
|
|
400
414
|
drop_missing_features: bool,
|
|
401
|
-
validation: Optional[Callable[[], Tuple[np.ndarray, np.ndarray]]] = None,
|
|
415
|
+
validation: Optional[Callable[[], Tuple[np.ndarray, np.ndarray, Dict[str, int]]]] = None,
|
|
402
416
|
) -> Dict[str, Any]:
|
|
403
417
|
"""
|
|
404
418
|
Run the holdout test on a client and return the entries to add to the
|
|
@@ -408,9 +422,9 @@ def run_local_holdout(
|
|
|
408
422
|
predict maps the holdout features to positive-class probabilities with
|
|
409
423
|
the final global model.
|
|
410
424
|
|
|
411
|
-
When the server optimizes the threshold, the
|
|
425
|
+
When the server optimizes the threshold, the learning-set summary is
|
|
412
426
|
added too (see run_local_validation), even without a holdout file:
|
|
413
|
-
the client
|
|
427
|
+
the client learning set still helps choose the threshold.
|
|
414
428
|
"""
|
|
415
429
|
entries = {}
|
|
416
430
|
if bool(config.get("threshold_optimization_enabled", False)):
|
|
@@ -534,7 +548,7 @@ def select_threshold_from_histograms(
|
|
|
534
548
|
positives, negatives = int(pos_hist.sum()), int(neg_hist.sum())
|
|
535
549
|
if positives == 0 or negatives == 0:
|
|
536
550
|
raise ValueError(
|
|
537
|
-
f"The
|
|
551
|
+
f"The learning-set data has a single class ({positives} positive, "
|
|
538
552
|
f"{negatives} negative): the threshold cannot be optimized"
|
|
539
553
|
)
|
|
540
554
|
|
|
@@ -558,7 +572,7 @@ def select_threshold_from_histograms(
|
|
|
558
572
|
reached = np.flatnonzero(sensitivity >= target - 1e-12)
|
|
559
573
|
if not len(reached):
|
|
560
574
|
raise ValueError(
|
|
561
|
-
f"No threshold reaches a
|
|
575
|
+
f"No threshold reaches a learning-set sensitivity of {target}: "
|
|
562
576
|
f"{int(pos_hist[0])} of the {positives} positive case(s) score below "
|
|
563
577
|
f"the lowest candidate threshold {1 / num_bins:g}. Increase the number "
|
|
564
578
|
f"of bins or lower the target sensitivity"
|
|
@@ -572,9 +586,10 @@ def select_threshold_from_histograms(
|
|
|
572
586
|
|
|
573
587
|
def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict[str, Any]:
|
|
574
588
|
"""
|
|
575
|
-
Choose the threshold on the pooled
|
|
576
|
-
round. Returns the selection with status
|
|
577
|
-
|
|
589
|
+
Choose the threshold on the pooled learning-set (train + validation +
|
|
590
|
+
test) histograms of the final round. Returns the selection with status
|
|
591
|
+
"completed" (threshold, edge, learning-set metrics) or "failed" (error),
|
|
592
|
+
and the excluded clients.
|
|
578
593
|
"""
|
|
579
594
|
completed, skipped, missing, failed = [], [], [], []
|
|
580
595
|
|
|
@@ -586,7 +601,7 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
586
601
|
if status is None:
|
|
587
602
|
missing.append({
|
|
588
603
|
"client_id": name,
|
|
589
|
-
"error": "No
|
|
604
|
+
"error": "No learning-set data sent (the client MEDfl version may not support the optimized threshold)",
|
|
590
605
|
})
|
|
591
606
|
continue
|
|
592
607
|
|
|
@@ -595,9 +610,17 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
595
610
|
try:
|
|
596
611
|
summary = json.loads(metrics["threshold_validation"])
|
|
597
612
|
except Exception as exc:
|
|
598
|
-
failed.append({"client_id": name, "error": f"Invalid
|
|
613
|
+
failed.append({"client_id": name, "error": f"Invalid learning-set JSON: {exc}"})
|
|
599
614
|
continue
|
|
600
615
|
|
|
616
|
+
if summary and summary.get("data") != LEARNING_SET:
|
|
617
|
+
# an older client sends only its validation split: never mix it with learning sets
|
|
618
|
+
failed.append({
|
|
619
|
+
"client_id": summary.get("client_id", name),
|
|
620
|
+
"error": "This client sent only its validation split: update MEDfl on it to use its whole learning set",
|
|
621
|
+
})
|
|
622
|
+
continue
|
|
623
|
+
|
|
601
624
|
if status == HOLDOUT_COMPLETED and summary:
|
|
602
625
|
if int(summary.get("num_bins", -1)) != int(holdout_config.num_bins):
|
|
603
626
|
failed.append({
|
|
@@ -620,7 +643,7 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
620
643
|
else:
|
|
621
644
|
failed.append({
|
|
622
645
|
"client_id": name,
|
|
623
|
-
"error": metrics.get("threshold_validation_error", "No
|
|
646
|
+
"error": metrics.get("threshold_validation_error", "No learning-set result returned"),
|
|
624
647
|
})
|
|
625
648
|
|
|
626
649
|
criterion = holdout_config.threshold_criterion
|
|
@@ -630,7 +653,8 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
630
653
|
"target_sensitivity": (
|
|
631
654
|
float(holdout_config.target_sensitivity) if criterion == "target_sensitivity" else None
|
|
632
655
|
),
|
|
633
|
-
"
|
|
656
|
+
"data": LEARNING_SET,
|
|
657
|
+
"method": f"pooled {int(holdout_config.num_bins)}-bin learning-set (train + validation + test) score histograms",
|
|
634
658
|
"num_bins": int(holdout_config.num_bins),
|
|
635
659
|
# the threshold is a bin edge
|
|
636
660
|
"resolution": 1.0 / int(holdout_config.num_bins),
|
|
@@ -640,16 +664,20 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
640
664
|
}
|
|
641
665
|
|
|
642
666
|
if not completed:
|
|
643
|
-
return {**selection, "status": "failed", "error": "No client returned
|
|
667
|
+
return {**selection, "status": "failed", "error": "No client returned learning-set data"}
|
|
644
668
|
|
|
645
669
|
pos_hist = np.sum([summary["pos_hist"] for summary in completed], axis=0)
|
|
646
670
|
neg_hist = np.sum([summary["neg_hist"] for summary in completed], axis=0)
|
|
647
671
|
positives, negatives = int(pos_hist.sum()), int(neg_hist.sum())
|
|
648
|
-
selection["
|
|
672
|
+
selection["learning_set"] = {
|
|
649
673
|
"clients": len(completed),
|
|
650
674
|
"rows": positives + negatives,
|
|
651
675
|
"positives": positives,
|
|
652
676
|
"negatives": negatives,
|
|
677
|
+
"splits": {
|
|
678
|
+
split: int(sum(summary["dataset"].get("splits", {}).get(split, 0) for summary in completed))
|
|
679
|
+
for split in LEARNING_SET_SPLITS
|
|
680
|
+
},
|
|
653
681
|
}
|
|
654
682
|
|
|
655
683
|
try:
|
|
@@ -660,7 +688,8 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
660
688
|
return {**selection, "status": "failed", "error": str(exc)}
|
|
661
689
|
|
|
662
690
|
confusion = histogram_confusion(pos_hist, neg_hist, edge)
|
|
663
|
-
|
|
691
|
+
# learning-set ROC curve: its points are the bin edges, so the chosen threshold is one of them
|
|
692
|
+
auc, roc = pooled_roc_auc(pos_hist, neg_hist)
|
|
664
693
|
return {
|
|
665
694
|
**selection,
|
|
666
695
|
"status": "completed",
|
|
@@ -668,6 +697,7 @@ def optimize_federated_threshold(results, holdout_config: HoldoutConfig) -> Dict
|
|
|
668
697
|
"edge": edge,
|
|
669
698
|
"metrics": {"auc": auc, **confusion_metrics(confusion)},
|
|
670
699
|
"confusion_matrix": confusion,
|
|
700
|
+
"roc_curve": roc,
|
|
671
701
|
}
|
|
672
702
|
|
|
673
703
|
|
|
@@ -746,7 +776,7 @@ def aggregate_federated_holdout(
|
|
|
746
776
|
}
|
|
747
777
|
|
|
748
778
|
if holdout_config.threshold_source == "optimized":
|
|
749
|
-
# chosen on the
|
|
779
|
+
# chosen on the learning-set histograms only, the holdout ones are not read here
|
|
750
780
|
selection = optimize_federated_threshold(results, holdout_config)
|
|
751
781
|
result["threshold_selection"] = selection
|
|
752
782
|
if selection["status"] != "completed":
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Name a real-world client reports to the server (properties, metrics, SHAP and holdout results).
|
|
3
|
+
|
|
4
|
+
The MEDfl agent ID (MEDFL_AGENT_ID, inherited from the agent that starts the client) when it is set,
|
|
5
|
+
so several agents on the same machine are told apart. Else the machine hostname, as before.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def client_name() -> str:
|
|
12
|
+
return (os.getenv("MEDFL_AGENT_ID") or "").strip() or socket.gethostname()
|
|
@@ -31,6 +31,7 @@ try:
|
|
|
31
31
|
except ImportError:
|
|
32
32
|
GPUtil = None
|
|
33
33
|
|
|
34
|
+
from MEDfl.rw.identity import client_name
|
|
34
35
|
from MEDfl.rw.xgboost.data import XGBoostDataModule
|
|
35
36
|
from MEDfl.rw.xgboost.metrics import evaluate_predictions
|
|
36
37
|
from MEDfl.rw.xgboost.serialization import (
|
|
@@ -374,7 +375,7 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
374
375
|
)
|
|
375
376
|
|
|
376
377
|
return {
|
|
377
|
-
"client_id":
|
|
378
|
+
"client_id": client_name(),
|
|
378
379
|
"feature_names": feature_names,
|
|
379
380
|
"available_samples": available_samples,
|
|
380
381
|
"sample_count": explained_samples,
|
|
@@ -457,7 +458,7 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
457
458
|
metrics = {
|
|
458
459
|
"backend": "xgboost",
|
|
459
460
|
"task": self.task,
|
|
460
|
-
"hostname":
|
|
461
|
+
"hostname": client_name(),
|
|
461
462
|
"os_type": platform.system(),
|
|
462
463
|
"num_train": int(self.metadata.get("num_train", 0)),
|
|
463
464
|
"features": ",".join(self.metadata.get("features", [])),
|
|
@@ -574,7 +575,7 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
574
575
|
if bool(config.get("holdout_enabled", False)):
|
|
575
576
|
if self.task != "binary":
|
|
576
577
|
metrics["holdout_status"] = HOLDOUT_UNSUPPORTED
|
|
577
|
-
metrics["holdout_client_id"] =
|
|
578
|
+
metrics["holdout_client_id"] = client_name()
|
|
578
579
|
metrics["holdout_error"] = (
|
|
579
580
|
"The holdout test supports binary classification "
|
|
580
581
|
f"only (task is '{self.task}')"
|
|
@@ -587,9 +588,9 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
587
588
|
features=self.data_module.effective_features,
|
|
588
589
|
target=self.data_module.effective_target,
|
|
589
590
|
predict=self._predict_probabilities,
|
|
590
|
-
client_id=
|
|
591
|
+
client_id=client_name(),
|
|
591
592
|
drop_missing_features=False,
|
|
592
|
-
validation=self.
|
|
593
|
+
validation=self._learning_set_predictions,
|
|
593
594
|
)
|
|
594
595
|
)
|
|
595
596
|
|
|
@@ -608,21 +609,28 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
608
609
|
)
|
|
609
610
|
return np.asarray(self.booster.predict(matrix), dtype=float)
|
|
610
611
|
|
|
611
|
-
def
|
|
612
|
-
"""
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
612
|
+
def _learning_set_predictions(self) -> Tuple[np.ndarray, np.ndarray, Dict[str, int]]:
|
|
613
|
+
"""
|
|
614
|
+
(y_true, y_prob, rows per split) of the whole learning set (train + validation + test splits),
|
|
615
|
+
for the optimized holdout threshold.
|
|
616
|
+
"""
|
|
617
|
+
y_parts, prob_parts, splits = [], [], {}
|
|
618
|
+
for split, matrix in (("train", self.dtrain), ("validation", self.dval), ("test", self.dtest)):
|
|
619
|
+
rows = 0 if matrix is None else int(matrix.num_row())
|
|
620
|
+
splits[split] = rows
|
|
621
|
+
if rows:
|
|
622
|
+
y_parts.append(np.asarray(matrix.get_label(), dtype=float))
|
|
623
|
+
prob_parts.append(np.asarray(self.booster.predict(matrix), dtype=float))
|
|
624
|
+
if not y_parts:
|
|
625
|
+
return np.empty((0,)), np.empty((0,)), splits
|
|
626
|
+
return np.concatenate(y_parts), np.concatenate(prob_parts), splits
|
|
619
627
|
|
|
620
628
|
def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes:
|
|
621
629
|
"""
|
|
622
630
|
Return machine and dataset properties.
|
|
623
631
|
"""
|
|
624
632
|
|
|
625
|
-
hostname =
|
|
633
|
+
hostname = client_name()
|
|
626
634
|
os_type = platform.system()
|
|
627
635
|
|
|
628
636
|
df = self.data_module.df
|
|
@@ -14,7 +14,7 @@ with open('requirements.txt') as f:
|
|
|
14
14
|
|
|
15
15
|
setup(
|
|
16
16
|
name="medfl",
|
|
17
|
-
version="2.0.10.
|
|
17
|
+
version="2.0.10.dev11",
|
|
18
18
|
author="MEDomics consortium",
|
|
19
19
|
author_email="medomics.info@gmail.com",
|
|
20
20
|
description="Python Open-source package for simulating federated learning and differential privacy",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|