From 46067773feb924de0a8af584a183ae7fdc941a54 Mon Sep 17 00:00:00 2001 From: Vadym Doroshenko <53558779+dvadym@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:10:59 +0200 Subject: [PATCH] Creating Additive mechanism from standard deviation (#470) --- pipeline_dp/budget_accounting.py | 7 +++ pipeline_dp/dp_computations.py | 81 +++++++++++++++++++++++++++----- tests/dp_computations_test.py | 76 ++++++++++++++++++++++++------ 3 files changed, 136 insertions(+), 28 deletions(-) diff --git a/pipeline_dp/budget_accounting.py b/pipeline_dp/budget_accounting.py index 522b5219..c23a646f 100644 --- a/pipeline_dp/budget_accounting.py +++ b/pipeline_dp/budget_accounting.py @@ -95,9 +95,16 @@ def set_eps_delta(self, eps: float, delta: Optional[float]) -> None: self._delta = delta return + def set_noise_standard_deviation(self, stddev: float): + self._noise_standard_deviation = stddev + def use_delta(self) -> bool: return self.mechanism_type != agg_params.MechanismType.LAPLACE + @property + def standard_deviation_is_set(self) -> bool: + return self._noise_standard_deviation is not None + @dataclass class MechanismSpecInternal: diff --git a/pipeline_dp/dp_computations.py b/pipeline_dp/dp_computations.py index 16c1092b..05d7211a 100644 --- a/pipeline_dp/dp_computations.py +++ b/pipeline_dp/dp_computations.py @@ -430,9 +430,29 @@ def describe(self) -> float: class LaplaceMechanism(AdditiveMechanism): - def __init__(self, epsilon: float, l1_sensitivity: float): - self._mechanism = dp_mechanisms.LaplaceMechanism( - epsilon=epsilon, sensitivity=l1_sensitivity) + def __init__(self, mechanism): + self._mechanism = mechanism + + @classmethod + def create_from_epsilon(cls, epsilon: float, + l1_sensitivity: float) -> 'LaplaceMechanism': + return LaplaceMechanism( + dp_mechanisms.LaplaceMechanism(epsilon=epsilon, + sensitivity=l1_sensitivity)) + + @classmethod + def create_from_std_deviation(cls, normalized_stddev: float, + l1_sensitivity: float) -> 'LaplaceMechanism': + """Creates Laplace mechanism from the standard deviation. + + Args: + normalized_stddev: the standard deviation divided by l1_sensitivity. + l1_sensitivity: the l1 sensitivity of the query. + """ + b = normalized_stddev / math.sqrt(2) + return LaplaceMechanism( + dp_mechanisms.LaplaceMechanism(epsilon=1 / b, + sensitivity=l1_sensitivity)) def add_noise(self, value: Union[int, float]) -> float: return self._mechanism.add_noise(1.0 * value) @@ -460,10 +480,31 @@ def describe(self) -> str: class GaussianMechanism(AdditiveMechanism): - def __init__(self, epsilon: float, delta: float, l2_sensitivity: float): + def __init__(self, mechanism, l2_sensitivity: float): + self._mechanism = mechanism self._l2_sensitivity = l2_sensitivity - self._mechanism = dp_mechanisms.GaussianMechanism( - epsilon=epsilon, delta=delta, sensitivity=l2_sensitivity) + + @classmethod + def create_from_epsilon_delta(cls, epsilon: float, delta: float, + l2_sensitivity: float) -> 'GaussianMechanism': + return GaussianMechanism(dp_mechanisms.GaussianMechanism( + epsilon=epsilon, delta=delta, sensitivity=l2_sensitivity), + l2_sensitivity=l2_sensitivity) + + @classmethod + def create_from_std_deviation(cls, normalized_stddev: float, + l2_sensitivity: float) -> 'GaussianMechanism': + """Creates Gaussian mechanism from the standard deviation. + + Args: + normalized_stddev: the standard deviation divided by l2_sensitivity. + l2_sensitivity: the l2 sensitivity of the query. + """ + stddev = normalized_stddev * l2_sensitivity + return GaussianMechanism( + dp_mechanisms.GaussianMechanism.create_from_standard_deviation( + stddev), + l2_sensitivity=l2_sensitivity) def add_noise(self, value: Union[int, float]) -> float: return self._mechanism.add_noise(1.0 * value) @@ -482,12 +523,19 @@ def std(self) -> float: @property def sensitivity(self) -> float: - return self._mechanism.l2_sensitivity + return self._l2_sensitivity def describe(self) -> str: - return (f"Gaussian mechanism: parameter={self.noise_parameter} eps=" - f"{self._mechanism.epsilon} delta={self._mechanism.delta} " - f"l2_sensitivity={self.sensitivity}") + if self._mechanism.epsilon > 0: + # The naive budget accounting, the mechanism is specified with + # (eps, delta). + eps_delta_str = f"eps={self._mechanism.epsilon} " \ + f"delta={self._mechanism.delta} " + else: + # The PLD accounting, the mechanism is specified with stddev. + eps_delta_str = "" + return (f"Gaussian mechanism: parameter={self.noise_parameter}" + f" {eps_delta_str}l2_sensitivity={self.sensitivity}") class MeanMechanism: @@ -580,14 +628,21 @@ def create_additive_mechanism( if sensitivities.l1 is None: raise ValueError("L1 or (L0 and Linf) sensitivities must be set for" " Laplace mechanism.") - return LaplaceMechanism(mechanism_spec.eps, sensitivities.l1) + if mechanism_spec.standard_deviation_is_set: + return LaplaceMechanism.create_from_std_deviation( + mechanism_spec.noise_standard_deviation, sensitivities.l1) + return LaplaceMechanism.create_from_epsilon(mechanism_spec.eps, + sensitivities.l1) if noise_kind == pipeline_dp.NoiseKind.GAUSSIAN: if sensitivities.l2 is None: raise ValueError("L2 or (L0 and Linf) sensitivities must be set for" " Gaussian mechanism.") - return GaussianMechanism(mechanism_spec.eps, mechanism_spec.delta, - sensitivities.l2) + if mechanism_spec.standard_deviation_is_set: + return GaussianMechanism.create_from_std_deviation( + mechanism_spec.noise_standard_deviation, sensitivities.l2) + return GaussianMechanism.create_from_epsilon_delta( + mechanism_spec.eps, mechanism_spec.delta, sensitivities.l2) assert False, f"{noise_kind} not supported." diff --git a/tests/dp_computations_test.py b/tests/dp_computations_test.py index c4b0bbef..8ea52c18 100644 --- a/tests/dp_computations_test.py +++ b/tests/dp_computations_test.py @@ -430,9 +430,9 @@ class AdditiveMechanismTests(parameterized.TestCase): dict(epsilon=2, l1_sensitivity=4.5, expected_noise=2.25), dict(epsilon=0.1, l1_sensitivity=0.55, expected_noise=5.5), ) - def test_laplace_mechanism_creation(self, epsilon, l1_sensitivity, - expected_noise): - mechanism = dp_computations.LaplaceMechanism( + def test_laplace_create_from_epsilon(self, epsilon, l1_sensitivity, + expected_noise): + mechanism = dp_computations.LaplaceMechanism.create_from_epsilon( epsilon=epsilon, l1_sensitivity=l1_sensitivity) self.assertEqual(mechanism.noise_kind, pipeline_dp.NoiseKind.LAPLACE) @@ -445,6 +445,19 @@ def test_laplace_mechanism_creation(self, epsilon, l1_sensitivity, self.assertEqual(mechanism.sensitivity, l1_sensitivity) self.assertIsInstance(mechanism.add_noise(1000), float) + def test_laplace_create_from_stddev(self): + mechanism = dp_computations.LaplaceMechanism.create_from_std_deviation( + normalized_stddev=10, l1_sensitivity=3.5) + + self.assertEqual(mechanism.noise_kind, pipeline_dp.NoiseKind.LAPLACE) + expected_noise_parameter = 10 / np.sqrt(2) * 3.5 + self.assertAlmostEqual(mechanism.noise_parameter, + expected_noise_parameter, + delta=1e-12) + self.assertAlmostEqual(mechanism.std, 35) + self.assertEqual(mechanism.sensitivity, 3.5) + self.assertIsInstance(mechanism.add_noise(1000), float) + @parameterized.parameters( dict(epsilon=2, l1_sensitivity=4.5, value=0, expected_noise_scale=2.25), dict(epsilon=0.1, @@ -460,7 +473,7 @@ def test_laplace_mechanism_distribution(self, epsilon, l1_sensitivity, value, expected_noise_scale): # Use Kolmogorov-Smirnov test to verify the output noise distribution. # https://en.wikipedia.org/wiki/Kolmogorov-Smirnov_test - mechanism = dp_computations.LaplaceMechanism( + mechanism = dp_computations.LaplaceMechanism.create_from_epsilon( epsilon=epsilon, l1_sensitivity=l1_sensitivity) expected_cdf = stats.laplace(loc=value, scale=expected_noise_scale).cdf @@ -470,11 +483,10 @@ def test_laplace_mechanism_distribution(self, epsilon, l1_sensitivity, self.assertGreater(res.pvalue, 1e-4) def test_gaussian_mechanism_describe(self): - mechanism = dp_computations.GaussianMechanism(epsilon=1.0, - delta=1e-10, - l2_sensitivity=15) + mechanism = dp_computations.GaussianMechanism.create_from_epsilon_delta( + epsilon=1.0, delta=1e-10, l2_sensitivity=15) expected = ("Gaussian mechanism: parameter=88.06640625 eps=1.0 " - "delta=1e-10 l2_sensitivity=15.0") + "delta=1e-10 l2_sensitivity=15") self.assertEqual(mechanism.describe(), expected) @parameterized.parameters( @@ -489,7 +501,7 @@ def test_gaussian_mechanism_describe(self): ) def test_gaussian_mechanism_creation(self, epsilon, delta, l2_sensitivity, expected_noise_scale): - mechanism = dp_computations.GaussianMechanism( + mechanism = dp_computations.GaussianMechanism.create_from_epsilon_delta( epsilon=epsilon, delta=delta, l2_sensitivity=l2_sensitivity) self.assertEqual(mechanism.noise_kind, pipeline_dp.NoiseKind.GAUSSIAN) @@ -522,7 +534,7 @@ def test_gaussian_mechanism_distribution(self, epsilon, delta, expected_noise_scale): # Use Kolmogorov-Smirnov test to verify the output noise distribution. # https://en.wikipedia.org/wiki/Kolmogorov-Smirnov_test - mechanism = dp_computations.GaussianMechanism( + mechanism = dp_computations.GaussianMechanism.create_from_epsilon_delta( epsilon=epsilon, delta=delta, l2_sensitivity=l2_sensitivity) self.assertEqual(mechanism.std, expected_noise_scale) @@ -532,9 +544,19 @@ def test_gaussian_mechanism_distribution(self, epsilon, delta, res = stats.ks_1samp(noised_values, expected_cdf) self.assertGreater(res.pvalue, 1e-4) + def test_gaussian_create_from_stddev(self): + mechanism = dp_computations.GaussianMechanism.create_from_std_deviation( + normalized_stddev=5, l2_sensitivity=15) + + self.assertEqual(mechanism.noise_kind, pipeline_dp.NoiseKind.GAUSSIAN) + self.assertEqual(mechanism.noise_parameter, 75) + self.assertEqual(mechanism.std, 75) + self.assertEqual(mechanism.sensitivity, 15) + self.assertIsInstance(mechanism.add_noise(1000), float) + def test_laplace_mechanism_describe(self): - mechanism = dp_computations.LaplaceMechanism(epsilon=2.0, - l1_sensitivity=25) + mechanism = dp_computations.LaplaceMechanism.create_from_epsilon( + epsilon=2.0, l1_sensitivity=25) expected = ("Laplace mechanism: parameter=12.5 eps=2.0 " "l1_sensitivity=25.0") self.assertEqual(mechanism.describe(), expected) @@ -606,9 +628,9 @@ def test_sensitivities_post_init_l1_l2_computation(self): l1_sensitivity=None, expected_noise_parameter=48), ) - def test_create_laplace_mechanism(self, epsilon, l0_sensitivity, - linf_sensitivity, l1_sensitivity, - expected_noise_parameter): + def test_create_additive_mechanism_laplace(self, epsilon, l0_sensitivity, + linf_sensitivity, l1_sensitivity, + expected_noise_parameter): spec = budget_accounting.MechanismSpec( aggregate_params.MechanismType.LAPLACE) spec.set_eps_delta(epsilon, delta=0) @@ -623,6 +645,19 @@ def test_create_laplace_mechanism(self, epsilon, l0_sensitivity, expected_noise_parameter, delta=1e-12) + def test_create_additive_mechanism_laplace_from_stddev(self): + spec = budget_accounting.MechanismSpec( + aggregate_params.MechanismType.LAPLACE) + spec.set_noise_standard_deviation(7) + sensitivities = dp_computations.Sensitivities(l1=2) + + mechanism = dp_computations.create_additive_mechanism( + spec, sensitivities) + + self.assertAlmostEqual(mechanism.noise_parameter, + 14 / np.sqrt(2), + delta=1e-12) + @parameterized.parameters( dict(epsilon=2, delta=1e-10, @@ -647,6 +682,17 @@ def test_create_gaussian_mechanism(self, epsilon, delta, l2_sensitivity, expected_noise_parameter, delta=1e-6) + def test_create_additive_mechanism_gaussian_from_stddev(self): + spec = budget_accounting.MechanismSpec( + aggregate_params.MechanismType.GAUSSIAN) + spec.set_noise_standard_deviation(9) + sensitivities = dp_computations.Sensitivities(l2=2) + + mechanism = dp_computations.create_additive_mechanism( + spec, sensitivities) + + self.assertEqual(mechanism.noise_parameter, 18) + def test_compute_sensitivities_for_count(self): params = create_aggregate_params(max_partitions_contributed=4, max_contributions_per_partition=11)