在 Python 中实施广义生日悖论
Implementing the generalized birthday paradox in Python
我的问题是关于我在实现概率函数时 运行 遇到的数值问题,而不是关于它背后的 probability/mathematics。我也知道我下面的代码可能没有得到很好的优化(例如,如果我在 comb
中使用 exact=False
,我可以向量化第一个函数)。所以我愿意接受优化建议,但这不是我现在主要关心的问题。
我正在尝试用数值验证给定 here 的公式 "the probability of getting m unique values from [0,k) when choosing n times"。
为此,在 Python 3.6.5 中,我使用 numpy.ramdom.choice(k, n, replace=True)
获取多重集,然后计算多重集中的唯一值,保存这个数字。并重复。
对于较小的 k 和 n 值,我在模拟和公式之间取得了很好的一致性,所以我很高兴它或多或少是正确的。但是,当 k 和 n 稍大时,我从公式中得到负值。我怀疑这是因为它包含微小分数和非常大的阶乘的乘积,因此在某些阶段可能会损失精度。
为了尝试解决这个问题,我实现了相同的公式,但在最终求幂之前尽可能使用日志。恼人的是,它并没有真正的帮助,正如我在下面给出的代码的输出中所看到的那样。
因此,我的问题是,有人建议我如何继续为更大的 n 和 k 值执行此公式吗?我认为这是由大数和小数的乘积引入的数字怪异是对的吗?
我的代码:
import numpy as np
import numpy.random as npr
from scipy.special import comb, gammaln
import matplotlib.pyplot as plt
def p_unique_birthdays(m, k, n):
"""PMF for obtaining m unique elements when selecting from [0,k) n times.
I wanted to use exact=True to see if that helped, hence why this is not
vectorised.
"""
total = 0
for i in range(m):
total += (-1)**i * comb(m, i, exact=True) * ((m-i)/k)**n
return comb(k, m, exact=True) * total
def p_unique_birthdays_logs(m, k, n):
"""PMF for obtaining m unique elements when selecting from [0,k) n times.
I use logs to try and deal with some of the numerical craziness that seems
to arise.
"""
total = 0
for i in range(m):
log_mCi = gammaln(m+1) - gammaln(i+1) - gammaln(m-i+1)
log_exp_bit = n * (np.log(m-i) - np.log(k))
total += (-1)**i * np.exp(log_mCi + log_exp_bit)
return comb(k, m, exact=True) * total
def do_stuff(k, n, pmf):
n_samples = 50000
p_ms = np.zeros(n)
for i in range(n):
temp_p = pmf(i+1, k, n)
p_ms[i] = temp_p
print("Sum of probabilities:", p_ms.sum())
samples = np.zeros(n_samples)
for i in range(n_samples):
samples[i] = np.unique(npr.choice(k, n, replace=True)).size
# So that the histogram is centered on the correct integers.
d = np.diff(np.unique(samples)).min()
left_of_first_bin = samples.min() - float(d)/2
right_of_last_bin = samples.max() + float(d)/2
fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(111)
ax.grid()
ax.bar(range(1, n+1), p_ms, color="C0",
label=labels[j])
ax.hist(samples, np.arange(left_of_first_bin, right_of_last_bin + d, d),
alpha=0.5, color="C1", density=True, label="Samples")
ax.legend()
ax.set_xlabel("Unique birthdays")
ax.set_ylabel("Normalised frequency")
ax.set_title(f"k = {k}, n = {n}")
#fig.savefig(f"k{k}_n{n}_{labels[j]}.png")
plt.show()
random_seed = 1234
npr.seed(random_seed)
labels = ["PMF", "PMF (logs)"]
pmfs = [p_unique_birthdays, p_unique_birthdays_logs]
for j in range(2):
for k, n in [(30, 20), (60, 40)]:
do_stuff(k, n, pmfs[j])
输出的数字:
感谢任何ideas/advice/suggestions。
你是对的,这是一些奇数的原因。
更改此行:
total += (-1)**i * comb(m, i, exact=True) * ((m-i)/k)**n
对此:
total += (-1)**i * comb(m, i, exact=True) * ((m-i)**n)/(k**n)
出于某种原因,如果您强制执行不同的操作顺序,结果会很好。
您可能需要花更多时间来弄清楚如何修改您的 "log'd" 版本,但鉴于上述更改修复了一些问题,您可能只想完全放弃 "log'd" 版本。
希望对您有所帮助!
您可以使用内置的 decimal 模块来提高精度。
from decimal import *
getcontext().prec = 10000
def factorial(n):
res = Decimal(1)
for i in range(int(n)):
res = res * Decimal(i + 1)
return res
def binomial_coefficient(n, k):
return factorial(n) / factorial(k) / factorial(n - k)
def p_unique_birthdays(m, k, n):
m = Decimal(m)
k = Decimal(k)
n = Decimal(n)
total = Decimal(0)
for i in range(int(m) + 1):
total += Decimal((-1) ** i) * binomial_coefficient(m, i) * binomial_coefficient(k, m) * ((m - i) / k) ** n
return total
print(p_unique_birthdays(49, 365, 50))
上面的代码打印 0.11484925 与 http://www.wolframalpha.com/input/?i=sum+combination(49,x)combination(365,49)++(((49-x)%2F365)%5E50)+*+(-1)%5Ex,+x%3D0+to+49
相同
我的问题是关于我在实现概率函数时 运行 遇到的数值问题,而不是关于它背后的 probability/mathematics。我也知道我下面的代码可能没有得到很好的优化(例如,如果我在 comb
中使用 exact=False
,我可以向量化第一个函数)。所以我愿意接受优化建议,但这不是我现在主要关心的问题。
我正在尝试用数值验证给定 here 的公式 "the probability of getting m unique values from [0,k) when choosing n times"。
为此,在 Python 3.6.5 中,我使用 numpy.ramdom.choice(k, n, replace=True)
获取多重集,然后计算多重集中的唯一值,保存这个数字。并重复。
对于较小的 k 和 n 值,我在模拟和公式之间取得了很好的一致性,所以我很高兴它或多或少是正确的。但是,当 k 和 n 稍大时,我从公式中得到负值。我怀疑这是因为它包含微小分数和非常大的阶乘的乘积,因此在某些阶段可能会损失精度。
为了尝试解决这个问题,我实现了相同的公式,但在最终求幂之前尽可能使用日志。恼人的是,它并没有真正的帮助,正如我在下面给出的代码的输出中所看到的那样。
因此,我的问题是,有人建议我如何继续为更大的 n 和 k 值执行此公式吗?我认为这是由大数和小数的乘积引入的数字怪异是对的吗?
我的代码:
import numpy as np
import numpy.random as npr
from scipy.special import comb, gammaln
import matplotlib.pyplot as plt
def p_unique_birthdays(m, k, n):
"""PMF for obtaining m unique elements when selecting from [0,k) n times.
I wanted to use exact=True to see if that helped, hence why this is not
vectorised.
"""
total = 0
for i in range(m):
total += (-1)**i * comb(m, i, exact=True) * ((m-i)/k)**n
return comb(k, m, exact=True) * total
def p_unique_birthdays_logs(m, k, n):
"""PMF for obtaining m unique elements when selecting from [0,k) n times.
I use logs to try and deal with some of the numerical craziness that seems
to arise.
"""
total = 0
for i in range(m):
log_mCi = gammaln(m+1) - gammaln(i+1) - gammaln(m-i+1)
log_exp_bit = n * (np.log(m-i) - np.log(k))
total += (-1)**i * np.exp(log_mCi + log_exp_bit)
return comb(k, m, exact=True) * total
def do_stuff(k, n, pmf):
n_samples = 50000
p_ms = np.zeros(n)
for i in range(n):
temp_p = pmf(i+1, k, n)
p_ms[i] = temp_p
print("Sum of probabilities:", p_ms.sum())
samples = np.zeros(n_samples)
for i in range(n_samples):
samples[i] = np.unique(npr.choice(k, n, replace=True)).size
# So that the histogram is centered on the correct integers.
d = np.diff(np.unique(samples)).min()
left_of_first_bin = samples.min() - float(d)/2
right_of_last_bin = samples.max() + float(d)/2
fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(111)
ax.grid()
ax.bar(range(1, n+1), p_ms, color="C0",
label=labels[j])
ax.hist(samples, np.arange(left_of_first_bin, right_of_last_bin + d, d),
alpha=0.5, color="C1", density=True, label="Samples")
ax.legend()
ax.set_xlabel("Unique birthdays")
ax.set_ylabel("Normalised frequency")
ax.set_title(f"k = {k}, n = {n}")
#fig.savefig(f"k{k}_n{n}_{labels[j]}.png")
plt.show()
random_seed = 1234
npr.seed(random_seed)
labels = ["PMF", "PMF (logs)"]
pmfs = [p_unique_birthdays, p_unique_birthdays_logs]
for j in range(2):
for k, n in [(30, 20), (60, 40)]:
do_stuff(k, n, pmfs[j])
输出的数字:
感谢任何ideas/advice/suggestions。
你是对的,这是一些奇数的原因。
更改此行:
total += (-1)**i * comb(m, i, exact=True) * ((m-i)/k)**n
对此:
total += (-1)**i * comb(m, i, exact=True) * ((m-i)**n)/(k**n)
出于某种原因,如果您强制执行不同的操作顺序,结果会很好。
您可能需要花更多时间来弄清楚如何修改您的 "log'd" 版本,但鉴于上述更改修复了一些问题,您可能只想完全放弃 "log'd" 版本。
希望对您有所帮助!
您可以使用内置的 decimal 模块来提高精度。
from decimal import *
getcontext().prec = 10000
def factorial(n):
res = Decimal(1)
for i in range(int(n)):
res = res * Decimal(i + 1)
return res
def binomial_coefficient(n, k):
return factorial(n) / factorial(k) / factorial(n - k)
def p_unique_birthdays(m, k, n):
m = Decimal(m)
k = Decimal(k)
n = Decimal(n)
total = Decimal(0)
for i in range(int(m) + 1):
total += Decimal((-1) ** i) * binomial_coefficient(m, i) * binomial_coefficient(k, m) * ((m - i) / k) ** n
return total
print(p_unique_birthdays(49, 365, 50))
上面的代码打印 0.11484925 与 http://www.wolframalpha.com/input/?i=sum+combination(49,x)combination(365,49)++(((49-x)%2F365)%5E50)+*+(-1)%5Ex,+x%3D0+to+49
相同