文章目录
- 0.事先声明
- 1.local_differential_privacy_library中的算法
- eps2p(epsilon, n=2),计算概率
- discretization(value, lower=0, upper=1),离散化
- random_response(bit_array: np.ndarray, p, q=None),随机响应
- random_response_adjust(s: np.ndarray, data_len, prob):随机响应的反推
- k_random_response(value, values, epsilon):k随机响应
- unary_encoding(bit_array: np.ndarray, epsilon),一元编码
- symmetric_unary_encoding(bit_array: np.ndarray, epsilon)
- optimized_unary_encoding(bit_array: np.ndarray, epsilon)
- 2.项目中的实验部分,待整理!
参考自:
知乎:Dper
Github:DPer
如果感觉有用的话,希望去Github上star一下原作者!!!
0.事先声明
1. 内容基于https://github.com/forestneo/sunDP的开源项目而整理。
2. 内容出自个人理解,并不保证正确性。
3. 本文主要是针对上述开源项目中的算法进行学习和整理。
1.local_differential_privacy_library中的算法
函数名称 | 功能 |
---|---|
eps2p(epsilon, n=2) | 计算概率 |
discretization(value, lower=0, upper=1) | 离散化 |
random_response(bits: np.ndarray, p, q=None) | 随机响应 |
k_random_response(value, values, epsilon) | k随机响应 |
unary_encoding(bits: np.ndarray, epsilon) | UE |
symmetric_unary_encoding(bits: np.ndarray, epsilon) | SUE |
optimized_unary_encoding(bits: np.ndarray, epsilon) | OUE |
eps2p(epsilon, n=2),计算概率
-
此函数是用来计算概率的,这个概率是核心部分。用来实现后面随机响应({0,1}反转)。
-
概率给出
p = e ε e ε + 1 p=\frac{e^{\varepsilon}}{e^{\varepsilon}+1} p=eε+1eε -
说明:
- 这是一个单调增函数,假定 ε ∈ [ 0 , + ∞ ) \varepsilon \in [0,+\infty) ε∈[0,+∞) ,那么函数的值域 p ∈ [ 1 2 , 1 ) p \in [\frac{1}{2},1) p∈[21,1)。
- 随着 ε \varepsilon ε的逐渐增大, p p p的概率也在逐渐增大,np.random.binomial(1, p, len(bit_array))函数上看,数据维持原样的概率增大,扰动数据越接近真实数据,保护的强度也在减小。这个和预期是一致的。( ε \varepsilon ε越大,扰动越小,保护强度越小。 ε \varepsilon ε越小,扰动越大(在 ε = 0 \varepsilon=0 ε=0时, p = 1 2 p=\frac{1}{2} p=21,此时数据都有可能发生反转),保护强度越高)
- 虽然现在我还了解为什么要这样设置 p p p的概率,是从实验结果上来看,效果还是可以的。
- 从函数设计上说,这就是一个sigmod函数。
-
函数定义:
# 这个是核心的概率 # 下面的这个函数是单调增函数 # 假定epsilon的值 >= 0,函数的值域为[0,1) def eps2p(epsilon, n=2): return np.e ** epsilon / (np.e ** epsilon + n - 1)
discretization(value, lower=0, upper=1),离散化
-
这里原作者使用了传统的离散化处理
-
计算概率:
p [ v a l u e ] = v a l u e − l o w e r u p p e r − l o w e r p[value]=\frac{value-lower}{upper-lower} p[value]=upper−lowervalue−lower -
这个 p [ v a l u e ] p[value] p[value]的大小对于最终离散化后结果取lower还是取upper的影响是重要的。
-
举例说明,假定 p [ v a l u e ] = 0.8 p[value]=0.8 p[value]=0.8,则说明分子比较接近分母(给定lower和upper的情况下,分母是定值),即 v a l u e − l o w e r value-lower value−lower越接近 u p p e r − l o w e r upper-lower upper−lower,说明 v a l u e value value越接近 u p p p e r uppper uppper。
-
当然我们可以把这个计算出来的概率和 0.5 0.5 0.5做比较,只要它 p [ v a l u e ] p[value] p[value]大于等于0.5,就返回upper(1), p p p小于0.5,就返回lower(0)。这是一种很朴素的想法。
-
但是原作者,为了离散化更随机,加入了一个和随机数比较的过程。假定生成一个随机数 r n d rnd rnd,当发生 r n d < p [ v a l u e ] rnd< p[value] rnd<p[value]的概率越大,说明 p [ v a l u e ] p[value] p[value]的值越大,value的值越接近upper(1),反之越接近lower(0)。
-
函数定义:
# 这里的离散化可不可以改变一下? def discretization(value, lower=0, upper=1): if value > upper or value < lower: raise Exception("the range of value is not valid in Function @Func: discretization") p = (value - lower) / (upper - lower) rnd = np.random.random() return upper if rnd < p else lower
-
计算 p [ v a l u e ] p[value] p[value]的方法是可以自定义。(毕竟离散化的方法有很多)
-
是否引入和随机数比较,这也是可以自定义的。
random_response(bit_array: np.ndarray, p, q=None),随机响应
-
随机响应式很常见的机制。个人感觉,使用这种方式的好处在于很容易通过扰动后的数据进行反推,近似得出近似原数据的直方图统计(至少从实验上看,是可行的)。
-
原作者在这里使用的基于二项式分布随机响应。从np.random.binomial()函数就可以看出。
-
原作者在这里是以一个向量(具体对对应的就是数据集中的一个样本)做随机响应。
-
说明:原作者在github上的代码和在知乎上关于这部分的阐述有表述上的差异。个人感觉github上中代码在这部解释较好。
-
为什么采用随机扰动,后面再解释。
-
函数定义:
def random_response(bit_array: np.ndarray, p, q=None): """ :param bit_array: :param p: probability of 1->1 :param q: probability of 0->1 update: 2020.03.06 :return: """ q = 1-p if q is None else q # 类型检查 # 如果bit_array是一个标量的int,那么这个isinstance(bit_array, int)就为真, # 但是传入的一个一维的数组,这判断就为假,直接执行后面的return if isinstance(bit_array, int): probability = p if bit_array == 1 else q return np.random.binomial(n=1, p=probability) return np.where(bit_array == 1, np.random.binomial(1, p, len(bit_array)), np.random.binomial(1, q, len(bit_array)))
- 虽然原作者这里使用是的二项分布的api,但是本质上是一个两点的{0,1}分布,一次只做一次抛硬币的实验,生成 l e n ( l e n ( b i t _ a r r a y ) ) len(len(bit\_array)) len(len(bit_array))的序列而已。
- 原作者使用单次的二项分布。其他,如正太分布进行生成数据进行随机响应,是否可以?。(有这个想法,但是还么有测试)
- 使用上述函数的前提是,传入的bit_array是np.ndarray类型。而不是变量。在原作者的使用中,是以一维向量传入。
- np.where()函数的使用。(这个一开始没看懂,现在看懂了)
random_response_adjust(s: np.ndarray, data_len, prob):随机响应的反推
-
这个函数的定义和解释,在原作者的知乎上有,但是在Github上没有。自己把它写完贴上了
-
理论解释:
-
假定数据集中共有 N N N条数据(每个数据非0即1),其中有 x x x个1,则有 N − x N-x N−x个0,通过随机响应处理之后,统计到 s s s个1(求和即可),那么估计原来 N N N条记录中应该有多个1? p p p为不反转的概率。
-
s = x ∗ p + ( N − x ) ∗ ( 1 − p ) s=x*p+(N-x)*(1-p) s=x∗p+(N−x)∗(1−p)
-
x ~ = s + p ∗ N − N 2 ∗ p − 1 \widetilde{x}=\frac{s+p*N-N}{2*p -1} x =2∗p−1s+p∗N−N
-
-
函数定义:
def random_response_adjust(s: np.ndarray, data_len, prob): """ 对random response的结果进行校正 :param s: 收到数据中1的个数,观察到的1的总数 :param data_len: 总的数据个数,样本个数 :param prob: eps2p() :return: 估计实际中1的个数 """ return (s + prob * data_len - data_len) / (2 * prob - 1)
-
真正的随机响应机制:
-
基于之前的概率计算,这个对于隐私保护预算 ε \varepsilon ε的确定很重要
p = e ε e ε + 1 ⇒ ε = ln p 1 − p p=\frac{e^{\varepsilon}}{e^{\varepsilon}+1} \Rightarrow \varepsilon = \ln \frac{p}{1-p} p=eε+1eε⇒ε=ln1−pp -
举例说明:
设对 n n n个用户(样本总数)的问答进行统计,得到患病人数的统计值。其中真实患病的比例记为 π \pi π
,假定回答“yes”的人数为 n 1 n_1 n1,回答"no"的人数记为 n 2 n_2 n2。 p p p为非均匀硬币出现的正面概率。【规定,若抛出正面则必须诚实回答;若抛出反面,则可以随意回答。意思就是,正面说出自己的真实患病情况,反面的话,允许说假话,也可以说真话。这就是随机响应】
有:
P r [ x i = " y e s " ] = π ∗ p + ( 1 − π ) ∗ ( 1 − p ) P r [ x i = " n o " ] = ( 1 − π ) ∗ p + π ∗ ( 1 − p ) Pr[x_i="yes"]=\pi * p + (1 - \pi)*(1-p)\\ Pr[x_i="no"]=(1-\pi)*p + \pi * (1-p) Pr[xi="yes"]=π∗p+(1−π)∗(1−p)Pr[xi="no"]=(1−π)∗p+π∗(1−p)- 公式解释,一个用户的回答是”yes“,会有一种情况。一,自己真的患病,并且说了真话。二,自己没有患病,但是说了假话。同理,一个用户的回答是”no“,也是来自两个方面。【随机响应】
为了得到无偏估计,采用极大似然的方法进行估计:
L = ( π ∗ p + ( 1 − π ) ∗ ( 1 − p ) ) n 1 ∗ ( ( 1 − π ) ∗ p + π ∗ ( 1 − p ) ) n − n 1 L=(\pi * p + (1 - \pi)*(1-p))^{n_1}*((1-\pi)*p + \pi * (1-p))^{n-n_1} L=(π∗p+(1−π)∗(1−p))n1∗((1−π)∗p+π∗(1−p))n−n1
求导,求极值。得到 π \pi π的估计, π ~ \widetilde{\pi} π 。至于怎么求,基本操作,略。
π ~ = p − 1 2 p − 1 + n 1 ( 2 p − 1 ) n \widetilde{\pi}=\frac{p-1}{2p-1}+\frac{n_1}{(2p-1)n} π =2p−1p−1+(2p−1)nn1
则,估计的患病人数 n ~ \widetilde{n} n 可表示为:
n ~ = n ∗ π ~ = p − 1 2 p − 1 n + n 1 2 p − 1 \widetilde{n}=n* \widetilde{\pi}=\frac{p-1}{2p-1}n+\frac{n_1}{2p-1} n =n∗π =2p−1p−1n+2p−1n1- 上述公式的几点说明
- n n n是样本总数,这个是可以值得通过数据集统计得到的。 n 1 n_1 n1是数据集中回答"yes"的人数,这也是可以统计得到。 问题是i, p p p是如何等到的或者设定的?
- 一般的是把 p = e ε e ε + 1 p=\frac{e^\varepsilon}{e^\varepsilon +1} p=eε+1eε, ε \varepsilon ε成为隐私预算。这个就是一开始提到的关键点。反之通过 p p p也可反推出 ε \varepsilon ε。 ε = ln p 1 − p 。 \varepsilon = \ln \frac{p}{1-p} 。 ε=ln1−pp。p , ε ,\varepsilon ,ε是增长是同向的。
- 一般的为了能够获得真实的情况,希望用户总是说真话,那么 p p p的概率能够大一些(至少要大于0.5,因为公正的抛硬币都0.5了。)。说明 p p p的默认概率应该在( 1 2 \frac{1}{2} 21,1)之间。
- 一般情况下,我们设 ε = ln 3 \varepsilon=\ln3 ε=ln3,反过来就是设 p = 3 4 = 0.75 p=\frac{3}{4}=0.75 p=43=0.75。记住,这是一枚不公正的硬币。
-
k_random_response(value, values, epsilon):k随机响应
- 计算概率p
- 从原作者关于该函数的设计上看,在 l e n ( v a l u e s ) len(values) len(values)一定的情况下, ε \varepsilon ε越小,返回原来数据的可能性越小。在 ε \varepsilon ε一定的情况下, l e n ( v a l u e s ) len(values) len(values)越大,返回原来数据的可能性越小。
- 结论: ε \varepsilon ε越小, l e n ( v a l u e s ) len(values) len(values)越大,返回原数据的可能性越小,返回values中非value中的数据的可能性就越大,保护的可能性就越大。
- 函数定义:
# 返回value值,或返回values中的非value值
def k_random_response(value, values, epsilon):
"""
the k-random response
:param value: current value
:param values: the possible value
:param epsilon: privacy budget
:return:
"""
if not isinstance(values, list):
raise Exception("The values should be list")
if value not in values:
raise Exception("Errors in k-random response")
p = np.e ** epsilon / (np.e ** epsilon + len(values) - 1)
if np.random.random() < p: # p是一个大于1/2概率的值
return value
values.remove(value) # 将value移除
return values[np.random.randint(low=0, high=len(values))] # 返回values中任意一个非value的值
unary_encoding(bit_array: np.ndarray, epsilon),一元编码
-
虽然原作者写了这个函数,但是在项目中明没有用。
-
这个和下面的symmetric_unary_encoding是一样的
-
函数定义:
# 这个函数和下面的对称的symmetric_unary_encoding一元编码(反转)是一样的 def unary_encoding(bit_array: np.ndarray, epsilon): """ the unary encoding, the default UE is SUE update: 2020.02.25 """ if not isinstance(bit_array, np.ndarray): raise Exception("Type Err: ", type(bit_array)) return symmetric_unary_encoding(bit_array, epsilon)
symmetric_unary_encoding(bit_array: np.ndarray, epsilon)
-
虽然原作者写了这个函数,但是在项目中明没有用。
-
这里的对称性symmetric,个人感觉体现在 p+q=1上
-
这个函数的本质上自定义概率 p , q p,q p,q(见随机响应random_response)来进行随机响应。
-
函数定义:
# 规定 epsilon >= 0 恒成立 # p+q=1 # 当且仅当,epsilon=0时,p=q=1/2,此时隐私保护的强度最大(因为大家都有可能反转) # 一般情况下,p!=q 且 p > q # ==>也就是说,随着epsilon的逐渐增大,p越大于q,大家都尽可能的不反转(保持原样),数据就越真实,保护强度就越小,这和预期一致 # 这里的对称性,个人感觉体现在 p+q=1上 def symmetric_unary_encoding(bit_array: np.ndarray, epsilon): p = eps2p(epsilon / 2) / (eps2p(epsilon / 2) + 1) q = 1 / (eps2p(epsilon / 2) + 1) return random_response(bit_array, p, q)
optimized_unary_encoding(bit_array: np.ndarray, epsilon)
-
虽然原作者写了这个函数,但是在项目中明没有用。
-
这里的对称性symmetric,个人感觉体现在 p+q !=1(大部分情况下)上
-
这个函数的本质上自定义概率 p , q p,q p,q(见随机响应random_response)来进行随机响应。
-
函数定义:
# p的概率1/2固定, 1->1或者1->0的概率是一样的。 # epsilon的值越小,q的概率就越接近与1/2。即0->1的概率逐渐增大到1/2(结合下面的公式)。 # 当且仅当 epsilon的值为0时,此时p=q=1/2,大家都是公平反转,保护强度最大,一般而言epsilon越小,越接近这个增强保护的趋势 # 而且p+q !=1 (一般情况下,结合下面的公式) # 这里的优化,个人感觉体现在,p+q !=q(大多数情况下);且p恒等于1/2,q也有较小可能实现0->1反转 # 优化点在与,无论epsilon怎样变化,都会提供数据保护(数据发生反转的概率总是较大,至少1反转的概率始终是1/2) # epsilon的逐渐增大,只会让0尽可能的不发生反转(1该以1/2概率反转还是反转);0尽可能的不反转,也是保证了部分数据没有被保护 # 结论就是,epsilon逐渐增大,数据护的强度就会减弱,这和预期是一致的 def optimized_unary_encoding(bit_array: np.ndarray, epsilon): p = 1 / 2 q = 1 / (eps2p(epsilon) + 1) return random_response(bit_array, p, q)