108贝叶斯方法数据分析实战--大数定律

大数定律

大数定律的概念

image.png

由于已经定义了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 水平线靠近,且在很小的幅度内进行摆动。数学家和统计学家习惯把这种“摆动”称之为收敛。
说到收敛,那么上面这个例子的收敛是快还是慢呢?模型收敛到期望值的速度应该怎么求呢?


image.png

求上面这个期望值,我们也需要用到 大数定律。


image.png

接下来,让我们根据上面式子,计算收敛状态 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)的斜率在逐渐减小,收敛速度在逐渐变小。


image.png

当然,由于我们已经可以很直观的,从上面的距离图中了解速度的变化了,因此这里的计算就不再做赘述。
但是只有在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后验分布,并进行排序。但是,我们可以很明显的看到,每一条评论的后验计算开销太大。而当我们计算完时,可能此时的数据又发生了变化。如果,我们可以使用公式直接计算每条评论的下界,就可以省去贝叶斯推断所消耗的时间。

image.png

image.png

让我们来编写这个函数,并且计算出每条评论的好评率最低下界:

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))

你可以双击上面的图像,进而放大观察。上图中,我们对每个区间的左边界进行了排序(即排序最小可信值)。从图中我们可以看到,随着最小可信值的减小,均值不一定减小。这也是为什么,开始的时候我们说根据均值排序不是一个较佳的选择。
这样我们就能实时的求出不同时刻的评论的质量,向用户推荐最佳的评论,即实时的把最佳评论置顶。
补充说明

image.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容