forked from 626_privacy/tensorflow_privacy
New DpEvent/PrivacyAccountant libraries.
PiperOrigin-RevId: 392977699
This commit is contained in:
parent
853b18929d
commit
433b66b316
4 changed files with 336 additions and 0 deletions
84
tensorflow_privacy/privacy/analysis/dp_event.py
Normal file
84
tensorflow_privacy/privacy/analysis/dp_event.py
Normal 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
|
78
tensorflow_privacy/privacy/analysis/dp_event_builder.py
Normal file
78
tensorflow_privacy/privacy/analysis/dp_event_builder.py
Normal 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
|
73
tensorflow_privacy/privacy/analysis/dp_event_builder_test.py
Normal file
73
tensorflow_privacy/privacy/analysis/dp_event_builder_test.py
Normal 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()
|
101
tensorflow_privacy/privacy/analysis/privacy_accountant.py
Normal file
101
tensorflow_privacy/privacy/analysis/privacy_accountant.py
Normal 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()
|
Loading…
Reference in a new issue