105贝叶斯方法数据分析实战--挑战者号的事故模拟

挑战者号的事故模拟

数据加载
1986 年 1 月 28 号,挑战者号起飞不久后,一个火箭推动器发生了爆炸。这次事故造成航天飞机上的 7 名成员全部死亡,并导致美国的第 25 次航天飞行计划胎死腹中。
这场事故的官方结论是,本次事故是由连接在火箭推进器上的,一个有缺陷的O型圈造成的。这种缺陷来自于设计的不合理,进而造成O型圈对外界环境非常敏感。数据显示之前的 24 次飞行中有 23 次的O型圈都是有缺陷的。但是,有时候由于 O 型线圈出现缺陷,但却未发生事故,进而导致专家们并没有重视这一缺陷。
有人推测这些 O 型圈发生变化的主要原因是因为外界的温度,我们将在本次实验中,利用贝叶斯推断来对这种说法进行判断和分析。
我们从 McLeish 和 Struthers 那里得到了,每次飞行实验的外界温度和是否发生事故的对照数据,下面让我们将这些数据下载到本地:

!wget -nc "https://labfile.oss.aliyuncs.com/courses/1520/challenger_data.csv"

接下来,让我们利用 Numpy 加载这张表:

import scipy.stats as stats
import numpy as np
from IPython.core.pylabtools import figsize
import matplotlib.pyplot as plt
# 从 hallenger_data.csv 中读取数据
# skip_header:跳过文件的开头第一行,即第一行文字
# usecols:我们需要的数据存在于表的第1列和第2列,因此我们只需将读入这两列数据
# delimiter=“,”:将读出的数据根据逗号切割成一列一列的数据
challenger_data = np.genfromtxt("challenger_data.csv", skip_header=1,
                                usecols=[1, 2], missing_values="NA",
                                delimiter=",")

# 删除数据中的空值
challenger_data = challenger_data[~np.isnan(challenger_data[:, 1])]
temperature = challenger_data[:, 0]
D = challenger_data[:, 1]
print(challenger_data)

从上表中可以看出,第一列代表的是外界的温度(华氏温度),第二列代表的是该次飞行是否发生事故(1:代表了发生事故。0:代表未发生事故)。
让我们根据这些数据画出温度与事故之间的关系图:

plt.scatter(challenger_data[:, 0], challenger_data[:, 1], s=75, color="k",
            alpha=0.5)
plt.yticks([0, 1])
plt.ylabel("Damage Incident?")
plt.xlabel("Outside temperature (Fahrenheit)")
plt.title("Defects of the Space Shuttle O-Rings vs temperature")

从图中可以清晰地看出:随着室外温度的下降,发生事故的概率变得更高。并且,我们可以从图中看到,针对于什么温度下会发生事故,什么温度上不会发生事故并没有一个严格的界限。因此,本实验的目的就是回答下面这个问题:
在温度t时,事故发生的概率p为多少?
逻辑函数
现在我们假设概率与温度间存在的函数关系为p(t)。我们要求该函数的值在 0 和 1 之间(因为概率最大为 1 ,最小为 0),且随着温度的升高,它的取值也会从 1 向 0 变化。
当然这样的函数其实有很多,接下来,我们会介绍一种最受欢迎的函数:逻辑函数。

image.png

其中t代表温度,β 是该模型中的一个参数。让我们用先 Python 实现这个函数:

# 实现上面函数
def logistic(x, beta):
    return 1.0 / (1.0 + np.exp(np.dot(beta, x)))


# 测试
logistic(1, 2)

那么,为什么说上面这个函数就满足上面的条件呢(即所有值的界限都在 0-1 之间)?让我们画出逻辑函数的图,观察它的上下界。现在我们先随便给β 取几个值。代码如下:

# 取 100 个点传入函数,得到每个
figsize(12, 3)

x = np.linspace(-4, 4, 100)


# 画出 beta=1的逻辑函数图
plt.plot(x, logistic(x, 1), label=r"$\beta = 1$")

# 画出 beta=3的逻辑函数图
plt.plot(x, logistic(x, 3), label=r"$\beta = 3$")

# 画出 beta=-5的逻辑函数图
plt.plot(x, logistic(x, -5), label=r"$\beta = -5$")
plt.legend()
plt.legend(loc="lower left")

从图可以看到,我们已经解决了界限为 0-1 的问题。但是,我们也可以看到,逻辑函数的值只在 0 附近发生变化,超过 -1,+1 之后,值就开始趋于平稳。而我们从温度与事故的对照表中可以发现。事故是否发生的转折点,大致是在 65-70 华氏度之间。因此我们主要是需要逻辑函数去求得是温度在 65~70 之间,发生事故的概率。


image.png

下面我们展示一下,不同的 \alphaα 对函数的影响:

def logistic(x, beta, alpha=0):
    return 1.0 / (1.0 + np.exp(np.dot(beta, x) + alpha))


x = np.linspace(-4, 4, 100)

plt.plot(x, logistic(x, 1), label=r"$\beta = 1$", ls="--", lw=1)
plt.plot(x, logistic(x, 3), label=r"$\beta = 3$", ls="--", lw=1)
plt.plot(x, logistic(x, -5), label=r"$\beta = -5$", ls="--", lw=1)

plt.plot(x, logistic(x, 1, 1), label=r"$\beta = 1, \alpha = 1$",
         color="#348ABD")
plt.plot(x, logistic(x, 3, -2), label=r"$\beta = 3, \alpha = -2$",
         color="#A60628")
plt.plot(x, logistic(x, -5, 7), label=r"$\beta = -5, \alpha = 7$",
         color="#7A68A6")

plt.legend(loc="lower left")

从图中,我们可以看到给 \alphaα 赋予不同的值,逻辑曲线可以发生向左或者向右的偏移。
至此,让我总结一下接下来的任务:
我们已经找到了一个合适的函数模型(逻辑函数)来描述事故发生概率和温度之间的关系。现在我们的任务就是找到这个函数中的β 参数和α参数的最佳取值。

再将这个任务细分一下就是:
找到合适的先验概率分布来描述β,α。
将真实数据传入模型。
利用贝叶斯推断,推断出两个参数的后验概率。
得到精确的p(t)函数。

那么,接下来,我们就需要找一个合适的先验概率分布来描述β 和α 。虽然,这两个参数的取值没有限定,可以为正数,也可以为负数。但要求它在某个范围内变化很大。因此,我们自然而然的就会想到正态分布函数。
正态分布

image.png

接下来,让我们用图像的方式展示不同(μ,τ) 下的正态分布函数图像:

import scipy.stats as stats

# 设置三组参数,分别为:(-2,7),(0,1),(3,2.8)
mu = (-2, 0, 3)
tau = (.7, 1, 2.8)
colors = ["#348ABD", "#A60628", "#7A68A6"]
parameters = zip(mu, tau, colors)
# 加载正态分布函数,并画出图像
nor = stats.norm
x = np.linspace(-8, 7, 150)

for _mu, _tau, _color in parameters:
    plt.plot(x, nor.pdf(x, _mu, scale=1./_tau),
             label="$\mu = %d,\;\\tau = %.1f$" % (_mu, _tau), color=_color)
    plt.fill_between(x, nor.pdf(x, _mu, scale=1./_tau), color=_color,
                     alpha=.33)

plt.legend(loc="upper right")
plt.xlabel("$x$")
plt.ylabel("density function at $x$")
plt.title("Probability distribution of three different Normal random variables")

由图可以看出τ 越小,分布越宽(即我们越不能确定,因为概率相差不大)。τ 越大,分布越窄,我们越能确定。一个正态随机变量可以为任何实数,但是它的取值一般都会接近μ (从图中可以看出,μ 对应的概率最大)。

image.png

当然,你不需要记下所有分布函数的具体公式,你只需要记住每种分布函数的分布图像即可。
后验概率的求取
让我们回到模拟挑战者航天飞机的实验中,现在我们需要做的第一步就是,将β,α的先验概率设置为正态分布:

import pymc3 as pm
import theano.tensor as tt

with pm.Model() as model:
    # 定义 p(t) 函数所需要的参数,先将这两个参数的初始设置为 0
    beta = pm.Normal("beta", mu=0, tau=0.001, testval=0)
    alpha = pm.Normal("alpha", mu=0, tau=0.001, testval=0)
    # 定义 p(t) 函数:即一个逻辑函数
    p = pm.Deterministic("p", 1.0/(1. + tt.exp(beta*temperature + alpha)))
model

注意,在代码中,我们将α 和β 取值设置为 0 的目的是给p设置一个合理的初始值。因为,如果将这两个参数设置过大的话,p 值的图像会非常陡峭。换句话说,就是 p 值会很快的从 1 变化到 0。这样,不利于后面将 p 放入伯努利模型,因为伯努利模型不太接受 0 或者 1 这样非常极端的概率值。
现在我们有了概率值,但是怎样将它们和我们观测到的数据联系起来呢?我们的结果和我们得到的概率值之间又存在什么样的关系呢?还记得我们的结果的取值吗?如果本次飞行发生事故,则结果为 1。未发生,则结果为 0 。这样就表示,我们的结果只为 0 或者 1。那么我们之前学过的模型中,有哪一个输出是 0 或 1 的呢?没错,就是伯努利分布函数。


image.png
with model:
    # 将 D_i 与真实数据联系在一起
    observed = pm.Bernoulli("bernoulli_obs", p, observed=D)

    # 开始训练模型,寻找满足真实数据的最佳参数
    # 下面代码的具体内容,将在后面实验进行解释
    start = pm.find_MAP()
    step = pm.Metropolis()
    trace = pm.sample(60000, step=step, start=start)
    burned_trace = trace[50000::2]

现在我们已经训练好了模型,这个模型已经得到了较佳的两个随机型变量:α 和 β。其实模型训练,就是为了找这两个变量的最佳值,因为其他值都是确定型变量(即可以通过上面两个参数,计算得到的变量)。
老规矩,在训练完模型后,让我们提取参数的样本,将这两个参数的分布图画出来:

alpha_samples = burned_trace["alpha"][:, None]  # best to make them 1d
beta_samples = burned_trace["beta"][:, None]

figsize(12.5, 6)

# 统计每个值出现的次数,画出频率图
plt.subplot(211)
plt.title(r"Posterior distributions of the variables $\alpha, \beta$")
plt.hist(beta_samples, histtype='stepfilled', bins=35, alpha=0.85,
         label=r"posterior of $\beta$", color="#7A68A6", normed=True)
plt.legend()

plt.subplot(212)
plt.hist(alpha_samples, histtype='stepfilled', bins=35, alpha=0.85,
         label=r"posterior of $\alpha$", color="#A60628", normed=True)
plt.legend()

从图中可以看出,β 的后验概率的分布都很接近于 0 ,都是零点几。这可能表明,β 对事故概率没有影响。而通过 α 的后验分布,我们可以明显的看出,α 对事故发生的概率影响极大。
接下来,让我们对α 和 β 的样本进行抽样取值,来计算在 challenger_data 中的所有温度下的事故发生概率。由于有多组参数取值,因此每个温度下,我们都会计算出多个事故发生率。然后我们需要对这些值取平均数,得到一个温度下的平均事故发生概率。

# 得到温度取值
t = np.linspace(temperature.min() - 5, temperature.max()+5, 50)[:, None]

# 得到每种温度下的所有事故发生概率的取值
p_t = logistic(t.T, beta_samples, alpha_samples)

# 对同一温度下的所有事故发生概率取平均值,得到一个温度下的一个1可能取值
mean_prob_t = p_t.mean(axis=0)
p_t.shape, mean_prob_t.shape

上面,我们得到了 10000 组各个温度下的事故发生率 p_t 并把同一个温度下的所有发生率取平均值,得到平均发生率 mean_prob_t。
接下来,我们画出不同温度下的平均事故发生率,并随机从 10000 组发生率中抽取两组,展示在图中:

figsize(12.5, 4)
# 平均取值
plt.plot(t, mean_prob_t, lw=3, label="average posterior \nprobability \
of defect")

# 这里只画了两条可能取值
plt.plot(t, p_t[0, :], ls="--", label="realization from posterior")
plt.plot(t, p_t[-2, :], ls="--", label="realization from posterior")
plt.scatter(temperature, D, color="k", s=50, alpha=0.5)
plt.title("Posterior expected value of probability of defect; \
plus realizations")
plt.legend(loc="lower left")
plt.ylim(-0.1, 1.1)
plt.xlim(t.min(), t.max())
plt.ylabel("probability")
plt.xlabel("temperature")

我们可以从上上个代码段的结果中看到,虽然每个温度有 10000 个事故发生率,但是它们有些相差不大,有些相差很大。相差不大的,我们取这些值的平均值来描述该温度的事故发生率即可。但是,对于在某个温度下,事故发生率的取值相差很大时,我们就无法确定真正的事故发生率了。
那么在哪一个温度时,我们对事故发生的概率最不确定呢?下面,让我们画出期望值的曲线和每个点对应的 95% 的置信区间(CI)。

from scipy.stats.mstats import mquantiles

# vectorized bottom and top 2.5% quantiles for "confidence interval"
qs = mquantiles(p_t, [0.025, 0.975], axis=0)
plt.fill_between(t[:, 0], *qs, alpha=0.7,
                 color="#7A68A6")

plt.plot(t[:, 0], qs[0], label="95% CI", color="#7A68A6", alpha=0.7)

plt.plot(t, mean_prob_t, lw=1, ls="--", color="k",
         label="average posterior \nprobability of defect")

plt.xlim(t.min(), t.max())
plt.ylim(-0.02, 1.02)
plt.legend(loc="lower left")
plt.scatter(temperature, D, color="k", s=50, alpha=0.5)
plt.xlabel("temp, $t$")

plt.ylabel("probability estimate")
plt.title("Posterior probability estimates given temp. $t$")

95% CI:又名 95% 置信区间,即上图中紫色显示的部分。对于每个温度值,它都包含了 95% 的分布。举个例子,从上图中,我们可以看到,在 65 度时, 有 95% 的把握说该温度下的事故发生率在 0.25 ~ 0.75 之间。即有 95% 的数据取值在 0.25~0.75。
换句话说,我们可以从图中看到,在 6060 度左右, CI 的值分散的很快,而过了 7070 度,CI 的值又重新聚拢。也就是说这个区间内的事故发生率的波动很大,我们还需要更多的数据进行实验。
当然,置信区间还有一个用处就是合理性。当我们在向科学家汇报我们的估计值时,不是简单的告诉他们在该温度下,事故发生的概率是几点几,而是告诉他们事故发生概率的分布,即后验分布到底有多宽。只有知道这个,才能让科学家真正的对这个部件缺陷产生重视。
挑战者号事故当天的情景模拟
我们通过翻阅文献获取到,挑战者号事故发生当天的室外温度为 31 华氏度。那么在这个温度下,事故发生的概率是多少呢?
让我们利用之前计算得到的模型,对事故发生率进行估算:

figsize(12.5, 2.5)

prob_31 = logistic(31, beta_samples, alpha_samples)

# 设置 x 轴的范围
plt.xlim(0.995, 1)
plt.hist(prob_31, bins=1000, normed=True, histtype='stepfilled')
plt.title("Posterior distribution of probability of defect, given $t = 31$")
plt.xlabel("probability of defect occurring in O-ring")

从图中我们也可以发现,当天挑战者发生事故的概率极高,如果能够在发射之前进行一下贝叶斯估计,或许悲剧也就不会发生了。

度量模型的拟合优度

虽然我们成功预测出来 28 号当天事故的发生,但是如何证明我们模型对于其他日子也适用呢?换句话说,如果我选择的函数为p(t)=1,即针对于所有温度,都会发生事故。将这个模型带入 28 号的温度,也能够得到得到事故会发生的结论。那么,如何说明我们建立的逻辑函数模型更能够表达数据呢?这些都说明了,我们有必要度量模型的拟合优度,或者说度量模型对观测值的拟合的好坏程度。
那么,我们应该怎么评价模型拟合的好不好呢?一种方法是利用模型拟合出的人工数据于真实数据相对比。如果模拟出来的数据和真实的数据相似,则说明我们的模型较好。
其实在前面实验中,我们已经利用我们建立的短信接收模型来人工模拟了新的短信接收数据。同样,针对现在这个例子,我们需要通过得到的后验概率,来产生相似的数据集。
幸运的是,在贝叶斯框架下,这个很容易实现。我们只需要创建了一个跟我们存储观察值的变量一样类型的随机型变量即可。


image.png

让我们现在来重新编写模型,把模拟数据所需要的随机变量也加入模型中,并对该模型进行重新训练:

N = 10000
with pm.Model() as model:
    # 参数的定义
    beta = pm.Normal("beta", mu=0, tau=0.001, testval=0)
    alpha = pm.Normal("alpha", mu=0, tau=0.001, testval=0)
    p = pm.Deterministic("p", 1.0/(1. + tt.exp(beta*temperature + alpha)))
    observed = pm.Bernoulli("bernoulli_obs", p, observed=D)
    # 和1之前代码的唯一不同
    simulated = pm.Bernoulli("bernoulli_sim", p, shape=p.tag.test_value.shape)
    # 模型训练
    step = pm.Metropolis(vars=[p])
    trace = pm.sample(N, step=step)

接下来,我们将模拟的数据,采用同种方式,展示出来:

figsize(12.5, 5)

simulations = trace["bernoulli_sim"]
print(simulations.shape)

plt.title("Simulated dataset using posterior parameters")
figsize(12.5, 6)
for i in range(4):
    ax = plt.subplot(4, 1, i+1)
    plt.scatter(temperature, simulations[1000*i, :], color="k",
                s=50, alpha=0.6)

上面都是模拟的数据,但是它们三个的散点图是不一样的,因为它们的底层数据不一样。虽然这些数据都来自于同一个底层模型,但是由于改变了为随机型变量,因此,它们产生的值是随机的。这就是我们常说的“统计上一致,但样子随机”。
当然,我们也可以从上图发现,这些数据集从统计上看起来是和我们的原始数据非常相似的,这样即证明了我们的模型是很好的。
由于“好”是一个比较主观的词,而结果应该是相对于其他模型来说的。因此,接下来我们就会介绍分离图的概念,并用它来评价模型的好坏。

分离图

分离图是一种用于逻辑回归拟合的新型数据可视化方法。它可以让用户用一种图形化的方法来对比不同的模型并从中选出最合适的。
让我们先来介绍一个模型的分离图的画法。对于分离图的大部分技术细节,可以查找 原始论文。
首先,我们需要计算出,在某个温度下,后验模拟产生的 1 的次数所占比例,记作 P(Defect=1|t)。这样我们就能得到,每个温度下发生事故的后验概率。
其实这个占比就是模拟数据的平均值(因为数据只有0 或者 1)。因此,占比的求取代码如下:

posterior_probability = simulations.mean(axis=0)
print("posterior prob of defect | realized defect ")
for i in range(len(D)):
    # 第一个是模拟出来的值,第二个是每个温度下的事故发生的真实概率
    print("%.2f                     |   %d" % (posterior_probability[i], D[i]))

接下来,我们根据后验概率(即第一列的数据),对每一行进行排序,代码如下:

ix = np.argsort(posterior_probability)
print("probb | defect ")
for i in range(len(D)):
    print("%.2f  |   %d" % (posterior_probability[ix[i]], D[ix[i]]))

接下来我们需要编写分离图的函数,该函数由 Cameron Davidson-Pilon 提供,我们无需自行编写,你只需运行即可。

# separation plot
# Author: Cameron Davidson-Pilon,2013
# see http://mdwardlab.com/sites/default/files/GreenhillWardSacks.pdf


import matplotlib.pyplot as plt
import numpy as np


def separation_plot(p, y, **kwargs):
    """
    See http://mdwardlab.com/sites/default/files/GreenhillWardSacks.pdf  
    p:n 个模型,每个模型存在 M 个概率,形成一个 n*M 的矩阵
    y:表示结果,即 0-1变量
    """
    n = p.shape[0]
    try:
        M = p.shape[1]
    except:
        p = p.reshape(n, 1)
        M = p.shape[1]
    colors_bmh = np.array(["#eeeeee", "#348ABD"])
    fig = plt.figure()
    for i in range(M):
        ax = fig.add_subplot(M, 1, i+1)
        ix = np.argsort(p[:, i])
        # plot the different bars
        bars = ax.bar(np.arange(n), np.ones(n), width=1.,
                      color=colors_bmh[y[ix].astype(int)],
                      edgecolor='none')
        ax.plot(np.arange(n+1), np.append(p[ix, i], p[ix, i][-1]), "k",
                linewidth=1., drawstyle="steps-post")
        # create expected value bar.
        ax.vlines([(1-p[ix, i]).sum()], [0], [1])
        plt.xlim(0, n)

    plt.tight_layout()
    return

如上所示, separation_plot 函数接受两个参数,一个模型的后验概率(即预测值),一个是结果的真实值。现在让我们把本模型的输出传入其中。

figsize(11., 1.5)
separation_plot(posterior_probability, D)

其中蛇形线表示排序后的后验概率,蓝色柱子表示是否发生了事故。空的地方表示没有发生事故。从上图中我们可以看到,随着概率的升高,事故发生的次数也越来越多。又由于右手边的发生概率很大,因此所有的事故都应该聚集在右手边。这也是右手边的蓝色矩形很宽的原因。


image.png
figsize(11., 1.25)

# 我们的模型
separation_plot(posterior_probability, D)
plt.title("Temperature-dependent model")

# 完美模型
# 也就是实际的值
p = D
separation_plot(p, D)
plt.title("Perfect model")

# 随机模型,通过随机数,产生概率
p = np.random.rand(23)
separation_plot(p, D)
plt.title("Random model")

# 常数模型
constant_prob = 7./23*np.ones(23)
separation_plot(constant_prob, D)
plt.title("Constant-prediction model")

从上图中可以看到,随机模型和我们定义的模型都存在着概率增加的过程,但是随机模型的右手边并没有出现事故集合的聚集。虽然常数模型的概率未发生变化,但是结果显然有密有疏,因此也不合理。而完美模型未出现概率线的变化,导致它对其他温度的适应性不强。
因此,我们可以通过上面的分离图判断出我们建立的模型比这些模型“好”。

你可能感兴趣的:(105贝叶斯方法数据分析实战--挑战者号的事故模拟)