From 6ba0507140a80c1fa1725c2ee3c7867a3a22749f Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Mon, 15 Jan 2024 16:17:41 +0000 Subject: [PATCH 01/45] Add central DP server side fixed clipping --- src/py/flwr/server/strategy/__init__.py | 2 + .../dp_strategy_wrapper_fixed_clipping.py | 221 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py diff --git a/src/py/flwr/server/strategy/__init__.py b/src/py/flwr/server/strategy/__init__.py index 1750a7522379..e7c3573d8b45 100644 --- a/src/py/flwr/server/strategy/__init__.py +++ b/src/py/flwr/server/strategy/__init__.py @@ -16,6 +16,7 @@ from .bulyan import Bulyan as Bulyan +from .dp_strategy_wrapper_fixed_clipping import DPStrategyWrapperServerSideFixedClipping from .dpfedavg_adaptive import DPFedAvgAdaptive as DPFedAvgAdaptive from .dpfedavg_fixed import DPFedAvgFixed as DPFedAvgFixed from .fault_tolerant_fedavg import FaultTolerantFedAvg as FaultTolerantFedAvg @@ -57,4 +58,5 @@ "DPFedAvgAdaptive", "DPFedAvgFixed", "Strategy", + "DPStrategyWrapperServerSideFixedClipping", ] diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py new file mode 100644 index 000000000000..1e87822d9856 --- /dev/null +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -0,0 +1,221 @@ +# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Central DP. + +Papers: https://arxiv.org/pdf/1712.07557.pdf, https://arxiv.org/pdf/1710.06963.pdf +Note: unlike the above papers, we moved the clipping part to the server side. +""" +from typing import Dict, List, Optional, Tuple, Union + +import numpy as np + +from flwr.common import ( + EvaluateIns, + EvaluateRes, + FitIns, + FitRes, + NDArrays, + Parameters, + Scalar, + ndarrays_to_parameters, + parameters_to_ndarrays, +) +from flwr.server.client_manager import ClientManager +from flwr.server.client_proxy import ClientProxy +from flwr.server.strategy.strategy import Strategy + + +class DPStrategyWrapperServerSideFixedClipping(Strategy): + """Wrapper for Configuring a Strategy for Central DP. + + The clipping is at the server side. + + Parameters + ---------- + strategy: Strategy + The strategy to which DP functionalities will be added by this wrapper. + noise_multiplier: float + The noise multiplier for the Gaussian mechanism for model updates. + A value of 1.0 or higher is recommended for strong privacy. + clipping_threshold: float + The value of the clipping threshold. + num_sampled_clients: int + The number of clients that are sampled on each round. + """ + + # pylint: disable=too-many-arguments,too-many-instance-attributes + def __init__( + self, + strategy: Strategy, + noise_multiplier: float, + clipping_threshold: float, + num_sampled_clients: int, + ) -> None: + super().__init__() + + self.strategy = strategy + + if noise_multiplier < 0: + raise Exception("The noise multiplier should be a non-negative value.") + + if clipping_threshold <= 0: + raise Exception("The clipping threshold should be a positive value.") + + if num_sampled_clients <= 0: + raise Exception("The clipping threshold should be a positive value.") + + self.noise_multiplier = noise_multiplier + self.clipping_threshold = clipping_threshold + self.num_sampled_clients = num_sampled_clients + + self.current_round_params: NDArrays = [] + + def __repr__(self) -> str: + """Compute a string representation of the strategy.""" + rep = "DP Strategy Wrapper with Fixed Clipping" + return rep + + def initialize_parameters( + self, client_manager: ClientManager + ) -> Optional[Parameters]: + """Initialize global model parameters using given strategy.""" + return self.strategy.initialize_parameters(client_manager) + + def configure_fit( + self, server_round: int, parameters: Parameters, client_manager: ClientManager + ) -> List[Tuple[ClientProxy, FitIns]]: + """Configure the next round of training.""" + self.current_round_params = parameters_to_ndarrays(parameters) + return self.strategy.configure_fit(server_round, parameters, client_manager) + + def configure_evaluate( + self, server_round: int, parameters: Parameters, client_manager: ClientManager + ) -> List[Tuple[ClientProxy, EvaluateIns]]: + """Configure the next round of evaluation.""" + return self.strategy.configure_evaluate( + server_round, parameters, client_manager + ) + + def aggregate_fit( + self, + server_round: int, + results: List[Tuple[ClientProxy, FitRes]], + failures: List[Union[Tuple[ClientProxy, FitRes], BaseException]], + ) -> Tuple[Optional[Parameters], Dict[str, Scalar]]: + """Aggregate training results using unweighted aggregation.""" + if failures: + return None, {} + + # Extract all clients' model params + clients_params = [ + parameters_to_ndarrays(fit_res.parameters) for _, fit_res in results + ] + + # Compute the updates + all_clients_updates = self._compute_model_updates(clients_params) + + # Clip updates + for client_update in all_clients_updates: + client_update = self._clip_model_update(client_update) + + # Compute the new parameters with the clipped updates + for client_param, client_update in zip(clients_params, all_clients_updates): + self._update_clients_params(client_param, client_update) + + # Update the results with the new params + for res, params in zip(results, clients_params): + res[1].parameters = ndarrays_to_parameters(params) + + # Pass the new parameters for aggregation + aggregated_updates, metrics = self.strategy.aggregate_fit( + server_round, results, failures + ) + + # Add Gaussian noise to the aggregated parameters + if aggregated_updates: + aggregated_updates = self._add_noise_to_updates(aggregated_updates) + + return aggregated_updates, metrics + + def aggregate_evaluate( + self, + server_round: int, + results: List[Tuple[ClientProxy, EvaluateRes]], + failures: List[Union[Tuple[ClientProxy, EvaluateRes], BaseException]], + ) -> Tuple[Optional[float], Dict[str, Scalar]]: + """Aggregate evaluation losses using the given strategy.""" + return self.strategy.aggregate_evaluate(server_round, results, failures) + + def evaluate( + self, server_round: int, parameters: Parameters + ) -> Optional[Tuple[float, Dict[str, Scalar]]]: + """Evaluate model parameters using an evaluation function from the strategy.""" + return self.strategy.evaluate(server_round, parameters) + + def _clip_model_update(self, update: NDArrays) -> NDArrays: + """Clip model update based on the computed clipping_threshold. + + FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf + """ + update_norm = self.get_update_norm(update) + scaling_factor = min(1, self.clipping_threshold / update_norm) + update_clipped: NDArrays = [layer * scaling_factor for layer in update] + return update_clipped + + @staticmethod + def get_update_norm(update: NDArrays) -> float: + """Compute the L2 norm of the flattened update.""" + flattened_update = np.concatenate( + [np.asarray(sub_update).flatten() for sub_update in update] + ) + return float(np.linalg.norm(flattened_update)) + + def _add_noise_to_updates(self, parameters: Parameters) -> Parameters: + """Add Gaussian noise to model params.""" + return ndarrays_to_parameters( + self.add_gaussian_noise( + parameters_to_ndarrays(parameters), + float( + (self.noise_multiplier * self.clipping_threshold) + / self.num_sampled_clients ** (0.5) + ), + ) + ) + + @staticmethod + def add_gaussian_noise(update: NDArrays, std_dev: float) -> NDArrays: + """Add Gaussian noise to each element of the provided update.""" + update_noised = [ + layer + np.random.normal(0, std_dev, layer.shape) for layer in update + ] + return update_noised + + def _compute_model_updates( + self, all_clients_params: List[NDArrays] + ) -> List[NDArrays]: + all_client_updates = [] + for client_param in all_clients_params: + client_update = [ + np.subtract(x, y) + for (x, y) in zip(client_param, self.current_round_params) + ] + all_client_updates.append(client_update) + return all_client_updates + + def _update_clients_params( + self, client_param: NDArrays, client_update: NDArrays + ) -> None: + for i, _ in enumerate(self.current_round_params): + client_param[i] = self.current_round_params[i] + client_update[i] \ No newline at end of file From e66e1ad916643a34c90588f730fe2bd481a4deba Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Mon, 15 Jan 2024 16:22:36 +0000 Subject: [PATCH 02/45] Add central DP server-side fixed clipping unit tests --- .../dp_strategy_wrapper_fixed_clipping.py | 2 +- ...dp_strategy_wrapper_fixed_clipping_test.py | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 1e87822d9856..a5b9dee8851a 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -218,4 +218,4 @@ def _update_clients_params( self, client_param: NDArrays, client_update: NDArrays ) -> None: for i, _ in enumerate(self.current_round_params): - client_param[i] = self.current_round_params[i] + client_update[i] \ No newline at end of file + client_param[i] = self.current_round_params[i] + client_update[i] diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py new file mode 100644 index 000000000000..4ec14668bc4c --- /dev/null +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -0,0 +1,113 @@ +# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""DPWrapper_fixed_clipping tests.""" + +import numpy as np + +from flwr.common import ndarrays_to_parameters, parameters_to_ndarrays + +from .dp_strategy_wrapper_fixed_clipping import DPStrategyWrapperServerSideFixedClipping +from .fedavg import FedAvg + + +def test_add_gaussian_noise() -> None: + """Test add_gaussian_noise function.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + + update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + std_dev = 0.1 + + # Execute + update_noised = dp_wrapper.add_gaussian_noise(update, std_dev) + + # Assert + # Check that the shape of the result is the same as the input + for layer, layer_noised in zip(update, update_noised): + assert layer.shape == layer_noised.shape + + # Check that the values have been changed and is not equal to the original update + for layer, layer_noised in zip(update, update_noised): + assert not np.array_equal(layer, layer_noised) + + # Check that the noise has been added + for layer, layer_noised in zip(update, update_noised): + noise_added = layer_noised - layer + assert np.any(np.abs(noise_added) > 0) + + +def test_add_noise_to_updates() -> None: + """Test _add_noise_to_updates method.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + parameters = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + + # Execute + result = parameters_to_ndarrays( + # pylint: disable-next=protected-access + dp_wrapper._add_noise_to_updates(ndarrays_to_parameters(parameters)) + ) + + # Assert + for layer in result: + assert layer.shape == parameters[0].shape # Check shape consistency + assert not np.array_equal(layer, parameters[0]) # Check if noise was added + + +def test_get_update_norm() -> None: + """Test get_update_norm function.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + + # Execute + result = dp_wrapper.get_update_norm(update) + + expected = float( + np.linalg.norm(np.concatenate([sub_update.flatten() for sub_update in update])) + ) + + # Assert + assert expected == result + + +def test_clip_model_updates() -> None: + """Test _clip_model_updates method.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + + updates = [ + np.array([[1.5, -0.5], [2.0, -1.0]]), + np.array([0.5, -0.5]), + np.array([[-0.5, 1.5], [-1.0, 2.0]]), + np.array([-0.5, 0.5]), + ] + + # Execute + # pylint: disable-next=protected-access + clipped_updates = dp_wrapper._clip_model_update(updates) + + # Assert + assert len(clipped_updates) == len(updates) + + for clipped_update, original_update in zip(clipped_updates, updates): + clip_norm = np.linalg.norm(original_update) + assert np.all(clipped_update <= clip_norm) and np.all( + clipped_update >= -clip_norm + ) From a88f72e52fedeac31519612157db0c97f210f97e Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Mon, 15 Jan 2024 16:46:52 +0000 Subject: [PATCH 03/45] Fix exceptin msg --- .../flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index a5b9dee8851a..118562f68794 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -74,7 +74,7 @@ def __init__( raise Exception("The clipping threshold should be a positive value.") if num_sampled_clients <= 0: - raise Exception("The clipping threshold should be a positive value.") + raise Exception("The number of sampled clients should be a positive value.") self.noise_multiplier = noise_multiplier self.clipping_threshold = clipping_threshold From 16c1c41044b695deffbacf4e01ec3101f71c3029 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 25 Jan 2024 16:40:05 +0000 Subject: [PATCH 04/45] Clean code --- src/py/flwr/common/differential_privacy.py | 42 +++++++++++++++++++ .../flwr/common/differential_privacy_test.py | 41 ++++++++++++++++++ .../dp_strategy_wrapper_fixed_clipping.py | 21 ++-------- ...dp_strategy_wrapper_fixed_clipping_test.py | 29 +------------ 4 files changed, 87 insertions(+), 46 deletions(-) create mode 100644 src/py/flwr/common/differential_privacy.py create mode 100644 src/py/flwr/common/differential_privacy_test.py diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py new file mode 100644 index 000000000000..6f3446077ab9 --- /dev/null +++ b/src/py/flwr/common/differential_privacy.py @@ -0,0 +1,42 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Utility functions for differential privacy.""" + +import numpy as np + + +def get_norm(input: NDArrays) -> float: + """Compute the L2 norm of the flattened input.""" + flattened_input = np.concatenate( + [np.asarray(sub_input).flatten() for sub_input in input] + ) + return float(np.linalg.norm(flattened_input)) + + +def add_gaussian_noise(input: NDArrays, std_dev: float) -> NDArrays: + """Add noise to each element of the provided input from Gaussian (Normal) + distribution with respect to the passed standard deviation.""" + noised_input = [ + layer + np.random.normal(0, std_dev, layer.shape) for layer in input + ] + return noised_input + +def clip_inputs(self, update: NDArrays, ) -> NDArrays: + """Clip model update based on the computed clipping_threshold. + + FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf + """ + update_norm = get_norm(update) + scaling_factor = min(1, self.clipping_threshold / update_norm) + update_clipped: NDArrays = [layer * scaling_factor for layer in update] + return update_clipped diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py new file mode 100644 index 000000000000..570e167c9dbe --- /dev/null +++ b/src/py/flwr/common/differential_privacy_test.py @@ -0,0 +1,41 @@ +# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + + +def test_add_gaussian_noise() -> None: + """Test add_gaussian_noise function.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + + update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + std_dev = 0.1 + + # Execute + update_noised = dp_wrapper.add_gaussian_noise(update, std_dev) + + # Assert + # Check that the shape of the result is the same as the input + for layer, layer_noised in zip(update, update_noised): + assert layer.shape == layer_noised.shape + + # Check that the values have been changed and is not equal to the original update + for layer, layer_noised in zip(update, update_noised): + assert not np.array_equal(layer, layer_noised) + + # Check that the noise has been added + for layer, layer_noised in zip(update, update_noised): + noise_added = layer_noised - layer + assert np.any(np.abs(noise_added) > 0) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 118562f68794..9126a838007a 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -32,6 +32,7 @@ ndarrays_to_parameters, parameters_to_ndarrays, ) +from flwr.common.differential_privacy import add_gaussian_noise, get_norm from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.strategy.strategy import Strategy @@ -169,23 +170,15 @@ def _clip_model_update(self, update: NDArrays) -> NDArrays: FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf """ - update_norm = self.get_update_norm(update) + update_norm = get_norm(update) scaling_factor = min(1, self.clipping_threshold / update_norm) update_clipped: NDArrays = [layer * scaling_factor for layer in update] return update_clipped - @staticmethod - def get_update_norm(update: NDArrays) -> float: - """Compute the L2 norm of the flattened update.""" - flattened_update = np.concatenate( - [np.asarray(sub_update).flatten() for sub_update in update] - ) - return float(np.linalg.norm(flattened_update)) - def _add_noise_to_updates(self, parameters: Parameters) -> Parameters: """Add Gaussian noise to model params.""" return ndarrays_to_parameters( - self.add_gaussian_noise( + add_gaussian_noise( parameters_to_ndarrays(parameters), float( (self.noise_multiplier * self.clipping_threshold) @@ -194,14 +187,6 @@ def _add_noise_to_updates(self, parameters: Parameters) -> Parameters: ) ) - @staticmethod - def add_gaussian_noise(update: NDArrays, std_dev: float) -> NDArrays: - """Add Gaussian noise to each element of the provided update.""" - update_noised = [ - layer + np.random.normal(0, std_dev, layer.shape) for layer in update - ] - return update_noised - def _compute_model_updates( self, all_clients_params: List[NDArrays] ) -> List[NDArrays]: diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 4ec14668bc4c..28e4c7382384 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""DPWrapper_fixed_clipping tests.""" +"""dp_strategy_wrapper_fixed_clipping tests.""" import numpy as np @@ -22,33 +22,6 @@ from .fedavg import FedAvg -def test_add_gaussian_noise() -> None: - """Test add_gaussian_noise function.""" - # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) - - update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] - std_dev = 0.1 - - # Execute - update_noised = dp_wrapper.add_gaussian_noise(update, std_dev) - - # Assert - # Check that the shape of the result is the same as the input - for layer, layer_noised in zip(update, update_noised): - assert layer.shape == layer_noised.shape - - # Check that the values have been changed and is not equal to the original update - for layer, layer_noised in zip(update, update_noised): - assert not np.array_equal(layer, layer_noised) - - # Check that the noise has been added - for layer, layer_noised in zip(update, update_noised): - noise_added = layer_noised - layer - assert np.any(np.abs(noise_added) > 0) - - def test_add_noise_to_updates() -> None: """Test _add_noise_to_updates method.""" # Prepare From 4b84265dec0a59201ff0d71cffb9d90ed4779a34 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 25 Jan 2024 22:31:50 +0000 Subject: [PATCH 05/45] Clean code --- src/py/flwr/common/differential_privacy.py | 17 ++++--- .../flwr/common/differential_privacy_test.py | 48 +++++++++++++++++-- .../dp_strategy_wrapper_fixed_clipping.py | 34 +++++-------- ...dp_strategy_wrapper_fixed_clipping_test.py | 45 ----------------- 4 files changed, 65 insertions(+), 79 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 6f3446077ab9..f231067304e4 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -14,6 +14,8 @@ import numpy as np +from flwr.common.typing import NDArrays + def get_norm(input: NDArrays) -> float: """Compute the L2 norm of the flattened input.""" @@ -27,16 +29,17 @@ def add_gaussian_noise(input: NDArrays, std_dev: float) -> NDArrays: """Add noise to each element of the provided input from Gaussian (Normal) distribution with respect to the passed standard deviation.""" noised_input = [ - layer + np.random.normal(0, std_dev, layer.shape) for layer in input + layer + np.random.normal(0, std_dev**2, layer.shape) for layer in input ] return noised_input -def clip_inputs(self, update: NDArrays, ) -> NDArrays: - """Clip model update based on the computed clipping_threshold. + +def clip_inputs(input: NDArrays, clipping_norm: float) -> NDArrays: + """Clip model update based on the clipping norm. FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf """ - update_norm = get_norm(update) - scaling_factor = min(1, self.clipping_threshold / update_norm) - update_clipped: NDArrays = [layer * scaling_factor for layer in update] - return update_clipped + input_norm = get_norm(input) + scaling_factor = min(1, clipping_norm / input_norm) + clipped_inputs: NDArrays = [layer * scaling_factor for layer in input] + return clipped_inputs diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index 570e167c9dbe..9d997d1a790b 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -12,26 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== +"""DP utility functions tests.""" def test_add_gaussian_noise() -> None: """Test add_gaussian_noise function.""" # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) - update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] std_dev = 0.1 # Execute - update_noised = dp_wrapper.add_gaussian_noise(update, std_dev) + update_noised = add_gaussian_noise(update, std_dev) # Assert # Check that the shape of the result is the same as the input for layer, layer_noised in zip(update, update_noised): assert layer.shape == layer_noised.shape - # Check that the values have been changed and is not equal to the original update + # Check that the values have been changed and are not equal to the original update for layer, layer_noised in zip(update, update_noised): assert not np.array_equal(layer, layer_noised) @@ -39,3 +37,43 @@ def test_add_gaussian_noise() -> None: for layer, layer_noised in zip(update, update_noised): noise_added = layer_noised - layer assert np.any(np.abs(noise_added) > 0) + + +def test_get_update_norm() -> None: + """Test get_norm function.""" + # Prepare + update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + + # Execute + result = get_norm(update) + + expected = float( + np.linalg.norm(np.concatenate([sub_update.flatten() for sub_update in update])) + ) + + # Assert + assert expected == result + + +def test_clip_model_updates() -> None: + """Test clip_inputs function.""" + # Prepare + updates = [ + np.array([[1.5, -0.5], [2.0, -1.0]]), + np.array([0.5, -0.5]), + np.array([[-0.5, 1.5], [-1.0, 2.0]]), + np.array([-0.5, 0.5]), + ] + clipping_norm = 1.5 + + # Execute + clipped_updates = clip_inputs(updates, clipping_norm) + + # Assert + assert len(clipped_updates) == len(updates) + + for clipped_update, original_update in zip(clipped_updates, updates): + clip_norm = np.linalg.norm(original_update) + assert np.all(clipped_update <= clip_norm) and np.all( + clipped_update >= -clip_norm + ) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 9126a838007a..4d4441dbd2f6 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -32,16 +32,15 @@ ndarrays_to_parameters, parameters_to_ndarrays, ) -from flwr.common.differential_privacy import add_gaussian_noise, get_norm +from flwr.common.differential_privacy import add_gaussian_noise, clip_inputs, get_norm from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.strategy.strategy import Strategy class DPStrategyWrapperServerSideFixedClipping(Strategy): - """Wrapper for Configuring a Strategy for Central DP. - - The clipping is at the server side. + """Wrapper for Configuring a Strategy for Central DP with Server Side Fixed + Clipping. Parameters ---------- @@ -50,8 +49,8 @@ class DPStrategyWrapperServerSideFixedClipping(Strategy): noise_multiplier: float The noise multiplier for the Gaussian mechanism for model updates. A value of 1.0 or higher is recommended for strong privacy. - clipping_threshold: float - The value of the clipping threshold. + clipping_norm: float + The value of the clipping norm. num_sampled_clients: int The number of clients that are sampled on each round. """ @@ -61,7 +60,7 @@ def __init__( self, strategy: Strategy, noise_multiplier: float, - clipping_threshold: float, + clipping_norm: float, num_sampled_clients: int, ) -> None: super().__init__() @@ -71,14 +70,14 @@ def __init__( if noise_multiplier < 0: raise Exception("The noise multiplier should be a non-negative value.") - if clipping_threshold <= 0: + if clipping_norm <= 0: raise Exception("The clipping threshold should be a positive value.") if num_sampled_clients <= 0: raise Exception("The number of sampled clients should be a positive value.") self.noise_multiplier = noise_multiplier - self.clipping_threshold = clipping_threshold + self.clipping_norm = clipping_norm self.num_sampled_clients = num_sampled_clients self.current_round_params: NDArrays = [] @@ -129,7 +128,7 @@ def aggregate_fit( # Clip updates for client_update in all_clients_updates: - client_update = self._clip_model_update(client_update) + client_update = clip_inputs(client_update, self.clipping_norm) # Compute the new parameters with the clipped updates for client_param, client_update in zip(clients_params, all_clients_updates): @@ -145,6 +144,7 @@ def aggregate_fit( ) # Add Gaussian noise to the aggregated parameters + if aggregated_updates: aggregated_updates = self._add_noise_to_updates(aggregated_updates) @@ -165,24 +165,14 @@ def evaluate( """Evaluate model parameters using an evaluation function from the strategy.""" return self.strategy.evaluate(server_round, parameters) - def _clip_model_update(self, update: NDArrays) -> NDArrays: - """Clip model update based on the computed clipping_threshold. - - FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf - """ - update_norm = get_norm(update) - scaling_factor = min(1, self.clipping_threshold / update_norm) - update_clipped: NDArrays = [layer * scaling_factor for layer in update] - return update_clipped - def _add_noise_to_updates(self, parameters: Parameters) -> Parameters: """Add Gaussian noise to model params.""" return ndarrays_to_parameters( add_gaussian_noise( parameters_to_ndarrays(parameters), float( - (self.noise_multiplier * self.clipping_threshold) - / self.num_sampled_clients ** (0.5) + (self.noise_multiplier * self.clipping_norm) + / self.num_sampled_clients ), ) ) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 28e4c7382384..14b822e53297 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -39,48 +39,3 @@ def test_add_noise_to_updates() -> None: for layer in result: assert layer.shape == parameters[0].shape # Check shape consistency assert not np.array_equal(layer, parameters[0]) # Check if noise was added - - -def test_get_update_norm() -> None: - """Test get_update_norm function.""" - # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) - update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] - - # Execute - result = dp_wrapper.get_update_norm(update) - - expected = float( - np.linalg.norm(np.concatenate([sub_update.flatten() for sub_update in update])) - ) - - # Assert - assert expected == result - - -def test_clip_model_updates() -> None: - """Test _clip_model_updates method.""" - # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) - - updates = [ - np.array([[1.5, -0.5], [2.0, -1.0]]), - np.array([0.5, -0.5]), - np.array([[-0.5, 1.5], [-1.0, 2.0]]), - np.array([-0.5, 0.5]), - ] - - # Execute - # pylint: disable-next=protected-access - clipped_updates = dp_wrapper._clip_model_update(updates) - - # Assert - assert len(clipped_updates) == len(updates) - - for clipped_update, original_update in zip(clipped_updates, updates): - clip_norm = np.linalg.norm(original_update) - assert np.all(clipped_update <= clip_norm) and np.all( - clipped_update >= -clip_norm - ) From 93d8d75190669731a5b1a3d08652d1d5f54ff146 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 25 Jan 2024 22:35:37 +0000 Subject: [PATCH 06/45] Fix tests --- src/py/flwr/common/differential_privacy_test.py | 8 ++++++-- .../server/strategy/dp_strategy_wrapper_fixed_clipping.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index 9d997d1a790b..1732d4656ec8 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -14,6 +14,10 @@ # ============================================================================== """DP utility functions tests.""" +import numpy as np + +from .differential_privacy import add_gaussian_noise, clip_inputs, get_norm + def test_add_gaussian_noise() -> None: """Test add_gaussian_noise function.""" @@ -39,7 +43,7 @@ def test_add_gaussian_noise() -> None: assert np.any(np.abs(noise_added) > 0) -def test_get_update_norm() -> None: +def test_get_norm() -> None: """Test get_norm function.""" # Prepare update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] @@ -55,7 +59,7 @@ def test_get_update_norm() -> None: assert expected == result -def test_clip_model_updates() -> None: +def test_clip_inputs() -> None: """Test clip_inputs function.""" # Prepare updates = [ diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 4d4441dbd2f6..276f27f21241 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -32,7 +32,7 @@ ndarrays_to_parameters, parameters_to_ndarrays, ) -from flwr.common.differential_privacy import add_gaussian_noise, clip_inputs, get_norm +from flwr.common.differential_privacy import add_gaussian_noise, clip_inputs from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.strategy.strategy import Strategy From 464c3e9c1add108adb9a7c040a7273314eca1b57 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 25 Jan 2024 22:52:13 +0000 Subject: [PATCH 07/45] Fix --- .../server/strategy/dp_strategy_wrapper_fixed_clipping.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 276f27f21241..047db6233a84 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -68,13 +68,15 @@ def __init__( self.strategy = strategy if noise_multiplier < 0: - raise Exception("The noise multiplier should be a non-negative value.") + raise ValueError("The noise multiplier should be a non-negative value.") if clipping_norm <= 0: - raise Exception("The clipping threshold should be a positive value.") + raise ValueError("The clipping threshold should be a positive value.") if num_sampled_clients <= 0: - raise Exception("The number of sampled clients should be a positive value.") + raise ValueError( + "The number of sampled clients should be a positive value." + ) self.noise_multiplier = noise_multiplier self.clipping_norm = clipping_norm From bade827c7682aa4aae8fefebd4ebc57e1d433515 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 25 Jan 2024 23:01:07 +0000 Subject: [PATCH 08/45] Fix test error --- src/py/flwr/common/differential_privacy.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index f231067304e4..def8212bb70c 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -17,29 +17,29 @@ from flwr.common.typing import NDArrays -def get_norm(input: NDArrays) -> float: +def get_norm(input_array: NDArrays) -> float: """Compute the L2 norm of the flattened input.""" flattened_input = np.concatenate( - [np.asarray(sub_input).flatten() for sub_input in input] + [np.asarray(sub_input).flatten() for sub_input in input_array] ) return float(np.linalg.norm(flattened_input)) -def add_gaussian_noise(input: NDArrays, std_dev: float) -> NDArrays: +def add_gaussian_noise(input_array: NDArrays, std_dev: float) -> NDArrays: """Add noise to each element of the provided input from Gaussian (Normal) distribution with respect to the passed standard deviation.""" noised_input = [ - layer + np.random.normal(0, std_dev**2, layer.shape) for layer in input + layer + np.random.normal(0, std_dev**2, layer.shape) for layer in input_array ] return noised_input -def clip_inputs(input: NDArrays, clipping_norm: float) -> NDArrays: +def clip_inputs(input_array: NDArrays, clipping_norm: float) -> NDArrays: """Clip model update based on the clipping norm. FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf """ - input_norm = get_norm(input) + input_norm = get_norm(input_array) scaling_factor = min(1, clipping_norm / input_norm) - clipped_inputs: NDArrays = [layer * scaling_factor for layer in input] + clipped_inputs: NDArrays = [layer * scaling_factor for layer in input_array] return clipped_inputs From 800eac62a47324eaad05e8b825beb89478d1b4c1 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 25 Jan 2024 23:25:59 +0000 Subject: [PATCH 09/45] Clean code --- .../dp_strategy_wrapper_fixed_clipping.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 047db6233a84..236dcebdea55 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -116,7 +116,11 @@ def aggregate_fit( results: List[Tuple[ClientProxy, FitRes]], failures: List[Union[Tuple[ClientProxy, FitRes], BaseException]], ) -> Tuple[Optional[Parameters], Dict[str, Scalar]]: - """Aggregate training results using unweighted aggregation.""" + """Compute the updates, clip them, and pass them to the child strategy for + aggregation. + + Afterward, add noise to the aggregated parameters. + """ if failures: return None, {} @@ -141,16 +145,15 @@ def aggregate_fit( res[1].parameters = ndarrays_to_parameters(params) # Pass the new parameters for aggregation - aggregated_updates, metrics = self.strategy.aggregate_fit( + aggregated_params, metrics = self.strategy.aggregate_fit( server_round, results, failures ) # Add Gaussian noise to the aggregated parameters + if aggregated_params: + aggregated_params = self._add_noise_to_updates(aggregated_params) - if aggregated_updates: - aggregated_updates = self._add_noise_to_updates(aggregated_updates) - - return aggregated_updates, metrics + return aggregated_params, metrics def aggregate_evaluate( self, From a257d7f9a71936cfba8e2d387bf6f155ca16a9b6 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 26 Jan 2024 10:29:14 +0000 Subject: [PATCH 10/45] Clean code --- .../dp_strategy_wrapper_fixed_clipping.py | 3 + ...dp_strategy_wrapper_fixed_clipping_test.py | 73 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 236dcebdea55..38f4f3835e5c 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -185,6 +185,8 @@ def _add_noise_to_updates(self, parameters: Parameters) -> Parameters: def _compute_model_updates( self, all_clients_params: List[NDArrays] ) -> List[NDArrays]: + """Compute model updates for each client based on the current round + parameters.""" all_client_updates = [] for client_param in all_clients_params: client_update = [ @@ -197,5 +199,6 @@ def _compute_model_updates( def _update_clients_params( self, client_param: NDArrays, client_update: NDArrays ) -> None: + """Update the client parameters based on the model updates.""" for i, _ in enumerate(self.current_round_params): client_param[i] = self.current_round_params[i] + client_update[i] diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 14b822e53297..12cd148b8f96 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -39,3 +39,76 @@ def test_add_noise_to_updates() -> None: for layer in result: assert layer.shape == parameters[0].shape # Check shape consistency assert not np.array_equal(layer, parameters[0]) # Check if noise was added + + +def test_compute_model_updates() -> None: + """Test _compute_model_updates method.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + + client_params = [ + [np.array([2, 3, 4]), np.array([5, 6, 7])], + [np.array([3, 4, 5]), np.array([6, 7, 8])], + ] + current_round_params = [np.array([1, 2, 3]), np.array([4, 5, 6])] + + expected_updates = [ + [ + np.subtract(client_params[0][0], current_round_params[0]), + np.subtract(client_params[0][1], current_round_params[1]), + ], + [ + np.subtract(client_params[1][0], current_round_params[0]), + np.subtract(client_params[1][1], current_round_params[1]), + ], + ] + # Set current model parameters in the wrapper + dp_wrapper.current_round_params = current_round_params + + # Execute + computed_updates = dp_wrapper._compute_model_updates(client_params) + + for expected, actual in zip(expected_updates, computed_updates): + for e, a in zip(expected, actual): + np.testing.assert_array_equal(e, a) + + +def test_update_clients_params() -> None: + """Test _update_clients_params method.""" + # Prepare + strategy = FedAvg() + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + + client_params = [ + [np.array([2, 3, 4]), np.array([5, 6, 7])], + [np.array([3, 4, 5]), np.array([6, 7, 8])], + ] + client_update = [ + [np.array([1, 1, 1]), np.array([1, 1, 1])], + [np.array([1, 1, 1]), np.array([1, 1, 1])], + ] + current_round_params = [np.array([1, 2, 3]), np.array([4, 5, 6])] + + # Set current model parameters in the wrapper + dp_wrapper.current_round_params = current_round_params + + # Execute + for params, update in zip(client_params, client_update): + dp_wrapper._update_clients_params(params, update) + + # Assert + expected_params = [ + [ + np.add(current_round_params[0], client_update[0][0]), + np.add(current_round_params[1], client_update[0][1]), + ], + [ + np.add(current_round_params[0], client_update[1][0]), + np.add(current_round_params[1], client_update[1][1]), + ], + ] + + for expected, actual in zip(expected_params, client_params): + for e, a in zip(expected, actual): + np.testing.assert_array_equal(e, a) From d1d3ff5be27e95b7679d5ff0c0cfa4a68def0a5b Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 26 Jan 2024 10:47:57 +0000 Subject: [PATCH 11/45] Fix pylint error --- .../server/strategy/dp_strategy_wrapper_fixed_clipping_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 12cd148b8f96..b423ee6d991f 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -41,6 +41,7 @@ def test_add_noise_to_updates() -> None: assert not np.array_equal(layer, parameters[0]) # Check if noise was added +# pylint: disable-next=protected-access def test_compute_model_updates() -> None: """Test _compute_model_updates method.""" # Prepare @@ -74,6 +75,7 @@ def test_compute_model_updates() -> None: np.testing.assert_array_equal(e, a) +# pylint: disable-next=protected-access def test_update_clients_params() -> None: """Test _update_clients_params method.""" # Prepare From d24de45a7103cb9de6f8a9fca26aff8cdd2fffcd Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 26 Jan 2024 10:53:12 +0000 Subject: [PATCH 12/45] Fix pylint error --- .../strategy/dp_strategy_wrapper_fixed_clipping_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index b423ee6d991f..5a3f7837310c 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -41,7 +41,6 @@ def test_add_noise_to_updates() -> None: assert not np.array_equal(layer, parameters[0]) # Check if noise was added -# pylint: disable-next=protected-access def test_compute_model_updates() -> None: """Test _compute_model_updates method.""" # Prepare @@ -68,6 +67,7 @@ def test_compute_model_updates() -> None: dp_wrapper.current_round_params = current_round_params # Execute + # pylint: disable-next=protected-access computed_updates = dp_wrapper._compute_model_updates(client_params) for expected, actual in zip(expected_updates, computed_updates): @@ -75,7 +75,6 @@ def test_compute_model_updates() -> None: np.testing.assert_array_equal(e, a) -# pylint: disable-next=protected-access def test_update_clients_params() -> None: """Test _update_clients_params method.""" # Prepare @@ -97,6 +96,7 @@ def test_update_clients_params() -> None: # Execute for params, update in zip(client_params, client_update): + # pylint: disable-next=protected-access dp_wrapper._update_clients_params(params, update) # Assert From ba7a00cd3b9cd6e8836864c45c6e9f28c4ad6bdc Mon Sep 17 00:00:00 2001 From: mohammadnaseri Date: Fri, 26 Jan 2024 16:22:51 +0000 Subject: [PATCH 13/45] Add minor comment --- .../flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 38f4f3835e5c..98157553f8c9 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""Central DP. +"""Central differential privacy with fixed clipping. Papers: https://arxiv.org/pdf/1712.07557.pdf, https://arxiv.org/pdf/1710.06963.pdf -Note: unlike the above papers, we moved the clipping part to the server side. """ from typing import Dict, List, Optional, Tuple, Union From 6cb406c4271f815d1d0aee2a5667326d10a8f2c3 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Mon, 29 Jan 2024 15:08:36 +0000 Subject: [PATCH 14/45] Minor fix --- .../server/strategy/dp_strategy_wrapper_fixed_clipping.py | 4 ++-- .../strategy/dp_strategy_wrapper_fixed_clipping_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 98157553f8c9..31ea72fc209c 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -150,7 +150,7 @@ def aggregate_fit( # Add Gaussian noise to the aggregated parameters if aggregated_params: - aggregated_params = self._add_noise_to_updates(aggregated_params) + aggregated_params = self._add_noise_to_params(aggregated_params) return aggregated_params, metrics @@ -169,7 +169,7 @@ def evaluate( """Evaluate model parameters using an evaluation function from the strategy.""" return self.strategy.evaluate(server_round, parameters) - def _add_noise_to_updates(self, parameters: Parameters) -> Parameters: + def _add_noise_to_params(self, parameters: Parameters) -> Parameters: """Add Gaussian noise to model params.""" return ndarrays_to_parameters( add_gaussian_noise( diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 5a3f7837310c..8a7176197f2d 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -22,8 +22,8 @@ from .fedavg import FedAvg -def test_add_noise_to_updates() -> None: - """Test _add_noise_to_updates method.""" +def test_add_noise_to_params() -> None: + """Test _add_noise_to_params method.""" # Prepare strategy = FedAvg() dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) @@ -32,7 +32,7 @@ def test_add_noise_to_updates() -> None: # Execute result = parameters_to_ndarrays( # pylint: disable-next=protected-access - dp_wrapper._add_noise_to_updates(ndarrays_to_parameters(parameters)) + dp_wrapper._add_noise_to_params(ndarrays_to_parameters(parameters)) ) # Assert From 27f4b634e978d6cd189e848d307c02a1575a0c1d Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 30 Jan 2024 11:05:34 +0000 Subject: [PATCH 15/45] Fix noise: --- src/py/flwr/common/differential_privacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index def8212bb70c..99bae9aee740 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -29,7 +29,7 @@ def add_gaussian_noise(input_array: NDArrays, std_dev: float) -> NDArrays: """Add noise to each element of the provided input from Gaussian (Normal) distribution with respect to the passed standard deviation.""" noised_input = [ - layer + np.random.normal(0, std_dev**2, layer.shape) for layer in input_array + layer + np.random.normal(0, std_dev, layer.shape) for layer in input_array ] return noised_input From bacde18a455b98cca01946e2338cd96056f9e37b Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 30 Jan 2024 17:30:06 +0000 Subject: [PATCH 16/45] Refactor --- src/py/flwr/common/differential_privacy.py | 27 +++++++++- .../flwr/common/differential_privacy_test.py | 52 ++++++++++++++++++- .../dp_strategy_wrapper_fixed_clipping.py | 25 ++++----- ...dp_strategy_wrapper_fixed_clipping_test.py | 21 -------- 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 99bae9aee740..c950536cca9b 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -14,7 +14,12 @@ import numpy as np -from flwr.common.typing import NDArrays +from flwr.common import ( + NDArrays, + Parameters, + ndarrays_to_parameters, + parameters_to_ndarrays, +) def get_norm(input_array: NDArrays) -> float: @@ -43,3 +48,23 @@ def clip_inputs(input_array: NDArrays, clipping_norm: float) -> NDArrays: scaling_factor = min(1, clipping_norm / input_norm) clipped_inputs: NDArrays = [layer * scaling_factor for layer in input_array] return clipped_inputs + + +def add_noise_to_params(parameters: Parameters, stdv: float) -> Parameters: + """Add Gaussian noise to model params.""" + return ndarrays_to_parameters( + add_gaussian_noise( + parameters_to_ndarrays(parameters), + stdv, + ) + ) + + +def compute_stdv( + noise_multiplier: float, clipping_norm: float, num_sampled_clients: int +): + """Compute standard deviation for noise addition. + + paper: https://arxiv.org/pdf/1710.06963.pdf + """ + return float((noise_multiplier * clipping_norm) / num_sampled_clients) diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index 1732d4656ec8..da0114da333b 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -16,7 +16,15 @@ import numpy as np -from .differential_privacy import add_gaussian_noise, clip_inputs, get_norm +from flwr.common import Parameters, ndarrays_to_parameters, parameters_to_ndarrays + +from .differential_privacy import ( + add_gaussian_noise, + add_noise_to_params, + clip_inputs, + compute_stdv, + get_norm, +) def test_add_gaussian_noise() -> None: @@ -81,3 +89,45 @@ def test_clip_inputs() -> None: assert np.all(clipped_update <= clip_norm) and np.all( clipped_update >= -clip_norm ) + + +def test_add_noise_to_params() -> None: + """Test add_noise_to_params function.""" + # Prepare + parameters = ndarrays_to_parameters( + [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + ) + std_dev = 0.1 + + # Execute + noised_parameters = add_noise_to_params(parameters, std_dev) + + original_params_list = parameters_to_ndarrays(parameters) + noised_params_list = parameters_to_ndarrays(noised_parameters) + + # Assert + assert isinstance(noised_parameters, Parameters) + + # Check the values have been changed and are not equal to the original parameters + for original_param, noised_param in zip(original_params_list, noised_params_list): + assert not np.array_equal(original_param, noised_param) + + # Check that the noise has been added + for original_param, noised_param in zip(original_params_list, noised_params_list): + noise_added = noised_param - original_param + assert np.any(np.abs(noise_added) > 0) + + +def test_compute_stdv() -> None: + """Test compute_stdv function.""" + # Prepare + noise_multiplier = 1.0 + clipping_norm = 0.5 + num_sampled_clients = 10 + + # Execute + stdv = compute_stdv(noise_multiplier, clipping_norm, num_sampled_clients) + + # Assert + expected_stdv = float((noise_multiplier * clipping_norm) / num_sampled_clients) + assert stdv == expected_stdv diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 31ea72fc209c..b9d6bf51fc98 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -31,7 +31,11 @@ ndarrays_to_parameters, parameters_to_ndarrays, ) -from flwr.common.differential_privacy import add_gaussian_noise, clip_inputs +from flwr.common.differential_privacy import ( + add_noise_to_params, + clip_inputs, + compute_stdv, +) from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.strategy.strategy import Strategy @@ -150,7 +154,12 @@ def aggregate_fit( # Add Gaussian noise to the aggregated parameters if aggregated_params: - aggregated_params = self._add_noise_to_params(aggregated_params) + aggregated_params = add_noise_to_params( + aggregated_params, + compute_stdv( + self.noise_multiplier, self.clipping_norm, self.num_sampled_clients + ), + ) return aggregated_params, metrics @@ -169,18 +178,6 @@ def evaluate( """Evaluate model parameters using an evaluation function from the strategy.""" return self.strategy.evaluate(server_round, parameters) - def _add_noise_to_params(self, parameters: Parameters) -> Parameters: - """Add Gaussian noise to model params.""" - return ndarrays_to_parameters( - add_gaussian_noise( - parameters_to_ndarrays(parameters), - float( - (self.noise_multiplier * self.clipping_norm) - / self.num_sampled_clients - ), - ) - ) - def _compute_model_updates( self, all_clients_params: List[NDArrays] ) -> List[NDArrays]: diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 8a7176197f2d..5fea0e056360 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -16,31 +16,10 @@ import numpy as np -from flwr.common import ndarrays_to_parameters, parameters_to_ndarrays - from .dp_strategy_wrapper_fixed_clipping import DPStrategyWrapperServerSideFixedClipping from .fedavg import FedAvg -def test_add_noise_to_params() -> None: - """Test _add_noise_to_params method.""" - # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) - parameters = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] - - # Execute - result = parameters_to_ndarrays( - # pylint: disable-next=protected-access - dp_wrapper._add_noise_to_params(ndarrays_to_parameters(parameters)) - ) - - # Assert - for layer in result: - assert layer.shape == parameters[0].shape # Check shape consistency - assert not np.array_equal(layer, parameters[0]) # Check if noise was added - - def test_compute_model_updates() -> None: """Test _compute_model_updates method.""" # Prepare From e70d9fef5789c82c9d5a5c1f80fdb4c97bd17630 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 30 Jan 2024 17:34:18 +0000 Subject: [PATCH 17/45] Fix test --- src/py/flwr/common/differential_privacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index c950536cca9b..e7b3a0d8e763 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -62,7 +62,7 @@ def add_noise_to_params(parameters: Parameters, stdv: float) -> Parameters: def compute_stdv( noise_multiplier: float, clipping_norm: float, num_sampled_clients: int -): +) -> float: """Compute standard deviation for noise addition. paper: https://arxiv.org/pdf/1710.06963.pdf From ce8b4864a5f197da9916ac5b2817387ab17082fc Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 31 Jan 2024 16:55:29 +0000 Subject: [PATCH 18/45] Address comments --- src/py/flwr/common/differential_privacy.py | 42 +++++----- .../flwr/common/differential_privacy_test.py | 76 ++++++------------- .../dp_strategy_wrapper_fixed_clipping.py | 27 ++++--- ...dp_strategy_wrapper_fixed_clipping_test.py | 20 +++-- 4 files changed, 69 insertions(+), 96 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index e7b3a0d8e763..15ca8a418bc4 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -14,29 +14,22 @@ import numpy as np -from flwr.common import ( - NDArrays, - Parameters, - ndarrays_to_parameters, - parameters_to_ndarrays, -) +from flwr.common import NDArrays -def get_norm(input_array: NDArrays) -> float: +def get_norm(input_arrays: NDArrays) -> float: """Compute the L2 norm of the flattened input.""" - flattened_input = np.concatenate( - [np.asarray(sub_input).flatten() for sub_input in input_array] - ) - return float(np.linalg.norm(flattened_input)) + array_norms = [np.linalg.norm(array.flat) for array in input_arrays] + return float(np.sqrt(sum([norm**2 for norm in array_norms]))) -def add_gaussian_noise(input_array: NDArrays, std_dev: float) -> NDArrays: +def add_gaussian_noise_inplace(input_array: NDArrays, std_dev: float) -> None: """Add noise to each element of the provided input from Gaussian (Normal) distribution with respect to the passed standard deviation.""" - noised_input = [ - layer + np.random.normal(0, std_dev, layer.shape) for layer in input_array - ] - return noised_input + for i in range(len(input_array)): + input_array[i] += np.random.normal(0, std_dev, input_array[i].shape).astype( + input_array[i].dtype + ) def clip_inputs(input_array: NDArrays, clipping_norm: float) -> NDArrays: @@ -50,14 +43,15 @@ def clip_inputs(input_array: NDArrays, clipping_norm: float) -> NDArrays: return clipped_inputs -def add_noise_to_params(parameters: Parameters, stdv: float) -> Parameters: - """Add Gaussian noise to model params.""" - return ndarrays_to_parameters( - add_gaussian_noise( - parameters_to_ndarrays(parameters), - stdv, - ) - ) +def clip_inputs_inplace(input_array: NDArrays, clipping_norm: float) -> None: + """Clip model update based on the clipping norm in-place. + + FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf + """ + input_norm = get_norm(input_array) + scaling_factor = min(1, clipping_norm / input_norm) + for i in range(len(input_array)): + input_array[i] *= scaling_factor def compute_stdv( diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index da0114da333b..d544705f9387 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -16,38 +16,41 @@ import numpy as np -from flwr.common import Parameters, ndarrays_to_parameters, parameters_to_ndarrays - from .differential_privacy import ( - add_gaussian_noise, - add_noise_to_params, - clip_inputs, + add_gaussian_noise_inplace, + clip_inputs_inplace, compute_stdv, get_norm, ) -def test_add_gaussian_noise() -> None: - """Test add_gaussian_noise function.""" +def test_add_gaussian_noise_inplace() -> None: + """Test add_gaussian_noise_inplace function.""" # Prepare - update = [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] + update = [np.array([[1.0, 2.0], [3.0, 4.0]]), np.array([[5.0, 6.0], [7.0, 8.0]])] std_dev = 0.1 # Execute - update_noised = add_gaussian_noise(update, std_dev) + add_gaussian_noise_inplace(update, std_dev) # Assert # Check that the shape of the result is the same as the input - for layer, layer_noised in zip(update, update_noised): - assert layer.shape == layer_noised.shape + for layer in update: + assert layer.shape == (2, 2) # Check that the values have been changed and are not equal to the original update - for layer, layer_noised in zip(update, update_noised): - assert not np.array_equal(layer, layer_noised) + for layer in update: + assert not np.array_equal( + layer, [[1.0, 2.0], [3.0, 4.0]] + ) and not np.array_equal(layer, [[5.0, 6.0], [7.0, 8.0]]) # Check that the noise has been added - for layer, layer_noised in zip(update, update_noised): - noise_added = layer_noised - layer + for layer in update: + noise_added = ( + layer - np.array([[1.0, 2.0], [3.0, 4.0]]) + if np.array_equal(layer, [[1.0, 2.0], [3.0, 4.0]]) + else layer - np.array([[5.0, 6.0], [7.0, 8.0]]) + ) assert np.any(np.abs(noise_added) > 0) @@ -67,8 +70,8 @@ def test_get_norm() -> None: assert expected == result -def test_clip_inputs() -> None: - """Test clip_inputs function.""" +def test_clip_inputs_inplace() -> None: + """Test clip_inputs_inplace function.""" # Prepare updates = [ np.array([[1.5, -0.5], [2.0, -1.0]]), @@ -78,44 +81,15 @@ def test_clip_inputs() -> None: ] clipping_norm = 1.5 - # Execute - clipped_updates = clip_inputs(updates, clipping_norm) - - # Assert - assert len(clipped_updates) == len(updates) - - for clipped_update, original_update in zip(clipped_updates, updates): - clip_norm = np.linalg.norm(original_update) - assert np.all(clipped_update <= clip_norm) and np.all( - clipped_update >= -clip_norm - ) - - -def test_add_noise_to_params() -> None: - """Test add_noise_to_params function.""" - # Prepare - parameters = ndarrays_to_parameters( - [np.array([[1, 2], [3, 4]]), np.array([[5, 6], [7, 8]])] - ) - std_dev = 0.1 + original_updates = [np.copy(update) for update in updates] # Execute - noised_parameters = add_noise_to_params(parameters, std_dev) - - original_params_list = parameters_to_ndarrays(parameters) - noised_params_list = parameters_to_ndarrays(noised_parameters) + clip_inputs_inplace(updates, clipping_norm) # Assert - assert isinstance(noised_parameters, Parameters) - - # Check the values have been changed and are not equal to the original parameters - for original_param, noised_param in zip(original_params_list, noised_params_list): - assert not np.array_equal(original_param, noised_param) - - # Check that the noise has been added - for original_param, noised_param in zip(original_params_list, noised_params_list): - noise_added = noised_param - original_param - assert np.any(np.abs(noise_added) > 0) + for updated, original_update in zip(updates, original_updates): + clip_norm = np.linalg.norm(original_update) + assert np.all(updated <= clip_norm) and np.all(updated >= -clip_norm) def test_compute_stdv() -> None: diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index b9d6bf51fc98..f878b3fb175a 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -32,8 +32,8 @@ parameters_to_ndarrays, ) from flwr.common.differential_privacy import ( - add_noise_to_params, - clip_inputs, + add_gaussian_noise_inplace, + clip_inputs_inplace, compute_stdv, ) from flwr.server.client_manager import ClientManager @@ -74,7 +74,7 @@ def __init__( raise ValueError("The noise multiplier should be a non-negative value.") if clipping_norm <= 0: - raise ValueError("The clipping threshold should be a positive value.") + raise ValueError("The clipping norm should be a positive value.") if num_sampled_clients <= 0: raise ValueError( @@ -132,12 +132,8 @@ def aggregate_fit( parameters_to_ndarrays(fit_res.parameters) for _, fit_res in results ] - # Compute the updates - all_clients_updates = self._compute_model_updates(clients_params) - - # Clip updates - for client_update in all_clients_updates: - client_update = clip_inputs(client_update, self.clipping_norm) + # Compute and clip the updates + all_clients_updates = self._compute_clip_model_updates(clients_params) # Compute the new parameters with the clipped updates for client_param, client_update in zip(clients_params, all_clients_updates): @@ -154,12 +150,14 @@ def aggregate_fit( # Add Gaussian noise to the aggregated parameters if aggregated_params: - aggregated_params = add_noise_to_params( - aggregated_params, + aggregated_params_ndarrays = parameters_to_ndarrays(aggregated_params) + add_gaussian_noise_inplace( + aggregated_params_ndarrays, compute_stdv( self.noise_multiplier, self.clipping_norm, self.num_sampled_clients ), ) + aggregated_params = ndarrays_to_parameters(aggregated_params_ndarrays) return aggregated_params, metrics @@ -178,17 +176,18 @@ def evaluate( """Evaluate model parameters using an evaluation function from the strategy.""" return self.strategy.evaluate(server_round, parameters) - def _compute_model_updates( + def _compute_clip_model_updates( self, all_clients_params: List[NDArrays] ) -> List[NDArrays]: - """Compute model updates for each client based on the current round - parameters.""" + """Compute model updates for each client based on the current round parameters + and then clip it.""" all_client_updates = [] for client_param in all_clients_params: client_update = [ np.subtract(x, y) for (x, y) in zip(client_param, self.current_round_params) ] + clip_inputs_inplace(client_update, self.clipping_norm) all_client_updates.append(client_update) return all_client_updates diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 5fea0e056360..9a9ff40992d9 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -20,17 +20,23 @@ from .fedavg import FedAvg -def test_compute_model_updates() -> None: - """Test _compute_model_updates method.""" +def test_compute_clip_model_updates() -> None: + """Test _compute_clip_model_updates method.""" # Prepare strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 6, 5) + + # Ensure all arrays have the same data type + dtype = np.float64 client_params = [ - [np.array([2, 3, 4]), np.array([5, 6, 7])], - [np.array([3, 4, 5]), np.array([6, 7, 8])], + [np.array([2, 3, 4], dtype=dtype), np.array([5, 6, 7], dtype=dtype)], + [np.array([3, 4, 5], dtype=dtype), np.array([6, 7, 8], dtype=dtype)], + ] + current_round_params = [ + np.array([1, 2, 3], dtype=dtype), + np.array([4, 5, 6], dtype=dtype), ] - current_round_params = [np.array([1, 2, 3]), np.array([4, 5, 6])] expected_updates = [ [ @@ -47,7 +53,7 @@ def test_compute_model_updates() -> None: # Execute # pylint: disable-next=protected-access - computed_updates = dp_wrapper._compute_model_updates(client_params) + computed_updates = dp_wrapper._compute_clip_model_updates(client_params) for expected, actual in zip(expected_updates, computed_updates): for e, a in zip(expected, actual): From a9d75bb25538354ae1daa7f253a610e306d858de Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 31 Jan 2024 18:02:46 +0000 Subject: [PATCH 19/45] Address comments --- src/py/flwr/common/differential_privacy.py | 26 ++++++------- .../dp_strategy_wrapper_fixed_clipping.py | 27 +++++-------- ...dp_strategy_wrapper_fixed_clipping_test.py | 39 ++++++++++++------- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 15ca8a418bc4..bf900a47344d 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -20,38 +20,38 @@ def get_norm(input_arrays: NDArrays) -> float: """Compute the L2 norm of the flattened input.""" array_norms = [np.linalg.norm(array.flat) for array in input_arrays] - return float(np.sqrt(sum([norm**2 for norm in array_norms]))) + return float( + np.sqrt(sum([norm**2 for norm in array_norms])) + ) # pylint: disable=consider-using-generator -def add_gaussian_noise_inplace(input_array: NDArrays, std_dev: float) -> None: +def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None: """Add noise to each element of the provided input from Gaussian (Normal) distribution with respect to the passed standard deviation.""" - for i in range(len(input_array)): - input_array[i] += np.random.normal(0, std_dev, input_array[i].shape).astype( - input_array[i].dtype - ) + for array in input_arrays: + array += np.random.normal(0, std_dev, array.shape).astype(array.dtype) -def clip_inputs(input_array: NDArrays, clipping_norm: float) -> NDArrays: +def clip_inputs(input_arrays: NDArrays, clipping_norm: float) -> NDArrays: """Clip model update based on the clipping norm. FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf """ - input_norm = get_norm(input_array) + input_norm = get_norm(input_arrays) scaling_factor = min(1, clipping_norm / input_norm) - clipped_inputs: NDArrays = [layer * scaling_factor for layer in input_array] + clipped_inputs: NDArrays = [layer * scaling_factor for layer in input_arrays] return clipped_inputs -def clip_inputs_inplace(input_array: NDArrays, clipping_norm: float) -> None: +def clip_inputs_inplace(input_arrays: NDArrays, clipping_norm: float) -> None: """Clip model update based on the clipping norm in-place. FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf """ - input_norm = get_norm(input_array) + input_norm = get_norm(input_arrays) scaling_factor = min(1, clipping_norm / input_norm) - for i in range(len(input_array)): - input_array[i] *= scaling_factor + for array in input_arrays: + array *= scaling_factor def compute_stdv( diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index f878b3fb175a..fbbcc9ae643c 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -133,11 +133,11 @@ def aggregate_fit( ] # Compute and clip the updates - all_clients_updates = self._compute_clip_model_updates(clients_params) + self._compute_clip_model_updates(clients_params) - # Compute the new parameters with the clipped updates - for client_param, client_update in zip(clients_params, all_clients_updates): - self._update_clients_params(client_param, client_update) + # # Compute the new parameters with the clipped updates + # for client_param, client_update in zip(clients_params, all_clients_updates): + # self._update_clients_params(client_param, client_update) # Update the results with the new params for res, params in zip(results, clients_params): @@ -176,24 +176,15 @@ def evaluate( """Evaluate model parameters using an evaluation function from the strategy.""" return self.strategy.evaluate(server_round, parameters) - def _compute_clip_model_updates( - self, all_clients_params: List[NDArrays] - ) -> List[NDArrays]: - """Compute model updates for each client based on the current round parameters - and then clip it.""" - all_client_updates = [] + def _compute_clip_model_updates(self, all_clients_params: List[NDArrays]) -> None: + """Compute model updates for each client model based on the current round + parameters and then clip it.""" for client_param in all_clients_params: client_update = [ np.subtract(x, y) for (x, y) in zip(client_param, self.current_round_params) ] clip_inputs_inplace(client_update, self.clipping_norm) - all_client_updates.append(client_update) - return all_client_updates - def _update_clients_params( - self, client_param: NDArrays, client_update: NDArrays - ) -> None: - """Update the client parameters based on the model updates.""" - for i, _ in enumerate(self.current_round_params): - client_param[i] = self.current_round_params[i] + client_update[i] + for i, _ in enumerate(self.current_round_params): + client_param[i] = self.current_round_params[i] + client_update[i] diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 9a9ff40992d9..49397532745d 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -24,40 +24,51 @@ def test_compute_clip_model_updates() -> None: """Test _compute_clip_model_updates method.""" # Prepare strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 6, 5) + dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 10, 5) # Ensure all arrays have the same data type dtype = np.float64 client_params = [ - [np.array([2, 3, 4], dtype=dtype), np.array([5, 6, 7], dtype=dtype)], - [np.array([3, 4, 5], dtype=dtype), np.array([6, 7, 8], dtype=dtype)], + [ + np.array([0.5, 1.5, 2.5], dtype=np.float64), + np.array([3.5, 4.5, 5.5], dtype=np.float64), + np.array([6.5, 7.5, 8.5], dtype=np.float64), + ], + [ + np.array([1.5, 2.5, 3.5], dtype=np.float64), + np.array([4.5, 5.5, 6.5], dtype=np.float64), + np.array([7.5, 8.5, 9.5], dtype=np.float64), + ], ] current_round_params = [ - np.array([1, 2, 3], dtype=dtype), - np.array([4, 5, 6], dtype=dtype), + np.array([1.0, 2.0, 3.0], dtype=np.float64), + np.array([4.0, 5.0, 6.0], dtype=np.float64), + np.array([7.0, 8.0, 9.0], dtype=np.float64), ] expected_updates = [ [ - np.subtract(client_params[0][0], current_round_params[0]), - np.subtract(client_params[0][1], current_round_params[1]), + np.array([0.5, 1.5, 2.5], dtype=np.float64), + np.array([3.5, 4.5, 5.5], dtype=np.float64), + np.array([6.5, 7.5, 8.5], dtype=np.float64), ], [ - np.subtract(client_params[1][0], current_round_params[0]), - np.subtract(client_params[1][1], current_round_params[1]), + np.array([1.5, 2.5, 3.5], dtype=np.float64), + np.array([4.5, 5.5, 6.5], dtype=np.float64), + np.array([7.5, 8.5, 9.5], dtype=np.float64), ], ] # Set current model parameters in the wrapper dp_wrapper.current_round_params = current_round_params # Execute - # pylint: disable-next=protected-access - computed_updates = dp_wrapper._compute_clip_model_updates(client_params) + dp_wrapper._compute_clip_model_updates(client_params) - for expected, actual in zip(expected_updates, computed_updates): - for e, a in zip(expected, actual): - np.testing.assert_array_equal(e, a) + # Verify + for i, client_param in enumerate(client_params): + for j, update in enumerate(client_param): + np.testing.assert_array_almost_equal(update, expected_updates[i][j]) def test_update_clients_params() -> None: From e7ba3130ee1d6a6adb5c1fc684a43cd7ddf7ac7c Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 31 Jan 2024 18:07:45 +0000 Subject: [PATCH 20/45] Address comments --- ...dp_strategy_wrapper_fixed_clipping_test.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 49397532745d..8d8f60677a72 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -26,37 +26,34 @@ def test_compute_clip_model_updates() -> None: strategy = FedAvg() dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 10, 5) - # Ensure all arrays have the same data type - dtype = np.float64 - client_params = [ [ - np.array([0.5, 1.5, 2.5], dtype=np.float64), - np.array([3.5, 4.5, 5.5], dtype=np.float64), - np.array([6.5, 7.5, 8.5], dtype=np.float64), + np.array([0.5, 1.5, 2.5]), + np.array([3.5, 4.5, 5.5]), + np.array([6.5, 7.5, 8.5]), ], [ - np.array([1.5, 2.5, 3.5], dtype=np.float64), - np.array([4.5, 5.5, 6.5], dtype=np.float64), - np.array([7.5, 8.5, 9.5], dtype=np.float64), + np.array([1.5, 2.5, 3.5]), + np.array([4.5, 5.5, 6.5]), + np.array([7.5, 8.5, 9.5]), ], ] current_round_params = [ - np.array([1.0, 2.0, 3.0], dtype=np.float64), - np.array([4.0, 5.0, 6.0], dtype=np.float64), - np.array([7.0, 8.0, 9.0], dtype=np.float64), + np.array([1.0, 2.0, 3.0]), + np.array([4.0, 5.0, 6.0]), + np.array([7.0, 8.0, 9.0]), ] expected_updates = [ [ - np.array([0.5, 1.5, 2.5], dtype=np.float64), - np.array([3.5, 4.5, 5.5], dtype=np.float64), - np.array([6.5, 7.5, 8.5], dtype=np.float64), + np.array([0.5, 1.5, 2.5]), + np.array([3.5, 4.5, 5.5]), + np.array([6.5, 7.5, 8.5]), ], [ - np.array([1.5, 2.5, 3.5], dtype=np.float64), - np.array([4.5, 5.5, 6.5], dtype=np.float64), - np.array([7.5, 8.5, 9.5], dtype=np.float64), + np.array([1.5, 2.5, 3.5]), + np.array([4.5, 5.5, 6.5]), + np.array([7.5, 8.5, 9.5]), ], ] # Set current model parameters in the wrapper From 3efa81ef9244b19a121e653c1f4b514090d5bf50 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 31 Jan 2024 18:13:37 +0000 Subject: [PATCH 21/45] Fix errors --- src/py/flwr/common/differential_privacy.py | 2 +- .../dp_strategy_wrapper_fixed_clipping.py | 6 +-- ...dp_strategy_wrapper_fixed_clipping_test.py | 41 ------------------- 3 files changed, 2 insertions(+), 47 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index bf900a47344d..79859ad76393 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -29,7 +29,7 @@ def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None: """Add noise to each element of the provided input from Gaussian (Normal) distribution with respect to the passed standard deviation.""" for array in input_arrays: - array += np.random.normal(0, std_dev, array.shape).astype(array.dtype) + array += np.random.normal(0, std_dev, array.shape) def clip_inputs(input_arrays: NDArrays, clipping_norm: float) -> NDArrays: diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index fbbcc9ae643c..6c8e66c9c42a 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -89,7 +89,7 @@ def __init__( def __repr__(self) -> str: """Compute a string representation of the strategy.""" - rep = "DP Strategy Wrapper with Fixed Clipping" + rep = "DP Strategy Wrapper with Server Side Fixed Clipping" return rep def initialize_parameters( @@ -135,10 +135,6 @@ def aggregate_fit( # Compute and clip the updates self._compute_clip_model_updates(clients_params) - # # Compute the new parameters with the clipped updates - # for client_param, client_update in zip(clients_params, all_clients_updates): - # self._update_clients_params(client_param, client_update) - # Update the results with the new params for res, params in zip(results, clients_params): res[1].parameters = ndarrays_to_parameters(params) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 8d8f60677a72..065dedd807e6 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -66,44 +66,3 @@ def test_compute_clip_model_updates() -> None: for i, client_param in enumerate(client_params): for j, update in enumerate(client_param): np.testing.assert_array_almost_equal(update, expected_updates[i][j]) - - -def test_update_clients_params() -> None: - """Test _update_clients_params method.""" - # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 1.5, 5) - - client_params = [ - [np.array([2, 3, 4]), np.array([5, 6, 7])], - [np.array([3, 4, 5]), np.array([6, 7, 8])], - ] - client_update = [ - [np.array([1, 1, 1]), np.array([1, 1, 1])], - [np.array([1, 1, 1]), np.array([1, 1, 1])], - ] - current_round_params = [np.array([1, 2, 3]), np.array([4, 5, 6])] - - # Set current model parameters in the wrapper - dp_wrapper.current_round_params = current_round_params - - # Execute - for params, update in zip(client_params, client_update): - # pylint: disable-next=protected-access - dp_wrapper._update_clients_params(params, update) - - # Assert - expected_params = [ - [ - np.add(current_round_params[0], client_update[0][0]), - np.add(current_round_params[1], client_update[0][1]), - ], - [ - np.add(current_round_params[0], client_update[1][0]), - np.add(current_round_params[1], client_update[1][1]), - ], - ] - - for expected, actual in zip(expected_params, client_params): - for e, a in zip(expected, actual): - np.testing.assert_array_equal(e, a) From 05944f4a7a2c93333ba39b772d97bb73552c3d6c Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 31 Jan 2024 18:17:40 +0000 Subject: [PATCH 22/45] Fix errors --- src/py/flwr/common/differential_privacy.py | 5 ++--- .../strategy/dp_strategy_wrapper_fixed_clipping_test.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 79859ad76393..40fca4ef7fe2 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -20,9 +20,8 @@ def get_norm(input_arrays: NDArrays) -> float: """Compute the L2 norm of the flattened input.""" array_norms = [np.linalg.norm(array.flat) for array in input_arrays] - return float( - np.sqrt(sum([norm**2 for norm in array_norms])) - ) # pylint: disable=consider-using-generator + # pylint: disable=consider-using-generator + return float(np.sqrt(sum([norm**2 for norm in array_norms]))) def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None: diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py index 065dedd807e6..989da9919631 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py @@ -60,6 +60,7 @@ def test_compute_clip_model_updates() -> None: dp_wrapper.current_round_params = current_round_params # Execute + # pylint: disable-next=protected-access dp_wrapper._compute_clip_model_updates(client_params) # Verify From b2353364c783961290ad7d24fdb210cca8c4154d Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 1 Feb 2024 00:49:59 +0000 Subject: [PATCH 23/45] Clean code --- src/py/flwr/common/differential_privacy.py | 14 ++++ .../flwr/common/differential_privacy_test.py | 30 ++++++++ .../dp_strategy_wrapper_fixed_clipping.py | 22 ++---- ...dp_strategy_wrapper_fixed_clipping_test.py | 69 ------------------- 4 files changed, 49 insertions(+), 86 deletions(-) delete mode 100644 src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 40fca4ef7fe2..43de31355acc 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -12,6 +12,8 @@ # ============================================================================== """Utility functions for differential privacy.""" +from typing import List + import numpy as np from flwr.common import NDArrays @@ -61,3 +63,15 @@ def compute_stdv( paper: https://arxiv.org/pdf/1710.06963.pdf """ return float((noise_multiplier * clipping_norm) / num_sampled_clients) + + +def compute_clip_model_update( + param1: List[NDArrays], param2: List[NDArrays], clipping_norm: float +) -> None: + """Compute model update (param1 - param2) and clip it. + Then add the clipped value to param1.""" + model_update = [np.subtract(x, y) for (x, y) in zip(param1, param2)] + clip_inputs_inplace(model_update, clipping_norm) + + for i, _ in enumerate(param2): + param1[i] = param2[i] + model_update[i] diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index d544705f9387..2232cdfe2e54 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -19,6 +19,7 @@ from .differential_privacy import ( add_gaussian_noise_inplace, clip_inputs_inplace, + compute_clip_model_update, compute_stdv, get_norm, ) @@ -105,3 +106,32 @@ def test_compute_stdv() -> None: # Assert expected_stdv = float((noise_multiplier * clipping_norm) / num_sampled_clients) assert stdv == expected_stdv + + +def test_compute_clip_model_update() -> None: + """Test compute_clip_model_update function.""" + # Prepare + param1 = [ + np.array([0.5, 1.5, 2.5]), + np.array([3.5, 4.5, 5.5]), + np.array([6.5, 7.5, 8.5]), + ] + param2 = [ + np.array([1.0, 2.0, 3.0]), + np.array([4.0, 5.0, 6.0]), + np.array([7.0, 8.0, 9.0]), + ] + clipping_norm = 4 + + expected_result = [ + np.array([0.5, 1.5, 2.5]), + np.array([3.5, 4.5, 5.5]), + np.array([6.5, 7.5, 8.5]), + ] + + # Execute + compute_clip_model_update(param1, param2, clipping_norm) + + # Verify + for i, param in enumerate(param1): + np.testing.assert_array_almost_equal(param, expected_result[i]) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 6c8e66c9c42a..3373407aa610 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -18,8 +18,6 @@ """ from typing import Dict, List, Optional, Tuple, Union -import numpy as np - from flwr.common import ( EvaluateIns, EvaluateRes, @@ -33,7 +31,7 @@ ) from flwr.common.differential_privacy import ( add_gaussian_noise_inplace, - clip_inputs_inplace, + compute_clip_model_update, compute_stdv, ) from flwr.server.client_manager import ClientManager @@ -133,7 +131,10 @@ def aggregate_fit( ] # Compute and clip the updates - self._compute_clip_model_updates(clients_params) + for client_param in clients_params: + compute_clip_model_update( + client_param, self.current_round_params, self.clipping_norm + ) # Update the results with the new params for res, params in zip(results, clients_params): @@ -171,16 +172,3 @@ def evaluate( ) -> Optional[Tuple[float, Dict[str, Scalar]]]: """Evaluate model parameters using an evaluation function from the strategy.""" return self.strategy.evaluate(server_round, parameters) - - def _compute_clip_model_updates(self, all_clients_params: List[NDArrays]) -> None: - """Compute model updates for each client model based on the current round - parameters and then clip it.""" - for client_param in all_clients_params: - client_update = [ - np.subtract(x, y) - for (x, y) in zip(client_param, self.current_round_params) - ] - clip_inputs_inplace(client_update, self.clipping_norm) - - for i, _ in enumerate(self.current_round_params): - client_param[i] = self.current_round_params[i] + client_update[i] diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py deleted file mode 100644 index 989da9919631..000000000000 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping_test.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2020 Flower Labs GmbH. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================== -"""dp_strategy_wrapper_fixed_clipping tests.""" - -import numpy as np - -from .dp_strategy_wrapper_fixed_clipping import DPStrategyWrapperServerSideFixedClipping -from .fedavg import FedAvg - - -def test_compute_clip_model_updates() -> None: - """Test _compute_clip_model_updates method.""" - # Prepare - strategy = FedAvg() - dp_wrapper = DPStrategyWrapperServerSideFixedClipping(strategy, 1.5, 10, 5) - - client_params = [ - [ - np.array([0.5, 1.5, 2.5]), - np.array([3.5, 4.5, 5.5]), - np.array([6.5, 7.5, 8.5]), - ], - [ - np.array([1.5, 2.5, 3.5]), - np.array([4.5, 5.5, 6.5]), - np.array([7.5, 8.5, 9.5]), - ], - ] - current_round_params = [ - np.array([1.0, 2.0, 3.0]), - np.array([4.0, 5.0, 6.0]), - np.array([7.0, 8.0, 9.0]), - ] - - expected_updates = [ - [ - np.array([0.5, 1.5, 2.5]), - np.array([3.5, 4.5, 5.5]), - np.array([6.5, 7.5, 8.5]), - ], - [ - np.array([1.5, 2.5, 3.5]), - np.array([4.5, 5.5, 6.5]), - np.array([7.5, 8.5, 9.5]), - ], - ] - # Set current model parameters in the wrapper - dp_wrapper.current_round_params = current_round_params - - # Execute - # pylint: disable-next=protected-access - dp_wrapper._compute_clip_model_updates(client_params) - - # Verify - for i, client_param in enumerate(client_params): - for j, update in enumerate(client_param): - np.testing.assert_array_almost_equal(update, expected_updates[i][j]) From 8cde98e5ac98861b8fda2d9dab89a549117d8e30 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 1 Feb 2024 00:58:41 +0000 Subject: [PATCH 24/45] Fix errors --- src/py/flwr/common/differential_privacy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 43de31355acc..57d1136bc284 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -12,7 +12,6 @@ # ============================================================================== """Utility functions for differential privacy.""" -from typing import List import numpy as np @@ -66,7 +65,7 @@ def compute_stdv( def compute_clip_model_update( - param1: List[NDArrays], param2: List[NDArrays], clipping_norm: float + param1: NDArrays, param2: NDArrays, clipping_norm: float ) -> None: """Compute model update (param1 - param2) and clip it. Then add the clipped value to param1.""" From 469dfca347c921e4fe8e28161f80c93c20d5d79f Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 2 Feb 2024 12:01:30 +0000 Subject: [PATCH 25/45] Clean code --- src/py/flwr/common/differential_privacy.py | 22 ++++++++++++++++++- .../dp_strategy_wrapper_fixed_clipping.py | 15 +++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 57d1136bc284..d8f193fc4201 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -15,7 +15,12 @@ import numpy as np -from flwr.common import NDArrays +from flwr.common import ( + NDArrays, + Parameters, + ndarrays_to_parameters, + parameters_to_ndarrays, +) def get_norm(input_arrays: NDArrays) -> float: @@ -74,3 +79,18 @@ def compute_clip_model_update( for i, _ in enumerate(param2): param1[i] = param2[i] + model_update[i] + + +def add_gaussian_to_params( + model_params: Parameters, + noise_multiplier: float, + clipping_norm: float, + num_sampled_clients: int, +) -> Parameters: + """Add gaussian noise to model parameters.""" + model_params_ndarrays = parameters_to_ndarrays(model_params) + add_gaussian_noise_inplace( + model_params_ndarrays, + compute_stdv(noise_multiplier, clipping_norm, num_sampled_clients), + ) + return ndarrays_to_parameters(model_params_ndarrays) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 3373407aa610..a3e8e4ae8826 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -30,9 +30,8 @@ parameters_to_ndarrays, ) from flwr.common.differential_privacy import ( - add_gaussian_noise_inplace, + add_gaussian_to_params, compute_clip_model_update, - compute_stdv, ) from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy @@ -147,14 +146,12 @@ def aggregate_fit( # Add Gaussian noise to the aggregated parameters if aggregated_params: - aggregated_params_ndarrays = parameters_to_ndarrays(aggregated_params) - add_gaussian_noise_inplace( - aggregated_params_ndarrays, - compute_stdv( - self.noise_multiplier, self.clipping_norm, self.num_sampled_clients - ), + aggregated_params = add_gaussian_to_params( + aggregated_params, + self.noise_multiplier, + self.clipping_norm, + self.num_sampled_clients, ) - aggregated_params = ndarrays_to_parameters(aggregated_params_ndarrays) return aggregated_params, metrics From c49180b34476ba12479439594ae2e8cf52c232c3 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Sat, 3 Feb 2024 23:12:24 +0000 Subject: [PATCH 26/45] Clean code --- .../strategy/dp_strategy_wrapper_fixed_clipping.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index a3e8e4ae8826..a32e92e79df8 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -139,6 +139,15 @@ def aggregate_fit( for res, params in zip(results, clients_params): res[1].parameters = ndarrays_to_parameters(params) + for _, res in results: + param = parameters_to_ndarrays(res.parameters) + # compute and clip update + compute_clip_model_update( + param, self.current_round_params, self.clipping_norm + ) + # convert back to parameters + res.parameters = ndarrays_to_parameters(param) + # Pass the new parameters for aggregation aggregated_params, metrics = self.strategy.aggregate_fit( server_round, results, failures From bbe5c5be38173da19af383b71c2b5680e8979266 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Sun, 4 Feb 2024 15:24:10 +0000 Subject: [PATCH 27/45] Improve --- .../dp_strategy_wrapper_fixed_clipping.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index a32e92e79df8..f4b17d3ca3f2 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -124,28 +124,13 @@ def aggregate_fit( if failures: return None, {} - # Extract all clients' model params - clients_params = [ - parameters_to_ndarrays(fit_res.parameters) for _, fit_res in results - ] - - # Compute and clip the updates - for client_param in clients_params: - compute_clip_model_update( - client_param, self.current_round_params, self.clipping_norm - ) - - # Update the results with the new params - for res, params in zip(results, clients_params): - res[1].parameters = ndarrays_to_parameters(params) - for _, res in results: param = parameters_to_ndarrays(res.parameters) - # compute and clip update + # Compute and clip update compute_clip_model_update( param, self.current_round_params, self.clipping_norm ) - # convert back to parameters + # Convert back to parameters res.parameters = ndarrays_to_parameters(param) # Pass the new parameters for aggregation From 656f180e4c5b1004a6f497fef91f98c04e351784 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 7 Feb 2024 10:59:09 +0000 Subject: [PATCH 28/45] Remove function --- src/py/flwr/common/differential_privacy.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index d8f193fc4201..1db8854f5b80 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -37,17 +37,6 @@ def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None: array += np.random.normal(0, std_dev, array.shape) -def clip_inputs(input_arrays: NDArrays, clipping_norm: float) -> NDArrays: - """Clip model update based on the clipping norm. - - FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf - """ - input_norm = get_norm(input_arrays) - scaling_factor = min(1, clipping_norm / input_norm) - clipped_inputs: NDArrays = [layer * scaling_factor for layer in input_arrays] - return clipped_inputs - - def clip_inputs_inplace(input_arrays: NDArrays, clipping_norm: float) -> None: """Clip model update based on the clipping norm in-place. From 46a6372b747bd58104cc4ebf565060378ec08ceb Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 9 Feb 2024 16:53:34 +0000 Subject: [PATCH 29/45] Add warning --- src/py/flwr/common/differential_privacy.py | 2 +- .../strategy/dp_strategy_wrapper_fixed_clipping.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 1db8854f5b80..a070de6b8170 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -70,7 +70,7 @@ def compute_clip_model_update( param1[i] = param2[i] + model_update[i] -def add_gaussian_to_params( +def add_gaussian_noise_to_params( model_params: Parameters, noise_multiplier: float, clipping_norm: float, diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index f4b17d3ca3f2..df263b95cd2d 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -16,6 +16,7 @@ Papers: https://arxiv.org/pdf/1712.07557.pdf, https://arxiv.org/pdf/1710.06963.pdf """ +import warnings from typing import Dict, List, Optional, Tuple, Union from flwr.common import ( @@ -30,7 +31,7 @@ parameters_to_ndarrays, ) from flwr.common.differential_privacy import ( - add_gaussian_to_params, + add_gaussian_noise_to_params, compute_clip_model_update, ) from flwr.server.client_manager import ClientManager @@ -124,6 +125,14 @@ def aggregate_fit( if failures: return None, {} + if len(results) != self.num_sampled_clients: + warnings.warn( + f"The number of clients returning parameters ({len(results)}) differs from " + f"the number of sampled clients ({self.num_sampled_clients}). This could impact " + f"the differential privacy guarantees of the system, potentially leading to privacy leakage " + f"or inadequate noise calibration.", + stacklevel=2, + ) for _, res in results: param = parameters_to_ndarrays(res.parameters) # Compute and clip update @@ -140,7 +149,7 @@ def aggregate_fit( # Add Gaussian noise to the aggregated parameters if aggregated_params: - aggregated_params = add_gaussian_to_params( + aggregated_params = add_gaussian_noise_to_params( aggregated_params, self.noise_multiplier, self.clipping_norm, From 510d6c63dd010b96618e883de79c736de98118fd Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 9 Feb 2024 17:00:59 +0000 Subject: [PATCH 30/45] Fix error --- .../server/strategy/dp_strategy_wrapper_fixed_clipping.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index df263b95cd2d..09f5c89ca140 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -127,10 +127,10 @@ def aggregate_fit( if len(results) != self.num_sampled_clients: warnings.warn( - f"The number of clients returning parameters ({len(results)}) differs from " - f"the number of sampled clients ({self.num_sampled_clients}). This could impact " - f"the differential privacy guarantees of the system, potentially leading to privacy leakage " - f"or inadequate noise calibration.", + f"The number of clients returning parameters ({len(results)})" + f" differs from the number of sampled clients ({self.num_sampled_clients})." + f" This could impact the differential privacy guarantees of the system," + f" potentially leading to privacy leakage or inadequate noise calibration.", stacklevel=2, ) for _, res in results: From 5eaade870ab99fa540557665c4664dbb6075db6d Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 9 Feb 2024 17:25:26 +0000 Subject: [PATCH 31/45] Minor --- .../flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 09f5c89ca140..9bba71633ee3 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -129,7 +129,7 @@ def aggregate_fit( warnings.warn( f"The number of clients returning parameters ({len(results)})" f" differs from the number of sampled clients ({self.num_sampled_clients})." - f" This could impact the differential privacy guarantees of the system," + f" This could impact the differential privacy guarantees," f" potentially leading to privacy leakage or inadequate noise calibration.", stacklevel=2, ) From eee4833411ce7bfc26371e145eb8ace2480be4d6 Mon Sep 17 00:00:00 2001 From: mohammadnaseri Date: Wed, 14 Feb 2024 15:00:12 +0000 Subject: [PATCH 32/45] Update src/py/flwr/common/differential_privacy.py Co-authored-by: Daniel J. Beutel --- src/py/flwr/common/differential_privacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index a070de6b8170..26f25635e4c2 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -53,7 +53,7 @@ def compute_stdv( ) -> float: """Compute standard deviation for noise addition. - paper: https://arxiv.org/pdf/1710.06963.pdf + Paper: https://arxiv.org/abs/1710.06963 """ return float((noise_multiplier * clipping_norm) / num_sampled_clients) From d66e6c4cb3a31b2e0b160b5603d734cd319f8605 Mon Sep 17 00:00:00 2001 From: mohammadnaseri Date: Wed, 14 Feb 2024 15:00:21 +0000 Subject: [PATCH 33/45] Update src/py/flwr/common/differential_privacy.py Co-authored-by: Daniel J. Beutel --- src/py/flwr/common/differential_privacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index 26f25635e4c2..daa70f947c74 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -40,7 +40,7 @@ def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None: def clip_inputs_inplace(input_arrays: NDArrays, clipping_norm: float) -> None: """Clip model update based on the clipping norm in-place. - FlatClip method of the paper: https://arxiv.org/pdf/1710.06963.pdf + FlatClip method of the paper: https://arxiv.org/abs/1710.06963 """ input_norm = get_norm(input_arrays) scaling_factor = min(1, clipping_norm / input_norm) From 8ac000a2b62f752c788b4c6fade1afc506b86889 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 14 Feb 2024 15:56:45 +0000 Subject: [PATCH 34/45] Address comments --- src/py/flwr/common/differential_privacy.py | 6 ++++-- src/py/flwr/common/differential_privacy_test.py | 3 ++- .../dp_strategy_wrapper_fixed_clipping.py | 16 +++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/py/flwr/common/differential_privacy.py b/src/py/flwr/common/differential_privacy.py index daa70f947c74..c6ec45d30f8e 100644 --- a/src/py/flwr/common/differential_privacy.py +++ b/src/py/flwr/common/differential_privacy.py @@ -1,3 +1,5 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -31,8 +33,7 @@ def get_norm(input_arrays: NDArrays) -> float: def add_gaussian_noise_inplace(input_arrays: NDArrays, std_dev: float) -> None: - """Add noise to each element of the provided input from Gaussian (Normal) - distribution with respect to the passed standard deviation.""" + """Add Gaussian noise to each element of the input arrays.""" for array in input_arrays: array += np.random.normal(0, std_dev, array.shape) @@ -62,6 +63,7 @@ def compute_clip_model_update( param1: NDArrays, param2: NDArrays, clipping_norm: float ) -> None: """Compute model update (param1 - param2) and clip it. + Then add the clipped value to param1.""" model_update = [np.subtract(x, y) for (x, y) in zip(param1, param2)] clip_inputs_inplace(model_update, clipping_norm) diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index 2232cdfe2e54..9c7b23323489 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -1,4 +1,4 @@ -# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ # ============================================================================== """DP utility functions tests.""" + import numpy as np from .differential_privacy import ( diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 9bba71633ee3..86cf33d243ed 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -14,7 +14,7 @@ # ============================================================================== """Central differential privacy with fixed clipping. -Papers: https://arxiv.org/pdf/1712.07557.pdf, https://arxiv.org/pdf/1710.06963.pdf +Papers: https://arxiv.org/abs/1712.07557, https://arxiv.org/abs/1710.06963 """ import warnings from typing import Dict, List, Optional, Tuple, Union @@ -40,17 +40,16 @@ class DPStrategyWrapperServerSideFixedClipping(Strategy): - """Wrapper for Configuring a Strategy for Central DP with Server Side Fixed - Clipping. + """Wrapper for Central DP with Server Side Fixed Clipping. Parameters ---------- - strategy: Strategy + strategy : Strategy The strategy to which DP functionalities will be added by this wrapper. - noise_multiplier: float + noise_multiplier : float The noise multiplier for the Gaussian mechanism for model updates. A value of 1.0 or higher is recommended for strong privacy. - clipping_norm: float + clipping_norm : float The value of the clipping norm. num_sampled_clients: int The number of clients that are sampled on each round. @@ -87,7 +86,7 @@ def __init__( def __repr__(self) -> str: """Compute a string representation of the strategy.""" - rep = "DP Strategy Wrapper with Server Side Fixed Clipping" + rep = "Differential Privacy Strategy Wrapper (Server-Side Fixed Clipping)" return rep def initialize_parameters( @@ -117,8 +116,7 @@ def aggregate_fit( results: List[Tuple[ClientProxy, FitRes]], failures: List[Union[Tuple[ClientProxy, FitRes], BaseException]], ) -> Tuple[Optional[Parameters], Dict[str, Scalar]]: - """Compute the updates, clip them, and pass them to the child strategy for - aggregation. + """Compute the updates, clip, and pass them for aggregation. Afterward, add noise to the aggregated parameters. """ From 59ea1f2552d12b431c8755cd4f4965ea7ce7e457 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 15 Feb 2024 14:06:37 +0000 Subject: [PATCH 35/45] minor --- .../flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 86cf33d243ed..574a7487a0aa 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -51,7 +51,7 @@ class DPStrategyWrapperServerSideFixedClipping(Strategy): A value of 1.0 or higher is recommended for strong privacy. clipping_norm : float The value of the clipping norm. - num_sampled_clients: int + num_sampled_clients : int The number of clients that are sampled on each round. """ From adbfd200aae1f98b0ab240037d70ff5c5c539a7c Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 15 Feb 2024 19:44:27 +0000 Subject: [PATCH 36/45] Add doc string example --- .../dp_strategy_wrapper_fixed_clipping.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 574a7487a0aa..fc6626530d12 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -53,6 +53,21 @@ class DPStrategyWrapperServerSideFixedClipping(Strategy): The value of the clipping norm. num_sampled_clients : int The number of clients that are sampled on each round. + + Examples + -------- + Create an strategy: + + >>> strategy = fl.server.strategy.FedAvg( ... ) + + Wrap the strategy with a DP wrapper + + >>> dpStrategy = DPStrategyWrapperServerSideFixedClipping( + strategy, + cfg.noise_multiplier, + cfg.clipping_norm, + cfg.num_sampled_clients + ) """ # pylint: disable=too-many-arguments,too-many-instance-attributes From aacfa0df83ef3431aadfbb9f0090a9cc26f86841 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 16 Feb 2024 10:51:23 +0000 Subject: [PATCH 37/45] Use common logger --- .../common/differential_privacy_constants.py | 22 +++++++++++++++++++ .../dp_strategy_wrapper_fixed_clipping.py | 17 ++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 src/py/flwr/common/differential_privacy_constants.py diff --git a/src/py/flwr/common/differential_privacy_constants.py b/src/py/flwr/common/differential_privacy_constants.py new file mode 100644 index 000000000000..c4e901103e45 --- /dev/null +++ b/src/py/flwr/common/differential_privacy_constants.py @@ -0,0 +1,22 @@ +# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Constants for differential privacy.""" + +CLIENTS_DISCREPENCY_WARNING = ( + "The number of clients returning parameters ({})" + " differs from the number of sampled clients ({})." + " This could impact the differential privacy guarantees," + " potentially leading to privacy leakage or inadequate noise calibration." +) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index fc6626530d12..5f0bfa4100aa 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -16,7 +16,9 @@ Papers: https://arxiv.org/abs/1712.07557, https://arxiv.org/abs/1710.06963 """ -import warnings + + +from logging import WARNING from typing import Dict, List, Optional, Tuple, Union from flwr.common import ( @@ -34,6 +36,8 @@ add_gaussian_noise_to_params, compute_clip_model_update, ) +from flwr.common.differential_privacy_constants import CLIENTS_DISCREPENCY_WARNING +from flwr.common.logger import log from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.strategy.strategy import Strategy @@ -139,12 +143,11 @@ def aggregate_fit( return None, {} if len(results) != self.num_sampled_clients: - warnings.warn( - f"The number of clients returning parameters ({len(results)})" - f" differs from the number of sampled clients ({self.num_sampled_clients})." - f" This could impact the differential privacy guarantees," - f" potentially leading to privacy leakage or inadequate noise calibration.", - stacklevel=2, + log( + WARNING, + CLIENTS_DISCREPENCY_WARNING.format( + len(results), self.num_sampled_clients + ), ) for _, res in results: param = parameters_to_ndarrays(res.parameters) From 405aa0ffa43368a75cd7fa96613d4897af0556ba Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 16 Feb 2024 11:13:22 +0000 Subject: [PATCH 38/45] Use common logger --- src/py/flwr/common/differential_privacy_constants.py | 4 ++-- .../server/strategy/dp_strategy_wrapper_fixed_clipping.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/py/flwr/common/differential_privacy_constants.py b/src/py/flwr/common/differential_privacy_constants.py index c4e901103e45..4158bac087b1 100644 --- a/src/py/flwr/common/differential_privacy_constants.py +++ b/src/py/flwr/common/differential_privacy_constants.py @@ -15,8 +15,8 @@ """Constants for differential privacy.""" CLIENTS_DISCREPENCY_WARNING = ( - "The number of clients returning parameters ({})" - " differs from the number of sampled clients ({})." + "The number of clients returning parameters (%s)" + " differs from the number of sampled clients (%s)." " This could impact the differential privacy guarantees," " potentially leading to privacy leakage or inadequate noise calibration." ) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 5f0bfa4100aa..1a607139040c 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -145,9 +145,7 @@ def aggregate_fit( if len(results) != self.num_sampled_clients: log( WARNING, - CLIENTS_DISCREPENCY_WARNING.format( - len(results), self.num_sampled_clients - ), + CLIENTS_DISCREPENCY_WARNING % (len(results), self.num_sampled_clients), ) for _, res in results: param = parameters_to_ndarrays(res.parameters) From f1e0e690ca29f5a8773e1833c5ee32dea4ba0c30 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 16 Feb 2024 11:38:54 +0000 Subject: [PATCH 39/45] Fix error --- src/py/flwr/common/differential_privacy_constants.py | 2 +- .../server/strategy/dp_strategy_wrapper_fixed_clipping.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/common/differential_privacy_constants.py b/src/py/flwr/common/differential_privacy_constants.py index 4158bac087b1..356d42dac7dd 100644 --- a/src/py/flwr/common/differential_privacy_constants.py +++ b/src/py/flwr/common/differential_privacy_constants.py @@ -14,7 +14,7 @@ # ============================================================================== """Constants for differential privacy.""" -CLIENTS_DISCREPENCY_WARNING = ( +CLIENTS_DISCREPANCY_WARNING = ( "The number of clients returning parameters (%s)" " differs from the number of sampled clients (%s)." " This could impact the differential privacy guarantees," diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 1a607139040c..9e2bb1dec31c 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -36,7 +36,7 @@ add_gaussian_noise_to_params, compute_clip_model_update, ) -from flwr.common.differential_privacy_constants import CLIENTS_DISCREPENCY_WARNING +from flwr.common.differential_privacy_constants import CLIENTS_DISCREPANCY_WARNING from flwr.common.logger import log from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy @@ -145,7 +145,9 @@ def aggregate_fit( if len(results) != self.num_sampled_clients: log( WARNING, - CLIENTS_DISCREPENCY_WARNING % (len(results), self.num_sampled_clients), + CLIENTS_DISCREPANCY_WARNING, + len(results), + self.num_sampled_clients, ) for _, res in results: param = parameters_to_ndarrays(res.parameters) From 00e1b3ee07e0e7c9253dc4665b91869a967ee501 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 16 Feb 2024 11:42:58 +0000 Subject: [PATCH 40/45] minor --- src/py/flwr/common/differential_privacy_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/common/differential_privacy_constants.py b/src/py/flwr/common/differential_privacy_constants.py index 356d42dac7dd..9ec080975aa3 100644 --- a/src/py/flwr/common/differential_privacy_constants.py +++ b/src/py/flwr/common/differential_privacy_constants.py @@ -1,4 +1,4 @@ -# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 7d694763196c2610b905b703c76f63d810e6f5c9 Mon Sep 17 00:00:00 2001 From: mohammadnaseri Date: Fri, 16 Feb 2024 16:21:23 +0000 Subject: [PATCH 41/45] fix copyright year --- .../flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 9e2bb1dec31c..2178bb303d4a 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -1,4 +1,4 @@ -# Copyright 2020 Flower Labs GmbH. All Rights Reserved. +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 73be298c822375db2fc2029bfea09a86b919f0e7 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 20 Feb 2024 11:17:28 +0000 Subject: [PATCH 42/45] Address comments --- src/py/flwr/server/strategy/__init__.py | 6 ++++-- .../dp_strategy_wrapper_fixed_clipping.py | 15 ++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/py/flwr/server/strategy/__init__.py b/src/py/flwr/server/strategy/__init__.py index e7c3573d8b45..e727265f0e36 100644 --- a/src/py/flwr/server/strategy/__init__.py +++ b/src/py/flwr/server/strategy/__init__.py @@ -16,7 +16,9 @@ from .bulyan import Bulyan as Bulyan -from .dp_strategy_wrapper_fixed_clipping import DPStrategyWrapperServerSideFixedClipping +from .dp_strategy_wrapper_fixed_clipping import ( + DifferentialPrivacyServerSideFixedClipping, +) from .dpfedavg_adaptive import DPFedAvgAdaptive as DPFedAvgAdaptive from .dpfedavg_fixed import DPFedAvgFixed as DPFedAvgFixed from .fault_tolerant_fedavg import FaultTolerantFedAvg as FaultTolerantFedAvg @@ -58,5 +60,5 @@ "DPFedAvgAdaptive", "DPFedAvgFixed", "Strategy", - "DPStrategyWrapperServerSideFixedClipping", + "DifferentialPrivacyServerSideFixedClipping", ] diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py index 2178bb303d4a..3382b915164d 100644 --- a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py @@ -43,7 +43,7 @@ from flwr.server.strategy.strategy import Strategy -class DPStrategyWrapperServerSideFixedClipping(Strategy): +class DifferentialPrivacyServerSideFixedClipping(Strategy): """Wrapper for Central DP with Server Side Fixed Clipping. Parameters @@ -60,18 +60,15 @@ class DPStrategyWrapperServerSideFixedClipping(Strategy): Examples -------- - Create an strategy: + Create a strategy: >>> strategy = fl.server.strategy.FedAvg( ... ) - Wrap the strategy with a DP wrapper + Wrap the strategy with the DifferentialPrivacyServerSideFixedClipping wrapper - >>> dpStrategy = DPStrategyWrapperServerSideFixedClipping( - strategy, - cfg.noise_multiplier, - cfg.clipping_norm, - cfg.num_sampled_clients - ) + >>> dp_strategy = DifferentialPrivacyServerSideFixedClipping( + >>> strategy, cfg.noise_multiplier, cfg.clipping_norm, cfg.num_sampled_clients + >>> ) """ # pylint: disable=too-many-arguments,too-many-instance-attributes From 1d430bd36b0958dcf4c9934651c6c9791c7da564 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 20 Feb 2024 11:56:03 +0000 Subject: [PATCH 43/45] Address comments --- src/py/flwr/server/strategy/__init__.py | 4 +--- ...trategy_wrapper_fixed_clipping.py => dp_fixed_clipping.py} | 0 2 files changed, 1 insertion(+), 3 deletions(-) rename src/py/flwr/server/strategy/{dp_strategy_wrapper_fixed_clipping.py => dp_fixed_clipping.py} (100%) diff --git a/src/py/flwr/server/strategy/__init__.py b/src/py/flwr/server/strategy/__init__.py index e727265f0e36..a31f5d48b77c 100644 --- a/src/py/flwr/server/strategy/__init__.py +++ b/src/py/flwr/server/strategy/__init__.py @@ -16,9 +16,7 @@ from .bulyan import Bulyan as Bulyan -from .dp_strategy_wrapper_fixed_clipping import ( - DifferentialPrivacyServerSideFixedClipping, -) +from .dp_fixed_clipping import DifferentialPrivacyServerSideFixedClipping from .dpfedavg_adaptive import DPFedAvgAdaptive as DPFedAvgAdaptive from .dpfedavg_fixed import DPFedAvgFixed as DPFedAvgFixed from .fault_tolerant_fedavg import FaultTolerantFedAvg as FaultTolerantFedAvg diff --git a/src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py b/src/py/flwr/server/strategy/dp_fixed_clipping.py similarity index 100% rename from src/py/flwr/server/strategy/dp_strategy_wrapper_fixed_clipping.py rename to src/py/flwr/server/strategy/dp_fixed_clipping.py From 821fe7ec1bf1c21a38f2a7002955ec67b89c2c44 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Tue, 20 Feb 2024 15:07:09 +0100 Subject: [PATCH 44/45] Update src/py/flwr/server/strategy/dp_fixed_clipping.py --- src/py/flwr/server/strategy/dp_fixed_clipping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/strategy/dp_fixed_clipping.py b/src/py/flwr/server/strategy/dp_fixed_clipping.py index 3382b915164d..d18a6b2079d9 100644 --- a/src/py/flwr/server/strategy/dp_fixed_clipping.py +++ b/src/py/flwr/server/strategy/dp_fixed_clipping.py @@ -67,8 +67,8 @@ class DifferentialPrivacyServerSideFixedClipping(Strategy): Wrap the strategy with the DifferentialPrivacyServerSideFixedClipping wrapper >>> dp_strategy = DifferentialPrivacyServerSideFixedClipping( - >>> strategy, cfg.noise_multiplier, cfg.clipping_norm, cfg.num_sampled_clients - >>> ) + >>> strategy, cfg.noise_multiplier, cfg.clipping_norm, cfg.num_sampled_clients + >>> ) """ # pylint: disable=too-many-arguments,too-many-instance-attributes From 3faee74a5b7ab0b7ba8cfd8a6233a1ab21faf25d Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Tue, 20 Feb 2024 15:09:56 +0100 Subject: [PATCH 45/45] Update src/py/flwr/common/differential_privacy_test.py --- src/py/flwr/common/differential_privacy_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/common/differential_privacy_test.py b/src/py/flwr/common/differential_privacy_test.py index 9c7b23323489..32b7bc9e4b36 100644 --- a/src/py/flwr/common/differential_privacy_test.py +++ b/src/py/flwr/common/differential_privacy_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""DP utility functions tests.""" +"""Differential Privacy (DP) utility functions tests.""" import numpy as np