forked from 626_privacy/tensorflow_privacy
Add missing licenses.
PiperOrigin-RevId: 229241117
This commit is contained in:
parent
e1ccf56492
commit
93e9585f18
29 changed files with 5353 additions and 0 deletions
9
research/README.md
Normal file
9
research/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Research
|
||||||
|
|
||||||
|
This folder contains code to reproduce results from research papers. Currently,
|
||||||
|
the following papers are included:
|
||||||
|
|
||||||
|
* Semi-supervised Knowledge Transfer for Deep Learning from Private Training
|
||||||
|
Data (ICLR 2017): `pate_2017`
|
||||||
|
|
||||||
|
* Scalable Private Learning with PATE (ICLR 2018): `pate_2018`
|
123
research/pate_2017/README.md
Normal file
123
research/pate_2017/README.md
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
# Learning private models with multiple teachers
|
||||||
|
|
||||||
|
This repository contains code to create a setup for learning privacy-preserving
|
||||||
|
student models by transferring knowledge from an ensemble of teachers trained
|
||||||
|
on disjoint subsets of the data for which privacy guarantees are to be provided.
|
||||||
|
|
||||||
|
Knowledge acquired by teachers is transferred to the student in a differentially
|
||||||
|
private manner by noisily aggregating the teacher decisions before feeding them
|
||||||
|
to the student during training.
|
||||||
|
|
||||||
|
The paper describing the approach is [arXiv:1610.05755](https://arxiv.org/abs/1610.05755)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This model uses `TensorFlow` to perform numerical computations associated with
|
||||||
|
machine learning models, as well as common Python libraries like: `numpy`,
|
||||||
|
`scipy`, and `six`. Instructions to install these can be found in their
|
||||||
|
respective documentations.
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
This repository supports the MNIST and SVHN datasets. The following
|
||||||
|
instructions are given for MNIST but can easily be adapted by replacing the
|
||||||
|
flag `--dataset=mnist` by `--dataset=svhn`.
|
||||||
|
There are 2 steps: teacher training and student training. Data will be
|
||||||
|
automatically downloaded when you start the teacher training.
|
||||||
|
|
||||||
|
The following is a two-step process: first we train an ensemble of teacher
|
||||||
|
models and second we train a student using predictions made by this ensemble.
|
||||||
|
|
||||||
|
**Training the teachers:** first run the `train_teachers.py` file with at least
|
||||||
|
three flags specifying (1) the number of teachers, (2) the ID of the teacher
|
||||||
|
you are training among these teachers, and (3) the dataset on which to train.
|
||||||
|
For instance, to train teacher number 10 among an ensemble of 100 teachers for
|
||||||
|
MNIST, you use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
python train_teachers.py --nb_teachers=100 --teacher_id=10 --dataset=mnist
|
||||||
|
```
|
||||||
|
|
||||||
|
Other flags like `train_dir` and `data_dir` should optionally be set to
|
||||||
|
respectively point to the directory where model checkpoints and temporary data
|
||||||
|
(like the dataset) should be saved. The flag `max_steps` (default at 3000)
|
||||||
|
controls the length of training. See `train_teachers.py` and `deep_cnn.py`
|
||||||
|
to find available flags and their descriptions.
|
||||||
|
|
||||||
|
**Training the student:** once the teachers are all trained, e.g., teachers
|
||||||
|
with IDs `0` to `99` are trained for `nb_teachers=100`, we are ready to train
|
||||||
|
the student. The student is trained by labeling some of the test data with
|
||||||
|
predictions from the teachers. The predictions are aggregated by counting the
|
||||||
|
votes assigned to each class among the ensemble of teachers, adding Laplacian
|
||||||
|
noise to these votes, and assigning the label with the maximum noisy vote count
|
||||||
|
to the sample. This is detailed in function `noisy_max` in the file
|
||||||
|
`aggregation.py`. To learn the student, use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
python train_student.py --nb_teachers=100 --dataset=mnist --stdnt_share=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
The flag `--stdnt_share=5000` indicates that the student should be able to
|
||||||
|
use the first `5000` samples of the dataset's test subset as unlabeled
|
||||||
|
training points (they will be labeled using the teacher predictions). The
|
||||||
|
remaining samples are used for evaluation of the student's accuracy, which
|
||||||
|
is displayed upon completion of training.
|
||||||
|
|
||||||
|
## Using semi-supervised GANs to train the student
|
||||||
|
|
||||||
|
In the paper, we describe how to train the student in a semi-supervised
|
||||||
|
fashion using Generative Adversarial Networks. This can be reproduced for MNIST
|
||||||
|
by cloning the [improved-gan](https://github.com/openai/improved-gan)
|
||||||
|
repository and adding to your `PATH` variable before running the shell
|
||||||
|
script `train_student_mnist_250_lap_20_count_50_epochs_600.sh`.
|
||||||
|
|
||||||
|
```
|
||||||
|
export PATH="/path/to/improved-gan/mnist_svhn_cifar10":$PATH
|
||||||
|
sh train_student_mnist_250_lap_20_count_50_epochs_600.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Alternative deeper convolutional architecture
|
||||||
|
|
||||||
|
Note that a deeper convolutional model is available. Both the default and
|
||||||
|
deeper models graphs are defined in `deep_cnn.py`, respectively by
|
||||||
|
functions `inference` and `inference_deeper`. Use the flag `--deeper=true`
|
||||||
|
to switch to that model when launching `train_teachers.py` and
|
||||||
|
`train_student.py`.
|
||||||
|
|
||||||
|
## Privacy analysis
|
||||||
|
|
||||||
|
In the paper, we detail how data-dependent differential privacy bounds can be
|
||||||
|
computed to estimate the cost of training the student. In order to reproduce
|
||||||
|
the bounds given in the paper, we include the label predicted by our two
|
||||||
|
teacher ensembles: MNIST and SVHN. You can run the privacy analysis for each
|
||||||
|
dataset with the following commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
python analysis.py --counts_file=mnist_250_teachers_labels.npy --indices_file=mnist_250_teachers_100_indices_used_by_student.npy
|
||||||
|
|
||||||
|
python analysis.py --counts_file=svhn_250_teachers_labels.npy --max_examples=1000 --delta=1e-6
|
||||||
|
```
|
||||||
|
|
||||||
|
To expedite experimentation with the privacy analysis of student training,
|
||||||
|
the `analysis.py` file is configured to download the labels produced by 250
|
||||||
|
teacher models, for MNIST and SVHN when running the two commands included
|
||||||
|
above. These 250 teacher models were trained using the following command lines,
|
||||||
|
where `XXX` takes values between `0` and `249`:
|
||||||
|
|
||||||
|
```
|
||||||
|
python train_teachers.py --nb_teachers=250 --teacher_id=XXX --dataset=mnist
|
||||||
|
python train_teachers.py --nb_teachers=250 --teacher_id=XXX --dataset=svhn
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that these labels may also be used in lieu of function `ensemble_preds`
|
||||||
|
in `train_student.py`, to compare the performance of alternative student model
|
||||||
|
architectures and learning techniques. This facilitates future work, by
|
||||||
|
removing the need for training the MNIST and SVHN teacher ensembles when
|
||||||
|
proposing new student training approaches.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
To ask questions, please email `nicolas@papernot.fr` or open an issue on
|
||||||
|
the `tensorflow/models` issues tracker. Please assign issues to
|
||||||
|
[@npapernot](https://github.com/npapernot).
|
1
research/pate_2017/__init__.py
Normal file
1
research/pate_2017/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
130
research/pate_2017/aggregation.py
Normal file
130
research/pate_2017/aggregation.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from six.moves import xrange
|
||||||
|
|
||||||
|
|
||||||
|
def labels_from_probs(probs):
|
||||||
|
"""
|
||||||
|
Helper function: computes argmax along last dimension of array to obtain
|
||||||
|
labels (max prob or max logit value)
|
||||||
|
:param probs: numpy array where probabilities or logits are on last dimension
|
||||||
|
:return: array with same shape as input besides last dimension with shape 1
|
||||||
|
now containing the labels
|
||||||
|
"""
|
||||||
|
# Compute last axis index
|
||||||
|
last_axis = len(np.shape(probs)) - 1
|
||||||
|
|
||||||
|
# Label is argmax over last dimension
|
||||||
|
labels = np.argmax(probs, axis=last_axis)
|
||||||
|
|
||||||
|
# Return as np.int32
|
||||||
|
return np.asarray(labels, dtype=np.int32)
|
||||||
|
|
||||||
|
|
||||||
|
def noisy_max(logits, lap_scale, return_clean_votes=False):
|
||||||
|
"""
|
||||||
|
This aggregation mechanism takes the softmax/logit output of several models
|
||||||
|
resulting from inference on identical inputs and computes the noisy-max of
|
||||||
|
the votes for candidate classes to select a label for each sample: it
|
||||||
|
adds Laplacian noise to label counts and returns the most frequent label.
|
||||||
|
:param logits: logits or probabilities for each sample
|
||||||
|
:param lap_scale: scale of the Laplacian noise to be added to counts
|
||||||
|
:param return_clean_votes: if set to True, also returns clean votes (without
|
||||||
|
Laplacian noise). This can be used to perform the
|
||||||
|
privacy analysis of this aggregation mechanism.
|
||||||
|
:return: pair of result and (if clean_votes is set to True) the clean counts
|
||||||
|
for each class per sample and the original labels produced by
|
||||||
|
the teachers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compute labels from logits/probs and reshape array properly
|
||||||
|
labels = labels_from_probs(logits)
|
||||||
|
labels_shape = np.shape(labels)
|
||||||
|
labels = labels.reshape((labels_shape[0], labels_shape[1]))
|
||||||
|
|
||||||
|
# Initialize array to hold final labels
|
||||||
|
result = np.zeros(int(labels_shape[1]))
|
||||||
|
|
||||||
|
if return_clean_votes:
|
||||||
|
# Initialize array to hold clean votes for each sample
|
||||||
|
clean_votes = np.zeros((int(labels_shape[1]), 10))
|
||||||
|
|
||||||
|
# Parse each sample
|
||||||
|
for i in xrange(int(labels_shape[1])):
|
||||||
|
# Count number of votes assigned to each class
|
||||||
|
label_counts = np.bincount(labels[:, i], minlength=10)
|
||||||
|
|
||||||
|
if return_clean_votes:
|
||||||
|
# Store vote counts for export
|
||||||
|
clean_votes[i] = label_counts
|
||||||
|
|
||||||
|
# Cast in float32 to prepare before addition of Laplacian noise
|
||||||
|
label_counts = np.asarray(label_counts, dtype=np.float32)
|
||||||
|
|
||||||
|
# Sample independent Laplacian noise for each class
|
||||||
|
for item in xrange(10):
|
||||||
|
label_counts[item] += np.random.laplace(loc=0.0, scale=float(lap_scale))
|
||||||
|
|
||||||
|
# Result is the most frequent label
|
||||||
|
result[i] = np.argmax(label_counts)
|
||||||
|
|
||||||
|
# Cast labels to np.int32 for compatibility with deep_cnn.py feed dictionaries
|
||||||
|
result = np.asarray(result, dtype=np.int32)
|
||||||
|
|
||||||
|
if return_clean_votes:
|
||||||
|
# Returns several array, which are later saved:
|
||||||
|
# result: labels obtained from the noisy aggregation
|
||||||
|
# clean_votes: the number of teacher votes assigned to each sample and class
|
||||||
|
# labels: the labels assigned by teachers (before the noisy aggregation)
|
||||||
|
return result, clean_votes, labels
|
||||||
|
else:
|
||||||
|
# Only return labels resulting from noisy aggregation
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def aggregation_most_frequent(logits):
|
||||||
|
"""
|
||||||
|
This aggregation mechanism takes the softmax/logit output of several models
|
||||||
|
resulting from inference on identical inputs and computes the most frequent
|
||||||
|
label. It is deterministic (no noise injection like noisy_max() above.
|
||||||
|
:param logits: logits or probabilities for each sample
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Compute labels from logits/probs and reshape array properly
|
||||||
|
labels = labels_from_probs(logits)
|
||||||
|
labels_shape = np.shape(labels)
|
||||||
|
labels = labels.reshape((labels_shape[0], labels_shape[1]))
|
||||||
|
|
||||||
|
# Initialize array to hold final labels
|
||||||
|
result = np.zeros(int(labels_shape[1]))
|
||||||
|
|
||||||
|
# Parse each sample
|
||||||
|
for i in xrange(int(labels_shape[1])):
|
||||||
|
# Count number of votes assigned to each class
|
||||||
|
label_counts = np.bincount(labels[:, i], minlength=10)
|
||||||
|
|
||||||
|
label_counts = np.asarray(label_counts, dtype=np.int32)
|
||||||
|
|
||||||
|
# Result is the most frequent label
|
||||||
|
result[i] = np.argmax(label_counts)
|
||||||
|
|
||||||
|
return np.asarray(result, dtype=np.int32)
|
304
research/pate_2017/analysis.py
Normal file
304
research/pate_2017/analysis.py
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
This script computes bounds on the privacy cost of training the
|
||||||
|
student model from noisy aggregation of labels predicted by teachers.
|
||||||
|
It should be used only after training the student (and therefore the
|
||||||
|
teachers as well). We however include the label files required to
|
||||||
|
reproduce key results from our paper (https://arxiv.org/abs/1610.05755):
|
||||||
|
the epsilon bounds for MNIST and SVHN students.
|
||||||
|
|
||||||
|
The command that computes the epsilon bound associated
|
||||||
|
with the training of the MNIST student model (100 label queries
|
||||||
|
with a (1/20)*2=0.1 epsilon bound each) is:
|
||||||
|
|
||||||
|
python analysis.py
|
||||||
|
--counts_file=mnist_250_teachers_labels.npy
|
||||||
|
--indices_file=mnist_250_teachers_100_indices_used_by_student.npy
|
||||||
|
|
||||||
|
The command that computes the epsilon bound associated
|
||||||
|
with the training of the SVHN student model (1000 label queries
|
||||||
|
with a (1/20)*2=0.1 epsilon bound each) is:
|
||||||
|
|
||||||
|
python analysis.py
|
||||||
|
--counts_file=svhn_250_teachers_labels.npy
|
||||||
|
--max_examples=1000
|
||||||
|
--delta=1e-6
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
from six.moves import xrange
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
from differential_privacy.multiple_teachers.input import maybe_download
|
||||||
|
|
||||||
|
# These parameters can be changed to compute bounds for different failure rates
|
||||||
|
# or different model predictions.
|
||||||
|
|
||||||
|
tf.flags.DEFINE_integer("moments",8, "Number of moments")
|
||||||
|
tf.flags.DEFINE_float("noise_eps", 0.1, "Eps value for each call to noisymax.")
|
||||||
|
tf.flags.DEFINE_float("delta", 1e-5, "Target value of delta.")
|
||||||
|
tf.flags.DEFINE_float("beta", 0.09, "Value of beta for smooth sensitivity")
|
||||||
|
tf.flags.DEFINE_string("counts_file","","Numpy matrix with raw counts")
|
||||||
|
tf.flags.DEFINE_string("indices_file","",
|
||||||
|
"File containting a numpy matrix with indices used."
|
||||||
|
"Optional. Use the first max_examples indices if this is not provided.")
|
||||||
|
tf.flags.DEFINE_integer("max_examples",1000,
|
||||||
|
"Number of examples to use. We will use the first"
|
||||||
|
" max_examples many examples from the counts_file"
|
||||||
|
" or indices_file to do the privacy cost estimate")
|
||||||
|
tf.flags.DEFINE_float("too_small", 1e-10, "Small threshold to avoid log of 0")
|
||||||
|
tf.flags.DEFINE_bool("input_is_counts", False, "False if labels, True if counts")
|
||||||
|
|
||||||
|
FLAGS = tf.flags.FLAGS
|
||||||
|
|
||||||
|
|
||||||
|
def compute_q_noisy_max(counts, noise_eps):
|
||||||
|
"""returns ~ Pr[outcome != winner].
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts: a list of scores
|
||||||
|
noise_eps: privacy parameter for noisy_max
|
||||||
|
Returns:
|
||||||
|
q: the probability that outcome is different from true winner.
|
||||||
|
"""
|
||||||
|
# For noisy max, we only get an upper bound.
|
||||||
|
# Pr[ j beats i*] \leq (2+gap(j,i*))/ 4 exp(gap(j,i*)
|
||||||
|
# proof at http://mathoverflow.net/questions/66763/
|
||||||
|
# tight-bounds-on-probability-of-sum-of-laplace-random-variables
|
||||||
|
|
||||||
|
winner = np.argmax(counts)
|
||||||
|
counts_normalized = noise_eps * (counts - counts[winner])
|
||||||
|
counts_rest = np.array(
|
||||||
|
[counts_normalized[i] for i in xrange(len(counts)) if i != winner])
|
||||||
|
q = 0.0
|
||||||
|
for c in counts_rest:
|
||||||
|
gap = -c
|
||||||
|
q += (gap + 2.0) / (4.0 * math.exp(gap))
|
||||||
|
return min(q, 1.0 - (1.0/len(counts)))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_q_noisy_max_approx(counts, noise_eps):
|
||||||
|
"""returns ~ Pr[outcome != winner].
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts: a list of scores
|
||||||
|
noise_eps: privacy parameter for noisy_max
|
||||||
|
Returns:
|
||||||
|
q: the probability that outcome is different from true winner.
|
||||||
|
"""
|
||||||
|
# For noisy max, we only get an upper bound.
|
||||||
|
# Pr[ j beats i*] \leq (2+gap(j,i*))/ 4 exp(gap(j,i*)
|
||||||
|
# proof at http://mathoverflow.net/questions/66763/
|
||||||
|
# tight-bounds-on-probability-of-sum-of-laplace-random-variables
|
||||||
|
# This code uses an approximation that is faster and easier
|
||||||
|
# to get local sensitivity bound on.
|
||||||
|
|
||||||
|
winner = np.argmax(counts)
|
||||||
|
counts_normalized = noise_eps * (counts - counts[winner])
|
||||||
|
counts_rest = np.array(
|
||||||
|
[counts_normalized[i] for i in xrange(len(counts)) if i != winner])
|
||||||
|
gap = -max(counts_rest)
|
||||||
|
q = (len(counts) - 1) * (gap + 2.0) / (4.0 * math.exp(gap))
|
||||||
|
return min(q, 1.0 - (1.0/len(counts)))
|
||||||
|
|
||||||
|
|
||||||
|
def logmgf_exact(q, priv_eps, l):
|
||||||
|
"""Computes the logmgf value given q and privacy eps.
|
||||||
|
|
||||||
|
The bound used is the min of three terms. The first term is from
|
||||||
|
https://arxiv.org/pdf/1605.02065.pdf.
|
||||||
|
The second term is based on the fact that when event has probability (1-q) for
|
||||||
|
q close to zero, q can only change by exp(eps), which corresponds to a
|
||||||
|
much smaller multiplicative change in (1-q)
|
||||||
|
The third term comes directly from the privacy guarantee.
|
||||||
|
Args:
|
||||||
|
q: pr of non-optimal outcome
|
||||||
|
priv_eps: eps parameter for DP
|
||||||
|
l: moment to compute.
|
||||||
|
Returns:
|
||||||
|
Upper bound on logmgf
|
||||||
|
"""
|
||||||
|
if q < 0.5:
|
||||||
|
t_one = (1-q) * math.pow((1-q) / (1 - math.exp(priv_eps) * q), l)
|
||||||
|
t_two = q * math.exp(priv_eps * l)
|
||||||
|
t = t_one + t_two
|
||||||
|
try:
|
||||||
|
log_t = math.log(t)
|
||||||
|
except ValueError:
|
||||||
|
print("Got ValueError in math.log for values :" + str((q, priv_eps, l, t)))
|
||||||
|
log_t = priv_eps * l
|
||||||
|
else:
|
||||||
|
log_t = priv_eps * l
|
||||||
|
|
||||||
|
return min(0.5 * priv_eps * priv_eps * l * (l + 1), log_t, priv_eps * l)
|
||||||
|
|
||||||
|
|
||||||
|
def logmgf_from_counts(counts, noise_eps, l):
|
||||||
|
"""
|
||||||
|
ReportNoisyMax mechanism with noise_eps with 2*noise_eps-DP
|
||||||
|
in our setting where one count can go up by one and another
|
||||||
|
can go down by 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
q = compute_q_noisy_max(counts, noise_eps)
|
||||||
|
return logmgf_exact(q, 2.0 * noise_eps, l)
|
||||||
|
|
||||||
|
|
||||||
|
def sens_at_k(counts, noise_eps, l, k):
|
||||||
|
"""Return sensitivity at distane k.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts: an array of scores
|
||||||
|
noise_eps: noise parameter used
|
||||||
|
l: moment whose sensitivity is being computed
|
||||||
|
k: distance
|
||||||
|
Returns:
|
||||||
|
sensitivity: at distance k
|
||||||
|
"""
|
||||||
|
counts_sorted = sorted(counts, reverse=True)
|
||||||
|
if 0.5 * noise_eps * l > 1:
|
||||||
|
print("l too large to compute sensitivity")
|
||||||
|
return 0
|
||||||
|
# Now we can assume that at k, gap remains positive
|
||||||
|
# or we have reached the point where logmgf_exact is
|
||||||
|
# determined by the first term and ind of q.
|
||||||
|
if counts[0] < counts[1] + k:
|
||||||
|
return 0
|
||||||
|
counts_sorted[0] -= k
|
||||||
|
counts_sorted[1] += k
|
||||||
|
val = logmgf_from_counts(counts_sorted, noise_eps, l)
|
||||||
|
counts_sorted[0] -= 1
|
||||||
|
counts_sorted[1] += 1
|
||||||
|
val_changed = logmgf_from_counts(counts_sorted, noise_eps, l)
|
||||||
|
return val_changed - val
|
||||||
|
|
||||||
|
|
||||||
|
def smoothed_sens(counts, noise_eps, l, beta):
|
||||||
|
"""Compute beta-smooth sensitivity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts: array of scors
|
||||||
|
noise_eps: noise parameter
|
||||||
|
l: moment of interest
|
||||||
|
beta: smoothness parameter
|
||||||
|
Returns:
|
||||||
|
smooth_sensitivity: a beta smooth upper bound
|
||||||
|
"""
|
||||||
|
k = 0
|
||||||
|
smoothed_sensitivity = sens_at_k(counts, noise_eps, l, k)
|
||||||
|
while k < max(counts):
|
||||||
|
k += 1
|
||||||
|
sensitivity_at_k = sens_at_k(counts, noise_eps, l, k)
|
||||||
|
smoothed_sensitivity = max(
|
||||||
|
smoothed_sensitivity,
|
||||||
|
math.exp(-beta * k) * sensitivity_at_k)
|
||||||
|
if sensitivity_at_k == 0.0:
|
||||||
|
break
|
||||||
|
return smoothed_sensitivity
|
||||||
|
|
||||||
|
|
||||||
|
def main(unused_argv):
|
||||||
|
##################################################################
|
||||||
|
# If we are reproducing results from paper https://arxiv.org/abs/1610.05755,
|
||||||
|
# download the required binaries with label information.
|
||||||
|
##################################################################
|
||||||
|
|
||||||
|
# Binaries for MNIST results
|
||||||
|
paper_binaries_mnist = \
|
||||||
|
["https://github.com/npapernot/multiple-teachers-for-privacy/blob/master/mnist_250_teachers_labels.npy?raw=true",
|
||||||
|
"https://github.com/npapernot/multiple-teachers-for-privacy/blob/master/mnist_250_teachers_100_indices_used_by_student.npy?raw=true"]
|
||||||
|
if FLAGS.counts_file == "mnist_250_teachers_labels.npy" \
|
||||||
|
or FLAGS.indices_file == "mnist_250_teachers_100_indices_used_by_student.npy":
|
||||||
|
maybe_download(paper_binaries_mnist, os.getcwd())
|
||||||
|
|
||||||
|
# Binaries for SVHN results
|
||||||
|
paper_binaries_svhn = ["https://github.com/npapernot/multiple-teachers-for-privacy/blob/master/svhn_250_teachers_labels.npy?raw=true"]
|
||||||
|
if FLAGS.counts_file == "svhn_250_teachers_labels.npy":
|
||||||
|
maybe_download(paper_binaries_svhn, os.getcwd())
|
||||||
|
|
||||||
|
input_mat = np.load(FLAGS.counts_file)
|
||||||
|
if FLAGS.input_is_counts:
|
||||||
|
counts_mat = input_mat
|
||||||
|
else:
|
||||||
|
# In this case, the input is the raw predictions. Transform
|
||||||
|
num_teachers, n = input_mat.shape
|
||||||
|
counts_mat = np.zeros((n, 10)).astype(np.int32)
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(num_teachers):
|
||||||
|
counts_mat[i, int(input_mat[j, i])] += 1
|
||||||
|
n = counts_mat.shape[0]
|
||||||
|
num_examples = min(n, FLAGS.max_examples)
|
||||||
|
|
||||||
|
if not FLAGS.indices_file:
|
||||||
|
indices = np.array(range(num_examples))
|
||||||
|
else:
|
||||||
|
index_list = np.load(FLAGS.indices_file)
|
||||||
|
indices = index_list[:num_examples]
|
||||||
|
|
||||||
|
l_list = 1.0 + np.array(xrange(FLAGS.moments))
|
||||||
|
beta = FLAGS.beta
|
||||||
|
total_log_mgf_nm = np.array([0.0 for _ in l_list])
|
||||||
|
total_ss_nm = np.array([0.0 for _ in l_list])
|
||||||
|
noise_eps = FLAGS.noise_eps
|
||||||
|
|
||||||
|
for i in indices:
|
||||||
|
total_log_mgf_nm += np.array(
|
||||||
|
[logmgf_from_counts(counts_mat[i], noise_eps, l)
|
||||||
|
for l in l_list])
|
||||||
|
total_ss_nm += np.array(
|
||||||
|
[smoothed_sens(counts_mat[i], noise_eps, l, beta)
|
||||||
|
for l in l_list])
|
||||||
|
delta = FLAGS.delta
|
||||||
|
|
||||||
|
# We want delta = exp(alpha - eps l).
|
||||||
|
# Solving gives eps = (alpha - ln (delta))/l
|
||||||
|
eps_list_nm = (total_log_mgf_nm - math.log(delta)) / l_list
|
||||||
|
|
||||||
|
print("Epsilons (Noisy Max): " + str(eps_list_nm))
|
||||||
|
print("Smoothed sensitivities (Noisy Max): " + str(total_ss_nm / l_list))
|
||||||
|
|
||||||
|
# If beta < eps / 2 ln (1/delta), then adding noise Lap(1) * 2 SS/eps
|
||||||
|
# is eps,delta DP
|
||||||
|
# Also if beta < eps / 2(gamma +1), then adding noise 2(gamma+1) SS eta / eps
|
||||||
|
# where eta has density proportional to 1 / (1+|z|^gamma) is eps-DP
|
||||||
|
# Both from Corolloary 2.4 in
|
||||||
|
# http://www.cse.psu.edu/~ads22/pubs/NRS07/NRS07-full-draft-v1.pdf
|
||||||
|
# Print the first one's scale
|
||||||
|
ss_eps = 2.0 * beta * math.log(1/delta)
|
||||||
|
ss_scale = 2.0 / ss_eps
|
||||||
|
print("To get an " + str(ss_eps) + "-DP estimate of epsilon, ")
|
||||||
|
print("..add noise ~ " + str(ss_scale))
|
||||||
|
print("... times " + str(total_ss_nm / l_list))
|
||||||
|
print("Epsilon = " + str(min(eps_list_nm)) + ".")
|
||||||
|
if min(eps_list_nm) == eps_list_nm[-1]:
|
||||||
|
print("Warning: May not have used enough values of l")
|
||||||
|
|
||||||
|
# Data independent bound, as mechanism is
|
||||||
|
# 2*noise_eps DP.
|
||||||
|
data_ind_log_mgf = np.array([0.0 for _ in l_list])
|
||||||
|
data_ind_log_mgf += num_examples * np.array(
|
||||||
|
[logmgf_exact(1.0, 2.0 * noise_eps, l) for l in l_list])
|
||||||
|
|
||||||
|
data_ind_eps_list = (data_ind_log_mgf - math.log(delta)) / l_list
|
||||||
|
print("Data independent bound = " + str(min(data_ind_eps_list)) + ".")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
tf.app.run()
|
603
research/pate_2017/deep_cnn.py
Normal file
603
research/pate_2017/deep_cnn.py
Normal file
|
@ -0,0 +1,603 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
from six.moves import xrange
|
||||||
|
import tensorflow as tf
|
||||||
|
import time
|
||||||
|
|
||||||
|
from differential_privacy.multiple_teachers import utils
|
||||||
|
|
||||||
|
FLAGS = tf.app.flags.FLAGS
|
||||||
|
|
||||||
|
# Basic model parameters.
|
||||||
|
tf.app.flags.DEFINE_integer('dropout_seed', 123, """seed for dropout.""")
|
||||||
|
tf.app.flags.DEFINE_integer('batch_size', 128, """Nb of images in a batch.""")
|
||||||
|
tf.app.flags.DEFINE_integer('epochs_per_decay', 350, """Nb epochs per decay""")
|
||||||
|
tf.app.flags.DEFINE_integer('learning_rate', 5, """100 * learning rate""")
|
||||||
|
tf.app.flags.DEFINE_boolean('log_device_placement', False, """see TF doc""")
|
||||||
|
|
||||||
|
|
||||||
|
# Constants describing the training process.
|
||||||
|
MOVING_AVERAGE_DECAY = 0.9999 # The decay to use for the moving average.
|
||||||
|
LEARNING_RATE_DECAY_FACTOR = 0.1 # Learning rate decay factor.
|
||||||
|
|
||||||
|
|
||||||
|
def _variable_on_cpu(name, shape, initializer):
|
||||||
|
"""Helper to create a Variable stored on CPU memory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: name of the variable
|
||||||
|
shape: list of ints
|
||||||
|
initializer: initializer for Variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Variable Tensor
|
||||||
|
"""
|
||||||
|
with tf.device('/cpu:0'):
|
||||||
|
var = tf.get_variable(name, shape, initializer=initializer)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def _variable_with_weight_decay(name, shape, stddev, wd):
|
||||||
|
"""Helper to create an initialized Variable with weight decay.
|
||||||
|
|
||||||
|
Note that the Variable is initialized with a truncated normal distribution.
|
||||||
|
A weight decay is added only if one is specified.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: name of the variable
|
||||||
|
shape: list of ints
|
||||||
|
stddev: standard deviation of a truncated Gaussian
|
||||||
|
wd: add L2Loss weight decay multiplied by this float. If None, weight
|
||||||
|
decay is not added for this Variable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Variable Tensor
|
||||||
|
"""
|
||||||
|
var = _variable_on_cpu(name, shape,
|
||||||
|
tf.truncated_normal_initializer(stddev=stddev))
|
||||||
|
if wd is not None:
|
||||||
|
weight_decay = tf.multiply(tf.nn.l2_loss(var), wd, name='weight_loss')
|
||||||
|
tf.add_to_collection('losses', weight_decay)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def inference(images, dropout=False):
|
||||||
|
"""Build the CNN model.
|
||||||
|
Args:
|
||||||
|
images: Images returned from distorted_inputs() or inputs().
|
||||||
|
dropout: Boolean controlling whether to use dropout or not
|
||||||
|
Returns:
|
||||||
|
Logits
|
||||||
|
"""
|
||||||
|
if FLAGS.dataset == 'mnist':
|
||||||
|
first_conv_shape = [5, 5, 1, 64]
|
||||||
|
else:
|
||||||
|
first_conv_shape = [5, 5, 3, 64]
|
||||||
|
|
||||||
|
# conv1
|
||||||
|
with tf.variable_scope('conv1') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=first_conv_shape,
|
||||||
|
stddev=1e-4,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv1 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
conv1 = tf.nn.dropout(conv1, 0.3, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
|
||||||
|
# pool1
|
||||||
|
pool1 = tf.nn.max_pool(conv1,
|
||||||
|
ksize=[1, 3, 3, 1],
|
||||||
|
strides=[1, 2, 2, 1],
|
||||||
|
padding='SAME',
|
||||||
|
name='pool1')
|
||||||
|
|
||||||
|
# norm1
|
||||||
|
norm1 = tf.nn.lrn(pool1,
|
||||||
|
4,
|
||||||
|
bias=1.0,
|
||||||
|
alpha=0.001 / 9.0,
|
||||||
|
beta=0.75,
|
||||||
|
name='norm1')
|
||||||
|
|
||||||
|
# conv2
|
||||||
|
with tf.variable_scope('conv2') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[5, 5, 64, 128],
|
||||||
|
stddev=1e-4,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(norm1, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [128], tf.constant_initializer(0.1))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv2 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
conv2 = tf.nn.dropout(conv2, 0.3, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
|
||||||
|
# norm2
|
||||||
|
norm2 = tf.nn.lrn(conv2,
|
||||||
|
4,
|
||||||
|
bias=1.0,
|
||||||
|
alpha=0.001 / 9.0,
|
||||||
|
beta=0.75,
|
||||||
|
name='norm2')
|
||||||
|
|
||||||
|
# pool2
|
||||||
|
pool2 = tf.nn.max_pool(norm2,
|
||||||
|
ksize=[1, 3, 3, 1],
|
||||||
|
strides=[1, 2, 2, 1],
|
||||||
|
padding='SAME',
|
||||||
|
name='pool2')
|
||||||
|
|
||||||
|
# local3
|
||||||
|
with tf.variable_scope('local3') as scope:
|
||||||
|
# Move everything into depth so we can perform a single matrix multiply.
|
||||||
|
reshape = tf.reshape(pool2, [FLAGS.batch_size, -1])
|
||||||
|
dim = reshape.get_shape()[1].value
|
||||||
|
weights = _variable_with_weight_decay('weights',
|
||||||
|
shape=[dim, 384],
|
||||||
|
stddev=0.04,
|
||||||
|
wd=0.004)
|
||||||
|
biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
|
||||||
|
local3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
local3 = tf.nn.dropout(local3, 0.5, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
# local4
|
||||||
|
with tf.variable_scope('local4') as scope:
|
||||||
|
weights = _variable_with_weight_decay('weights',
|
||||||
|
shape=[384, 192],
|
||||||
|
stddev=0.04,
|
||||||
|
wd=0.004)
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
|
||||||
|
local4 = tf.nn.relu(tf.matmul(local3, weights) + biases, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
local4 = tf.nn.dropout(local4, 0.5, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
# compute logits
|
||||||
|
with tf.variable_scope('softmax_linear') as scope:
|
||||||
|
weights = _variable_with_weight_decay('weights',
|
||||||
|
[192, FLAGS.nb_labels],
|
||||||
|
stddev=1/192.0,
|
||||||
|
wd=0.0)
|
||||||
|
biases = _variable_on_cpu('biases',
|
||||||
|
[FLAGS.nb_labels],
|
||||||
|
tf.constant_initializer(0.0))
|
||||||
|
logits = tf.add(tf.matmul(local4, weights), biases, name=scope.name)
|
||||||
|
|
||||||
|
return logits
|
||||||
|
|
||||||
|
|
||||||
|
def inference_deeper(images, dropout=False):
|
||||||
|
"""Build a deeper CNN model.
|
||||||
|
Args:
|
||||||
|
images: Images returned from distorted_inputs() or inputs().
|
||||||
|
dropout: Boolean controlling whether to use dropout or not
|
||||||
|
Returns:
|
||||||
|
Logits
|
||||||
|
"""
|
||||||
|
if FLAGS.dataset == 'mnist':
|
||||||
|
first_conv_shape = [3, 3, 1, 96]
|
||||||
|
else:
|
||||||
|
first_conv_shape = [3, 3, 3, 96]
|
||||||
|
|
||||||
|
# conv1
|
||||||
|
with tf.variable_scope('conv1') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=first_conv_shape,
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [96], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv1 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
|
||||||
|
# conv2
|
||||||
|
with tf.variable_scope('conv2') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[3, 3, 96, 96],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(conv1, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [96], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv2 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
|
||||||
|
# conv3
|
||||||
|
with tf.variable_scope('conv3') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[3, 3, 96, 96],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(conv2, kernel, [1, 2, 2, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [96], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv3 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
conv3 = tf.nn.dropout(conv3, 0.5, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
# conv4
|
||||||
|
with tf.variable_scope('conv4') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[3, 3, 96, 192],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv4 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
|
||||||
|
# conv5
|
||||||
|
with tf.variable_scope('conv5') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[3, 3, 192, 192],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv5 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
|
||||||
|
# conv6
|
||||||
|
with tf.variable_scope('conv6') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[3, 3, 192, 192],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(conv5, kernel, [1, 2, 2, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.0))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv6 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
conv6 = tf.nn.dropout(conv6, 0.5, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
|
||||||
|
# conv7
|
||||||
|
with tf.variable_scope('conv7') as scope:
|
||||||
|
kernel = _variable_with_weight_decay('weights',
|
||||||
|
shape=[5, 5, 192, 192],
|
||||||
|
stddev=1e-4,
|
||||||
|
wd=0.0)
|
||||||
|
conv = tf.nn.conv2d(conv6, kernel, [1, 1, 1, 1], padding='SAME')
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
|
||||||
|
bias = tf.nn.bias_add(conv, biases)
|
||||||
|
conv7 = tf.nn.relu(bias, name=scope.name)
|
||||||
|
|
||||||
|
|
||||||
|
# local1
|
||||||
|
with tf.variable_scope('local1') as scope:
|
||||||
|
# Move everything into depth so we can perform a single matrix multiply.
|
||||||
|
reshape = tf.reshape(conv7, [FLAGS.batch_size, -1])
|
||||||
|
dim = reshape.get_shape()[1].value
|
||||||
|
weights = _variable_with_weight_decay('weights',
|
||||||
|
shape=[dim, 192],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0)
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
|
||||||
|
local1 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
|
||||||
|
|
||||||
|
# local2
|
||||||
|
with tf.variable_scope('local2') as scope:
|
||||||
|
weights = _variable_with_weight_decay('weights',
|
||||||
|
shape=[192, 192],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0)
|
||||||
|
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
|
||||||
|
local2 = tf.nn.relu(tf.matmul(local1, weights) + biases, name=scope.name)
|
||||||
|
if dropout:
|
||||||
|
local2 = tf.nn.dropout(local2, 0.5, seed=FLAGS.dropout_seed)
|
||||||
|
|
||||||
|
# compute logits
|
||||||
|
with tf.variable_scope('softmax_linear') as scope:
|
||||||
|
weights = _variable_with_weight_decay('weights',
|
||||||
|
[192, FLAGS.nb_labels],
|
||||||
|
stddev=0.05,
|
||||||
|
wd=0.0)
|
||||||
|
biases = _variable_on_cpu('biases',
|
||||||
|
[FLAGS.nb_labels],
|
||||||
|
tf.constant_initializer(0.0))
|
||||||
|
logits = tf.add(tf.matmul(local2, weights), biases, name=scope.name)
|
||||||
|
|
||||||
|
return logits
|
||||||
|
|
||||||
|
|
||||||
|
def loss_fun(logits, labels):
|
||||||
|
"""Add L2Loss to all the trainable variables.
|
||||||
|
|
||||||
|
Add summary for "Loss" and "Loss/avg".
|
||||||
|
Args:
|
||||||
|
logits: Logits from inference().
|
||||||
|
labels: Labels from distorted_inputs or inputs(). 1-D tensor
|
||||||
|
of shape [batch_size]
|
||||||
|
distillation: if set to True, use probabilities and not class labels to
|
||||||
|
compute softmax loss
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Loss tensor of type float.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Calculate the cross entropy between labels and predictions
|
||||||
|
labels = tf.cast(labels, tf.int64)
|
||||||
|
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
|
||||||
|
logits=logits, labels=labels, name='cross_entropy_per_example')
|
||||||
|
|
||||||
|
# Calculate the average cross entropy loss across the batch.
|
||||||
|
cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
|
||||||
|
|
||||||
|
# Add to TF collection for losses
|
||||||
|
tf.add_to_collection('losses', cross_entropy_mean)
|
||||||
|
|
||||||
|
# The total loss is defined as the cross entropy loss plus all of the weight
|
||||||
|
# decay terms (L2 loss).
|
||||||
|
return tf.add_n(tf.get_collection('losses'), name='total_loss')
|
||||||
|
|
||||||
|
|
||||||
|
def moving_av(total_loss):
|
||||||
|
"""
|
||||||
|
Generates moving average for all losses
|
||||||
|
|
||||||
|
Args:
|
||||||
|
total_loss: Total loss from loss().
|
||||||
|
Returns:
|
||||||
|
loss_averages_op: op for generating moving averages of losses.
|
||||||
|
"""
|
||||||
|
# Compute the moving average of all individual losses and the total loss.
|
||||||
|
loss_averages = tf.train.ExponentialMovingAverage(0.9, name='avg')
|
||||||
|
losses = tf.get_collection('losses')
|
||||||
|
loss_averages_op = loss_averages.apply(losses + [total_loss])
|
||||||
|
|
||||||
|
return loss_averages_op
|
||||||
|
|
||||||
|
|
||||||
|
def train_op_fun(total_loss, global_step):
|
||||||
|
"""Train model.
|
||||||
|
|
||||||
|
Create an optimizer and apply to all trainable variables. Add moving
|
||||||
|
average for all trainable variables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
total_loss: Total loss from loss().
|
||||||
|
global_step: Integer Variable counting the number of training steps
|
||||||
|
processed.
|
||||||
|
Returns:
|
||||||
|
train_op: op for training.
|
||||||
|
"""
|
||||||
|
# Variables that affect learning rate.
|
||||||
|
nb_ex_per_train_epoch = int(60000 / FLAGS.nb_teachers)
|
||||||
|
|
||||||
|
num_batches_per_epoch = nb_ex_per_train_epoch / FLAGS.batch_size
|
||||||
|
decay_steps = int(num_batches_per_epoch * FLAGS.epochs_per_decay)
|
||||||
|
|
||||||
|
initial_learning_rate = float(FLAGS.learning_rate) / 100.0
|
||||||
|
|
||||||
|
# Decay the learning rate exponentially based on the number of steps.
|
||||||
|
lr = tf.train.exponential_decay(initial_learning_rate,
|
||||||
|
global_step,
|
||||||
|
decay_steps,
|
||||||
|
LEARNING_RATE_DECAY_FACTOR,
|
||||||
|
staircase=True)
|
||||||
|
tf.summary.scalar('learning_rate', lr)
|
||||||
|
|
||||||
|
# Generate moving averages of all losses and associated summaries.
|
||||||
|
loss_averages_op = moving_av(total_loss)
|
||||||
|
|
||||||
|
# Compute gradients.
|
||||||
|
with tf.control_dependencies([loss_averages_op]):
|
||||||
|
opt = tf.train.GradientDescentOptimizer(lr)
|
||||||
|
grads = opt.compute_gradients(total_loss)
|
||||||
|
|
||||||
|
# Apply gradients.
|
||||||
|
apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)
|
||||||
|
|
||||||
|
# Add histograms for trainable variables.
|
||||||
|
for var in tf.trainable_variables():
|
||||||
|
tf.summary.histogram(var.op.name, var)
|
||||||
|
|
||||||
|
# Track the moving averages of all trainable variables.
|
||||||
|
variable_averages = tf.train.ExponentialMovingAverage(
|
||||||
|
MOVING_AVERAGE_DECAY, global_step)
|
||||||
|
variables_averages_op = variable_averages.apply(tf.trainable_variables())
|
||||||
|
|
||||||
|
with tf.control_dependencies([apply_gradient_op, variables_averages_op]):
|
||||||
|
train_op = tf.no_op(name='train')
|
||||||
|
|
||||||
|
return train_op
|
||||||
|
|
||||||
|
|
||||||
|
def _input_placeholder():
|
||||||
|
"""
|
||||||
|
This helper function declares a TF placeholder for the graph input data
|
||||||
|
:return: TF placeholder for the graph input data
|
||||||
|
"""
|
||||||
|
if FLAGS.dataset == 'mnist':
|
||||||
|
image_size = 28
|
||||||
|
num_channels = 1
|
||||||
|
else:
|
||||||
|
image_size = 32
|
||||||
|
num_channels = 3
|
||||||
|
|
||||||
|
# Declare data placeholder
|
||||||
|
train_node_shape = (FLAGS.batch_size, image_size, image_size, num_channels)
|
||||||
|
return tf.placeholder(tf.float32, shape=train_node_shape)
|
||||||
|
|
||||||
|
|
||||||
|
def train(images, labels, ckpt_path, dropout=False):
|
||||||
|
"""
|
||||||
|
This function contains the loop that actually trains the model.
|
||||||
|
:param images: a numpy array with the input data
|
||||||
|
:param labels: a numpy array with the output labels
|
||||||
|
:param ckpt_path: a path (including name) where model checkpoints are saved
|
||||||
|
:param dropout: Boolean, whether to use dropout or not
|
||||||
|
:return: True if everything went well
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check training data
|
||||||
|
assert len(images) == len(labels)
|
||||||
|
assert images.dtype == np.float32
|
||||||
|
assert labels.dtype == np.int32
|
||||||
|
|
||||||
|
# Set default TF graph
|
||||||
|
with tf.Graph().as_default():
|
||||||
|
global_step = tf.Variable(0, trainable=False)
|
||||||
|
|
||||||
|
# Declare data placeholder
|
||||||
|
train_data_node = _input_placeholder()
|
||||||
|
|
||||||
|
# Create a placeholder to hold labels
|
||||||
|
train_labels_shape = (FLAGS.batch_size,)
|
||||||
|
train_labels_node = tf.placeholder(tf.int32, shape=train_labels_shape)
|
||||||
|
|
||||||
|
print("Done Initializing Training Placeholders")
|
||||||
|
|
||||||
|
# Build a Graph that computes the logits predictions from the placeholder
|
||||||
|
if FLAGS.deeper:
|
||||||
|
logits = inference_deeper(train_data_node, dropout=dropout)
|
||||||
|
else:
|
||||||
|
logits = inference(train_data_node, dropout=dropout)
|
||||||
|
|
||||||
|
# Calculate loss
|
||||||
|
loss = loss_fun(logits, train_labels_node)
|
||||||
|
|
||||||
|
# Build a Graph that trains the model with one batch of examples and
|
||||||
|
# updates the model parameters.
|
||||||
|
train_op = train_op_fun(loss, global_step)
|
||||||
|
|
||||||
|
# Create a saver.
|
||||||
|
saver = tf.train.Saver(tf.global_variables())
|
||||||
|
|
||||||
|
print("Graph constructed and saver created")
|
||||||
|
|
||||||
|
# Build an initialization operation to run below.
|
||||||
|
init = tf.global_variables_initializer()
|
||||||
|
|
||||||
|
# Create and init sessions
|
||||||
|
sess = tf.Session(config=tf.ConfigProto(log_device_placement=FLAGS.log_device_placement)) #NOLINT(long-line)
|
||||||
|
sess.run(init)
|
||||||
|
|
||||||
|
print("Session ready, beginning training loop")
|
||||||
|
|
||||||
|
# Initialize the number of batches
|
||||||
|
data_length = len(images)
|
||||||
|
nb_batches = math.ceil(data_length / FLAGS.batch_size)
|
||||||
|
|
||||||
|
for step in xrange(FLAGS.max_steps):
|
||||||
|
# for debug, save start time
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Current batch number
|
||||||
|
batch_nb = step % nb_batches
|
||||||
|
|
||||||
|
# Current batch start and end indices
|
||||||
|
start, end = utils.batch_indices(batch_nb, data_length, FLAGS.batch_size)
|
||||||
|
|
||||||
|
# Prepare dictionnary to feed the session with
|
||||||
|
feed_dict = {train_data_node: images[start:end],
|
||||||
|
train_labels_node: labels[start:end]}
|
||||||
|
|
||||||
|
# Run training step
|
||||||
|
_, loss_value = sess.run([train_op, loss], feed_dict=feed_dict)
|
||||||
|
|
||||||
|
# Compute duration of training step
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
assert not np.isnan(loss_value), 'Model diverged with loss = NaN'
|
||||||
|
|
||||||
|
# Echo loss once in a while
|
||||||
|
if step % 100 == 0:
|
||||||
|
num_examples_per_step = FLAGS.batch_size
|
||||||
|
examples_per_sec = num_examples_per_step / duration
|
||||||
|
sec_per_batch = float(duration)
|
||||||
|
|
||||||
|
format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f '
|
||||||
|
'sec/batch)')
|
||||||
|
print (format_str % (datetime.now(), step, loss_value,
|
||||||
|
examples_per_sec, sec_per_batch))
|
||||||
|
|
||||||
|
# Save the model checkpoint periodically.
|
||||||
|
if step % 1000 == 0 or (step + 1) == FLAGS.max_steps:
|
||||||
|
saver.save(sess, ckpt_path, global_step=step)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def softmax_preds(images, ckpt_path, return_logits=False):
|
||||||
|
"""
|
||||||
|
Compute softmax activations (probabilities) with the model saved in the path
|
||||||
|
specified as an argument
|
||||||
|
:param images: a np array of images
|
||||||
|
:param ckpt_path: a TF model checkpoint
|
||||||
|
:param logits: if set to True, return logits instead of probabilities
|
||||||
|
:return: probabilities (or logits if logits is set to True)
|
||||||
|
"""
|
||||||
|
# Compute nb samples and deduce nb of batches
|
||||||
|
data_length = len(images)
|
||||||
|
nb_batches = math.ceil(len(images) / FLAGS.batch_size)
|
||||||
|
|
||||||
|
# Declare data placeholder
|
||||||
|
train_data_node = _input_placeholder()
|
||||||
|
|
||||||
|
# Build a Graph that computes the logits predictions from the placeholder
|
||||||
|
if FLAGS.deeper:
|
||||||
|
logits = inference_deeper(train_data_node)
|
||||||
|
else:
|
||||||
|
logits = inference(train_data_node)
|
||||||
|
|
||||||
|
if return_logits:
|
||||||
|
# We are returning the logits directly (no need to apply softmax)
|
||||||
|
output = logits
|
||||||
|
else:
|
||||||
|
# Add softmax predictions to graph: will return probabilities
|
||||||
|
output = tf.nn.softmax(logits)
|
||||||
|
|
||||||
|
# Restore the moving average version of the learned variables for eval.
|
||||||
|
variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY)
|
||||||
|
variables_to_restore = variable_averages.variables_to_restore()
|
||||||
|
saver = tf.train.Saver(variables_to_restore)
|
||||||
|
|
||||||
|
# Will hold the result
|
||||||
|
preds = np.zeros((data_length, FLAGS.nb_labels), dtype=np.float32)
|
||||||
|
|
||||||
|
# Create TF session
|
||||||
|
with tf.Session() as sess:
|
||||||
|
# Restore TF session from checkpoint file
|
||||||
|
saver.restore(sess, ckpt_path)
|
||||||
|
|
||||||
|
# Parse data by batch
|
||||||
|
for batch_nb in xrange(0, int(nb_batches+1)):
|
||||||
|
# Compute batch start and end indices
|
||||||
|
start, end = utils.batch_indices(batch_nb, data_length, FLAGS.batch_size)
|
||||||
|
|
||||||
|
# Prepare feed dictionary
|
||||||
|
feed_dict = {train_data_node: images[start:end]}
|
||||||
|
|
||||||
|
# Run session ([0] because run returns a batch with len 1st dim == 1)
|
||||||
|
preds[start:end, :] = sess.run([output], feed_dict=feed_dict)[0]
|
||||||
|
|
||||||
|
# Reset graph to allow multiple calls
|
||||||
|
tf.reset_default_graph()
|
||||||
|
|
||||||
|
return preds
|
424
research/pate_2017/input.py
Normal file
424
research/pate_2017/input.py
Normal file
|
@ -0,0 +1,424 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import cPickle
|
||||||
|
import gzip
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from scipy.io import loadmat as loadmat
|
||||||
|
from six.moves import urllib
|
||||||
|
from six.moves import xrange
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
FLAGS = tf.flags.FLAGS
|
||||||
|
|
||||||
|
|
||||||
|
def create_dir_if_needed(dest_directory):
|
||||||
|
"""
|
||||||
|
Create directory if doesn't exist
|
||||||
|
:param dest_directory:
|
||||||
|
:return: True if everything went well
|
||||||
|
"""
|
||||||
|
if not tf.gfile.IsDirectory(dest_directory):
|
||||||
|
tf.gfile.MakeDirs(dest_directory)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_download(file_urls, directory):
|
||||||
|
"""
|
||||||
|
Download a set of files in temporary local folder
|
||||||
|
:param directory: the directory where to download
|
||||||
|
:return: a tuple of filepaths corresponding to the files given as input
|
||||||
|
"""
|
||||||
|
# Create directory if doesn't exist
|
||||||
|
assert create_dir_if_needed(directory)
|
||||||
|
|
||||||
|
# This list will include all URLS of the local copy of downloaded files
|
||||||
|
result = []
|
||||||
|
|
||||||
|
# For each file of the dataset
|
||||||
|
for file_url in file_urls:
|
||||||
|
# Extract filename
|
||||||
|
filename = file_url.split('/')[-1]
|
||||||
|
|
||||||
|
# If downloading from GitHub, remove suffix ?raw=True from local filename
|
||||||
|
if filename.endswith("?raw=true"):
|
||||||
|
filename = filename[:-9]
|
||||||
|
|
||||||
|
# Deduce local file url
|
||||||
|
#filepath = os.path.join(directory, filename)
|
||||||
|
filepath = directory + '/' + filename
|
||||||
|
|
||||||
|
# Add to result list
|
||||||
|
result.append(filepath)
|
||||||
|
|
||||||
|
# Test if file already exists
|
||||||
|
if not tf.gfile.Exists(filepath):
|
||||||
|
def _progress(count, block_size, total_size):
|
||||||
|
sys.stdout.write('\r>> Downloading %s %.1f%%' % (filename,
|
||||||
|
float(count * block_size) / float(total_size) * 100.0))
|
||||||
|
sys.stdout.flush()
|
||||||
|
filepath, _ = urllib.request.urlretrieve(file_url, filepath, _progress)
|
||||||
|
print()
|
||||||
|
statinfo = os.stat(filepath)
|
||||||
|
print('Successfully downloaded', filename, statinfo.st_size, 'bytes.')
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def image_whitening(data):
|
||||||
|
"""
|
||||||
|
Subtracts mean of image and divides by adjusted standard variance (for
|
||||||
|
stability). Operations are per image but performed for the entire array.
|
||||||
|
:param image: 4D array (ID, Height, Weight, Channel)
|
||||||
|
:return: 4D array (ID, Height, Weight, Channel)
|
||||||
|
"""
|
||||||
|
assert len(np.shape(data)) == 4
|
||||||
|
|
||||||
|
# Compute number of pixels in image
|
||||||
|
nb_pixels = np.shape(data)[1] * np.shape(data)[2] * np.shape(data)[3]
|
||||||
|
|
||||||
|
# Subtract mean
|
||||||
|
mean = np.mean(data, axis=(1,2,3))
|
||||||
|
|
||||||
|
ones = np.ones(np.shape(data)[1:4], dtype=np.float32)
|
||||||
|
for i in xrange(len(data)):
|
||||||
|
data[i, :, :, :] -= mean[i] * ones
|
||||||
|
|
||||||
|
# Compute adjusted standard variance
|
||||||
|
adj_std_var = np.maximum(np.ones(len(data), dtype=np.float32) / math.sqrt(nb_pixels), np.std(data, axis=(1,2,3))) #NOLINT(long-line)
|
||||||
|
|
||||||
|
# Divide image
|
||||||
|
for i in xrange(len(data)):
|
||||||
|
data[i, :, :, :] = data[i, :, :, :] / adj_std_var[i]
|
||||||
|
|
||||||
|
print(np.shape(data))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def extract_svhn(local_url):
|
||||||
|
"""
|
||||||
|
Extract a MATLAB matrix into two numpy arrays with data and labels
|
||||||
|
:param local_url:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tf.gfile.Open(local_url, mode='r') as file_obj:
|
||||||
|
# Load MATLAB matrix using scipy IO
|
||||||
|
dict = loadmat(file_obj)
|
||||||
|
|
||||||
|
# Extract each dictionary (one for data, one for labels)
|
||||||
|
data, labels = dict["X"], dict["y"]
|
||||||
|
|
||||||
|
# Set np type
|
||||||
|
data = np.asarray(data, dtype=np.float32)
|
||||||
|
labels = np.asarray(labels, dtype=np.int32)
|
||||||
|
|
||||||
|
# Transpose data to match TF model input format
|
||||||
|
data = data.transpose(3, 0, 1, 2)
|
||||||
|
|
||||||
|
# Fix the SVHN labels which label 0s as 10s
|
||||||
|
labels[labels == 10] = 0
|
||||||
|
|
||||||
|
# Fix label dimensions
|
||||||
|
labels = labels.reshape(len(labels))
|
||||||
|
|
||||||
|
return data, labels
|
||||||
|
|
||||||
|
|
||||||
|
def unpickle_cifar_dic(file):
|
||||||
|
"""
|
||||||
|
Helper function: unpickles a dictionary (used for loading CIFAR)
|
||||||
|
:param file: filename of the pickle
|
||||||
|
:return: tuple of (images, labels)
|
||||||
|
"""
|
||||||
|
fo = open(file, 'rb')
|
||||||
|
dict = cPickle.load(fo)
|
||||||
|
fo.close()
|
||||||
|
return dict['data'], dict['labels']
|
||||||
|
|
||||||
|
|
||||||
|
def extract_cifar10(local_url, data_dir):
|
||||||
|
"""
|
||||||
|
Extracts the CIFAR-10 dataset and return numpy arrays with the different sets
|
||||||
|
:param local_url: where the tar.gz archive is located locally
|
||||||
|
:param data_dir: where to extract the archive's file
|
||||||
|
:return: a tuple (train data, train labels, test data, test labels)
|
||||||
|
"""
|
||||||
|
# These numpy dumps can be reloaded to avoid performing the pre-processing
|
||||||
|
# if they exist in the working directory.
|
||||||
|
# Changing the order of this list will ruin the indices below.
|
||||||
|
preprocessed_files = ['/cifar10_train.npy',
|
||||||
|
'/cifar10_train_labels.npy',
|
||||||
|
'/cifar10_test.npy',
|
||||||
|
'/cifar10_test_labels.npy']
|
||||||
|
|
||||||
|
all_preprocessed = True
|
||||||
|
for file in preprocessed_files:
|
||||||
|
if not tf.gfile.Exists(data_dir + file):
|
||||||
|
all_preprocessed = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if all_preprocessed:
|
||||||
|
# Reload pre-processed training data from numpy dumps
|
||||||
|
with tf.gfile.Open(data_dir + preprocessed_files[0], mode='r') as file_obj:
|
||||||
|
train_data = np.load(file_obj)
|
||||||
|
with tf.gfile.Open(data_dir + preprocessed_files[1], mode='r') as file_obj:
|
||||||
|
train_labels = np.load(file_obj)
|
||||||
|
|
||||||
|
# Reload pre-processed testing data from numpy dumps
|
||||||
|
with tf.gfile.Open(data_dir + preprocessed_files[2], mode='r') as file_obj:
|
||||||
|
test_data = np.load(file_obj)
|
||||||
|
with tf.gfile.Open(data_dir + preprocessed_files[3], mode='r') as file_obj:
|
||||||
|
test_labels = np.load(file_obj)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Do everything from scratch
|
||||||
|
# Define lists of all files we should extract
|
||||||
|
train_files = ["data_batch_" + str(i) for i in xrange(1,6)]
|
||||||
|
test_file = ["test_batch"]
|
||||||
|
cifar10_files = train_files + test_file
|
||||||
|
|
||||||
|
# Check if all files have already been extracted
|
||||||
|
need_to_unpack = False
|
||||||
|
for file in cifar10_files:
|
||||||
|
if not tf.gfile.Exists(file):
|
||||||
|
need_to_unpack = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# We have to unpack the archive
|
||||||
|
if need_to_unpack:
|
||||||
|
tarfile.open(local_url, 'r:gz').extractall(data_dir)
|
||||||
|
|
||||||
|
# Load training images and labels
|
||||||
|
images = []
|
||||||
|
labels = []
|
||||||
|
for file in train_files:
|
||||||
|
# Construct filename
|
||||||
|
filename = data_dir + "/cifar-10-batches-py/" + file
|
||||||
|
|
||||||
|
# Unpickle dictionary and extract images and labels
|
||||||
|
images_tmp, labels_tmp = unpickle_cifar_dic(filename)
|
||||||
|
|
||||||
|
# Append to lists
|
||||||
|
images.append(images_tmp)
|
||||||
|
labels.append(labels_tmp)
|
||||||
|
|
||||||
|
# Convert to numpy arrays and reshape in the expected format
|
||||||
|
train_data = np.asarray(images, dtype=np.float32).reshape((50000,3,32,32))
|
||||||
|
train_data = np.swapaxes(train_data, 1, 3)
|
||||||
|
train_labels = np.asarray(labels, dtype=np.int32).reshape(50000)
|
||||||
|
|
||||||
|
# Save so we don't have to do this again
|
||||||
|
np.save(data_dir + preprocessed_files[0], train_data)
|
||||||
|
np.save(data_dir + preprocessed_files[1], train_labels)
|
||||||
|
|
||||||
|
# Construct filename for test file
|
||||||
|
filename = data_dir + "/cifar-10-batches-py/" + test_file[0]
|
||||||
|
|
||||||
|
# Load test images and labels
|
||||||
|
test_data, test_images = unpickle_cifar_dic(filename)
|
||||||
|
|
||||||
|
# Convert to numpy arrays and reshape in the expected format
|
||||||
|
test_data = np.asarray(test_data,dtype=np.float32).reshape((10000,3,32,32))
|
||||||
|
test_data = np.swapaxes(test_data, 1, 3)
|
||||||
|
test_labels = np.asarray(test_images, dtype=np.int32).reshape(10000)
|
||||||
|
|
||||||
|
# Save so we don't have to do this again
|
||||||
|
np.save(data_dir + preprocessed_files[2], test_data)
|
||||||
|
np.save(data_dir + preprocessed_files[3], test_labels)
|
||||||
|
|
||||||
|
return train_data, train_labels, test_data, test_labels
|
||||||
|
|
||||||
|
|
||||||
|
def extract_mnist_data(filename, num_images, image_size, pixel_depth):
|
||||||
|
"""
|
||||||
|
Extract the images into a 4D tensor [image index, y, x, channels].
|
||||||
|
|
||||||
|
Values are rescaled from [0, 255] down to [-0.5, 0.5].
|
||||||
|
"""
|
||||||
|
# if not os.path.exists(file):
|
||||||
|
if not tf.gfile.Exists(filename+".npy"):
|
||||||
|
with gzip.open(filename) as bytestream:
|
||||||
|
bytestream.read(16)
|
||||||
|
buf = bytestream.read(image_size * image_size * num_images)
|
||||||
|
data = np.frombuffer(buf, dtype=np.uint8).astype(np.float32)
|
||||||
|
data = (data - (pixel_depth / 2.0)) / pixel_depth
|
||||||
|
data = data.reshape(num_images, image_size, image_size, 1)
|
||||||
|
np.save(filename, data)
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
with tf.gfile.Open(filename+".npy", mode='r') as file_obj:
|
||||||
|
return np.load(file_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_mnist_labels(filename, num_images):
|
||||||
|
"""
|
||||||
|
Extract the labels into a vector of int64 label IDs.
|
||||||
|
"""
|
||||||
|
# if not os.path.exists(file):
|
||||||
|
if not tf.gfile.Exists(filename+".npy"):
|
||||||
|
with gzip.open(filename) as bytestream:
|
||||||
|
bytestream.read(8)
|
||||||
|
buf = bytestream.read(1 * num_images)
|
||||||
|
labels = np.frombuffer(buf, dtype=np.uint8).astype(np.int32)
|
||||||
|
np.save(filename, labels)
|
||||||
|
return labels
|
||||||
|
else:
|
||||||
|
with tf.gfile.Open(filename+".npy", mode='r') as file_obj:
|
||||||
|
return np.load(file_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def ld_svhn(extended=False, test_only=False):
|
||||||
|
"""
|
||||||
|
Load the original SVHN data
|
||||||
|
:param extended: include extended training data in the returned array
|
||||||
|
:param test_only: disables loading of both train and extra -> large speed up
|
||||||
|
:return: tuple of arrays which depend on the parameters
|
||||||
|
"""
|
||||||
|
# Define files to be downloaded
|
||||||
|
# WARNING: changing the order of this list will break indices (cf. below)
|
||||||
|
file_urls = ['http://ufldl.stanford.edu/housenumbers/train_32x32.mat',
|
||||||
|
'http://ufldl.stanford.edu/housenumbers/test_32x32.mat',
|
||||||
|
'http://ufldl.stanford.edu/housenumbers/extra_32x32.mat']
|
||||||
|
|
||||||
|
# Maybe download data and retrieve local storage urls
|
||||||
|
local_urls = maybe_download(file_urls, FLAGS.data_dir)
|
||||||
|
|
||||||
|
# Extra Train, Test, and Extended Train data
|
||||||
|
if not test_only:
|
||||||
|
# Load and applying whitening to train data
|
||||||
|
train_data, train_labels = extract_svhn(local_urls[0])
|
||||||
|
train_data = image_whitening(train_data)
|
||||||
|
|
||||||
|
# Load and applying whitening to extended train data
|
||||||
|
ext_data, ext_labels = extract_svhn(local_urls[2])
|
||||||
|
ext_data = image_whitening(ext_data)
|
||||||
|
|
||||||
|
# Load and applying whitening to test data
|
||||||
|
test_data, test_labels = extract_svhn(local_urls[1])
|
||||||
|
test_data = image_whitening(test_data)
|
||||||
|
|
||||||
|
if test_only:
|
||||||
|
return test_data, test_labels
|
||||||
|
else:
|
||||||
|
if extended:
|
||||||
|
# Stack train data with the extended training data
|
||||||
|
train_data = np.vstack((train_data, ext_data))
|
||||||
|
train_labels = np.hstack((train_labels, ext_labels))
|
||||||
|
|
||||||
|
return train_data, train_labels, test_data, test_labels
|
||||||
|
else:
|
||||||
|
# Return training and extended training data separately
|
||||||
|
return train_data,train_labels, test_data,test_labels, ext_data,ext_labels
|
||||||
|
|
||||||
|
|
||||||
|
def ld_cifar10(test_only=False):
|
||||||
|
"""
|
||||||
|
Load the original CIFAR10 data
|
||||||
|
:param extended: include extended training data in the returned array
|
||||||
|
:param test_only: disables loading of both train and extra -> large speed up
|
||||||
|
:return: tuple of arrays which depend on the parameters
|
||||||
|
"""
|
||||||
|
# Define files to be downloaded
|
||||||
|
file_urls = ['https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz']
|
||||||
|
|
||||||
|
# Maybe download data and retrieve local storage urls
|
||||||
|
local_urls = maybe_download(file_urls, FLAGS.data_dir)
|
||||||
|
|
||||||
|
# Extract archives and return different sets
|
||||||
|
dataset = extract_cifar10(local_urls[0], FLAGS.data_dir)
|
||||||
|
|
||||||
|
# Unpack tuple
|
||||||
|
train_data, train_labels, test_data, test_labels = dataset
|
||||||
|
|
||||||
|
# Apply whitening to input data
|
||||||
|
train_data = image_whitening(train_data)
|
||||||
|
test_data = image_whitening(test_data)
|
||||||
|
|
||||||
|
if test_only:
|
||||||
|
return test_data, test_labels
|
||||||
|
else:
|
||||||
|
return train_data, train_labels, test_data, test_labels
|
||||||
|
|
||||||
|
|
||||||
|
def ld_mnist(test_only=False):
|
||||||
|
"""
|
||||||
|
Load the MNIST dataset
|
||||||
|
:param extended: include extended training data in the returned array
|
||||||
|
:param test_only: disables loading of both train and extra -> large speed up
|
||||||
|
:return: tuple of arrays which depend on the parameters
|
||||||
|
"""
|
||||||
|
# Define files to be downloaded
|
||||||
|
# WARNING: changing the order of this list will break indices (cf. below)
|
||||||
|
file_urls = ['http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz',
|
||||||
|
'http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz',
|
||||||
|
'http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz',
|
||||||
|
'http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Maybe download data and retrieve local storage urls
|
||||||
|
local_urls = maybe_download(file_urls, FLAGS.data_dir)
|
||||||
|
|
||||||
|
# Extract it into np arrays.
|
||||||
|
train_data = extract_mnist_data(local_urls[0], 60000, 28, 1)
|
||||||
|
train_labels = extract_mnist_labels(local_urls[1], 60000)
|
||||||
|
test_data = extract_mnist_data(local_urls[2], 10000, 28, 1)
|
||||||
|
test_labels = extract_mnist_labels(local_urls[3], 10000)
|
||||||
|
|
||||||
|
if test_only:
|
||||||
|
return test_data, test_labels
|
||||||
|
else:
|
||||||
|
return train_data, train_labels, test_data, test_labels
|
||||||
|
|
||||||
|
|
||||||
|
def partition_dataset(data, labels, nb_teachers, teacher_id):
|
||||||
|
"""
|
||||||
|
Simple partitioning algorithm that returns the right portion of the data
|
||||||
|
needed by a given teacher out of a certain nb of teachers
|
||||||
|
:param data: input data to be partitioned
|
||||||
|
:param labels: output data to be partitioned
|
||||||
|
:param nb_teachers: number of teachers in the ensemble (affects size of each
|
||||||
|
partition)
|
||||||
|
:param teacher_id: id of partition to retrieve
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
assert len(data) == len(labels)
|
||||||
|
assert int(teacher_id) < int(nb_teachers)
|
||||||
|
|
||||||
|
# This will floor the possible number of batches
|
||||||
|
batch_len = int(len(data) / nb_teachers)
|
||||||
|
|
||||||
|
# Compute start, end indices of partition
|
||||||
|
start = teacher_id * batch_len
|
||||||
|
end = (teacher_id+1) * batch_len
|
||||||
|
|
||||||
|
# Slice partition off
|
||||||
|
partition_data = data[start:end]
|
||||||
|
partition_labels = labels[start:end]
|
||||||
|
|
||||||
|
return partition_data, partition_labels
|
49
research/pate_2017/metrics.py
Normal file
49
research/pate_2017/metrics.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def accuracy(logits, labels):
|
||||||
|
"""
|
||||||
|
Return accuracy of the array of logits (or label predictions) wrt the labels
|
||||||
|
:param logits: this can either be logits, probabilities, or a single label
|
||||||
|
:param labels: the correct labels to match against
|
||||||
|
:return: the accuracy as a float
|
||||||
|
"""
|
||||||
|
assert len(logits) == len(labels)
|
||||||
|
|
||||||
|
if len(np.shape(logits)) > 1:
|
||||||
|
# Predicted labels are the argmax over axis 1
|
||||||
|
predicted_labels = np.argmax(logits, axis=1)
|
||||||
|
else:
|
||||||
|
# Input was already labels
|
||||||
|
assert len(np.shape(logits)) == 1
|
||||||
|
predicted_labels = logits
|
||||||
|
|
||||||
|
# Check against correct labels to compute correct guesses
|
||||||
|
correct = np.sum(predicted_labels == labels.reshape(len(labels)))
|
||||||
|
|
||||||
|
# Divide by number of labels to obtain accuracy
|
||||||
|
accuracy = float(correct) / len(labels)
|
||||||
|
|
||||||
|
# Return float value
|
||||||
|
return accuracy
|
||||||
|
|
||||||
|
|
208
research/pate_2017/train_student.py
Normal file
208
research/pate_2017/train_student.py
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from six.moves import xrange
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
from differential_privacy.multiple_teachers import aggregation
|
||||||
|
from differential_privacy.multiple_teachers import deep_cnn
|
||||||
|
from differential_privacy.multiple_teachers import input
|
||||||
|
from differential_privacy.multiple_teachers import metrics
|
||||||
|
|
||||||
|
FLAGS = tf.flags.FLAGS
|
||||||
|
|
||||||
|
tf.flags.DEFINE_string('dataset', 'svhn', 'The name of the dataset to use')
|
||||||
|
tf.flags.DEFINE_integer('nb_labels', 10, 'Number of output classes')
|
||||||
|
|
||||||
|
tf.flags.DEFINE_string('data_dir','/tmp','Temporary storage')
|
||||||
|
tf.flags.DEFINE_string('train_dir','/tmp/train_dir','Where model chkpt are saved')
|
||||||
|
tf.flags.DEFINE_string('teachers_dir','/tmp/train_dir',
|
||||||
|
'Directory where teachers checkpoints are stored.')
|
||||||
|
|
||||||
|
tf.flags.DEFINE_integer('teachers_max_steps', 3000,
|
||||||
|
'Number of steps teachers were ran.')
|
||||||
|
tf.flags.DEFINE_integer('max_steps', 3000, 'Number of steps to run student.')
|
||||||
|
tf.flags.DEFINE_integer('nb_teachers', 10, 'Teachers in the ensemble.')
|
||||||
|
tf.flags.DEFINE_integer('stdnt_share', 1000,
|
||||||
|
'Student share (last index) of the test data')
|
||||||
|
tf.flags.DEFINE_integer('lap_scale', 10,
|
||||||
|
'Scale of the Laplacian noise added for privacy')
|
||||||
|
tf.flags.DEFINE_boolean('save_labels', False,
|
||||||
|
'Dump numpy arrays of labels and clean teacher votes')
|
||||||
|
tf.flags.DEFINE_boolean('deeper', False, 'Activate deeper CNN model')
|
||||||
|
|
||||||
|
|
||||||
|
def ensemble_preds(dataset, nb_teachers, stdnt_data):
|
||||||
|
"""
|
||||||
|
Given a dataset, a number of teachers, and some input data, this helper
|
||||||
|
function queries each teacher for predictions on the data and returns
|
||||||
|
all predictions in a single array. (That can then be aggregated into
|
||||||
|
one single prediction per input using aggregation.py (cf. function
|
||||||
|
prepare_student_data() below)
|
||||||
|
:param dataset: string corresponding to mnist, cifar10, or svhn
|
||||||
|
:param nb_teachers: number of teachers (in the ensemble) to learn from
|
||||||
|
:param stdnt_data: unlabeled student training data
|
||||||
|
:return: 3d array (teacher id, sample id, probability per class)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Compute shape of array that will hold probabilities produced by each
|
||||||
|
# teacher, for each training point, and each output class
|
||||||
|
result_shape = (nb_teachers, len(stdnt_data), FLAGS.nb_labels)
|
||||||
|
|
||||||
|
# Create array that will hold result
|
||||||
|
result = np.zeros(result_shape, dtype=np.float32)
|
||||||
|
|
||||||
|
# Get predictions from each teacher
|
||||||
|
for teacher_id in xrange(nb_teachers):
|
||||||
|
# Compute path of checkpoint file for teacher model with ID teacher_id
|
||||||
|
if FLAGS.deeper:
|
||||||
|
ckpt_path = FLAGS.teachers_dir + '/' + str(dataset) + '_' + str(nb_teachers) + '_teachers_' + str(teacher_id) + '_deep.ckpt-' + str(FLAGS.teachers_max_steps - 1) #NOLINT(long-line)
|
||||||
|
else:
|
||||||
|
ckpt_path = FLAGS.teachers_dir + '/' + str(dataset) + '_' + str(nb_teachers) + '_teachers_' + str(teacher_id) + '.ckpt-' + str(FLAGS.teachers_max_steps - 1) # NOLINT(long-line)
|
||||||
|
|
||||||
|
# Get predictions on our training data and store in result array
|
||||||
|
result[teacher_id] = deep_cnn.softmax_preds(stdnt_data, ckpt_path)
|
||||||
|
|
||||||
|
# This can take a while when there are a lot of teachers so output status
|
||||||
|
print("Computed Teacher " + str(teacher_id) + " softmax predictions")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_student_data(dataset, nb_teachers, save=False):
|
||||||
|
"""
|
||||||
|
Takes a dataset name and the size of the teacher ensemble and prepares
|
||||||
|
training data for the student model, according to parameters indicated
|
||||||
|
in flags above.
|
||||||
|
:param dataset: string corresponding to mnist, cifar10, or svhn
|
||||||
|
:param nb_teachers: number of teachers (in the ensemble) to learn from
|
||||||
|
:param save: if set to True, will dump student training labels predicted by
|
||||||
|
the ensemble of teachers (with Laplacian noise) as npy files.
|
||||||
|
It also dumps the clean votes for each class (without noise) and
|
||||||
|
the labels assigned by teachers
|
||||||
|
:return: pairs of (data, labels) to be used for student training and testing
|
||||||
|
"""
|
||||||
|
assert input.create_dir_if_needed(FLAGS.train_dir)
|
||||||
|
|
||||||
|
# Load the dataset
|
||||||
|
if dataset == 'svhn':
|
||||||
|
test_data, test_labels = input.ld_svhn(test_only=True)
|
||||||
|
elif dataset == 'cifar10':
|
||||||
|
test_data, test_labels = input.ld_cifar10(test_only=True)
|
||||||
|
elif dataset == 'mnist':
|
||||||
|
test_data, test_labels = input.ld_mnist(test_only=True)
|
||||||
|
else:
|
||||||
|
print("Check value of dataset flag")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Make sure there is data leftover to be used as a test set
|
||||||
|
assert FLAGS.stdnt_share < len(test_data)
|
||||||
|
|
||||||
|
# Prepare [unlabeled] student training data (subset of test set)
|
||||||
|
stdnt_data = test_data[:FLAGS.stdnt_share]
|
||||||
|
|
||||||
|
# Compute teacher predictions for student training data
|
||||||
|
teachers_preds = ensemble_preds(dataset, nb_teachers, stdnt_data)
|
||||||
|
|
||||||
|
# Aggregate teacher predictions to get student training labels
|
||||||
|
if not save:
|
||||||
|
stdnt_labels = aggregation.noisy_max(teachers_preds, FLAGS.lap_scale)
|
||||||
|
else:
|
||||||
|
# Request clean votes and clean labels as well
|
||||||
|
stdnt_labels, clean_votes, labels_for_dump = aggregation.noisy_max(teachers_preds, FLAGS.lap_scale, return_clean_votes=True) #NOLINT(long-line)
|
||||||
|
|
||||||
|
# Prepare filepath for numpy dump of clean votes
|
||||||
|
filepath = FLAGS.data_dir + "/" + str(dataset) + '_' + str(nb_teachers) + '_student_clean_votes_lap_' + str(FLAGS.lap_scale) + '.npy' # NOLINT(long-line)
|
||||||
|
|
||||||
|
# Prepare filepath for numpy dump of clean labels
|
||||||
|
filepath_labels = FLAGS.data_dir + "/" + str(dataset) + '_' + str(nb_teachers) + '_teachers_labels_lap_' + str(FLAGS.lap_scale) + '.npy' # NOLINT(long-line)
|
||||||
|
|
||||||
|
# Dump clean_votes array
|
||||||
|
with tf.gfile.Open(filepath, mode='w') as file_obj:
|
||||||
|
np.save(file_obj, clean_votes)
|
||||||
|
|
||||||
|
# Dump labels_for_dump array
|
||||||
|
with tf.gfile.Open(filepath_labels, mode='w') as file_obj:
|
||||||
|
np.save(file_obj, labels_for_dump)
|
||||||
|
|
||||||
|
# Print accuracy of aggregated labels
|
||||||
|
ac_ag_labels = metrics.accuracy(stdnt_labels, test_labels[:FLAGS.stdnt_share])
|
||||||
|
print("Accuracy of the aggregated labels: " + str(ac_ag_labels))
|
||||||
|
|
||||||
|
# Store unused part of test set for use as a test set after student training
|
||||||
|
stdnt_test_data = test_data[FLAGS.stdnt_share:]
|
||||||
|
stdnt_test_labels = test_labels[FLAGS.stdnt_share:]
|
||||||
|
|
||||||
|
if save:
|
||||||
|
# Prepare filepath for numpy dump of labels produced by noisy aggregation
|
||||||
|
filepath = FLAGS.data_dir + "/" + str(dataset) + '_' + str(nb_teachers) + '_student_labels_lap_' + str(FLAGS.lap_scale) + '.npy' #NOLINT(long-line)
|
||||||
|
|
||||||
|
# Dump student noisy labels array
|
||||||
|
with tf.gfile.Open(filepath, mode='w') as file_obj:
|
||||||
|
np.save(file_obj, stdnt_labels)
|
||||||
|
|
||||||
|
return stdnt_data, stdnt_labels, stdnt_test_data, stdnt_test_labels
|
||||||
|
|
||||||
|
|
||||||
|
def train_student(dataset, nb_teachers):
|
||||||
|
"""
|
||||||
|
This function trains a student using predictions made by an ensemble of
|
||||||
|
teachers. The student and teacher models are trained using the same
|
||||||
|
neural network architecture.
|
||||||
|
:param dataset: string corresponding to mnist, cifar10, or svhn
|
||||||
|
:param nb_teachers: number of teachers (in the ensemble) to learn from
|
||||||
|
:return: True if student training went well
|
||||||
|
"""
|
||||||
|
assert input.create_dir_if_needed(FLAGS.train_dir)
|
||||||
|
|
||||||
|
# Call helper function to prepare student data using teacher predictions
|
||||||
|
stdnt_dataset = prepare_student_data(dataset, nb_teachers, save=True)
|
||||||
|
|
||||||
|
# Unpack the student dataset
|
||||||
|
stdnt_data, stdnt_labels, stdnt_test_data, stdnt_test_labels = stdnt_dataset
|
||||||
|
|
||||||
|
# Prepare checkpoint filename and path
|
||||||
|
if FLAGS.deeper:
|
||||||
|
ckpt_path = FLAGS.train_dir + '/' + str(dataset) + '_' + str(nb_teachers) + '_student_deeper.ckpt' #NOLINT(long-line)
|
||||||
|
else:
|
||||||
|
ckpt_path = FLAGS.train_dir + '/' + str(dataset) + '_' + str(nb_teachers) + '_student.ckpt' # NOLINT(long-line)
|
||||||
|
|
||||||
|
# Start student training
|
||||||
|
assert deep_cnn.train(stdnt_data, stdnt_labels, ckpt_path)
|
||||||
|
|
||||||
|
# Compute final checkpoint name for student (with max number of steps)
|
||||||
|
ckpt_path_final = ckpt_path + '-' + str(FLAGS.max_steps - 1)
|
||||||
|
|
||||||
|
# Compute student label predictions on remaining chunk of test set
|
||||||
|
student_preds = deep_cnn.softmax_preds(stdnt_test_data, ckpt_path_final)
|
||||||
|
|
||||||
|
# Compute teacher accuracy
|
||||||
|
precision = metrics.accuracy(student_preds, stdnt_test_labels)
|
||||||
|
print('Precision of student after training: ' + str(precision))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main(argv=None): # pylint: disable=unused-argument
|
||||||
|
# Run student training according to values specified in flags
|
||||||
|
assert train_student(FLAGS.dataset, FLAGS.nb_teachers)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
tf.app.run()
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# Be sure to clone https://github.com/openai/improved-gan
|
||||||
|
# and add improved-gan/mnist_svhn_cifar10 to your PATH variable
|
||||||
|
|
||||||
|
# Download labels used to train the student
|
||||||
|
wget https://github.com/npapernot/multiple-teachers-for-privacy/blob/master/mnist_250_student_labels_lap_20.npy
|
||||||
|
|
||||||
|
# Train the student using improved-gan
|
||||||
|
THEANO_FLAGS='floatX=float32,device=gpu,lib.cnmem=1' train_mnist_fm_custom_labels.py --labels mnist_250_student_labels_lap_20.npy --count 50 --epochs 600
|
||||||
|
|
104
research/pate_2017/train_teachers.py
Normal file
104
research/pate_2017/train_teachers.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
from differential_privacy.multiple_teachers import deep_cnn
|
||||||
|
from differential_privacy.multiple_teachers import input
|
||||||
|
from differential_privacy.multiple_teachers import metrics
|
||||||
|
|
||||||
|
|
||||||
|
tf.flags.DEFINE_string('dataset', 'svhn', 'The name of the dataset to use')
|
||||||
|
tf.flags.DEFINE_integer('nb_labels', 10, 'Number of output classes')
|
||||||
|
|
||||||
|
tf.flags.DEFINE_string('data_dir','/tmp','Temporary storage')
|
||||||
|
tf.flags.DEFINE_string('train_dir','/tmp/train_dir',
|
||||||
|
'Where model ckpt are saved')
|
||||||
|
|
||||||
|
tf.flags.DEFINE_integer('max_steps', 3000, 'Number of training steps to run.')
|
||||||
|
tf.flags.DEFINE_integer('nb_teachers', 50, 'Teachers in the ensemble.')
|
||||||
|
tf.flags.DEFINE_integer('teacher_id', 0, 'ID of teacher being trained.')
|
||||||
|
|
||||||
|
tf.flags.DEFINE_boolean('deeper', False, 'Activate deeper CNN model')
|
||||||
|
|
||||||
|
FLAGS = tf.flags.FLAGS
|
||||||
|
|
||||||
|
|
||||||
|
def train_teacher(dataset, nb_teachers, teacher_id):
|
||||||
|
"""
|
||||||
|
This function trains a teacher (teacher id) among an ensemble of nb_teachers
|
||||||
|
models for the dataset specified.
|
||||||
|
:param dataset: string corresponding to dataset (svhn, cifar10)
|
||||||
|
:param nb_teachers: total number of teachers in the ensemble
|
||||||
|
:param teacher_id: id of the teacher being trained
|
||||||
|
:return: True if everything went well
|
||||||
|
"""
|
||||||
|
# If working directories do not exist, create them
|
||||||
|
assert input.create_dir_if_needed(FLAGS.data_dir)
|
||||||
|
assert input.create_dir_if_needed(FLAGS.train_dir)
|
||||||
|
|
||||||
|
# Load the dataset
|
||||||
|
if dataset == 'svhn':
|
||||||
|
train_data,train_labels,test_data,test_labels = input.ld_svhn(extended=True)
|
||||||
|
elif dataset == 'cifar10':
|
||||||
|
train_data, train_labels, test_data, test_labels = input.ld_cifar10()
|
||||||
|
elif dataset == 'mnist':
|
||||||
|
train_data, train_labels, test_data, test_labels = input.ld_mnist()
|
||||||
|
else:
|
||||||
|
print("Check value of dataset flag")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Retrieve subset of data for this teacher
|
||||||
|
data, labels = input.partition_dataset(train_data,
|
||||||
|
train_labels,
|
||||||
|
nb_teachers,
|
||||||
|
teacher_id)
|
||||||
|
|
||||||
|
print("Length of training data: " + str(len(labels)))
|
||||||
|
|
||||||
|
# Define teacher checkpoint filename and full path
|
||||||
|
if FLAGS.deeper:
|
||||||
|
filename = str(nb_teachers) + '_teachers_' + str(teacher_id) + '_deep.ckpt'
|
||||||
|
else:
|
||||||
|
filename = str(nb_teachers) + '_teachers_' + str(teacher_id) + '.ckpt'
|
||||||
|
ckpt_path = FLAGS.train_dir + '/' + str(dataset) + '_' + filename
|
||||||
|
|
||||||
|
# Perform teacher training
|
||||||
|
assert deep_cnn.train(data, labels, ckpt_path)
|
||||||
|
|
||||||
|
# Append final step value to checkpoint for evaluation
|
||||||
|
ckpt_path_final = ckpt_path + '-' + str(FLAGS.max_steps - 1)
|
||||||
|
|
||||||
|
# Retrieve teacher probability estimates on the test data
|
||||||
|
teacher_preds = deep_cnn.softmax_preds(test_data, ckpt_path_final)
|
||||||
|
|
||||||
|
# Compute teacher accuracy
|
||||||
|
precision = metrics.accuracy(teacher_preds, test_labels)
|
||||||
|
print('Precision of teacher after training: ' + str(precision))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None): # pylint: disable=unused-argument
|
||||||
|
# Make a call to train_teachers with values specified in flags
|
||||||
|
assert train_teacher(FLAGS.dataset, FLAGS.nb_teachers, FLAGS.teacher_id)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
tf.app.run()
|
35
research/pate_2017/utils.py
Normal file
35
research/pate_2017/utils.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def batch_indices(batch_nb, data_length, batch_size):
|
||||||
|
"""
|
||||||
|
This helper function computes a batch start and end index
|
||||||
|
:param batch_nb: the batch number
|
||||||
|
:param data_length: the total length of the data being parsed by batches
|
||||||
|
:param batch_size: the number of inputs in each batch
|
||||||
|
:return: pair of (start, end) indices
|
||||||
|
"""
|
||||||
|
# Batch start and end index
|
||||||
|
start = int(batch_nb * batch_size)
|
||||||
|
end = int((batch_nb + 1) * batch_size)
|
||||||
|
|
||||||
|
# When there are not enough inputs left, we reuse some to complete the batch
|
||||||
|
if end > data_length:
|
||||||
|
shift = end - data_length
|
||||||
|
start -= shift
|
||||||
|
end -= shift
|
||||||
|
|
||||||
|
return start, end
|
61
research/pate_2018/ICLR2018/README.md
Normal file
61
research/pate_2018/ICLR2018/README.md
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
Scripts in support of the paper "Scalable Private Learning with PATE" by Nicolas
|
||||||
|
Papernot, Shuang Song, Ilya Mironov, Ananth Raghunathan, Kunal Talwar, Ulfar
|
||||||
|
Erlingsson (ICLR 2018, https://arxiv.org/abs/1802.08908).
|
||||||
|
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* Python, version ≥ 2.7
|
||||||
|
* absl (see [here](https://github.com/abseil/abseil-py), or just type `pip install absl-py`)
|
||||||
|
* matplotlib
|
||||||
|
* numpy
|
||||||
|
* scipy
|
||||||
|
* sympy (for smooth sensitivity analysis)
|
||||||
|
* write access to the current directory (otherwise, output directories in download.py and *.sh
|
||||||
|
scripts must be changed)
|
||||||
|
|
||||||
|
## Reproducing Figures 1 and 5, and Table 2
|
||||||
|
|
||||||
|
Before running any of the analysis scripts, create the data/ directory and download votes files by running\
|
||||||
|
`$ python download.py`
|
||||||
|
|
||||||
|
To generate Figures 1 and 5 run\
|
||||||
|
`$ sh generate_figures.sh`\
|
||||||
|
The output is written to the figures/ directory.
|
||||||
|
|
||||||
|
For Table 2 run (may take several hours)\
|
||||||
|
`$ sh generate_table.sh`\
|
||||||
|
The output is written to the console.
|
||||||
|
|
||||||
|
For data-independent bounds (for comparison with Table 2), run\
|
||||||
|
`$ sh generate_table_data_independent.sh`\
|
||||||
|
The output is written to the console.
|
||||||
|
|
||||||
|
## Files in this directory
|
||||||
|
|
||||||
|
* generate_figures.sh — Master script for generating Figures 1 and 5.
|
||||||
|
|
||||||
|
* generate_table.sh — Master script for generating Table 2.
|
||||||
|
|
||||||
|
* generate_table_data_independent.sh — Master script for computing data-independent
|
||||||
|
bounds.
|
||||||
|
|
||||||
|
* rdp_bucketized.py — Script for producing Figure 1 (right) and Figure 5 (right).
|
||||||
|
|
||||||
|
* rdp_cumulative.py — Script for producing Figure 1 (middle) and Figure 5 (left).
|
||||||
|
|
||||||
|
* smooth_sensitivity_table.py — Script for generating Table 2.
|
||||||
|
|
||||||
|
* utility_queries_answered — Script for producing Figure 1 (left).
|
||||||
|
|
||||||
|
* plot_partition.py — Script for producing partition.pdf, a detailed breakdown of privacy
|
||||||
|
costs for Confident-GNMax with smooth sensitivity analysis (takes ~50 hours).
|
||||||
|
|
||||||
|
* plots_for_slides.py — Script for producing several plots for the slide deck.
|
||||||
|
|
||||||
|
* download.py — Utility script for populating the data/ directory.
|
||||||
|
|
||||||
|
* plot_ls_q.py is not used.
|
||||||
|
|
||||||
|
|
||||||
|
All Python files take flags. Run script_name.py --help for help on flags.
|
43
research/pate_2018/ICLR2018/download.py
Normal file
43
research/pate_2018/ICLR2018/download.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""Script to download votes files to the data/ directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from six.moves import urllib
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
FILE_URI = 'https://storage.googleapis.com/pate-votes/votes.gz'
|
||||||
|
DATA_DIR = 'data/'
|
||||||
|
|
||||||
|
|
||||||
|
def download():
|
||||||
|
print('Downloading ' + FILE_URI)
|
||||||
|
tar_filename, _ = urllib.request.urlretrieve(FILE_URI)
|
||||||
|
print('Unpacking ' + tar_filename)
|
||||||
|
with tarfile.open(tar_filename, "r:gz") as tar:
|
||||||
|
tar.extractall(DATA_DIR)
|
||||||
|
print('Done!')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if not os.path.exists(DATA_DIR):
|
||||||
|
print('Data directory does not exist. Creating ' + DATA_DIR)
|
||||||
|
os.makedirs(DATA_DIR)
|
||||||
|
download()
|
43
research/pate_2018/ICLR2018/generate_figures.sh
Normal file
43
research/pate_2018/ICLR2018/generate_figures.sh
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
counts_file="data/glyph_5000_teachers.npy"
|
||||||
|
output_dir="figures/"
|
||||||
|
|
||||||
|
mkdir -p $output_dir
|
||||||
|
|
||||||
|
if [ ! -d "$output_dir" ]; then
|
||||||
|
echo "Directory $output_dir does not exist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python rdp_bucketized.py \
|
||||||
|
--plot=small \
|
||||||
|
--counts_file=$counts_file \
|
||||||
|
--plot_file=$output_dir"noisy_thresholding_check_perf.pdf"
|
||||||
|
|
||||||
|
python rdp_bucketized.py \
|
||||||
|
--plot=large \
|
||||||
|
--counts_file=$counts_file \
|
||||||
|
--plot_file=$output_dir"noisy_thresholding_check_perf_details.pdf"
|
||||||
|
|
||||||
|
python rdp_cumulative.py \
|
||||||
|
--cache=False \
|
||||||
|
--counts_file=$counts_file \
|
||||||
|
--figures_dir=$output_dir
|
||||||
|
|
||||||
|
python utility_queries_answered.py --plot_file=$output_dir"utility_queries_answered.pdf"
|
93
research/pate_2018/ICLR2018/generate_table.sh
Normal file
93
research/pate_2018/ICLR2018/generate_table.sh
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
echo "Reproducing Table 2. Takes a couple of hours."
|
||||||
|
|
||||||
|
executable="python smooth_sensitivity_table.py"
|
||||||
|
data_dir="data"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## MNIST ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/mnist_250_teachers.npy" \
|
||||||
|
--threshold=200 \
|
||||||
|
--sigma1=150 \
|
||||||
|
--sigma2=40 \
|
||||||
|
--queries=640 \
|
||||||
|
--delta=1e-5
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## SVHN ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/svhn_250_teachers.npy" \
|
||||||
|
--threshold=300 \
|
||||||
|
--sigma1=200 \
|
||||||
|
--sigma2=40 \
|
||||||
|
--queries=8500 \
|
||||||
|
--delta=1e-6
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Adult ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/adult_250_teachers.npy" \
|
||||||
|
--threshold=300 \
|
||||||
|
--sigma1=200 \
|
||||||
|
--sigma2=40 \
|
||||||
|
--queries=1500 \
|
||||||
|
--delta=1e-5
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Glyph (Confident) ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/glyph_5000_teachers.npy" \
|
||||||
|
--threshold=1000 \
|
||||||
|
--sigma1=500 \
|
||||||
|
--sigma2=100 \
|
||||||
|
--queries=12000 \
|
||||||
|
--delta=1e-8
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Glyph (Interactive, Round 1) ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/glyph_round1.npy" \
|
||||||
|
--threshold=3500 \
|
||||||
|
--sigma1=1500 \
|
||||||
|
--sigma2=100 \
|
||||||
|
--delta=1e-8
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Glyph (Interactive, Round 2) ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/glyph_round2.npy" \
|
||||||
|
--baseline_file=$data_dir"/glyph_round2_student.npy" \
|
||||||
|
--threshold=3500 \
|
||||||
|
--sigma1=2000 \
|
||||||
|
--sigma2=200 \
|
||||||
|
--teachers=5000 \
|
||||||
|
--delta=1e-8
|
|
@ -0,0 +1,99 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
echo "Table 2 with data-independent analysis."
|
||||||
|
|
||||||
|
executable="python smooth_sensitivity_table.py"
|
||||||
|
data_dir="data"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## MNIST ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/mnist_250_teachers.npy" \
|
||||||
|
--threshold=200 \
|
||||||
|
--sigma1=150 \
|
||||||
|
--sigma2=40 \
|
||||||
|
--queries=640 \
|
||||||
|
--delta=1e-5 \
|
||||||
|
--data_independent
|
||||||
|
echo
|
||||||
|
echo "######## SVHN ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/svhn_250_teachers.npy" \
|
||||||
|
--threshold=300 \
|
||||||
|
--sigma1=200 \
|
||||||
|
--sigma2=40 \
|
||||||
|
--queries=8500 \
|
||||||
|
--delta=1e-6 \
|
||||||
|
--data_independent
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Adult ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/adult_250_teachers.npy" \
|
||||||
|
--threshold=300 \
|
||||||
|
--sigma1=200 \
|
||||||
|
--sigma2=40 \
|
||||||
|
--queries=1500 \
|
||||||
|
--delta=1e-5 \
|
||||||
|
--data_independent
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Glyph (Confident) ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/glyph_5000_teachers.npy" \
|
||||||
|
--threshold=1000 \
|
||||||
|
--sigma1=500 \
|
||||||
|
--sigma2=100 \
|
||||||
|
--queries=12000 \
|
||||||
|
--delta=1e-8 \
|
||||||
|
--data_independent
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Glyph (Interactive, Round 1) ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/glyph_round1.npy" \
|
||||||
|
--threshold=3500 \
|
||||||
|
--sigma1=1500 \
|
||||||
|
--sigma2=100 \
|
||||||
|
--delta=1e-8 \
|
||||||
|
--data_independent
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "######## Glyph (Interactive, Round 2) ########"
|
||||||
|
echo
|
||||||
|
|
||||||
|
$executable \
|
||||||
|
--counts_file=$data_dir"/glyph_round2.npy" \
|
||||||
|
--baseline_file=$data_dir"/glyph_round2_student.npy" \
|
||||||
|
--threshold=3500 \
|
||||||
|
--sigma1=2000 \
|
||||||
|
--sigma2=200 \
|
||||||
|
--teachers=5000 \
|
||||||
|
--delta=1e-8 \
|
||||||
|
--order=8.5 \
|
||||||
|
--data_independent
|
105
research/pate_2018/ICLR2018/plot_ls_q.py
Normal file
105
research/pate_2018/ICLR2018/plot_ls_q.py
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Plots LS(q).
|
||||||
|
|
||||||
|
A script in support of the PATE2 paper. NOT PRESENTLY USED.
|
||||||
|
|
||||||
|
The output is written to a specified directory as a pdf file.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('..') # Main modules reside in the parent directory.
|
||||||
|
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
import matplotlib.pyplot as plt # pylint: disable=g-import-not-at-top
|
||||||
|
import numpy as np
|
||||||
|
import smooth_sensitivity as pate_ss
|
||||||
|
|
||||||
|
plt.style.use('ggplot')
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
|
||||||
|
flags.DEFINE_string('figures_dir', '', 'Path where the output is written to.')
|
||||||
|
|
||||||
|
|
||||||
|
def compute_ls_q(sigma, order, num_classes):
|
||||||
|
|
||||||
|
def beta(q):
|
||||||
|
return pate_ss._compute_rdp_gnmax(sigma, math.log(q), order)
|
||||||
|
|
||||||
|
def bu(q):
|
||||||
|
return pate_ss._compute_bu_gnmax(q, sigma, order)
|
||||||
|
|
||||||
|
def bl(q):
|
||||||
|
return pate_ss._compute_bl_gnmax(q, sigma, order)
|
||||||
|
|
||||||
|
def delta_beta(q):
|
||||||
|
if q == 0 or q > .8:
|
||||||
|
return 0
|
||||||
|
beta_q = beta(q)
|
||||||
|
beta_bu_q = beta(bu(q))
|
||||||
|
beta_bl_q = beta(bl(q))
|
||||||
|
assert beta_bl_q <= beta_q <= beta_bu_q
|
||||||
|
return beta_bu_q - beta_q # max(beta_bu_q - beta_q, beta_q - beta_bl_q)
|
||||||
|
|
||||||
|
logq0 = pate_ss.compute_logq0_gnmax(sigma, order)
|
||||||
|
logq1 = pate_ss._compute_logq1(sigma, order, num_classes)
|
||||||
|
print(math.exp(logq1), math.exp(logq0))
|
||||||
|
xs = np.linspace(0, .1, num=1000, endpoint=True)
|
||||||
|
ys = [delta_beta(x) for x in xs]
|
||||||
|
return xs, ys
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
|
||||||
|
sigma = 20
|
||||||
|
order = 20.
|
||||||
|
num_classes = 10
|
||||||
|
|
||||||
|
# sigma = 20
|
||||||
|
# order = 25.
|
||||||
|
# num_classes = 10
|
||||||
|
|
||||||
|
x_axis, ys = compute_ls_q(sigma, order, num_classes)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.5)
|
||||||
|
fig.set_figwidth(4.7)
|
||||||
|
|
||||||
|
ax.plot(x_axis, ys, alpha=.8, linewidth=5)
|
||||||
|
plt.xlabel('Number of queries answered', fontsize=16)
|
||||||
|
plt.ylabel(r'Privacy cost $\varepsilon$ at $\delta=10^{-8}$', fontsize=16)
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
fout_name = os.path.join(FLAGS.figures_dir, 'ls_of_q.pdf')
|
||||||
|
print('Saving the graph to ' + fout_name)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
plt.close('all')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
412
research/pate_2018/ICLR2018/plot_partition.py
Normal file
412
research/pate_2018/ICLR2018/plot_partition.py
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Produces two plots. One compares aggregators and their analyses. The other
|
||||||
|
illustrates sources of privacy loss for Confident-GNMax.
|
||||||
|
|
||||||
|
A script in support of the paper "Scalable Private Learning with PATE" by
|
||||||
|
Nicolas Papernot, Shuang Song, Ilya Mironov, Ananth Raghunathan, Kunal Talwar,
|
||||||
|
Ulfar Erlingsson (https://arxiv.org/abs/1802.08908).
|
||||||
|
|
||||||
|
The input is a file containing a numpy array of votes, one query per row, one
|
||||||
|
class per column. Ex:
|
||||||
|
43, 1821, ..., 3
|
||||||
|
31, 16, ..., 0
|
||||||
|
...
|
||||||
|
0, 86, ..., 438
|
||||||
|
The output is written to a specified directory and consists of two files.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('..') # Main modules reside in the parent directory.
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
from collections import namedtuple
|
||||||
|
import matplotlib
|
||||||
|
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
import matplotlib.pyplot as plt # pylint: disable=g-import-not-at-top
|
||||||
|
import numpy as np
|
||||||
|
import core as pate
|
||||||
|
import smooth_sensitivity as pate_ss
|
||||||
|
|
||||||
|
plt.style.use('ggplot')
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_boolean('cache', False,
|
||||||
|
'Read results of privacy analysis from cache.')
|
||||||
|
flags.DEFINE_string('counts_file', None, 'Counts file.')
|
||||||
|
flags.DEFINE_string('figures_dir', '', 'Path where figures are written to.')
|
||||||
|
flags.DEFINE_float('threshold', None, 'Threshold for step 1 (selection).')
|
||||||
|
flags.DEFINE_float('sigma1', None, 'Sigma for step 1 (selection).')
|
||||||
|
flags.DEFINE_float('sigma2', None, 'Sigma for step 2 (argmax).')
|
||||||
|
flags.DEFINE_integer('queries', None, 'Number of queries made by the student.')
|
||||||
|
flags.DEFINE_float('delta', 1e-8, 'Target delta.')
|
||||||
|
|
||||||
|
flags.mark_flag_as_required('counts_file')
|
||||||
|
flags.mark_flag_as_required('threshold')
|
||||||
|
flags.mark_flag_as_required('sigma1')
|
||||||
|
flags.mark_flag_as_required('sigma2')
|
||||||
|
|
||||||
|
Partition = namedtuple('Partition', ['step1', 'step2', 'ss', 'delta'])
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_gnmax_conf_data_ind(votes, threshold, sigma1, sigma2, delta):
|
||||||
|
orders = np.logspace(np.log10(1.5), np.log10(500), num=100)
|
||||||
|
n = votes.shape[0]
|
||||||
|
|
||||||
|
rdp_total = np.zeros(len(orders))
|
||||||
|
answered_total = 0
|
||||||
|
answered = np.zeros(n)
|
||||||
|
eps_cum = np.full(n, None, dtype=float)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
v = votes[i,]
|
||||||
|
if threshold is not None and sigma1 is not None:
|
||||||
|
q_step1 = np.exp(pate.compute_logpr_answered(threshold, sigma1, v))
|
||||||
|
rdp_total += pate.rdp_data_independent_gaussian(sigma1, orders)
|
||||||
|
else:
|
||||||
|
q_step1 = 1. # always answer
|
||||||
|
|
||||||
|
answered_total += q_step1
|
||||||
|
answered[i] = answered_total
|
||||||
|
|
||||||
|
rdp_total += q_step1 * pate.rdp_data_independent_gaussian(sigma2, orders)
|
||||||
|
|
||||||
|
eps_cum[i], order_opt = pate.compute_eps_from_delta(orders, rdp_total,
|
||||||
|
delta)
|
||||||
|
|
||||||
|
if i > 0 and (i + 1) % 1000 == 0:
|
||||||
|
print('queries = {}, E[answered] = {:.2f}, E[eps] = {:.3f} '
|
||||||
|
'at order = {:.2f}.'.format(
|
||||||
|
i + 1,
|
||||||
|
answered[i],
|
||||||
|
eps_cum[i],
|
||||||
|
order_opt))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
return eps_cum, answered
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_gnmax_conf_data_dep(votes, threshold, sigma1, sigma2, delta):
|
||||||
|
# Short list of orders.
|
||||||
|
# orders = np.round(np.logspace(np.log10(20), np.log10(200), num=20))
|
||||||
|
|
||||||
|
# Long list of orders.
|
||||||
|
orders = np.concatenate((np.arange(20, 40, .2),
|
||||||
|
np.arange(40, 75, .5),
|
||||||
|
np.logspace(np.log10(75), np.log10(200), num=20)))
|
||||||
|
|
||||||
|
n = votes.shape[0]
|
||||||
|
num_classes = votes.shape[1]
|
||||||
|
num_teachers = int(sum(votes[0,]))
|
||||||
|
|
||||||
|
if threshold is not None and sigma1 is not None:
|
||||||
|
is_data_ind_step1 = pate.is_data_independent_always_opt_gaussian(
|
||||||
|
num_teachers, num_classes, sigma1, orders)
|
||||||
|
else:
|
||||||
|
is_data_ind_step1 = [True] * len(orders)
|
||||||
|
|
||||||
|
is_data_ind_step2 = pate.is_data_independent_always_opt_gaussian(
|
||||||
|
num_teachers, num_classes, sigma2, orders)
|
||||||
|
|
||||||
|
eps_partitioned = np.full(n, None, dtype=Partition)
|
||||||
|
order_opt = np.full(n, None, dtype=float)
|
||||||
|
ss_std_opt = np.full(n, None, dtype=float)
|
||||||
|
answered = np.zeros(n)
|
||||||
|
|
||||||
|
rdp_step1_total = np.zeros(len(orders))
|
||||||
|
rdp_step2_total = np.zeros(len(orders))
|
||||||
|
|
||||||
|
ls_total = np.zeros((len(orders), num_teachers))
|
||||||
|
answered_total = 0
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
v = votes[i,]
|
||||||
|
|
||||||
|
if threshold is not None and sigma1 is not None:
|
||||||
|
logq_step1 = pate.compute_logpr_answered(threshold, sigma1, v)
|
||||||
|
rdp_step1_total += pate.compute_rdp_threshold(logq_step1, sigma1, orders)
|
||||||
|
else:
|
||||||
|
logq_step1 = 0. # always answer
|
||||||
|
|
||||||
|
pr_answered = np.exp(logq_step1)
|
||||||
|
logq_step2 = pate.compute_logq_gaussian(v, sigma2)
|
||||||
|
rdp_step2_total += pr_answered * pate.rdp_gaussian(logq_step2, sigma2,
|
||||||
|
orders)
|
||||||
|
|
||||||
|
answered_total += pr_answered
|
||||||
|
|
||||||
|
rdp_ss = np.zeros(len(orders))
|
||||||
|
ss_std = np.zeros(len(orders))
|
||||||
|
|
||||||
|
for j, order in enumerate(orders):
|
||||||
|
if not is_data_ind_step1[j]:
|
||||||
|
ls_step1 = pate_ss.compute_local_sensitivity_bounds_threshold(v,
|
||||||
|
num_teachers, threshold, sigma1, order)
|
||||||
|
else:
|
||||||
|
ls_step1 = np.full(num_teachers, 0, dtype=float)
|
||||||
|
|
||||||
|
if not is_data_ind_step2[j]:
|
||||||
|
ls_step2 = pate_ss.compute_local_sensitivity_bounds_gnmax(
|
||||||
|
v, num_teachers, sigma2, order)
|
||||||
|
else:
|
||||||
|
ls_step2 = np.full(num_teachers, 0, dtype=float)
|
||||||
|
|
||||||
|
ls_total[j,] += ls_step1 + pr_answered * ls_step2
|
||||||
|
|
||||||
|
beta_ss = .49 / order
|
||||||
|
|
||||||
|
ss = pate_ss.compute_discounted_max(beta_ss, ls_total[j,])
|
||||||
|
sigma_ss = ((order * math.exp(2 * beta_ss)) / ss) ** (1 / 3)
|
||||||
|
rdp_ss[j] = pate_ss.compute_rdp_of_smooth_sensitivity_gaussian(
|
||||||
|
beta_ss, sigma_ss, order)
|
||||||
|
ss_std[j] = ss * sigma_ss
|
||||||
|
|
||||||
|
rdp_total = rdp_step1_total + rdp_step2_total + rdp_ss
|
||||||
|
|
||||||
|
answered[i] = answered_total
|
||||||
|
_, order_opt[i] = pate.compute_eps_from_delta(orders, rdp_total, delta)
|
||||||
|
order_idx = np.searchsorted(orders, order_opt[i])
|
||||||
|
|
||||||
|
# Since optimal orders are always non-increasing, shrink orders array
|
||||||
|
# and all cumulative arrays to speed up computation.
|
||||||
|
if order_idx < len(orders):
|
||||||
|
orders = orders[:order_idx + 1]
|
||||||
|
rdp_step1_total = rdp_step1_total[:order_idx + 1]
|
||||||
|
rdp_step2_total = rdp_step2_total[:order_idx + 1]
|
||||||
|
|
||||||
|
eps_partitioned[i] = Partition(step1=rdp_step1_total[order_idx],
|
||||||
|
step2=rdp_step2_total[order_idx],
|
||||||
|
ss=rdp_ss[order_idx],
|
||||||
|
delta=-math.log(delta) / (order_opt[i] - 1))
|
||||||
|
ss_std_opt[i] = ss_std[order_idx]
|
||||||
|
if i > 0 and (i + 1) % 1 == 0:
|
||||||
|
print('queries = {}, E[answered] = {:.2f}, E[eps] = {:.3f} +/- {:.3f} '
|
||||||
|
'at order = {:.2f}. Contributions: delta = {:.3f}, step1 = {:.3f}, '
|
||||||
|
'step2 = {:.3f}, ss = {:.3f}'.format(
|
||||||
|
i + 1,
|
||||||
|
answered[i],
|
||||||
|
sum(eps_partitioned[i]),
|
||||||
|
ss_std_opt[i],
|
||||||
|
order_opt[i],
|
||||||
|
eps_partitioned[i].delta,
|
||||||
|
eps_partitioned[i].step1,
|
||||||
|
eps_partitioned[i].step2,
|
||||||
|
eps_partitioned[i].ss))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
return eps_partitioned, answered, ss_std_opt, order_opt
|
||||||
|
|
||||||
|
|
||||||
|
def plot_comparison(figures_dir, simple_ind, conf_ind, simple_dep, conf_dep):
|
||||||
|
"""Plots variants of GNMax algorithm and their analyses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def pivot(x_axis, eps, answered):
|
||||||
|
y = np.full(len(x_axis), None, dtype=float) # delta
|
||||||
|
for i, x in enumerate(x_axis):
|
||||||
|
idx = np.searchsorted(answered, x)
|
||||||
|
if idx < len(eps):
|
||||||
|
y[i] = eps[idx]
|
||||||
|
return y
|
||||||
|
|
||||||
|
def pivot_dep(x_axis, data_dep):
|
||||||
|
eps_partitioned, answered, _, _ = data_dep
|
||||||
|
eps = [sum(p) for p in eps_partitioned] # Flatten eps
|
||||||
|
return pivot(x_axis, eps, answered)
|
||||||
|
|
||||||
|
xlim = 10000
|
||||||
|
x_axis = range(0, xlim, 10)
|
||||||
|
|
||||||
|
y_simple_ind = pivot(x_axis, *simple_ind)
|
||||||
|
y_conf_ind = pivot(x_axis, *conf_ind)
|
||||||
|
|
||||||
|
y_simple_dep = pivot_dep(x_axis, simple_dep)
|
||||||
|
y_conf_dep = pivot_dep(x_axis, conf_dep)
|
||||||
|
|
||||||
|
# plt.close('all')
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.5)
|
||||||
|
fig.set_figwidth(4.7)
|
||||||
|
|
||||||
|
ax.plot(x_axis, y_simple_ind, ls='--', color='r', lw=3, label=r'Simple GNMax, data-ind analysis')
|
||||||
|
ax.plot(x_axis, y_conf_ind, ls='--', color='b', lw=3, label=r'Confident GNMax, data-ind analysis')
|
||||||
|
ax.plot(x_axis, y_simple_dep, ls='-', color='r', lw=3, label=r'Simple GNMax, data-dep analysis')
|
||||||
|
ax.plot(x_axis, y_conf_dep, ls='-', color='b', lw=3, label=r'Confident GNMax, data-dep analysis')
|
||||||
|
|
||||||
|
plt.xticks(np.arange(0, xlim + 1000, 2000))
|
||||||
|
plt.xlim([0, xlim])
|
||||||
|
plt.ylim(bottom=0)
|
||||||
|
plt.legend(fontsize=16)
|
||||||
|
ax.set_xlabel('Number of queries answered', fontsize=16)
|
||||||
|
ax.set_ylabel(r'Privacy cost $\varepsilon$ at $\delta=10^{-8}$', fontsize=16)
|
||||||
|
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
plot_filename = os.path.join(figures_dir, 'comparison.pdf')
|
||||||
|
print('Saving the graph to ' + plot_filename)
|
||||||
|
fig.savefig(plot_filename, bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_partition(figures_dir, gnmax_conf, print_order):
|
||||||
|
"""Plots an expert version of the privacy-per-answered-query graph.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
figures_dir: A name of the directory where to save the plot.
|
||||||
|
eps: The cumulative privacy cost.
|
||||||
|
partition: Allocation of the privacy cost.
|
||||||
|
answered: Cumulative number of queries answered.
|
||||||
|
order_opt: The list of optimal orders.
|
||||||
|
"""
|
||||||
|
eps_partitioned, answered, ss_std_opt, order_opt = gnmax_conf
|
||||||
|
|
||||||
|
xlim = 10000
|
||||||
|
x = range(0, int(xlim), 10)
|
||||||
|
lenx = len(x)
|
||||||
|
y0 = np.full(lenx, np.nan, dtype=float) # delta
|
||||||
|
y1 = np.full(lenx, np.nan, dtype=float) # delta + step1
|
||||||
|
y2 = np.full(lenx, np.nan, dtype=float) # delta + step1 + step2
|
||||||
|
y3 = np.full(lenx, np.nan, dtype=float) # delta + step1 + step2 + ss
|
||||||
|
noise_std = np.full(lenx, np.nan, dtype=float)
|
||||||
|
|
||||||
|
y_right = np.full(lenx, np.nan, dtype=float)
|
||||||
|
|
||||||
|
for i in range(lenx):
|
||||||
|
idx = np.searchsorted(answered, x[i])
|
||||||
|
if idx < len(eps_partitioned):
|
||||||
|
y0[i] = eps_partitioned[idx].delta
|
||||||
|
y1[i] = y0[i] + eps_partitioned[idx].step1
|
||||||
|
y2[i] = y1[i] + eps_partitioned[idx].step2
|
||||||
|
y3[i] = y2[i] + eps_partitioned[idx].ss
|
||||||
|
|
||||||
|
noise_std[i] = ss_std_opt[idx]
|
||||||
|
y_right[i] = order_opt[idx]
|
||||||
|
|
||||||
|
# plt.close('all')
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.5)
|
||||||
|
fig.set_figwidth(4.7)
|
||||||
|
fig.patch.set_alpha(0)
|
||||||
|
|
||||||
|
l1 = ax.plot(
|
||||||
|
x, y3, color='b', ls='-', label=r'Total privacy cost', linewidth=1).pop()
|
||||||
|
|
||||||
|
for y in (y0, y1, y2):
|
||||||
|
ax.plot(x, y, color='b', ls='-', label=r'_nolegend_', alpha=.5, linewidth=1)
|
||||||
|
|
||||||
|
ax.fill_between(x, [0] * lenx, y0.tolist(), facecolor='b', alpha=.5)
|
||||||
|
ax.fill_between(x, y0.tolist(), y1.tolist(), facecolor='b', alpha=.4)
|
||||||
|
ax.fill_between(x, y1.tolist(), y2.tolist(), facecolor='b', alpha=.3)
|
||||||
|
ax.fill_between(x, y2.tolist(), y3.tolist(), facecolor='b', alpha=.2)
|
||||||
|
|
||||||
|
ax.fill_between(x, (y3 - noise_std).tolist(), (y3 + noise_std).tolist(),
|
||||||
|
facecolor='r', alpha=.5)
|
||||||
|
|
||||||
|
|
||||||
|
plt.xticks(np.arange(0, xlim + 1000, 2000))
|
||||||
|
plt.xlim([0, xlim])
|
||||||
|
ax.set_ylim([0, 3.])
|
||||||
|
|
||||||
|
ax.set_xlabel('Number of queries answered', fontsize=16)
|
||||||
|
ax.set_ylabel(r'Privacy cost $\varepsilon$ at $\delta=10^{-8}$', fontsize=16)
|
||||||
|
|
||||||
|
# Merging legends.
|
||||||
|
if print_order:
|
||||||
|
ax2 = ax.twinx()
|
||||||
|
l2 = ax2.plot(
|
||||||
|
x, y_right, 'r', ls='-', label=r'Optimal order', linewidth=5,
|
||||||
|
alpha=.5).pop()
|
||||||
|
ax2.grid(False)
|
||||||
|
# ax2.set_ylabel(r'Optimal Renyi order', fontsize=16)
|
||||||
|
ax2.set_ylim([0, 200.])
|
||||||
|
# ax.legend((l1, l2), (l1.get_label(), l2.get_label()), loc=0, fontsize=13)
|
||||||
|
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
plot_filename = os.path.join(figures_dir, 'partition.pdf')
|
||||||
|
print('Saving the graph to ' + plot_filename)
|
||||||
|
fig.savefig(plot_filename, bbox_inches='tight', dpi=800)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_analyses(votes, threshold, sigma1, sigma2, delta):
|
||||||
|
simple_ind = analyze_gnmax_conf_data_ind(votes, None, None, sigma2,
|
||||||
|
delta)
|
||||||
|
|
||||||
|
conf_ind = analyze_gnmax_conf_data_ind(votes, threshold, sigma1, sigma2,
|
||||||
|
delta)
|
||||||
|
|
||||||
|
simple_dep = analyze_gnmax_conf_data_dep(votes, None, None, sigma2,
|
||||||
|
delta)
|
||||||
|
|
||||||
|
conf_dep = analyze_gnmax_conf_data_dep(votes, threshold, sigma1, sigma2,
|
||||||
|
delta)
|
||||||
|
|
||||||
|
return (simple_ind, conf_ind, simple_dep, conf_dep)
|
||||||
|
|
||||||
|
|
||||||
|
def run_or_load_all_analyses():
|
||||||
|
temp_filename = os.path.expanduser('~/tmp/partition_cached.pkl')
|
||||||
|
|
||||||
|
if FLAGS.cache and os.path.isfile(temp_filename):
|
||||||
|
print('Reading from cache ' + temp_filename)
|
||||||
|
with open(temp_filename, 'rb') as f:
|
||||||
|
all_analyses = pickle.load(f)
|
||||||
|
else:
|
||||||
|
fin_name = os.path.expanduser(FLAGS.counts_file)
|
||||||
|
print('Reading raw votes from ' + fin_name)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
votes = np.load(fin_name)
|
||||||
|
|
||||||
|
if FLAGS.queries is not None:
|
||||||
|
if votes.shape[0] < FLAGS.queries:
|
||||||
|
raise ValueError('Expect {} rows, got {} in {}'.format(
|
||||||
|
FLAGS.queries, votes.shape[0], fin_name))
|
||||||
|
# Truncate the votes matrix to the number of queries made.
|
||||||
|
votes = votes[:FLAGS.queries, ]
|
||||||
|
|
||||||
|
all_analyses = run_all_analyses(votes, FLAGS.threshold, FLAGS.sigma1,
|
||||||
|
FLAGS.sigma2, FLAGS.delta)
|
||||||
|
|
||||||
|
print('Writing to cache ' + temp_filename)
|
||||||
|
with open(temp_filename, 'wb') as f:
|
||||||
|
pickle.dump(all_analyses, f)
|
||||||
|
|
||||||
|
return all_analyses
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
|
||||||
|
simple_ind, conf_ind, simple_dep, conf_dep = run_or_load_all_analyses()
|
||||||
|
|
||||||
|
figures_dir = os.path.expanduser(FLAGS.figures_dir)
|
||||||
|
|
||||||
|
plot_comparison(figures_dir, simple_ind, conf_ind, simple_dep, conf_dep)
|
||||||
|
plot_partition(figures_dir, conf_dep, True)
|
||||||
|
plt.close('all')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
283
research/pate_2018/ICLR2018/plots_for_slides.py
Normal file
283
research/pate_2018/ICLR2018/plots_for_slides.py
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Plots graphs for the slide deck.
|
||||||
|
|
||||||
|
A script in support of the PATE2 paper. The input is a file containing a numpy
|
||||||
|
array of votes, one query per row, one class per column. Ex:
|
||||||
|
43, 1821, ..., 3
|
||||||
|
31, 16, ..., 0
|
||||||
|
...
|
||||||
|
0, 86, ..., 438
|
||||||
|
The output graphs are visualized using the TkAgg backend.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('..') # Main modules reside in the parent directory.
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
import matplotlib
|
||||||
|
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
import matplotlib.pyplot as plt # pylint: disable=g-import-not-at-top
|
||||||
|
import numpy as np
|
||||||
|
import core as pate
|
||||||
|
import random
|
||||||
|
|
||||||
|
plt.style.use('ggplot')
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_string('counts_file', None, 'Counts file.')
|
||||||
|
flags.DEFINE_string('figures_dir', '', 'Path where figures are written to.')
|
||||||
|
flags.DEFINE_boolean('transparent', False, 'Set background to transparent.')
|
||||||
|
|
||||||
|
flags.mark_flag_as_required('counts_file')
|
||||||
|
|
||||||
|
|
||||||
|
def setup_plot():
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.5)
|
||||||
|
fig.set_figwidth(4.7)
|
||||||
|
|
||||||
|
if FLAGS.transparent:
|
||||||
|
fig.patch.set_alpha(0)
|
||||||
|
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
|
||||||
|
def plot_rdp_curve_per_example(votes, sigmas):
|
||||||
|
orders = np.linspace(1., 100., endpoint=True, num=1000)
|
||||||
|
orders[0] = 1.001
|
||||||
|
fig, ax = setup_plot()
|
||||||
|
|
||||||
|
for i in range(votes.shape[0]):
|
||||||
|
for sigma in sigmas:
|
||||||
|
logq = pate.compute_logq_gaussian(votes[i,], sigma)
|
||||||
|
rdp = pate.rdp_gaussian(logq, sigma, orders)
|
||||||
|
ax.plot(
|
||||||
|
orders,
|
||||||
|
rdp,
|
||||||
|
alpha=1.,
|
||||||
|
label=r'Data-dependent bound, $\sigma$={}'.format(int(sigma)),
|
||||||
|
linewidth=5)
|
||||||
|
|
||||||
|
for sigma in sigmas:
|
||||||
|
ax.plot(
|
||||||
|
orders,
|
||||||
|
pate.rdp_data_independent_gaussian(sigma, orders),
|
||||||
|
alpha=.3,
|
||||||
|
label=r'Data-independent bound, $\sigma$={}'.format(int(sigma)),
|
||||||
|
linewidth=10)
|
||||||
|
|
||||||
|
plt.xlim(xmin=1, xmax=100)
|
||||||
|
plt.ylim(ymin=0)
|
||||||
|
plt.xticks([1, 20, 40, 60, 80, 100])
|
||||||
|
plt.yticks([0, .0025, .005, .0075, .01])
|
||||||
|
plt.xlabel(r'Order $\alpha$', fontsize=16)
|
||||||
|
plt.ylabel(r'RDP value $\varepsilon$ at $\alpha$', fontsize=16)
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
|
||||||
|
plt.legend(loc=0, fontsize=13)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_rdp_of_sigma(v, order):
|
||||||
|
sigmas = np.linspace(1., 1000., endpoint=True, num=1000)
|
||||||
|
fig, ax = setup_plot()
|
||||||
|
|
||||||
|
y = np.zeros(len(sigmas))
|
||||||
|
|
||||||
|
for i, sigma in enumerate(sigmas):
|
||||||
|
logq = pate.compute_logq_gaussian(v, sigma)
|
||||||
|
y[i] = pate.rdp_gaussian(logq, sigma, order)
|
||||||
|
|
||||||
|
ax.plot(sigmas, y, alpha=.8, linewidth=5)
|
||||||
|
|
||||||
|
plt.xlim(xmin=1, xmax=1000)
|
||||||
|
plt.ylim(ymin=0)
|
||||||
|
# plt.yticks([0, .0004, .0008, .0012])
|
||||||
|
ax.tick_params(labelleft='off')
|
||||||
|
plt.xlabel(r'Noise $\sigma$', fontsize=16)
|
||||||
|
plt.ylabel(r'RDP at order $\alpha={}$'.format(order), fontsize=16)
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
|
||||||
|
# plt.legend(loc=0, fontsize=13)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def compute_rdp_curve(votes, threshold, sigma1, sigma2, orders,
|
||||||
|
target_answered):
|
||||||
|
rdp_cum = np.zeros(len(orders))
|
||||||
|
answered = 0
|
||||||
|
for i, v in enumerate(votes):
|
||||||
|
v = sorted(v, reverse=True)
|
||||||
|
q_step1 = math.exp(pate.compute_logpr_answered(threshold, sigma1, v))
|
||||||
|
logq_step2 = pate.compute_logq_gaussian(v, sigma2)
|
||||||
|
rdp = pate.rdp_gaussian(logq_step2, sigma2, orders)
|
||||||
|
rdp_cum += q_step1 * rdp
|
||||||
|
|
||||||
|
answered += q_step1
|
||||||
|
if answered >= target_answered:
|
||||||
|
print('Processed {} queries to answer {}.'.format(i, target_answered))
|
||||||
|
return rdp_cum
|
||||||
|
|
||||||
|
assert False, 'Never reached {} answered queries.'.format(target_answered)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_rdp_total(votes, sigmas):
|
||||||
|
orders = np.linspace(1., 100., endpoint=True, num=100)
|
||||||
|
orders[0] = 1.1
|
||||||
|
|
||||||
|
fig, ax = setup_plot()
|
||||||
|
|
||||||
|
target_answered = 2000
|
||||||
|
|
||||||
|
for sigma in sigmas:
|
||||||
|
rdp = compute_rdp_curve(votes, 5000, 1000, sigma, orders, target_answered)
|
||||||
|
ax.plot(
|
||||||
|
orders,
|
||||||
|
rdp,
|
||||||
|
alpha=.8,
|
||||||
|
label=r'Data-dependent bound, $\sigma$={}'.format(int(sigma)),
|
||||||
|
linewidth=5)
|
||||||
|
|
||||||
|
# for sigma in sigmas:
|
||||||
|
# ax.plot(
|
||||||
|
# orders,
|
||||||
|
# target_answered * pate.rdp_data_independent_gaussian(sigma, orders),
|
||||||
|
# alpha=.3,
|
||||||
|
# label=r'Data-independent bound, $\sigma$={}'.format(int(sigma)),
|
||||||
|
# linewidth=10)
|
||||||
|
|
||||||
|
plt.xlim(xmin=1, xmax=100)
|
||||||
|
plt.ylim(ymin=0)
|
||||||
|
plt.xticks([1, 20, 40, 60, 80, 100])
|
||||||
|
plt.yticks([0, .0005, .001, .0015, .002])
|
||||||
|
|
||||||
|
plt.xlabel(r'Order $\alpha$', fontsize=16)
|
||||||
|
plt.ylabel(r'RDP value $\varepsilon$ at $\alpha$', fontsize=16)
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
|
||||||
|
plt.legend(loc=0, fontsize=13)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_data_ind_curve():
|
||||||
|
fig, ax = setup_plot()
|
||||||
|
|
||||||
|
orders = np.linspace(1., 10., endpoint=True, num=1000)
|
||||||
|
orders[0] = 1.01
|
||||||
|
|
||||||
|
ax.plot(
|
||||||
|
orders,
|
||||||
|
pate.rdp_data_independent_gaussian(1., orders),
|
||||||
|
alpha=.5,
|
||||||
|
color='gray',
|
||||||
|
linewidth=10)
|
||||||
|
|
||||||
|
# plt.yticks([])
|
||||||
|
plt.xlim(xmin=1, xmax=10)
|
||||||
|
plt.ylim(ymin=0)
|
||||||
|
plt.xticks([1, 3, 5, 7, 9])
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_two_data_ind_curves():
|
||||||
|
orders = np.linspace(1., 100., endpoint=True, num=1000)
|
||||||
|
orders[0] = 1.001
|
||||||
|
|
||||||
|
fig, ax = setup_plot()
|
||||||
|
|
||||||
|
for sigma in [100, 150]:
|
||||||
|
ax.plot(
|
||||||
|
orders,
|
||||||
|
pate.rdp_data_independent_gaussian(sigma, orders),
|
||||||
|
alpha=.3,
|
||||||
|
label=r'Data-independent bound, $\sigma$={}'.format(int(sigma)),
|
||||||
|
linewidth=10)
|
||||||
|
|
||||||
|
plt.xlim(xmin=1, xmax=100)
|
||||||
|
plt.ylim(ymin=0)
|
||||||
|
plt.xticks([1, 20, 40, 60, 80, 100])
|
||||||
|
plt.yticks([0, .0025, .005, .0075, .01])
|
||||||
|
plt.xlabel(r'Order $\alpha$', fontsize=16)
|
||||||
|
plt.ylabel(r'RDP value $\varepsilon$ at $\alpha$', fontsize=16)
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
|
||||||
|
plt.legend(loc=0, fontsize=13)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def scatter_plot(votes, threshold, sigma1, sigma2, order):
|
||||||
|
fig, ax = setup_plot()
|
||||||
|
x = []
|
||||||
|
y = []
|
||||||
|
for i, v in enumerate(votes):
|
||||||
|
if threshold is not None and sigma1 is not None:
|
||||||
|
q_step1 = math.exp(pate.compute_logpr_answered(threshold, sigma1, v))
|
||||||
|
else:
|
||||||
|
q_step1 = 1.
|
||||||
|
if random.random() < q_step1:
|
||||||
|
logq_step2 = pate.compute_logq_gaussian(v, sigma2)
|
||||||
|
x.append(max(v))
|
||||||
|
y.append(pate.rdp_gaussian(logq_step2, sigma2, order))
|
||||||
|
|
||||||
|
print('Selected {} queries.'.format(len(x)))
|
||||||
|
# Plot the data-independent curve:
|
||||||
|
# data_ind = pate.rdp_data_independent_gaussian(sigma, order)
|
||||||
|
# plt.plot([0, 5000], [data_ind, data_ind], color='tab:blue', linestyle='-', linewidth=2)
|
||||||
|
ax.set_yscale('log')
|
||||||
|
plt.xlim(xmin=0, xmax=5000)
|
||||||
|
plt.ylim(ymin=1e-300, ymax=1)
|
||||||
|
plt.yticks([1, 1e-100, 1e-200, 1e-300])
|
||||||
|
plt.scatter(x, y, s=1, alpha=0.5)
|
||||||
|
plt.ylabel(r'RDP at $\alpha={}$'.format(order), fontsize=16)
|
||||||
|
plt.xlabel(r'max count', fontsize=16)
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
fin_name = os.path.expanduser(FLAGS.counts_file)
|
||||||
|
print('Reading raw votes from ' + fin_name)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
plot_data_ind_curve()
|
||||||
|
plot_two_data_ind_curves()
|
||||||
|
|
||||||
|
v1 = [2550, 2200, 250] # based on votes[2,]
|
||||||
|
# v2 = [2600, 2200, 200] # based on votes[381,]
|
||||||
|
plot_rdp_curve_per_example(np.array([v1]), (100., 150.))
|
||||||
|
|
||||||
|
plot_rdp_of_sigma(np.array(v1), 20.)
|
||||||
|
|
||||||
|
votes = np.load(fin_name)
|
||||||
|
|
||||||
|
plot_rdp_total(votes[:12000, ], (100., 150.))
|
||||||
|
scatter_plot(votes[:6000, ], None, None, 100, 20) # w/o thresholding
|
||||||
|
scatter_plot(votes[:6000, ], 3500, 1500, 100, 20) # with thresholding
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
263
research/pate_2018/ICLR2018/rdp_bucketized.py
Normal file
263
research/pate_2018/ICLR2018/rdp_bucketized.py
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Illustrates how noisy thresholding check changes distribution of queries.
|
||||||
|
|
||||||
|
A script in support of the paper "Scalable Private Learning with PATE" by
|
||||||
|
Nicolas Papernot, Shuang Song, Ilya Mironov, Ananth Raghunathan, Kunal Talwar,
|
||||||
|
Ulfar Erlingsson (https://arxiv.org/abs/1802.08908).
|
||||||
|
|
||||||
|
The input is a file containing a numpy array of votes, one query per row, one
|
||||||
|
class per column. Ex:
|
||||||
|
43, 1821, ..., 3
|
||||||
|
31, 16, ..., 0
|
||||||
|
...
|
||||||
|
0, 86, ..., 438
|
||||||
|
The output is one of two graphs depending on the setting of the plot variable.
|
||||||
|
The output is written to a pdf file.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('..') # Main modules reside in the parent directory.
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
import matplotlib.pyplot as plt # pylint: disable=g-import-not-at-top
|
||||||
|
import numpy as np
|
||||||
|
import core as pate
|
||||||
|
|
||||||
|
plt.style.use('ggplot')
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_enum('plot', 'small', ['small', 'large'], 'Selects which of'
|
||||||
|
'the two plots is produced.')
|
||||||
|
flags.DEFINE_string('counts_file', None, 'Counts file.')
|
||||||
|
flags.DEFINE_string('plot_file', '', 'Plot file to write.')
|
||||||
|
|
||||||
|
flags.mark_flag_as_required('counts_file')
|
||||||
|
|
||||||
|
|
||||||
|
def compute_count_per_bin(bin_num, votes):
|
||||||
|
"""Tabulates number of examples in each bin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bin_num: Number of bins.
|
||||||
|
votes: A matrix of votes, where each row contains votes in one instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Array of counts of length bin_num.
|
||||||
|
"""
|
||||||
|
sums = np.sum(votes, axis=1)
|
||||||
|
# Check that all rows contain the same number of votes.
|
||||||
|
assert max(sums) == min(sums)
|
||||||
|
|
||||||
|
s = max(sums)
|
||||||
|
|
||||||
|
counts = np.zeros(bin_num)
|
||||||
|
n = votes.shape[0]
|
||||||
|
|
||||||
|
for i in xrange(n):
|
||||||
|
v = votes[i,]
|
||||||
|
bin_idx = int(math.floor(max(v) * bin_num / s))
|
||||||
|
assert 0 <= bin_idx < bin_num
|
||||||
|
counts[bin_idx] += 1
|
||||||
|
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def compute_privacy_cost_per_bins(bin_num, votes, sigma2, order):
|
||||||
|
"""Outputs average privacy cost per bin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bin_num: Number of bins.
|
||||||
|
votes: A matrix of votes, where each row contains votes in one instance.
|
||||||
|
sigma2: The scale (std) of the Gaussian noise. (Same as sigma_2 in
|
||||||
|
Algorithms 1 and 2.)
|
||||||
|
order: The Renyi order for which privacy cost is computed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expected eps of RDP (ignoring delta) per example in each bin.
|
||||||
|
"""
|
||||||
|
n = votes.shape[0]
|
||||||
|
|
||||||
|
bin_counts = np.zeros(bin_num)
|
||||||
|
bin_rdp = np.zeros(bin_num) # RDP at order=order
|
||||||
|
|
||||||
|
for i in xrange(n):
|
||||||
|
v = votes[i,]
|
||||||
|
logq = pate.compute_logq_gaussian(v, sigma2)
|
||||||
|
rdp_at_order = pate.rdp_gaussian(logq, sigma2, order)
|
||||||
|
|
||||||
|
bin_idx = int(math.floor(max(v) * bin_num / sum(v)))
|
||||||
|
assert 0 <= bin_idx < bin_num
|
||||||
|
bin_counts[bin_idx] += 1
|
||||||
|
bin_rdp[bin_idx] += rdp_at_order
|
||||||
|
if (i + 1) % 1000 == 0:
|
||||||
|
print('example {}'.format(i + 1))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
return bin_rdp / bin_counts
|
||||||
|
|
||||||
|
|
||||||
|
def compute_expected_answered_per_bin(bin_num, votes, threshold, sigma1):
|
||||||
|
"""Computes expected number of answers per bin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bin_num: Number of bins.
|
||||||
|
votes: A matrix of votes, where each row contains votes in one instance.
|
||||||
|
threshold: The threshold against which check is performed.
|
||||||
|
sigma1: The std of the Gaussian noise with which check is performed. (Same
|
||||||
|
as sigma_1 in Algorithms 1 and 2.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expected number of queries answered per bin.
|
||||||
|
"""
|
||||||
|
n = votes.shape[0]
|
||||||
|
|
||||||
|
bin_answered = np.zeros(bin_num)
|
||||||
|
|
||||||
|
for i in xrange(n):
|
||||||
|
v = votes[i,]
|
||||||
|
p = math.exp(pate.compute_logpr_answered(threshold, sigma1, v))
|
||||||
|
bin_idx = int(math.floor(max(v) * bin_num / sum(v)))
|
||||||
|
assert 0 <= bin_idx < bin_num
|
||||||
|
bin_answered[bin_idx] += p
|
||||||
|
if (i + 1) % 1000 == 0:
|
||||||
|
print('example {}'.format(i + 1))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
return bin_answered
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
fin_name = os.path.expanduser(FLAGS.counts_file)
|
||||||
|
print('Reading raw votes from ' + fin_name)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
votes = np.load(fin_name)
|
||||||
|
votes = votes[:4000,] # truncate to 4000 samples
|
||||||
|
|
||||||
|
if FLAGS.plot == 'small':
|
||||||
|
bin_num = 5
|
||||||
|
m_check = compute_expected_answered_per_bin(bin_num, votes, 3500, 1500)
|
||||||
|
elif FLAGS.plot == 'large':
|
||||||
|
bin_num = 10
|
||||||
|
m_check = compute_expected_answered_per_bin(bin_num, votes, 3500, 1500)
|
||||||
|
a_check = compute_expected_answered_per_bin(bin_num, votes, 5000, 1500)
|
||||||
|
eps = compute_privacy_cost_per_bins(bin_num, votes, 100, 50)
|
||||||
|
else:
|
||||||
|
raise ValueError('--plot flag must be one of ["small", "large"]')
|
||||||
|
|
||||||
|
counts = compute_count_per_bin(bin_num, votes)
|
||||||
|
bins = np.linspace(0, 100, num=bin_num, endpoint=False)
|
||||||
|
|
||||||
|
plt.close('all')
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
if FLAGS.plot == 'small':
|
||||||
|
fig.set_figheight(5)
|
||||||
|
fig.set_figwidth(5)
|
||||||
|
ax.bar(
|
||||||
|
bins,
|
||||||
|
counts,
|
||||||
|
20,
|
||||||
|
color='orangered',
|
||||||
|
linestyle='dotted',
|
||||||
|
linewidth=5,
|
||||||
|
edgecolor='red',
|
||||||
|
fill=False,
|
||||||
|
alpha=.5,
|
||||||
|
align='edge',
|
||||||
|
label='LNMax answers')
|
||||||
|
ax.bar(
|
||||||
|
bins,
|
||||||
|
m_check,
|
||||||
|
20,
|
||||||
|
color='g',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=0,
|
||||||
|
edgecolor='g',
|
||||||
|
align='edge',
|
||||||
|
label='Confident-GNMax\nanswers')
|
||||||
|
elif FLAGS.plot == 'large':
|
||||||
|
fig.set_figheight(4.7)
|
||||||
|
fig.set_figwidth(7)
|
||||||
|
ax.bar(
|
||||||
|
bins,
|
||||||
|
counts,
|
||||||
|
10,
|
||||||
|
linestyle='dashed',
|
||||||
|
linewidth=5,
|
||||||
|
edgecolor='red',
|
||||||
|
fill=False,
|
||||||
|
alpha=.5,
|
||||||
|
align='edge',
|
||||||
|
label='LNMax answers')
|
||||||
|
ax.bar(
|
||||||
|
bins,
|
||||||
|
m_check,
|
||||||
|
10,
|
||||||
|
color='g',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=0,
|
||||||
|
edgecolor='g',
|
||||||
|
align='edge',
|
||||||
|
label='Confident-GNMax\nanswers (moderate)')
|
||||||
|
ax.bar(
|
||||||
|
bins,
|
||||||
|
a_check,
|
||||||
|
10,
|
||||||
|
color='b',
|
||||||
|
alpha=.5,
|
||||||
|
align='edge',
|
||||||
|
label='Confident-GNMax\nanswers (aggressive)')
|
||||||
|
ax2 = ax.twinx()
|
||||||
|
bin_centers = [x + 5 for x in bins]
|
||||||
|
ax2.plot(bin_centers, eps, 'ko', alpha=.8)
|
||||||
|
ax2.set_ylim([1e-200, 1.])
|
||||||
|
ax2.set_yscale('log')
|
||||||
|
ax2.grid(False)
|
||||||
|
ax2.set_yticks([1e-3, 1e-50, 1e-100, 1e-150, 1e-200])
|
||||||
|
plt.tick_params(which='minor', right='off')
|
||||||
|
ax2.set_ylabel(r'Per query privacy cost $\varepsilon$', fontsize=16)
|
||||||
|
|
||||||
|
plt.xlim([0, 100])
|
||||||
|
ax.set_ylim([0, 2500])
|
||||||
|
# ax.set_yscale('log')
|
||||||
|
ax.set_xlabel('Percentage of teachers that agree', fontsize=16)
|
||||||
|
ax.set_ylabel('Number of queries answered', fontsize=16)
|
||||||
|
vals = ax.get_xticks()
|
||||||
|
ax.set_xticklabels([str(int(x)) + '%' for x in vals])
|
||||||
|
ax.tick_params(labelsize=14, bottom=True, top=True, left=True, right=True)
|
||||||
|
ax.legend(loc=2, prop={'size': 16})
|
||||||
|
|
||||||
|
# simple: 'figures/noisy_thresholding_check_perf.pdf')
|
||||||
|
# detailed: 'figures/noisy_thresholding_check_perf_details.pdf'
|
||||||
|
|
||||||
|
print('Saving the graph to ' + FLAGS.plot_file)
|
||||||
|
plt.savefig(os.path.expanduser(FLAGS.plot_file), bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
378
research/pate_2018/ICLR2018/rdp_cumulative.py
Normal file
378
research/pate_2018/ICLR2018/rdp_cumulative.py
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Plots three graphs illustrating cost of privacy per answered query.
|
||||||
|
|
||||||
|
A script in support of the paper "Scalable Private Learning with PATE" by
|
||||||
|
Nicolas Papernot, Shuang Song, Ilya Mironov, Ananth Raghunathan, Kunal Talwar,
|
||||||
|
Ulfar Erlingsson (https://arxiv.org/abs/1802.08908).
|
||||||
|
|
||||||
|
The input is a file containing a numpy array of votes, one query per row, one
|
||||||
|
class per column. Ex:
|
||||||
|
43, 1821, ..., 3
|
||||||
|
31, 16, ..., 0
|
||||||
|
...
|
||||||
|
0, 86, ..., 438
|
||||||
|
The output is written to a specified directory and consists of three pdf files.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('..') # Main modules reside in the parent directory.
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
import matplotlib
|
||||||
|
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
import matplotlib.pyplot as plt # pylint: disable=g-import-not-at-top
|
||||||
|
import numpy as np
|
||||||
|
import core as pate
|
||||||
|
|
||||||
|
plt.style.use('ggplot')
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_boolean('cache', False,
|
||||||
|
'Read results of privacy analysis from cache.')
|
||||||
|
flags.DEFINE_string('counts_file', None, 'Counts file.')
|
||||||
|
flags.DEFINE_string('figures_dir', '', 'Path where figures are written to.')
|
||||||
|
|
||||||
|
flags.mark_flag_as_required('counts_file')
|
||||||
|
|
||||||
|
def run_analysis(votes, mechanism, noise_scale, params):
|
||||||
|
"""Computes data-dependent privacy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
votes: A matrix of votes, where each row contains votes in one instance.
|
||||||
|
mechanism: A name of the mechanism ('lnmax', 'gnmax', or 'gnmax_conf')
|
||||||
|
noise_scale: A mechanism privacy parameter.
|
||||||
|
params: Other privacy parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Four lists: cumulative privacy cost epsilon, how privacy budget is split,
|
||||||
|
how many queries were answered, optimal order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compute_partition(order_opt, eps):
|
||||||
|
order_opt_idx = np.searchsorted(orders, order_opt)
|
||||||
|
if mechanism == 'gnmax_conf':
|
||||||
|
p = (rdp_select_cum[order_opt_idx],
|
||||||
|
rdp_cum[order_opt_idx] - rdp_select_cum[order_opt_idx],
|
||||||
|
-math.log(delta) / (order_opt - 1))
|
||||||
|
else:
|
||||||
|
p = (rdp_cum[order_opt_idx], -math.log(delta) / (order_opt - 1))
|
||||||
|
return [x / eps for x in p] # Ensures that sum(x) == 1
|
||||||
|
|
||||||
|
# Short list of orders.
|
||||||
|
# orders = np.round(np.concatenate((np.arange(2, 50 + 1, 1),
|
||||||
|
# np.logspace(np.log10(50), np.log10(1000), num=20))))
|
||||||
|
|
||||||
|
# Long list of orders.
|
||||||
|
orders = np.concatenate((np.arange(2, 100 + 1, .5),
|
||||||
|
np.logspace(np.log10(100), np.log10(500), num=100)))
|
||||||
|
delta = 1e-8
|
||||||
|
|
||||||
|
n = votes.shape[0]
|
||||||
|
eps_total = np.zeros(n)
|
||||||
|
partition = [None] * n
|
||||||
|
order_opt = np.full(n, np.nan, dtype=float)
|
||||||
|
answered = np.zeros(n, dtype=float)
|
||||||
|
|
||||||
|
rdp_cum = np.zeros(len(orders))
|
||||||
|
rdp_sqrd_cum = np.zeros(len(orders))
|
||||||
|
rdp_select_cum = np.zeros(len(orders))
|
||||||
|
answered_sum = 0
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
v = votes[i,]
|
||||||
|
if mechanism == 'lnmax':
|
||||||
|
logq_lnmax = pate.compute_logq_laplace(v, noise_scale)
|
||||||
|
rdp_query = pate.rdp_pure_eps(logq_lnmax, 2. / noise_scale, orders)
|
||||||
|
rdp_sqrd = rdp_query ** 2
|
||||||
|
pr_answered = 1
|
||||||
|
elif mechanism == 'gnmax':
|
||||||
|
logq_gmax = pate.compute_logq_gaussian(v, noise_scale)
|
||||||
|
rdp_query = pate.rdp_gaussian(logq_gmax, noise_scale, orders)
|
||||||
|
rdp_sqrd = rdp_query ** 2
|
||||||
|
pr_answered = 1
|
||||||
|
elif mechanism == 'gnmax_conf':
|
||||||
|
logq_step1 = pate.compute_logpr_answered(params['t'], params['sigma1'], v)
|
||||||
|
logq_step2 = pate.compute_logq_gaussian(v, noise_scale)
|
||||||
|
q_step1 = np.exp(logq_step1)
|
||||||
|
logq_step1_min = min(logq_step1, math.log1p(-q_step1))
|
||||||
|
rdp_gnmax_step1 = pate.rdp_gaussian(logq_step1_min,
|
||||||
|
2 ** .5 * params['sigma1'], orders)
|
||||||
|
rdp_gnmax_step2 = pate.rdp_gaussian(logq_step2, noise_scale, orders)
|
||||||
|
rdp_query = rdp_gnmax_step1 + q_step1 * rdp_gnmax_step2
|
||||||
|
# The expression below evaluates
|
||||||
|
# E[(cost_of_step_1 + Bernoulli(pr_of_step_2) * cost_of_step_2)^2]
|
||||||
|
rdp_sqrd = (
|
||||||
|
rdp_gnmax_step1 ** 2 + 2 * rdp_gnmax_step1 * q_step1 * rdp_gnmax_step2
|
||||||
|
+ q_step1 * rdp_gnmax_step2 ** 2)
|
||||||
|
rdp_select_cum += rdp_gnmax_step1
|
||||||
|
pr_answered = q_step1
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Mechanism must be one of ["lnmax", "gnmax", "gnmax_conf"]')
|
||||||
|
|
||||||
|
rdp_cum += rdp_query
|
||||||
|
rdp_sqrd_cum += rdp_sqrd
|
||||||
|
answered_sum += pr_answered
|
||||||
|
|
||||||
|
answered[i] = answered_sum
|
||||||
|
eps_total[i], order_opt[i] = pate.compute_eps_from_delta(
|
||||||
|
orders, rdp_cum, delta)
|
||||||
|
partition[i] = compute_partition(order_opt[i], eps_total[i])
|
||||||
|
|
||||||
|
if i > 0 and (i + 1) % 1000 == 0:
|
||||||
|
rdp_var = rdp_sqrd_cum / i - (
|
||||||
|
rdp_cum / i) ** 2 # Ignore Bessel's correction.
|
||||||
|
order_opt_idx = np.searchsorted(orders, order_opt[i])
|
||||||
|
eps_std = ((i + 1) * rdp_var[order_opt_idx]) ** .5 # Std of the sum.
|
||||||
|
print(
|
||||||
|
'queries = {}, E[answered] = {:.2f}, E[eps] = {:.3f} (std = {:.5f}) '
|
||||||
|
'at order = {:.2f} (contribution from delta = {:.3f})'.format(
|
||||||
|
i + 1, answered_sum, eps_total[i], eps_std, order_opt[i],
|
||||||
|
-math.log(delta) / (order_opt[i] - 1)))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
return eps_total, partition, answered, order_opt
|
||||||
|
|
||||||
|
|
||||||
|
def print_plot_small(figures_dir, eps_lap, eps_gnmax, answered_gnmax):
|
||||||
|
"""Plots a graph of LNMax vs GNMax.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
figures_dir: A name of the directory where to save the plot.
|
||||||
|
eps_lap: The cumulative privacy costs of the Laplace mechanism.
|
||||||
|
eps_gnmax: The cumulative privacy costs of the Gaussian mechanism
|
||||||
|
answered_gnmax: The cumulative count of queries answered.
|
||||||
|
"""
|
||||||
|
xlim = 6000
|
||||||
|
x_axis = range(0, int(xlim), 10)
|
||||||
|
y_lap = np.zeros(len(x_axis), dtype=float)
|
||||||
|
y_gnmax = np.full(len(x_axis), np.nan, dtype=float)
|
||||||
|
|
||||||
|
for i in range(len(x_axis)):
|
||||||
|
x = x_axis[i]
|
||||||
|
y_lap[i] = eps_lap[x]
|
||||||
|
idx = np.searchsorted(answered_gnmax, x)
|
||||||
|
if idx < len(eps_gnmax):
|
||||||
|
y_gnmax[i] = eps_gnmax[idx]
|
||||||
|
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.5)
|
||||||
|
fig.set_figwidth(4.7)
|
||||||
|
ax.plot(
|
||||||
|
x_axis, y_lap, color='r', ls='--', label='LNMax', alpha=.5, linewidth=5)
|
||||||
|
ax.plot(
|
||||||
|
x_axis,
|
||||||
|
y_gnmax,
|
||||||
|
color='g',
|
||||||
|
ls='-',
|
||||||
|
label='Confident-GNMax',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=5)
|
||||||
|
plt.xticks(np.arange(0, 7000, 1000))
|
||||||
|
plt.xlim([0, 6000])
|
||||||
|
plt.ylim([0, 6.])
|
||||||
|
plt.xlabel('Number of queries answered', fontsize=16)
|
||||||
|
plt.ylabel(r'Privacy cost $\varepsilon$ at $\delta=10^{-8}$', fontsize=16)
|
||||||
|
plt.legend(loc=2, fontsize=13) # loc=2 -- upper left
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
fout_name = os.path.join(figures_dir, 'lnmax_vs_gnmax.pdf')
|
||||||
|
print('Saving the graph to ' + fout_name)
|
||||||
|
fig.savefig(fout_name, bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def print_plot_large(figures_dir, eps_lap, eps_gnmax1, answered_gnmax1,
|
||||||
|
eps_gnmax2, partition_gnmax2, answered_gnmax2):
|
||||||
|
"""Plots a graph of LNMax vs GNMax with two parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
figures_dir: A name of the directory where to save the plot.
|
||||||
|
eps_lap: The cumulative privacy costs of the Laplace mechanism.
|
||||||
|
eps_gnmax1: The cumulative privacy costs of the Gaussian mechanism (set 1).
|
||||||
|
answered_gnmax1: The cumulative count of queries answered (set 1).
|
||||||
|
eps_gnmax2: The cumulative privacy costs of the Gaussian mechanism (set 2).
|
||||||
|
partition_gnmax2: Allocation of eps for set 2.
|
||||||
|
answered_gnmax2: The cumulative count of queries answered (set 2).
|
||||||
|
"""
|
||||||
|
xlim = 6000
|
||||||
|
x_axis = range(0, int(xlim), 10)
|
||||||
|
lenx = len(x_axis)
|
||||||
|
y_lap = np.zeros(lenx)
|
||||||
|
y_gnmax1 = np.full(lenx, np.nan, dtype=float)
|
||||||
|
y_gnmax2 = np.full(lenx, np.nan, dtype=float)
|
||||||
|
y1_gnmax2 = np.full(lenx, np.nan, dtype=float)
|
||||||
|
|
||||||
|
for i in range(lenx):
|
||||||
|
x = x_axis[i]
|
||||||
|
y_lap[i] = eps_lap[x]
|
||||||
|
idx1 = np.searchsorted(answered_gnmax1, x)
|
||||||
|
if idx1 < len(eps_gnmax1):
|
||||||
|
y_gnmax1[i] = eps_gnmax1[idx1]
|
||||||
|
idx2 = np.searchsorted(answered_gnmax2, x)
|
||||||
|
if idx2 < len(eps_gnmax2):
|
||||||
|
y_gnmax2[i] = eps_gnmax2[idx2]
|
||||||
|
fraction_step1, fraction_step2, _ = partition_gnmax2[idx2]
|
||||||
|
y1_gnmax2[i] = eps_gnmax2[idx2] * fraction_step1 / (
|
||||||
|
fraction_step1 + fraction_step2)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.5)
|
||||||
|
fig.set_figwidth(4.7)
|
||||||
|
ax.plot(
|
||||||
|
x_axis,
|
||||||
|
y_lap,
|
||||||
|
color='r',
|
||||||
|
ls='dashed',
|
||||||
|
label='LNMax',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=5)
|
||||||
|
ax.plot(
|
||||||
|
x_axis,
|
||||||
|
y_gnmax1,
|
||||||
|
color='g',
|
||||||
|
ls='-',
|
||||||
|
label='Confident-GNMax (moderate)',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=5)
|
||||||
|
ax.plot(
|
||||||
|
x_axis,
|
||||||
|
y_gnmax2,
|
||||||
|
color='b',
|
||||||
|
ls='-',
|
||||||
|
label='Confident-GNMax (aggressive)',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=5)
|
||||||
|
ax.fill_between(
|
||||||
|
x_axis, [0] * lenx,
|
||||||
|
y1_gnmax2.tolist(),
|
||||||
|
facecolor='b',
|
||||||
|
alpha=.3,
|
||||||
|
hatch='\\')
|
||||||
|
ax.plot(
|
||||||
|
x_axis,
|
||||||
|
y1_gnmax2,
|
||||||
|
color='b',
|
||||||
|
ls='-',
|
||||||
|
label='_nolegend_',
|
||||||
|
alpha=.5,
|
||||||
|
linewidth=1)
|
||||||
|
ax.fill_between(
|
||||||
|
x_axis, y1_gnmax2.tolist(), y_gnmax2.tolist(), facecolor='b', alpha=.3)
|
||||||
|
plt.xticks(np.arange(0, 7000, 1000))
|
||||||
|
plt.xlim([0, xlim])
|
||||||
|
plt.ylim([0, 1.])
|
||||||
|
plt.xlabel('Number of queries answered', fontsize=16)
|
||||||
|
plt.ylabel(r'Privacy cost $\varepsilon$ at $\delta=10^{-8}$', fontsize=16)
|
||||||
|
plt.legend(loc=2, fontsize=13) # loc=2 -- upper left
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
fout_name = os.path.join(figures_dir, 'lnmax_vs_2xgnmax_large.pdf')
|
||||||
|
print('Saving the graph to ' + fout_name)
|
||||||
|
fig.savefig(fout_name, bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_analyses(votes, lambda_laplace, gnmax_parameters, sigma2):
|
||||||
|
"""Sequentially runs all analyses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
votes: A matrix of votes, where each row contains votes in one instance.
|
||||||
|
lambda_laplace: The scale of the Laplace noise (lambda).
|
||||||
|
gnmax_parameters: A list of parameters for GNMax.
|
||||||
|
sigma2: Shared parameter for the GNMax mechanisms.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Five lists whose length is the number of queries.
|
||||||
|
"""
|
||||||
|
print('=== Laplace Mechanism ===')
|
||||||
|
eps_lap, _, _, _ = run_analysis(votes, 'lnmax', lambda_laplace, None)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Does not go anywhere, for now
|
||||||
|
# print('=== Gaussian Mechanism (simple) ===')
|
||||||
|
# eps, _, _, _ = run_analysis(votes[:n,], 'gnmax', sigma1, None)
|
||||||
|
|
||||||
|
eps_gnmax = [[] for p in gnmax_parameters]
|
||||||
|
partition_gmax = [[] for p in gnmax_parameters]
|
||||||
|
answered = [[] for p in gnmax_parameters]
|
||||||
|
order_opt = [[] for p in gnmax_parameters]
|
||||||
|
for i, p in enumerate(gnmax_parameters):
|
||||||
|
print('=== Gaussian Mechanism (confident) {}: ==='.format(p))
|
||||||
|
eps_gnmax[i], partition_gmax[i], answered[i], order_opt[i] = run_analysis(
|
||||||
|
votes, 'gnmax_conf', sigma2, p)
|
||||||
|
print()
|
||||||
|
|
||||||
|
return eps_lap, eps_gnmax, partition_gmax, answered, order_opt
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
lambda_laplace = 50. # corresponds to eps = 1. / lambda_laplace
|
||||||
|
|
||||||
|
# Paramaters of the GNMax
|
||||||
|
gnmax_parameters = ({
|
||||||
|
't': 1000,
|
||||||
|
'sigma1': 500
|
||||||
|
}, {
|
||||||
|
't': 3500,
|
||||||
|
'sigma1': 1500
|
||||||
|
}, {
|
||||||
|
't': 5000,
|
||||||
|
'sigma1': 1500
|
||||||
|
})
|
||||||
|
sigma2 = 100 # GNMax parameters differ only in Step 1 (selection).
|
||||||
|
ftemp_name = '/tmp/precomputed.pkl'
|
||||||
|
|
||||||
|
figures_dir = os.path.expanduser(FLAGS.figures_dir)
|
||||||
|
|
||||||
|
if FLAGS.cache and os.path.isfile(ftemp_name):
|
||||||
|
print('Reading from cache ' + ftemp_name)
|
||||||
|
with open(ftemp_name, 'rb') as f:
|
||||||
|
(eps_lap, eps_gnmax, partition_gmax, answered_gnmax,
|
||||||
|
orders_opt_gnmax) = pickle.load(f)
|
||||||
|
else:
|
||||||
|
fin_name = os.path.expanduser(FLAGS.counts_file)
|
||||||
|
print('Reading raw votes from ' + fin_name)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
votes = np.load(fin_name)
|
||||||
|
|
||||||
|
(eps_lap, eps_gnmax, partition_gmax,
|
||||||
|
answered_gnmax, orders_opt_gnmax) = run_all_analyses(
|
||||||
|
votes, lambda_laplace, gnmax_parameters, sigma2)
|
||||||
|
|
||||||
|
print('Writing to cache ' + ftemp_name)
|
||||||
|
with open(ftemp_name, 'wb') as f:
|
||||||
|
pickle.dump((eps_lap, eps_gnmax, partition_gmax, answered_gnmax,
|
||||||
|
orders_opt_gnmax), f)
|
||||||
|
|
||||||
|
print_plot_small(figures_dir, eps_lap, eps_gnmax[0], answered_gnmax[0])
|
||||||
|
print_plot_large(figures_dir, eps_lap, eps_gnmax[1], answered_gnmax[1],
|
||||||
|
eps_gnmax[2], partition_gmax[2], answered_gnmax[2])
|
||||||
|
plt.close('all')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
358
research/pate_2018/ICLR2018/smooth_sensitivity_table.py
Normal file
358
research/pate_2018/ICLR2018/smooth_sensitivity_table.py
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Performs privacy analysis of GNMax with smooth sensitivity.
|
||||||
|
|
||||||
|
A script in support of the paper "Scalable Private Learning with PATE" by
|
||||||
|
Nicolas Papernot, Shuang Song, Ilya Mironov, Ananth Raghunathan, Kunal Talwar,
|
||||||
|
Ulfar Erlingsson (https://arxiv.org/abs/1802.08908).
|
||||||
|
|
||||||
|
Several flavors of the GNMax algorithm can be analyzed.
|
||||||
|
- Plain GNMax (argmax w/ Gaussian noise) is assumed when arguments threshold
|
||||||
|
and sigma2 are missing.
|
||||||
|
- Confident GNMax (thresholding + argmax w/ Gaussian noise) is used when
|
||||||
|
threshold, sigma1, and sigma2 are given.
|
||||||
|
- Interactive GNMax (two- or multi-round) is triggered by specifying
|
||||||
|
baseline_file, which provides baseline values for votes selection in Step 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('..') # Main modules reside in the parent directory.
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
import numpy as np
|
||||||
|
import core as pate
|
||||||
|
import smooth_sensitivity as pate_ss
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
|
||||||
|
flags.DEFINE_string('counts_file', None, 'Counts file.')
|
||||||
|
flags.DEFINE_string('baseline_file', None, 'File with baseline scores.')
|
||||||
|
flags.DEFINE_boolean('data_independent', False,
|
||||||
|
'Force data-independent bounds.')
|
||||||
|
flags.DEFINE_float('threshold', None, 'Threshold for step 1 (selection).')
|
||||||
|
flags.DEFINE_float('sigma1', None, 'Sigma for step 1 (selection).')
|
||||||
|
flags.DEFINE_float('sigma2', None, 'Sigma for step 2 (argmax).')
|
||||||
|
flags.DEFINE_integer('queries', None, 'Number of queries made by the student.')
|
||||||
|
flags.DEFINE_float('delta', 1e-8, 'Target delta.')
|
||||||
|
flags.DEFINE_float(
|
||||||
|
'order', None,
|
||||||
|
'Fixes a Renyi DP order (if unspecified, finds an optimal order from a '
|
||||||
|
'hardcoded list).')
|
||||||
|
flags.DEFINE_integer(
|
||||||
|
'teachers', None,
|
||||||
|
'Number of teachers (if unspecified, derived from the counts file).')
|
||||||
|
|
||||||
|
flags.mark_flag_as_required('counts_file')
|
||||||
|
flags.mark_flag_as_required('sigma2')
|
||||||
|
|
||||||
|
|
||||||
|
def _check_conditions(sigma, num_classes, orders):
|
||||||
|
"""Symbolic-numeric verification of conditions C5 and C6.
|
||||||
|
|
||||||
|
The conditions on the beta function are verified by constructing the beta
|
||||||
|
function symbolically, and then checking that its derivative (computed
|
||||||
|
symbolically) is non-negative within the interval of conjectured monotonicity.
|
||||||
|
The last check is performed numerically.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print('Checking conditions C5 and C6 for all orders.')
|
||||||
|
sys.stdout.flush()
|
||||||
|
conditions_hold = True
|
||||||
|
|
||||||
|
for order in orders:
|
||||||
|
cond5, cond6 = pate_ss.check_conditions(sigma, num_classes, order)
|
||||||
|
conditions_hold &= cond5 and cond6
|
||||||
|
if not cond5:
|
||||||
|
print('Condition C5 does not hold for order =', order)
|
||||||
|
elif not cond6:
|
||||||
|
print('Condition C6 does not hold for order =', order)
|
||||||
|
|
||||||
|
if conditions_hold:
|
||||||
|
print('Conditions C5-C6 hold for all orders.')
|
||||||
|
sys.stdout.flush()
|
||||||
|
return conditions_hold
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_rdp(votes, baseline, threshold, sigma1, sigma2, delta, orders,
|
||||||
|
data_ind):
|
||||||
|
"""Computes the (data-dependent) RDP curve for Confident GNMax."""
|
||||||
|
rdp_cum = np.zeros(len(orders))
|
||||||
|
rdp_sqrd_cum = np.zeros(len(orders))
|
||||||
|
answered = 0
|
||||||
|
|
||||||
|
for i, v in enumerate(votes):
|
||||||
|
if threshold is None:
|
||||||
|
logq_step1 = 0 # No thresholding, always proceed to step 2.
|
||||||
|
rdp_step1 = np.zeros(len(orders))
|
||||||
|
else:
|
||||||
|
logq_step1 = pate.compute_logpr_answered(threshold, sigma1,
|
||||||
|
v - baseline[i,])
|
||||||
|
if data_ind:
|
||||||
|
rdp_step1 = pate.compute_rdp_data_independent_threshold(sigma1, orders)
|
||||||
|
else:
|
||||||
|
rdp_step1 = pate.compute_rdp_threshold(logq_step1, sigma1, orders)
|
||||||
|
|
||||||
|
if data_ind:
|
||||||
|
rdp_step2 = pate.rdp_data_independent_gaussian(sigma2, orders)
|
||||||
|
else:
|
||||||
|
logq_step2 = pate.compute_logq_gaussian(v, sigma2)
|
||||||
|
rdp_step2 = pate.rdp_gaussian(logq_step2, sigma2, orders)
|
||||||
|
|
||||||
|
q_step1 = np.exp(logq_step1)
|
||||||
|
rdp = rdp_step1 + rdp_step2 * q_step1
|
||||||
|
# The expression below evaluates
|
||||||
|
# E[(cost_of_step_1 + Bernoulli(pr_of_step_2) * cost_of_step_2)^2]
|
||||||
|
rdp_sqrd = (
|
||||||
|
rdp_step1**2 + 2 * rdp_step1 * q_step1 * rdp_step2 +
|
||||||
|
q_step1 * rdp_step2**2)
|
||||||
|
rdp_sqrd_cum += rdp_sqrd
|
||||||
|
|
||||||
|
rdp_cum += rdp
|
||||||
|
answered += q_step1
|
||||||
|
if ((i + 1) % 1000 == 0) or (i == votes.shape[0] - 1):
|
||||||
|
rdp_var = rdp_sqrd_cum / i - (
|
||||||
|
rdp_cum / i)**2 # Ignore Bessel's correction.
|
||||||
|
eps_total, order_opt = pate.compute_eps_from_delta(orders, rdp_cum, delta)
|
||||||
|
order_opt_idx = np.searchsorted(orders, order_opt)
|
||||||
|
eps_std = ((i + 1) * rdp_var[order_opt_idx])**.5 # Std of the sum.
|
||||||
|
print(
|
||||||
|
'queries = {}, E[answered] = {:.2f}, E[eps] = {:.3f} (std = {:.5f}) '
|
||||||
|
'at order = {:.2f} (contribution from delta = {:.3f})'.format(
|
||||||
|
i + 1, answered, eps_total, eps_std, order_opt,
|
||||||
|
-math.log(delta) / (order_opt - 1)))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
_, order_opt = pate.compute_eps_from_delta(orders, rdp_cum, delta)
|
||||||
|
|
||||||
|
return order_opt
|
||||||
|
|
||||||
|
|
||||||
|
def _find_optimal_smooth_sensitivity_parameters(
|
||||||
|
votes, baseline, num_teachers, threshold, sigma1, sigma2, delta, ind_step1,
|
||||||
|
ind_step2, order):
|
||||||
|
"""Optimizes smooth sensitivity parameters by minimizing a cost function.
|
||||||
|
|
||||||
|
The cost function is
|
||||||
|
exact_eps + cost of GNSS + two stds of noise,
|
||||||
|
which captures that upper bound of the confidence interval of the sanitized
|
||||||
|
privacy budget.
|
||||||
|
|
||||||
|
Since optimization is done with full view of sensitive data, the results
|
||||||
|
cannot be released.
|
||||||
|
"""
|
||||||
|
rdp_cum = 0
|
||||||
|
answered_cum = 0
|
||||||
|
ls_cum = 0
|
||||||
|
|
||||||
|
# Define a plausible range for the beta values.
|
||||||
|
betas = np.arange(.3 / order, .495 / order, .01 / order)
|
||||||
|
cost_delta = math.log(1 / delta) / (order - 1)
|
||||||
|
|
||||||
|
for i, v in enumerate(votes):
|
||||||
|
if threshold is None:
|
||||||
|
log_pr_answered = 0
|
||||||
|
rdp1 = 0
|
||||||
|
ls_step1 = np.zeros(num_teachers)
|
||||||
|
else:
|
||||||
|
log_pr_answered = pate.compute_logpr_answered(threshold, sigma1,
|
||||||
|
v - baseline[i,])
|
||||||
|
if ind_step1: # apply data-independent bound for step 1 (thresholding).
|
||||||
|
rdp1 = pate.compute_rdp_data_independent_threshold(sigma1, order)
|
||||||
|
ls_step1 = np.zeros(num_teachers)
|
||||||
|
else:
|
||||||
|
rdp1 = pate.compute_rdp_threshold(log_pr_answered, sigma1, order)
|
||||||
|
ls_step1 = pate_ss.compute_local_sensitivity_bounds_threshold(
|
||||||
|
v - baseline[i,], num_teachers, threshold, sigma1, order)
|
||||||
|
|
||||||
|
pr_answered = math.exp(log_pr_answered)
|
||||||
|
answered_cum += pr_answered
|
||||||
|
|
||||||
|
if ind_step2: # apply data-independent bound for step 2 (GNMax).
|
||||||
|
rdp2 = pate.rdp_data_independent_gaussian(sigma2, order)
|
||||||
|
ls_step2 = np.zeros(num_teachers)
|
||||||
|
else:
|
||||||
|
logq_step2 = pate.compute_logq_gaussian(v, sigma2)
|
||||||
|
rdp2 = pate.rdp_gaussian(logq_step2, sigma2, order)
|
||||||
|
# Compute smooth sensitivity.
|
||||||
|
ls_step2 = pate_ss.compute_local_sensitivity_bounds_gnmax(
|
||||||
|
v, num_teachers, sigma2, order)
|
||||||
|
|
||||||
|
rdp_cum += rdp1 + pr_answered * rdp2
|
||||||
|
ls_cum += ls_step1 + pr_answered * ls_step2 # Expected local sensitivity.
|
||||||
|
|
||||||
|
if ind_step1 and ind_step2:
|
||||||
|
# Data-independent bounds.
|
||||||
|
cost_opt, beta_opt, ss_opt, sigma_ss_opt = None, 0., 0., np.inf
|
||||||
|
else:
|
||||||
|
# Data-dependent bounds.
|
||||||
|
cost_opt, beta_opt, ss_opt, sigma_ss_opt = np.inf, None, None, None
|
||||||
|
|
||||||
|
for beta in betas:
|
||||||
|
ss = pate_ss.compute_discounted_max(beta, ls_cum)
|
||||||
|
|
||||||
|
# Solution to the minimization problem:
|
||||||
|
# min_sigma {order * exp(2 * beta)/ sigma^2 + 2 * ss * sigma}
|
||||||
|
sigma_ss = ((order * math.exp(2 * beta)) / ss)**(1 / 3)
|
||||||
|
cost_ss = pate_ss.compute_rdp_of_smooth_sensitivity_gaussian(
|
||||||
|
beta, sigma_ss, order)
|
||||||
|
|
||||||
|
# Cost captures exact_eps + cost of releasing SS + two stds of noise.
|
||||||
|
cost = rdp_cum + cost_ss + 2 * ss * sigma_ss
|
||||||
|
if cost < cost_opt:
|
||||||
|
cost_opt, beta_opt, ss_opt, sigma_ss_opt = cost, beta, ss, sigma_ss
|
||||||
|
|
||||||
|
if ((i + 1) % 100 == 0) or (i == votes.shape[0] - 1):
|
||||||
|
eps_before_ss = rdp_cum + cost_delta
|
||||||
|
eps_with_ss = (
|
||||||
|
eps_before_ss + pate_ss.compute_rdp_of_smooth_sensitivity_gaussian(
|
||||||
|
beta_opt, sigma_ss_opt, order))
|
||||||
|
print('{}: E[answered queries] = {:.1f}, RDP at {} goes from {:.3f} to '
|
||||||
|
'{:.3f} +/- {:.3f} (ss = {:.4}, beta = {:.4f}, sigma_ss = {:.3f})'.
|
||||||
|
format(i + 1, answered_cum, order, eps_before_ss, eps_with_ss,
|
||||||
|
ss_opt * sigma_ss_opt, ss_opt, beta_opt, sigma_ss_opt))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# Return optimal parameters for the last iteration.
|
||||||
|
return beta_opt, ss_opt, sigma_ss_opt
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# HELPER FUNCTIONS #
|
||||||
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
def _load_votes(counts_file, baseline_file, queries):
|
||||||
|
counts_file_expanded = os.path.expanduser(counts_file)
|
||||||
|
print('Reading raw votes from ' + counts_file_expanded)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
votes = np.load(counts_file_expanded)
|
||||||
|
print('Shape of the votes matrix = {}'.format(votes.shape))
|
||||||
|
|
||||||
|
if baseline_file is not None:
|
||||||
|
baseline_file_expanded = os.path.expanduser(baseline_file)
|
||||||
|
print('Reading baseline values from ' + baseline_file_expanded)
|
||||||
|
sys.stdout.flush()
|
||||||
|
baseline = np.load(baseline_file_expanded)
|
||||||
|
if votes.shape != baseline.shape:
|
||||||
|
raise ValueError(
|
||||||
|
'Counts file and baseline file must have the same shape. Got {} and '
|
||||||
|
'{} instead.'.format(votes.shape, baseline.shape))
|
||||||
|
else:
|
||||||
|
baseline = np.zeros_like(votes)
|
||||||
|
|
||||||
|
if queries is not None:
|
||||||
|
if votes.shape[0] < queries:
|
||||||
|
raise ValueError('Expect {} rows, got {} in {}'.format(
|
||||||
|
queries, votes.shape[0], counts_file))
|
||||||
|
# Truncate the votes matrix to the number of queries made.
|
||||||
|
votes = votes[:queries,]
|
||||||
|
baseline = baseline[:queries,]
|
||||||
|
else:
|
||||||
|
print('Process all {} input rows. (Use --queries flag to truncate.)'.format(
|
||||||
|
votes.shape[0]))
|
||||||
|
|
||||||
|
return votes, baseline
|
||||||
|
|
||||||
|
|
||||||
|
def _count_teachers(votes):
|
||||||
|
s = np.sum(votes, axis=1)
|
||||||
|
num_teachers = int(max(s))
|
||||||
|
if min(s) != num_teachers:
|
||||||
|
raise ValueError(
|
||||||
|
'Matrix of votes is malformed: the number of votes is not the same '
|
||||||
|
'across rows.')
|
||||||
|
return num_teachers
|
||||||
|
|
||||||
|
|
||||||
|
def _is_data_ind_step1(num_teachers, threshold, sigma1, orders):
|
||||||
|
if threshold is None:
|
||||||
|
return True
|
||||||
|
return np.all(
|
||||||
|
pate.is_data_independent_always_opt_threshold(num_teachers, threshold,
|
||||||
|
sigma1, orders))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_data_ind_step2(num_teachers, num_classes, sigma, orders):
|
||||||
|
return np.all(
|
||||||
|
pate.is_data_independent_always_opt_gaussian(num_teachers, num_classes,
|
||||||
|
sigma, orders))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
|
||||||
|
if (FLAGS.threshold is None) != (FLAGS.sigma1 is None):
|
||||||
|
raise ValueError(
|
||||||
|
'--threshold flag and --sigma1 flag must be present or absent '
|
||||||
|
'simultaneously.')
|
||||||
|
|
||||||
|
if FLAGS.order is None:
|
||||||
|
# Long list of orders.
|
||||||
|
orders = np.concatenate((np.arange(2, 100 + 1, .5),
|
||||||
|
np.logspace(np.log10(100), np.log10(500),
|
||||||
|
num=100)))
|
||||||
|
# Short list of orders.
|
||||||
|
# orders = np.round(
|
||||||
|
# np.concatenate((np.arange(2, 50 + 1, 1),
|
||||||
|
# np.logspace(np.log10(50), np.log10(1000), num=20))))
|
||||||
|
else:
|
||||||
|
orders = np.array([FLAGS.order])
|
||||||
|
|
||||||
|
votes, baseline = _load_votes(FLAGS.counts_file, FLAGS.baseline_file,
|
||||||
|
FLAGS.queries)
|
||||||
|
|
||||||
|
if FLAGS.teachers is None:
|
||||||
|
num_teachers = _count_teachers(votes)
|
||||||
|
else:
|
||||||
|
num_teachers = FLAGS.teachers
|
||||||
|
|
||||||
|
num_classes = votes.shape[1]
|
||||||
|
|
||||||
|
order = _compute_rdp(votes, baseline, FLAGS.threshold, FLAGS.sigma1,
|
||||||
|
FLAGS.sigma2, FLAGS.delta, orders,
|
||||||
|
FLAGS.data_independent)
|
||||||
|
|
||||||
|
ind_step1 = _is_data_ind_step1(num_teachers, FLAGS.threshold, FLAGS.sigma1,
|
||||||
|
order)
|
||||||
|
|
||||||
|
ind_step2 = _is_data_ind_step2(num_teachers, num_classes, FLAGS.sigma2, order)
|
||||||
|
|
||||||
|
if FLAGS.data_independent or (ind_step1 and ind_step2):
|
||||||
|
print('Nothing to do here, all analyses are data-independent.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not _check_conditions(FLAGS.sigma2, num_classes, [order]):
|
||||||
|
return # Quit early: sufficient conditions for correctness fail to hold.
|
||||||
|
|
||||||
|
beta_opt, ss_opt, sigma_ss_opt = _find_optimal_smooth_sensitivity_parameters(
|
||||||
|
votes, baseline, num_teachers, FLAGS.threshold, FLAGS.sigma1,
|
||||||
|
FLAGS.sigma2, FLAGS.delta, ind_step1, ind_step2, order)
|
||||||
|
|
||||||
|
print('Optimal beta = {:.4f}, E[SS_beta] = {:.4}, sigma_ss = {:.2f}'.format(
|
||||||
|
beta_opt, ss_opt, sigma_ss_opt))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
90
research/pate_2018/ICLR2018/utility_queries_answered.py
Normal file
90
research/pate_2018/ICLR2018/utility_queries_answered.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
import matplotlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
plt.style.use('ggplot')
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_string('plot_file', '', 'Output file name.')
|
||||||
|
|
||||||
|
qa_lnmax = [500, 750] + range(1000, 12500, 500)
|
||||||
|
|
||||||
|
acc_lnmax = [43.3, 52.3, 59.8, 66.7, 68.8, 70.5, 71.6, 72.3, 72.6, 72.9, 73.4,
|
||||||
|
73.4, 73.7, 73.9, 74.2, 74.4, 74.5, 74.7, 74.8, 75, 75.1, 75.1,
|
||||||
|
75.4, 75.4, 75.4]
|
||||||
|
|
||||||
|
qa_gnmax = [456, 683, 908, 1353, 1818, 2260, 2702, 3153, 3602, 4055, 4511, 4964,
|
||||||
|
5422, 5875, 6332, 6792, 7244, 7696, 8146, 8599, 9041, 9496, 9945,
|
||||||
|
10390, 10842]
|
||||||
|
|
||||||
|
acc_gnmax = [39.6, 52.2, 59.6, 66.6, 69.6, 70.5, 71.8, 72, 72.7, 72.9, 73.3,
|
||||||
|
73.4, 73.4, 73.8, 74, 74.2, 74.4, 74.5, 74.5, 74.7, 74.8, 75, 75.1,
|
||||||
|
75.1, 75.4]
|
||||||
|
|
||||||
|
qa_gnmax_aggressive = [167, 258, 322, 485, 647, 800, 967, 1133, 1282, 1430,
|
||||||
|
1573, 1728, 1889, 2028, 2190, 2348, 2510, 2668, 2950,
|
||||||
|
3098, 3265, 3413, 3581, 3730]
|
||||||
|
|
||||||
|
acc_gnmax_aggressive = [17.8, 26.8, 39.3, 48, 55.7, 61, 62.8, 64.8, 65.4, 66.7,
|
||||||
|
66.2, 68.3, 68.3, 68.7, 69.1, 70, 70.2, 70.5, 70.9,
|
||||||
|
70.7, 71.3, 71.3, 71.3, 71.8]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
|
||||||
|
plt.close('all')
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
fig.set_figheight(4.7)
|
||||||
|
fig.set_figwidth(5)
|
||||||
|
ax.plot(qa_lnmax, acc_lnmax, color='r', ls='--', linewidth=5., marker='o',
|
||||||
|
alpha=.5, label='LNMax')
|
||||||
|
ax.plot(qa_gnmax, acc_gnmax, color='g', ls='-', linewidth=5., marker='o',
|
||||||
|
alpha=.5, label='Confident-GNMax')
|
||||||
|
# ax.plot(qa_gnmax_aggressive, acc_gnmax_aggressive, color='b', ls='-', marker='o', alpha=.5, label='Confident-GNMax (aggressive)')
|
||||||
|
plt.xticks([0, 2000, 4000, 6000])
|
||||||
|
plt.xlim([0, 6000])
|
||||||
|
# ax.set_yscale('log')
|
||||||
|
plt.ylim([65, 76])
|
||||||
|
ax.tick_params(labelsize=14)
|
||||||
|
plt.xlabel('Number of queries answered', fontsize=16)
|
||||||
|
plt.ylabel('Student test accuracy (%)', fontsize=16)
|
||||||
|
plt.legend(loc=2, prop={'size': 16})
|
||||||
|
|
||||||
|
x = [400, 2116, 4600, 4680]
|
||||||
|
y = [69.5, 68.5, 74, 72.5]
|
||||||
|
annotations = [0.76, 2.89, 1.42, 5.76]
|
||||||
|
color_annotations = ['g', 'r', 'g', 'r']
|
||||||
|
for i, txt in enumerate(annotations):
|
||||||
|
ax.annotate(r'${\varepsilon=}$' + str(txt), (x[i], y[i]), fontsize=16,
|
||||||
|
color=color_annotations[i])
|
||||||
|
|
||||||
|
plot_filename = os.path.expanduser(FLAGS.plot_file)
|
||||||
|
plt.savefig(plot_filename, bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(main)
|
71
research/pate_2018/README.md
Normal file
71
research/pate_2018/README.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
Implementation of an RDP privacy accountant and smooth sensitivity analysis for
|
||||||
|
the PATE framework. The underlying theory and supporting experiments appear in
|
||||||
|
"Scalable Private Learning with PATE" by Nicolas Papernot, Shuang Song, Ilya
|
||||||
|
Mironov, Ananth Raghunathan, Kunal Talwar, Ulfar Erlingsson (ICLR 2018,
|
||||||
|
https://arxiv.org/abs/1802.08908).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The PATE ('Private Aggregation of Teacher Ensembles') framework was introduced
|
||||||
|
by Papernot et al. in "Semi-supervised Knowledge Transfer for Deep Learning from
|
||||||
|
Private Training Data" (ICLR 2017, https://arxiv.org/abs/1610.05755). The
|
||||||
|
framework enables model-agnostic training that provably provides [differential
|
||||||
|
privacy](https://en.wikipedia.org/wiki/Differential_privacy) of the training
|
||||||
|
dataset.
|
||||||
|
|
||||||
|
The framework consists of _teachers_, the _student_ model, and the _aggregator_. The
|
||||||
|
teachers are models trained on disjoint subsets of the training datasets. The student
|
||||||
|
model has access to an insensitive (e.g., public) unlabelled dataset, which is labelled by
|
||||||
|
interacting with the ensemble of teachers via the _aggregator_. The aggregator tallies
|
||||||
|
outputs of the teacher models, and either forwards a (noisy) aggregate to the student, or
|
||||||
|
refuses to answer.
|
||||||
|
|
||||||
|
Differential privacy is enforced by the aggregator. The privacy guarantees can be _data-independent_,
|
||||||
|
which means that they are solely the function of the aggregator's parameters. Alternatively, privacy
|
||||||
|
analysis can be _data-dependent_, which allows for finer reasoning where, under certain conditions on
|
||||||
|
the input distribution, the final privacy guarantees can be improved relative to the data-independent
|
||||||
|
analysis. Data-dependent privacy guarantees may, by themselves, be a function of sensitive data and
|
||||||
|
therefore publishing these guarantees requires its own sanitization procedure. In our case
|
||||||
|
sanitization of data-dependent privacy guarantees proceeds via _smooth sensitivity_ analysis.
|
||||||
|
|
||||||
|
The common machinery used for all privacy analyses in this repository is the
|
||||||
|
Rényi differential privacy, or RDP (see https://arxiv.org/abs/1702.07476).
|
||||||
|
|
||||||
|
This repository contains implementations of privacy accountants and smooth
|
||||||
|
sensitivity analysis for several data-independent and data-dependent mechanism that together
|
||||||
|
comprise the PATE framework.
|
||||||
|
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* Python, version ≥ 2.7
|
||||||
|
* absl (see [here](https://github.com/abseil/abseil-py), or just type `pip install absl-py`)
|
||||||
|
* numpy
|
||||||
|
* scipy
|
||||||
|
* sympy (for smooth sensitivity analysis)
|
||||||
|
* unittest (for testing)
|
||||||
|
|
||||||
|
|
||||||
|
### Self-testing
|
||||||
|
|
||||||
|
To verify the installation run
|
||||||
|
```bash
|
||||||
|
$ python core_test.py
|
||||||
|
$ python smooth_sensitivity_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Files in this directory
|
||||||
|
|
||||||
|
* core.py — RDP privacy accountant for several vote aggregators (GNMax,
|
||||||
|
Threshold, Laplace).
|
||||||
|
|
||||||
|
* smooth_sensitivity.py — Smooth sensitivity analysis for GNMax and
|
||||||
|
Threshold mechanisms.
|
||||||
|
|
||||||
|
* core_test.py and smooth_sensitivity_test.py — Unit tests for the
|
||||||
|
files above.
|
||||||
|
|
||||||
|
## Contact information
|
||||||
|
|
||||||
|
You may direct your comments to mironov@google.com and PR to @ilyamironov.
|
370
research/pate_2018/core.py
Normal file
370
research/pate_2018/core.py
Normal file
|
@ -0,0 +1,370 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Core functions for RDP analysis in PATE framework.
|
||||||
|
|
||||||
|
This library comprises the core functions for doing differentially private
|
||||||
|
analysis of the PATE architecture and its various Noisy Max and other
|
||||||
|
mechanisms.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from absl import app
|
||||||
|
import numpy as np
|
||||||
|
import scipy.stats
|
||||||
|
|
||||||
|
|
||||||
|
def _logaddexp(x):
|
||||||
|
"""Addition in the log space. Analogue of numpy.logaddexp for a list."""
|
||||||
|
m = max(x)
|
||||||
|
return m + math.log(sum(np.exp(x - m)))
|
||||||
|
|
||||||
|
|
||||||
|
def _log1mexp(x):
|
||||||
|
"""Numerically stable computation of log(1-exp(x))."""
|
||||||
|
if x < -1:
|
||||||
|
return math.log1p(-math.exp(x))
|
||||||
|
elif x < 0:
|
||||||
|
return math.log(-math.expm1(x))
|
||||||
|
elif x == 0:
|
||||||
|
return -np.inf
|
||||||
|
else:
|
||||||
|
raise ValueError("Argument must be non-positive.")
|
||||||
|
|
||||||
|
|
||||||
|
def compute_eps_from_delta(orders, rdp, delta):
|
||||||
|
"""Translates between RDP and (eps, delta)-DP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
orders: A list (or a scalar) of orders.
|
||||||
|
rdp: A list of RDP guarantees (of the same length as orders).
|
||||||
|
delta: Target delta.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pair of (eps, optimal_order).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If input is malformed.
|
||||||
|
"""
|
||||||
|
if len(orders) != len(rdp):
|
||||||
|
raise ValueError("Input lists must have the same length.")
|
||||||
|
eps = np.array(rdp) - math.log(delta) / (np.array(orders) - 1)
|
||||||
|
idx_opt = np.argmin(eps)
|
||||||
|
return eps[idx_opt], orders[idx_opt]
|
||||||
|
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# RDP FOR THE GNMAX #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
|
||||||
|
def compute_logq_gaussian(counts, sigma):
|
||||||
|
"""Returns an upper bound on ln Pr[outcome != argmax] for GNMax.
|
||||||
|
|
||||||
|
Implementation of Proposition 7.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts: A numpy array of scores.
|
||||||
|
sigma: The standard deviation of the Gaussian noise in the GNMax mechanism.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logq: Natural log of the probability that outcome is different from argmax.
|
||||||
|
"""
|
||||||
|
n = len(counts)
|
||||||
|
variance = sigma**2
|
||||||
|
idx_max = np.argmax(counts)
|
||||||
|
counts_normalized = counts[idx_max] - counts
|
||||||
|
counts_rest = counts_normalized[np.arange(n) != idx_max] # exclude one index
|
||||||
|
# Upper bound q via a union bound rather than a more precise calculation.
|
||||||
|
logq = _logaddexp(
|
||||||
|
scipy.stats.norm.logsf(counts_rest, scale=math.sqrt(2 * variance)))
|
||||||
|
|
||||||
|
# A sketch of a more accurate estimate, which is currently disabled for two
|
||||||
|
# reasons:
|
||||||
|
# 1. Numerical instability;
|
||||||
|
# 2. Not covered by smooth sensitivity analysis.
|
||||||
|
# covariance = variance * (np.ones((n - 1, n - 1)) + np.identity(n - 1))
|
||||||
|
# logq = np.log1p(-statsmodels.sandbox.distributions.extras.mvnormcdf(
|
||||||
|
# counts_rest, np.zeros(n - 1), covariance, maxpts=1e4))
|
||||||
|
|
||||||
|
return min(logq, math.log(1 - (1 / n)))
|
||||||
|
|
||||||
|
|
||||||
|
def rdp_data_independent_gaussian(sigma, orders):
|
||||||
|
"""Computes a data-independent RDP curve for GNMax.
|
||||||
|
|
||||||
|
Implementation of Proposition 8.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sigma: Standard deviation of Gaussian noise.
|
||||||
|
orders: An array_like list of Renyi orders.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Upper bound on RPD for all orders. A scalar if orders is a scalar.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the input is malformed.
|
||||||
|
"""
|
||||||
|
if sigma < 0 or np.any(orders <= 1): # not defined for alpha=1
|
||||||
|
raise ValueError("Inputs are malformed.")
|
||||||
|
|
||||||
|
variance = sigma**2
|
||||||
|
if np.isscalar(orders):
|
||||||
|
return orders / variance
|
||||||
|
else:
|
||||||
|
return np.atleast_1d(orders) / variance
|
||||||
|
|
||||||
|
|
||||||
|
def rdp_gaussian(logq, sigma, orders):
|
||||||
|
"""Bounds RDP from above of GNMax given an upper bound on q (Theorem 6).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
logq: Natural logarithm of the probability of a non-argmax outcome.
|
||||||
|
sigma: Standard deviation of Gaussian noise.
|
||||||
|
orders: An array_like list of Renyi orders.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Upper bound on RPD for all orders. A scalar if orders is a scalar.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the input is malformed.
|
||||||
|
"""
|
||||||
|
if logq > 0 or sigma < 0 or np.any(orders <= 1): # not defined for alpha=1
|
||||||
|
raise ValueError("Inputs are malformed.")
|
||||||
|
|
||||||
|
if np.isneginf(logq): # If the mechanism's output is fixed, it has 0-DP.
|
||||||
|
if np.isscalar(orders):
|
||||||
|
return 0.
|
||||||
|
else:
|
||||||
|
return np.full_like(orders, 0., dtype=np.float)
|
||||||
|
|
||||||
|
variance = sigma**2
|
||||||
|
|
||||||
|
# Use two different higher orders: mu_hi1 and mu_hi2 computed according to
|
||||||
|
# Proposition 10.
|
||||||
|
mu_hi2 = math.sqrt(variance * -logq)
|
||||||
|
mu_hi1 = mu_hi2 + 1
|
||||||
|
|
||||||
|
orders_vec = np.atleast_1d(orders)
|
||||||
|
|
||||||
|
ret = orders_vec / variance # baseline: data-independent bound
|
||||||
|
|
||||||
|
# Filter out entries where data-dependent bound does not apply.
|
||||||
|
mask = np.logical_and(mu_hi1 > orders_vec, mu_hi2 > 1)
|
||||||
|
|
||||||
|
rdp_hi1 = mu_hi1 / variance
|
||||||
|
rdp_hi2 = mu_hi2 / variance
|
||||||
|
|
||||||
|
log_a2 = (mu_hi2 - 1) * rdp_hi2
|
||||||
|
|
||||||
|
# Make sure q is in the increasing wrt q range and A is positive.
|
||||||
|
if (np.any(mask) and logq <= log_a2 - mu_hi2 *
|
||||||
|
(math.log(1 + 1 / (mu_hi1 - 1)) + math.log(1 + 1 / (mu_hi2 - 1))) and
|
||||||
|
-logq > rdp_hi2):
|
||||||
|
# Use log1p(x) = log(1 + x) to avoid catastrophic cancellations when x ~ 0.
|
||||||
|
log1q = _log1mexp(logq) # log1q = log(1-q)
|
||||||
|
log_a = (orders - 1) * (
|
||||||
|
log1q - _log1mexp((logq + rdp_hi2) * (1 - 1 / mu_hi2)))
|
||||||
|
log_b = (orders - 1) * (rdp_hi1 - logq / (mu_hi1 - 1))
|
||||||
|
|
||||||
|
# Use logaddexp(x, y) = log(e^x + e^y) to avoid overflow for large x, y.
|
||||||
|
log_s = np.logaddexp(log1q + log_a, logq + log_b)
|
||||||
|
ret[mask] = np.minimum(ret, log_s / (orders - 1))[mask]
|
||||||
|
|
||||||
|
assert np.all(ret >= 0)
|
||||||
|
|
||||||
|
if np.isscalar(orders):
|
||||||
|
return np.asscalar(ret)
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def is_data_independent_always_opt_gaussian(num_teachers, num_classes, sigma,
|
||||||
|
orders):
|
||||||
|
"""Tests whether data-ind bound is always optimal for GNMax.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_teachers: Number of teachers.
|
||||||
|
num_classes: Number of classes.
|
||||||
|
sigma: Standard deviation of the Gaussian noise.
|
||||||
|
orders: An array_like list of Renyi orders.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean array of length |orders| (a scalar if orders is a scalar). True if
|
||||||
|
the data-independent bound is always the same as the data-dependent bound.
|
||||||
|
|
||||||
|
"""
|
||||||
|
unanimous = np.array([num_teachers] + [0] * (num_classes - 1))
|
||||||
|
logq = compute_logq_gaussian(unanimous, sigma)
|
||||||
|
|
||||||
|
rdp_dep = rdp_gaussian(logq, sigma, orders)
|
||||||
|
rdp_ind = rdp_data_independent_gaussian(sigma, orders)
|
||||||
|
return np.isclose(rdp_dep, rdp_ind)
|
||||||
|
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# RDP FOR THE THRESHOLD MECHANISM #
|
||||||
|
###################################
|
||||||
|
|
||||||
|
|
||||||
|
def compute_logpr_answered(t, sigma, counts):
|
||||||
|
"""Computes log of the probability that a noisy threshold is crossed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
t: The threshold.
|
||||||
|
sigma: The stdev of the Gaussian noise added to the threshold.
|
||||||
|
counts: An array of votes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Natural log of the probability that max is larger than a noisy threshold.
|
||||||
|
"""
|
||||||
|
# Compared to the paper, max(counts) is rounded to the nearest integer. This
|
||||||
|
# is done to facilitate computation of smooth sensitivity for the case of
|
||||||
|
# the interactive mechanism, where votes are not necessarily integer.
|
||||||
|
return scipy.stats.norm.logsf(t - round(max(counts)), scale=sigma)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_rdp_data_independent_threshold(sigma, orders):
|
||||||
|
# The input to the threshold mechanism has stability 1, compared to
|
||||||
|
# GNMax, which has stability = 2. Hence the sqrt(2) factor below.
|
||||||
|
return rdp_data_independent_gaussian(2**.5 * sigma, orders)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_rdp_threshold(log_pr_answered, sigma, orders):
|
||||||
|
logq = min(log_pr_answered, _log1mexp(log_pr_answered))
|
||||||
|
# The input to the threshold mechanism has stability 1, compared to
|
||||||
|
# GNMax, which has stability = 2. Hence the sqrt(2) factor below.
|
||||||
|
return rdp_gaussian(logq, 2**.5 * sigma, orders)
|
||||||
|
|
||||||
|
|
||||||
|
def is_data_independent_always_opt_threshold(num_teachers, threshold, sigma,
|
||||||
|
orders):
|
||||||
|
"""Tests whether data-ind bound is always optimal for the threshold mechanism.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_teachers: Number of teachers.
|
||||||
|
threshold: The cut-off threshold.
|
||||||
|
sigma: Standard deviation of the Gaussian noise.
|
||||||
|
orders: An array_like list of Renyi orders.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean array of length |orders| (a scalar if orders is a scalar). True if
|
||||||
|
the data-independent bound is always the same as the data-dependent bound.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Since the data-dependent bound depends only on max(votes), it suffices to
|
||||||
|
# check whether the data-dependent bounds are better than data-independent
|
||||||
|
# bounds in the extreme cases when max(votes) is minimal or maximal.
|
||||||
|
# For both Confident GNMax and Interactive GNMax it holds that
|
||||||
|
# 0 <= max(votes) <= num_teachers.
|
||||||
|
# The upper bound is trivial in both cases.
|
||||||
|
# The lower bound is trivial for Confident GNMax (and a stronger one, based on
|
||||||
|
# the pigeonhole principle, is possible).
|
||||||
|
# For Interactive GNMax (Algorithm 2), the lower bound follows from the
|
||||||
|
# following argument. Since the votes vector is the difference between the
|
||||||
|
# actual teachers' votes and the student's baseline, we need to argue that
|
||||||
|
# max(n_j - M * p_j) >= 0.
|
||||||
|
# The bound holds because sum_j n_j = sum M * p_j = M. Thus,
|
||||||
|
# sum_j (n_j - M * p_j) = 0, and max_j (n_j - M * p_j) >= 0 as needed.
|
||||||
|
logq1 = compute_logpr_answered(threshold, sigma, [0])
|
||||||
|
logq2 = compute_logpr_answered(threshold, sigma, [num_teachers])
|
||||||
|
|
||||||
|
rdp_dep1 = compute_rdp_threshold(logq1, sigma, orders)
|
||||||
|
rdp_dep2 = compute_rdp_threshold(logq2, sigma, orders)
|
||||||
|
|
||||||
|
rdp_ind = compute_rdp_data_independent_threshold(sigma, orders)
|
||||||
|
return np.isclose(rdp_dep1, rdp_ind) and np.isclose(rdp_dep2, rdp_ind)
|
||||||
|
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# RDP FOR THE LAPLACE NOISE #
|
||||||
|
#############################
|
||||||
|
|
||||||
|
|
||||||
|
def compute_logq_laplace(counts, lmbd):
|
||||||
|
"""Computes an upper bound on log Pr[outcome != argmax] for LNMax.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts: A list of scores.
|
||||||
|
lmbd: The lambda parameter of the Laplace distribution ~exp(-|x| / lambda).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logq: Natural log of the probability that outcome is different from argmax.
|
||||||
|
"""
|
||||||
|
# For noisy max, we only get an upper bound via the union bound. See Lemma 4
|
||||||
|
# in https://arxiv.org/abs/1610.05755.
|
||||||
|
#
|
||||||
|
# Pr[ j beats i*] = (2+gap(j,i*))/ 4 exp(gap(j,i*)
|
||||||
|
# proof at http://mathoverflow.net/questions/66763/
|
||||||
|
|
||||||
|
idx_max = np.argmax(counts)
|
||||||
|
counts_normalized = (counts - counts[idx_max]) / lmbd
|
||||||
|
counts_rest = np.array(
|
||||||
|
[counts_normalized[i] for i in range(len(counts)) if i != idx_max])
|
||||||
|
|
||||||
|
logq = _logaddexp(np.log(2 - counts_rest) + math.log(.25) + counts_rest)
|
||||||
|
|
||||||
|
return min(logq, math.log(1 - (1 / len(counts))))
|
||||||
|
|
||||||
|
|
||||||
|
def rdp_pure_eps(logq, pure_eps, orders):
|
||||||
|
"""Computes the RDP value given logq and pure privacy eps.
|
||||||
|
|
||||||
|
Implementation of https://arxiv.org/abs/1610.05755, Theorem 3.
|
||||||
|
|
||||||
|
The bound used is the min of three terms. The first term is from
|
||||||
|
https://arxiv.org/pdf/1605.02065.pdf.
|
||||||
|
The second term is based on the fact that when event has probability (1-q) for
|
||||||
|
q close to zero, q can only change by exp(eps), which corresponds to a
|
||||||
|
much smaller multiplicative change in (1-q)
|
||||||
|
The third term comes directly from the privacy guarantee.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
logq: Natural logarithm of the probability of a non-optimal outcome.
|
||||||
|
pure_eps: eps parameter for DP
|
||||||
|
orders: array_like list of moments to compute.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Array of upper bounds on rdp (a scalar if orders is a scalar).
|
||||||
|
"""
|
||||||
|
orders_vec = np.atleast_1d(orders)
|
||||||
|
q = math.exp(logq)
|
||||||
|
log_t = np.full_like(orders_vec, np.inf)
|
||||||
|
if q <= 1 / (math.exp(pure_eps) + 1):
|
||||||
|
logt_one = math.log1p(-q) + (
|
||||||
|
math.log1p(-q) - _log1mexp(pure_eps + logq)) * (
|
||||||
|
orders_vec - 1)
|
||||||
|
logt_two = logq + pure_eps * (orders_vec - 1)
|
||||||
|
log_t = np.logaddexp(logt_one, logt_two)
|
||||||
|
|
||||||
|
ret = np.minimum(
|
||||||
|
np.minimum(0.5 * pure_eps * pure_eps * orders_vec,
|
||||||
|
log_t / (orders_vec - 1)), pure_eps)
|
||||||
|
if np.isscalar(orders):
|
||||||
|
return np.asscalar(ret)
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(main)
|
124
research/pate_2018/core_test.py
Normal file
124
research/pate_2018/core_test.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 pate.core."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import core as pate
|
||||||
|
|
||||||
|
|
||||||
|
class PateTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def _test_rdp_gaussian_value_errors(self):
|
||||||
|
# Test for ValueErrors.
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
pate.rdp_gaussian(1.0, 1.0, np.array([2, 3, 4]))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
pate.rdp_gaussian(np.log(0.5), -1.0, np.array([2, 3, 4]))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
pate.rdp_gaussian(np.log(0.5), 1.0, np.array([1, 3, 4]))
|
||||||
|
|
||||||
|
def _test_rdp_gaussian_as_function_of_q(self):
|
||||||
|
# Test for data-independent and data-dependent ranges over q.
|
||||||
|
# The following corresponds to orders 1.1, 2.5, 32, 250
|
||||||
|
# sigmas 1.5, 15, 1500, 15000.
|
||||||
|
# Hand calculated -log(q0)s arranged in a 'sigma major' ordering.
|
||||||
|
neglogq0s = [
|
||||||
|
2.8, 2.6, 427, None, 4.8, 4.0, 4.7, 275, 9.6, 8.8, 6.0, 4, 12, 11.2,
|
||||||
|
8.6, 6.4
|
||||||
|
]
|
||||||
|
idx_neglogq0s = 0 # To iterate through neglogq0s.
|
||||||
|
orders = [1.1, 2.5, 32, 250]
|
||||||
|
sigmas = [1.5, 15, 1500, 15000]
|
||||||
|
for sigma in sigmas:
|
||||||
|
for order in orders:
|
||||||
|
curr_neglogq0 = neglogq0s[idx_neglogq0s]
|
||||||
|
idx_neglogq0s += 1
|
||||||
|
if curr_neglogq0 is None: # sigma == 1.5 and order == 250:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rdp_at_q0 = pate.rdp_gaussian(-curr_neglogq0, sigma, order)
|
||||||
|
|
||||||
|
# Data-dependent range. (Successively halve the value of q.)
|
||||||
|
logq_dds = (-curr_neglogq0 - np.array(
|
||||||
|
[0, np.log(2), np.log(4), np.log(8)]))
|
||||||
|
# Check that in q_dds, rdp is decreasing.
|
||||||
|
for idx in range(len(logq_dds) - 1):
|
||||||
|
self.assertGreater(
|
||||||
|
pate.rdp_gaussian(logq_dds[idx], sigma, order),
|
||||||
|
pate.rdp_gaussian(logq_dds[idx + 1], sigma, order))
|
||||||
|
|
||||||
|
# Data-independent range.
|
||||||
|
q_dids = np.exp(-curr_neglogq0) + np.array([0.1, 0.2, 0.3, 0.4])
|
||||||
|
# Check that in q_dids, rdp is constant.
|
||||||
|
for q in q_dids:
|
||||||
|
self.assertEqual(rdp_at_q0, pate.rdp_gaussian(
|
||||||
|
np.log(q), sigma, order))
|
||||||
|
|
||||||
|
def _test_compute_eps_from_delta_value_error(self):
|
||||||
|
# Test for ValueError.
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
pate.compute_eps_from_delta([1.1, 2, 3, 4], [1, 2, 3], 0.001)
|
||||||
|
|
||||||
|
def _test_compute_eps_from_delta_monotonicity(self):
|
||||||
|
# Test for monotonicity with respect to delta.
|
||||||
|
orders = [1.1, 2.5, 250.0]
|
||||||
|
sigmas = [1e-3, 1.0, 1e5]
|
||||||
|
deltas = [1e-60, 1e-6, 0.1, 0.999]
|
||||||
|
for sigma in sigmas:
|
||||||
|
list_of_eps = []
|
||||||
|
rdps_for_gaussian = np.array(orders) / (2 * sigma**2)
|
||||||
|
for delta in deltas:
|
||||||
|
list_of_eps.append(
|
||||||
|
pate.compute_eps_from_delta(orders, rdps_for_gaussian, delta)[0])
|
||||||
|
|
||||||
|
# Check that in list_of_eps, epsilons are decreasing (as delta increases).
|
||||||
|
sorted_list_of_eps = list(list_of_eps)
|
||||||
|
sorted_list_of_eps.sort(reverse=True)
|
||||||
|
self.assertEqual(list_of_eps, sorted_list_of_eps)
|
||||||
|
|
||||||
|
def _test_compute_q0(self):
|
||||||
|
# Stub code to search a logq space and figure out logq0 by eyeballing
|
||||||
|
# results. This code does not run with the tests. Remove underscore to run.
|
||||||
|
sigma = 15
|
||||||
|
order = 250
|
||||||
|
logqs = np.arange(-290, -270, 1)
|
||||||
|
count = 0
|
||||||
|
for logq in logqs:
|
||||||
|
count += 1
|
||||||
|
sys.stdout.write("\t%0.5g: %0.10g" %
|
||||||
|
(logq, pate.rdp_gaussian(logq, sigma, order)))
|
||||||
|
sys.stdout.flush()
|
||||||
|
if count % 5 == 0:
|
||||||
|
print("")
|
||||||
|
|
||||||
|
def test_rdp_gaussian(self):
|
||||||
|
self._test_rdp_gaussian_value_errors()
|
||||||
|
self._test_rdp_gaussian_as_function_of_q()
|
||||||
|
|
||||||
|
def test_compute_eps_from_delta(self):
|
||||||
|
self._test_compute_eps_from_delta_value_error()
|
||||||
|
self._test_compute_eps_from_delta_monotonicity()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
419
research/pate_2018/smooth_sensitivity.py
Normal file
419
research/pate_2018/smooth_sensitivity.py
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""Functions for smooth sensitivity analysis for PATE mechanisms.
|
||||||
|
|
||||||
|
This library implements functionality for doing smooth sensitivity analysis
|
||||||
|
for Gaussian Noise Max (GNMax), Threshold with Gaussian noise, and Gaussian
|
||||||
|
Noise with Smooth Sensitivity (GNSS) mechanisms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import math
|
||||||
|
from absl import app
|
||||||
|
import numpy as np
|
||||||
|
import scipy
|
||||||
|
import sympy as sp
|
||||||
|
|
||||||
|
import core as pate
|
||||||
|
|
||||||
|
################################
|
||||||
|
# SMOOTH SENSITIVITY FOR GNMAX #
|
||||||
|
################################
|
||||||
|
|
||||||
|
# Global dictionary for storing cached q0 values keyed by (sigma, order).
|
||||||
|
_logq0_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_logq0(sigma, order):
|
||||||
|
key = (sigma, order)
|
||||||
|
if key in _logq0_cache:
|
||||||
|
return _logq0_cache[key]
|
||||||
|
|
||||||
|
logq0 = compute_logq0_gnmax(sigma, order)
|
||||||
|
|
||||||
|
_logq0_cache[key] = logq0 # Update the global variable.
|
||||||
|
return logq0
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_logq1(sigma, order, num_classes):
|
||||||
|
logq0 = _compute_logq0(sigma, order) # Most likely already cached.
|
||||||
|
logq1 = math.log(_compute_bl_gnmax(math.exp(logq0), sigma, num_classes))
|
||||||
|
assert logq1 <= logq0
|
||||||
|
return logq1
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_mu1_mu2_gnmax(sigma, logq):
|
||||||
|
# Computes mu1, mu2 according to Proposition 10.
|
||||||
|
mu2 = sigma * math.sqrt(-logq)
|
||||||
|
mu1 = mu2 + 1
|
||||||
|
return mu1, mu2
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_data_dep_bound_gnmax(sigma, logq, order):
|
||||||
|
# Applies Theorem 6 in Appendix without checking that logq satisfies necessary
|
||||||
|
# constraints. The pre-conditions must be assured by comparing logq against
|
||||||
|
# logq0 by the caller.
|
||||||
|
variance = sigma**2
|
||||||
|
mu1, mu2 = _compute_mu1_mu2_gnmax(sigma, logq)
|
||||||
|
eps1 = mu1 / variance
|
||||||
|
eps2 = mu2 / variance
|
||||||
|
|
||||||
|
log1q = np.log1p(-math.exp(logq)) # log1q = log(1-q)
|
||||||
|
log_a = (order - 1) * (
|
||||||
|
log1q - (np.log1p(-math.exp((logq + eps2) * (1 - 1 / mu2)))))
|
||||||
|
log_b = (order - 1) * (eps1 - logq / (mu1 - 1))
|
||||||
|
|
||||||
|
return np.logaddexp(log1q + log_a, logq + log_b) / (order - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_rdp_gnmax(sigma, logq, order):
|
||||||
|
logq0 = _compute_logq0(sigma, order)
|
||||||
|
if logq >= logq0:
|
||||||
|
return pate.rdp_data_independent_gaussian(sigma, order)
|
||||||
|
else:
|
||||||
|
return _compute_data_dep_bound_gnmax(sigma, logq, order)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_logq0_gnmax(sigma, order):
|
||||||
|
"""Computes the point where we start using data-independent bounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sigma: std of the Gaussian noise
|
||||||
|
order: Renyi order lambda
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logq0: the point above which the data-ind bound overtakes data-dependent
|
||||||
|
bound.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _check_validity_conditions(logq):
|
||||||
|
# Function returns true iff logq is in the range where data-dependent bound
|
||||||
|
# is valid. (Theorem 6 in Appendix.)
|
||||||
|
mu1, mu2 = _compute_mu1_mu2_gnmax(sigma, logq)
|
||||||
|
if mu1 < order:
|
||||||
|
return False
|
||||||
|
eps2 = mu2 / sigma**2
|
||||||
|
# Do computation in the log space. The condition below comes from Lemma 9
|
||||||
|
# from Appendix.
|
||||||
|
return (logq <= (mu2 - 1) * eps2 - mu2 * math.log(mu1 / (mu1 - 1) * mu2 /
|
||||||
|
(mu2 - 1)))
|
||||||
|
|
||||||
|
def _compare_dep_vs_ind(logq):
|
||||||
|
return (_compute_data_dep_bound_gnmax(sigma, logq, order) -
|
||||||
|
pate.rdp_data_independent_gaussian(sigma, order))
|
||||||
|
|
||||||
|
# Natural upper bounds on q0.
|
||||||
|
logub = min(-(1 + 1. / sigma)**2, -((order - .99) / sigma)**2, -1 / sigma**2)
|
||||||
|
assert _check_validity_conditions(logub)
|
||||||
|
|
||||||
|
# If data-dependent bound is already better, we are done already.
|
||||||
|
if _compare_dep_vs_ind(logub) < 0:
|
||||||
|
return logub
|
||||||
|
|
||||||
|
# Identifying a reasonable lower bound to bracket logq0.
|
||||||
|
loglb = 2 * logub # logub is negative, and thus loglb < logub.
|
||||||
|
while _compare_dep_vs_ind(loglb) > 0:
|
||||||
|
assert loglb > -10000, "The lower bound on q0 is way too low."
|
||||||
|
loglb *= 1.5
|
||||||
|
|
||||||
|
logq0, r = scipy.optimize.brentq(
|
||||||
|
_compare_dep_vs_ind, loglb, logub, full_output=True)
|
||||||
|
assert r.converged, "The root finding procedure failed to converge."
|
||||||
|
assert _check_validity_conditions(logq0) # just in case.
|
||||||
|
|
||||||
|
return logq0
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_bl_gnmax(q, sigma, num_classes):
|
||||||
|
return ((num_classes - 1) / 2 * scipy.special.erfc(
|
||||||
|
1 / sigma + scipy.special.erfcinv(2 * q / (num_classes - 1))))
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_bu_gnmax(q, sigma, num_classes):
|
||||||
|
return min(1, (num_classes - 1) / 2 * scipy.special.erfc(
|
||||||
|
-1 / sigma + scipy.special.erfcinv(2 * q / (num_classes - 1))))
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_local_sens_gnmax(logq, sigma, num_classes, order):
|
||||||
|
"""Implements Algorithm 3 (computes an upper bound on local sensitivity).
|
||||||
|
|
||||||
|
(See Proposition 13 for proof of correctness.)
|
||||||
|
"""
|
||||||
|
logq0 = _compute_logq0(sigma, order)
|
||||||
|
logq1 = _compute_logq1(sigma, order, num_classes)
|
||||||
|
if logq1 <= logq <= logq0:
|
||||||
|
logq = logq1
|
||||||
|
|
||||||
|
beta = _compute_rdp_gnmax(sigma, logq, order)
|
||||||
|
beta_bu_q = _compute_rdp_gnmax(
|
||||||
|
sigma, math.log(_compute_bu_gnmax(math.exp(logq), sigma, num_classes)),
|
||||||
|
order)
|
||||||
|
beta_bl_q = _compute_rdp_gnmax(
|
||||||
|
sigma, math.log(_compute_bl_gnmax(math.exp(logq), sigma, num_classes)),
|
||||||
|
order)
|
||||||
|
return max(beta_bu_q - beta, beta - beta_bl_q)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_local_sensitivity_bounds_gnmax(votes, num_teachers, sigma, order):
|
||||||
|
"""Computes a list of max-LS-at-distance-d for the GNMax mechanism.
|
||||||
|
|
||||||
|
A more efficient implementation of Algorithms 4 and 5 working in time
|
||||||
|
O(teachers*classes). A naive implementation is O(teachers^2*classes) or worse.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
votes: A numpy array of votes.
|
||||||
|
num_teachers: Total number of voting teachers.
|
||||||
|
sigma: Standard deviation of the Guassian noise.
|
||||||
|
order: The Renyi order.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A numpy array of local sensitivities at distances d, 0 <= d <= num_teachers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
num_classes = len(votes) # Called m in the paper.
|
||||||
|
|
||||||
|
logq0 = _compute_logq0(sigma, order)
|
||||||
|
logq1 = _compute_logq1(sigma, order, num_classes)
|
||||||
|
logq = pate.compute_logq_gaussian(votes, sigma)
|
||||||
|
plateau = _compute_local_sens_gnmax(logq1, sigma, num_classes, order)
|
||||||
|
|
||||||
|
res = np.full(num_teachers, plateau)
|
||||||
|
|
||||||
|
if logq1 <= logq <= logq0:
|
||||||
|
return res
|
||||||
|
|
||||||
|
# Invariant: votes is sorted in the non-increasing order.
|
||||||
|
votes = sorted(votes, reverse=True)
|
||||||
|
|
||||||
|
res[0] = _compute_local_sens_gnmax(logq, sigma, num_classes, order)
|
||||||
|
curr_d = 0
|
||||||
|
|
||||||
|
go_left = logq > logq0 # Otherwise logq < logq1 and we go right.
|
||||||
|
|
||||||
|
# Iterate while the following is true:
|
||||||
|
# 1. If we are going left, logq is still larger than logq0 and we may still
|
||||||
|
# increase the gap between votes[0] and votes[1].
|
||||||
|
# 2. If we are going right, logq is still smaller than logq1.
|
||||||
|
while ((go_left and logq > logq0 and votes[1] > 0) or
|
||||||
|
(not go_left and logq < logq1)):
|
||||||
|
curr_d += 1
|
||||||
|
if go_left: # Try decreasing logq.
|
||||||
|
votes[0] += 1
|
||||||
|
votes[1] -= 1
|
||||||
|
idx = 1
|
||||||
|
# Restore the invariant. (Can be implemented more efficiently by keeping
|
||||||
|
# track of the range of indices equal to votes[1]. Does not seem to matter
|
||||||
|
# for the overall running time.)
|
||||||
|
while idx < len(votes) - 1 and votes[idx] < votes[idx + 1]:
|
||||||
|
votes[idx], votes[idx + 1] = votes[idx + 1], votes[idx]
|
||||||
|
idx += 1
|
||||||
|
else: # Go right, i.e., try increasing logq.
|
||||||
|
votes[0] -= 1
|
||||||
|
votes[1] += 1 # The invariant holds since otherwise logq >= logq1.
|
||||||
|
|
||||||
|
logq = pate.compute_logq_gaussian(votes, sigma)
|
||||||
|
res[curr_d] = _compute_local_sens_gnmax(logq, sigma, num_classes, order)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# SMOOTH SENSITIVITY FOR THE THRESHOLD MECHANISM #
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
# A global dictionary of RDPs for various threshold values. Indexed by a 4-tuple
|
||||||
|
# (num_teachers, threshold, sigma, order).
|
||||||
|
_rdp_thresholds = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_rdp_list_threshold(num_teachers, threshold, sigma, order):
|
||||||
|
key = (num_teachers, threshold, sigma, order)
|
||||||
|
if key in _rdp_thresholds:
|
||||||
|
return _rdp_thresholds[key]
|
||||||
|
|
||||||
|
res = np.zeros(num_teachers + 1)
|
||||||
|
for v in range(0, num_teachers + 1):
|
||||||
|
logp = scipy.stats.norm.logsf(threshold - v, scale=sigma)
|
||||||
|
res[v] = pate.compute_rdp_threshold(logp, sigma, order)
|
||||||
|
|
||||||
|
_rdp_thresholds[key] = res
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def compute_local_sensitivity_bounds_threshold(counts, num_teachers, threshold,
|
||||||
|
sigma, order):
|
||||||
|
"""Computes a list of max-LS-at-distance-d for the threshold mechanism."""
|
||||||
|
|
||||||
|
def _compute_ls(v):
|
||||||
|
ls_step_up, ls_step_down = None, None
|
||||||
|
if v > 0:
|
||||||
|
ls_step_down = abs(rdp_list[v - 1] - rdp_list[v])
|
||||||
|
if v < num_teachers:
|
||||||
|
ls_step_up = abs(rdp_list[v + 1] - rdp_list[v])
|
||||||
|
return max(ls_step_down, ls_step_up) # Rely on max(x, None) = x.
|
||||||
|
|
||||||
|
cur_max = int(round(max(counts)))
|
||||||
|
rdp_list = _compute_rdp_list_threshold(num_teachers, threshold, sigma, order)
|
||||||
|
|
||||||
|
ls = np.zeros(num_teachers)
|
||||||
|
for d in range(max(cur_max, num_teachers - cur_max)):
|
||||||
|
ls_up, ls_down = None, None
|
||||||
|
if cur_max + d <= num_teachers:
|
||||||
|
ls_up = _compute_ls(cur_max + d)
|
||||||
|
if cur_max - d >= 0:
|
||||||
|
ls_down = _compute_ls(cur_max - d)
|
||||||
|
ls[d] = max(ls_up, ls_down)
|
||||||
|
return ls
|
||||||
|
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
# PROCEDURES FOR SMOOTH SENSITIVITY RELEASE #
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# A global dictionary of exponentially decaying arrays. Indexed by beta.
|
||||||
|
dict_beta_discount = {}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_discounted_max(beta, a):
|
||||||
|
n = len(a)
|
||||||
|
|
||||||
|
if beta not in dict_beta_discount or (len(dict_beta_discount[beta]) < n):
|
||||||
|
dict_beta_discount[beta] = np.exp(-beta * np.arange(n))
|
||||||
|
|
||||||
|
return max(a * dict_beta_discount[beta][:n])
|
||||||
|
|
||||||
|
|
||||||
|
def compute_smooth_sensitivity_gnmax(beta, counts, num_teachers, sigma, order):
|
||||||
|
"""Computes smooth sensitivity of a single application of GNMax."""
|
||||||
|
|
||||||
|
ls = compute_local_sensitivity_bounds_gnmax(counts, sigma, order,
|
||||||
|
num_teachers)
|
||||||
|
return compute_discounted_max(beta, ls)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_rdp_of_smooth_sensitivity_gaussian(beta, sigma, order):
|
||||||
|
"""Computes the RDP curve for the GNSS mechanism.
|
||||||
|
|
||||||
|
Implements Theorem 23 (https://arxiv.org/pdf/1802.08908.pdf).
|
||||||
|
"""
|
||||||
|
if beta > 0 and not 1 < order < 1 / (2 * beta):
|
||||||
|
raise ValueError("Order outside the (1, 1/(2*beta)) range.")
|
||||||
|
|
||||||
|
return order * math.exp(2 * beta) / sigma**2 + (
|
||||||
|
-.5 * math.log(1 - 2 * order * beta) + beta * order) / (
|
||||||
|
order - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_params_for_ss_release(eps, delta):
|
||||||
|
"""Computes sigma for additive Gaussian noise scaled by smooth sensitivity.
|
||||||
|
|
||||||
|
Presently not used. (We proceed via RDP analysis.)
|
||||||
|
|
||||||
|
Compute beta, sigma for applying Lemma 2.6 (full version of Nissim et al.) via
|
||||||
|
Lemma 2.10.
|
||||||
|
"""
|
||||||
|
# Rather than applying Lemma 2.10 directly, which would give suboptimal alpha,
|
||||||
|
# (see http://www.cse.psu.edu/~ads22/pubs/NRS07/NRS07-full-draft-v1.pdf),
|
||||||
|
# we extract a sufficient condition on alpha from its proof.
|
||||||
|
#
|
||||||
|
# Let a = rho_(delta/2)(Z_1). Then solve for alpha such that
|
||||||
|
# 2 alpha a + alpha^2 = eps/2.
|
||||||
|
a = scipy.special.ndtri(1 - delta / 2)
|
||||||
|
alpha = math.sqrt(a**2 + eps / 2) - a
|
||||||
|
|
||||||
|
beta = eps / (2 * scipy.special.chdtri(1, delta / 2))
|
||||||
|
|
||||||
|
return alpha, beta
|
||||||
|
|
||||||
|
|
||||||
|
#######################################################
|
||||||
|
# SYMBOLIC-NUMERIC VERIFICATION OF CONDITIONS C5--C6. #
|
||||||
|
#######################################################
|
||||||
|
|
||||||
|
|
||||||
|
def _construct_symbolic_beta(q, sigma, order):
|
||||||
|
mu2 = sigma * sp.sqrt(sp.log(1 / q))
|
||||||
|
mu1 = mu2 + 1
|
||||||
|
eps1 = mu1 / sigma**2
|
||||||
|
eps2 = mu2 / sigma**2
|
||||||
|
a = (1 - q) / (1 - (q * sp.exp(eps2))**(1 - 1 / mu2))
|
||||||
|
b = sp.exp(eps1) / q**(1 / (mu1 - 1))
|
||||||
|
s = (1 - q) * a**(order - 1) + q * b**(order - 1)
|
||||||
|
return (1 / (order - 1)) * sp.log(s)
|
||||||
|
|
||||||
|
|
||||||
|
def _construct_symbolic_bu(q, sigma, m):
|
||||||
|
return (m - 1) / 2 * sp.erfc(sp.erfcinv(2 * q / (m - 1)) - 1 / sigma)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_non_decreasing(fn, q, bounds):
|
||||||
|
"""Verifies whether the function is non-decreasing within a range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fn: Symbolic function of a single variable.
|
||||||
|
q: The name of f's variable.
|
||||||
|
bounds: Pair of (lower_bound, upper_bound) reals.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True iff the function is non-decreasing in the range.
|
||||||
|
"""
|
||||||
|
diff_fn = sp.diff(fn, q) # Symbolically compute the derivative.
|
||||||
|
diff_fn_lambdified = sp.lambdify(
|
||||||
|
q,
|
||||||
|
diff_fn,
|
||||||
|
modules=[
|
||||||
|
"numpy", {
|
||||||
|
"erfc": scipy.special.erfc,
|
||||||
|
"erfcinv": scipy.special.erfcinv
|
||||||
|
}
|
||||||
|
])
|
||||||
|
r = scipy.optimize.minimize_scalar(
|
||||||
|
diff_fn_lambdified, bounds=bounds, method="bounded")
|
||||||
|
assert r.success, "Minimizer failed to converge."
|
||||||
|
return r.fun >= 0 # Check whether the derivative is non-negative.
|
||||||
|
|
||||||
|
|
||||||
|
def check_conditions(sigma, m, order):
|
||||||
|
"""Checks conditions C5 and C6 (Section B.4.2 in Appendix)."""
|
||||||
|
q = sp.symbols("q", positive=True, real=True)
|
||||||
|
|
||||||
|
beta = _construct_symbolic_beta(q, sigma, order)
|
||||||
|
q0 = math.exp(compute_logq0_gnmax(sigma, order))
|
||||||
|
|
||||||
|
cond5 = _is_non_decreasing(beta, q, (0, q0))
|
||||||
|
|
||||||
|
if cond5:
|
||||||
|
bl_q0 = _compute_bl_gnmax(q0, sigma, m)
|
||||||
|
|
||||||
|
bu = _construct_symbolic_bu(q, sigma, m)
|
||||||
|
delta_beta = beta.subs(q, bu) - beta
|
||||||
|
|
||||||
|
cond6 = _is_non_decreasing(delta_beta, q, (0, bl_q0))
|
||||||
|
else:
|
||||||
|
cond6 = False # Skip the check, since Condition 5 is false already.
|
||||||
|
|
||||||
|
return (cond5, cond6)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
del argv # Unused.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(main)
|
126
research/pate_2018/smooth_sensitivity_test.py
Normal file
126
research/pate_2018/smooth_sensitivity_test.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# Copyright 2017 The 'Scalable Private Learning with PATE' Authors All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 pate.smooth_sensitivity."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import smooth_sensitivity as pate_ss
|
||||||
|
|
||||||
|
|
||||||
|
class PateSmoothSensitivityTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_check_conditions(self):
|
||||||
|
self.assertEqual(pate_ss.check_conditions(20, 10, 25.), (True, False))
|
||||||
|
self.assertEqual(pate_ss.check_conditions(30, 10, 25.), (True, True))
|
||||||
|
|
||||||
|
def _assert_all_close(self, x, y):
|
||||||
|
"""Asserts that two numpy arrays are close."""
|
||||||
|
self.assertEqual(len(x), len(y))
|
||||||
|
self.assertTrue(np.allclose(x, y, rtol=1e-8, atol=0))
|
||||||
|
|
||||||
|
def test_compute_local_sensitivity_bounds_gnmax(self):
|
||||||
|
counts1 = np.array([10, 0, 0])
|
||||||
|
sigma1 = .5
|
||||||
|
order1 = 1.5
|
||||||
|
|
||||||
|
answer1 = np.array(
|
||||||
|
[3.13503646e-17, 1.60178280e-08, 5.90681786e-03] + [5.99981308e+00] * 7)
|
||||||
|
|
||||||
|
# Test for "going right" in the smooth sensitivity computation.
|
||||||
|
out1 = pate_ss.compute_local_sensitivity_bounds_gnmax(
|
||||||
|
counts1, 10, sigma1, order1)
|
||||||
|
|
||||||
|
self._assert_all_close(out1, answer1)
|
||||||
|
|
||||||
|
counts2 = np.array([1000, 500, 300, 200, 0])
|
||||||
|
sigma2 = 250.
|
||||||
|
order2 = 10.
|
||||||
|
|
||||||
|
# Test for "going left" in the smooth sensitivity computation.
|
||||||
|
out2 = pate_ss.compute_local_sensitivity_bounds_gnmax(
|
||||||
|
counts2, 2000, sigma2, order2)
|
||||||
|
|
||||||
|
answer2 = np.array([0.] * 298 + [2.77693450548e-7, 2.10853979548e-6] +
|
||||||
|
[2.73113623988e-6] * 1700)
|
||||||
|
self._assert_all_close(out2, answer2)
|
||||||
|
|
||||||
|
def test_compute_local_sensitivity_bounds_threshold(self):
|
||||||
|
counts1_3 = np.array([20, 10, 0])
|
||||||
|
num_teachers = sum(counts1_3)
|
||||||
|
t1 = 16 # high threshold
|
||||||
|
sigma = 2
|
||||||
|
order = 10
|
||||||
|
|
||||||
|
out1 = pate_ss.compute_local_sensitivity_bounds_threshold(
|
||||||
|
counts1_3, num_teachers, t1, sigma, order)
|
||||||
|
answer1 = np.array([0] * 3 + [
|
||||||
|
1.48454129e-04, 1.47826870e-02, 3.94153241e-02, 6.45775697e-02,
|
||||||
|
9.01543247e-02, 1.16054002e-01, 1.42180452e-01, 1.42180452e-01,
|
||||||
|
1.48454129e-04, 1.47826870e-02, 3.94153241e-02, 6.45775697e-02,
|
||||||
|
9.01543266e-02, 1.16054000e-01, 1.42180452e-01, 1.68302106e-01,
|
||||||
|
1.93127860e-01
|
||||||
|
] + [0] * 10)
|
||||||
|
self._assert_all_close(out1, answer1)
|
||||||
|
|
||||||
|
t2 = 2 # low threshold
|
||||||
|
|
||||||
|
out2 = pate_ss.compute_local_sensitivity_bounds_threshold(
|
||||||
|
counts1_3, num_teachers, t2, sigma, order)
|
||||||
|
answer2 = np.array([
|
||||||
|
1.60212079e-01, 2.07021132e-01, 2.07021132e-01, 1.93127860e-01,
|
||||||
|
1.68302106e-01, 1.42180452e-01, 1.16054002e-01, 9.01543247e-02,
|
||||||
|
6.45775697e-02, 3.94153241e-02, 1.47826870e-02, 1.48454129e-04
|
||||||
|
] + [0] * 18)
|
||||||
|
self._assert_all_close(out2, answer2)
|
||||||
|
|
||||||
|
t3 = 50 # very high threshold (larger than the number of teachers).
|
||||||
|
|
||||||
|
out3 = pate_ss.compute_local_sensitivity_bounds_threshold(
|
||||||
|
counts1_3, num_teachers, t3, sigma, order)
|
||||||
|
|
||||||
|
answer3 = np.array([
|
||||||
|
1.35750725752e-19, 1.88990500499e-17, 2.05403154065e-15,
|
||||||
|
1.74298153642e-13, 1.15489723995e-11, 5.97584949325e-10,
|
||||||
|
2.41486826748e-08, 7.62150641922e-07, 1.87846248741e-05,
|
||||||
|
0.000360973025976, 0.000360973025976, 2.76377015215e-50,
|
||||||
|
1.00904975276e-53, 2.87254164748e-57, 6.37583360761e-61,
|
||||||
|
1.10331620211e-64, 1.48844393335e-68, 1.56535552444e-72,
|
||||||
|
1.28328011060e-76, 8.20047697109e-81
|
||||||
|
] + [0] * 10)
|
||||||
|
|
||||||
|
self._assert_all_close(out3, answer3)
|
||||||
|
|
||||||
|
# Fractional values.
|
||||||
|
counts4 = np.array([19.5, -5.1, 0])
|
||||||
|
t4 = 10.1
|
||||||
|
out4 = pate_ss.compute_local_sensitivity_bounds_threshold(
|
||||||
|
counts4, num_teachers, t4, sigma, order)
|
||||||
|
|
||||||
|
answer4 = np.array([
|
||||||
|
0.0620410301, 0.0875807131, 0.113451958, 0.139561671, 0.1657074530,
|
||||||
|
0.1908244840, 0.2070270720, 0.207027072, 0.169718100, 0.0575152142,
|
||||||
|
0.00678695871
|
||||||
|
] + [0] * 6 + [0.000536304908, 0.0172181073, 0.041909870] + [0] * 10)
|
||||||
|
self._assert_all_close(out4, answer4)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in a new issue