[译] 第三章 MCMC

黑客级概率程序设计和贝叶斯方法

揭开MCMC的神秘面纱

前面两章对读者隐藏了PyMC的内部机制,也就是Markov chain Monte Carlo(MCMC)。我们引入本章目的有三。第一点,任何关于贝叶斯推断的书籍肯定会讨论MCMC。我无法抗拒此点。要抱怨就去找那些统计学家吧。:)第二点,知道MCMC的过程会给你关于算法收敛的理解。(收敛什么?等会我们再介绍。)第三点,我们会弄清楚为何我们可以从后验分布中产生的上千个样本作为最终的结果,而这些刚开始我们会觉得相当困惑的。

贝叶斯理论地貌(landscape)

landscape 不知道怎么翻译,太 tm 纠结了。

当我们开始一个包含N个未知量的贝叶斯推断问题,我们隐式地创建了一个N维空间来包含先验分布。与这个空间关联的是一个额外的维度,这个位于空间之上的东西被称作表面surface或者曲线curve,该维度也反应了一个特定点的先验概率。空间上的表面由我们的先验分布定义。例如,若我们有两个未知量p_1p_2,两者的先验分布都是Uniform(0, 5),那么创建出来的空间就是一个长度为5的正方形,而表面就是一个扁平的平面,位于这个正方形纸上,代表了每个点。

%matplotlib inline
import scipy.stats as stats
from IPython.core.pylabtools import figsize
import numpy as np
figsize(12.5, 4)

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

jet = plt.cm.jet
fig = plt.figure()
x = y = np.linspace(0, 5, 100)
X, Y = np.meshgrid(x, y)

plt.subplot(121)
uni_x = stats.uniform.pdf(x, loc=0, scale=5)
uni_y = stats.uniform.pdf(y, loc=0, scale=5)
M = np.dot(uni_x[:, None], uni_y[None, :])
im = plt.imshow(M, interpolation='none', origin='lower',
                cmap=jet, vmax=1, vmin=-.15, extent=(0, 5, 0, 5))

plt.xlim(0, 5)
plt.ylim(0, 5)
plt.title("Landscape formed by Uniform priors.")

ax = fig.add_subplot(122, projection='3d')
ax.plot_surface(X, Y, M, cmap=plt.cm.jet, vmax=1, vmin=-.15)
ax.view_init(azim=390)
plt.title("Uniform prior landscape; alternate view");

Uniform prior.png

换个例子,如果两个先验分布是Exp(3)Exp(10),那么空间是在2-D平面上的所有正数,并且由两个先验分布产生的表面看起来就像是瀑布一样从(0, 0)开始,流经所有的正数。

下面的图将这个场景图形化出来了。颜色越是深红,先验概率就分配得越高。相反地,越是深蓝则表示分配的概率越小。

figsize(12.5, 5)
fig = plt.figure()
plt.subplot(121)

exp_x = stats.expon.pdf(x, scale=3)
exp_y = stats.expon.pdf(x, scale=10)
M = np.dot(exp_x[:, None], exp_y[None, :])
CS = plt.contour(X, Y, M)
im = plt.imshow(M, interpolation='none', origin='lower',
                cmap=jet, extent=(0, 5, 0, 5))
#plt.xlabel("prior on $p_1$")
#plt.ylabel("prior on $p_2$")
plt.title("$Exp(3), Exp(10)$ prior landscape")

ax = fig.add_subplot(122, projection='3d')
ax.plot_surface(X, Y, M, cmap=jet)
ax.view_init(azim=390)
plt.title("$Exp(3), Exp(10)$ prior landscape; \nalternate view")

exponential prior.png

这些例子都是很简单的2D空间上的,我们的大脑还是能够理解这些表面的。实际应用中,由先验分布产生出来的空间和表面都是很高维的。

如果这些表面描述了关于未知量的先验分布,那么在我们获得了观测值X之后,这个空间将会发生怎样的变化呢?数据不会对空间产生变化,但是却会对空间的表面产生影响,拉伸或者压缩先验表面的特性来反应哪些地方是真的参数。更多的数据意味着更多的拉伸和压缩,原始的形状会和最终形成的形状有一定的差异,有时候会完全被颠覆掉。少的数据,则原始形状和最终结果的形状差不多。不管怎样,最终的表面描述了后验分布

不幸的是,我必须强调的是,高维空间不可能可视化。对2D空间,数据常常会上推表面来产生高的山峰。观察道的数据在某些区域上推后验概率的倾向(tendency)由先验概率分布检查,使得低先验概率意味着更多的阻抗(resistance)。因此,在上面的双指数先验例子中,山峰可能在靠近(0, 0)爆发得比靠近(5, 5)处爆发得更高,因为在(5, 5)处附近有更多的阻抗。这个峰值反映了后验概率,哪里真的参数更可能被发现。需要重视的是,如果先验已经被分配概率为0,那么没有后验概率被分配在那个地方。

假设上面提到的先验概率表示不同的参数\lambda给定的泊松分布。我们观察到一些数据点并可视化新的地貌(landscape):

# create the observed data

# sample size of data we observe, trying varying this (keep it less than 100 ;)
N = 1

# the true parameters, but of course we do not see these values...
lambda_1_true = 1
lambda_2_true = 3

#...we see the data generated, dependent on the above two values.
data = np.concatenate([
    stats.poisson.rvs(lambda_1_true, size=(N, 1)),
    stats.poisson.rvs(lambda_2_true, size=(N, 1))
], axis=1)
print "observed (2-dimensional,sample size = %d):" % N, data

# plotting details.
x = y = np.linspace(.01, 5, 100)
likelihood_x = np.array([stats.poisson.pmf(data[:, 0], _x)
                        for _x in x]).prod(axis=1)
likelihood_y = np.array([stats.poisson.pmf(data[:, 1], _y)
                        for _y in y]).prod(axis=1)
L = np.dot(likelihood_x[:, None], likelihood_y[None, :])

观察到(2D,样本大小=1):[[2 2]]

figsize(12.5, 12)
# matplotlib heavy lifting below, beware!
plt.subplot(221)
uni_x = stats.uniform.pdf(x, loc=0, scale=5)
uni_y = stats.uniform.pdf(x, loc=0, scale=5)
M = np.dot(uni_x[:, None], uni_y[None, :])
im = plt.imshow(M, interpolation='none', origin='lower',
                cmap=jet, vmax=1, vmin=-.15, extent=(0, 5, 0, 5))
plt.scatter(lambda_2_true, lambda_1_true, c="k", s=50, edgecolor="none")
plt.xlim(0, 5)
plt.ylim(0, 5)
plt.title("Landscape formed by Uniform priors on $p_1, p_2$.")

plt.subplot(223)
plt.contour(x, y, M * L)
im = plt.imshow(M * L, interpolation='none', origin='lower',
                cmap=jet, extent=(0, 5, 0, 5))
plt.title("Landscape warped by %d data observation;\n Uniform priors on $p_1, p_2$." % N)
plt.scatter(lambda_2_true, lambda_1_true, c="k", s=50, edgecolor="none")
plt.xlim(0, 5)
plt.ylim(0, 5)

plt.subplot(222)
exp_x = stats.expon.pdf(x, loc=0, scale=3)
exp_y = stats.expon.pdf(x, loc=0, scale=10)
M = np.dot(exp_x[:, None], exp_y[None, :])

plt.contour(x, y, M)
im = plt.imshow(M, interpolation='none', origin='lower',
                cmap=jet, extent=(0, 5, 0, 5))
plt.scatter(lambda_2_true, lambda_1_true, c="k", s=50, edgecolor="none")
plt.xlim(0, 5)
plt.ylim(0, 5)
plt.title("Landscape formed by Exponential priors on $p_1, p_2$.")

plt.subplot(224)
# This is the likelihood times prior, that results in the posterior.
plt.contour(x, y, M * L)
im = plt.imshow(M * L, interpolation='none', origin='lower',
                cmap=jet, extent=(0, 5, 0, 5))

plt.scatter(lambda_2_true, lambda_1_true, c="k", s=50, edgecolor="none")
plt.title("Landscape warped by %d data observation;\n Exponential priors on \
$p_1, p_2$." % N)
plt.xlim(0, 5)
plt.ylim(0, 5)

landscape.png


在左侧的变形后的地貌对应于Uniform(0, 5)先验,在右侧的就是对应于指数先验的地貌。注意后验地貌看起来是很不相同的,尽管他们的观测数据都是一样的。原因如下,注意指数先验地貌 ,右下角的那幅,只给了极少的后验权值给右上角的角落:这是因为先验分布没有在那里给出更多的权重。另一方面,均匀分布地貌则给了右上角更多的权重。、

还有要注意的是,那些对应于深红颜色的地方的那些最高的点,在指数情形下更加偏向在(0, 0)角落,这里是指数先验分布给出更多权重的地方。黑色的点告诉我们真实的参数,即使有一个样本点,山峰尝试包含真实参数。当然了,以样本大小为1进行的推断是相当简易的,选择这样一个小的样本作为例子只是为了更好地解释原理。

当我们尝试更大的样本集合,可以观测到山峰后验分布如何改变。

使用MCMC来探索地貌

我们应该去研究变形后的后验空间来寻找后验山峰。然而,我们不能够天真地直接对空间进行搜索:任何计算机科学家都会告诉你遍历N维空间是N的指数级困难的:空间的大小会在N增长时,发生快速的膨胀(维度诅咒)。我们如何找寻这些隐藏的山峰呢?MCMC背后的思想是执行一个空间上的智能搜索。这里的search指的是我们在找寻一个特定的点,这个可能b

MCMC从后验分布中返回样本而不是分布本身。MCMC执行了一种类似于询问“我发现的这个pebble来自我寻找的山峰的可能性有多大?”并且完成这个任务通过返回可以接受的pebble来重新创造原始的山峰。用MCMC和PyMC的语言就是,返回的pebble序列也就是那些样本,合起来称为轨迹(traces)。

当我说MCMC智能地搜索时,我实际上在说MCMC会有希望收敛到高后验概率的地方。MCMC通过探索周围的位置并移动到具有更高概率的地方。并且,收敛不是一个精确的空间,但是MCMC移动到一个空间中更加广阔的地方并且在那里进行随机游走,从那片地方中选出样本。

为何需要成千个样本?

首先,返回成千个样本给用户可能听起来不是一种可以高效地描述后验分布的方式。我将论证这种方式是特别高效的。考虑下面替代的方式:

  1. 返回一个对山峰范围一个数学公式,描述了带有任意峰顶和山谷的N维表面
  2. 返回地貌的峰顶,数学上可能和合理的东西往往是最高点,对应于最可能的对未知量的估计,忽略地貌的形状,而这个东西是我们之前认为是相当重要的可以确定后验置信度。

除了计算方面的原因,返回样本的很可能最强的原因是我们可以很容易地使用大数定律来解决难解问题。我们将这个讨论推后到下一章节。拥有了上千个样本,我们可以将他们放在一个直方图中重新构造后验分布的表面。

执行MCMC的算法

有一大群算法都可以完成MCMC。大多数算法粗略的过程如下:

  1. 从当前位置开始
  2. 提议一个到新位置的移动(找到靠近你的pebble)
  3. 基于这个位置对数据的信任度和先验分布(询问pebble是否可能来自山峰)
  4. 如果你接受:移动到新的位置,返回step 1;否则不移动,返回step 1.
  5. 在大量迭代后,返回所有接受的位置

这个方式让我们在宏观上可以移动到后验分布存在的区域,并且谨慎地收集到在这个过程中产生的样本。
如果MCMC算法当前的位置是在一个极其低的概率的地方,常常是当算法开始的时候(典型滴就是空间中随机获得一个位置),算法会移动到那些不太可能从后验分布中产生出来的位置,不过比周围的地方要略好。因此算法首先的移动不会对后验分布产生影响。
在上面的伪代码中,只有当前位置起到作用。我们可以将这种特性称为无记忆性,算法不关注如何到达当前位置的,只关心在当前位置上是什么。

其余的一些求解后验分布的近似解

除了MCMC,还有一些方法可以确定后验分布。Laplace近似使用简单的函数来近似后验分布。更加高级的方法叫做变分贝叶斯法。这三种方法都有各自的优缺点。本书中我们将只关注MCMC法。

That being said, my friend Imri Sofar likes to classify MCMC algorithms as either "they suck", or "they really suck". He classifies the particular flavour of MCMC used by PyMC as just sucks ;)

例子:使用mixture模型来进行无监督聚类

假设我们有下面的数据集:

figsize(12.5, 4)
data = np.loadtxt("data/mixture_data.csv", delimiter=",")

plt.hist(data, bins=20, color="k", histtype="stepfilled", alpha=0.8)
plt.title("Histogram of the dataset")
plt.ylim([0, None])
print data[:10], "..."

[ 115.85679142  152.26153716  178.87449059  162.93500815  107.02820697
  105.19141146  118.38288501  125.3769803   102.88054011  206.71326136] ...

example.png

这个数据暗示了什么?看起来是一个双模型,有两个峰值,一个接近120,另一个200。可能这个数据集合有两个聚类了。

这个数据集是上一章里面的数据生成技术的很好的例子。我们可以假设数据是如何被产生出来的。下面就是数据产生算法:

  1. 对每个数据点,以概率p选择cluster 1,否则就选择cluster 2.
  2. 然后按照1中选出来的cluster i,从对应的参数为\mu_i\sigma_i的正态分布中产生一个随机值
  3. 重复这个步骤,知道所有的点都被产生。

这个算法可以创建一个与真实观测数据集合相似的数据集合,所以我们选择它作为自己的模型。当然,我们并不知道p或者正态分布的参数。因此我们肯定要推断,或者学习这些未知量。

将正态分布表示为Nor_0Nor_1(从0开始就是pythonic而已)。两个都是不知道对应的均值和标准方差,分别表示为\mu_i\sigma_ii=0,1。特定的数据点可以从Nor_0或者Nor_1中产生,我们假设数据点以概率p分配给Nor_0

使用一个PyMC Categorical随机变量可以用来将数据点分给相应的cluster。它的参数是一个k长度的概率的数组,其中项的和应该为1,它的值是在0k-1之间的整数,取的概率就是对应于数组中的概率。在我们这里的情形k=2。先验地,我们并不知道分配给cluster 1的概率是多少,所以我们创建了一个0,1上的均匀分布变量对此进行建模。设为p。因此这个输入到Categorical变量的概率数组就是[p, 1-p]

import pymc as pm

p = pm.Uniform("p", 0, 1)

assignment = pm.Categorical("assignment", [p, 1 - p], size=data.shape[0])
print "prior assignment, with p = %.2f:" % p.value
print assignment.value[:10], "..."

观察一下上面的数据集,我们猜测这两个正态分布的标准方差都是不同的。为了保持对标准方差的未知,我们首先会将他们建模成0~100上的均匀分布。实际上我们在谈论的是\tau,也就是正态分布的准确性,但是可以从标准方差的角度来衡量这样的准确性。我们PyMC的代码需要转换我们的标准方差称准确性,他们有下面的关系:

\tau = \frac{1}{\sigma^2}

在PyMC中,我们可以使用下面这句代码完成转换:

\tau = 1.0 / pm.Uniform( "stds", 0, 100, size=2 ) ** 2

注意我们将size设置为2:我们将这两个\tau均建模称一个单个的PyMC变量。注意这里不包含两个\tau之间的关系。

我们同样需要指定cluster的中心的先验。中心实际上就是正态分布的参数\mu。他们的先验可以建模成一个正态分布。看一下数据,我猜测这两个中心分别是120和190,尽管我们对这两个肉眼得到的结果并不是很确定。因此我将\mu_0=120, \mu_1=190,\sigma_{0,1}=10(然后在PyMC变量中的\tau = frac{1}{\sigma^2} = 0.01

taus = 1.0 / pm.Uniform("stds", 0, 100, size=2) ** 2
centers = pm.Normal("centers", [120, 190], [0.01, 0.01], size=2)

"""
The below deterministic functions map an assignment, in this case 0 or 1,
to a set of parameters, located in the (1,2) arrays `taus` and `centers`.
"""

@pm.deterministic
def center_i(assignment=assignment, centers=centers):
    return centers[assignment]

@pm.deterministic
def tau_i(assignment=assignment, taus=taus):
    return taus[assignment]

print "Random assignments: ", assignment.value[:4], "..."
print "Assigned center: ", center_i.value[:4], "..."
print "Assigned precision: ", tau_i.value[:4], "..."

PyMC有一个MCMC的类,MCMC在PyMC的主命名空间中,实现了MCMC的搜索算法。我们通过下面的方法初始化一个Model实例:

mcmc=pm.MCMC( model )

MCMC搜索空间的方式是采样(迭代),其中迭代就是你期望算法执行的步数。我们尝试50000步:

mcmc=pm.MCMC(model)
mcmc.sample(50000)
_____________
[****************100%******************]  50000 of 50000 complete

下面我们画出来未知参数(中心,准确度和p)的path(或者轨迹)。我们这里可以使用MCMC对象中trace方法,这个接受的参数是PyMC变量名称。例如,mcmc.trace("centers")会检索到一个可以被索引的Trace对象(使用[:]或者.gettrace()来查询所有的路径或者fancy-indexing[1000:]

figsize(12.5, 9)
plt.subplot(311)
lw = 1
center_trace = mcmc.trace("centers")[:]

# for pretty colors later in the book.
colors = ["#348ABD", "#A60628"] \
if center_trace[-1, 0] > center_trace[-1, 1] \
    else ["#A60628", "#348ABD"]

plt.plot(center_trace[:, 0], label="trace of center 0", c=colors[0], lw=lw)
plt.plot(center_trace[:, 1], label="trace of center 1", c=colors[1], lw=lw)
plt.title("Traces of unknown parameters")
leg = plt.legend(loc="upper right")
leg.get_frame().set_alpha(0.7)

plt.subplot(312)
std_trace = mcmc.trace("stds")[:]
plt.plot(std_trace[:, 0], label="trace of standard deviation of cluster 0",
     c=colors[0], lw=lw)
plt.plot(std_trace[:, 1], label="trace of standard deviation of cluster 1",
     c=colors[1], lw=lw)
plt.legend(loc="upper left")

plt.subplot(313)
p_trace = mcmc.trace("p")[:]
plt.plot(p_trace, label="$p$: frequency of assignment to cluster 0",
     color="#467821", lw=lw)
plt.xlabel("Steps")
plt.ylim(0, 1)
plt.legend()

MCMC4.png

注意下面的特征:

  1. 路径收敛到,不是到一个单一的点,但是是到一个可能点的分布。这就是MCMC算法中的收敛性。
  2. 使用前面几千个点作为推断的依据不是很靠谱,因为他们与最终的分布没有太大的关联。所以最好的方法是将前面的样本去除掉。我们成这个阶段为燃烧阶段(burn-in period)
  3. 轨迹看起来像是在空间上进行随机游走,路径表现出来的关联性只是和前一个位置相关。这个有好处也有坏处:我们总是可以让当前位置仅和下一位置相关;而无法很好的探索整个空间。在本章最后我们会详细讨论。

为了达到最终的收敛,我们将这行更多的MCMC步数。在MCMC已经被调用后重新开始并不是指重新整个算法。在上面给出的伪代码中,只有当前的位置才是起作用的,这个被存放在PyMC变量value中。因此停止一个MCMC算法和观察它的整个过程,然后重新开始是OK的。value属性不会被重写。

我们将会进行100000次取样并且可视化这个过程:

mcmc.sample(100000)
[****************100%******************]  100000 of 100000 complete
figsize(12.5, 4)
center_trace = mcmc.trace("centers", chain=1)[:]
prev_center_trace = mcmc.trace("centers", chain=0)[:]

x = np.arange(50000)
plt.plot(x, prev_center_trace[:, 0], label="previous trace of center 0",
     lw=lw, alpha=0.4, c=colors[1])
plt.plot(x, prev_center_trace[:, 1], label="previous trace of center 1",
     lw=lw, alpha=0.4, c=colors[0])

x = np.arange(50000, 150000)
plt.plot(x, center_trace[:, 0], label="new trace of center 0", lw=lw, c="#348ABD")
plt.plot(x, center_trace[:, 1], label="new trace of center 1", lw=lw, c="#A60628")

plt.title("Traces of unknown center parameters")
leg = plt.legend(loc="upper right")
leg.get_frame().set_alpha(0.8)
plt.xlabel("Steps")

MCMC5.png


在MCMC中实例中的trace方法有一个参数为chain,这个东西索引了那些你想要返回的那些样本。(我们常常需要调用sample很多次,所以这个功能是挺有用的)。chain默认为-1,这个将会返回最后的sample的调用

聚类分析

我们还未忘记自己的使命——识别其中的聚类。我们已经确定了未知量的后验分布。我们在下面画出了中心和标准方差的变量:

figsize(11.0, 4)
std_trace = mcmc.trace("stds")[:]

_i = [1, 2, 3, 0]
for i in range(2):
    plt.subplot(2, 2, _i[2 * i])
    plt.title("Posterior of center of cluster %d" % i)
    plt.hist(center_trace[:, i], color=colors[i], bins=30,
             histtype="stepfilled")

    plt.subplot(2, 2, _i[2 * i + 1])
    plt.title("Posterior of standard deviation of cluster %d" % i)
    plt.hist(std_trace[:, i], color=colors[i], bins=30,
             histtype="stepfilled")
    # plt.autoscale(tight=True)

plt.tight_layout()

MCMC6.png


MCMC算法已经假设了最为可能的中心分别为120200。相似的推断可以被应用到标准方差上。
我们同样有这些数据标签的后验分布,这个在mcmc.trace("assignment")中给出。下面是这个可视化结果。y-axis表示了一个每个数据点的后验标签的子样本。x-axis是数据点的排序后的值。红色的方块表示被分给了cluster 1,蓝色方块就是分给了cluster 0。

import matplotlib as mpl
figsize(12.5, 4.5)
plt.cmap = mpl.colors.ListedColormap(colors)
plt.imshow(mcmc.trace("assignment")[::400, np.argsort(data)],
       cmap=plt.cmap, aspect=.4, alpha=.9)
plt.xticks(np.arange(0, data.shape[0], 40),
       ["%.2f" % s for s in np.sort(data)[::40]])
plt.ylabel("posterior sample")
plt.xlabel("value of $i$th data point")
plt.title("Posterior labels of data points")

MCMC7.png


看看上面的图,看起来最为不确定的是在150到170之间的。上面的图可能会产生些许的不准确,因为xaxis不是真实的标量(它展示了第i个排序后的数据点的值)。更加清晰的图我们在下面给出,其中我们已经估计了属于label0和1的每个数据点的频率。

cmap = mpl.colors.LinearSegmentedColormap.from_list("BMH", colors)
assign_trace = mcmc.trace("assignment")[:]
plt.scatter(data, 1 - assign_trace.mean(axis=0), cmap=cmap,
        c=assign_trace.mean(axis=0), s=50)
plt.ylim(-0.05, 1.05)
plt.xlim(35, 300)
plt.title("Probability of data point belonging to cluster 0")
plt.ylabel("probability")
plt.xlabel("value of data point")

MCMC9.png


即使我们建模cluster使用的是正态分布,我们也没有得到一个简单的正态分布来很好的拟合数据,但是获得了一个正态分布的参数的值的分布。那么我们如何选择一个均值和方差的对来确定一个拟合最后的正态分布呢?

一个简单粗暴的方式是使用后验分布的均值。下面我们使用观测的数据来更新了正态密度函数,使用后验分布的均值作为被选择的参数:

norm = stats.norm
x = np.linspace(20, 300, 500)
posterior_center_means = center_trace.mean(axis=0)
posterior_std_means = std_trace.mean(axis=0)
posterior_p_mean = mcmc.trace("p")[:].mean()

plt.hist(data, bins=20, histtype="step", normed=True, color="k",
     lw=2, label="histogram of data")
y = posterior_p_mean * norm.pdf(x, loc=posterior_center_means[0],
                                scale=posterior_std_means[0])
plt.plot(x, y, label="Cluster 0 (using posterior-mean parameters)", lw=3)
plt.fill_between(x, y, color=colors[1], alpha=0.3)

y = (1 - posterior_p_mean) * norm.pdf(x, loc=posterior_center_means[1],
                                      scale=posterior_std_means[1])
plt.plot(x, y, label="Cluster 1 (using posterior-mean parameters)", lw=3)
plt.fill_between(x, y, color=colors[0], alpha=0.3)

plt.legend(loc="upper left")
plt.title("Visualizing Clusters using posterior-mean parameters")

MCMC10.png

不要混淆后验样本

在上面的例子中,可能场景就是cluster 0有一个非常大的标准方差,而cluster1有个小一些的标准方差。这个可与你满足这个证据,尽管会比我们初始时的推断略少。另外,它会变的不可能对两个分布都有一个小的标准方差,因为数据不可能支持这样的假设。因此两个标准方差时依赖于彼此的:如果一个小,另外一个就会表达。实际上,所有这些未知量都是以一个相似的方式关联在一起的。例如,如果一个标准方差很大,均值就会在一个宽泛的空间中取值。相反地,小的标准方差限制了均值落在一娇小的区间内。
在MCMC的过程中,我们返回表示从后验分布中产生的样本的向量。不同向量的元素不可以在一起使用,因为这个会打破上述逻辑:可能一个样本已经返回了cluster 1 有一个小的标准方差,因此所有其他的变量在这个样本中将会被影响并且被做出相应的调整。当然避免这样的问题倒是很简单,只需要你确保正确地索引了轨迹。

另一个小的例子可以解释这点。假设有两个变量xy,其关联是x+y=10。我们将x建模为一个正态随机变量其均值是4,探索500个样本。

import pymc as pm

x = pm.Normal("x", 4, 10)
y = pm.Lambda("y", lambda x=x: 10 - x, trace=True)

ex_mcmc = pm.MCMC(pm.Model([x, y]))
ex_mcmc.sample(500)

plt.plot(ex_mcmc.trace("x")[:])
plt.plot(ex_mcmc.trace("y")[:])
plt.title("Displaying (extreme) case of dependence between unknowns")

MCMC11.png


如你所见,这两个变量不是相关的。将x的第i个样本和y的第j个样本加起来肯定是错误的,除非i=j

回到聚类分析:预测

上面的聚类可以推广至 k 个簇。选择 k = 2 我们可以更好地对 MCMC 进行可视化,并看看某些非常有趣的画图。
那关于预测呢?假设我们遇到一个新的数据点,比方说 x = 175。我们希望给它分配一个 簇。直接将其分配到最近的簇中心是很傻的,因为这忽略了簇本身的标准差。更加形式化的说法是,我们对分配cluster 1 给 x = 175的概率感兴趣。记 x 的分配为 L_x(取值为 0 或者 1),我们关注的是概率 P(L_x =1 | x = 175)
一种原始的方法就是使用额外的数据点来重新运行上面的 MCMC 。这个方法的缺点就是对每个新来的数据点的推断会相当缓慢。所以,我们尝试一个略微不精确,但是更快捷的方法。
这里我们使用贝叶斯定理。回想一下贝叶斯定理是这样的:


贝叶斯定理


在这个例子中,A 表示 L_x = 1 而 X 则是我们观测:x = 175。对我们后验分布的参数的一个特定样本集合,(\mu_0,\sigma_0,\mu_1,\sigma_1, p),我们关心的问题是:“x 在cluster 1中的 是不是大于其在 cluster 0 中的概率?”,其中概率依赖于选择的参数。


分配概率 1


因为分母都是一样的,所以可以忽略分母(总算解脱了,因为计算这个分母的概率还是非常困难的)


分配概率 2
norm_pdf = stats.norm.pdf
p_trace = mcmc.trace("p")[:]
x = 175

v = p_trace * norm_pdf(x, loc=center_trace[:, 0], scale=std_trace[:, 0]>(1 - p_trace) * norm_pdf(x, loc=center_trace[:,1], scale=std_trace[:,1])

print "Probability of belonging to cluster 1: ", v.mean()
# Probability of belonging to cluster 1: 0.025

给出概率而非特定标签其实是很有用的。不再需要 L = 1 if prob > 0.5 else 0,我们可以使用损失函数来优化我们的学长,这也是第五章我们会介绍的内容。

使用 MAP 来提升收敛效果

如果你自己运行上面的例子,可能会注意到我们的结果并不是一致的:可能你获得的划分更加分散,也可能是比较集中。问题在于我们的 trace 是 MCMC 算法初始值的函数。
可以用数学的方式证明,让 MCMC 运行足够长时间,算法会忘记其初始位置。实际上,这就是 MCMC 收敛的真正含义(在实践中,我们可能难以到达完全的收敛)。因此如果我们观察不同的后验分析结果,可能就是因为我们的 MCMC 还没有完全收敛,因此我们还不能从这种状态下的分布中进行采样(也就是说,需要更长的 burn-in 阶段)。
实际上,很差的初始值可以影响收敛的效果,或者极大地减缓收敛过程。理想地看,我们希望让 chain 从一个地貌的峰顶开始运行,因为这正是后验分布所在之处。因此,如果我们从峰顶开始,就可以避免长期 burn-in 过程和错误的推断。一般地,我们就叫这样的峰顶为 MAP(maximum a posterior)。
当然,我们不知道 MAP 在那里。 PyMC 提供了一个对象用来近似(如果不是确切找到) MAP 位置。在 PyMC 的主命名空间中, MAP 对象接受一个 PyMC 模型实例。调用 MAP 实例中的 .fit() 方法可以设置模型中变量为他们对应的 MAP 值。

map_ = pm.MAP( model )
map_.fit()

MAP.fit() 方法给拥有允许用户选择使用哪一种优化算法的灵活性(总之,这是一个优化问题:我们在寻找那些最大化地貌的值),因为不是所有优化算法都是适用的。fit 适用的默认优化算法就是 scipy 的 fmin 算法(尝试最小化地貌的负值)。另一个可以使用算法是 Powell Method,PyMC 的 blogger Abraham FlaxmanAbraham Flaxman最喜欢的一种方法,可以使用 fit(method='fmin_powell) 得到。从我个人的经验看来,默认的用得更多,但是如果收敛很慢的话,我也会尝试 Powell method。
MAP 也可以看成是推断问题的解,因为数学上它就是未知量的最可能的值。但是本章前面也提到过,这种结果忽略了不确定性,并没有返回一个分布。
一般来说在调用 mcmc 之前使用 MAP(model).fit() 都是一个很好的想法。对 fit 的调用计算量不会非常重,也能够在一定能够程度上节约花在 burn-in 阶段的时间。

Burn-in 阶段的信息

即使我们使用 MAP 先验来调用 MCMC.sample,提供 Burn-in 阶段还是很好的做法,这样可以更加安全。我们可以使用 PyMC 设置 burn 的参数来自动地丢弃前 n 个样本。因为我们不知道 chain 什么时候已经完全收敛,我倾向于丢弃前面一半的样本,甚至有时达到 90% 的样本。接着前面的聚类例子,我现在的代码就像这样:

model = pm.Model( [p, assignment, taus, centers ] )

map_ = pm.MAP( model )
map_.fit() #stores the fitted variables' values in foo.value

mcmc = pm.MCMC( model )
mcmc.sample( 100000, 50000 )

Diagnosing Convergence

Autocorrelation

自相关是一系列数字其本身相关性的度量。度量值为 1.0 则是完美的正自相关,0.0 则是没有自相关,而 - 1.0 表示完美的负自相关。如果你熟悉标准相关性定义,那么自相关就是一个序列 x_{\tau} 在时间 t 和 在时间 t-k 的相关性:


Paste_Image.png


例如,下面两个序列:


Paste_Image.png


拥有对应的样本路径:

figsize(12.5, 4)
import pymc as pm
x_t = pm.rnormal(0, 1, 200)
x_t[0] = 0
y_t = np.zeros(200)
for i in range(1, 200): 
    y_t[i] = pm.rnormal(y_t[i - 1], 1)
plt.plot(y_t, label="$y_t$", lw=3)
plt.plot(x_t, label="$x_t$", lw=3)
plt.xlabel("time, $t$")
plt.legend()

Paste_Image.png

一种考虑自相关性的角度是“如果我知道序列在时间 s 处的位置,这个信息能不能帮助我知道在时间 t 所处的位置?”在序列 x_t 处,答案是 不能。通过构造,x_t 是随机变量。如果我告诉你 x_2 = 0.5,你能够告诉我更加准确的关于 x_3 的推测么 ?也不行。
换句话说,y_t 是自相关的。通过构造,如果我知道 y_2 = 10,我可以很确定地认为 y_3
不会太偏离 10。类似的,我甚至可以对 y_4 进行一个相对不太肯定的推测:它不太可能靠近 0 或者 20,但是 5 却是很有可能的。我可以对 y_5 进行推测,但是这种置信度是更低的。更加严格的表述是,我们承认在时间点之间的距离 k 增加时,自相关性会随之下降。可视化如下:

def autocorr(x):
    # from http://tinyurl.com/afz57c4
    result = np.correlate(x, x, mode='full')
    result = result / np.max(result)
    return result[result.size / 2:]

colors = ["#348ABD", "#A60628", "#7A68A6"]

x = np.arange(1, 200)
plt.bar(x, autocorr(y_t)[1:], width=1, label="$y_t$",
        edgecolor=colors[0], color=colors[0])
plt.bar(x, autocorr(x_t)[1:], width=1, label="$x_t$",
        color=colors[1], edgecolor=colors[1])

plt.legend(title="Autocorrelation")
plt.ylabel("measured correlation \nbetween $y_t$ and $y_{t-k}$.")
plt.xlabel("k (lag)")
plt.title("Autocorrelation plot of $y_t$ and $x_t$ for differing $k$ lags.")

Paste_Image.png

注意,在随着 k 增加时,y_t 的自相关性会从一个很高的点开始下降。对比 x_t 的自相关性(非常像噪声),我们可以说自相关性存在于这些序列中。

How does this relate to MCMC convergence

根据 MCMC 算法的本质,我们总是返回那些显示出自相关性的样本(这是因为从你当前位置的步骤,移动到靠近你的另外一个)
能够很好滴遍历空间 chain 会表现出很高的自相关性。从可视化图上看,如果 trace 看起来如河流一样蜿蜒曲折,并且没有静止下来,这个 chain 就会有很高的自相关性。
这并不是说,一个收敛的 MCMC 拥有低自相关性。因此低自相关性并不是收敛的必要条件。PyMC 在模块 Matplot 中拥有内置的画出自相关性函数。

Thinning

如果在后验分布的样本中存在高自相关性,会出现另一个问题。很多后期处理算法需要样本之间是独立的。通过仅仅返回第 n 个样本来解决或者至少缓解这个问题,可以去除一些自相关性。下面我们执行对 y_t 的自相关性画图,按照不同 thinning 的层次进行:

max_x = 200 / 3 + 1
x = np.arange(1, max_x)

plt.bar(x, autocorr(y_t)[1:max_x], edgecolor=colors[0],
        label="no thinning", color=colors[0], width=1)
plt.bar(x, autocorr(y_t[::2])[1:max_x], edgecolor=colors[1],
        label="keeping every 2nd sample", color=colors[1], width=1)
plt.bar(x, autocorr(y_t[::3])[1:max_x], width=1, edgecolor=colors[2],
        label="keeping every 3rd sample", color=colors[2])

plt.autoscale(tight=True)
plt.legend(title="Autocorrelation plot for $y_t$", loc="lower left")
plt.ylabel("measured correlation \nbetween $y_t$ and $y_{t-k}$.")
plt.xlabel("k (lag)")
plt.title("Autocorrelation of $y_t$ (no thinning vs. thinning) \
at differing $k$ lags.")

Paste_Image.png


更大的 thinning,自相关性会下降得更快。这里存在着一个平衡:更高的 thinning 需要更多的 MCMC 迭代来达到同样数量返回的样本。例如,10 000 样本需要 thinning = 10 的 100 000 个样本(尽管后者是更低的自相关性)。
所以,什么样的 thinning 是更好的设置呢?返回的样本总是展示某种程度的自相关性,不管设置了多大的 thinning。只要自相关性趋近于 0,你才能够真的放心。一般来说, thinning 不需要超过 10。
PyMC 在 sample 中给出了参数 thinning,例如:sample(10000, burn=5000, thinning=5)

pymc.Matplot.plot()

在每次执行 MCMC 时手动创建直方图、自相关画图和 trace 画图并不是很方便。PyMC 的作者已经包含了这样的可视化工具。
正如标题所说的,pymc.Matplot 模块包含一个命名不太好的函数 plot,我倾向于作为 mcplot 导入,这样可以避免一些与其他的命名空间的 plot 或者 mcplot 的冲突。plot 接受 MCMC 对象返回每个变量(最多 10 个变量)的后验分布,trace 和 自相关性。
下面我们使用这个工具来画出设置 thinning = 10在25 000次采样后簇的中心。

from pymc.Matplot import plot as mcplot

mcmc.sample(25000, 0, 10)
mcplot(mcmc.trace("centers", 2), common_scale=False)

# [****************100%******************]  25000 of 25000 completePlotting centers_0
# Plotting centers_1

Paste_Image.png


这幅图中包含两个图,一个是对每个在变量 centers 中的未知量。在每个图中,在最左上角的子图是变量的 trace。这对于查看表示可能的未收敛的曲折特征比较有用。
在右侧最大的图中,是样本的直方图,加上一些额外的特性。最厚重的垂直线表示后验分布的均值,这也是后验分布的很好的总结。而在两个垂直虚线之间的区间表示 95% 可信区间(不要和置信区间混淆)。我不会深入讨论后者,前者可以看做是“有 95%的几率我们感兴趣的参数落在这个区间”。(改变 mcplot 的默认参数可以将这里的 95% 替换)在你将结果给他人看的时候,尤其重要的一点就是指出这个区间。学习 贝叶斯方法的 目标之一就是对那些未知量本身的不确定性有一种清晰的认识。结合 后验分布的均值,95%可信区间给出来了一个靠谱的衡量方式来对未知量(均值)和不确定性(区间的大小)进行刻画。

名称为 center_0_acorr 和 center_1_acorr 是生成的自相关性的画图。这看起来和之前展示的那些不同,但是仅仅在于 0-lag 在不在图的中央。

Useful tips for MCMC

如果不是MCMC的计算上造成的困难,那么贝叶斯推断的首选肯定是这个方法。实际上,MCMC 是让很多的人都放弃了在实际问题使用它。下面给出一些好的启发式建议来帮助快速收敛。

Intelligent starting values

从后验分布附近开始 MCMC 算法肯定很有效,这样只需要少量的时间就可以正确地进行采样了。我们可以通过在创建 Stochastic 变量时指定 value 参数来帮助算法。在很多情况下,我们可以产生关于参数的合理的猜测。例如,如果我们有来自正态分布的数据,想要估计 \mu 参数,那么就可以从数据的均值来作为开始点。

 mu = pm.Uniform( "mu", 0, 100, value = data.mean() )

对大多数的模型参数,有一个频率主义的估计。这些估计是 MCMC 算法很好的起始点。当然,这并不总是可行的,但是包含尽可能多的合适的初始值总归是很好的想法。即使你猜测的值是错误的,MCMC 仍然会收敛到合适的分布,很少会失败。
这就是 MAP 试着做出的事情,通过给 MCMC 设置出好的初始值。所以为何还需要指定用户自定义的值呢?因为,给定好的 MAP 值就能够帮助它找到 MAP。
同样重要的一点是,坏的初始值也是 PyMC 主要的问题之一,会导致收敛失败。

Priors

如果先验选择得不好,MCMC 算法可能不会收敛,至少收敛困难。想想如果选择的先验没有能够包含真实的参数:先验赋给未知量概率为 0,这样后验同样得到 0 的概率。这就出问题了。
所以,最好精心选择先验。同样,收敛的缺失或者样本集中在边界上的情况就是因为选错了先验(参看下面的 统计计算的 Folk Theorem)。

Covariance matrices and eliminating parameters

The Folk Theorem of Statistical Computing

如果你在计算上出了问题,可能你的模型就已经错了。

Conclusion

PyMC 给出了执行贝叶斯推断的强大支持平台,因为其拥有抽象的 MCMC 的内部机制。除此之外,在确保推断本身没有受到 MCMC 的迭代特性的影响也是需要注意的。

References

Flaxman, Abraham. "Powell's Methods for Maximization in PyMC." Healthy Algorithms. N.p., 9 02 2012. Web. 28 Feb 2013. http://healthyalgorithms.com/2012/02/09/powells-method-for-maximization-in-pymc/.

你可能感兴趣的:(AI)