Skip to content
Open
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
1 change: 1 addition & 0 deletions python/dp_accounting/dp_accounting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from dp_accounting.dp_event import LaplaceDpEvent
from dp_accounting.dp_event import NonPrivateDpEvent
from dp_accounting.dp_event import NoOpDpEvent
from dp_accounting.dp_event import PermuteAndFlipDpEvent
from dp_accounting.dp_event import PoissonSampledDpEvent
from dp_accounting.dp_event import RandomizedResponseDpEvent
from dp_accounting.dp_event import SampledWithoutReplacementDpEvent
Expand Down
34 changes: 34 additions & 0 deletions python/dp_accounting/dp_accounting/dp_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ class RandomizedResponseDpEvent(DpEvent):
corresponding to the case where the mechanism outputs a bucket drawn
uniformly at random from the k buckets regardless of the input bucket.
"""

noise_parameter: float
num_buckets: int

Expand All @@ -268,6 +269,7 @@ class EpsilonDeltaDpEvent(DpEvent):
characterize the privacy loss. If a more specific DpEvent that characterizes
a mechanism is available, it should be preferred over this one.
"""

epsilon: float
delta: float

Expand All @@ -280,6 +282,7 @@ class GaussianDpEvent(DpEvent):
If the norms of the values are bounded ||v_i|| <= C, the noise_multiplier is
defined as s / C.
"""

noise_multiplier: float


Expand All @@ -294,6 +297,7 @@ class LaplaceDpEvent(DpEvent):
If the L_1 norm of the values are bounded ||v_i||_1 <= C, the noise_multiplier
is defined as s / C.
"""

noise_multiplier: float


Expand All @@ -308,6 +312,7 @@ class DiscreteLaplaceDpEvent(DpEvent):
integer value x. If the L_1 norm of the values are bounded ||v_i||_1 <= C,
the sensitivity is `C`.
"""

noise_parameter: float
sensitivity: int

Expand Down Expand Up @@ -349,6 +354,7 @@ class SelfComposedDpEvent(DpEvent):
This is equivalent to `ComposedDpEvent` that contains a list of length `count`
of identical copies of `event`.
"""

event: DpEvent
count: int

Expand All @@ -360,6 +366,7 @@ class ComposedDpEvent(DpEvent):
The composition may be adaptive, where the query producing each event depends
on the results of prior queries.
"""

events: List[DpEvent]


Expand All @@ -371,6 +378,7 @@ class PoissonSampledDpEvent(DpEvent):
probability `sampling_probability`. Then the `DpEvent` `event` is applied
to the sample of records.
"""

sampling_probability: float
event: DpEvent

Expand All @@ -384,6 +392,7 @@ class SampledWithReplacementDpEvent(DpEvent):
`source_dataset_size`. Then the `DpEvent` `event` is applied to the sample of
records.
"""

source_dataset_size: int
sample_size: int
event: DpEvent
Expand All @@ -397,6 +406,7 @@ class SampledWithoutReplacementDpEvent(DpEvent):
set of possible samples of a source dataset of size `source_dataset_size`.
Then the `DpEvent` `event` is applied to the sample of records.
"""

source_dataset_size: int
sample_size: int
event: DpEvent
Expand All @@ -420,6 +430,7 @@ class SingleEpochTreeAggregationDpEvent(DpEvent):
step_counts: The number of steps in each tree. May be a scalar for a single
tree.
"""

noise_multiplier: float
step_counts: Union[int, List[int]]

Expand All @@ -439,6 +450,7 @@ class RepeatAndSelectDpEvent(DpEvent):
mean: The mean number of repetitions.
shape: The shape of the distribution of the number of repetitions.
"""

event: DpEvent
mean: float
shape: float
Expand Down Expand Up @@ -499,6 +511,28 @@ class ExponentialMechanismDpEvent(DpEvent):
epsilon: float


@attr.s(frozen=True, slots=True, auto_attribs=True)
class PermuteAndFlipDpEvent(DpEvent):
"""Represents an application of the permute-and-flip mechanism.

See https://arxiv.org/abs/2010.12603 (McKenna & Sheldon, 2020) for details.

Unlike the exponential mechanism which is epsilon-bounded range, the
permute-and-flip mechanism satisfies standard epsilon-DP. Its privacy loss
distribution is identical to that of the Laplace mechanism with parameter
1/epsilon. A dedicated event type is provided for API clarity and to allow
future tighter analyses.

For RDP accounting, we use the tight Laplace RDP formula (Mironov, 2017).
For PLD accounting, we use the Laplace privacy loss distribution.

Attributes:
epsilon: The epsilon parameter of the permute-and-flip mechanism.
"""

epsilon: float


@attr.s(frozen=True, slots=True, auto_attribs=True)
class TruncatedSubsampledGaussianDpEvent(DpEvent):
"""Represents the Gaussian mechanism with truncated Poisson sampling.
Expand Down
4 changes: 4 additions & 0 deletions python/dp_accounting/dp_accounting/dp_event_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class DpEventTest(parameterized.TestCase):
'exponential_mechanism',
dp_event.ExponentialMechanismDpEvent(1.0),
),
(
'permute_and_flip',
dp_event.PermuteAndFlipDpEvent(1.0),
),
(
'zcdp',
dp_event.ZCDpEvent(1.0, 2.0),
Expand Down
79 changes: 58 additions & 21 deletions python/dp_accounting/dp_accounting/pld/pld_privacy_accountant.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from dp_accounting.pld import common
from dp_accounting.pld import privacy_loss_distribution


NeighborRel = privacy_accountant.NeighboringRelation
CompositionErrorDetails = (
privacy_accountant.PrivacyAccountant.CompositionErrorDetails
Expand All @@ -43,11 +42,13 @@ def __init__(
super().__init__(neighboring_relation)
self._contains_non_dp_event = False
self._pld = PLD.identity(
value_discretization_interval=value_discretization_interval)
value_discretization_interval=value_discretization_interval
)
self._value_discretization_interval = value_discretization_interval

def _maybe_compose(self, event: dp_event.DpEvent, count: int,
do_compose: bool) -> Optional[CompositionErrorDetails]:
def _maybe_compose(
self, event: dp_event.DpEvent, count: int, do_compose: bool
) -> Optional[CompositionErrorDetails]:
if isinstance(event, dp_event.NoOpDpEvent):
return None
elif isinstance(event, dp_event.NonPrivateDpEvent):
Expand All @@ -74,14 +75,18 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
)
return None
elif isinstance(event, dp_event.RandomizedResponseDpEvent):
if self.neighboring_relation not in [NeighborRel.REPLACE_ONE,
NeighborRel.REPLACE_SPECIAL]:
if self.neighboring_relation not in [
NeighborRel.REPLACE_ONE,
NeighborRel.REPLACE_SPECIAL,
]:
error_msg = (
'neighboring_relation must be `REPLACE_ONE` or '
'`REPLACE_SPECIAL` for `RandomizedResponseDpEvent`. Found '
f'{self._neighboring_relation}.')
f'{self._neighboring_relation}.'
)
return CompositionErrorDetails(
invalid_event=event, error_message=error_msg)
invalid_event=event, error_message=error_msg
)
if do_compose:
if event.num_buckets == 1:
# This is a NoOp event, even when noise_parameter is zero.
Expand All @@ -105,12 +110,14 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
gaussian_pld = PLD.from_gaussian_mechanism(
standard_deviation=event.noise_multiplier / math.sqrt(count),
value_discretization_interval=self._value_discretization_interval,
neighboring_relation=self.neighboring_relation)
neighboring_relation=self.neighboring_relation,
)
self._pld = self._pld.compose(gaussian_pld)
return None
elif isinstance(event, dp_event.LaplaceDpEvent):
if self.neighboring_relation not in [
NeighborRel.ADD_OR_REMOVE_ONE, NeighborRel.REPLACE_SPECIAL
NeighborRel.ADD_OR_REMOVE_ONE,
NeighborRel.REPLACE_SPECIAL,
]:
return CompositionErrorDetails(
invalid_event=event,
Expand All @@ -126,13 +133,14 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
else:
laplace_pld = PLD.from_laplace_mechanism(
parameter=event.noise_multiplier,
value_discretization_interval=self._value_discretization_interval
value_discretization_interval=self._value_discretization_interval,
).self_compose(count)
self._pld = self._pld.compose(laplace_pld)
return None
elif isinstance(event, dp_event.DiscreteLaplaceDpEvent):
if self.neighboring_relation not in [
NeighborRel.ADD_OR_REMOVE_ONE, NeighborRel.REPLACE_SPECIAL
NeighborRel.ADD_OR_REMOVE_ONE,
NeighborRel.REPLACE_SPECIAL,
]:
return CompositionErrorDetails(
invalid_event=event,
Expand All @@ -149,13 +157,14 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
discrete_laplace_pld = PLD.from_discrete_laplace_mechanism(
parameter=event.noise_parameter,
sensitivity=event.sensitivity,
value_discretization_interval=self._value_discretization_interval
value_discretization_interval=self._value_discretization_interval,
).self_compose(count)
self._pld = self._pld.compose(discrete_laplace_pld)
return None
elif isinstance(event, dp_event.MixtureOfGaussiansDpEvent):
if self.neighboring_relation not in [
NeighborRel.ADD_OR_REMOVE_ONE, NeighborRel.REPLACE_SPECIAL
NeighborRel.ADD_OR_REMOVE_ONE,
NeighborRel.REPLACE_SPECIAL,
]:
return CompositionErrorDetails(
invalid_event=event,
Expand Down Expand Up @@ -194,6 +203,33 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
).self_compose(count)
self._pld = self._pld.compose(eps_dp_pld)
return None
elif isinstance(event, dp_event.PermuteAndFlipDpEvent):
if self.neighboring_relation not in [
NeighborRel.ADD_OR_REMOVE_ONE,
NeighborRel.REPLACE_SPECIAL,
]:
return CompositionErrorDetails(
invalid_event=event,
error_message=(
'neighboring_relation must be `ADD_OR_REMOVE_ONE` or '
'`REPLACE_SPECIAL` for `PermuteAndFlipDpEvent`. Found '
f'{self._neighboring_relation}.'
),
)
if do_compose:
if event.epsilon < 0:
raise ValueError(f'epsilon must be >= 0. Got {event.epsilon}')
if event.epsilon == 0:
pass # NoOp: zero epsilon means infinite noise.
else:
# Permute-and-flip has the same privacy loss distribution as the
# Laplace mechanism with parameter 1/epsilon.
pf_pld = PLD.from_laplace_mechanism(
parameter=1.0 / event.epsilon,
value_discretization_interval=self._value_discretization_interval,
).self_compose(count)
self._pld = self._pld.compose(pf_pld)
return None
elif isinstance(event, dp_event.PoissonSampledDpEvent):
if isinstance(event.event, dp_event.GaussianDpEvent):
if do_compose:
Expand All @@ -204,16 +240,16 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
else:
subsampled_gaussian_pld = PLD.from_gaussian_mechanism(
standard_deviation=event.event.noise_multiplier,
value_discretization_interval=self
._value_discretization_interval,
value_discretization_interval=self._value_discretization_interval,
sampling_prob=event.sampling_probability,
neighboring_relation=self.neighboring_relation,
).self_compose(count)
self._pld = self._pld.compose(subsampled_gaussian_pld)
return None
elif isinstance(event.event, dp_event.LaplaceDpEvent):
if self.neighboring_relation not in [
NeighborRel.ADD_OR_REMOVE_ONE, NeighborRel.REPLACE_SPECIAL
NeighborRel.ADD_OR_REMOVE_ONE,
NeighborRel.REPLACE_SPECIAL,
]:
return CompositionErrorDetails(
invalid_event=event,
Expand All @@ -231,9 +267,9 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
else:
subsampled_laplace_pld = PLD.from_laplace_mechanism(
parameter=event.event.noise_multiplier,
value_discretization_interval=self
._value_discretization_interval,
sampling_prob=event.sampling_probability).self_compose(count)
value_discretization_interval=self._value_discretization_interval,
sampling_prob=event.sampling_probability,
).self_compose(count)
self._pld = self._pld.compose(subsampled_laplace_pld)
return None
else:
Expand Down Expand Up @@ -270,7 +306,8 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
else:
# Unsupported event (including `UnsupportedDpEvent`).
return CompositionErrorDetails(
invalid_event=event, error_message='Unsupported event.')
invalid_event=event, error_message='Unsupported event.'
)

def get_epsilon(self, target_delta: float) -> float:
if self._contains_non_dp_event:
Expand Down
Loading
Loading