MEDfl 2.0.10.dev0__tar.gz → 2.0.10.dev2__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.dev2/MEDfl/LearningManager/xgboost/__init__.py +15 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/client.py +328 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/config.py +63 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/data.py +122 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/metrics.py +192 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/serialization.py +52 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/server.py +262 -0
- medfl-2.0.10.dev2/MEDfl/LearningManager/xgboost/strategy.py +576 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/strategy.py +303 -15
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/PKG-INFO +1 -1
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/medfl.egg-info/PKG-INFO +1 -1
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/medfl.egg-info/SOURCES.txt +8 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/setup.py +1 -1
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/LICENSE +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/client.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/dynamicModal.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/federated_dataset.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/flpipeline.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/model.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/params.yaml +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/params_optimiser.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/plot.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/server.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/strategy.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/LearningManager/utils.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/database_connector.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/dataset.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/flsetup.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/net_helper.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/net_manager_queries.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/network.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/NetManager/node.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/client.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/model.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/server.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/strategy.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/client.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/config.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/data.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/metrics.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/rw/xgboost/serialization.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/scripts/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/scripts/base.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/MEDfl/scripts/create_db.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/README.md +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/alembic/__init__.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/alembic/env.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/medfl.egg-info/dependency_links.txt +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/medfl.egg-info/requires.txt +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/medfl.egg-info/top_level.txt +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/setup.cfg +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/tests/test_dataset.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/tests/test_federated_dataset.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/tests/test_flpipeline.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/tests/test_flsetup.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/tests/test_network.py +0 -0
- {medfl-2.0.10.dev0 → medfl-2.0.10.dev2}/tests/test_node.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Federated XGBoost backend for MEDfl simulation mode.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import XGBoostSimulationClient
|
|
6
|
+
from .config import XGBoostConfig
|
|
7
|
+
from .server import XGBoostSimulationServer
|
|
8
|
+
from .strategy import XGBoostSimulationStrategy
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"XGBoostSimulationClient",
|
|
12
|
+
"XGBoostConfig",
|
|
13
|
+
"XGBoostSimulationServer",
|
|
14
|
+
"XGBoostSimulationStrategy",
|
|
15
|
+
]
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import flwr as fl
|
|
8
|
+
import xgboost as xgb
|
|
9
|
+
from flwr.common import (
|
|
10
|
+
Code,
|
|
11
|
+
EvaluateIns,
|
|
12
|
+
EvaluateRes,
|
|
13
|
+
FitIns,
|
|
14
|
+
FitRes,
|
|
15
|
+
GetParametersIns,
|
|
16
|
+
GetParametersRes,
|
|
17
|
+
GetPropertiesIns,
|
|
18
|
+
GetPropertiesRes,
|
|
19
|
+
Status,
|
|
20
|
+
)
|
|
21
|
+
from torch.utils.data import DataLoader
|
|
22
|
+
|
|
23
|
+
from .data import SimulationXGBoostDataModule
|
|
24
|
+
from .metrics import evaluate_predictions
|
|
25
|
+
from .serialization import (
|
|
26
|
+
booster_to_parameters,
|
|
27
|
+
parameters_to_booster,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_logger() -> logging.Logger:
|
|
32
|
+
logger = logging.getLogger(
|
|
33
|
+
"MEDfl.simulation.xgboost.client"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not logger.handlers:
|
|
37
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
38
|
+
handler.setFormatter(
|
|
39
|
+
logging.Formatter(
|
|
40
|
+
"[%(asctime)s][%(name)s][%(levelname)s] "
|
|
41
|
+
"%(message)s",
|
|
42
|
+
datefmt="%H:%M:%S",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
logger.addHandler(handler)
|
|
47
|
+
logger.setLevel(logging.INFO)
|
|
48
|
+
logger.propagate = False
|
|
49
|
+
|
|
50
|
+
return logger
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class XGBoostSimulationClient(fl.client.Client):
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
cid: str,
|
|
57
|
+
trainloader: DataLoader,
|
|
58
|
+
valloader: Optional[DataLoader] = None,
|
|
59
|
+
testloader: Optional[DataLoader] = None,
|
|
60
|
+
task: str = "binary",
|
|
61
|
+
xgb_params: Optional[Dict[str, Any]] = None,
|
|
62
|
+
local_num_boost_round: int = 10,
|
|
63
|
+
threshold: float = 0.5,
|
|
64
|
+
feature_names: Optional[List[str]] = None,
|
|
65
|
+
):
|
|
66
|
+
self.cid = str(cid)
|
|
67
|
+
self.task = task
|
|
68
|
+
self.xgb_params = xgb_params or {}
|
|
69
|
+
self.local_num_boost_round = int(
|
|
70
|
+
local_num_boost_round
|
|
71
|
+
)
|
|
72
|
+
self.threshold = float(threshold)
|
|
73
|
+
self.feature_names = feature_names
|
|
74
|
+
|
|
75
|
+
self.log = _get_logger()
|
|
76
|
+
self.booster: Optional[xgb.Booster] = None
|
|
77
|
+
|
|
78
|
+
data_module = SimulationXGBoostDataModule(
|
|
79
|
+
trainloader=trainloader,
|
|
80
|
+
valloader=valloader,
|
|
81
|
+
testloader=testloader,
|
|
82
|
+
feature_names=feature_names,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.dtrain = data_module.dtrain
|
|
86
|
+
self.dval = data_module.dval
|
|
87
|
+
self.dtest = data_module.dtest
|
|
88
|
+
self.metadata = data_module.metadata
|
|
89
|
+
|
|
90
|
+
self._validate()
|
|
91
|
+
|
|
92
|
+
self.log.info(
|
|
93
|
+
"[Client %s] initialized: "
|
|
94
|
+
"train=%s, val=%s, test=%s, features=%s",
|
|
95
|
+
self.cid,
|
|
96
|
+
self.metadata["num_train"],
|
|
97
|
+
self.metadata["num_val"],
|
|
98
|
+
self.metadata["num_test"],
|
|
99
|
+
self.metadata["num_features"],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _validate(self) -> None:
|
|
103
|
+
if self.task not in {
|
|
104
|
+
"binary",
|
|
105
|
+
"regression",
|
|
106
|
+
"multiclass",
|
|
107
|
+
}:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Unsupported task: {self.task}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if self.dtrain is None:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Client {self.cid} has no training data."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if self.local_num_boost_round <= 0:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"local_num_boost_round must be greater than zero."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def get_parameters(
|
|
123
|
+
self,
|
|
124
|
+
ins: GetParametersIns,
|
|
125
|
+
) -> GetParametersRes:
|
|
126
|
+
return GetParametersRes(
|
|
127
|
+
status=Status(
|
|
128
|
+
code=Code.OK,
|
|
129
|
+
message="Success",
|
|
130
|
+
),
|
|
131
|
+
parameters=booster_to_parameters(
|
|
132
|
+
self.booster
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def fit(self, ins: FitIns) -> FitRes:
|
|
137
|
+
self.log.info(
|
|
138
|
+
"[Client %s] starting local XGBoost fit",
|
|
139
|
+
self.cid,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
received_booster = parameters_to_booster(
|
|
143
|
+
ins.parameters
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if received_booster is not None:
|
|
147
|
+
self.booster = received_booster
|
|
148
|
+
|
|
149
|
+
evals = []
|
|
150
|
+
|
|
151
|
+
if self.dval is not None:
|
|
152
|
+
evals.append(
|
|
153
|
+
(self.dval, "validation")
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.booster = xgb.train(
|
|
157
|
+
params=self.xgb_params,
|
|
158
|
+
dtrain=self.dtrain,
|
|
159
|
+
num_boost_round=self.local_num_boost_round,
|
|
160
|
+
xgb_model=self.booster,
|
|
161
|
+
evals=evals,
|
|
162
|
+
verbose_eval=False,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
train_predictions = self.booster.predict(
|
|
166
|
+
self.dtrain
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
train_metrics = evaluate_predictions(
|
|
170
|
+
task=self.task,
|
|
171
|
+
y_true=self.dtrain.get_label(),
|
|
172
|
+
y_pred_or_prob=train_predictions,
|
|
173
|
+
threshold=self.threshold,
|
|
174
|
+
num_features=int(
|
|
175
|
+
self.metadata["num_features"]
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
metrics = {
|
|
180
|
+
"backend": "xgboost",
|
|
181
|
+
"task": self.task,
|
|
182
|
+
"cid": self.cid,
|
|
183
|
+
"num_train": int(
|
|
184
|
+
self.metadata["num_train"]
|
|
185
|
+
),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for key, value in train_metrics.items():
|
|
189
|
+
metrics[f"train_{key}"] = value
|
|
190
|
+
|
|
191
|
+
self.log.info(
|
|
192
|
+
"[Client %s] fit completed: %s",
|
|
193
|
+
self.cid,
|
|
194
|
+
metrics,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return FitRes(
|
|
198
|
+
status=Status(
|
|
199
|
+
code=Code.OK,
|
|
200
|
+
message="Success",
|
|
201
|
+
),
|
|
202
|
+
parameters=booster_to_parameters(
|
|
203
|
+
self.booster
|
|
204
|
+
),
|
|
205
|
+
num_examples=int(
|
|
206
|
+
self.metadata["num_train"]
|
|
207
|
+
),
|
|
208
|
+
metrics=metrics,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def evaluate(
|
|
212
|
+
self,
|
|
213
|
+
ins: EvaluateIns,
|
|
214
|
+
) -> EvaluateRes:
|
|
215
|
+
received_booster = parameters_to_booster(
|
|
216
|
+
ins.parameters
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if received_booster is not None:
|
|
220
|
+
self.booster = received_booster
|
|
221
|
+
|
|
222
|
+
evaluation_matrix = (
|
|
223
|
+
self.dtest
|
|
224
|
+
if self.dtest is not None
|
|
225
|
+
else self.dval
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
self.booster is None
|
|
230
|
+
or evaluation_matrix is None
|
|
231
|
+
):
|
|
232
|
+
return EvaluateRes(
|
|
233
|
+
status=Status(
|
|
234
|
+
code=Code.EVALUATE_NOT_IMPLEMENTED,
|
|
235
|
+
message=(
|
|
236
|
+
"No booster or evaluation data."
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
loss=0.0,
|
|
240
|
+
num_examples=0,
|
|
241
|
+
metrics={},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
predictions = self.booster.predict(
|
|
245
|
+
evaluation_matrix
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
eval_metrics = evaluate_predictions(
|
|
249
|
+
task=self.task,
|
|
250
|
+
y_true=evaluation_matrix.get_label(),
|
|
251
|
+
y_pred_or_prob=predictions,
|
|
252
|
+
threshold=self.threshold,
|
|
253
|
+
num_features=int(
|
|
254
|
+
self.metadata["num_features"]
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
metrics = {
|
|
259
|
+
"backend": "xgboost",
|
|
260
|
+
"task": self.task,
|
|
261
|
+
"cid": self.cid,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for key, value in eval_metrics.items():
|
|
265
|
+
metrics[f"eval_{key}"] = value
|
|
266
|
+
|
|
267
|
+
if self.task == "binary":
|
|
268
|
+
loss = float(
|
|
269
|
+
eval_metrics.get("logloss", 0.0)
|
|
270
|
+
)
|
|
271
|
+
elif self.task == "regression":
|
|
272
|
+
loss = float(
|
|
273
|
+
eval_metrics.get("rmse", 0.0)
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
loss = float(
|
|
277
|
+
eval_metrics.get("mlogloss", 0.0)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self.log.info(
|
|
281
|
+
"[Client %s] evaluation completed: "
|
|
282
|
+
"loss=%s metrics=%s",
|
|
283
|
+
self.cid,
|
|
284
|
+
loss,
|
|
285
|
+
metrics,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return EvaluateRes(
|
|
289
|
+
status=Status(
|
|
290
|
+
code=Code.OK,
|
|
291
|
+
message="Success",
|
|
292
|
+
),
|
|
293
|
+
loss=loss,
|
|
294
|
+
num_examples=int(
|
|
295
|
+
evaluation_matrix.num_row()
|
|
296
|
+
),
|
|
297
|
+
metrics=metrics,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def get_properties(
|
|
301
|
+
self,
|
|
302
|
+
ins: GetPropertiesIns,
|
|
303
|
+
) -> GetPropertiesRes:
|
|
304
|
+
properties = {
|
|
305
|
+
"backend": "xgboost",
|
|
306
|
+
"cid": self.cid,
|
|
307
|
+
"task": self.task,
|
|
308
|
+
"num_train": int(
|
|
309
|
+
self.metadata["num_train"]
|
|
310
|
+
),
|
|
311
|
+
"num_val": int(
|
|
312
|
+
self.metadata["num_val"]
|
|
313
|
+
),
|
|
314
|
+
"num_test": int(
|
|
315
|
+
self.metadata["num_test"]
|
|
316
|
+
),
|
|
317
|
+
"num_features": int(
|
|
318
|
+
self.metadata["num_features"]
|
|
319
|
+
),
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return GetPropertiesRes(
|
|
323
|
+
status=Status(
|
|
324
|
+
code=Code.OK,
|
|
325
|
+
message="Success",
|
|
326
|
+
),
|
|
327
|
+
properties=properties,
|
|
328
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class XGBoostConfig:
|
|
7
|
+
task: str = "binary"
|
|
8
|
+
mode: str = "bagging"
|
|
9
|
+
local_num_boost_round: int = 10
|
|
10
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
11
|
+
num_classes: Optional[int] = None
|
|
12
|
+
|
|
13
|
+
def build_params(self) -> Dict[str, Any]:
|
|
14
|
+
if self.task == "binary":
|
|
15
|
+
defaults = {
|
|
16
|
+
"objective": "binary:logistic",
|
|
17
|
+
"eval_metric": "auc",
|
|
18
|
+
"tree_method": "hist",
|
|
19
|
+
"max_depth": 6,
|
|
20
|
+
"eta": 0.1,
|
|
21
|
+
"subsample": 1.0,
|
|
22
|
+
"colsample_bytree": 1.0,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
elif self.task == "regression":
|
|
26
|
+
defaults = {
|
|
27
|
+
"objective": "reg:squarederror",
|
|
28
|
+
"eval_metric": "rmse",
|
|
29
|
+
"tree_method": "hist",
|
|
30
|
+
"max_depth": 6,
|
|
31
|
+
"eta": 0.1,
|
|
32
|
+
"subsample": 1.0,
|
|
33
|
+
"colsample_bytree": 1.0,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
elif self.task == "multiclass":
|
|
37
|
+
if self.num_classes is None or self.num_classes < 2:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"num_classes must be provided and >= 2 "
|
|
40
|
+
"for multiclass XGBoost."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
defaults = {
|
|
44
|
+
"objective": "multi:softprob",
|
|
45
|
+
"num_class": self.num_classes,
|
|
46
|
+
"eval_metric": "mlogloss",
|
|
47
|
+
"tree_method": "hist",
|
|
48
|
+
"max_depth": 6,
|
|
49
|
+
"eta": 0.1,
|
|
50
|
+
"subsample": 1.0,
|
|
51
|
+
"colsample_bytree": 1.0,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Unsupported XGBoost task: {self.task}. "
|
|
57
|
+
"Expected binary, regression, or multiclass."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
merged = defaults.copy()
|
|
61
|
+
merged.update(self.params or {})
|
|
62
|
+
|
|
63
|
+
return merged
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
import xgboost as xgb
|
|
6
|
+
from torch.utils.data import DataLoader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def dataloader_to_numpy(
|
|
10
|
+
loader: DataLoader,
|
|
11
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
12
|
+
if loader is None:
|
|
13
|
+
raise ValueError("DataLoader cannot be None.")
|
|
14
|
+
|
|
15
|
+
x_batches = []
|
|
16
|
+
y_batches = []
|
|
17
|
+
|
|
18
|
+
for x_batch, y_batch in loader:
|
|
19
|
+
if isinstance(x_batch, torch.Tensor):
|
|
20
|
+
x_batch = x_batch.detach().cpu().numpy()
|
|
21
|
+
|
|
22
|
+
if isinstance(y_batch, torch.Tensor):
|
|
23
|
+
y_batch = y_batch.detach().cpu().numpy()
|
|
24
|
+
|
|
25
|
+
x_batch = np.asarray(x_batch)
|
|
26
|
+
y_batch = np.asarray(y_batch).reshape(-1)
|
|
27
|
+
|
|
28
|
+
x_batches.append(x_batch)
|
|
29
|
+
y_batches.append(y_batch)
|
|
30
|
+
|
|
31
|
+
if not x_batches:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Cannot convert an empty DataLoader to DMatrix."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
x = np.concatenate(x_batches, axis=0)
|
|
37
|
+
y = np.concatenate(y_batches, axis=0)
|
|
38
|
+
|
|
39
|
+
if x.ndim != 2:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"XGBoost tabular simulation expects a two-dimensional "
|
|
42
|
+
f"feature matrix. Received shape={x.shape}."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return x, y
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def dataloader_to_dmatrix(
|
|
49
|
+
loader: Optional[DataLoader],
|
|
50
|
+
feature_names: Optional[List[str]] = None,
|
|
51
|
+
) -> Optional[xgb.DMatrix]:
|
|
52
|
+
if loader is None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
if len(loader.dataset) == 0:
|
|
57
|
+
return None
|
|
58
|
+
except TypeError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
x, y = dataloader_to_numpy(loader)
|
|
62
|
+
|
|
63
|
+
if feature_names is not None:
|
|
64
|
+
if len(feature_names) != x.shape[1]:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"feature_names length does not match the number "
|
|
67
|
+
f"of features: {len(feature_names)} != {x.shape[1]}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return xgb.DMatrix(
|
|
71
|
+
data=x,
|
|
72
|
+
label=y,
|
|
73
|
+
feature_names=feature_names,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SimulationXGBoostDataModule:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
trainloader: DataLoader,
|
|
81
|
+
valloader: Optional[DataLoader] = None,
|
|
82
|
+
testloader: Optional[DataLoader] = None,
|
|
83
|
+
feature_names: Optional[List[str]] = None,
|
|
84
|
+
):
|
|
85
|
+
self.dtrain = dataloader_to_dmatrix(
|
|
86
|
+
trainloader,
|
|
87
|
+
feature_names,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self.dval = dataloader_to_dmatrix(
|
|
91
|
+
valloader,
|
|
92
|
+
feature_names,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.dtest = dataloader_to_dmatrix(
|
|
96
|
+
testloader,
|
|
97
|
+
feature_names,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self.metadata = {
|
|
101
|
+
"num_train": (
|
|
102
|
+
self.dtrain.num_row()
|
|
103
|
+
if self.dtrain is not None
|
|
104
|
+
else 0
|
|
105
|
+
),
|
|
106
|
+
"num_val": (
|
|
107
|
+
self.dval.num_row()
|
|
108
|
+
if self.dval is not None
|
|
109
|
+
else 0
|
|
110
|
+
),
|
|
111
|
+
"num_test": (
|
|
112
|
+
self.dtest.num_row()
|
|
113
|
+
if self.dtest is not None
|
|
114
|
+
else 0
|
|
115
|
+
),
|
|
116
|
+
"num_features": (
|
|
117
|
+
self.dtrain.num_col()
|
|
118
|
+
if self.dtrain is not None
|
|
119
|
+
else 0
|
|
120
|
+
),
|
|
121
|
+
"feature_names": feature_names or [],
|
|
122
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.metrics import (
|
|
6
|
+
accuracy_score,
|
|
7
|
+
f1_score,
|
|
8
|
+
log_loss,
|
|
9
|
+
mean_absolute_error,
|
|
10
|
+
mean_squared_error,
|
|
11
|
+
r2_score,
|
|
12
|
+
roc_auc_score,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _safe_float(value: Any) -> float:
|
|
17
|
+
try:
|
|
18
|
+
value = float(value)
|
|
19
|
+
|
|
20
|
+
if math.isnan(value) or math.isinf(value):
|
|
21
|
+
return float("nan")
|
|
22
|
+
|
|
23
|
+
return value
|
|
24
|
+
except Exception:
|
|
25
|
+
return float("nan")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def binary_metrics(
|
|
29
|
+
y_true,
|
|
30
|
+
y_prob,
|
|
31
|
+
threshold: float = 0.5,
|
|
32
|
+
) -> Dict[str, float]:
|
|
33
|
+
y_true = np.asarray(y_true)
|
|
34
|
+
y_prob = np.asarray(y_prob)
|
|
35
|
+
y_pred = (y_prob >= threshold).astype(int)
|
|
36
|
+
|
|
37
|
+
metrics = {
|
|
38
|
+
"accuracy": _safe_float(
|
|
39
|
+
accuracy_score(y_true, y_pred)
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
metrics["auc"] = _safe_float(
|
|
45
|
+
roc_auc_score(y_true, y_prob)
|
|
46
|
+
)
|
|
47
|
+
except Exception:
|
|
48
|
+
metrics["auc"] = float("nan")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
metrics["logloss"] = _safe_float(
|
|
52
|
+
log_loss(y_true, y_prob)
|
|
53
|
+
)
|
|
54
|
+
except Exception:
|
|
55
|
+
metrics["logloss"] = float("nan")
|
|
56
|
+
|
|
57
|
+
return metrics
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def regression_metrics(
|
|
61
|
+
y_true,
|
|
62
|
+
y_pred,
|
|
63
|
+
num_features: int = 0,
|
|
64
|
+
) -> Dict[str, float]:
|
|
65
|
+
y_true = np.asarray(y_true)
|
|
66
|
+
y_pred = np.asarray(y_pred)
|
|
67
|
+
|
|
68
|
+
rmse = mean_squared_error(
|
|
69
|
+
y_true,
|
|
70
|
+
y_pred,
|
|
71
|
+
squared=False,
|
|
72
|
+
)
|
|
73
|
+
mae = mean_absolute_error(y_true, y_pred)
|
|
74
|
+
r2 = r2_score(y_true, y_pred)
|
|
75
|
+
|
|
76
|
+
n = len(y_true)
|
|
77
|
+
p = int(num_features)
|
|
78
|
+
|
|
79
|
+
if n > p + 1:
|
|
80
|
+
adjusted_r2 = (
|
|
81
|
+
1.0
|
|
82
|
+
- (1.0 - r2)
|
|
83
|
+
* ((n - 1.0) / (n - p - 1.0))
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
adjusted_r2 = float("nan")
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"rmse": _safe_float(rmse),
|
|
90
|
+
"mae": _safe_float(mae),
|
|
91
|
+
"r2": _safe_float(r2),
|
|
92
|
+
"adjusted_r2": _safe_float(adjusted_r2),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def multiclass_metrics(
|
|
97
|
+
y_true,
|
|
98
|
+
y_prob,
|
|
99
|
+
) -> Dict[str, float]:
|
|
100
|
+
y_true = np.asarray(y_true)
|
|
101
|
+
y_prob = np.asarray(y_prob)
|
|
102
|
+
y_pred = np.argmax(y_prob, axis=1)
|
|
103
|
+
|
|
104
|
+
metrics = {
|
|
105
|
+
"accuracy": _safe_float(
|
|
106
|
+
accuracy_score(y_true, y_pred)
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
metrics["macro_f1"] = _safe_float(
|
|
112
|
+
f1_score(
|
|
113
|
+
y_true,
|
|
114
|
+
y_pred,
|
|
115
|
+
average="macro",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
except Exception:
|
|
119
|
+
metrics["macro_f1"] = float("nan")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
metrics["mlogloss"] = _safe_float(
|
|
123
|
+
log_loss(y_true, y_prob)
|
|
124
|
+
)
|
|
125
|
+
except Exception:
|
|
126
|
+
metrics["mlogloss"] = float("nan")
|
|
127
|
+
|
|
128
|
+
return metrics
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def evaluate_predictions(
|
|
132
|
+
task: str,
|
|
133
|
+
y_true,
|
|
134
|
+
y_pred_or_prob,
|
|
135
|
+
threshold: float = 0.5,
|
|
136
|
+
num_features: int = 0,
|
|
137
|
+
) -> Dict[str, float]:
|
|
138
|
+
if task == "binary":
|
|
139
|
+
return binary_metrics(
|
|
140
|
+
y_true,
|
|
141
|
+
y_pred_or_prob,
|
|
142
|
+
threshold=threshold,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if task == "regression":
|
|
146
|
+
return regression_metrics(
|
|
147
|
+
y_true,
|
|
148
|
+
y_pred_or_prob,
|
|
149
|
+
num_features=num_features,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if task == "multiclass":
|
|
153
|
+
return multiclass_metrics(
|
|
154
|
+
y_true,
|
|
155
|
+
y_pred_or_prob,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
raise ValueError(f"Unsupported task: {task}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def aggregate_weighted_metrics(results):
|
|
162
|
+
aggregated = {}
|
|
163
|
+
|
|
164
|
+
keys = set()
|
|
165
|
+
for _, metrics in results:
|
|
166
|
+
keys.update(metrics.keys())
|
|
167
|
+
|
|
168
|
+
for key in keys:
|
|
169
|
+
weighted_sum = 0.0
|
|
170
|
+
contributing_examples = 0
|
|
171
|
+
|
|
172
|
+
for num_examples, metrics in results:
|
|
173
|
+
if key not in metrics:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
value = float(metrics[key])
|
|
178
|
+
except Exception:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
if math.isnan(value) or math.isinf(value):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
weighted_sum += num_examples * value
|
|
185
|
+
contributing_examples += num_examples
|
|
186
|
+
|
|
187
|
+
if contributing_examples > 0:
|
|
188
|
+
aggregated[key] = (
|
|
189
|
+
weighted_sum / contributing_examples
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return aggregated
|