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 @@ -20,6 +20,7 @@
from dp_accounting import pld
from dp_accounting import privacy_accountant
from dp_accounting import rdp
from dp_accounting.dp_event import ApproximateDpEvent
from dp_accounting.dp_event import ComposedDpEvent
from dp_accounting.dp_event import DiscreteGaussianDpEvent
from dp_accounting.dp_event import DpEvent
Expand Down
25 changes: 25 additions & 0 deletions python/dp_accounting/dp_accounting/dp_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,28 @@ class TruncatedSubsampledGaussianDpEvent(DpEvent):
sampling_probability: float
truncated_batch_size: int
noise_multiplier: float


@attr.s(frozen=True, slots=True, auto_attribs=True)
class ApproximateDpEvent(DpEvent):
"""Represents a mechanism with an additional additive delta term.

If the inner `event` satisfies (epsilon, delta_inner)-DP for some
(epsilon, delta_inner) pair, then this event satisfies
(epsilon, delta_inner + delta)-DP. The `delta` field captures an additional
probability of failure that is separate from the privacy loss distribution
of the inner mechanism. A canonical example is the Gaussian Thresholding
algorithm for partition selection, where the inner event is a Gaussian
mechanism and the extra delta accounts for the thresholding step.

Accountants process this by composing the inner event normally and tracking
the extra delta additively. At query time, `get_epsilon(target_delta)` uses
`target_delta - sum(extra_deltas)` as the effective delta for the inner
events.

Attributes:
event: The inner DpEvent whose privacy loss is tracked by the accountant.
delta: The additional delta term. Must be in [0, 1].
"""
event: DpEvent
delta: float
11 changes: 11 additions & 0 deletions python/dp_accounting/dp_accounting/dp_event_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ class DpEventTest(parameterized.TestCase):
1.0,
),
),
(
'approximate',
dp_event.ApproximateDpEvent(dp_event.GaussianDpEvent(1.0), 0.01),
),
(
'approximate_composed',
dp_event.ComposedDpEvent([
dp_event.ApproximateDpEvent(dp_event.GaussianDpEvent(1.0), 0.01),
dp_event.LaplaceDpEvent(1.0),
]),
),
)
def test_to_from_named_tuple(self, event):
named_tuple = event.to_named_tuple()
Expand Down
47 changes: 38 additions & 9 deletions python/dp_accounting/dp_accounting/pld/pld_privacy_accountant.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
if result is not None:
return result
return None
elif isinstance(event, dp_event.ApproximateDpEvent):
# REVIEWER NOTE: The extra delta scales linearly with `count` via basic
# composition. If this event is inside SelfComposedDpEvent(event, T),
# then count = T, so we accumulate T * delta. This is the standard
# advanced composition bound for additive delta terms.
if do_compose:
self._extra_delta += count * event.delta
return self._maybe_compose(event.event, count, do_compose)
elif isinstance(event, dp_event.EpsilonDeltaDpEvent):
if do_compose:
self._pld = self._pld.compose(
Expand Down Expand Up @@ -195,23 +203,36 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
self._pld = self._pld.compose(eps_dp_pld)
return None
elif isinstance(event, dp_event.PoissonSampledDpEvent):
if isinstance(event.event, dp_event.GaussianDpEvent):
# REVIEWER NOTE: When ApproximateDpEvent wraps the sub-event of a
# PoissonSampledDpEvent, we strip it and track the extra delta
# *without* subsampling amplification. This is a conservative upper
# bound: the extra delta may represent a mechanism failure probability
# that is not reduced by subsampling. The inner event IS amplified by
# subsampling as usual. We only add the delta when the mechanism is
# actually invoked (sampling_probability > 0 and noise > 0).
inner_event = event.event
approximate_delta = 0.0
if isinstance(inner_event, dp_event.ApproximateDpEvent):
approximate_delta = inner_event.delta
inner_event = inner_event.event
if isinstance(inner_event, dp_event.GaussianDpEvent):
if do_compose:
if event.sampling_probability == 0:
pass
elif event.event.noise_multiplier == 0:
elif inner_event.noise_multiplier == 0:
self._contains_non_dp_event = True
else:
self._extra_delta += count * approximate_delta
subsampled_gaussian_pld = PLD.from_gaussian_mechanism(
standard_deviation=event.event.noise_multiplier,
standard_deviation=inner_event.noise_multiplier,
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):
elif isinstance(inner_event, dp_event.LaplaceDpEvent):
if self.neighboring_relation not in [
NeighborRel.ADD_OR_REMOVE_ONE, NeighborRel.REPLACE_SPECIAL
]:
Expand All @@ -226,11 +247,12 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
if do_compose:
if event.sampling_probability == 0:
pass
elif event.event.noise_multiplier == 0:
elif inner_event.noise_multiplier == 0:
self._contains_non_dp_event = True
else:
self._extra_delta += count * approximate_delta
subsampled_laplace_pld = PLD.from_laplace_mechanism(
parameter=event.event.noise_multiplier,
parameter=inner_event.noise_multiplier,
value_discretization_interval=self
._value_discretization_interval,
sampling_prob=event.sampling_probability).self_compose(count)
Expand All @@ -241,7 +263,8 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
invalid_event=event,
error_message=(
'Subevent of `PoissonSampledEvent` must be either '
f'`GaussianDpEvent` or `LaplaceDpEvent`. Found {event.event}.'
'`GaussianDpEvent` or `LaplaceDpEvent` (optionally wrapped '
f'in `ApproximateDpEvent`). Found {inner_event}.'
),
)
elif isinstance(event, dp_event.TruncatedSubsampledGaussianDpEvent):
Expand Down Expand Up @@ -275,12 +298,18 @@ def _maybe_compose(self, event: dp_event.DpEvent, count: int,
def get_epsilon(self, target_delta: float) -> float:
if self._contains_non_dp_event:
return math.inf
return self._pld.get_epsilon_for_delta(target_delta)
# REVIEWER NOTE: Subtract the accumulated extra delta from
# ApproximateDpEvents. If the remaining delta budget is negative,
# the approximate terms alone exhaust the budget.
effective_delta = target_delta - self._extra_delta
if effective_delta < 0:
return math.inf
return self._pld.get_epsilon_for_delta(effective_delta)

def get_delta(self, target_epsilon: float) -> float:
if self._contains_non_dp_event:
return 1
return self._pld.get_delta_for_epsilon(target_epsilon) # pytype: disable=bad-return-type
return self._pld.get_delta_for_epsilon(target_epsilon) + self._extra_delta # pytype: disable=bad-return-type

def get_true_positive_rates(
self,
Expand Down
Loading
Loading