blob: 9bafb5fc30fd0db30a7417417d333598a031613f [file] [log] [blame]
//
// Copyright 2020 Google LLC
//
// 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.
//
package com.google.privacy.differentialprivacy.testing;
import static com.google.common.base.Preconditions.checkArgument;
import java.security.SecureRandom;
import org.apache.commons.math3.distribution.NormalDistribution;
/**
* Utility class providing tools for drawing random samples that may serve as a reference when
* evaluating statistical tests. Note that these samples do not secure against privacy attacks and
* should not be used as a source of noise for differentially private mechanisms.
*/
public final class ReferenceNoiseUtil {
private static final double GAUSSIAN_VARIANCE_ACCURACY = 1e-3;
private static final NormalDistribution NORMAL_DISTRIBUTION = new NormalDistribution(null, 0, 1);
private static final SecureRandom RANDOM = new SecureRandom();
private ReferenceNoiseUtil() {}
/**
* Returns a random sample drawn from a Laplace distribution with the specified {@code mean} and
* {@code variance}. The {@code variance} must be a positive value.
*/
public static double sampleLaplace(double mean, double variance) {
checkArgument(variance > 0.0, "variance must be positive");
// Draw a random sample from the interval (0,1) uniformly at random and transform it to the
// Laplace distribution via the inverse transform method. The values 0 and 1 are excluded as
// they are discontinuities of the transformation function.
double randomDouble = 0;
while (randomDouble <= 0 || 1 <= randomDouble) {
randomDouble = RANDOM.nextDouble();
}
if (randomDouble < 0.5) {
return mean + Math.sqrt(variance / 2) * Math.log(2 * randomDouble);
} else {
return mean - Math.sqrt(variance / 2) * Math.log(2 - 2 * randomDouble);
}
}
/**
* Returns a random sample drawn from a Laplace distribution centered around the {@code rawInput}
* such that the sample is {@code epsilon}-differentially private for the specified {@code
* l1Sensitivity}. The {@code l1Sensitivity} and {@code epsilon} must be a positive.
*/
public static double sampleLaplace(double rawInput, double epsilon, double l1Sensitivity) {
return sampleLaplace(rawInput, getLaplaceVariance(epsilon, l1Sensitivity));
}
/**
* Returns a random sample drawn from a Gaussian distribution with the specified mean and
* variance. The variance must be a positive value.
*/
public static double sampleGaussian(double mean, double variance) {
checkArgument(variance > 0.0, "variance must be positive");
return mean + RANDOM.nextGaussian() * Math.sqrt(variance);
}
/**
* Returns a random sample drawn from a Gaussian distribution centered around the {@code rawInput}
* such that the sample is ({@code epsilon}, {@code delta})-differentially private for the
* specified {@code l2Sensitivity}. The {@code l2Sensitivity}, {@code epsilon} and {@code delta}
* must be a positive.
*/
public static double sampleGaussian(
double rawInput, double epsilon, double delta, double l2Sensitivity) {
return sampleGaussian(rawInput, getGaussianVariance(epsilon, delta, l2Sensitivity));
}
/**
* Returns the smallest variance for which a Laplace distribution is {@code
* epsilon}-differentially private with respect to the provided {@code l1Sensitivity}.
*/
public static double getLaplaceVariance(double epsilon, double l1Sensitivity) {
checkArgument(epsilon > 0.0, "epsilon must be positive");
checkArgument(l1Sensitivity > 0.0, "l1Sensitivity must be positive");
return 2.0 * Math.pow(l1Sensitivity / epsilon, 2.0);
}
/**
* Returns the smallest variance for which a Gaussian distribution is ({@code epsilon}, {@code
* delta})-differentially private with respect to the provided {@code l2Sensitivity}.
*/
public static double getGaussianVariance(double epsilon, double delta, double l2Sensitivity) {
checkArgument(epsilon > 0.0, "epsilon must be positive");
checkArgument(delta > 0.0, "delta must be positive");
checkArgument(l2Sensitivity > 0.0, "l2Sensitivity must be positive");
// We use l2sensitivity as a starting guess for the upper bound, since the required noise grows
// linearly with sensitivity.
double upperBound = l2Sensitivity;
double lowerBound = 0;
// Increase lowerBound and upperBound until upperBound is actually an upper bound of
// sigma_tight, using exponential search.
while (getGaussianDelta(upperBound, l2Sensitivity, epsilon) > delta) {
lowerBound = upperBound;
upperBound = upperBound * 2;
}
// Binary search [lowerBound,upperBound] to find a good enough approximation of sigma_tight.
while (upperBound - lowerBound > GAUSSIAN_VARIANCE_ACCURACY * lowerBound) {
double middle = lowerBound * 0.5 + upperBound * 0.5;
if (getGaussianDelta(middle, l2Sensitivity, epsilon) > delta) {
lowerBound = middle;
} else {
upperBound = middle;
}
}
// Return the over-approximation to err on the safe side.
return upperBound * upperBound;
}
/**
* Returns the smallest delta such that the Gaussian mechanism with standard deviation {@code
* sigma} obtains {@code (epsilon, delta)}-differential privacy with respect to the provided
* {@code l2Sensitivity}. The calculation is based on Theorem 8 of Balle and Wang's "Improving the
* Gaussian Mechanism for Differential Privacy: Analytical Calibration and Optimal Denoising",
* available at https://arxiv.org/abs/1805.06530v2.
*/
private static double getGaussianDelta(double sigma, double l2Sensitivity, double epsilon) {
// Denoting by CDF the CDF function of the standard Gaussian distribution (mean 0, variance 1),
// and s the L2 sensitivity, the tight choice of delta is:
// CDF(s/(2*sigma) - epsilon*sigma/s) - exp(epsilon)*CDF(-s/(2*sigma) - epsilon*sigma/s)
// To simplify the reasoning floating-point underflow/overflows, we rewrite this formula into:
// CDF(a - b) - c * CDF(-a - b)
// where a = s / (2 * sigma), b = epsilon * sigma / s, and c = exp(epsilon).
double a = l2Sensitivity / (2 * sigma);
double b = epsilon * sigma / l2Sensitivity;
double c = Math.exp(epsilon);
if (b == Double.POSITIVE_INFINITY || c == Double.POSITIVE_INFINITY) {
// If either l2Sensitivity goes to 0 or e^epsilon goes to infinity, delta goes to 0.
return 0;
}
return NORMAL_DISTRIBUTION.cumulativeProbability(a - b)
- c * NORMAL_DISTRIBUTION.cumulativeProbability(-a - b);
}
/**
* Returns an upper bound on the l1Sensitivity based on the provided {@code l0Sensitivity} and
* {@code lInfSensitivity}. The {@code l0Sensitivity} and {@code lInfSensitivity} must be
* positive.
*/
public static double getL1Sensitivity(int l0Sensitivity, double lInfSensitivity) {
checkArgument(l0Sensitivity > 0.0, "l0Sensitivity must be positive");
checkArgument(lInfSensitivity > 0.0, "lInfSensitivity must be positive");
return l0Sensitivity * lInfSensitivity;
}
/**
* Returns an upper bound on the l2Senistvity based on the provided {@code l0Sensitivity} and
* {@code lInfSensitivity}. The {@code l0Sensitivity} and {@code lInfSensitivity} must be
* positive.
*/
public static double getL2Sensitivity(int l0Sensitivity, double lInfSensitivity) {
checkArgument(l0Sensitivity > 0.0, "l0Sensitivity must be positive");
checkArgument(lInfSensitivity > 0.0, "lInfSensitivity must be positive");
return Math.sqrt(l0Sensitivity) * lInfSensitivity;
}
}