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

作者: WilliamY | 来源:发表于2019-11-27 20:17 被阅读0次

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

    假定我们研究的问题涉及N个观察数据,通过modelguide计算ELBO的复杂度,随着N的增加而急速上升。这是由于ELBO的计算包括了全部的观察数据,当数据库较大时,遍历它们将大量耗时。
    幸运的是,当隐变量条件独立时,估算ELBO可以只采样部分样本(subsampling),这时对数似然的估计值为
    \sum_{i=1}^N \log p({\bf x}_i | {\bf z}) \approx \frac{N}{M} \sum_{i\in{\mathcal{I}_M}} \log p({\bf x}_i | {\bf z})
    其中\mathcal I_M是某批次数据(mini-batch)的指标集,其规模为M并满足M<N。下面我们介绍在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个样本点验证其似然值,而对数似然的计算也相应地乘以系数\frac{10}{5}=2。下面我们将其写成向量化的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将重新采样一次,只要运行用户需要的次数就实现了部分采样。这样做有很大的弊端:对于大数据集来说,一些样本可能永远都不会被采样到。

    只有局部变量的部分采样

    对于联合分布密度
    p({\bf x}, {\bf z}) = \prod_{i=1}^N p({\bf x}_i | {\bf z}_i) p({\bf z}_i)
    来说,依赖结构是定义好的,所以部分采样带来的缩放因子对于ELBO的所有项是相同的。回顾ELBO的定义:
    {\rm ELBO} \equiv \mathbb{E}_{q_{\phi}({\bf z})} \left [ \log \frac{p_{\theta}({\bf x}, {\bf z})}{q_{\phi}({\bf z})} \right]分子分母的缩放因子抵消了。在这种情况下,比如经典VAE模型,用户可以控制部分采样的过程,将分批的随机变量输入到model和guide中。plate仍旧被用到,但是不需要subsample_sizesubsample关键字。更详细的解释参考VAE 教程

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

    举例说明。考虑如下联合分布:
    p({\bf x}, {\bf z}, \beta) = p(\beta) \prod_{i=1}^N p({\bf x}_i | {\bf z}_i) p({\bf z}_i | \beta)
    该分布包括局部变量:N个观察变量\{{\bf x}_i\}N个隐变量\{{\bf z}_i\},全局变量:一个隐变量\beta。我们定义guide为:
    q({\bf z}, \beta) = q(\beta)\prod_{i=1}^N q({\bf z}_i|\beta, \lambda_i)
    这里引入了N个变分变量\{\lambda_i\}。model和guide都存在条件独立。对于model来说,给定\{{\bf z}_i\},观察变量\{{\bf x}_i\}是独立的;给定\beta,隐变量\{{\bf z}_i\}是独立的。对于guide来说,给定\{{\lambda}_i\}\beta,隐变量\{{\bf z}_i\}是独立的。为了标记条件独立性,我们在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 二

        本文链接:https://www.haomeiwen.com/subject/tehywctx.html