Programming Differential Privacy第十三章MACHINE LEARNING机器学习

MACHINE LEARNING机器学习

1.学习目标

  1. 描述并实现梯度下降的基本算法
  2. 使用高斯机制实现差分私有梯度下降
  3. 剪辑梯度,以强制差异隐私的任意损失函数
  4. 描述噪声对训练过程的影响

机器学习的基础理解
在本章中,我们将探索构建差分私有机器学习分类器。我们将关注一种监督学习问题:给定一组标记训练示例{(𝑥1,𝑦1),…,(𝑥𝑛,𝑦𝑛)},其中𝑥𝑖称为特征向量,𝑦𝑖称为标签。训练一个模型𝜃,它可以预测训练集中不存在的新特征向量的标签。每个𝑥𝑖通常是一个描述训练示例特征的实数向量,而𝑦𝑖是从一组预定义的类(通常表示为整数)中提取的,可以从中提取示例。二进制分类器有两个类(通常是1和0,或者1和-1)。

准备工作:

import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import pandas as pd
import numpy as np
from collections import defaultdict

# Some useful utilities

def laplace_mech(v, sensitivity, epsilon):
    return v + np.random.laplace(loc=0, scale=sensitivity / epsilon)

def gaussian_mech(v, sensitivity, epsilon, delta):
    return v + np.random.normal(loc=0, scale=sensitivity * np.sqrt(2*np.log(1.25/delta)) / epsilon)

def gaussian_mech_vec(v, sensitivity, epsilon, delta):
    return v + np.random.normal(loc=0, scale=sensitivity * np.sqrt(2*np.log(1.25/delta)) / epsilon, size=len(v))

def pct_error(orig, priv):
    return np.abs(orig - priv)/orig * 100.0

def z_clip(xs, b):
    return [min(x, b) for x in xs]

def g_clip(v):
    n = np.linalg.norm(v, ord=2)
    if n > 1:
        return v / n
    else:
        return v

2.Logistic Regression with Scikit-Learn¶基于Scikit-Learn的逻辑回归

为了训练模型,我们将使用一些可用的数据来构建一组训练示例(如前所述),但我们也将留出一些数据作为测试示例。一旦我们训练了模型,我们想知道它在训练集中没有出现的例子上工作得如何。一个模型如果能很好地应用于它从未见过的新例子,就被称为能很好地泛化。一种不能很好地泛化的方法被称为训练数据过度拟合

为了测试泛化,我们将使用测试示例—我们为它们提供了标签,因此我们可以通过要求模型对每个类进行分类,然后将预测的类与数据集中的实际标签进行比较,来测试模型的泛化准确性。我们将把数据分成包含80%示例的训练集和包含20%示例的测试集。

X = np.load('../data/adult_processed_x.npy')
y = np.load('../data/adult_processed_y.npy')

training_size = int(X.shape[0] * 0.8)

X_train = X[:training_size]
X_test = X[training_size:]

y_train = y[:training_size]
y_test = y[training_size:]

print(y_test.shape)

(9044,)
Process finished with exit code 0

构建二元分类器的一种简单方法是使用逻辑回归。scikit-learn库有一个用于执行逻辑回归的内置模块,称为LogisticRegression,使用它可以很容易地使用我们的数据构建模型。

那么,我们的模型得到了多少个正确的测试示例呢?我们可以将预测的标签与数据集中的实际标签进行比较;如果我们用正确预测的标签的数量除以测试示例的总数,我们可以测量正确分类的示例的百分比。

# 2
model = LogisticRegression().fit(X_train[:1000], y_train[:1000])
print('预测成功率为{}%'.format(np.sum(model.predict(X_test) == y_test) / X_test.shape[0] * 100.0))

预测成功率为82.43034055727554%

Process finished with exit code 0

82.43%结果还行。

3.What is a Model?什么是模型

什么是模型,以及它如何对已经编码的信息作预测。有许多不同类型的模型,但我们将在这里探讨的是线性模型

对于带有𝑘-维度特征向量𝑥1,…,𝑥𝑘, 线性模型通过首先计算以下量来预测标签:𝑤1𝑥1+⋯+𝑤𝑘𝑥𝑘+𝑏𝑖𝑎𝑠。然后取它的符号(即,如果上面的数量为负,我们预测标签为-1;如果为正,我们预测为1)。然后,模型本身可以由包含值的向量𝑤1,…,𝑤𝑘 和值𝑏𝑖𝑎𝑠来表示。

该模型被称为线性模型,因为我们在预测标签时计算的数量是1次多项式。变量𝑤1,…,𝑤𝑘 通常称为模型的权重或系数,𝑏𝑖𝑎𝑠 通常称为偏差项或截距

事实上,这也是scikit-learn表示其逻辑回归模型的方式,我们可以使用模型的coef_属性检查训练模型的权重:

# 然后,model.coef_ 和 model.intercept_ 属于 Model 的属性,
# 例如对于 LinearRegressor 这个模型,这两个属性分别输出模型的斜率和截距(与y轴的交点)。
print(model.intercept_[0], model.coef_[0])

0.8243034055727554
-5.346167750303712 [ 3.76035057e-01 -2.55358856e-01 -3.21341426e-02  3.74545737e-01
 -6.85885223e-01  3.91875239e-01 -1.69476241e-01 -7.41793527e-02
 -5.76496538e-01  3.94976503e-01 -3.41457312e-01 -6.24912317e-01

请注意,我们的权重始终完全相同𝑤𝑖 因为我们有功能𝑥𝑖, 因为我们必须将每个特征乘以其相应的权重。个人理解,权重相当于系数,特征相当于自变量Xi。

这意味着我们的模型与我们的特征向量具有完全相同的维度

现在我们有了一种获得权重和偏差项的方法,我们可以实现自己的函数来执行预测:

def predict(xi, theta, bias = 0):
    label = np.sign(xi @ theta + bias)
    return label

result = np.sum(predict(X_test, model.coef_[0], model.intercept_[0]) == y_test) / X_test.shape[0]
print(result)

0.8243034055727554

我们在这里选择了偏差项,因为在很多情况下,没有它也可以做得很好。为了使事情更简单,我们不必费心在自己的算法中训练偏差项。

4.Training a Model with Gradient Descent使用梯度下降训练模型

模型训练过程实际上是如何工作的?scikit学习库有一些非常复杂的算法,但我们也可以通过实现一个称为梯度下降的简单算法来实现。

大多数机器学习的训练算法都是根据“损失函数”定义的,它指定了一种方法来衡量模型在预测方面的“糟糕”程度。训练算法的目标是最小化损失函数的输出——低损失的模型将“擅长”预测。

机器学习社区开发了许多不同的常用损失函数。简单的损失函数可能会为每个正确预测的示例返回0,而为每个错误预测的示例则返回1。当损失变为0时,这意味着我们正确预测了每个示例的标签。二元分类中更常用的损失函数称为“逻辑损失”;logistic损失为我们提供了一个“距离”预测正确标签的度量(这比简单的0对1方法更具信息性)。

# The loss function measures how good our model is. The training goal is to minimize the loss.
# This is the logistic loss function.
def loss(theta, xi, yi):
    exponent = - yi * (xi.dot(theta))
    return np.log(1 + np.exp(exponent))

我们可以使用损失函数来衡量一个特定的模型有多好。让我们用一个权重都为零的模型来尝试一下。这个模型不太可能很好地工作,但它是我们训练更好的模型的起点。

def loss(xi, theta, yi):
    exponent = - yi * (xi.dot(theta))
    return np.log(1 + np.exp(exponent))
theta = np.zeros(X_train.shape[1])  #初始为0
result = loss(theta, X_train[0], y_train[0])
print(result)

0.6931471805599453

我们通常通过简单地对训练数据中所有示例的损失进行平均来衡量我们的模型在整个训练集中的表现。

在这种情况下,我们把每个例子都弄错了,所以整个训练集的平均损失完全等于我们上面针对一个例子计算的损失

result = np.mean([loss(x_i, theta, y_i) for x_i, y_i in zip(X_train, y_train)])
print(result)

0.6931471805599453

我们训练模型的目标是将损失降到最低。因此,关键问题是:我们如何修改模型,使损失更小?

梯度下降是一种通过根据损失的梯度更新模型来减小损失的方法。

梯度就像一个多维导数:对于一个有多维输入的函数(就像我们上面的损失函数),梯度告诉你函数的输出相对于输入的每个维度变化的速度有多快。

如果某个维度的梯度为正,这意味着如果我们增加该维度的模型权重,函数的值将增加(相当于系数为正,成正比)。我们希望损失减少,所以我们应该通过否定梯度来修改我们的模型,也就是说,做与梯度相反的事情。

由于我们沿梯度相反的方向移动模型,因此这称为梯度下降。同时也有梯度上升算法。

当我们反复执行这个下降过程的许多步骤时,我们慢慢地越来越接近使损失最小化的模型。这种算法称为梯度下降。让我们看看这在Python中的外观;首先,我们将定义梯度函数。

这里不知道为什么这函数这样子定义,留到以后再探讨!!!!

# 这是物流损失的梯度
# 梯度是一个向量,表示每个方向上损失的变化率
def gradient(theta, xi, yi):
    exponent = yi * (xi.dot(theta))
    return - (yi*xi) / (1+np.exp(exponent))

5.A Single Step of Gradient Descent梯度下降的步骤

接下来,让我们执行一步梯度下降。我们可以将gradient函数应用于训练数据中的单个示例,这将为我们提供足够的信息来改进该示例的模型。我们通过从当前模型θ中减去梯度来“降低”梯度。

# 如果我们在梯度的“相反”方向上迈出一步(通过否定它),我们应该将θ移向一个使损耗更低的方向*
# 这是梯度下降的一步-在每一步中,我们都试图“下降”梯度
# 在本例中,我们只在一个训练示例(第一个)上使用梯度
theta = theta - gradient(theta, X_train[0], y_train[0])
print(theta)

[ 0.          0.          0.          0.         -0.5         0.
  0.          0.         -0.5         0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.         -0.5         0.          0.          0.          0.
  0.          0.         -0.5         0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.         -0.5         0.          0.          0.
  0.          0.          0.          0.          0.          0.
 -0.5         0.         -0.5         0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.          0.
  0.          0.          0.          0.          0.         -0.5
  0.          0.         -0.25       -0.0606146  -0.21875     0.
  0.         -0.17676768]

Process finished with exit code 0

现在,如果我们从训练数据中对同一个示例调用predict,那么它的标签是正确预测的!这意味着我们的更新确实改进了模型,因为它现在能够对这个示例进行分类。

print('y_train[0]', y_train[0], ', predict(theta, X_train[0])', predict(X_train[0], theta))
print('y_train[1]', y_train[10], ', predict(theta, X_train[1])', predict(X_train[10], theta))

y_train[0] -1.0 , predict(theta, X_train[0]) -1.0
y_train[1] 1.0 , predict(theta, X_train[1]) -1.0

Process finished with exit code 0

这里对比两行的输出结果,我们前边只根据训练数据中的单个示例更改了模型,因此在那个示例的对比下是正确的,但是随便挑了个其他案例就可能不符合喽

我们将多次测量模型的精度,所以让我们定义一个用于测量精度的辅助函数。它的工作方式与上述sklearn模型的精度测量相同。

我们可以在通过在一个示例下降低梯度建立的θ上使用它,例如,看看我们的模型在测试集上有多好。

def accuracy(theta):
    return np.sum(predict(X_test, theta) == y_test) / X_test.shape[0]
print('accuracy(theta)', accuracy(theta))

accuracy(theta) 0.7585139318885449

我们改进的模型现在可以正确预测测试集75%的标签!这是一个很好的进展——我们已经大大改进了模型。

6.A Gradient Descent Algorithm一种梯度下降算法

我们需要进行两次更改才能得到基本的梯度下降算法。首先,我们上面的步骤只使用了训练数据中的一个示例;我们希望在更新模型时考虑整个训练集,以便改进所有示例的模型。其次,我们需要执行多次迭代,以尽可能减少损失。
解决第一个问题

def avg_grad(theta, X, y):
    grads = [gradient(theta, xi, yi) for xi, yi in zip(X, y)]
    return np.sum(grads, axis=0)
print('avg_grad(theta, X_train, y_train)', avg_grad(theta, X_train, y_train))

avg_grad(theta, X_train, y_train) [-2.90566529e+02 -3.95639050e+02 -2.12226449e+03 -6.16069265e+02
 -6.72689281e+02 -1.92732017e+02  1.14110710e+00  8.12847832e+01
  6.54576397e+01  4.01160430e+01  2.59722287e+01  4.41393076e+01
  3.95714265e+01  5.14976104e+01 -1.55291342e+02 -2.07329725e+02
 -1.79581278e+03 -2.86096043e+02 -3.94210117e+02 -9.06606392e+02
  1.18443957e+01 -4.34483094e+02 -4.68873465e+02  4.16683564e+02
 -8.20817290e+00 -6.62945034e+03  4.87081846e+01  1.61961201e+03
  1.56242524e+02  1.07592709e+02  2.22950909e+02 -1.76865895e+00
 -7.73924762e+02 -1.76166207e+03 -3.12127426e+01  1.12674916e+02
  4.45014898e+01  5.58870356e+02  4.38948273e+01 -1.58763797e+03
 -1.57253140e+02 -5.95975757e+02 -1.64088259e+02 -1.98188667e+02
 -6.05849293e+03  6.98253660e+02  1.71332463e+02  8.83235968e+02
  5.85753789e+02 -5.68902477e+02  2.38421002e+01 -5.73135506e+01
  3.33520388e+02  3.17255891e+01 -4.62059405e+03  1.22930385e+03
 -5.51812337e+03 -4.04661452e+00 -2.68961697e+01 -8.91181924e+00
  9.83665508e+00 -9.23814615e+00  1.63090615e+01  3.99304455e+00
  1.29005978e+01 -2.33432153e+01 -8.32027496e+00 -1.39797311e+01
 -1.11648323e+01  1.07118005e+01  6.80931088e+00  3.10418633e-01
  2.60780963e+00 -1.51918367e+00 -3.17655597e+00 -3.03757750e+01
 -1.10906874e+01 -3.04136308e+00 -2.06407103e+01  1.44813272e+01
 -9.55618321e+00  2.52890917e+00  8.78025178e+01  6.60106282e+00
  3.16930676e+00  5.59446262e+00 -9.84216973e+00 -2.30516162e+00
  4.52129628e-01  1.61054516e+01  1.66996039e+00  4.16983292e+00
 -1.00366462e+01  2.15824355e+00  4.36062576e+00 -4.30370215e+03
  1.24782743e+01 -2.68293613e+00 -2.50291009e+03 -5.26419053e+02
 -3.60217291e+03 -3.20399637e+02 -3.29208155e+02 -2.29885089e+03]

Process finished with exit code 0

为了解决第二个问题,我们将定义一个迭代算法,使梯度多次下降。
解决第二个问题

def gradient_descent(iterations):
    # 从“猜测”模型应该是什么开始(全部为零)
    theta = np.zeros(X_train.shape[1])
    # 使用训练数据执行梯度下降的“迭代”步骤
    for i in range(iterations):
        theta -= avg_grad(theta, X_train, y_train)
    return theta
theta = gradient_descent(10)
print('theta:\n', theta, ', accuracy(theta):', accuracy(theta))

accuracy(theta): 0.7585139318885449

Process finished with exit code 0

经过10次迭代,我们的模型达到了近78%的准确率-不错!我们的梯度下降算法看起来很简单(而且确实如此!)但不要让它的简单愚弄你。这种基本方法是最近在大规模深度学习中取得的许多成功的背后,我们的算法在设计上非常接近于在Tensorflow等流行的机器学习框架中实现的算法。

请注意,我们没有达到我们之前训练的sklearn模型的84%的准确率。别担心,我们的算法绝对能够做到这一点!我们只需要更多的迭代,以接近最小的损失。

通过100次迭代,我们更接近82%的准确率。越迭代准确性越高!然而,当我们要求如此多的迭代时,算法需要很长时间才能运行。更糟糕的是,我们越接近最小化损失,改进就越困难-因此,我们可能在100次迭代后达到82%的准确率,但可能需要1000次迭代才能达到84%。

这表明了机器学习中的一个基本张力——一般来说,更多的训练迭代可以提高准确性,但更多的迭代需要更多的计算时间。大多数用于使大规模深度学习实用的“技巧”实际上旨在加快梯度下降的每次迭代,以便在相同的时间内执行更多迭代。

还有一件值得注意的事情:损失函数的值确实会随着梯度下降的每次迭代而下降(目的所在)。因此,随着我们执行更多的迭代,我们慢慢地接近最小化损失。还要注意,训练和测试损失非常接近,这表明我们的模型没有过度拟合训练数据。(一旦我们训练了模型,我们想知道它在训练集中没有出现的例子上工作得如何。一个模型如果能很好地应用于它从未见过的新例子,就被称为能很好地泛化。一种不能很好地泛化的方法被称为训练数据过度拟合。)

def gradient_descent_log(iterations):
    # 从“猜测”模型应该是什么开始(全部为零)
    theta = np.zeros(X_train.shape[1])
    # 使用训练数据执行梯度下降的“迭代”步骤
    for i in range(iterations):
        theta -= avg_grad(theta, X_train, y_train)
        print(f'Training loss: {np.mean(loss(theta, X_train, y_train))}')
        print(f'Testing loss: {np.mean(loss(theta, X_test, y_test))}\n')
    return theta
print(gradient_descent_log(5))

Training loss: 0.549109439168421
Testing loss: 0.5415350837580458

Training loss: 0.5224689105514977
Testing loss: 0.5162665121068426

Training loss: 0.5028090736020403
Testing loss: 0.49753785424732383

Training loss: 0.4878874803989895
Testing loss: 0.48335633696635527

Training loss: 0.47628573924997925
Testing loss: 0.4723742456095848

7.Gradient Descent with Differential Privacy具有差异隐私的梯度下降

我们如何使上述算法有区别地私有化?我们希望设计一种算法,确保训练数据的差异隐私,这样最终的模型就不会透露任何关于单个训练示例的信息。

算法中唯一使用训练数据的部分是梯度计算。使算法满足差分隐私的一种方法是在更新模型之前,在每次迭代时向梯度本身添加噪声。这种方法通常被称为噪声梯度下降,因为我们将噪声直接添加到梯度中。

我们的梯度函数是一个向量值函数,因此我们可以使用gaussian_mech_vec将噪声添加到其输出中:

def noisy_gradient_descent(iterations, epsilon, delta):
    theta = np.zeros(X_train.shape[1])
    sensitivity = '???'

    for i in range(iterations):
        grad = avg_grad(theta, X_train, y_train)
        # 等于多了这一步
        noisy_grad = gaussian_mech_vec(grad, sensitivity, epsilon, delta)
        theta -= noisy_grad
    return theta

这个谜题只缺了一部分——梯度函数的灵敏度是多少?回答这个问题是使算法工作的核心困难。

这里有两大挑战。首先,梯度是平均查询的结果——它是每个示例的许多梯度的平均值。正如我们前面所看到的,最好将这样的查询拆分为总和查询和计数查询。这并不难做到——我们可以计算每个示例的梯度之和,而不是它们的平均值,然后再除以噪声计数

其次,我们需要限制每个示例梯度的灵敏度。有两种基本的方法:

  1. 我们可以分析梯度函数本身(就像我们在前面的查询中所做的那样)以确定其最坏情况下的全局敏感性,
  2. 或者我们可以通过裁剪梯度函数的输出(就像我们对样本和聚合所做的一样)来增强敏感性。

我们将从第二种方法开始——通常称为梯度裁剪——因为它在概念上更简单,在应用中更通用。

8.Gradient Clipping梯度裁剪

回想一下,当我们实现样本和聚合时,我们对函数强制执行了所需的敏感性𝑓 通过削波其输出而具有未知的灵敏度。灵敏度𝑓 是:
∣ f ( x ) − f ( x ′ ) ∣ \left|f(x)-f\left(x^{\prime}\right)\right| f(x)f(x)
使用参数剪裁后𝑏, 这将变成:
∣ clip ⁡ ( f ( x ) , b ) − clip ⁡ ( f ( x ′ ) , b ) ∣ \left|\operatorname{clip}(f(x), b)-\operatorname{clip}\left(f\left(x^{\prime}\right), b\right)\right| clip(f(x),b)clip(f(x),b)

最坏情况下: clip ⁡ ( f ( x ) , b ) = b \operatorname{clip}(f(x), b)=b clip(f(x),b)=b同时 clip ⁡ ( f ( x ′ ) , b ) = 0 \operatorname{clip}\left(f\left(x^{\prime}\right), b\right)=0 clip(f(x),b)=0。此时切片结果敏感度刚好是b(剪裁参数的值)

我们可以使用相同的技巧来限制梯度函数的L2灵敏度。我们需要定义一个函数,该函数“剪裁”一个向量,使其具有期望范围内的L2范数。

我们可以通过缩放向量来实现这一点:如果我们将向量元素除以其L2范数,那么得到的向量的L2范量将为1。如果我们想针对特定的剪裁参数𝑏(极端情况敏感度为b), 我们可以将缩放向量乘以𝑏 将其扩展到L2标准𝑏。

我们希望避免修改已经具有低于L2范数的向量𝑏,在这种情况下,我们只返回原始向量。我们可以使用 np.linalg.norm以及参数为2以计算向量的L2范数。

注意代码:
def L2_clip(v, b):
    # 求L2范数
    norm = np.linalg.norm(v, ord=2)

    if norm > b:
        return b * (v / norm)
    else:
        return v

现在我们准备分析裁剪梯度的灵敏度。我们将梯度表示为∇(𝜃;𝑋,𝑦) (对应于Python代码中的梯度):
∥ L 2 − c l i p ( ∇ ( θ ; X , y ) , b ) − L 2 − clip ⁡ ( ∇ ( θ ; X ′ , y ) , 0 ) ∥ 2 \left\|\mathrm{L} 2-\mathrm{clip}(\nabla(\theta ; X, y), b)-\mathrm{L} 2-\operatorname{clip}\left(\nabla\left(\theta ; X^{\prime}, y\right), 0\right)\right\|_{2} L2clip((θ;X,y),b)L2clip((θ;X,y),0)2

在最坏的情况下,𝖫𝟤⎯𝖼𝗅𝗂𝗉(∇(𝜃;𝑋,𝑦),𝑏) L2范数为𝑏, 和𝖫𝟤⎯𝖼𝗅𝗂𝗉(∇(𝜃;𝑋′,𝑦)) 都是0。

因此差分的 L2 范数等于b。因此,剪切渐变的 L2 灵敏度受剪切参数b的限制!

现在,我们可以继续计算裁剪梯度的总和,并根据我们通过裁剪强制执行的 L2 灵敏度b 添加噪声。

现在我们准备完成我们的噪声梯度下降算法。要计算噪声平均梯度,我们需要:

  1. 根据灵敏度将噪声添加到梯度的总和中𝑏
  2. 计算训练示例数量的噪声计数(灵敏度1)
  3. 将(1)的有噪和除以(2)的有噪声计数

此时没对X_train数据集进行剪切

这里等于是噪音添加到theta上边了

def noisy_gradient_descent(iterations, epsilon, delta):
    theta = np.zeros(X_train.shape[1])
    sensitivity = 5.0

    # X_train.shape[0]:36176
    noisy_count = laplace_mech(X_train.shape[0], 1, epsilon)
    print('noisy_count:', noisy_count)

    for i in range(iterations):
        # grad = avg_grad(theta, X_train, y_train)
        # # 等于多了这一步
        # noisy_grad = gaussian_mech_vec(grad, sensitivity, epsilon, delta)
        # theta -= noisy_grad
        grad_sum = gradient_sum(theta, X_train, y_train, sensitivity)
        noisy_grad_sum = gaussian_mech_vec(grad_sum, sensitivity, epsilon, delta)
        noisy_avg_grad = noisy_grad_sum / noisy_count
        # 这里等于是噪音添加到theta上边了
        theta = theta - noisy_avg_grad
    return theta

theta = noisy_gradient_descent(10, 0.1, 1e-5)
print('给梯度加上差分隐私噪声的theta精确度为:', accuracy(theta))

给梯度加上差分隐私噪声的theta精确度为: 0.7795223352498895

该算法的每次迭代都满足(𝜖,𝛿)-差异隐私,我们执行一个附加查询以确定满足𝜖-差分隐私。

如果我们执行𝑘 迭代,然后通过顺序组成,算法满足(𝑘𝜖+𝜖,𝑘𝛿)-差分隐私

我们还可以使用高级合成来分析总的隐私成本;更好的是,我们可以将算法转换为Rényi差分隐私或零集中差分隐私,并获得合成的严格边界。

9.Sensitivity of the Gradient梯度敏感度

我们之前的方法非常通用,因为它没有对梯度的行为做出任何假设。然而,有时我们确实知道一些关于梯度的行为.

特别是,一大类有用的梯度函数(包括我们在这里使用的logistic损失梯度)是Lipschitz连续的.-这意味着它们具有有限的全局敏感性

从形式上讲,可以证明:

 If  ∥ x i ∥ 2 ≤ b  then  ∥ ∇ ( θ ; x i , y i ) ∥ 2 ≤ b \text { If }\left\|x_{i}\right\|_{2} \leq b \text { then }\left\|\nabla\left(\theta ; x_{i}, y_{i}\right)\right\|_{2} \leq b  If xi2b then (θ;xi,yi)2b

这一事实允许我们剪辑训练示例的值(即梯度函数的输入),而不是梯度函数的输出,并获得梯度的L2灵敏度的界限

剪切训练样本而不是梯度有两个优点。(个人理解是剪切样本计算出梯度,而不是直接剪切梯度。)

  1. 首先,估计训练数据的尺度(从而选择一个好的裁剪参数)通常比估计训练期间计算的梯度的尺度更容易。
  2. 第二,它的计算效率更高:我们可以裁剪训练样本一次,并在每次训练模型时重复使用剪辑的训练数据;使用梯度裁剪,我们需要在训练期间裁剪每个梯度。

此外,我们不再被迫计算每个示例的梯度,以便我们可以剪裁它们;相反,我们可以一次计算所有梯度,这可以非常有效地完成(这是机器学习中常用的技巧,但我们在此不再讨论)。

然而,请注意,许多有用的损失函数,特别是那些从深度学习中的神经网络导出的函数,并不具有有界限的全局灵敏度。对于这些损失函数,我们被迫使用梯度剪裁。(不能剪切训练样本)

我们可以通过对算法进行一些简单的修改来剪辑训练示例而不是梯度。
首先,在开始训练之前,我们使用L2_clip剪切训练样本。其次,我们只需删除用于剪切梯度的代码。

此时对X_train数据集进行剪切。精确度上升了2 个百分点。

def noisy_gradient_descent(iterations, epsilon, delta):
    theta = np.zeros(X_train.shape[1])
    sensitivity = 5.0

    # X_train.shape[0]:36176
    noisy_count = laplace_mech(X_train.shape[0], 1, epsilon)
    print('noisy_count:', noisy_count)

    clipped_X = [L2_clip(x_i, sensitivity) for x_i in X_train]
    for i in range(iterations):
        # grad = avg_grad(theta, X_train, y_train)
        # # 等于多了这一步
        # noisy_grad = gaussian_mech_vec(grad, sensitivity, epsilon, delta)
        # theta -= noisy_grad
        grad_sum = gradient_sum(theta, clipped_X, y_train, sensitivity)
        noisy_grad_sum = gaussian_mech_vec(grad_sum, sensitivity, epsilon, delta)
        noisy_avg_grad = noisy_grad_sum / noisy_count
        # 这里等于是噪音添加到theta上边了
        theta = theta - noisy_avg_grad
    return theta

theta = noisy_gradient_descent(10, 0.1, 1e-5)
print(accuracy(theta))

noisy_count: 36213.364076190126
0.7906899601946041

对该算法的许多改进是可能的,这可以提高隐私成本和准确性。许多都是从机器学习文献中提取的。一些示例包括:

  1. 限制总隐私成本𝜖 通过计算每次迭代𝜖𝑖 作为算法的一部分。
  2. 通过高级合成、RDP或zCDP为大量迭代提供更好的合成。
  3. 微型批处理:使用一小块训练数据而不是整个训练集计算每次迭代的梯度(这减少了计算梯度所需的计算)。
  4. 和微型批处理结合的并行组合。
  5. 批次随机抽样和微型批处理结合。
  6. 其他超参数,如学习率𝜂。

10.Effect of Noise on Training噪声对训练的影响

到目前为止,我们已经看到迭代次数对我们得到的模型的准确性有很大影响,因为更多的迭代可以使您更接近损失的最小值。

由于我们的差分私有算法在梯度中添加了噪声,这也会影响精度-噪声会导致我们的算法在训练过程中向错误的方向移动,实际上会使模型变得更糟

可以合理地预期𝜖 将导致模型不太准确。(因为这是迄今为止我们看到的每一种差异私有算法的趋势)。

他的说法是正确的,但也有一个稍微微妙的权衡,这是因为我们在执行算法的多次迭代时需要考虑的组合:迭代次数越多意味着隐私成本越大

在标准梯度下降算法中,更多的迭代通常会产生更好的模型。在我们的差异私有版本中,更多的迭代会使模型变得更糟,

在我们的差异私有版本中,更多的迭代可能会使模型变得更糟,因为我们必须使用更小的𝜖 对于每一次迭代,噪声的尺度都会增大。

在差异私有机器学习中,在使用的迭代次数和添加的噪声规模之间取得适当的平衡是很重要的(有时也是非常具有挑战性的)。

让我们做一个小实验,看看𝜖 影响我们模型的准确性。我们将训练模型的几个值𝜖,每次使用20次迭代,观察每个模型在不同的𝜖 值下训练后的精确度。

delta = 1e-5
epsilons = [0.001, 0.003, 0.005, 0.008, 0.01, 0.03, 0.05, 0.08, 0.1]
thetas = [noisy_gradient_descent(10, epsilon, delta) for epsilon in epsilons]
accs = [accuracy(theta) for theta in thetas]

plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号 #有中文出现的情况,需要u'内容'
plt.title('精确度随着Epsilon增大变化图')
plt.xlabel('Epsilon')
plt.ylabel('Accuracy')
plt.plot(epsilons, accs)
plt.show()

在这里插入图片描述
该图显示𝜖 导致模型的准确性大大降低。请记住𝜖 我们在图中指定为每次迭代𝜖, 所以合成后的隐私成本要高得多。

总结

1、梯度下降是一种通过根据损失的梯度更新模型来使损失变小的方法。梯度就像一个多维导数:对于具有多维输入的函数(如上面的损失函数),梯度体现函数的输出相对于输入的每个维度的变化速度。如果梯度在特定维度中为正,则意味着如果我们增加该维度的模型权重,则该函数的值将增加;我们希望损失减少,因此我们应该通过朝着梯度的反方向来修改我们的模型,即做与梯度相反的事情。由于我们沿梯度相反的方向移动模型,因此这称为梯度下降。

2、一般来说,更多的训练迭代可以提高准确性,但更多的迭代需要更多的计算时间。大多数用于使大规模深度学习变得实用的"技巧"实际上都是为了加快梯度下降的每次迭代,以便在相同的时间内执行更多的迭代。

3、我们的目的是使得最终模型不会显示有关单个训练示例的任何信息。算法中唯一使用训练数据的部分是梯度计算。使算法具有差分隐私的一种方法是在每次迭代时在更新模型之前向梯度本身添加噪声。这种方法通常称为噪声梯度下降,因为我们直接将噪声添加到梯度中。

4、这里有两个主要挑战。首先,梯度是平均查询的结果,它是每个示例的许多梯度的平均值。正如我们之前所看到的,最好将此类查询拆分为总和查询和计数查询。这并不难做到,我们可以计算每个示例梯度的总和,而不是它们的平均值,并在以后除以噪声计数。其次,我们需要绑定每个示例梯度的灵敏度。有两种基本方法:我们可以分析梯度函数本身(就像我们在之前的查询中所做的那样)来确定其最坏情况下的全局灵敏度,或者我们可以通过剪裁梯度函数的输出来强制执行灵敏度(就像我们在样本和聚合中所做的那样)。

5、渐变剪裁算法的每次迭代都满足( ϵ , δ ) (\epsilon, \delta)(ϵ,δ)-差分隐私,我们执行一个额外的查询来确定满足ϵ \epsilonϵ-差分隐私的噪声计数。如果我们执行k kk迭代,则通过顺序组合,算法满足( k ϵ + ϵ , k δ ) (k\epsilon + \epsilon, k\delta)(kϵ+ϵ,kδ)-差分隐私。我们还可以使用高级组合来分析总隐私成本;更好的是,我们可以将算法转换为 Rényi 差分隐私或零集中差分隐私,并获得隐私成本的严格限制。

6、我们之前的方法非常通用,因为它不对梯度的行为做出任何假设。然而,有时我们确实对梯度的行为有所了解。特别是,一大类有用的梯度函数(包括我们在这里使用的逻辑损失的梯度)是利普希茨连续,这意味着它们具有有界的全局灵敏度。

7、裁剪训练示例而不是梯度有两个优点。首先,估计训练数据的比例(从而选择一个好的裁剪参数)通常比估计训练期间将要计算的梯度的尺度更容易。其次,它在计算上更有效:我们可以裁剪一次训练示例,并在每次训练模型时重用裁剪的训练数据。使用渐变剪切,我们需要在训练期间修剪每个梯度。此外,我们不再被迫计算每个示例的梯度,以便我们可以裁剪它们,相反,我们可以一次计算所有梯度,这可以非常有效地完成(这是机器学习中常用的技巧,但我们不会在这里讨论它)。

8、可以合理地预期,ϵ \epsilonϵ的值越小,模型的精度就越低(因为这是我们迄今为止看到的每个差分隐私算法的趋势)。这是事实,但也有一个稍微更微妙的权衡,这是由于我们在执行算法的多次迭代时需要考虑的组成:更多的迭代意味着更大的隐私成本。在标准梯度下降算法中,迭代次数越多,通常会产生更好的模型。在我们的差分隐私版本中,更多的迭代可能会使模型变得更糟,因为我们必须为每次迭代使用较小的ϵ \epsilonϵ,因此噪声的规模会上升。在差分隐私机器学习中,在使用的迭代次数和添加的噪声规模之间取得适当的平衡是很重要的(有时是非常具有挑战性的)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值