目标:将SVI应用到大型数据集
假定我们研究的问题涉及N个观察数据,通过model
和guide
计算ELBO的复杂度,随着N的增加而急速上升。这是由于ELBO的计算包括了全部的观察数据,当数据库较大时,遍历它们将大量耗时。
幸运的是,当隐变量条件独立时,估算ELBO可以只采样部分样本(subsampling),这时对数似然的估计值为
其中是某批次数据(mini-batch)的指标集,其规模为并满足。下面我们介绍在Pyro中实现这一过程。
【注:这里的subsampling和神经网络中的含义是不同的。这里指从全部数据集合截取部分样本,样本量减少,每个样本保持不变;而神经网络中subsampling操作是将大尺寸的特征“降采样”到尺寸较小的特征,样本量保持不变,每个样本尺寸减少。】
在Pyro中标记条件独立
Pyro提供了两种机制标记随机变量间条件独立性:plate
和markov
,下面我们分别介绍它们。
序列数据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])
从这个例子,我们可以看到plate
和range
的唯一区别:每次启动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_size
和subsample
关键字。更详细的解释参考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
中也采样相同的数量。