diff --git a/declearn/dataset/__init__.py b/declearn/dataset/__init__.py index 4c46d218cd0e5af76610fb0a256b25ac55955427..56dfdb2e00869e1e1db8e4c0bdb40c2f9906595c 100644 --- a/declearn/dataset/__init__.py +++ b/declearn/dataset/__init__.py @@ -30,6 +30,9 @@ API tools * [DataSpec][declearn.dataset.DataSpecs]: Dataclass to wrap a dataset's metadata. * [load_dataset_from_json][declearn.dataset.load_dataset_from_json"] + Utility function to parse a JSON into a dataset object +* [split_data][declearn.dataset.split_data] + Utility to split a single dataset into shards Dataset subclasses ------------------ @@ -48,3 +51,4 @@ from . import utils from . import examples from ._base import Dataset, DataSpecs, load_dataset_from_json from ._inmemory import InMemoryDataset +from .split_data import split_data diff --git a/declearn/quickrun/_split_data.py b/declearn/dataset/split_data.py similarity index 66% rename from declearn/quickrun/_split_data.py rename to declearn/dataset/split_data.py index 4e595f424ab7a3d5c33b6df0142042c25cb225ae..b6094f342048703190e1a7459ecf05289018ca68 100644 --- a/declearn/quickrun/_split_data.py +++ b/declearn/dataset/split_data.py @@ -35,11 +35,11 @@ instance sparse data import os from typing import Optional, Tuple, Union +import fire import numpy as np from declearn.dataset.examples import load_mnist from declearn.dataset.utils import load_data_array, split_multi_classif_dataset -from declearn.quickrun._config import DataSplitConfig def load_data( @@ -91,8 +91,17 @@ def load_data( return inputs, labels -def split_data(data_config: DataSplitConfig, folder: str) -> None: - """Download and randomly split a dataset into shards. +def split_data( + folder: str = ".", + data_file: Optional[str] = None, + label_file: Optional[Union[str, int]] = None, + n_shards: int = 3, + scheme: str = "iid", + perc_train: float = 0.8, + seed: Optional[int] = None, +) -> None: + + """Randomly split a dataset into shards. The resulting folder structure is : folder/ @@ -107,29 +116,49 @@ def split_data(data_config: DataSplitConfig, folder: str) -> None: Parameters ---------- - data_config: DataSplitConfig - A DataSplitConfig instance, see class documentation for details + folder: str, default = "." + Path to the folder where to add a data folder + holding output shard-wise files + data_file: str or None, default=None + Optional path to a folder where to find the data. + If None, default to the MNIST example. + target_file: str or int or None, default=None + If str, path to the labels file to import. If int, column of + the data file to be used as labels. Required if data is not None, + ignored if data is None. + n_shards: int + Number of shards between which to split the data. + scheme: {"iid", "labels", "biased"}, default="iid" + Splitting scheme(s) to use. In all cases, shards contain mutually- + exclusive samples and cover the full raw training data. + - If "iid", split the dataset through iid random sampling. + - If "labels", split into shards that hold all samples associated + with mutually-exclusive target classes. + - If "biased", split the dataset through random sampling according + to a shard-specific random labels distribution. + perc_train: float, default= 0.8 + Train/validation split in each client dataset, must be in the + ]0,1] range. + seed: int or None, default=None + Optional seed to the RNG used for all sampling operations. """ # Select output folder. - if data_config.data_folder: - folder = os.path.dirname(data_config.data_folder) - else: - folder = f"data_{data_config.scheme}" + folder = os.path.join(folder, f"data_{scheme}") # Value-check the 'perc_train' parameter. - if not 0.0 < data_config.perc_train <= 1.0: + if not 0.0 < perc_train <= 1.0: raise ValueError("'perc_train' should be a float in ]0,1]") # Load the dataset and split it. - inputs, labels = load_data(data_config.data_file, data_config.label_file) + inputs, labels = load_data(data_file, label_file) print( - f"Splitting data into {data_config.n_shards} shards " - f"using the '{data_config.scheme}' scheme." + f"Splitting data into {n_shards} shards " + f"using the '{scheme}' scheme." ) split = split_multi_classif_dataset( dataset=(inputs, labels), - n_shards=data_config.n_shards, - scheme=data_config.scheme, # type: ignore - p_valid=(1 - data_config.perc_train), - seed=data_config.seed, + n_shards=n_shards, + scheme=scheme, # type: ignore + p_valid=(1 - perc_train), + seed=seed, ) # Export the resulting shard-wise data to files. for idx, ((x_train, y_train), (x_valid, y_valid)) in enumerate(split): @@ -140,3 +169,12 @@ def split_data(data_config: DataSplitConfig, folder: str) -> None: if len(x_valid): np.save(os.path.join(subdir, "valid_data.npy"), x_valid) np.save(os.path.join(subdir, "valid_target.npy"), y_valid) + + +def main(): + "fire-wrapped split data" + fire.Fire(split_data) + + +if __name__ == "__main__": + main() diff --git a/declearn/dataset/utils/__init__.py b/declearn/dataset/utils/__init__.py index c19f9cb1efe452c6763bff5c3286ca5e9f442dc0..1516e660f8c896b2b81942f45ea92ee5d36d43b3 100644 --- a/declearn/dataset/utils/__init__.py +++ b/declearn/dataset/utils/__init__.py @@ -37,6 +37,6 @@ Data splitting [declearn.dataset.utils.split_multi_classif_dataset]: Split a classification dataset into (opt. heterogeneous) shards. """ -from ._sparse import sparse_from_file, sparse_to_file from ._save_load import load_data_array, save_data_array +from ._sparse import sparse_from_file, sparse_to_file from ._split_classif import split_multi_classif_dataset diff --git a/declearn/quickrun/_config.py b/declearn/quickrun/_config.py index 7614ed316dc6ded4e4de9dedce8121c4e1071a1a..ad32be23e20cbd96cd80c3cf7e9fd035111bbf56 100644 --- a/declearn/quickrun/_config.py +++ b/declearn/quickrun/_config.py @@ -25,7 +25,7 @@ from declearn.utils import TomlConfig __all__ = [ "ModelConfig", - "DataSplitConfig", + "DataSourceConfig", "ExperimentConfig", ] @@ -40,36 +40,12 @@ class ModelConfig(TomlConfig): @dataclasses.dataclass -class DataSplitConfig(TomlConfig): +class DataSourceConfig(TomlConfig): """Dataclass associated with the functions - declearn.quickrun._split_data:split_data and declearn.quickrun._parser:parse_data_folder data_folder: str - Absolute path to the folder where to export shard-wise files, - and/or to the main folder hosting the data. - n_shards: int - Number of shards between which to split the data. - data_file: str or None, default=None - Optional path to a folder where to find the data. - If None, default to the MNIST example. - target_file: str or int or None, default=None - If str, path to the labels file to import. If int, column of - the data file to be used as labels. Required if data is not None, - ignored if data is None. - scheme: {"iid", "labels", "biased"}, default="iid" - Splitting scheme(s) to use. In all cases, shards contain mutually- - exclusive samples and cover the full raw training data. - - If "iid", split the dataset through iid random sampling. - - If "labels", split into shards that hold all samples associated - with mutually-exclusive target classes. - - If "biased", split the dataset through random sampling according - to a shard-specific random labels distribution. - perc_train: float, default= 0.8 - Train/validation split in each client dataset, must be in the - ]0,1] range. - seed: int or None, default=None - Optional seed to the RNG used for all sampling operations. + Absolute path to the to the main folder hosting the data. client_names: list or None List of custom client names to look for in the data_folder. If None, default to expected prefix search. @@ -79,16 +55,7 @@ class DataSplitConfig(TomlConfig): If None, , default to expected prefix search. """ - # Common args data_folder: Optional[str] = None - # split_data args - n_shards: int = 3 - data_file: Optional[str] = None - label_file: Optional[Union[str, int]] = None - scheme: str = "iid" - perc_train: float = 0.8 - seed: Optional[int] = None - # parse_data_folder args client_names: Optional[List[str]] = None dataset_names: Optional[Dict[str, str]] = None diff --git a/declearn/quickrun/_parser.py b/declearn/quickrun/_parser.py index a753d7dcfd26f5379dcb883af8947f13355d5ea3..b1b67b6741505a7886d8f355a2991cdf46a3d7fe 100644 --- a/declearn/quickrun/_parser.py +++ b/declearn/quickrun/_parser.py @@ -16,24 +16,19 @@ # limitations under the License. """ -#TODO +Utils parsing a data folder following a standard format into a nested +dictionnary """ import os from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, Optional -from declearn.test_utils import make_importable - -# Perform local imports. -# pylint: disable=wrong-import-order, wrong-import-position -with make_importable(os.path.dirname(__file__)): - from _config import ExperimentConfig -# pylint: enable=wrong-import-order, wrong-import-position +from declearn.quickrun._config import DataSourceConfig def parse_data_folder( - expe_config: ExperimentConfig, + data_config: DataSourceConfig, folder: Optional[str] = None, ) -> Dict: """Utils parsing a data folder following a standard format into a nested @@ -53,17 +48,16 @@ def parse_data_folder( Parameters: ----------- - expe_config : ExperimentConfig - ExperimentConfig instance, see class documentation for details. + data_config : DataSourceConfig + DataSourceConfig instance, see class documentation for details. folder : str or None The main experiment folder in which to look for a `data*` folder. Overwritten by data_folder. """ - data_folder = expe_config.data_folder - client_names = expe_config.client_names - dataset_names = expe_config.dataset_names - scheme = expe_config.scheme + data_folder = data_config.data_folder + client_names = data_config.client_names + dataset_names = data_config.dataset_names if not folder and not data_folder: raise ValueError( @@ -71,13 +65,13 @@ def parse_data_folder( ) # Data_folder if not data_folder: - search_str = f"_{scheme}" if scheme else "*" - gen_folders = Path(folder).glob(f"data{search_str}") # type: ignore + gen_folders = Path(folder).glob("data*") # type: ignore data_folder = next(gen_folders, False) # type: ignore if not data_folder: raise ValueError( f"No folder starting with 'data' found in {folder}. " - "Please store your data under a 'data_*' folder" + "Please store your split data under a 'data_*' folder. " + "To use an example dataset run `declearn-split` first." ) if next(gen_folders, False): raise ValueError( @@ -86,7 +80,13 @@ def parse_data_folder( "parent folder" ) else: - data_folder = Path(data_folder) + if os.path.isdir(data_folder): + data_folder = Path(data_folder) + else: + raise ValueError( + f"{data_folder} is not a valid path. To use an example " + "dataset run `declearn-split` first." + ) # Get clients dir if client_names: if isinstance(client_names, list): diff --git a/declearn/quickrun/run.py b/declearn/quickrun/run.py index 36db434b2d4f08ff2c797025bbbcd2746d08fe27..71441b4946c9eb7ce58faf1374b15e1975fa10ba 100644 --- a/declearn/quickrun/run.py +++ b/declearn/quickrun/run.py @@ -40,18 +40,19 @@ import textwrap from datetime import datetime from typing import Dict, List, Optional, Tuple +import fire + from declearn.communication import NetworkClientConfig, NetworkServerConfig from declearn.dataset import InMemoryDataset from declearn.main import FederatedClient, FederatedServer from declearn.main.config import FLOptimConfig, FLRunConfig from declearn.model.api import Model from declearn.quickrun._config import ( - DataSplitConfig, + DataSourceConfig, ExperimentConfig, ModelConfig, ) from declearn.quickrun._parser import parse_data_folder -from declearn.quickrun._split_data import split_data from declearn.test_utils import make_importable from declearn.utils import ( LOGGING_LEVEL_MAJOR, @@ -63,12 +64,6 @@ from declearn.utils import ( __all__ = ["quickrun"] -DEFAULT_FOLDER = os.path.join( - os.path.dirname(__file__), - "../../examples/quickrun", -) - - def get_model(folder, model_config) -> Model: "Return a model instance from a model config instance" path = model_config.model_file or os.path.join(folder, "model.py") @@ -82,6 +77,7 @@ def get_model(folder, model_config) -> Model: def get_checkpoint(folder: str, expe_config: ExperimentConfig) -> str: + """Return the checkpoint folder, either default or as given in config""" if expe_config.checkpoint: checkpoint = expe_config.checkpoint else: @@ -146,15 +142,13 @@ def run_client( client.run() -def get_toml_folder(config: Optional[str] = None) -> Tuple[str, str]: - """Deternmine if provided config is a file or a directory, and +def get_toml_folder(config: str) -> Tuple[str, str]: + """Determine if provided config is a file or a directory, and return : * The path to the TOML config file * The path to the main folder of the experiment """ # default to the mnist example - if not config: - config = DEFAULT_FOLDER config = os.path.abspath(config) # check if config is TOML or dir if os.path.isfile(config): @@ -170,16 +164,11 @@ def get_toml_folder(config: Optional[str] = None) -> Tuple[str, str]: return toml, folder -def locate_or_create_split_data(toml: str, folder: str) -> Dict: +def locate_split_data(toml: str, folder: str) -> Dict: """Attempts to find split data according to the config toml or - or the default behavior. If failed, attempts to find full data - according to the config toml and split it""" - data_config = DataSplitConfig.from_toml(toml, False, "data") - try: - client_dict = parse_data_folder(data_config, folder) - except ValueError: - split_data(data_config, folder) - client_dict = parse_data_folder(data_config, folder) + or the default behavior""" + data_config = DataSourceConfig.from_toml(toml, False, "data") + client_dict = parse_data_folder(data_config, folder) return client_dict @@ -194,22 +183,36 @@ def server_to_client_network( ) -def quickrun(config: Optional[str] = None) -> None: - """Run a server and its clients using multiprocessing. +def quickrun(config: str) -> None: + """ + Run a server and its clients using multiprocessing. + + The script requires to be provided with the path a TOML file + with all the elements required to configurate an FL experiment, + or the path to a folder containing : + + * a TOML file with all the elements required to configurate an + FL experiment + * A declearn model + * A data folder, structured in a specific way + + Parameters: + ---- + config: str + Path to either a toml file or a properly formatted folder + containing the elements required to launch an FL experiment - The kwargs are the arguments expected by split_data, - see [the documentation][declearn.quickrun._split_data] """ toml, folder = get_toml_folder(config) # locate split data or split it if needed - client_dict = locate_or_create_split_data(toml, folder) + client_dict = locate_split_data(toml, folder) # Parse toml files ntk_server_cfg = NetworkServerConfig.from_toml(toml, False, "network") ntk_client_cfg = server_to_client_network(ntk_server_cfg) optim_cgf = FLOptimConfig.from_toml(toml, False, "optim") run_cfg = FLRunConfig.from_toml(toml, False, "run") - model_cfg = ModelConfig.from_toml(toml, False, "model") - expe_cfg = ExperimentConfig.from_toml(toml, False, "experiment") + model_cfg = ModelConfig.from_toml(toml, False, "model", True) + expe_cfg = ExperimentConfig.from_toml(toml, False, "experiment", True) # Set up a (func, args) tuple specifying the server process. p_server = ( run_server, @@ -230,39 +233,9 @@ def quickrun(config: Optional[str] = None) -> None: ) -def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: - """Set up and run a command-line arguments parser.""" - usage = """ - Quickly run an example locally using declearn. - The script requires to be provided with the path a TOML file - with all the elements required to configurate an FL experiment, - or the path to a folder containing : - * a TOML file with all the elements required to configurate an - FL experiment - * A declearn model - * A data folder, structured in a specific way - - If not provided with this, the script defaults to the MNIST example - provided by declearn in `declearn.example.quickrun`. - """ - usage = re.sub("\n *(?=[a-z])", " ", textwrap.dedent(usage)) - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - usage=re.sub("- ", "-", usage), - ) - parser.add_argument( - "--config", - default=None, - dest="config", - help="Path to the root folder where to export data.", - ) - return parser.parse_args(args) - - -def main(args: Optional[List[str]] = None) -> None: - """Quikcrun based on commandline-input arguments.""" - cmdargs = parse_args(args) - quickrun(config=cmdargs.config) +def main() -> None: + """Fire-wrapped quickrun""" + fire.Fire(quickrun) if __name__ == "__main__": diff --git a/declearn/utils/_toml_config.py b/declearn/utils/_toml_config.py index 249db82556941d6264afe6701ac47ee93f052b2e..476d960e9da8ed3dc23510cc7a11dad082176b22 100644 --- a/declearn/utils/_toml_config.py +++ b/declearn/utils/_toml_config.py @@ -298,6 +298,7 @@ class TomlConfig: path: str, warn_user: bool = True, use_section: Optional[str] = None, + section_fail_ok: bool = False, ) -> Self: """Parse a structured configuration from a TOML file. @@ -325,6 +326,9 @@ class TomlConfig: If not None, points to a specific section of the TOML that should be used, rather than the whole file. Useful to parse orchestrating TOML files, e.g. quickrun. + section_fail_ok: bool, default=False + If True, allow the section specified in use_section to be + missing from the TOML file without raising an Error. Raises ------ @@ -352,7 +356,8 @@ class TomlConfig: try: config = config[use_section] except KeyError as exc: - raise KeyError("Specified section not found") from exc + if not section_fail_ok: + raise KeyError("Specified section not found") from exc params = {} # type: Dict[str, Any] for field in dataclasses.fields(cls): # Case when the section is provided: set it up for parsing. diff --git a/docs/quickstart.md b/docs/quickstart.md index 72b041025376b0a29cc152db0209fee50ca9e2c2..a8d5c355a643214e833e93abffa5ddb75921ca7a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,38 +1,91 @@ # Quickstart -This section provides with demonstration code on how to run a simple federated -learning task using declearn, that requires minimal adjustments to be run for -real (mainly, to provide with a valid network configuration and actual data). +**Here's where to start if you want to quickly understand what `declearn` does**. This tutorial exepects a basic understanding of [federated learning](https://en.wikipedia.org/wiki/Federated_learning). -You may find even more concrete examples on our gitlab repository -[here](https://gitlab.inria.fr/magnet/declearn/declearn2/examples). -The Heart UCI example may notably be run as-is, either locally or on a -real-life network with minimal command-line parametrization. +We show different ways to use `declearn` on a well-known example, the +[MNIST dataset](http://yann.lecun.com/exdb/mnist/) (see [section 1](#1-federated-learning-on-the-mnist-dataset)). We then look at how to +use declearn on your own problem (see [section 2](#2-federated-learning-on-your-own-dataset)). -## Setting +## 1. Federated learning on the MNIST dataset -Here is a quickstart example on how to set up a federated learning process -to learn a LASSO logistic regression model (using a scikit-learn backend) -using pre-processed data, formatted as csv files with a "label" column, +**We are going to train a common model between three simulated clients on the +classic [MNIST dataset](http://yann.lecun.com/exdb/mnist/)**. The input of the +model is a set of images of handwritten digits, and the model needs to +determine which number between $0$ and $9$ each image corresponds to. +We show two ways to use `declearn` on this problem. + +### 1.1. Quickrun mode + +**The quickrun mode is the simplest way to simulate a federated learning +process on a single machine with `declearn`**. It does not require to +understand the details of `declearn` implementation. It requires a basic +understanding of federated learning. + +--- +**To test this on the MNIST example**, you can follow along +[this colab notebook](https://colab.research.google.com/drive/13sBDOQeorI6dfziSoyRpU4q4iGuESIPo?usp=sharing). + +--- + +**If you want to run this locally**, the detailed notebook can be boiled down +to five `bash` commands. Set up a dedicated `conda` of `venv` environment, +and run : + +```bash +git clone https://gitlab.inria.fr/magnet/declearn/declearn2 && +cd declearn2 && +pip install .[tensorflow,websockets] && +declearn-split --folder "examples/mnist_quickrun" && +declearn-quickrun --config "examples/mnist_quickrun/config.toml" +``` + +**To better understand the details** of how what happens under the hood you +can look at what the key element of the declearn process are in +[section 1.2.](#12-python-script). To understand how to use the quickrun mode +in practice see [section 2.1.](#21-quickrun-on-your-problem). + +### 1.2. Python script + +#### MNIST + +**The quickrun mode abstracts away a lot of important elements** of the +process, and is only designed to simulate an FL experiment: the clients all +run on the same machine. In real life deployment, a `declearn` experiment is +built in python. + +--- +**To see what this looks like in practice**, you can head to +`examples/mnist/readme.md` in the `declearn` repository. + +--- + +#### Stylized structure + +At a very high-level, declearn is structured around two key objects. The +`Clients` hold the data and perform calculations locally. The `Server` owns +the model and the global training process. They communicate through a `network`, hosted by the `Server`. + +We provide below a stylized view of the main elements of +the `Server` and `Client` script. For more details, you can look at the +hands on usage [section](user-guide/usage.md) of the documentation. + +We show what a `Client` and `Server` script can look like on a hypothetical +LASSO logistic regression model, using a scikit-learn backend and +pre-processed data. The data is in csv files with a "label" column, where each client has two files: one for training, the other for validation. Here, the code uses: -- standard FedAvg strategy (SGD for local steps, averaging of updates weighted - by clients' training dataset size, no modifications of server-side updates) -- 10 rounds of training, with 5 local epochs performed at each round and - 128-samples batch size -- at least 1 and at most 3 clients, awaited for 180 seconds by the server -- network communications using gRPC, on host "example.com" and port 8888 - -Note that this example code may easily be adjusted to suit use cases, using -other types of models, alternative federated learning algorithms and/or -modifying the communication, training and validation hyper-parameters. -Please refer to the [Hands-on usage](./user-guide/usage.md) section for a more -detailed and general description of how to set up a federated learning -task and process with declearn. +* **Aggregation**: the standard `FedAvg` strategy +* **Optimizer**: standard SGD for both client and server +* **Training**: + * $10$ rounds of training, with $5$ local epochs performed at each round and + $128$-samples batch size + * At least $1$ and at most $3$ clients, awaited for $180$ seconds by the + server +* **Network**: communications using `websockets` -## Server-side script +The server-side script: ```python import declearn @@ -41,7 +94,7 @@ model = declearn.model.sklearn.SklearnSGDModel.from_parameters( kind="classifier", loss="log_loss", penalty="l1" ) netwk = declearn.communication.NetworkServerConfig( - protocol="grpc", host="example.com", port=8888, + protocol="websockets", host="127.0.0.1"", port=8888, certificate="path/to/certificate.pem", private_key="path/to/private_key.pem" ) @@ -60,14 +113,14 @@ config = declearn.main.config.FLRunConfig.from_params( server.run(config) ``` -## Client-side script +The client-side script ```python import declearn netwk = declearn.communication.NetworkClientConfig( - protocol="grpc", - server_uri="example.com:8888", + protocol="websockets", + server_uri="127.0.0.1":8888", name="client_name", certificate="path/to/root_ca.pem" ) @@ -82,22 +135,165 @@ client = declearn.main.FederatedClient( client.run() ``` -## Simulating this experiment locally - -To simulate the previous experiment on a single computer, you may set up -network communications to go through the localhost, and resort to one of -two possibilities: - -1. Run the server and client-wise scripts parallelly, e.g. in distinct - terminals. -2. Use declearn-provided tools to run the server and clients' routines - concurrently using multiprocessing. - -While technically similar (both solutions resolve on isolating the agents' -routines in separate python processes that communicate over the localhost), -the second solution offers more practicality in terms of offering a single -entrypoint for your experiment, and optionally automatically stopping any -running agent in case one of the other has failed. -To find out more about this solution, please have a look at the Heart UCI -example [implemented here](https://gitlab.inria.fr/magnet/declearn/declearn2\ --/tree/develop/examples/heart-uci). +## 2. Federated learning on your own dataset + +### 2.1. Quickrun on your problem + +Using the mode `declearn-quickrun` requires a configuration file, some data, and a model: + +* A TOML file, to store your experiment configurations. In the MNIST example: +`examples/mnist_quickrun/config.toml`. +* A folder with your data, split by client. In the MNIST example, `examples/mnist_quickrun/data_iid` (after running `declearn-split --folder "examples/mnist_quickrun"`). +* A model file, to store your model wrapped in a `declearn` object. In the +MNIST example, `examples/mnist_quickrun/model.py`. + +#### The TOML file + +TOML is a minimal, human-readable configuration file format. +We use is to store all the configurations of an FL experiment. +The TOML is parsed by python as dictionnary with each `[header]` +as a key. For more details, see the [TOML doc](https://toml.io/en/) + +This file is your main entry point to everything else. +The absolute path to this file should be given as an argument in: + +```bash +declearn-quickrun --config <path_to_toml_file> +``` + +The TOML file has six sections, some optional. Note the order does not matter, +and that we give illustrative, not necessarily functionnal examples. + +`[network]`: Network configuration used by both client and server, +most notably the port, host, and ssl certificates. An example: + +``` python +[network] + protocol = "websockets" # Protocol used, to keep things simple use websocket + host = "127.0.0.1" # Address used, works as is on most set ups + port = 8765 # Port used, works as is on most set ups +``` + +This section is parsed as the initialization arguments to the `NetworkServer` +class. Check its [documentation][declearn.communication.api.NetworkServer] +to see all available fields. Note it is also used to initialize +a [`NetworkClient`][declearn.communication.api.NetworkClient], mirroring the +server. + +`[data]`: Where to find your data. This is particularly useful if you have split your data yourself, using custom names for files and folders. An example: + +```python +[data] + data_folder = "./custom/data_custom" # Your main data folder + client_names = ["client_a","client_b","client_c"] # The names of your client folders + [dataset_names] # The names of train and test datasets + train_data = "cifar_train" + train_target = "label_train" + valid_data = "cifar_valid" + valid_target = "label_valid" +``` + +This section is parsed as the fields of a `DataSourceConfig` dataclass. Check its +[documentation][declearn.quickrun.DataSourceConfig] to see all +available fields. This `DataSourceConfig` is then parsed by the +[`parse_data_folder`][`declearn.quickrun.parse_data_folder`] function. + +`[optim]`: Optimization options for both client and server, with three distinct sub-sections : the server-side aggregator (i) and optimizer (ii), and the client optimizer (iii). An example: + +```python +[optim] + aggregator = "averaging" # The basic server aggregation strategy + + [optim.server_opt] # Server optimization strategy + lrate = 1.0 # Server learning rate + + [optim.client_opt] # Client optimization strategy + lrate = 0.001 # Client learning rate + modules = [["momentum", {"beta" = 0.9}]] # List of optimizer modules used + regularizers = [["lasso", {alpha = 0.1}]] # List of regularizer modules +``` + +This section is parsed as the fields of a `FLOptimConfig` dataclass. Check its +[documentation][declearn.main.FLOptimConfig] to see more details on the three +sub-sections. For more details on available fields within those subsections, you +can naviguate inside the documentation of the [`Aggregator`][declearn.aggregator.Aggregator] and [`Optimizer`][declearn.optimizer.Optimizer] classes. + +`[run]`: Training process option for both client and server. Most notably, includes the number of rounds as well as the registration, training, and evaluation parameters. An example: + +```python +[run] + rounds = 10 # Number of overall training rounds + + [run.register] # Client registration options + min_clients = 1 # Minimum of clients that need to connect + max_clients = 6 # The maximum number of clients that can connect + timeout = 5 # How long to wait for clients, in seconds + + [run.training] # Client training procedure + n_epoch = 1 # Number of local epochs + batch_size = 48 # Training batch size + drop_remainder = false # Whether to drop the last training examples + + [run.evaluate] + batch_size = 128 # Evaluation batch size +``` + +This section is parsed as the fields of a `FLRunConfig` dataclass. Check its +[documentation][declearn.main.FLOptimConfig] to see more details on the +sub-sections. For more details on available fields within those subsections, you +can naviguate inside the documentation of `FLRunConfig` to the relevant +dataclass, for instance [`TrainingConfig`][declearn.main.TrainingConfig] + +`[model]`: Optional section, where to find the model. An example: + +```python +[model] +# The location to a model file +model_file = "./custom/model_custom.py" +# The name of your model file, if different from "MyModel" +model_name = "MyCustomModel" +``` + +This section is parsed as the fields of a `ModelConfig` dataclass. Check its +[documentation][declearn.quickrun.ModelConfig] to see all +available fields. + +`[experiment]`: Optional section, what to report during the experiment and where to report it. An example: + +```python +[experiment] +metrics=[["multi-classif",{labels = [0,1,2,3,4,5,6,7,8,9]}]] # Accuracy metric +checkpoint = "./result_custom" # Custom location for results +``` + +This section is parsed as the fields of a `ExperimentConfig` dataclass. Check its +[documentation][declearn.quickrun.ExperimentConfig] to see all +available fields. + +#### The data + +Your data, in a standard tabular format, split by client. Within each client +folder, we expect four files : training data and labels, validation data and +labels. + +If your data is not already split by client, we are developping an experimental +data splitting utility. It currently has a limited scope, only dealing +with classification tasks, excluding multi-label. You can call it using +`declearn-split --folder <path_to_original_data>`. For more details, refer to +the [documentation][declearn.dataset.split_data]. + +#### The Model file + +The model file should just contain the model you built for +your data, e.g. a `torch` model, wrapped in a declearn object. +See `examples/mnist_quickrun/model.py` for an example. + +The wrapped model should be named "MyModel" by default. If you use +any other name, you'll need to mention it in the TOML file, as done +in `./custom/config_custom.toml` + +### 2.2. Using declearn full capabilities + +To upgrade your experimental setting beyond the `quickrun` mode, +head to the hands on usage [section](user-guide/usage.md) of the +documentation. diff --git a/docs/setup.md b/docs/setup.md index 547f44b1126135e50cdf39724d0c2d178a623cdf..805f6d140f4794965f404fdbbdd6da5282e821bb 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -107,3 +107,5 @@ pip install declearn[all,tests] # install all extra and testing dependencies package, and then to manually install the dependencies listed in the `pyproject.toml` file, using `conda install` rather than `pip install` whenever it is possible. +- On some systems, the square brackets used our pip install are not properly +parsed. Try replacing `[` by `\[` and `]` by `\]`. diff --git a/examples/mnist_quickrun/config.toml b/examples/mnist_quickrun/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..b01422a743b1631857f799686e13be08f60df34b --- /dev/null +++ b/examples/mnist_quickrun/config.toml @@ -0,0 +1,41 @@ +# This is a minimal TOML file for the MNIST example +# It contains the bare minimum to make the experiment run. +# See quickstart for more details. + +# The TOML is parsed by python as dictionnary with each `[header]` +# as a key. Note the "=" sign and the absence of quotes around keys. +# For more details, see the full doc : https://toml.io/en/ + +[network] # Network configuration used by both client and server + protocol = "websockets" # Protocol used, to keep things simple use websocket + host = "127.0.0.1" # Address used, works as-is on most set ups + port = 8765 # Port used, works as-is on most set ups + +[data] # Where to find your data + data_folder = "examples/mnist_quickrun/data_iid" + +[optim] # Optimization options for both client and server + aggregator = "averaging" # Server aggregation strategy + + [optim.client_opt] # Client optimization strategy + lrate = 0.001 # Client learning rate + modules = ["adam"] # List of optimizer modules used + + [optim.server_opt] # Server optimization strategy + lrate = 1.0 # Server learning rate + +[run] # Training process option for both client and server + rounds = 10 # Number of overall training rounds + + [run.register] # Client registration options + timeout = 5 # How long to wait for clients, in seconds + + [run.training] # Client training options + batch_size = 48 # Training batch size + + [run.evaluate] # Client evaluation options + batch_size = 128 # Evaluation batch size + +[experiment] # What to report during the experiment and where to report it + metrics=[["multi-classif",{labels = [0,1,2,3,4,5,6,7,8,9]}]] # Accuracy metric + diff --git a/examples/mnist_quickrun/mnist.ipynb b/examples/mnist_quickrun/mnist.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..7753314dcda05051be7177eb88705051a07d3188 --- /dev/null +++ b/examples/mnist_quickrun/mnist.ipynb @@ -0,0 +1,576 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is meant to be run in google colab. You can find import your local copy of the file in the the [colab welcome page](https://colab.research.google.com/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s9bpLdH5ThpJ" + }, + "source": [ + "# Setting up your declearn " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Clzf4NTja121" + }, + "source": [ + "We first clone the repo, to have both the package itself and the `examples` folder we will use in this tutorial, then naviguate to the package directory, and finally install the required dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u2QDwb0_QQ_f", + "outputId": "cac0761c-b229-49b0-d71d-c7b5cef919b3" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloning into 'declearn2'...\n", + "warning: redirecting to https://gitlab.inria.fr/magnet/declearn/declearn2.git/\n", + "remote: Enumerating objects: 4997, done.\u001b[K\n", + "remote: Counting objects: 100% (79/79), done.\u001b[K\n", + "remote: Compressing objects: 100% (79/79), done.\u001b[K\n", + "remote: Total 4997 (delta 39), reused 0 (delta 0), pack-reused 4918\u001b[K\n", + "Receiving objects: 100% (4997/4997), 1.15 MiB | 777.00 KiB/s, done.\n", + "Resolving deltas: 100% (3248/3248), done.\n" + ] + } + ], + "source": [ + "!git clone -b experimental https://gitlab.inria.fr/magnet/declearn/declearn2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "9kDHh_AfPG2l", + "outputId": "74e2f85f-7f93-40ae-a218-f4403470d72c" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/content/declearn2\n" + ] + } + ], + "source": [ + "cd declearn2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Un212t1GluHB", + "outputId": "0ea67577-da6e-4f80-a412-7b7a79803aa1" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Processing /content/declearn2\n", + " Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n", + " Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n", + " Installing backend dependencies ... \u001b[?25l\u001b[?25hdone\n", + " Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", + "Requirement already satisfied: cryptography>=35.0 in /usr/local/lib/python3.9/dist-packages (from declearn==2.1.0) (40.0.1)\n", + "Requirement already satisfied: scikit-learn>=1.0 in /usr/local/lib/python3.9/dist-packages (from declearn==2.1.0) (1.2.2)\n", + "Requirement already satisfied: requests~=2.18 in /usr/local/lib/python3.9/dist-packages (from declearn==2.1.0) (2.27.1)\n", + "Requirement already satisfied: pandas>=1.2 in /usr/local/lib/python3.9/dist-packages (from declearn==2.1.0) (1.5.3)\n", + "Requirement already satisfied: tomli>=2.0 in /usr/local/lib/python3.9/dist-packages (from declearn==2.1.0) (2.0.1)\n", + "Collecting fire>=0.4\n", + " Downloading fire-0.5.0.tar.gz (88 kB)\n", + "\u001b[2K \u001b[90mâ”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”\u001b[0m \u001b[32m88.3/88.3 kB\u001b[0m \u001b[31m1.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25h Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + "Requirement already satisfied: typing-extensions>=4.0 in /usr/local/lib/python3.9/dist-packages (from declearn==2.1.0) (4.5.0)\n", + "Collecting websockets~=10.1\n", + " Downloading websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (106 kB)\n", + "\u001b[2K \u001b[90mâ”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”\u001b[0m \u001b[32m106.5/106.5 kB\u001b[0m \u001b[31m3.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: cffi>=1.12 in /usr/local/lib/python3.9/dist-packages (from cryptography>=35.0->declearn==2.1.0) (1.15.1)\n", + "Requirement already satisfied: six in /usr/local/lib/python3.9/dist-packages (from fire>=0.4->declearn==2.1.0) (1.16.0)\n", + "Requirement already satisfied: termcolor in /usr/local/lib/python3.9/dist-packages (from fire>=0.4->declearn==2.1.0) (2.2.0)\n", + "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.9/dist-packages (from pandas>=1.2->declearn==2.1.0) (2022.7.1)\n", + "Requirement already satisfied: numpy>=1.20.3 in /usr/local/lib/python3.9/dist-packages (from pandas>=1.2->declearn==2.1.0) (1.22.4)\n", + "Requirement already satisfied: python-dateutil>=2.8.1 in /usr/local/lib/python3.9/dist-packages (from pandas>=1.2->declearn==2.1.0) (2.8.2)\n", + "Requirement already satisfied: charset-normalizer~=2.0.0 in /usr/local/lib/python3.9/dist-packages (from requests~=2.18->declearn==2.1.0) (2.0.12)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.9/dist-packages (from requests~=2.18->declearn==2.1.0) (1.26.15)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.9/dist-packages (from requests~=2.18->declearn==2.1.0) (2022.12.7)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.9/dist-packages (from requests~=2.18->declearn==2.1.0) (3.4)\n", + "Requirement already satisfied: scipy>=1.3.2 in /usr/local/lib/python3.9/dist-packages (from scikit-learn>=1.0->declearn==2.1.0) (1.10.1)\n", + "Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.9/dist-packages (from scikit-learn>=1.0->declearn==2.1.0) (3.1.0)\n", + "Requirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.9/dist-packages (from scikit-learn>=1.0->declearn==2.1.0) (1.2.0)\n", + "Requirement already satisfied: pycparser in /usr/local/lib/python3.9/dist-packages (from cffi>=1.12->cryptography>=35.0->declearn==2.1.0) (2.21)\n", + "Building wheels for collected packages: fire, declearn\n", + " Building wheel for fire (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + " Created wheel for fire: filename=fire-0.5.0-py2.py3-none-any.whl size=116952 sha256=ab01943c400d3267450974ec56a6572193bed40710845edd44623e56c7757799\n", + " Stored in directory: /root/.cache/pip/wheels/f7/f1/89/b9ea2bf8f80ec027a88fef1d354b3816b4d3d29530988972f6\n", + " Building wheel for declearn (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", + " Created wheel for declearn: filename=declearn-2.1.0-py3-none-any.whl size=276123 sha256=4969a91ded8b704c8c9497bcda8f514f847c49098715d659cc8e96a947ec887f\n", + " Stored in directory: /tmp/pip-ephem-wheel-cache-fgkx9jiw/wheels/cc/79/79/6586306a117d40a1f8b251a22e50583b8abb2d7e855a62ecf7\n", + "Successfully built fire declearn\n", + "Installing collected packages: websockets, fire, declearn\n", + "Successfully installed declearn-2.1.0 fire-0.5.0 websockets-10.4\n" + ] + } + ], + "source": [ + "!pip install .[websockets]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hC8Fty8YTy9P" + }, + "source": [ + "# Running your first experiment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rcWcZJdob1IG" + }, + "source": [ + "We are going to train a common model between three simulated clients on the classic [MNIST dataset](http://yann.lecun.com/exdb/mnist/). The input of the model is a set of images of handwritten digits, and the model needs to determine which number between 0 and 9 each image corresponds to." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KlY_vVtFHv2P" + }, + "source": [ + "## The model\n", + "\n", + "To do this, we will use a simple CNN, defined in `examples/mnist_quickrun/model.py`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "C7D52a8_dEr7", + "outputId": "a25223f8-c8eb-4998-d7fd-4b8bfde92486" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model: \"sequential\"\n", + "_________________________________________________________________\n", + " Layer (type) Output Shape Param # \n", + "=================================================================\n", + " conv2d (Conv2D) (None, 26, 26, 8) 80 \n", + " \n", + " max_pooling2d (MaxPooling2D (None, 13, 13, 8) 0 \n", + " ) \n", + " \n", + " dropout (Dropout) (None, 13, 13, 8) 0 \n", + " \n", + " flatten (Flatten) (None, 1352) 0 \n", + " \n", + " dense (Dense) (None, 64) 86592 \n", + " \n", + " dropout_1 (Dropout) (None, 64) 0 \n", + " \n", + " dense_1 (Dense) (None, 10) 650 \n", + " \n", + "=================================================================\n", + "Total params: 87,322\n", + "Trainable params: 87,322\n", + "Non-trainable params: 0\n", + "_________________________________________________________________\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/content/declearn2/declearn/model/tensorflow/utils/_gpu.py:66: UserWarning: Cannot use a GPU device: either CUDA is unavailable or no GPU is visible to tensorflow.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "from examples.mnist_quickrun.model import model\n", + "model.summary()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HoBcOs9hH2QA" + }, + "source": [ + "## The data\n", + "\n", + "We start by splitting the MNIST dataset between 3 clients and storing the output in the `examples/mnist_quickrun` folder. For this we use an experimental utility provided by `declearn`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "quduXkpIWFjL", + "outputId": "ddf7d45d-acf0-44ee-ce77-357c0987a2a1" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading MNIST source file train-images-idx3-ubyte.gz.\n", + "Downloading MNIST source file train-labels-idx1-ubyte.gz.\n", + "Splitting data into 3 shards using the 'iid' scheme.\n" + ] + } + ], + "source": [ + "from declearn.dataset import split_data\n", + "split_data(folder=\"examples/mnist_quickrun\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3-2hKmz-2RF4" + }, + "source": [ + "Here is what the first image of the first client looks like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 430 + }, + "id": "MLVI9GOZ1TGd", + "outputId": "f34a6a93-cb5f-4a45-bc24-4146ea119d1a" + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "<Figure size 640x480 with 1 Axes>" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "images = np.load(\"examples/mnist_quickrun/data_iid/client_0/train_data.npy\")\n", + "sample_img = images[0]\n", + "sample_fig = plt.imshow(sample_img,cmap='Greys')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1vNWNGjefSfH" + }, + "source": [ + "For more information on how the `split_data` function works, you can look at the documentation. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-wORmq5DYfRF", + "outputId": "4d79da63-ccad-4622-e600-ac36fae1ff3f" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Randomly split a dataset into shards.\n", + "\n", + " The resulting folder structure is :\n", + " folder/\n", + " └─── data*/\n", + " └─── client*/\n", + " │ train_data.* - training data\n", + " │ train_target.* - training labels\n", + " │ valid_data.* - validation data\n", + " │ valid_target.* - validation labels\n", + " └─── client*/\n", + " │ ...\n", + "\n", + " Parameters\n", + " ----------\n", + " folder: str, default = \".\"\n", + " Path to the folder where to add a data folder\n", + " holding output shard-wise files\n", + " data_file: str or None, default=None\n", + " Optional path to a folder where to find the data.\n", + " If None, default to the MNIST example.\n", + " target_file: str or int or None, default=None\n", + " If str, path to the labels file to import. If int, column of\n", + " the data file to be used as labels. Required if data is not None,\n", + " ignored if data is None.\n", + " n_shards: int\n", + " Number of shards between which to split the data.\n", + " scheme: {\"iid\", \"labels\", \"biased\"}, default=\"iid\"\n", + " Splitting scheme(s) to use. In all cases, shards contain mutually-\n", + " exclusive samples and cover the full raw training data.\n", + " - If \"iid\", split the dataset through iid random sampling.\n", + " - If \"labels\", split into shards that hold all samples associated\n", + " with mutually-exclusive target classes.\n", + " - If \"biased\", split the dataset through random sampling according\n", + " to a shard-specific random labels distribution.\n", + " perc_train: float, default= 0.8\n", + " Train/validation split in each client dataset, must be in the\n", + " ]0,1] range.\n", + " seed: int or None, default=None\n", + " Optional seed to the RNG used for all sampling operations.\n", + " \n" + ] + } + ], + "source": [ + "print(split_data.__doc__)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kZtbxlwUftKd" + }, + "source": [ + "## Quickrun\n", + "\n", + "We can now run our experiment. As explained in the section 2.1 of the [quickstart documentation](https://magnet.gitlabpages.inria.fr/declearn/docs/2.1/quickstart), using the mode `declearn-quickrun` requires a configuration file, some data, and a model:\n", + "\n", + "* A TOML file, to store your experiment configurations. Here: \n", + "`examples/mnist_quickrun/config.toml`.\n", + "* A folder with your data, split by client. Here: `examples/mnist_quickrun/data_iid`\n", + "* A model file, to store your model wrapped in a `declearn` object. Here: `examples/mnist_quickrun/model.py`.\n", + "\n", + "We then only have to run the `quickrun` util with the path to the TOML file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1n_mvTIIWpRf" + }, + "outputs": [], + "source": [ + "from declearn.quickrun import quickrun\n", + "quickrun(config=\"examples/mnist_quickrun/config.toml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "O0kuw7UxJqKk" + }, + "source": [ + "The output obtained is the combination of the CLI output of our server and our clients, going through: \n", + "\n", + "* `INFO:Server:Starting clients registration process.` : a first registration step, where clients register with the server\n", + "* `INFO:Server:Sending initialization requests to clients.`: the initilization of the object needed for training on both the server and clients side.\n", + "* `Server:INFO: Initiating training round 1`: the training starts, where each client makes its local update(s) and send the result to the server which aggregates them\n", + "* `INFO: Initiating evaluation round 1`: the model is evaluated at each round\n", + "* `Server:INFO: Stopping training`: the training is finalized " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wo6NDugiOH6V" + }, + "source": [ + "## Results \n", + "\n", + "You can have a look at the results in the `examples/mnist_quickrun/result_*` folder, including the metrics evolution during training. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zlm5El13SvnG" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import glob\n", + "import os \n", + "\n", + "res_file = glob.glob('examples/mnist_quickrun/result*') \n", + "res = pd.read_csv(os.path.join(res_file[0],'server/metrics.csv'))\n", + "res_fig = res.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Kd_MBQt9OJ40" + }, + "source": [ + "# Experiment further\n", + "\n", + "\n", + "You can change the TOML config file to experiment with different strategies." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E3OOeAYJRGqU" + }, + "source": [ + "For instance, try splitting the data in a very heterogenous way, by distributing digits in mutually exclusive way between clients. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BNPLnpQuQ8Au" + }, + "outputs": [], + "source": [ + "split_data(folder=\"examples/mnist_quickrun\",scheme='labels')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Xfs-3wH-3Eio" + }, + "source": [ + "And change the `examples/mnist_quickrun/config.toml` file with:\n", + "\n", + "```\n", + "[data] \n", + " data_folder = \"examples/mnist_quickrun/data_labels\" \n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZZVFNO07O1ry" + }, + "source": [ + "If you run the model as is, you should see a drop of performance\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7kFa0EbINJXq" + }, + "outputs": [], + "source": [ + "quickrun(config=\"examples/mnist_quickrun/config.toml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XV6JfaRzR3ee" + }, + "source": [ + "Now try modifying the `examples/mnist_quickrun/config.toml` file like this, to implement the [scaffold algorithm](https://arxiv.org/abs/1910.06378) and running the experiment again. \n", + "\n", + "```\n", + " [optim]\n", + "\n", + " [optim.client_opt]\n", + " lrate = 0.005 \n", + " modules = [\"scaffold-client\"] \n", + "\n", + " [optim.server_opt]\n", + " lrate = 1.0 \n", + " modules = [\"scaffold-client\"]\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FK6c9HDjSdGZ" + }, + "outputs": [], + "source": [ + "quickrun(config=\"examples/mnist_quickrun/config.toml\")" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "s9bpLdH5ThpJ", + "KlY_vVtFHv2P", + "HoBcOs9hH2QA", + "kZtbxlwUftKd", + "wo6NDugiOH6V", + "Kd_MBQt9OJ40" + ], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/quickrun/model.py b/examples/mnist_quickrun/model.py similarity index 100% rename from examples/quickrun/model.py rename to examples/mnist_quickrun/model.py diff --git a/examples/quickrun/config.toml b/examples/quickrun/config.toml deleted file mode 100644 index 35c23aebda424df9cd53372d6dfc87dde084f21b..0000000000000000000000000000000000000000 --- a/examples/quickrun/config.toml +++ /dev/null @@ -1,39 +0,0 @@ -# This is a minimal TOML file for the MNIST example -# It contains the bare minimum to make the experiment run -# It can be used as a template for other experiments. -# See `examples/quickrun/readme.md` for more details. - - -[network] # Network configuration used by both client and server -protocol = "websockets" # Protocol used, to keep things simple use websocket -host = "127.0.0.1" # Address used, works as is on most set ups -port = 8765 # Port used, works as is on most set ups - -[model] # Information on where to find the model file - -[data] # How to split your data -scheme = "iid" # SPlit your data iid between simulated clients - -[optim] # Optimizers options for both client and server -aggregator = "averaging" # Server aggregation strategy - - [optim.client_opt] # Client optimization strategy - lrate = 0.001 # Client learning rate - modules = ["adam"] # List of optimizer modules used - -[run] # Training process option for both client and server -rounds = 10 # Number of overall training rounds - - [run.register] # Client registration options - timeout = 5 # How long to wait for clients, in seconds - - [run.training] # Client training procedure - batch_size = 48 # Training batch size - - [run.evaluate] - # batch_size = 128 # Evaluation batch size - - -[experiment] # What to report during the experiment and where to report it -metrics=[["multi-classif",{labels = [0,1,2,3,4,5,6,7,8,9]}]] # Accuracy metric - diff --git a/examples/quickrun/custom/config_custom.toml b/examples/quickrun/custom/config_custom.toml deleted file mode 100644 index 73c0c9e5e1e5fdf551985019eb67962907e57819..0000000000000000000000000000000000000000 --- a/examples/quickrun/custom/config_custom.toml +++ /dev/null @@ -1,81 +0,0 @@ -# This is a TOML file for demonstrating the extend of customization -# available for experiments through TOMl. It contains commented mock -# fields and can be used as a template for other experiments. It will -# not run as is as if references some not existing files for demonstration -# purposes. - - -[network] # Network configuration used by both client and server -protocol = "websockets" # Protocol used, to keep things simple use websocket -host = "127.0.0.1" # Address used, works as is on most set ups -port = 8765 # Port used, works as is on most set ups - -[model] # Information on where to find the model file -# The location to a model file, if not following expected structure -model_file = "examples/quickrun/custom/model_custom.py" -# The name of your model file, if different from "MyModel" -model_name = "MyCustomModel" - -[data] # How to split your data - -# If you want to split your data -# Where to save your split data, if not in the expected "result" folder -data_folder = "examples/quickrun/custom/data_custom" -n_shards = 3 # Number of simulated clients -# Where to find the original data -data_file = "examples/quickrun/data_unsplit/train_data.npy" -# Where to find the original data labels -label_file = "examples/quickrun/data_unsplit/train_target.npy" -scheme = "iid" # How to split your data between simulated clients -perc_train = 0.8 # For each client, how much data -seed = 22 - -# If you have already split data, not using expected names -# The custom names of your clients -client_names = ["client_0","client_1","client_2"] - [dataset_names] # The names train and test datasets - train_data = "train_data" - train_target = "train_target" - valid_data = "valid_data" - valid_target = "valid_target" - -[optim] # Optimizers options for both client and server -aggregator = "averaging" # Server aggregation strategy -server_opt = 1.0 # The server learning rate - - [optim.client_opt] # Client optimization strategy - lrate = 0.001 # Client learning rate - # List of optimizer modules used - modules = [["momentum", {"beta" = 0.9}]] - # List of regularizer modules - regularizers = [["lasso", {alpha = 0.1}]] - -[run] # Training process option for both client and server -rounds = 10 # Number of overall training rounds - - [run.register] # Client registration options - min_clients = 1 # Minimum of clients that need to connect - max_clients = 6 # The maximum number of clients that can connect - timeout = 5 # How long to wait for clients, in seconds - - [run.training] # Client training procedure - n_epoch = 1 # Number of local epochs - batch_size = 48 # Training batch size - drop_remainder = false # Whether to drop the last trainig examples - - [run.evaluate] - batch_size = 128 # Evaluation batch size - - -[experiment] # What to report during the experiment and where to report it -metrics=[["multi-classif",{labels = [0,1,2,3,4,5,6,7,8,9]}]] # Accuracy metric -checkpoint = "examples/quickrun/result_custom" # Custom location to results - - - - - - - - - diff --git a/examples/quickrun/custom/model_custom.py b/examples/quickrun/custom/model_custom.py deleted file mode 100644 index 4aaef2c33527b260fe04187cb0a65a7a8f662624..0000000000000000000000000000000000000000 --- a/examples/quickrun/custom/model_custom.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Wrapping a torch model""" - -import tensorflow as tf -import torch -import torch.nn as nn -import torch.nn.functional as F - -from declearn.model.tensorflow import TensorflowModel -from declearn.model.torch import TorchModel - - -stack = [ - tf.keras.layers.InputLayer(input_shape=(28, 28, 1)), - tf.keras.layers.Conv2D(32, 3, 1, activation="relu"), - tf.keras.layers.Conv2D(64, 3, 1, activation="relu"), - tf.keras.layers.MaxPool2D(2), - tf.keras.layers.Dropout(0.25), - tf.keras.layers.Flatten(), - tf.keras.layers.Dense(128, activation="relu"), - tf.keras.layers.Dropout(0.5), - tf.keras.layers.Dense(10, activation="softmax"), -] -model = tf.keras.models.Sequential(stack) -MyCustomModel = TensorflowModel(model, loss="sparse_categorical_crossentropy") diff --git a/examples/quickrun/readme.md b/examples/quickrun/readme.md deleted file mode 100644 index 2eb05b6444cc95f981a1956b45d9d7e7664eb16a..0000000000000000000000000000000000000000 --- a/examples/quickrun/readme.md +++ /dev/null @@ -1,122 +0,0 @@ -# Using quickrun - -## The main idea - -The `quickrun` mode is a way to quickly run an FL experiment -without needing to understand the details of `declearn`. - -Once you have `declearn` installed, try running `declearn-quickrun` -to get run the MNIST example. - -This mode requires two files and some data : - -* A TOML file, to store your experiment configurations. -* A model file, to store your model wrapped in a `declearn` object -* A folder with your data, either already split between clients or -to be split usiong our utility function - -To run your own expriments, these three elements needs to be either -organizzed in a specific way, or referenced in the TOML file. See details -in the last section. - -## The TOML file - -TOML is a minimal, human-readable configuration file format. -We use is to store all the configurations of an FL experiment. - -This file is your main entry point to everythiong else. -If you write your own, the absolute path to this file should be given -as an argument : `declearn-quickrun --config <path_to_toml_file>` - -For a minimal example of what it looks like in `declearn`, see -`./config.toml`. You can use it as a template -to write your own. - -The TOML is parsed to python as dictionnary with each `[header]` -as a key. If you are a first time user, you just need to -understand how to write dictionnaries and lists. This : - -``` -[key1] -sub_key = ["a","b"] -[key2] -sub_key = 1 -``` - -will be parsed as : - -```python -{"key1":{"sub_key":["a","b"]},"key2":{"sub_key":1}} -``` - -Note the = sign and the absence of quotes around keys. - -For more details, see the full doc : <https://toml.io/en/> - -For a more detailed templates, with all options used -See `./custom/config_custom.toml` - -## The Model file - -The model file should just contain the model you built for -your data, e.g. a `torch` model, wrapped in a declearn object. -See `./model.py` for an example. - -The wrapped model should be named "MyModel" by default. If you use -any other name, you'll need to mention it in the TOML file, as done -in `./custom/config_custom.toml` - -## The data - -Your data, in a standard tabular format. This data can either require -splitting or be already split by client. If your data requires splitting, -we provide with an experimental data splitter with currently a limited -scope. - -Requires splitting: - -* You have a single dataset and want to use provided utils to split it -between simulated clients -* In which case you need to mention your data source in the TOML file, -as well as details on how to split your data. See -`./custom/config_custom.toml` for details. -* Note that our data splitting util currently has a limited scope, -only dealing with classification tasks, excluding multi-label. If your -use case falls outside of that, you can write custom splitting code -and refer to the next paragraph - -Already split: - -* If your data is already split between clients, you will need to -add details in the TOML file on where to find this data. See -`./custom/config_custom.toml` for details. - -## Organizing your files - -The quickrun mode expects a `config` path as an argument. This can be the path to : - -* A folder, expected to be structured a certain way -* A TOML file, where the location of other object is mentionned - -In both cases, the default is to check the folder provided, or the TOML -parent folder, is structured as follows: - -``` - folder/ - │ config.toml - the config file - │ model.py - the model - └─── data*/ - a folder starting with 'data' containing split data - └─── client*/ - one folder per client, each containing 4 files - │ train_data.* - training data - │ train_target.* - training labels - │ valid_data.* - validation data - │ valid_target.* - validation labels - └─── client*/ - │ ... - └─── result/ - └─── client*/ - ... -``` - -Any changes to this structure should be referenced in the TOML file, as -shown in `./custom/config_custom.toml`. diff --git a/pyproject.toml b/pyproject.toml index 4e008541bd004462d4b8e304f4b1f389b583d6df..140a4aa184e8a1e4cb64917e73a8023d597a70c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ "scikit-learn >= 1.0", "tomli >= 2.0 ; python_version < '3.11'", "typing_extensions >= 4.0", + "fire >= 0.4", ] [project.optional-dependencies] @@ -132,3 +133,4 @@ declearn = ["py.typed"] [project.scripts] declearn-quickrun = "declearn.quickrun.run:main" +declearn-split = "declearn.dataset.split_data:main"