diff --git a/examples/quickstart-monai/.gitignore b/examples/quickstart-monai/.gitignore index a218cab9669..2626387e2a4 100644 --- a/examples/quickstart-monai/.gitignore +++ b/examples/quickstart-monai/.gitignore @@ -1 +1,2 @@ MedNIST* +.data_download.lock diff --git a/examples/quickstart-monai/README.md b/examples/quickstart-monai/README.md index dc31f03e4b1..c470a6a6c86 100644 --- a/examples/quickstart-monai/README.md +++ b/examples/quickstart-monai/README.md @@ -4,88 +4,76 @@ dataset: [MedNIST] framework: [MONAI] --- -# Flower Example using MONAI +# Federated Learning with MONAI and Flower (Quickstart Example) This introductory example to Flower uses MONAI, but deep knowledge of MONAI is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. -Running this example in itself is quite easy. +Running this example in itself is quite easy. [MONAI](https://docs.monai.io/en/latest/index.html)(Medical Open Network for AI) is a PyTorch-based, open-source framework for deep learning in healthcare imaging, part of the PyTorch Ecosystem. This example uses a subset of the [MedMNIST](https://medmnist.com/) dataset including 6 classes, as done in [MONAI's classification demo](https://colab.research.google.com/drive/1wy8XUSnNWlhDNazFdvGBHLfdkGvOHBKe). Each client trains am [DenseNet121](https://docs.monai.io/en/stable/networks.html#densenet121) from MONAI. -[MONAI](https://docs.monai.io/en/latest/index.html)(Medical Open Network for AI) is a PyTorch-based, open-source framework for deep learning in healthcare imaging, part of the PyTorch Ecosystem. +> \[!NOTE\] +> This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to partition the MedMNIST dataset. Its a good example to show how to bring any dataset into Flower and partition it using any of the built-in [partitioners](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.html) (e.g. `DirichletPartitioner`, `PathologicalPartitioner`). Learn [how to use partitioners](https://flower.ai/docs/datasets/tutorial-use-partitioners.html) in a step-by-step tutorial. -Its ambitions are: +## Set up the project -- developing a community of academic, industrial and clinical researchers collaborating on a common foundation; +### Clone the project -- creating state-of-the-art, end-to-end training workflows for healthcare imaging; - -- providing researchers with an optimized and standardized way to create and evaluate deep learning models. - -## Project Setup - -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +Start by cloning the example project: ```shell -git clone --depth=1 https://github.com/adap/flower.git _tmp && mv _tmp/examples/quickstart-monai . && rm -rf _tmp && cd quickstart-monai +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-monai . \ + && rm -rf _tmp \ + && cd quickstart-monai ``` -This will create a new directory called `quickstart-monai` containing the following files: +This will create a new directory called `quickstart-monai` with the following structure: ```shell --- pyproject.toml --- requirements.txt --- client.py --- data.py --- model.py --- server.py --- README.md +quickstart-monai +├── monaiexample +│ ├── __init__.py +│ ├── client_app.py # Defines your ClientApp +│ ├── server_app.py # Defines your ServerApp +│ └── task.py # Defines your model, training and data loading +├── pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing Dependencies - -Project dependencies (such as `monai` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +### Install dependencies and project -#### Poetry +Install the dependencies defined in `pyproject.toml` as well as the `monaiexample` package. -```shell -poetry install -poetry shell +```bash +pip install -e . ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: - -```shell -poetry run python3 -c "import flwr" -``` +## Run the project -If you don't see any errors you're good to go! +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -#### pip +### Run with the Simulation Engine -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. +> \[!TIP\] +> This example runs faster when the `ClientApp`s have access to a GPU. If your system has one, you can make use of it by configuring the `backend.client-resources` component in `pyproject.toml`. If you want to try running the example with GPU right away, use the `local-simulation-gpu` federation as shown below. -```shell -pip install -r requirements.txt +```bash +# Run with the default federation (CPU only) +flwr run . ``` -## Run Federated Learning with MONAI and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +Run the project in the `local-simulation-gpu` federation that gives CPU and GPU resources to each `ClientApp`. By default, at most 4x`ClientApp` will run in parallel in the available GPU. -```shell -python3 server.py +```bash +# Run with the `local-simulation-gpu` federation +flwr run . local-simulation-gpu ``` -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminal windows and run the following commands. Clients will train a [DenseNet121](https://docs.monai.io/en/stable/networks.html#densenet121) from MONAI. If a GPU is present in your system, clients will use it. - -Start client 1 in the first terminal: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python3 client.py --partition-id 0 +```bash +flwr run . --run-config num-server-rounds=5,batch-size=32 ``` -Start client 2 in the second terminal: - -```shell -python3 client.py --partition-id 1 -``` +### Run with the Deployment Engine -You will see that the federated training is starting. Look at the [code](https://github.com/adap/flower/tree/main/examples/quickstart-monai) for a detailed explanation. +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-monai/client.py b/examples/quickstart-monai/client.py deleted file mode 100644 index 1401928af1f..00000000000 --- a/examples/quickstart-monai/client.py +++ /dev/null @@ -1,61 +0,0 @@ -import argparse -import warnings -from collections import OrderedDict - -import flwr as fl -import torch -from monai.networks.nets.densenet import DenseNet121 - -from data import load_data -from model import test, train - -warnings.filterwarnings("ignore", category=UserWarning) -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - -# Define Flower client -class FlowerClient(fl.client.NumPyClient): - def __init__(self, net, trainloader, testloader, device): - self.net = net - self.trainloader = trainloader - self.testloader = testloader - self.device = device - - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in self.net.state_dict().items()] - - def set_parameters(self, parameters): - params_dict = zip(self.net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - self.net.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - self.set_parameters(parameters) - train(self.net, self.trainloader, epoch_num=1, device=self.device) - return self.get_parameters(config={}), len(self.trainloader), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss, accuracy = test(self.net, self.testloader, self.device) - return loss, len(self.testloader), {"accuracy": accuracy} - - -if __name__ == "__main__": - total_partitions = 10 - parser = argparse.ArgumentParser() - parser.add_argument( - "--partition-id", type=int, choices=range(total_partitions), required=True - ) - args = parser.parse_args() - - # Load model and data (simple CNN, CIFAR-10) - trainloader, _, testloader, num_class = load_data( - total_partitions, args.partition_id - ) - net = DenseNet121(spatial_dims=2, in_channels=1, out_channels=num_class).to(DEVICE) - - # Start Flower client - fl.client.start_numpy_client( - server_address="127.0.0.1:8080", - client=FlowerClient(net, trainloader, testloader, DEVICE), - ) diff --git a/examples/quickstart-monai/data.py b/examples/quickstart-monai/data.py deleted file mode 100644 index d184476522e..00000000000 --- a/examples/quickstart-monai/data.py +++ /dev/null @@ -1,158 +0,0 @@ -import os -import tarfile -from urllib import request - -import numpy as np -from monai.data import DataLoader, Dataset -from monai.transforms import ( - Compose, - EnsureChannelFirst, - LoadImage, - RandFlip, - RandRotate, - RandZoom, - ScaleIntensity, - ToTensor, -) - - -def _partition(files_list, labels_list, num_shards, index): - total_size = len(files_list) - assert total_size == len( - labels_list - ), f"List of datapoints and labels must be of the same length" - shard_size = total_size // num_shards - - # Calculate start and end indices for the shard - start_idx = index * shard_size - if index == num_shards - 1: - # Last shard takes the remainder - end_idx = total_size - else: - end_idx = start_idx + shard_size - - # Create a subset for the shard - files = files_list[start_idx:end_idx] - labels = labels_list[start_idx:end_idx] - return files, labels - - -def load_data(num_shards, index): - image_file_list, image_label_list, _, num_class = _download_data() - - # Get partition given index - files_list, labels_list = _partition( - image_file_list, image_label_list, num_shards, index - ) - - trainX, trainY, valX, valY, testX, testY = _split_data( - files_list, labels_list, len(files_list) - ) - train_transforms, val_transforms = _get_transforms() - - train_ds = MedNISTDataset(trainX, trainY, train_transforms) - train_loader = DataLoader(train_ds, batch_size=300, shuffle=True) - - val_ds = MedNISTDataset(valX, valY, val_transforms) - val_loader = DataLoader(val_ds, batch_size=300) - - test_ds = MedNISTDataset(testX, testY, val_transforms) - test_loader = DataLoader(test_ds, batch_size=300) - - return train_loader, val_loader, test_loader, num_class - - -class MedNISTDataset(Dataset): - def __init__(self, image_files, labels, transforms): - self.image_files = image_files - self.labels = labels - self.transforms = transforms - - def __len__(self): - return len(self.image_files) - - def __getitem__(self, index): - return self.transforms(self.image_files[index]), self.labels[index] - - -def _download_data(): - data_dir = "./MedNIST/" - _download_and_extract( - "https://dl.dropboxusercontent.com/s/5wwskxctvcxiuea/MedNIST.tar.gz", - os.path.join(data_dir), - ) - - class_names = sorted( - [x for x in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, x))] - ) - num_class = len(class_names) - image_files = [ - [ - os.path.join(data_dir, class_name, x) - for x in os.listdir(os.path.join(data_dir, class_name)) - ] - for class_name in class_names - ] - image_file_list = [] - image_label_list = [] - for i, class_name in enumerate(class_names): - image_file_list.extend(image_files[i]) - image_label_list.extend([i] * len(image_files[i])) - num_total = len(image_label_list) - return image_file_list, image_label_list, num_total, num_class - - -def _split_data(image_file_list, image_label_list, num_total): - valid_frac, test_frac = 0.1, 0.1 - trainX, trainY = [], [] - valX, valY = [], [] - testX, testY = [], [] - - for i in range(num_total): - rann = np.random.random() - if rann < valid_frac: - valX.append(image_file_list[i]) - valY.append(image_label_list[i]) - elif rann < test_frac + valid_frac: - testX.append(image_file_list[i]) - testY.append(image_label_list[i]) - else: - trainX.append(image_file_list[i]) - trainY.append(image_label_list[i]) - - return trainX, trainY, valX, valY, testX, testY - - -def _get_transforms(): - train_transforms = Compose( - [ - LoadImage(image_only=True), - EnsureChannelFirst(), - ScaleIntensity(), - RandRotate(range_x=15, prob=0.5, keep_size=True), - RandFlip(spatial_axis=0, prob=0.5), - RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5, keep_size=True), - ToTensor(), - ] - ) - - val_transforms = Compose( - [LoadImage(image_only=True), EnsureChannelFirst(), ScaleIntensity(), ToTensor()] - ) - - return train_transforms, val_transforms - - -def _download_and_extract(url, dest_folder): - if not os.path.isdir(dest_folder): - # Download the tar.gz file - tar_gz_filename = url.split("/")[-1] - if not os.path.isfile(tar_gz_filename): - with request.urlopen(url) as response, open( - tar_gz_filename, "wb" - ) as out_file: - out_file.write(response.read()) - - # Extract the tar.gz file - with tarfile.open(tar_gz_filename, "r:gz") as tar_ref: - tar_ref.extractall() diff --git a/examples/quickstart-monai/model.py b/examples/quickstart-monai/model.py deleted file mode 100644 index 4c74d50553e..00000000000 --- a/examples/quickstart-monai/model.py +++ /dev/null @@ -1,33 +0,0 @@ -import torch - - -def train(model, train_loader, epoch_num, device): - loss_function = torch.nn.CrossEntropyLoss() - optimizer = torch.optim.Adam(model.parameters(), 1e-5) - for _ in range(epoch_num): - model.train() - for inputs, labels in train_loader: - optimizer.zero_grad() - loss_function(model(inputs.to(device)), labels.to(device)).backward() - optimizer.step() - - -def test(model, test_loader, device): - model.eval() - loss = 0.0 - y_true = list() - y_pred = list() - loss_function = torch.nn.CrossEntropyLoss() - with torch.no_grad(): - for test_images, test_labels in test_loader: - out = model(test_images.to(device)) - test_labels = test_labels.to(device) - loss += loss_function(out, test_labels).item() - pred = out.argmax(dim=1) - for i in range(len(pred)): - y_true.append(test_labels[i].item()) - y_pred.append(pred[i].item()) - accuracy = sum([1 if t == p else 0 for t, p in zip(y_true, y_pred)]) / len( - test_loader.dataset - ) - return loss, accuracy diff --git a/examples/quickstart-monai/monaiexample/__init__.py b/examples/quickstart-monai/monaiexample/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/quickstart-monai/monaiexample/client_app.py b/examples/quickstart-monai/monaiexample/client_app.py new file mode 100644 index 00000000000..c0dcac0cdae --- /dev/null +++ b/examples/quickstart-monai/monaiexample/client_app.py @@ -0,0 +1,41 @@ +"""monaiexample: A Flower / MONAI app.""" + +import torch +from flwr.common import Context +from flwr.client import NumPyClient, ClientApp + +from monaiexample.task import load_data, load_model, test, train, get_params, set_params + + +# Define Flower client +class FlowerClient(NumPyClient): + def __init__(self, net, trainloader, valloader): + self.net = net + self.trainloader = trainloader + self.valloader = valloader + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + def fit(self, parameters, config): + set_params(self.net, parameters) + train(self.net, self.trainloader, epoch_num=1, device=self.device) + return get_params(self.net), len(self.trainloader), {} + + def evaluate(self, parameters, config): + set_params(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader), {"accuracy": accuracy} + + +def client_fn(context: Context): + + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + batch_size = context.run_config["batch-size"] + trainloader, valloader = load_data(num_partitions, partition_id, batch_size) + net = load_model() + + return FlowerClient(net, trainloader, valloader).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-monai/monaiexample/server_app.py b/examples/quickstart-monai/monaiexample/server_app.py new file mode 100644 index 00000000000..f68d3887a48 --- /dev/null +++ b/examples/quickstart-monai/monaiexample/server_app.py @@ -0,0 +1,46 @@ +"""monaiexample: A Flower / MONAI app.""" + +from typing import List, Tuple + +from flwr.common import Metrics, Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from monaiexample.task import load_model, get_params + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context): + + # Init model + model = load_model() + + # Convert model parameters to flwr.common.Parameters + ndarrays = get_params(model) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define strategy + fraction_fit = context.run_config["fraction-fit"] + strategy = FedAvg( + fraction_fit=fraction_fit, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=global_model_init, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-monai/monaiexample/task.py b/examples/quickstart-monai/monaiexample/task.py new file mode 100644 index 00000000000..09597562a1f --- /dev/null +++ b/examples/quickstart-monai/monaiexample/task.py @@ -0,0 +1,199 @@ +"""monaiexample: A Flower / MONAI app.""" + +import os +import tarfile +from urllib import request +from collections import OrderedDict + +import torch +import monai +from monai.networks.nets import densenet +from monai.transforms import ( + Compose, + EnsureChannelFirst, + LoadImage, + RandFlip, + RandRotate, + RandZoom, + ScaleIntensity, + ToTensor, +) +from filelock import FileLock +from datasets import Dataset +from flwr_datasets.partitioner import IidPartitioner + + +def load_model(): + """Load a DenseNet12.""" + return densenet.DenseNet121(spatial_dims=2, in_channels=1, out_channels=6) + + +def get_params(model): + """Return tensors in the model's state_dict.""" + return [val.cpu().numpy() for _, val in model.state_dict().items()] + + +def set_params(model, ndarrays): + """Apply parameters to a model.""" + params_dict = zip(model.state_dict().keys(), ndarrays) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + model.load_state_dict(state_dict, strict=True) + + +def train(model, train_loader, epoch_num, device): + """Train a model using the supplied dataloader.""" + model.to(device) + loss_function = torch.nn.CrossEntropyLoss() + optimizer = torch.optim.Adam(model.parameters(), 1e-5) + for _ in range(epoch_num): + model.train() + for batch in train_loader: + images, labels = batch["img"], batch["label"] + optimizer.zero_grad() + loss_function(model(images.to(device)), labels.to(device)).backward() + optimizer.step() + + +def test(model, test_loader, device): + """Evaluate a model on a held-out dataset.""" + model.to(device) + model.eval() + loss = 0.0 + y_true = list() + y_pred = list() + loss_function = torch.nn.CrossEntropyLoss() + with torch.no_grad(): + for batch in test_loader: + images, labels = batch["img"], batch["label"] + out = model(images.to(device)) + labels = labels.to(device) + loss += loss_function(out, labels).item() + pred = out.argmax(dim=1) + for i in range(len(pred)): + y_true.append(labels[i].item()) + y_pred.append(pred[i].item()) + accuracy = sum([1 if t == p else 0 for t, p in zip(y_true, y_pred)]) / len( + test_loader.dataset + ) + return loss, accuracy + + +def _get_transforms(): + """Return transforms to be used for training and evaluation.""" + train_transforms = Compose( + [ + LoadImage(image_only=True), + EnsureChannelFirst(), + ScaleIntensity(), + RandRotate(range_x=15, prob=0.5, keep_size=True), + RandFlip(spatial_axis=0, prob=0.5), + RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5, keep_size=True), + ToTensor(), + ] + ) + + val_transforms = Compose( + [LoadImage(image_only=True), EnsureChannelFirst(), ScaleIntensity(), ToTensor()] + ) + + return train_transforms, val_transforms + + +def get_apply_transforms_fn(transforms_to_apply): + """Return a function that applies the transforms passed as input argument.""" + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [transforms_to_apply(img) for img in batch["img_file"]] + return batch + + return apply_transforms + + +ds = None +partitioner = None + + +def load_data(num_partitions, partition_id, batch_size): + """Download dataset, partition it and return data loader of specific partition.""" + # Set dataset and partitioner only once + global ds, partitioner + if ds is None: + image_file_list, image_label_list = _download_data() + + # Construct HuggingFace dataset + ds = Dataset.from_dict({"img_file": image_file_list, "label": image_label_list}) + # Set partitioner + partitioner = IidPartitioner(num_partitions) + partitioner.dataset = ds + + partition = partitioner.load_partition(partition_id) + + # Split train/validation + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + + # Get transforms + train_t, test_t = _get_transforms() + + # Apply transforms individually to each split + train_partition = partition_train_test["train"] + test_partition = partition_train_test["test"] + + partition_train = train_partition.with_transform(get_apply_transforms_fn(train_t)) + partition_val = test_partition.with_transform(get_apply_transforms_fn(test_t)) + + # Create dataloaders + train_loader = monai.data.DataLoader( + partition_train, batch_size=batch_size, shuffle=True + ) + val_loader = monai.data.DataLoader(partition_val, batch_size=batch_size) + + return train_loader, val_loader + + +def _download_data(): + """Download and extract dataset.""" + data_dir = "./MedNIST/" + _download_and_extract_if_needed( + "https://dl.dropboxusercontent.com/s/5wwskxctvcxiuea/MedNIST.tar.gz", + os.path.join(data_dir), + ) + + # Compute list of files and thier associated labels + class_names = sorted( + [x for x in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, x))] + ) + image_files = [ + [ + os.path.join(data_dir, class_name, x) + for x in os.listdir(os.path.join(data_dir, class_name)) + ] + for class_name in class_names + ] + image_file_list = [] + image_label_list = [] + for i, _ in enumerate(class_names): + image_file_list.extend(image_files[i]) + image_label_list.extend([i] * len(image_files[i])) + + return image_file_list, image_label_list + + +def _download_and_extract_if_needed(url, dest_folder): + """Download dataset if not present.""" + + # Logic behind a filelock to prevent multiple processes (e.g. ClientApps) + # from downloading the dataset at the same time. + with FileLock(".data_download.lock"): + if not os.path.isdir(dest_folder): + # Download the tar.gz file + tar_gz_filename = url.split("/")[-1] + if not os.path.isfile(tar_gz_filename): + with request.urlopen(url) as response, open( + tar_gz_filename, "wb" + ) as out_file: + out_file.write(response.read()) + + # Extract the tar.gz file + with tarfile.open(tar_gz_filename, "r:gz") as tar_ref: + tar_ref.extractall() diff --git a/examples/quickstart-monai/pyproject.toml b/examples/quickstart-monai/pyproject.toml index 2b77a2fc061..6ecf5011d24 100644 --- a/examples/quickstart-monai/pyproject.toml +++ b/examples/quickstart-monai/pyproject.toml @@ -1,19 +1,41 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -name = "quickstart-monai" -version = "0.1.0" -description = "MONAI Federated Learning Quickstart with Flower" -authors = ["The Flower Authors "] - -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -torch = "1.13.1" -tqdm = "4.66.3" -scikit-learn = "1.3.1" -monai = { version = "1.3.0", extras=["gdown", "nibabel", "tqdm", "itk"] } -numpy = "1.24.4" -pillow = "10.2.0" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "monaiexample" +version = "1.0.0" +description = "Federated Learning with MONAI and Flower (Quickstart Example)" +license = "Apache-2.0" +dependencies = [ + "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr-datasets[vision]>=0.3.0", + "monai==1.3.2", + "filelock==3.15.4", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "monaiexample.server_app:app" +clientapp = "monaiexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 5 +fraction-fit = 0.5 +batch-size = 128 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 + +[tool.flwr.federations.local-simulation-gpu] +options.num-supernodes = 10 +options.backend.client-resources.num-cpus = 4 +options.backend.client-resources.num-gpus = 0.25 # at most 4 ClientApps will run in a given GPU diff --git a/examples/quickstart-monai/requirements.txt b/examples/quickstart-monai/requirements.txt deleted file mode 100644 index e3f1e463c62..00000000000 --- a/examples/quickstart-monai/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -flwr>=1.0, <2.0 -torch==1.13.1 -tqdm==4.65.0 -scikit-learn==1.3.1 -monai[gdown,nibabel,tqdm,itk]==1.3.0 -numpy==1.24.4 -pillow==10.2.0 diff --git a/examples/quickstart-monai/run.sh b/examples/quickstart-monai/run.sh deleted file mode 100755 index 1da60bccb86..00000000000 --- a/examples/quickstart-monai/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -python -c "from data import _download_data; _download_data()" - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in `seq 0 1`; do - echo "Starting client $i" - python client.py --partition-id $i & -done - -# Enable CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-monai/server.py b/examples/quickstart-monai/server.py deleted file mode 100644 index fe691a88aba..00000000000 --- a/examples/quickstart-monai/server.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List, Tuple - -import flwr as fl -from flwr.common import Metrics - - -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = fl.server.strategy.FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -)