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

目标:将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中也采样相同的数量。

分期

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容