New DpEvent/PrivacyAccountant libraries.

PiperOrigin-RevId: 392977699
This commit is contained in:
Galen Andrew 2021-08-25 14:15:39 -07:00 committed by A. Unique TensorFlower
parent 853b18929d
commit 433b66b316
4 changed files with 336 additions and 0 deletions

View file

@ -0,0 +1,84 @@
# Copyright 2021, The TensorFlow Authors.
#
# 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
#
# https://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.
"""Standard DpEvent classes."""
from typing import List
import attr
class DpEvent(object):
"""Base class for `DpEvent`s.
A `DpEvent` describes a differentially private mechanism sufficiently for
computing the associated privacy losses, both in isolation and in combination
with other `DpEvent`s.
"""
@attr.s(frozen=True)
class NoOpDpEvent(DpEvent):
"""A `DpEvent` to represent operations with no privacy impact.
A `NoOpDpEvent` is generally never required, but it can be useful as a
placeholder where a `DpEvent` is expected, such as in tests or some live
accounting pipelines.
"""
@attr.s(frozen=True, slots=True, auto_attribs=True)
class GaussianDpEvent(DpEvent):
"""The Gaussian mechanism."""
noise_multiplier: float
@attr.s(frozen=True, slots=True, auto_attribs=True)
class SelfComposedDpEvent(DpEvent):
"""A mechanism composed with itself multiple times."""
event: DpEvent
count: int
@attr.s(frozen=True, slots=True, auto_attribs=True)
class ComposedDpEvent(DpEvent):
"""A series of composed mechanisms."""
events: List[SelfComposedDpEvent]
@attr.s(frozen=True, slots=True, auto_attribs=True)
class PoissonSampledDpEvent(DpEvent):
"""An application of Poisson subsampling."""
sampling_probability: float
event: DpEvent
@attr.s(frozen=True, slots=True, auto_attribs=True)
class EqualBatchSampledDpEvent(DpEvent):
"""An application of sampling exactly `batch_size` records."""
dataset_size: int
batch_size: int
event: DpEvent
@attr.s(frozen=True, slots=True, auto_attribs=True)
class ShuffledDatasetDpEvent(DpEvent):
"""Shuffling a dataset and applying a mechanism to each partition."""
partition_events: ComposedDpEvent
@attr.s(frozen=True, slots=True, auto_attribs=True)
class TreeAggregationDpEvent(DpEvent):
"""Applying a series of mechanisms with tree aggregation."""
round_events: ComposedDpEvent
max_record_occurences_across_all_rounds: int

View file

@ -0,0 +1,78 @@
# Copyright 2021, The TensorFlow Authors.
#
# 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
#
# https://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.
"""Builder class for ComposedDpEvent."""
import collections
from tensorflow_privacy.privacy.analysis import dp_event
class DpEventBuilder(object):
"""Constructs a `DpEvent` representing the composition of a series of events.
Two common use cases of the `DpEventBuilder` are 1) for producing and tracking
a ledger of `DpEvent`s during sequential accounting using a
`PrivacyAccountant`, and 2) for building up a description of a composite
mechanism for subsequent batch accounting.
"""
def __init__(self):
self._events = collections.OrderedDict()
self._composed_event = None
def compose(self, event: dp_event.DpEvent, count: int = 1):
"""Composes new event into event represented by builder.
Args:
event: The new event to compose.
count: The number of times to compose the event.
"""
if not isinstance(event, dp_event.DpEvent):
raise TypeError('`event` must be a subclass of `DpEvent`. '
f'Found {type(event)}.')
if not isinstance(count, int):
raise TypeError(f'`count` must be an integer. Found {type(count)}.')
if count < 1:
raise ValueError(f'`count` must be positive. Found {count}.')
if isinstance(event, dp_event.ComposedDpEvent):
for composed_event in event.events:
self.compose(composed_event, count)
elif isinstance(event, dp_event.SelfComposedDpEvent):
self.compose(event.event, count * event.count)
elif isinstance(event, dp_event.NoOpDpEvent):
return
else:
current_count = self._events.get(event, 0)
self._events[event] = current_count + count
self._composed_event = None
def build(self) -> dp_event.DpEvent:
"""Builds and returns the composed DpEvent represented by the builder."""
if not self._composed_event:
self_composed_events = []
for event, num_self_compositions in self._events.items():
if num_self_compositions == 1:
self_composed_events.append(event)
else:
self_composed_events.append(
dp_event.SelfComposedDpEvent(event, num_self_compositions))
if not self_composed_events:
return dp_event.NoOpDpEvent()
elif len(self_composed_events) == 1:
self._composed_event = self_composed_events[0]
else:
self._composed_event = dp_event.ComposedDpEvent(self_composed_events)
return self._composed_event

View file

@ -0,0 +1,73 @@
# Copyright 2021, The TensorFlow Authors.
#
# 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
#
# https://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 DpEventBuilder."""
from absl.testing import absltest
from tensorflow_privacy.privacy.analysis import dp_event
from tensorflow_privacy.privacy.analysis import dp_event_builder
_gaussian_event = dp_event.GaussianDpEvent(1.0)
_poisson_event = dp_event.PoissonSampledDpEvent(_gaussian_event, 0.1)
_self_composed_event = dp_event.SelfComposedDpEvent(_gaussian_event, 3)
_composed_event = dp_event.ComposedDpEvent(
[_self_composed_event, _poisson_event])
class DpEventBuilderTest(absltest.TestCase):
def test_no_op(self):
builder = dp_event_builder.DpEventBuilder()
self.assertEqual(dp_event.NoOpDpEvent(), builder.build())
def test_single(self):
builder = dp_event_builder.DpEventBuilder()
builder.compose(_gaussian_event)
self.assertEqual(_gaussian_event, builder.build())
def test_compose_no_op(self):
builder = dp_event_builder.DpEventBuilder()
builder.compose(dp_event.NoOpDpEvent())
builder.compose(_gaussian_event)
builder.compose(dp_event.NoOpDpEvent())
self.assertEqual(_gaussian_event, builder.build())
def test_compose_self(self):
builder = dp_event_builder.DpEventBuilder()
builder.compose(_gaussian_event)
builder.compose(_gaussian_event, 2)
self.assertEqual(_self_composed_event, builder.build())
def test_compose_heterogenous(self):
builder = dp_event_builder.DpEventBuilder()
builder.compose(_gaussian_event)
builder.compose(_poisson_event)
builder.compose(_gaussian_event, 2)
self.assertEqual(_composed_event, builder.build())
def test_compose_complex(self):
builder = dp_event_builder.DpEventBuilder()
builder.compose(_gaussian_event, 2)
builder.compose(_composed_event)
builder.compose(_poisson_event)
builder.compose(_composed_event, 2)
expected_event = dp_event.ComposedDpEvent([
dp_event.SelfComposedDpEvent(_gaussian_event, 11),
dp_event.SelfComposedDpEvent(_poisson_event, 4)
])
self.assertEqual(expected_event, builder.build())
if __name__ == '__main__':
absltest.main()

View file

@ -0,0 +1,101 @@
# Copyright 2021, The TensorFlow Authors.
#
# 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
#
# https://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.
"""PrivacyAccountant abstract base class."""
import abc
from tensorflow_privacy.privacy.dp_event import dp_event
from tensorflow_privacy.privacy.dp_event import dp_event_builder
class PrivacyAccountant(metaclass=abc.ABCMeta):
"""Abstract base class for privacy accountants."""
def __init__(self):
self._ledger = dp_event_builder.DpEventBuilder()
@abc.abstractmethod
def is_supported(self, event: dp_event.DpEvent) -> bool:
"""Checks whether the `DpEvent` can be processed by this accountant.
In general this will require recursively checking the structure of the
`DpEvent`. In particular `ComposedDpEvent` and `SelfComposedDpEvent` should
be recursively examined.
Args:
event: The `DpEvent` to check.
Returns:
True iff this accountant supports processing `event`.
"""
@abc.abstractmethod
def _compose(self, event: dp_event.DpEvent, count: int = 1):
"""Update internal state to account for application of a `DpEvent`.
Calls to `get_epsilon` or `get_delta` after calling `_compose` will return
values that account for this `DpEvent`.
Args:
event: A `DpEvent` to process.
count: The number of times to compose the event.
"""
def compose(self, event: dp_event.DpEvent, count: int = 1):
"""Update internal state to account for application of a `DpEvent`.
Calls to `get_epsilon` or `get_delta` after calling `compose` will return
values that account for this `DpEvent`.
Args:
event: A `DpEvent` to process.
count: The number of times to compose the event.
Raises:
TypeError: `event` is not supported by this `PrivacyAccountant`.
"""
if not self.is_supported(event):
raise TypeError(f'`DpEvent` {event} is of unsupported type.')
self._ledger.compose(event, count)
self._compose(event, count)
@property
def ledger(self) -> dp_event.DpEvent:
"""Returns the (composed) `DpEvent` processed so far by this accountant."""
return self._ledger.build()
@abc.abstractmethod
def get_epsilon(self, target_delta: float) -> float:
"""Gets the current epsilon.
Args:
target_delta: The target delta.
Returns:
The current epsilon, accounting for all composed `DpEvent`s.
"""
def get_delta(self, target_epsilon: float) -> float:
"""Gets the current delta.
An implementer of `PrivacyAccountant` may choose not to override this, in
which case `NotImplementedError` will be raised.
Args:
target_epsilon: The target epsilon.
Returns:
The current delta, accounting for all composed `DpEvent`s.
"""
raise NotImplementedError()