大数定律
大数定律的概念
由于已经定义了Zi只能取c1或c2。
接下来,让我们将大数定律套用到泊松变量中,观察其收敛图像。
实例:随机变量的收敛
假设我们有三组由同一个泊松分布函数产生的随机变量,接下来,让我们先产生这三组随机变量:
import numpy as np
from IPython.core.pylabtools import figsize
import matplotlib.pyplot as plt
%matplotlib inline
figsize(12.5, 5)
# 假设每组都有 100000 个样本事例
sample_size = 100000
expected_value = lambda_ = 4.5
# 定义泊松分布函数
poi = np.random.poisson
# 定义出入泊松分布函数的x,采用稀释的手法,从一个大集合中的第1,101,201,301、、、这样每次增加100的取
# 一共去 sample_size 个
N_samples = range(1, sample_size, 100)
# 产生三组随机变量
samples = []
for k in range(3):
samples.append(poi(lambda_, sample_size))
samples
接下来,让我们计算出每一组序列的前 i 个的平均值,并以图像的方式进行展示:
for k in range(3):
# 求出第 k 组序列的前 i 个值的平均值
# 并且形成一个新的序列
# 根据大数定律,这里我们把计算出的平均值,当做期望
partial_average = [samples[k][:i].mean() for i in N_samples]
plt.plot(N_samples, partial_average, lw=1.5, label="average \
of $n$ samples; seq. %d" % k)
# 画出实际的期望
plt.plot(N_samples, expected_value*np.ones_like(partial_average),
ls="--", label="true expected value", c="k")
plt.ylim(4.35, 4.65)
plt.title("Convergence of the average of \n random variables to its \
expected value")
plt.ylabel("average of $n$ samples")
plt.xlabel("# of samples, $n$")
plt.legend()
从上图可以看出,黑色虚线是分布函数的实际期望,即λ 的值,4.5。蓝、橙、绿三条曲线分别为通过三组变量估计得到的期望值,其中横坐标为第 n 个数,纵坐标为 前 n 个数的平均值(用以估计期望)。
从图中可以看到,当n很小的时候,样本均值的变动会很大。但是,随着样本量的增多,三组变量都向着 4.5 水平线靠近,且在很小的幅度内进行摆动。数学家和统计学家习惯把这种“摆动”称之为收敛。
说到收敛,那么上面这个例子的收敛是快还是慢呢?模型收敛到期望值的速度应该怎么求呢?
求上面这个期望值,我们也需要用到 大数定律。
接下来,让我们根据上面式子,计算收敛状态 Dn(由于选择的样本很多,因此这里需要运行 1-2 min):
# 定义一共 250 组样本集合
N_Y = 250
# 定义每组样本共 50000
# 从np的泊松数据集中取,每隔2500 取一次
N_array = np.arange(1000, 50000, 2500)
# 定义装 Dn 的数据集合
D_N_results = np.zeros(len(N_array))
# 定义 Z(i) 的期望
lambda_ = 4.5
expected_value = lambda_ # 因为 X ~ Poi(lambda) , E[ X ] = lambda
def D_N(n):
"""
该函数为上面式子的代码实现
"""
# 计算 N_Y 组样本集合
Z = poi(lambda_, (n, N_Y))
# 计算前n个样本的,所有数据的平均值
average_Z = Z.mean(axis=0)
# 得到 dn
return np.sqrt(((average_Z - expected_value)**2).mean())
for i, n in enumerate(N_array):
D_N_results[i] = D_N(n)
D_N_results
正如我们预期的,随着 N 的增大,样本均值与实际值间的距离的期望越来越小。接下来,让我们把这些数据呈现到图像中,并根据图像的斜率判断函数速度的变化。
plt.xlabel("$N$")
plt.ylabel("expected squared-distance from true value")
plt.plot(N_array, D_N_results, lw=3,
label="expected distance between\n\
expected value and \naverage of $N$ random variables.")
plt.legend()
plt.title("How 'fast' is the sample average converging? ")
蓝色的线表示 Dn 的值的变化。
从上图可以明显的看出D(n)的斜率在逐渐减小,收敛速度在逐渐变小。
当然,由于我们已经可以很直观的,从上面的距离图中了解速度的变化了,因此这里的计算就不再做赘述。
但是只有在N足够大的时候,大数定律才成立,然而数据量并非总是足够大的。如果任意地运用这一定律,不管它有多有用,都有可能会导致愚蠢的错误。下面这个例子将阐述这一点。
例子: Reddit 网站的评论排序
其实每个人都隐式地知道大数定律的有效性和无效性,并且在决策的时候会下意识地使用到这个原理。你可以想想网上的商品评论,如果一个上面被打了五星,但只有一个评论者,你会相信吗?如果有两个或者三个呢?我想大多人会说是不相信。这其实表明的就是,我们隐式的知道,评论人数很少的时候,评分的均值并不能很好的反映产品的真实价值。
因此,针对较少的评论,如果我们直接对他们的评分取平均再排序的话是很有问题的。
其实,很多人都意识到了,无论是书籍、视频还是网上评论,根据评分对搜索结果进行排序的效果其实是很差的。通常,看起来最靠前的评论(即给该视频打分最高的评论),一般都是某些狂热粉丝打出来的。而真正高品质的评论,反而被藏在了后面几页,且分数都是像劣质商品一般的 4.8分(10分制)。
这里我们拿国际上很有名的 Reddit 网站进行举例。该网站上有很多的趣事新闻。而最有趣的在于是每条新闻下的评论。“Redditors”(该网站用户的名称),可以对评论进行投票(反对或赞成)。Reddit 网站会从中选出人气最高的评论,即最佳评论。换做是你,你会判断哪一条评论最佳呢?
当然,其实也有很多方法可以实现这一点,如下所示:
热度: 认为点赞数越多的评论越好。但问题是,可能这条评论有几百个赞,但是也有上千个反对。那么虽然这条评论热度高,但是它具有很大争议,就不适合作为最佳。
差数:即点赞数减去反对数。这解决了上一个指标的问题,但是不能解决评论的时间属性所导致的问题。因为一个评论可以在数小时之前才发出,但是这个评论的差数疯涨。那么尽管这条评论的差数没有一周前的老评论所积累的点赞数多,差数大,但也应当有机会获得最佳的称号。
时间调节:即差数除以评论寿命。这样就得到了每秒上涨的差数,就能解决上一个问题了。但是这种方法也很容易找到一个反例,比如一个前一秒发布的评论,只需要一票。就能击败 100 秒前发布,现在拥有 90 票的评论了。
好评率:即赞成票除以总票数。这样就能解决时间的问题。只要新的评论有足够高的好评率,那么他们也可以与老评论一较高下。但是这也有一个问题,如果一条评论它只有 1 个赞成票(好评率100%),那么它就会胜过有 999 个赞和一个反对的评论(好评率 99.9%)。但是,显然后者更有可能比前者更优质。
其实好评率的评价方式是非常好的,只是我们需要得到的是真实的好评率,而非观测到的。真实的好评率是一个隐藏的值,而我们能观测到的只有赞成和反对的次数。根据大数定律,我们可以断定,有 999 次赞同和 1 次反对的评论的好评率更有可能接近于 1。而如果只有 1 次赞同 0 次反对的评论,那就没那么确定了。
因此我们需要获得好评率的先验知识,获得先验知识的方法就是观测好评率的历史分布。然后,再利用贝叶斯定理对好评率的后验分布进行推断,进而得到真正的好评率。
为了解贝叶斯推断的好评率的历史分布,我们抓取了 Reddit 上某张图片下的评论,并保存到了蓝桥云课中,让我们先来加载这个数据集合:
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1520/lab9_data.csv"
接下来,让我们利用 Pandas 加载数据并观察:
import numpy as np
import pandas as pd
data = pd.read_csv("lab9_data.csv", header=0)
# 输出data的部分数据
data.head()
通过上表可以看出,data 主要有赞成列、反对列、评论的内容列三列组成。为了方便后面处理数据,让我们先把这些数据分为两部分:
votes = data.loc[:, ['赞', '反对']].values
contents = data.loc[:, ['contents']].values
votes.shape, contents.shape
如果已知真实的好评率p和投票数N,则赞同票的次数分布将类似于参数为p和N的二项分布。因此,我们可以构建一个函数来对单条评论的好评率 (赞成数/反对数) 进行概率值 p 的贝叶斯推断。
import pymc3 as pm
def posterior_upvote_ratio(upvotes, downvotes, samples=20000):
"""
该函数用于求好评率的后验分布,传入参数:好评数和反对数,还有利用马尔科夫链蒙特卡洛算法,所寻找的样本个数
"""
N = upvotes + downvotes
with pm.Model() as model:
# 好评率的先验服从均匀分布
upvote_ratio = pm.Uniform("upvote_ratio", 0, 1)
# 通过好评率和总数得到好评数,然后再将好评数和观测到的好评数 upvotes结合
observations = pm.Binomial(
"obs", N, upvote_ratio, observed=upvotes)
# 训练,得到样本
trace = pm.sample(samples, step=pm.Metropolis())
# 舍去前1/4的预热样本
burned_trace = trace[int(samples/4):]
return burned_trace["upvote_ratio"]
为了方便展示,且节约时间,这里我们只计算这些评论中的四条评论的正确率即可,其他的方法类似(因为需要训练四个后验分布,因此,本段代码需要运行 5~6 min,请耐心等待)。
# 随机从98中选择4条评论
submissions = np.random.randint(98, size=4)
posteriors = []
for i in range(len(submissions)):
j = submissions[i]
posteriors.append(posterior_upvote_ratio(votes[j, 0], votes[j, 1]))
# 得到后验分布
posteriors
最后,让我们对这四条评论的后验分布进行可视化:
figsize(11., 8)
# 设置四种颜色
colours = ["#348ABD", "#A60628", "#7A68A6", "#467821", "#CF4457"]
for i in range(len(submissions)):
plt.hist(posteriors[i], bins=10, normed=True, alpha=.9,
histtype="step", color=colours[i % 5], lw=3,
label='(%d up:%d down)\n%s...' % (votes[j, 0], votes[j, 1], contents[j][:50]))
plt.hist(posteriors[i], bins=10, normed=True, alpha=.2,
histtype="stepfilled", color=colours[i], lw=3, )
plt.legend(loc="upper left")
plt.xlim(0, 1)
plt.title("Posterior distributions of upvote ratios on different submissions")
从上图中,我们可以看到,某些分布很窄,其他分布表现为长尾(相对来说)。这正体现了每条评论的差异性和真实的好评率的不确定性。
排序
接下来就是对真实的好评率进行排序找到最佳评论。但是我们得到的是每条评论的分布,而分布是无法排序的,排序的只能是标量。
当然有很多方法能够从分布中提取标量值,用期望或均值来表示分布就是一种方法。但是,均值一定不是一个好办法,因为它没有考虑到分布的不决定性。
这里我们使用 95% 的最小可信值 (也就是找到所有样本中的至少 95% 的数据所在的范围),并且画出后验分布:
N = posteriors[0].shape[0]
lower_limits = []
for i in range(len(submissions)):
j = submissions[i]
# 只需要在画图时指定最小可信值 alpha 即可
plt.hist(posteriors[i], bins=20, normed=True, alpha=.9,
histtype="step", color=colours[i], lw=3,
label='(%d up:%d down)\n%s...' % (votes[j, 0], votes[j, 1], contents[j][:50]))
plt.hist(posteriors[i], bins=20, normed=True, alpha=.2,
histtype="stepfilled", color=colours[i], lw=3, )
# 求出最小可信值
v = np.sort(posteriors[i])[int(0.05*N)]
plt.vlines(v, 0, 10, color=colours[i], linestyles="--", linewidths=3)
lower_limits.append(v)
plt.legend(loc="upper left")
plt.legend(loc="upper left")
plt.title("Posterior distributions of upvote ratios on different submissions")
# 输出最小可信值
order = np.argsort(-np.array(lower_limits))
print(order, lower_limits)
上图中的垂直虚线表示的就是 95% 的最小可信值,最小可信值就是 95% 可信度下的范围的下界。根据我们的方法,最佳评论是最有可能得到高好评率的评论。因此,我们只需要找最小可信值中最接近于 1 的评论即可。
为何基于这个量的排序是个好主意呢?因为基于 95% 的最小可信值排序,是对最佳评论的保守估计。换句话说,即时在最差情况下,也能确保的正确率。这种排序思路可以保证下面两个特性:
给定两个好评率相同的评论时,可以选择出票数最多的作为最佳评论。
给定两个票数一样的评论,我们会选择好评数最多的。
实时性的优化
虽然上面的模型算法已经可以预测出每条评论的好评率的1后验分布,并进行排序。但是,我们可以很明显的看到,每一条评论的后验计算开销太大。而当我们计算完时,可能此时的数据又发生了变化。如果,我们可以使用公式直接计算每条评论的下界,就可以省去贝叶斯推断所消耗的时间。
让我们来编写这个函数,并且计算出每条评论的好评率最低下界:
def intervals(u, d):
a = 1. + u
b = 1. + d
# 计算被减数,即后验均值
mu = a/(a+b)
# 计算减数,即容错数
std_err = 1.65*np.sqrt((a*b)/((a+b)**2*(a+b+1.)))
return (mu, std_err)
posterior_mean, std_err = intervals(votes[:, 0], votes[:, 1])
lb = posterior_mean - std_err
lb
让我们对比排在前40的评论的后验均值和边界值,并进行可视化:
# 提取前40条正确率最高的评论的正确率和内容
order = np.argsort(-lb)
r_order = order[::-1][-40:]
ordered_contents = []
for i in order[:40]:
ordered_contents.append(contents[i])
# 对他们进行可视化
plt.errorbar(posterior_mean[r_order], np.arange(len(r_order)),
xerr=std_err[r_order], capsize=0, fmt="o",
color="#7A68A6")
plt.xlim(0.3, 1)
plt.yticks(np.arange(len(r_order)-1, -1, -1),
map(lambda x: x[:30][0].replace("\n", ""), ordered_contents))
你可以双击上面的图像,进而放大观察。上图中,我们对每个区间的左边界进行了排序(即排序最小可信值)。从图中我们可以看到,随着最小可信值的减小,均值不一定减小。这也是为什么,开始的时候我们说根据均值排序不是一个较佳的选择。
这样我们就能实时的求出不同时刻的评论的质量,向用户推荐最佳的评论,即实时的把最佳评论置顶。
补充说明