Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating Additive mechanism from standard deviation #470

Merged
merged 10 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pipeline_dp/budget_accounting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
81 changes: 68 additions & 13 deletions pipeline_dp/dp_computations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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."

Expand Down
76 changes: 61 additions & 15 deletions tests/dp_computations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand Down
Loading