forked from google/differential-privacy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
privacy_accountant.py
160 lines (123 loc) · 5.45 KB
/
privacy_accountant.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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
import enum
from typing import Any, Optional
import attr
from dp_accounting import dp_event
from dp_accounting import dp_event_builder
class NeighboringRelation(enum.Enum):
ADD_OR_REMOVE_ONE = 1
REPLACE_ONE = 2
# A record is replaced with a special record, such as the "zero record". See
# https://arxiv.org/pdf/2103.00039.pdf, Definition 1.1.
REPLACE_SPECIAL = 3
class UnsupportedEventError(Exception):
"""Exception to raise if _compose is called on unsupported event type."""
class PrivacyAccountant(metaclass=abc.ABCMeta):
"""Abstract base class for privacy accountants."""
def __init__(self, neighboring_relation: NeighboringRelation):
self._neighboring_relation = neighboring_relation
self._ledger = dp_event_builder.DpEventBuilder()
@property
def neighboring_relation(self) -> NeighboringRelation:
"""The neighboring relation used by the accountant.
The neighboring relation is expected to remain constant after
initialization. Subclasses should not override this property or change the
value of the private attribute.
"""
return self._neighboring_relation
def supports(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`.
"""
return self._maybe_compose(event, 0, False) is None
@attr.s(frozen=True, slots=True, auto_attribs=True)
class CompositionErrorDetails(object):
"""Describes offending subevent and error in case composition fails."""
invalid_event: Optional[dp_event.DpEvent]
error_message: Optional[str]
@abc.abstractmethod
def _maybe_compose(self, event: dp_event.DpEvent, count: int,
do_compose: bool) -> Optional[CompositionErrorDetails]:
"""Traverses `event` and performs composition if `do_compose` is True.
If `do_compose` is True, updates internal state to account for application
of a `DpEvent`. Subsequent calls to `get_epsilon` or `get_delta` will return
values that account for composition of this `DpEvent`.
If `do_compose` is False, traverses structure of event to check whether
composition is supported *without updating internal state*. Returns None if
composition would succeed, otherwise returns a `CompositionErrorDetails`
with information about why the composition would fail.
Args:
event: A `DpEvent` to process.
count: The number of times to compose the event.
do_compose: Whether to actually perform the composition.
Returns:
None if composition is valid. If composition is not supported, returns
`CompositionErrorDetails` describing why the composition fails.
"""
def compose(self, event: dp_event.DpEvent, count: int = 1) -> Any:
"""Updates 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.
Returns:
Reference to this privacy accountant. This allows one to write, e.g.,
`eps = Accountant().compose(event1).compose(event2).get_epsilon(delta)`.
Raises:
UnsupportedEventError: `event` is not supported by this
`PrivacyAccountant`.
"""
if not isinstance(event, dp_event.DpEvent):
raise TypeError(f'`event` must be `DpEvent`. Found {type(event)}.')
composition_error = self._maybe_compose(event, count, False)
if composition_error:
raise UnsupportedEventError(
f'Unsupported event: {event}. Error: '
f'[{composition_error.error_message}] caused by subevent '
f'{composition_error.invalid_event}.')
self._ledger.compose(event, count)
self._maybe_compose(event, count, True)
return self
@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()