Pyro简介:产生式模型实现库(四),SVI 二

目标:将SVI应用到大型数据集

假定我们研究的问题涉及N个观察数据,通过modelguide计算ELBO的复杂度,随着N的增加而急速上升。这是由于ELBO的计算包括了全部的观察数据,当数据库较大时,遍历它们将大量耗时。
幸运的是,当隐变量条件独立时,估算ELBO可以只采样部分样本(subsampling),这时对数似然的估计值为

其中是某批次数据(mini-batch)的指标集,其规模为并满足。下面我们介绍在Pyro中实现这一过程。
【注:这里的subsampling和神经网络中的含义是不同的。这里指从全部数据集合截取部分样本,样本量减少,每个样本保持不变;而神经网络中subsampling操作是将大尺寸的特征“降采样”到尺寸较小的特征,样本量保持不变,每个样本尺寸减少。】

在Pyro中标记条件独立

Pyro提供了两种机制标记随机变量间条件独立性:platemarkov,下面我们分别介绍它们。

序列数据plate

我们回到上一个教程的例子。方便起见,我们只写以前代码的主要逻辑:

def model(data):
    # 从先验的beta分布采样得到f
    f = pyro.sample('latent_fairness', dist.Beta(alpha0, beta0))
    # 遍历整个观察数据集,将其输入在obs关键字中
    for i in range(len(data)):
        # 似然函数服从伯努利分布
        pyro.sample('obs_{}'.format(i), dist.Bernoulli(f), obs=data[i])

在上述例子中,给定隐变量latent_fairness,观察数据是条件独立的。明确标记这一条件独立性,只需要将range替换为plate即可。

def model(data):
    # 从先验分布中采样得到f
    f = pyro.sample('latent_fairness', dist.Beta(alpha0, beta0))
    # 遍历观察数据【我们仅仅改变range为plate】
    for i in pyro.plate('data_loop', len(data)):
        # 在数据点i上的似然函数
        pyro.sample('obs_{}'.format(i), dist.Bernoulli(f), obs=data[i])

从这个例子,我们可以看到platerange的唯一区别:每次启动plate都需要用户指定一个名字。其余都是一样的。
到目前为止,我们顺利地利用Pyro实现了条件独立性。如果我们追问这一机制是如何发挥作用的,简单来说,pyro.plate的实现用到了上下文管理器。当程序进入for循环后,系统启动了(条件)独立机制,直到循环结束才关闭。所以

  • for循环内的部分,pyro.sample下的变量都是独立的;
  • 这一独立性是条件独立,这是因为latent_fairness的采样过程在for循环外,不在data_loop的上下文中。

在向下进行之前,我需要提醒用户避免一类自作聪明的错误。在使用序列数据的plate的时候,考虑下面这段代码:

# 警告, 不要写成下面这样!!!
my_reified_list = list(pyro.plate('data_loop', len(data)))
for i in my_reified_list:
    pyro.sample('obs_{}'.format(i), dist.Bernoulli(f), obs=data[i]) 

这样写不会实现条件独立性,这是因为list()将把单个的pyro.plate断开,这种条件独立机制就失效了。基于同样的原因,pyro.plate不能处理时间上断开的序列,比如自循环系统,这时则应使用pyro.markov,这时后话。

向量化plate

从概念上说,向量化的plate和上面序列的plate没有本质差别,写法上却更省事。我们举个例子来说明。假如我们定义data如下:

data = torch.zeros(10)
data[:6] = torch.ones(6) # 6次正面,4次反面

我们只需要写:

with plate('observe_data'):
    pyro.sample('obs', dist.Bernoulli(f), obs=data)

与序列的写法相比,这里不需要1-v-1指定观察的名称、观察的数据点,整个调用只需要起一个名字,也不用指定张量的长度。
和上面的警告一样,注意不要犯上面的错误。

部分采样(subsampling)

我们已经学习了怎样用Pyro表示条件独立。下一个感兴趣的问题是,怎样在数据库规模较大时,使用部分采样。Pyro中,部分采样的实现有许多种,现在我们一一介绍它们。

使用plate自动地部分采样

我们先看一个最简单的例子。

for i in pyro.plate('data_loop', len(data), subsample_size=5):
    pyro.sample('obs_{}'.format(i), dist.Bernoulli(f), obs=data[i])

只需要加上关键字subsample_size,这样系统将选出的5个样本点验证其似然值,而对数似然的计算也相应地乘以系数。下面我们将其写成向量化的plate

with plate('observed_data', size=10, subsample_size=5) as ind:
    pyro.sample('obs', dist.Bernoulli(f), obs=data.index_select(0, ind))
# 这里的data是torch.tensor,如果是list将报错!
# 结果:
# tensor([1., 0., 1., 0., 1.])

plate返回的张量的指标是ind,其长度为5。除了subsample_size外,我们还需要传入参数size,这是因为plate计算重要性系数时要了解张量的总长度。
如果用户要使用GPU,plate应传入参数device,使data并行计算。

运行model模型带来部分采样

每次运行model,plate将重新采样一次,只要运行用户需要的次数就实现了部分采样。这样做有很大的弊端:对于大数据集来说,一些样本可能永远都不会被采样到。

只有局部变量的部分采样

对于联合分布密度

来说,依赖结构是定义好的,所以部分采样带来的缩放因子对于ELBO的所有项是相同的。回顾ELBO的定义:
分子分母的缩放因子抵消了。在这种情况下,比如经典VAE模型,用户可以控制部分采样的过程,将分批的随机变量输入到model和guide中。plate仍旧被用到,但是不需要subsample_sizesubsample关键字。更详细的解释参考VAE 教程。

局部变量和全局变量都参与其中的部分采样

举例说明。考虑如下联合分布:

该分布包括局部变量:个观察变量、个隐变量,全局变量:一个隐变量。我们定义guide为:

这里引入了个变分变量。model和guide都存在条件独立。对于model来说,给定,观察变量是独立的;给定,隐变量是独立的。对于guide来说,给定和,隐变量是独立的。为了标记条件独立性,我们在model和guide中都需要使用plate,下面我们写下代码的大致框架(完整的代码需要用到pyro.sample等)。首先是model:

def model(data):
    beta = pyro.sample('beta', ...) # 采样全局随机变量
    for i in pyro.plate('locals', len(data)):
        z_i = pyro.sample('z_{}'.format(i), ...)
        # 利用观察变量计算参数
        # 利用局部变量计算似然
        theta_i = compute_something(z_i)
        pyro.sample('obs_{}'.format(i), dist.Mydist(theta_i), obs=data[i])

接着是guide:

def guide(data):
    beta = pyro.sample('beta', ...) # 采样全局变量
    for i in pyro.plate('locals', len(data), subsample_size=5):
        # 采样局部变量
        pyro.sample('z_{}'.format(i), ..., lambda_i)

这里需要注意的是,我们只需要在guide中使用subsample_size参数,Pyro会自动地在model中也采样相同的数量。

分期

你可能感兴趣的:(Pyro简介:产生式模型实现库(四),SVI 二)