blob: cd0766c33262d72e3d0d23931a52d22a5f454a34 [file] [log] [blame]
# Copyright 2020 Google LLC.
#
# 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.
"""Tests for privacy_loss_distribution.py."""
import math
from typing import Any, Mapping, Optional
import unittest
from absl.testing import parameterized
from scipy import stats
from dp_accounting.pld import common
from dp_accounting.pld import pld_pmf
from dp_accounting.pld import privacy_loss_distribution
from dp_accounting.pld import test_util
def _assert_pld_pmf_equal(
testcase: unittest.TestCase,
pld: privacy_loss_distribution.PrivacyLossDistribution,
expected_rounded_pmf_add: Mapping[int, float],
expected_infinity_mass_add: float,
expected_rounded_pmf_remove: Optional[Mapping[int, float]] = None,
expected_infinity_mass_remove: Optional[float] = None):
"""Asserts equality of PLD with expected values."""
def sparse_loss_probs(pmf: pld_pmf.PLDPmf) -> Mapping[int, float]:
if isinstance(pmf, pld_pmf.SparsePLDPmf):
return pmf._loss_probs
elif isinstance(pmf, pld_pmf.DensePLDPmf):
return common.list_to_dictionary(pmf._probs, pmf._lower_loss)
return {}
test_util.assert_dictionary_almost_equal(
testcase, expected_rounded_pmf_add, sparse_loss_probs(pld._pmf_add))
testcase.assertAlmostEqual(expected_infinity_mass_add,
pld._pmf_add._infinity_mass)
if expected_rounded_pmf_remove is None:
testcase.assertTrue(pld._symmetric)
else:
test_util.assert_dictionary_almost_equal(
testcase,
expected_rounded_pmf_remove,
sparse_loss_probs(pld._pmf_remove))
testcase.assertAlmostEqual(expected_infinity_mass_remove,
pld._pmf_remove._infinity_mass)
testcase.assertFalse(pld._symmetric)
class AddRemovePrivacyLossDistributionTest(parameterized.TestCase):
def _create_pld(
self,
log_pmf_lower: Mapping[Any, float],
log_pmf_upper: Mapping[Any, float],
pessimistic: bool = True
) -> privacy_loss_distribution.PrivacyLossDistribution:
pmf_remove = (
privacy_loss_distribution.from_two_probability_mass_functions(
log_pmf_lower, log_pmf_upper,
pessimistic_estimate=pessimistic)._pmf_remove)
pmf_add = (
privacy_loss_distribution.from_two_probability_mass_functions(
log_pmf_upper, log_pmf_lower,
pessimistic_estimate=pessimistic)._pmf_remove)
return privacy_loss_distribution.PrivacyLossDistribution(
pmf_remove, pmf_add)
def test_init_errors(self):
rounded_pmf = {1: 0.5, -1: 0.5}
value_discretization_interval = 1
infinity_mass = 0
pessimistic_estimate = True
pld = privacy_loss_distribution.PrivacyLossDistribution
with self.assertRaises(ValueError):
pld.create_from_rounded_probability(
rounded_probability_mass_function=rounded_pmf,
infinity_mass=infinity_mass,
value_discretization_interval=value_discretization_interval,
pessimistic_estimate=pessimistic_estimate,
rounded_probability_mass_function_add=rounded_pmf,
infinity_mass_add=infinity_mass,
symmetric=True)
with self.assertRaises(ValueError):
pld.create_from_rounded_probability(
rounded_probability_mass_function=rounded_pmf,
infinity_mass=infinity_mass,
value_discretization_interval=value_discretization_interval,
pessimistic_estimate=pessimistic_estimate,
rounded_probability_mass_function_add=None,
infinity_mass_add=None,
symmetric=False)
def test_hockey_stick_basic(self):
# Basic hockey stick divergence computation test
log_pmf_lower = {1: math.log(0.5), 2: math.log(0.5)}
log_pmf_upper = {1: math.log(0.6), 2: math.log(0.4)}
pld_pessimistic = self._create_pld(
log_pmf_lower, log_pmf_upper, pessimistic=True)
pld_optimistic = self._create_pld(
log_pmf_lower, log_pmf_upper, pessimistic=False)
# 0-hockey stick divergence is 0.1 (for basic_pld_remove & basic_pld_add)
# When using pessimistic estimate, the output should be in [0.1, 0.1+1e-4]
self.assertTrue(
0.1 <= pld_pessimistic.get_delta_for_epsilon(0.0) <= 0.1 + 1e-4)
# When using optimistic estimate, the output should be in [0.1 - 1e-4, 0.1]
self.assertTrue(
0.1 - 1e-4 <= pld_optimistic.get_delta_for_epsilon(0.0) <= 0.1)
# math.log(1.1)-hockey stick divergence is 0.06 (for basic_pld_add)
# When using pessimistic estimate, the output should be in [0.06, 0.06+1e-4]
self.assertTrue(0.06 <= pld_pessimistic
.get_delta_for_epsilon(math.log(1.1)) <= 0.06 + 1e-4)
# When using optimistic estimate, the output should be in [0.06-1e-4, 0.06]
self.assertTrue(0.06 - 1e-4 <= pld_optimistic
.get_delta_for_epsilon(math.log(1.1)) <= 0.06)
# math.log(0.9)-hockey stick divergence is 0.15 (for basic_pld_remove)
# When using pessimistic estimate, the output should be in [0.15, 0.15+1e-4]
self.assertTrue(0.15 <= pld_pessimistic
.get_delta_for_epsilon(math.log(0.9)) <= 0.15 + 1e-4)
# When using optimistic estimate, the output should be in [0.15-1e-4, 0.15]
self.assertTrue(0.15 - 1e-4 <= pld_optimistic
.get_delta_for_epsilon(math.log(0.9)) <= 0.15)
self.assertFalse(pld_pessimistic._symmetric)
self.assertFalse(pld_optimistic._symmetric)
def test_hockey_stick_unequal_support(self):
# Hockey stick divergence computation test when the two distributions have
# differenet supports
log_pmf_lower = {1: math.log(0.2), 2: math.log(0.2), 3: math.log(0.6)}
log_pmf_upper = {1: math.log(0.5), 2: math.log(0.4), 4: math.log(0.1)}
pld_pessimistic = self._create_pld(
log_pmf_lower, log_pmf_upper, pessimistic=True)
pld_optimistic = self._create_pld(
log_pmf_lower, log_pmf_upper, pessimistic=False)
# Here 4 appears as an outcome of only mu_upper and hence should be included
# in the infinity_mass variable of _pmf_remove.
self.assertAlmostEqual(pld_pessimistic._pmf_remove._infinity_mass, 0.1)
self.assertAlmostEqual(pld_optimistic._pmf_remove._infinity_mass, 0.1)
# Here 3 appears as an outcome of only mu_lower and hence should be included
# in the infinity_mass variable of basic_pld_add.
self.assertAlmostEqual(pld_pessimistic._pmf_add._infinity_mass, 0.6)
self.assertAlmostEqual(pld_optimistic._pmf_add._infinity_mass, 0.6)
# 0-hockey stick divergence is 0.6 (for basic_pld_remove & basic_pld_add)
# When using pessimistic estimate, the output should be in [0.6, 0.6+1e-4]
self.assertTrue(0.6 <= pld_pessimistic
.get_delta_for_epsilon(0.0) <= 0.6 + 1e-4)
# When using optimistic estimate, the output should lie in [0.6 - 1e-4, 0.6]
self.assertTrue(0.6 - 1e-4 <= pld_optimistic
.get_delta_for_epsilon(0.0) <= 0.6)
# math.log(1.1)-hockey stick divergence is 0.6 (for basic_pld_add)
# When using pessimistic estimate, the output should be in [0.6, 0.6 + 1e-4]
self.assertTrue(0.6 <= pld_pessimistic
.get_delta_for_epsilon(math.log(1.1)) <= 0.6 + 1e-4)
# When using optimistic estimate, the output should lie in [0.6 - 1e-4, 0.6]
self.assertTrue(0.6 - 1e-4 <= pld_optimistic
.get_delta_for_epsilon(math.log(1.1)) <= 0.6)
# math.log(0.9)-hockey stick divergence is 0.64 (for basic_pld_remove)
# When using pessimistic estimate, the output should be
# in [0.64, 0.64 + 1e-4]
self.assertTrue(0.64 <= pld_pessimistic
.get_delta_for_epsilon(math.log(0.9)) <= 0.64 + 1e-4)
# When using optimistic estimate, the output should lie in
# [0.64 - 1e-4, 0.64]
self.assertTrue(0.64 - 1e-4 <= pld_optimistic
.get_delta_for_epsilon(math.log(0.9)) <= 0.64)
def test_composition(self):
# Test for composition of privacy loss distribution
log_pmf_lower1 = {1: math.log(0.2), 2: math.log(0.2), 3: math.log(0.6)}
log_pmf_upper1 = {1: math.log(0.5), 2: math.log(0.2), 4: math.log(0.3)}
pld1 = self._create_pld(log_pmf_lower1, log_pmf_upper1, pessimistic=True)
log_pmf_lower2 = {1: math.log(0.4), 2: math.log(0.6)}
log_pmf_upper2 = {2: math.log(0.7), 3: math.log(0.3)}
pld2 = self._create_pld(log_pmf_lower2, log_pmf_upper2, pessimistic=True)
# Result from composing the above two privacy loss distributions
result = pld1.compose(pld2)
# The correct result
log_pmf_lower_composed = {
(1, 1): math.log(0.08),
(1, 2): math.log(0.12),
(2, 1): math.log(0.08),
(2, 2): math.log(0.12),
(3, 1): math.log(0.24),
(3, 2): math.log(0.36)
}
log_pmf_upper_composed = {
(1, 2): math.log(0.35),
(1, 3): math.log(0.15),
(2, 2): math.log(0.14),
(2, 3): math.log(0.06),
(4, 2): math.log(0.21),
(4, 3): math.log(0.09)
}
expected_result = self._create_pld(log_pmf_lower_composed,
log_pmf_upper_composed)
# Check that the result is as expected. Note that we cannot check that the
# rounded_down_probability_mass_function and
# rounded_up_probability_mass_function of the two distributions are equal
# directly because the rounding might cause off-by-one error in index.
self.assertAlmostEqual(expected_result._pmf_remove._discretization,
result._pmf_remove._discretization)
self.assertAlmostEqual(expected_result._pmf_add._discretization,
result._pmf_add._discretization)
self.assertAlmostEqual(expected_result._pmf_remove._infinity_mass,
result._pmf_remove._infinity_mass)
self.assertAlmostEqual(expected_result._pmf_add._infinity_mass,
result._pmf_add._infinity_mass)
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(0),
result.get_delta_for_epsilon(0))
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(0.5),
result.get_delta_for_epsilon(0.5))
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(-0.5),
result.get_delta_for_epsilon(-0.5))
def test_composition_asymmetric_with_symmetric(self):
# Test for composition of privacy loss distribution
log_pmf_lower1 = {1: math.log(0.2), 2: math.log(0.2), 3: math.log(0.6)}
log_pmf_upper1 = {1: math.log(0.5), 2: math.log(0.2), 4: math.log(0.3)}
pld1 = self._create_pld(log_pmf_lower1, log_pmf_upper1)
log_pmf_lower2 = {1: math.log(0.4), 2: math.log(0.6)}
log_pmf_upper2 = {2: math.log(0.7), 3: math.log(0.3)}
pld2 = self._create_pld(log_pmf_lower2, log_pmf_upper2)
# Result from composing the above two privacy loss distributions
result12 = pld1.compose(pld2)
result21 = pld2.compose(pld1)
# The correct result
log_pmf_lower1_lower2_composed = {
(1, 1): math.log(0.08),
(1, 2): math.log(0.12),
(2, 1): math.log(0.08),
(2, 2): math.log(0.12),
(3, 1): math.log(0.24),
(3, 2): math.log(0.36)
}
log_pmf_upper1_upper2_composed = {
(1, 2): math.log(0.35),
(1, 3): math.log(0.15),
(2, 2): math.log(0.14),
(2, 3): math.log(0.06),
(4, 2): math.log(0.21),
(4, 3): math.log(0.09)
}
expected_result = self._create_pld(log_pmf_lower1_lower2_composed,
log_pmf_upper1_upper2_composed)
# Check that the result is as expected. Note that we cannot check that the
# rounded_down_probability_mass_function and
# rounded_up_probability_mass_function of the two distributions are equal
# directly because the rounding might cause off-by-one error in index.
for result in [result12, result21]:
self.assertAlmostEqual(expected_result._pmf_remove._discretization,
result._pmf_remove._discretization)
self.assertAlmostEqual(expected_result._pmf_add._discretization,
result._pmf_add._discretization)
self.assertAlmostEqual(expected_result._pmf_remove._infinity_mass,
result._pmf_remove._infinity_mass)
self.assertAlmostEqual(expected_result._pmf_add._infinity_mass,
result._pmf_add._infinity_mass)
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(0),
result.get_delta_for_epsilon(0))
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(0.5),
result.get_delta_for_epsilon(0.5))
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(-0.5),
result.get_delta_for_epsilon(-0.5))
def test_self_composition(self):
log_pmf_lower = {1: math.log(0.2), 2: math.log(0.2), 3: math.log(0.6)}
log_pmf_upper = {1: math.log(0.5), 2: math.log(0.2), 4: math.log(0.3)}
pld = self._create_pld(log_pmf_lower, log_pmf_upper)
result = pld.self_compose(3)
expected_log_pmf_lower = {}
for i, vi in log_pmf_lower.items():
for j, vj in log_pmf_lower.items():
for k, vk in log_pmf_lower.items():
expected_log_pmf_lower[(i, j, k)] = vi + vj + vk
expected_log_pmf_upper = {}
for i, vi in log_pmf_upper.items():
for j, vj in log_pmf_upper.items():
for k, vk in log_pmf_upper.items():
expected_log_pmf_upper[(i, j, k)] = vi + vj + vk
expected_result = self._create_pld(expected_log_pmf_lower,
expected_log_pmf_upper)
self.assertAlmostEqual(expected_result._pmf_remove._discretization,
result._pmf_remove._discretization)
self.assertAlmostEqual(expected_result._pmf_remove._infinity_mass,
result._pmf_remove._infinity_mass)
self.assertAlmostEqual(expected_result._pmf_add._discretization,
result._pmf_add._discretization)
self.assertAlmostEqual(expected_result._pmf_add._infinity_mass,
result._pmf_add._infinity_mass)
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(0),
result.get_delta_for_epsilon(0))
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(0.5),
result.get_delta_for_epsilon(0.5))
self.assertAlmostEqual(
expected_result.get_delta_for_epsilon(-0.2),
result.get_delta_for_epsilon(-0.2))
class LaplacePrivacyLossDistributionTest(parameterized.TestCase):
@parameterized.parameters((1.0, 1.0, -0.1), (1.0, 1.0, 1.1), (1.0, 1.0, 0.0),
(-0.1, 1.0, 1.0), (0.0, 1.0, 1.0), (1.0, -1.0, 1.0),
(1.0, 0.0, 1.0))
def test_laplace_value_errors(self, parameter, sensitivity, sampling_prob):
with self.assertRaises(ValueError):
privacy_loss_distribution.from_laplace_mechanism(
parameter, sensitivity=sensitivity, value_discretization_interval=1,
sampling_prob=sampling_prob)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1.0, 1.0, {
1: 0.69673467,
0: 0.11932561,
-1: 0.18393972
}),
(3.0, 3.0, 1.0, {
1: 0.69673467,
0: 0.11932561,
-1: 0.18393972
}),
(1.0, 2.0, 1.0, {
2: 0.69673467,
1: 0.11932561,
0: 0.07237464,
-1: 0.04389744,
-2: 0.06766764
}),
(2.0, 4.0, 1.0, {
2: 0.69673467,
1: 0.11932561,
0: 0.07237464,
-1: 0.04389744,
-2: 0.06766764
}),
# Tests with sampling_prob < 1
(1.0, 1.0, 0.8, {
1: 0.69673467,
0: 0.30326533
}, {
1: 0.6180408,
0: 0.3819592
}),
(3.0, 3.0, 0.5, {
1: 0.69673467,
0: 0.30326533
}, {
1: 0.5,
0: 0.5
}),
(1.0, 2.0, 0.7, {
1: 0.81606028,
0: 0.08497712,
-1: 0.09896260
}, {
2: 0.49036933,
1: 0.13605478,
0: 0.37357589
}),
(2.0, 4.0, 0.3, {
1: 0.81606028,
0: 0.11302356,
-1: 0.07091617
}, {
2: 0.20651251,
1: 0.16706338,
0: 0.62642411
}))
def test_laplace_varying_parameter_and_sensitivity(
self, parameter, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_rounded_pmf_remove=None):
"""Verifies correctness of pessimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_laplace_mechanism(
parameter, sensitivity=sensitivity, value_discretization_interval=1,
sampling_prob=sampling_prob)
_assert_pld_pmf_equal(self, pld,
expected_rounded_pmf_add, 0.0,
expected_rounded_pmf_remove, 0.0)
@parameterized.parameters((0.5, {
2: 0.61059961,
1: 0.08613506,
0: 0.06708205,
-1: 0.05224356,
-2: 0.18393972
}), (0.3, {
4: 0.52438529,
3: 0.06624934,
2: 0.05702133,
1: 0.04907872,
0: 0.04224244,
-1: 0.03635841,
-2: 0.03129397,
-3: 0.19337051
}))
def test_laplace_discretization(self, value_discretization_interval,
expected_rounded_pmf):
"""Verifies correctness of pessimistic PLD for varying discretization."""
pld = privacy_loss_distribution.from_laplace_mechanism(
1, value_discretization_interval=value_discretization_interval)
_assert_pld_pmf_equal(self, pld, expected_rounded_pmf, 0.0)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1.0, 1.0, {
1: 0.5,
0: 0.19673467,
-1: 0.30326533
}),
(1.0, 2.0, 1.0, {
2: 0.5,
1: 0.19673467,
0: 0.11932561,
-1: 0.07237464,
-2: 0.11156508
}),
# Tests with sampling_prob < 1
(1.0, 1.0, 0.8, {
0: 0.69673467,
-1: 0.30326533
}, {
0: 0.6180408,
-1: 0.3819592
}),
(3.0, 3.0, 0.5, {
0: 0.69673467,
-1: 0.30326533
}, {
0: 0.5,
-1: 0.5
}),
(1.0, 2.0, 0.7, {
0: 0.81606028,
-1: 0.08497712,
-2: 0.09896260
}, {
1: 0.49036933,
0: 0.13605478,
-1: 0.37357589
}),
(2.0, 4.0, 0.3, {
0: 0.81606028,
-1: 0.11302356,
-2: 0.07091617
}, {
1: 0.20651251,
0: 0.16706338,
-1: 0.62642411
}))
def test_laplace_varying_parameter_and_sensitivity_optimistic(
self, parameter, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_rounded_pmf_remove=None):
"""Verifies correctness of optimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_laplace_mechanism(
parameter=parameter,
sensitivity=sensitivity,
pessimistic_estimate=False,
value_discretization_interval=1,
sampling_prob=sampling_prob)
_assert_pld_pmf_equal(self, pld,
expected_rounded_pmf_add, 0.0,
expected_rounded_pmf_remove, 0.0)
class GaussianPrivacyLossDistributionTest(parameterized.TestCase):
@parameterized.parameters((1.0, 1.0, -0.1), (1.0, 1.0, 1.1), (1.0, 1.0, 0.0),
(-0.1, 1.0, 1.0), (0.0, 1.0, 1.0), (1.0, -1.0, 1.0),
(1.0, 0.0, 1.0))
def test_gaussian_value_errors(self, standard_deviation, sensitivity,
sampling_prob):
with self.assertRaises(ValueError):
privacy_loss_distribution.from_gaussian_mechanism(
standard_deviation,
sensitivity=sensitivity,
value_discretization_interval=1,
sampling_prob=sampling_prob)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1.0, 1.0, {
2: 0.12447741,
1: 0.38292492,
0: 0.24173034,
-1: 0.0668072
}),
(5.0, 5.0, 1.0, {
2: 0.12447741,
1: 0.38292492,
0: 0.24173034,
-1: 0.0668072
}),
(1.0, 2.0, 1.0, {
-3: 0.00620967,
-2: 0.01654047,
-1: 0.04405707,
0: 0.09184805,
1: 0.14988228,
2: 0.19146246,
3: 0.19146246,
4: 0.12447741
}),
(3.0, 6.0, 1.0, {
-3: 0.00620967,
-2: 0.01654047,
-1: 0.04405707,
0: 0.09184805,
1: 0.14988228,
2: 0.19146246,
3: 0.19146246,
4: 0.12447741
}),
# Tests with sampling_prob < 1
(1.0, 1.0, 0.8, {
1: 0.50740234,
0: 0.25872977,
-1: 0.04980776
}, {
2: 0.06409531,
1: 0.39779076,
0: 0.38512252
}),
(5.0, 5.0, 0.6, {
1: 0.50740234,
0: 0.27649963,
-1: 0.03203791
}, {
2: 0.00921465,
1: 0.40715514,
0: 0.46170751
}),
(1.0, 2.0, 0.4, {
1: 0.65728462,
0: 0.12528727,
-1: 0.02551767,
-2: 0.00785031
}, {
3: 0.06547773,
2: 0.10625501,
1: 0.18525477,
0: 0.56826895
}),
(3.0, 6.0, 0.2, {
1: 0.65728462,
0: 0.14208735,
-1: 0.01356463,
-2: 0.00300327
}, {
3: 0.00957871,
2: 0.05499325,
1: 0.19231652,
0: 0.70480685
}))
def test_gaussian_varying_standard_deviation_and_sensitivity(
self, standard_deviation, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_rounded_pmf_remove=None):
"""Verifies correctness of pessimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_gaussian_mechanism(
standard_deviation,
sensitivity=sensitivity,
log_mass_truncation_bound=math.log(2) + stats.norm.logcdf(-0.9),
value_discretization_interval=1,
sampling_prob=sampling_prob)
test_util.assert_dictionary_almost_equal(self, expected_rounded_pmf_add,
pld._pmf_add._loss_probs) # pytype: disable=attribute-error
test_util.assert_almost_greater_equal(self, stats.norm.cdf(-0.9),
pld._pmf_add._infinity_mass)
if expected_rounded_pmf_remove is None:
self.assertTrue(pld._symmetric)
else:
test_util.assert_dictionary_almost_equal(self,
expected_rounded_pmf_remove,
pld._pmf_remove._loss_probs) # pytype: disable=attribute-error
test_util.assert_almost_greater_equal(self, stats.norm.cdf(-0.9),
pld._pmf_remove._infinity_mass)
self.assertFalse(pld._symmetric)
@parameterized.parameters((0.5, {
3: 0.12447741,
2: 0.19146246,
1: 0.19146246,
0: 0.14988228,
-1: 0.09184805,
-2: 0.06680720
}), (0.3, {
5: 0.05790353,
4: 0.10261461,
3: 0.11559390,
2: 0.11908755,
1: 0.11220275,
0: 0.09668214,
-1: 0.07618934,
-2: 0.0549094,
-3: 0.0361912,
-4: 0.04456546
}))
def test_gaussian_discretization(self, value_discretization_interval,
expected_rounded_pmf):
"""Verifies correctness of pessimistic PLD for varying discretization."""
pld = privacy_loss_distribution.from_gaussian_mechanism(
1,
log_mass_truncation_bound=math.log(2) + stats.norm.logcdf(-0.9),
value_discretization_interval=value_discretization_interval)
test_util.assert_almost_greater_equal(self, stats.norm.cdf(-0.9),
pld._pmf_remove._infinity_mass)
test_util.assert_dictionary_almost_equal(
self, expected_rounded_pmf,
pld._pmf_remove._loss_probs) # pytype: disable=attribute-error
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1.0, 1.0, {
1: 0.30853754,
0: 0.38292492,
-1: 0.24173034,
-2: 0.03809064
}),
(5.0, 5.0, 1.0, {
1: 0.30853754,
0: 0.38292492,
-1: 0.24173034,
-2: 0.03809064
}),
(1.0, 2.0, 1.0, {
3: 0.30853754,
2: 0.19146246,
1: 0.19146246,
0: 0.14988228,
-1: 0.09184805,
-2: 0.04405707,
-3: 0.01654047,
-4: 0.00434385
}),
(3.0, 6.0, 1.0, {
3: 0.30853754,
2: 0.19146246,
1: 0.19146246,
0: 0.14988228,
-1: 0.09184805,
-2: 0.04405707,
-3: 0.01654047,
-4: 0.00434385
}),
# Tests with sampling_prob < 1
(1.0, 1.0, 0.8, {
0: 0.69146246,
-1: 0.25872977,
-2: 0.0210912
}, {
1: 0.21708672,
0: 0.39779076,
-1: 0.32533725
}),
(5.0, 5.0, 0.6, {
0: 0.69146246,
-1: 0.27649963,
-2: 0.00332135
}, {
1: 0.13113735,
0: 0.40715514,
-1: 0.37085352
}),
(1.0, 2.0, 0.4, {
0: 0.84134475,
-1: 0.12528727,
-2: 0.02551767,
-3: 0.0059845
}, {
2: 0.14022127,
1: 0.10625501,
0: 0.18525477,
-1: 0.45708655
}),
(3.0, 6.0, 0.2, {
0: 0.84134475,
-1: 0.14208735,
-2: 0.01356463,
-3: 0.00113746
}, {
2: 0.04788338,
1: 0.05499325,
0: 0.19231652,
-1: 0.55718558
}))
def test_gaussian_varying_standard_deviation_and_sensitivity_optimistic(
self, standard_deviation, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_rounded_pmf_remove=None):
"""Verifies correctness of optimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_gaussian_mechanism(
standard_deviation,
sensitivity=sensitivity,
pessimistic_estimate=False,
log_mass_truncation_bound=math.log(2) + stats.norm.logcdf(-0.9),
value_discretization_interval=1,
sampling_prob=sampling_prob)
test_util.assert_dictionary_almost_equal(self, expected_rounded_pmf_add,
pld._pmf_add._loss_probs) # pytype: disable=attribute-error
test_util.assert_almost_greater_equal(self, stats.norm.cdf(-0.9),
pld._pmf_add._infinity_mass)
if expected_rounded_pmf_remove is None:
self.assertTrue(pld._symmetric)
else:
test_util.assert_dictionary_almost_equal(self,
expected_rounded_pmf_remove,
pld._pmf_remove._loss_probs) # pytype: disable=attribute-error
test_util.assert_almost_greater_equal(self, stats.norm.cdf(-0.9),
pld._pmf_remove._infinity_mass)
self.assertFalse(pld._symmetric)
def test_subsampled_gaussian_does_not_overflow(self):
"""Verifies that creating subsampled Gaussian PLD does not result in overflow."""
privacy_loss_distribution.from_gaussian_mechanism(
0.02,
value_discretization_interval=1,
sampling_prob=0.1)
class DiscreteLaplacePrivacyLossDistributionTest(parameterized.TestCase):
@parameterized.parameters((1.0, 1, -0.1), (1.0, 1, 1.1), (1.0, 1, 0.0),
(-0.1, 1, 1.0), (0.0, 1, 1.0), (1.0, -1, 1.0),
(1.0, 0, 1.0), (1.0, 0.5, 1.0), (1.0, 1.0, 1.0))
def test_discrete_laplace_value_errors(self, parameter, sensitivity,
sampling_prob):
with self.assertRaises(ValueError):
privacy_loss_distribution.from_discrete_laplace_mechanism(
parameter, sensitivity=sensitivity, value_discretization_interval=1,
sampling_prob=sampling_prob)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1, 1, {
1: 0.73105858,
-1: 0.26894142
}),
(1.0, 2, 1, {
2: 0.73105858,
0: 0.17000340,
-2: 0.09893802
}),
(0.8, 2, 1, {
2: 0.68997448,
0: 0.17072207,
-1: 0.13930345
}),
(0.8, 3, 1, {
3: 0.68997448,
1: 0.17072207,
0: 0.07671037,
-2: 0.06259307
}),
# Tests with sampling_prob < 1
(1.0, 1, 0.8, {
1: 0.7310585786300049,
0: 0.2689414213699951
}, {
1: 0.63863515,
0: 0.36136485
}),
(1.0, 2, 0.5, {
1.0: 0.7310585786300049,
0.0: 0.17000340156854787,
-1.0: 0.09893801980144723
}, {
2.0: 0.41499829921572606,
1.0: 0.0,
0.0: 0.5850017007842739
}),
(0.8, 2, 0.3, {
1: 0.6899744811276125,
0: 0.3100255188723875
}, {
1: 0.30450475600966753,
0: 0.6954952439903325
}),
(0.8, 3, 0.2, {
1: 0.8606965547551659,
0: 0.07671037249501267,
-1: 0.0625930727498214
}, {
2: 0.1880693544253796,
1: 0.09551271272152079,
0: 0.7164179328530996,
}))
def test_discrete_laplace_varying_parameter_and_sensitivity(
self, parameter, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_rounded_pmf_remove=None):
"""Verifies correctness of pessimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_discrete_laplace_mechanism(
parameter, sensitivity=sensitivity, value_discretization_interval=1,
sampling_prob=sampling_prob)
_assert_pld_pmf_equal(self, pld,
expected_rounded_pmf_add, 0.0,
expected_rounded_pmf_remove, 0.0)
@parameterized.parameters((0.1, {
10: 0.73105858,
-10: 0.26894142
}), (0.03, {
34: 0.73105858,
-33: 0.26894142
}))
def test_discrete_laplace_discretization(
self, value_discretization_interval,
expected_rounded_pmf):
"""Verifies correctness of pessimistic PLD for varying discretization."""
pld = privacy_loss_distribution.from_discrete_laplace_mechanism(
1, value_discretization_interval=value_discretization_interval)
_assert_pld_pmf_equal(self, pld, expected_rounded_pmf, 0.0)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1, 1, {
1: 0.73105858,
-1: 0.26894142
}),
(1.0, 2, 1, {
2: 0.73105858,
0: 0.17000340,
-2: 0.09893802
}),
(0.8, 2, 1, {
1: 0.68997448,
0: 0.17072207,
-2: 0.13930345
}),
(0.8, 3, 1, {
2: 0.68997448,
0: 0.17072207,
-1: 0.07671037,
-3: 0.06259307
}),
# Tests with sampling_prob < 1
(1.0, 1, 0.8, {
0: 0.7310585786300049,
-1: 0.2689414213699951
}, {
0: 0.63863515,
-1: 0.36136485
}),
(1.0, 2, 0.5, {
0: 0.9010619801985528,
-2: 0.09893801980144723,
}, {
1: 0.41499829921572606,
0: 0.17000340156854787,
-1: 0.41499829921572606
}),
(0.8, 2, 0.3, {
0: 0.8606965547551659,
-1: 0.13930344524483407
}, {
0: 0.47522682963722107,
-1: 0.5247731703627789
}),
(0.8, 3, 0.2, {
0: 0.8606965547551659,
-1: 0.07671037249501267,
-2: 0.0625930727498214
}, {
1: 0.1880693544253796,
0: 0.09551271272152079,
-1: 0.7164179328530996
}))
def test_discrete_laplace_varying_parameter_and_sensitivity_optimistic(
self, parameter, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_rounded_pmf_remove=None):
"""Verifies correctness of optimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_discrete_laplace_mechanism(
parameter, sensitivity=sensitivity, value_discretization_interval=1,
pessimistic_estimate=False,
sampling_prob=sampling_prob)
_assert_pld_pmf_equal(self, pld,
expected_rounded_pmf_add, 0.0,
expected_rounded_pmf_remove, 0.0)
class DiscreteGaussianPrivacyLossDistributionTest(parameterized.TestCase):
@parameterized.parameters((1.0, 1, -0.1), (1.0, 1, 1.1), (1.0, 1, 0.0),
(-0.1, 1, 1.0), (0.0, 1, 1.0), (1.0, -1, 1.0),
(1.0, 0, 1.0), (1.0, 0.5, 1.0), (1.0, 1.0, 1.0))
def test_discrete_gaussian_value_errors(self, sigma, sensitivity,
sampling_prob):
with self.assertRaises(ValueError):
privacy_loss_distribution.from_discrete_gaussian_mechanism(
sigma, sensitivity=sensitivity, truncation_bound=1,
sampling_prob=sampling_prob)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1, 1.0, {
5000: 0.45186276,
-5000: 0.27406862
}, 0.27406862),
(1.0, 2, 1.0, {
0: 0.27406862
}, 0.72593138),
(3.0, 1, 1.0, {
556: 0.34579116,
-555: 0.32710442
}, 0.32710442),
# Tests with sampling_prob < 1
(1.0, 1, 0.6, {
-3287: 0.27406862,
2693: 0.45186276,
9163: 0.27406862
}, 0.0, {
3288: 0.3807451,
-2692: 0.34518628,
-9162: 0.10962745
}, 0.16444117),
(1.0, 2, 0.3, {
0: 0.27406862,
3567: 0.7259314,
}, 0.0, {
0: 0.27406862,
-3566: 0.50815197,
}, 0.2177794),
(3.0, 1, 0.1, {
-56: 0.32710442,
55: 0.34579116,
1054: 0.32710442
}, 0.0, {
57: 0.32897309,
-54: 0.34392248,
-1053: 0.29439398
}, 0.03271044))
def test_discrete_gaussian_varying_sigma_and_sensitivity(
self, sigma, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_infinity_mass_add,
expected_rounded_pmf_remove=None, expected_infinity_mass_remove=None):
"""Verifies correctness of pessimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_discrete_gaussian_mechanism(
sigma, sensitivity=sensitivity, truncation_bound=1,
sampling_prob=sampling_prob)
_assert_pld_pmf_equal(
self, pld,
expected_rounded_pmf_add, expected_infinity_mass_add,
expected_rounded_pmf_remove, expected_infinity_mass_remove)
@parameterized.parameters((2, {
15000: 0.24420134,
5000: 0.40261995,
-5000: 0.24420134,
-15000: 0.05448868
}, 0.05448868), (3, {
25000: 0.05400558,
15000: 0.24203622,
5000: 0.39905027,
-5000: 0.24203623,
-15000: 0.05400558,
-25000: 0.00443305
}, 0.00443305))
def test_discrete_gaussian_truncation(
self, truncation_bound, expected_rounded_pmf, expected_infinity_mass):
"""Verifies correctness of pessimistic PLD for varying truncation bound."""
pld = privacy_loss_distribution.from_discrete_gaussian_mechanism(
1, truncation_bound=truncation_bound)
_assert_pld_pmf_equal(
self, pld, expected_rounded_pmf, expected_infinity_mass)
@parameterized.parameters(
# Tests with sampling_prob = 1
(1.0, 1, 1.0, {
5000: 0.45186276,
-5000: 0.27406862
}, 0.27406862),
(1.0, 2, 1.0, {
0: 0.27406862
}, 0.72593138),
(3.0, 1, 1.0, {
555: 0.34579116,
-556: 0.32710442
}, 0.32710442),
# Tests with sampling_prob < 1
(1.0, 1, 0.6, {
-3288: 0.27406862,
2692: 0.45186276,
9162: 0.27406862
}, 0.0, {
3287: 0.3807451,
-2693: 0.34518628,
-9163: 0.10962745
}, 0.16444117),
(1.0, 2, 0.3, {
0: 0.27406862,
3566: 0.7259314,
}, 0.0, {
0: 0.27406862,
-3567: 0.50815197,
}, 0.2177794),
(3.0, 1, 0.1, {
-57: 0.32710442,
54: 0.34579116,
1053: 0.32710442
}, 0.0, {
56: 0.32897309,
-55: 0.34392248,
-1054: 0.29439398
}, 0.03271044))
def test_discrete_gaussian_varying_sigma_and_sensitivity_optimistic(
self, sigma, sensitivity, sampling_prob,
expected_rounded_pmf_add, expected_infinity_mass_add,
expected_rounded_pmf_remove=None, expected_infinity_mass_remove=None):
"""Verifies correctness of optimistic PLD for various parameter values."""
pld = privacy_loss_distribution.from_discrete_gaussian_mechanism(
sigma, sensitivity=sensitivity, truncation_bound=1,
pessimistic_estimate=False,
sampling_prob=sampling_prob)
_assert_pld_pmf_equal(
self, pld,
expected_rounded_pmf_add, expected_infinity_mass_add,
expected_rounded_pmf_remove, expected_infinity_mass_remove)
class RandomizedResponsePrivacyLossDistributionTest(parameterized.TestCase):
@parameterized.parameters((0.5, 2, {
2: 0.75,
-1: 0.25
}), (0.2, 4, {
3: 0.85,
-2: 0.05,
0: 0.1
}))
def test_randomized_response_basic(
self, noise_parameter, num_buckets,
expected_rounded_pmf):
# Set value_discretization_interval = 1 here.
pld = privacy_loss_distribution.from_randomized_response(
noise_parameter, num_buckets, value_discretization_interval=1)
_assert_pld_pmf_equal(self, pld, expected_rounded_pmf, 0.0)
@parameterized.parameters((0.7, {
5: 0.85,
-4: 0.05,
0: 0.1
}), (2, {
2: 0.85,
-1: 0.05,
0: 0.1
}))
def test_randomized_response_discretization(
self, value_discretization_interval, expected_rounded_pmf):
# Set noise_parameter = 0.2, num_buckets = 4 here.
# The true (non-discretized) PLD is
# {2.83321334: 0.85, -2.83321334: 0.05, 0: 0.1}.
pld = privacy_loss_distribution.from_randomized_response(
0.2, 4, value_discretization_interval=value_discretization_interval)
_assert_pld_pmf_equal(self, pld, expected_rounded_pmf, 0.0)
@parameterized.parameters((0.5, 2, {
1: 0.75,
-2: 0.25
}), (0.2, 4, {
2: 0.85,
-3: 0.05,
0: 0.1
}))
def test_randomized_response_optimistic(
self, noise_parameter, num_buckets, expected_rounded_pmf):
# Set value_discretization_interval = 1 here.
pld = privacy_loss_distribution.from_randomized_response(
noise_parameter,
num_buckets,
pessimistic_estimate=False,
value_discretization_interval=1)
_assert_pld_pmf_equal(self, pld, expected_rounded_pmf, 0.0)
@parameterized.parameters((0.0, 10), (1.1, 4), (0.5, 1))
def test_randomized_response_value_errors(self, noise_parameter, num_buckets):
with self.assertRaises(ValueError):
privacy_loss_distribution.from_randomized_response(
noise_parameter, num_buckets)
class IdentityPrivacyLossDistributionTest(parameterized.TestCase):
def test_identity(self):
pld = privacy_loss_distribution.identity()
_assert_pld_pmf_equal(self, pld, {0: 1}, 0.0)
pld = pld.compose(
privacy_loss_distribution.PrivacyLossDistribution
.create_from_rounded_probability({
1: 0.5,
-1: 0.5
}, 0, 1e-4))
_assert_pld_pmf_equal(self, pld, {1: 0.5, -1: 0.5}, 0.0)
if __name__ == '__main__':
unittest.main()