-
-
Notifications
You must be signed in to change notification settings - Fork 77
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
(WIP) Support to PLD composition #259 #405
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ class ScalarNoiseParams: | |
max_partitions_contributed: int | ||
max_contributions_per_partition: Optional[int] | ||
noise_kind: pipeline_dp.NoiseKind # Laplace or Gaussian | ||
noise_standard_deviation: Optional[float] = None | ||
|
||
def __post_init__(self): | ||
assert (self.min_value is None) == ( | ||
|
@@ -103,50 +104,70 @@ def compute_sigma(eps: float, delta: float, l2_sensitivity: float): | |
delta: The delta value. | ||
l2_sensitivity: The L2 sensitivity. | ||
""" | ||
# TODO: use named arguments, when argument names are added in PyDP on PR | ||
# https://github.com/OpenMined/PyDP/pull/398. | ||
return dp_mechanisms.GaussianMechanism(eps, delta, l2_sensitivity).std | ||
return dp_mechanisms.GaussianMechanism(epsilon=eps, | ||
delta=delta, | ||
sensitivity=l2_sensitivity).std | ||
|
||
|
||
def apply_laplace_mechanism(value: float, eps: float, l1_sensitivity: float): | ||
def apply_laplace_mechanism(value: float, | ||
eps: float, | ||
l1_sensitivity: float, | ||
noise_standard_deviation: Optional[float] = None): | ||
"""Applies the Laplace mechanism to the value. | ||
If noise_standard_deviation is set, it is used and eps is ignored. | ||
|
||
Args: | ||
value: The initial value. | ||
eps: The epsilon value. | ||
l1_sensitivity: The L1 sensitivity. | ||
noise_standard_deviation: The standard deviation for the noise. | ||
|
||
Returns: | ||
The value resulted after adding the noise. | ||
""" | ||
mechanism = dp_mechanisms.LaplaceMechanism(epsilon=eps, | ||
sensitivity=l1_sensitivity) | ||
if noise_standard_deviation is not None: | ||
mechanism = dp_mechanisms.LaplaceMechanism(epsilon=1 / | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add comment that here workaround is used, since we can't set the Laplace parameter directly |
||
noise_standard_deviation, | ||
sensitivity=l1_sensitivity) | ||
else: | ||
mechanism = dp_mechanisms.LaplaceMechanism(epsilon=eps, | ||
sensitivity=l1_sensitivity) | ||
return mechanism.add_noise(1.0 * value) | ||
|
||
|
||
def apply_gaussian_mechanism(value: float, eps: float, delta: float, | ||
l2_sensitivity: float): | ||
def apply_gaussian_mechanism(value: float, | ||
eps: float, | ||
delta: float, | ||
l2_sensitivity: float, | ||
noise_standard_deviation: Optional[float] = None): | ||
"""Applies the Gaussian mechanism to the value. | ||
If noise_standard_deviation is set, it is used and eps&delta are ignored. | ||
|
||
Args: | ||
value: The initial value. | ||
eps: The epsilon value. | ||
delta: The delta value. | ||
l2_sensitivity: The L2 sensitivity. | ||
noise_standard_deviation: The standard deviation for the noise. | ||
|
||
Returns: | ||
The value resulted after adding the noise. | ||
""" | ||
# TODO: use named arguments, when argument names are added in PyDP on PR | ||
# https://github.com/OpenMined/PyDP/pull/398. | ||
mechanism = dp_mechanisms.GaussianMechanism(eps, delta, l2_sensitivity) | ||
if noise_standard_deviation is not None: | ||
mechanism = dp_mechanisms.GaussianMechanism.create_from_standard_deviation( | ||
std=l2_sensitivity * noise_standard_deviation) | ||
else: | ||
mechanism = dp_mechanisms.GaussianMechanism(epsilon=eps, | ||
delta=delta, | ||
sensitivity=l2_sensitivity) | ||
return mechanism.add_noise(1.0 * value) | ||
|
||
|
||
def _add_random_noise( | ||
value: float, | ||
eps: float, | ||
delta: float, | ||
noise_standard_deviation: float, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional[float]? |
||
l0_sensitivity: float, | ||
linf_sensitivity: float, | ||
noise_kind: pipeline_dp.NoiseKind, | ||
|
@@ -164,14 +185,19 @@ def _add_random_noise( | |
Returns: | ||
The value resulted after adding the random noise. | ||
""" | ||
|
||
if noise_kind == pipeline_dp.NoiseKind.LAPLACE: | ||
l1_sensitivity = compute_l1_sensitivity(l0_sensitivity, | ||
linf_sensitivity) | ||
return apply_laplace_mechanism(value, eps, l1_sensitivity) | ||
return apply_laplace_mechanism(value, eps, l1_sensitivity, | ||
noise_standard_deviation) | ||
|
||
if noise_kind == pipeline_dp.NoiseKind.GAUSSIAN: | ||
l2_sensitivity = compute_l2_sensitivity(l0_sensitivity, | ||
linf_sensitivity) | ||
return apply_gaussian_mechanism(value, eps, delta, l2_sensitivity) | ||
return apply_gaussian_mechanism(value, eps, delta, l2_sensitivity, | ||
noise_standard_deviation) | ||
|
||
raise ValueError("Noise kind must be either Laplace or Gaussian.") | ||
|
||
|
||
|
@@ -213,6 +239,7 @@ def add_noise_vector(vec: np.ndarray, noise_params: AdditiveVectorNoiseParams): | |
s, | ||
noise_params.eps_per_coordinate, | ||
noise_params.delta_per_coordinate, | ||
None, # TODO: Add noise_standard_deviation for vector sum computation | ||
noise_params.l0_sensitivity, | ||
noise_params.linf_sensitivity, | ||
noise_params.noise_kind, | ||
|
@@ -269,6 +296,7 @@ def compute_dp_count(count: int, dp_params: ScalarNoiseParams): | |
count, | ||
dp_params.eps, | ||
dp_params.delta, | ||
dp_params.noise_standard_deviation, | ||
l0_sensitivity, | ||
linf_sensitivity, | ||
dp_params.noise_kind, | ||
|
@@ -301,6 +329,7 @@ def compute_dp_sum(sum: float, dp_params: ScalarNoiseParams): | |
sum, | ||
dp_params.eps, | ||
dp_params.delta, | ||
dp_params.noise_standard_deviation, | ||
l0_sensitivity, | ||
linf_sensitivity, | ||
dp_params.noise_kind, | ||
|
@@ -314,6 +343,7 @@ def _compute_mean_for_normalized_sum( | |
max_value: float, | ||
eps: float, | ||
delta: float, | ||
noise_standard_deviation: float, | ||
l0_sensitivity: float, | ||
max_contributions_per_partition: float, | ||
noise_kind: pipeline_dp.NoiseKind, | ||
|
@@ -325,6 +355,7 @@ def _compute_mean_for_normalized_sum( | |
sum: Non-DP normalized sum. | ||
min_value, max_value: The lowest/highest contribution of the non-normalized values. | ||
eps, delta: The budget allocated. | ||
noise_standard_deviation: The standard deviation for the noise. | ||
l0_sensitivity: The L0 sensitivity. | ||
max_contributions_per_partition: The maximum number of contributions | ||
per partition. | ||
|
@@ -341,8 +372,10 @@ def _compute_mean_for_normalized_sum( | |
middle = compute_middle(min_value, max_value) | ||
linf_sensitivity = max_contributions_per_partition * abs(middle - min_value) | ||
|
||
dp_normalized_sum = _add_random_noise(sum, eps, delta, l0_sensitivity, | ||
linf_sensitivity, noise_kind) | ||
dp_normalized_sum = _add_random_noise(sum, eps, delta, | ||
noise_standard_deviation, | ||
l0_sensitivity, linf_sensitivity, | ||
noise_kind) | ||
# Clamps dp_count to 1.0. We know that actual count > 1 except when the | ||
# input set is empty, in which case it shouldn't matter much what the | ||
# denominator is. | ||
|
@@ -370,10 +403,18 @@ def compute_dp_mean(count: int, normalized_sum: float, | |
dp_params.eps, dp_params.delta, 2) | ||
l0_sensitivity = dp_params.l0_sensitivity() | ||
|
||
# Increases noise std.dev. equally due to multiple computations | ||
count_noise_standard_deviation = \ | ||
sum_noise_standard_deviation = \ | ||
2*dp_params.noise_standard_deviation \ | ||
if dp_params.noise_standard_deviation is not None \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: maybe something like that
|
||
else None | ||
|
||
dp_count = _add_random_noise( | ||
count, | ||
count_eps, | ||
count_delta, | ||
count_noise_standard_deviation, | ||
l0_sensitivity, | ||
dp_params.max_contributions_per_partition, | ||
dp_params.noise_kind, | ||
|
@@ -386,6 +427,7 @@ def compute_dp_mean(count: int, normalized_sum: float, | |
dp_params.max_value, | ||
sum_eps, | ||
sum_delta, | ||
sum_noise_standard_deviation, | ||
l0_sensitivity, | ||
dp_params.max_contributions_per_partition, | ||
dp_params.noise_kind, | ||
|
@@ -421,10 +463,19 @@ def compute_dp_var(count: int, normalized_sum: float, | |
) = equally_split_budget(dp_params.eps, dp_params.delta, 3) | ||
l0_sensitivity = dp_params.l0_sensitivity() | ||
|
||
# Increases noise std.dev. equally due to multiple computations | ||
count_noise_standard_deviation = \ | ||
sum_noise_standard_deviation = \ | ||
sum_squares_noise_standard_deviation = \ | ||
3*dp_params.noise_standard_deviation \ | ||
if dp_params.noise_standard_deviation is not None \ | ||
else None | ||
|
||
dp_count = _add_random_noise( | ||
count, | ||
count_eps, | ||
count_delta, | ||
count_noise_standard_deviation, | ||
l0_sensitivity, | ||
dp_params.max_contributions_per_partition, | ||
dp_params.noise_kind, | ||
|
@@ -438,6 +489,7 @@ def compute_dp_var(count: int, normalized_sum: float, | |
dp_params.max_value, | ||
sum_eps, | ||
sum_delta, | ||
sum_noise_standard_deviation, | ||
l0_sensitivity, | ||
dp_params.max_contributions_per_partition, | ||
dp_params.noise_kind, | ||
|
@@ -449,7 +501,8 @@ def compute_dp_var(count: int, normalized_sum: float, | |
# Computes and adds noise to the mean of squares. | ||
dp_mean_squares = _compute_mean_for_normalized_sum( | ||
dp_count, normalized_sum_squares, squares_min_value, squares_max_value, | ||
sum_squares_eps, sum_squares_delta, l0_sensitivity, | ||
sum_squares_eps, sum_squares_delta, | ||
sum_squares_noise_standard_deviation, l0_sensitivity, | ||
dp_params.max_contributions_per_partition, dp_params.noise_kind) | ||
|
||
dp_var = dp_mean_squares - dp_mean**2 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -198,9 +198,8 @@ def test_not_enough_aggregations(self, use_num_aggregations): | |
class PLDBudgetAccountantTest(unittest.TestCase): | ||
|
||
def test_noise_not_calculated(self): | ||
with self.assertRaises(AssertionError): | ||
mechanism = MechanismSpec(MechanismType.LAPLACE) | ||
print(mechanism.noise_standard_deviation()) | ||
mechanism = MechanismSpec(MechanismType.LAPLACE) | ||
self.assertEqual(None, mechanism.noise_standard_deviation) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: self.assertNone( |
||
|
||
def test_invalid_epsilon(self): | ||
with self.assertRaises(ValueError): | ||
|
@@ -257,7 +256,7 @@ class ComputeBudgetTestCase: | |
epsilon: float | ||
delta: float | ||
expected_pipeline_noise_std: float | ||
mechanisms: [] | ||
mechanisms: list | ||
|
||
testcases = [ | ||
ComputeBudgetTestCase( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing this TODO