A lightweight, fully reproducible benchmark for studying adversarial robustness of Federated Learning (FL) systems.
View on GitHubThis project began as an analysis of an existing open-source repository, Adversarial_Robustness_of_FL_systems, which explores the same general research question — the robustness of Federated Learning to data- and model-poisoning attacks — using TensorFlow-Federated and the EMNIST dataset.
That project was used only as conceptual inspiration; no code, wording, structure, or identifiers were copied. Where the original project had a real, working idea, FedRobustBench keeps the idea and rebuilds the engineering from scratch, addressing several concrete weaknesses identified during review:
| Original project | Observed limitation | FedRobustBench's approach |
|---|---|---|
| Pinned to `tensorflow-federated==0.20.0`, an old, network-heavy, Colab-only dependency | Hard to install locally, not reproducible offline, version-locked to an unsupported release | Pure-NumPy FL core with zero heavyweight FL-framework dependency; runs anywhere Python + NumPy runs |
| Uses EMNIST via `tff.simulation.datasets`, requiring a large network download on every fresh environment | Experiments cannot be reproduced offline or in restricted/CI environments | Uses scikit-learn's bundled `digits` dataset (real 8×8 handwritten-digit images, no download) |
| Several notebooks contain incomplete/broken code (e.g. an `AbstractAttacker.get_attacked_clients` method that returns `self.` — a syntax error; `Dataset.partition_dataset` references `self.x_train`, which is never set; unresolved imports like `ABC`, `Set`) | Code does not run as-is; no tests; no CI | Every module is executable, covered by unit tests (20/20 passing, including a numerical gradient check on the backprop implementation), and runnable end-to-end from a single script |
| Only one aggregation rule (implicit FedAvg via TFF) is available; "defense" is not actually implemented anywhere in the notebooks | No way to answer the project's own research question ("what are the weaknesses, and what can be recommended?") because there is nothing to compare FedAvg against | Five aggregation rules implemented and empirically compared: FedAvg, coordinate-wise median, trimmed mean, Krum, Multi-Krum |
| No non-IID data partitioning logic is wired up (client sharding is a simple `sorted(client_ids)[:NUM_CLIENTS]` slice) | Real FL deployments are non-IID; ignoring this makes robustness results unrepresentative | Dirichlet label-skew partitioning (Hsu et al., 2019) with a tunable heterogeneity parameter `alpha` |
| No results are saved in a structured, comparable way; no plots; no metrics beyond a raw `.npy` dump | Findings are not interpretable or presentable | CSV summary/history tables, matplotlib figures, and macro precision/recall/F1 in addition to accuracy |
| No tests | No confidence that the pipeline (or an attack, or an aggregator) is implemented correctly | 20 unit tests, including a numerical gradient check, correctness properties for each aggregator (e.g. "median must stay close to the honest mean under outliers"), and attack-specific invariants |
The result is a small but genuinely research-grade artifact: every experiment in results/ was produced by actually running this code, end to end, with no network access — not hand-written or copied from elsewhere.
Under a static, cross-device federated-learning threat model with non-IID client data, how much do data-poisoning and Byzantine model-poisoning attacks degrade a FedAvg-trained global model, and to what extent do Byzantine-robust aggregation rules (median, trimmed mean, Krum, Multi-Krum) restore accuracy as the fraction of malicious clients grows?
See docs/THREAT_MODEL.md for the full specification. In short: a static adversary controls a fraction f ∈ {0, 0.1, 0.2, 0.3} of clients and either (a) poisons their local training data, or (b) submits an arbitrary malicious parameter update directly (Byzantine setting). The defender only ever observes client updates and chooses the aggregation rule.
src/fedrobustbench/attacks/data_attacks.py
label_flip — every local label is randomly reassigned to a different class.targeted_label_flip — only one source class is relabeled to a target class (stealthier).feature_noise — Gaussian noise injected into pixel features.sample_deletion — a large fraction of local samples is dropped (free-rider / data-scarcity).backdoor_trigger — a fixed bright-pixel patch is stamped into a subset of images, relabeled to a target class (BadNets-style, Gu et al., 2017).src/fedrobustbench/attacks/model_attacks.py
sign_flip — negates and amplifies the honest update (Fang et al., 2020).gaussian — replaces the update with scaled Gaussian noise.zero_update — a free-rider submits an all-zero update.constant_scaling — amplifies an otherwise-honest update to dominate the aggregate mean.src/fedrobustbench/aggregation.py: fedavg, median (Yin et al., 2018), trimmed_mean (Yin et al., 2018), krum, multi_krum (Blanchard et al., 2017).
All numbers below are taken verbatim from results/tables/summary.csv, produced by experiments/run_experiment.py (30 simulated clients, Dirichlet alpha=0.5, 25 rounds, 50% client participation per round, seed=42). No numbers in this section are hand-typed or estimated — re-running the script reproduces them exactly (fixed seed, no network access required).
| Malicious fraction | FedAvg accuracy | Median accuracy | Trimmed-mean accuracy |
|---|---|---|---|
| 0.0 (baseline) | 0.9472 | – | – |
| 0.1 | 0.9500 | 0.9306 | 0.9389 |
| 0.2 | 0.9472 | 0.9361 | 0.9444 |
| 0.3 | 0.9500 | 0.9472 | 0.9389 |
At the scale tested, plain label-flip data poisoning turns out to be a weak attack against sample-size-weighted FedAvg: because each malicious client's locally trained update still partially fits genuine gradient structure (the poisoned labels are still internally consistent for that client's SGD), and updates are weighted-averaged across many clients, FedAvg does not collapse even at f=0.3. Interestingly, the robust aggregators (median, trimmed mean) show slightly lower accuracy than FedAvg here — they are not "wrong," but discarding/down-weighting legitimate non-IID client diversity has a small cost when there is no strong attack to defend against. This is a genuine, useful finding: robust aggregation is not free, and its benefit is attack-dependent.
| Malicious fraction | FedAvg accuracy | Krum accuracy | Multi-Krum accuracy |
|---|---|---|---|
| 0.0 (baseline) | 0.9472 | – | – |
| 0.1 | 0.2944 | 0.8333 | 0.9528 |
| 0.2 | 0.1000 | 0.8333 | 0.9556 |
| 0.3 | 0.1222 | 0.8250 | 0.9500 |
Here the contrast is stark: an amplified sign-flip attack collapses FedAvg to near-random accuracy (~10-30%, vs. a 10-class random baseline of 10%) with as few as 10% malicious clients, because a handful of large-magnitude, oppositely-signed updates dominate the weighted mean. Both Krum-family defenses recover most of the accuracy; Multi-Krum in particular is essentially unaffected up to f=0.3 (0.95 vs. 0.947 baseline), because it explicitly selects updates by pairwise proximity to their neighbours and discards statistical outliers before averaging.
These are exploratory results on one dataset/model/seed and should not be over-generalized — see §10 (Limitations).
get_params/set_params/loss_and_grads interface used by src/fedrobustbench/model.py.digits), train/test split, and precision/recall/F1 metrics.results/tables/*.csv.unittest, pytest optional).No GPU is required; a full 19-scenario experiment sweep (25 rounds each) runs in under 30 seconds on a single CPU core.
git clone https://github.com/clementbouni-arch/fedrobustbench.git
cd fedrobustbench
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Tested on Linux and macOS; Windows works via the standard venv workflow above. No CUDA/GPU setup is needed.
Run the full experiment sweep (19 scenarios: baseline, label-flip × {FedAvg, median, trimmed mean} × 3 malicious fractions, sign-flip × {FedAvg, Krum, Multi-Krum} × 3 malicious fractions):
python experiments/run_experiment.py
This writes:
results/tables/summary.csv — final-round metrics per scenarioresults/tables/history.csv — per-round metrics per scenarioresults/figures/*.png — the plots shown in §6To experiment with a single custom scenario in Python:
from fedrobustbench.data import load_dataset, partition_dirichlet
from fedrobustbench.server import run_federated_training, TrainConfig, AttackConfig
(x_train, y_train), (x_test, y_test) = load_dataset(seed=42)
clients = partition_dirichlet(x_train, y_train, num_clients=30, alpha=0.5, seed=42)
history, model, malicious = run_federated_training(
clients, x_test, y_test, num_classes=10,
aggregator="multi_krum",
train_cfg=TrainConfig(num_rounds=25, seed=42),
attack_cfg=AttackConfig(model_attack="sign_flip", malicious_fraction=0.3),
)
print(history[-1].metrics)
python -m unittest discover -s tests -v
# or, if pytest is installed:
pytest tests/ -v
20 tests cover: a numerical gradient check on the MLP backward pass, aggregator correctness/robustness properties (e.g. FedAvg is provably dragged by outliers while median/trimmed-mean/Krum/Multi-Krum are not, on a synthetic outlier fixture), and attack-specific invariants (e.g. targeted_label_flip only touches the intended source class; sign_flip output has negative dot-product with the honest update).
fedrobustbench/
├── README.md
├── LICENSE
├── requirements.txt
├── .gitignore
├── docs/
│ └── THREAT_MODEL.md
├── src/fedrobustbench/
│ ├── __init__.py
│ ├── data.py # dataset loading + Dirichlet non-IID partitioning
│ ├── model.py # NumPy MLP: forward/backward, param (de)serialization
│ ├── aggregation.py # FedAvg, median, trimmed mean, Krum, Multi-Krum
│ ├── metrics.py # accuracy/precision/recall/F1, ASR, degradation
│ ├── server.py # federated training orchestration loop
│ └── attacks/
│ ├── __init__.py
│ ├── data_attacks.py # label flip, targeted flip, noise, deletion, backdoor
│ └── model_attacks.py # sign flip, gaussian, zero-update, constant scaling
├── experiments/
│ └── run_experiment.py # end-to-end experiment sweep, tables + figures
├── tests/
│ ├── test_model.py
│ ├── test_aggregation.py
│ └── test_attacks.py
└── results/
├── tables/{summary,history}.csv
└── figures/*.png
digits, a small, comparatively easy 10-class task), one model (a single-hidden-layer MLP), one seed set, and one client-count/participation regime; they illustrate the mechanics of these attacks/defenses well but should not be read as absolute robustness guarantees for other settings.Krum/Multi-Krum are evaluated with the true number of Byzantine clients known to the defender (a favorable assumption for the defense); robustness to a misestimated bound is not yet measured.docs/THREAT_MODEL.md).attack_success_rate metric are implemented and unit-tested but not yet included in the main experiment sweep in experiments/run_experiment.py.sklearn's fetch_openml('mnist_784') when network access is available) and a convolutional model to test scale sensitivity.Conceptual origin: Adversarial_Robustness_of_FL_systems (used only as a starting point for the research direction; no code or text reused — see §1).
MIT — see LICENSE.