Source code for nvflare.app_common.filters.percentile_privacy

# Copyright (c) 2021, NVIDIA CORPORATION.  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.

from typing import List, Union

import numpy as np

from nvflare.apis.dxo import DXO, DataKind, MetaKey
from nvflare.apis.dxo_filter import DXOFilter
from nvflare.apis.fl_context import FLContext
from nvflare.apis.shareable import Shareable


[docs]class PercentilePrivacy(DXOFilter): def __init__(self, percentile=10, gamma=0.01, data_kinds: List[str] = None): """Implementation of "largest percentile to share" privacy preserving policy. Shokri and Shmatikov, Privacy-preserving deep learning, CCS '15 Args: percentile (int, optional): Only abs diff greater than this percentile is updated. Allowed range 0..100. Defaults to 10. gamma (float, optional): The upper limit to truncate abs values of weight diff. Defaults to 0.01. Any weight diff with abs<gamma will become 0. data_kinds: kinds of DXO to filter """ if not data_kinds: data_kinds = [DataKind.WEIGHT_DIFF, DataKind.WEIGHTS] super().__init__(supported_data_kinds=[DataKind.WEIGHTS, DataKind.WEIGHT_DIFF], data_kinds_to_filter=data_kinds) # must be in 0..100, only update abs diff greater than percentile self.percentile = percentile # must be positive self.gamma = gamma # truncate absolute value of delta W
[docs] def process_dxo(self, dxo: DXO, shareable: Shareable, fl_ctx: FLContext) -> Union[None, DXO]: """Compute the percentile on the abs delta_W. Only share the params where absolute delta_W greater than the percentile value Args: dxo: information from client shareable: that the dxo belongs to fl_ctx: context provided by workflow Returns: filtered dxo """ self.log_debug(fl_ctx, "inside filter") self.logger.debug("check gamma") if self.gamma <= 0: self.log_debug(fl_ctx, "no partial model: gamma: {}".format(self.gamma)) return None if self.percentile < 0 or self.percentile > 100: self.log_debug(fl_ctx, "no partial model: percentile: {}".format(self.percentile)) return None # do nothing # invariant to local steps model_diff = dxo.data total_steps = dxo.get_meta_prop(MetaKey.NUM_STEPS_CURRENT_ROUND, 1) delta_w = {name: model_diff[name] / total_steps for name in model_diff} # abs delta all_abs_values = np.concatenate([np.abs(delta_w[name].ravel()) for name in delta_w]) cutoff = np.percentile(a=all_abs_values, q=self.percentile, overwrite_input=False) self.log_info( fl_ctx, f"Max abs delta_w: {np.max(all_abs_values)}, Min abs delta_w: {np.min(all_abs_values)}," f"cutoff: {cutoff}, scale: {total_steps}.", ) for name in delta_w: diff_w = delta_w[name] if np.ndim(diff_w) == 0: # single scalar, no clipping delta_w[name] = diff_w * total_steps continue selector = (diff_w > -cutoff) & (diff_w < cutoff) diff_w[selector] = 0.0 diff_w = np.clip(diff_w, a_min=-self.gamma, a_max=self.gamma) delta_w[name] = diff_w * total_steps dxo.data = delta_w return dxo