diff --git a/tensorflow_privacy/privacy/analysis/dp_event.py b/tensorflow_privacy/privacy/analysis/dp_event.py new file mode 100644 index 0000000..fbec1d1 --- /dev/null +++ b/tensorflow_privacy/privacy/analysis/dp_event.py @@ -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 diff --git a/tensorflow_privacy/privacy/analysis/dp_event_builder.py b/tensorflow_privacy/privacy/analysis/dp_event_builder.py new file mode 100644 index 0000000..a0a9435 --- /dev/null +++ b/tensorflow_privacy/privacy/analysis/dp_event_builder.py @@ -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 diff --git a/tensorflow_privacy/privacy/analysis/dp_event_builder_test.py b/tensorflow_privacy/privacy/analysis/dp_event_builder_test.py new file mode 100644 index 0000000..a10d4bb --- /dev/null +++ b/tensorflow_privacy/privacy/analysis/dp_event_builder_test.py @@ -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() diff --git a/tensorflow_privacy/privacy/analysis/privacy_accountant.py b/tensorflow_privacy/privacy/analysis/privacy_accountant.py new file mode 100644 index 0000000..2f1265f --- /dev/null +++ b/tensorflow_privacy/privacy/analysis/privacy_accountant.py @@ -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()