diff --git a/privacy/optimizers/gaussian_query.py b/privacy/optimizers/gaussian_query.py index 62a7c57..6d1ce55 100644 --- a/privacy/optimizers/gaussian_query.py +++ b/privacy/optimizers/gaussian_query.py @@ -132,12 +132,12 @@ class GaussianAverageQuery(private_queries.PrivateAverageQuery): denominator: The normalization constant (applied after noise is added to the sum). """ - self._sum_query = GaussianSumQuery(l2_norm_clip, sum_stddev) + self._numerator = GaussianSumQuery(l2_norm_clip, sum_stddev) self._denominator = denominator def initial_global_state(self): """Returns the initial global state for the GaussianAverageQuery.""" - sum_global_state = self._sum_query.initial_global_state() + sum_global_state = self._numerator.initial_global_state() return self._GlobalState(sum_global_state, float(self._denominator)) def derive_sample_params(self, global_state): @@ -149,7 +149,7 @@ class GaussianAverageQuery(private_queries.PrivateAverageQuery): Returns: Parameters to use to process records in the next sample. """ - return self._sum_query.derive_sample_params(global_state.sum_state) + return self._numerator.derive_sample_params(global_state.sum_state) def initial_sample_state(self, global_state, tensors): """Returns an initial state to use for the next sample. @@ -162,7 +162,7 @@ class GaussianAverageQuery(private_queries.PrivateAverageQuery): Returns: An initial sample state. """ # GaussianAverageQuery has no state beyond the sum state. - return self._sum_query.initial_sample_state(global_state.sum_state, tensors) + return self._numerator.initial_sample_state(global_state.sum_state, tensors) def accumulate_record(self, params, sample_state, record): """Accumulates a single record into the sample state. @@ -175,7 +175,7 @@ class GaussianAverageQuery(private_queries.PrivateAverageQuery): Returns: The updated sample state. """ - return self._sum_query.accumulate_record(params, sample_state, record) + return self._numerator.accumulate_record(params, sample_state, record) def get_noised_average(self, sample_state, global_state): """Gets noised average after all records of sample have been accumulated. @@ -188,7 +188,7 @@ class GaussianAverageQuery(private_queries.PrivateAverageQuery): A tuple (estimate, new_global_state) where "estimate" is the estimated average of the records and "new_global_state" is the updated global state. """ - noised_sum, new_sum_global_state = self._sum_query.get_noised_sum( + noised_sum, new_sum_global_state = self._numerator.get_noised_sum( sample_state, global_state.sum_state) new_global_state = self._GlobalState( new_sum_global_state, global_state.denominator) diff --git a/privacy/optimizers/no_privacy_query.py b/privacy/optimizers/no_privacy_query.py new file mode 100644 index 0000000..f15c3c3 --- /dev/null +++ b/privacy/optimizers/no_privacy_query.py @@ -0,0 +1,95 @@ +# Copyright 2018, 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 +# +# http://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. +"""Implements PrivateQuery interface for no privacy average queries.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import tensorflow as tf + +from privacy.optimizers import private_queries + +nest = tf.contrib.framework.nest + + +class NoPrivacySumQuery(private_queries.PrivateSumQuery): + """Implements PrivateQuery interface for a sum query with no privacy. + + Accumulates vectors without clipping or adding noise. + """ + + def initial_global_state(self): + """Returns the initial global state for the NoPrivacySumQuery.""" + return None + + def derive_sample_params(self, global_state): + """See base class.""" + del global_state # unused. + return None + + def initial_sample_state(self, global_state, tensors): + """See base class.""" + del global_state # unused. + return nest.map_structure(tf.zeros_like, tensors) + + def accumulate_record(self, params, sample_state, record): + """See base class.""" + del params # unused. + return nest.map_structure(tf.add, sample_state, record) + + def get_noised_sum(self, sample_state, global_state): + """See base class.""" + return sample_state, global_state + + +class NoPrivacyAverageQuery(private_queries.PrivateAverageQuery): + """Implements PrivateQuery interface for an average query with no privacy. + + Accumulates vectors and normalizes by the total number of accumulated vectors. + """ + + def __init__(self): + """Initializes the NoPrivacyAverageQuery.""" + self._numerator = NoPrivacySumQuery() + + def initial_global_state(self): + """Returns the initial global state for the NoPrivacyAverageQuery.""" + return self._numerator.initial_global_state() + + def derive_sample_params(self, global_state): + """See base class.""" + del global_state # unused. + return None + + def initial_sample_state(self, global_state, tensors): + """See base class.""" + return self._numerator.initial_sample_state(global_state, tensors), 0.0 + + def accumulate_record(self, params, sample_state, record): + """See base class.""" + sum_sample_state, denominator = sample_state + return self._numerator.accumulate_record(params, sum_sample_state, + record), tf.add(denominator, 1.0) + + def get_noised_average(self, sample_state, global_state): + """See base class.""" + sum_sample_state, denominator = sample_state + exact_sum, new_global_state = self._numerator.get_noised_sum( + sum_sample_state, global_state) + + def normalize(v): + return tf.truediv(v, denominator) + + return nest.map_structure(normalize, exact_sum), new_global_state diff --git a/privacy/optimizers/no_privacy_query_test.py b/privacy/optimizers/no_privacy_query_test.py new file mode 100644 index 0000000..5c39eba --- /dev/null +++ b/privacy/optimizers/no_privacy_query_test.py @@ -0,0 +1,86 @@ +# Copyright 2018, 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 +# +# http://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 NoPrivacyAverageQuery.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from absl.testing import parameterized +import tensorflow as tf + +from privacy.optimizers import no_privacy_query + +try: + xrange +except NameError: + xrange = range + + +def _run_query(query, records): + """Executes query on the given set of records as a single sample. + + Args: + query: A PrivateQuery to run. + records: An iterable containing records to pass to the query. + + Returns: + The result of the query. + """ + global_state = query.initial_global_state() + params = query.derive_sample_params(global_state) + sample_state = query.initial_sample_state(global_state, next(iter(records))) + for record in records: + sample_state = query.accumulate_record(params, sample_state, record) + result, _ = query.get_query_result(sample_state, global_state) + return result + + +class NoPrivacyQueryTest(tf.test.TestCase, parameterized.TestCase): + + def test_no_privacy_sum(self): + with self.cached_session() as sess: + record1 = tf.constant([2.0, 0.0]) + record2 = tf.constant([-1.0, 1.0]) + + query = no_privacy_query.NoPrivacySumQuery() + query_result = _run_query(query, [record1, record2]) + result = sess.run(query_result) + expected = [1.0, 1.0] + self.assertAllClose(result, expected) + + def test_no_privacy_average(self): + with self.cached_session() as sess: + record1 = tf.constant([5.0, 0.0]) + record2 = tf.constant([-1.0, 2.0]) + + query = no_privacy_query.NoPrivacyAverageQuery() + query_result = _run_query(query, [record1, record2]) + result = sess.run(query_result) + expected_average = [2.0, 1.0] + self.assertAllClose(result, expected_average) + + @parameterized.named_parameters( + ('type_mismatch', [1.0], (1.0,), TypeError), + ('too_few_on_left', [1.0], [1.0, 1.0], ValueError), + ('too_few_on_right', [1.0, 1.0], [1.0], ValueError)) + def test_incompatible_records(self, record1, record2, error_type): + query = no_privacy_query.NoPrivacySumQuery() + with self.assertRaises(error_type): + _run_query(query, [record1, record2]) + + +if __name__ == '__main__': + tf.test.main()