MEDfl 2.0.10.dev10__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.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/client.py +24 -10
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/holdout.py +68 -38
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/client.py +16 -9
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/PKG-INFO +1 -1
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/medfl.egg-info/PKG-INFO +1 -1
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/setup.py +1 -1
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/LICENSE +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/client.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/dynamicModal.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/federated_dataset.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/flpipeline.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/model.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/params.yaml +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/params_optimiser.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/plot.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/server.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/aggregation.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/config.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/explainer.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/shap/xgboost_explainer.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/strategy.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/utils.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/client.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/config.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/data.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/metrics.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/serialization.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/server.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/LearningManager/xgboost/strategy.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/database_connector.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/dataset.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/flsetup.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/net_helper.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/net_manager_queries.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/network.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/NetManager/node.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/identity.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/model.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/server.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/strategy.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/config.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/data.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/metrics.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/serialization.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/rw/xgboost/strategy.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/scripts/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/scripts/base.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/MEDfl/scripts/create_db.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/README.md +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/alembic/__init__.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/alembic/env.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/medfl.egg-info/SOURCES.txt +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/medfl.egg-info/dependency_links.txt +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/medfl.egg-info/requires.txt +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/medfl.egg-info/top_level.txt +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/setup.cfg +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_dataset.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_federated_dataset.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_flpipeline.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_flsetup.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_network.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_node.py +0 -0
- {medfl-2.0.10.dev10 → medfl-2.0.10.dev11}/tests/test_rw_holdout.py +0 -0
|
@@ -263,6 +263,14 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
263
263
|
X_train, y_train = X_trval, y_trval
|
|
264
264
|
X_val, y_val = np.empty((0, X_all.shape[1])), np.empty((0,))
|
|
265
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
|
+
|
|
266
274
|
# ---------- build loaders
|
|
267
275
|
batch_size = self.dp_config.batch_size if self.dp_config else 32
|
|
268
276
|
self.train_loader = self._mk_loader(X_train, y_train, batch_size, shuffle=True)
|
|
@@ -611,7 +619,7 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
611
619
|
predict=self._predict_probabilities,
|
|
612
620
|
client_id=client_name(),
|
|
613
621
|
drop_missing_features=True,
|
|
614
|
-
validation=self.
|
|
622
|
+
validation=self._learning_set_predictions,
|
|
615
623
|
)
|
|
616
624
|
)
|
|
617
625
|
|
|
@@ -626,15 +634,21 @@ class FlowerClient(fl.client.NumPyClient):
|
|
|
626
634
|
logits = self.model(torch.tensor(X, dtype=torch.float32))
|
|
627
635
|
return torch.sigmoid(logits.reshape(-1)).cpu().numpy()
|
|
628
636
|
|
|
629
|
-
def
|
|
630
|
-
"""
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
638
652
|
|
|
639
653
|
def get_properties(self, config):
|
|
640
654
|
hostname = client_name()
|
|
@@ -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":
|
|
@@ -590,7 +590,7 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
590
590
|
predict=self._predict_probabilities,
|
|
591
591
|
client_id=client_name(),
|
|
592
592
|
drop_missing_features=False,
|
|
593
|
-
validation=self.
|
|
593
|
+
validation=self._learning_set_predictions,
|
|
594
594
|
)
|
|
595
595
|
)
|
|
596
596
|
|
|
@@ -609,14 +609,21 @@ class XGBoostFlowerClient(fl.client.Client):
|
|
|
609
609
|
)
|
|
610
610
|
return np.asarray(self.booster.predict(matrix), dtype=float)
|
|
611
611
|
|
|
612
|
-
def
|
|
613
|
-
"""
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
620
627
|
|
|
621
628
|
def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes:
|
|
622
629
|
"""
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|