Pyro模型介绍
概率编程的基础单元是随机函数Stochastic function。它是一种随机的Python可调用函数,由以下两个组成:
- 确定性的Python代码
- 原始随机函数,他们会调用一个随机数生成器
简单的来说,随机函数可以是任意一个拥有
__call__()
函数的对象,比如一个函数function,一个方法method,或者像PyTorch的一个nn.Module
.
在教程和文档中,因为随机函数可以用来简化地或者抽象地描述一个数据生成的过程,我们将会调用随机函数模型。将随机函数表述成模型意味着该模型可以像普通Python可调用函数一样,被创造composed,重用reused,引用import,和序列化serialized。1
2
3
4import torch
import pyro
pyro.set_rng_seed(101)
原始随机函数
原始随机函数,或者说分布,是随机函数中重要的一个类。给定输入,我们可以通过它们明确地计算并输出概率。就像PyTorch 0.4和Pyro 0.2,Pyro使用PyTorch的distribution library。你也可以通过transforms创建一个自己的分布。
1 | loc = 0. # mean zero |
输出为:1
2sample tensor(-1.3095)
log prob tensor(-1.8857)
在这里,torch.distributions.Normal
是一个Distribution
类的例子,展示他们可以接受参数,提供取样,并给方法method打分。Pyro的分布函数库pyro.distributions
是一个轻量化的torch.distributions
,因为我们希望在推断inference的过程中,可以使用PyTorch的tensor计算和autograd的能力。
一个简单的模型
所有的概率编程程序都通过组合原始随机函数和确定性的计算而得到的。既然我们的最终目标是用概率编程来模拟真实世界的事物,那我们就来一个实例的模型吧。
假设我们由一堆关于每日平均温度(daily mean temperature)和云层覆盖(cloud cover)的数据。我们希望找到温度是否受当天是晴是阴的影响。一个简单的描述数据是如何生成的随机函数如下:1
2
3
4
5
6
7def weather():
cloudy = torch.distributions.Bernoulli(0.3).sample()
cloudy = 'cloudy' if cloudy.item() == 1.0 else 'sunny'
mean_temp = {'cloudy': 55.0, 'sunny': 75.0}[cloudy]
scale_temp = {'cloudy': 10.0, 'sunny': 15.0}[cloudy]
temp = torch.distributions.Normal(mean_temp, scale_temp).rsample()
return cloudy, temp.item()
让我们一行一行看。首先,在第二行中,我们定义了一个二元随机变量‘cloudy’。它服从伯努利分布,其p值为0.3。因为伯努利分布返回0或者1,在第三行中,我们将cloudy
的值转变为string,方便weather
的返回值易于处理。那么根据这个模型,30%的时间它将是多云的阴天,而70%的时间将是晴天。
在第4-5行,我们定义了一些我们将在第6行中来取样温度的参数。这些参数由我们在第二行取样的cloudy
的值决定。比如说,如果取样到阴天,平均温度是55华氏度,若晴天则为75度。最后,在第7行中我们返回两个值,cloudy
和temp
。
但是,目前weather
完全独立于Pyro,因为它只用到了PyTorch的函数。如果我们希望将该模型用作除了生成假数据以外的他途的话,我们需要把它转变为Pyro的程序。
pyro.sample
原始类型
要将weather
转变为Pyro程序,我们需要将torch.distribution
替换成pyro.distribution
,同时.sample()
和.rsample
的调用替换成pyro.sample
的调用。pyro.sample
是Pyro的原始类型之一。使用pyro.sample
就像调用原始随机函数一样简单,除了一处重要的不同:1
2x = pyro.sample("my_sample", pyro.distributions.Normal(loc, scale))
print(x)
输出:tensor(-0.8152)
就像是直接调用torch.distributions.Normal().rsample()
,上述将返回一个服从正太分布的取样。重要的不同在于这个取样是被命名乐的。Pyro的后段用这些名字来各个识别取样的表述statement,且在运行时根据相关的随机函数是如何被使用的从而来改变他们的表现behavior。这就是Pyro如何完成对于推断算法实现的各种操作。[原文]
现在,我们引入pyro.sample
和pyro.distributions
,我们可以重写我们的简单模型,变为Pyro程序:1
2
3
4
5
6
7
8
9
10def weather():
cloudy = pyro.sample('cloudy', pyro.distributions.Bernoulli(0.3))
cloudy = 'cloudy' if cloudy.item() == 1.0 else 'sunny'
mean_temp = {'cloudy': 55.0, 'sunny': 75.0}[cloudy]
scale_temp = {'cloudy': 10.0, 'sunny': 15.0}[cloudy]
temp = pyro.sample('temp', pyro.distributions.Normal(mean_temp, scale_temp))
return cloudy, temp.item()
for _ in range(3):
print(weather())
程序上,weather()
依旧是一个非确定性的将返回两个随机取样的Python函数。因为随机值现在是由pyro.sample
产生的,但是它可以比现在做的更多。特别的,weather()
描述了一个对于两个命名变量cloudy
和temp
的关联概率分布joint probability distribution。就像这样,它定义了一个概率模型,在此之上我们可以用概率论的技巧来搞事情了。比如,我们可能会问,如果我观测到当前温度70华氏度,那么现在有多大概率是阴天?如何编写和解答这类问题将在下一章教程中。
普遍性:随机递归,高阶随机函数和随机流程控制语句
我们已经讲过了如何定义一个简单的模型。从此之上建立其他的模型是简单的,比如:1
2
3
4
5def ice_cream_sales():
cloudy, temp = weather()
expected_sales = 200. if cloudy == 'sunny' and temp > 80.0 else 50.
ice_cream = pyro.sample('ice_cream', pyro.distributions.Normal(expected_sales, 10.0))
return ice_cream
这种模块化,对于任何程序员是熟悉且明显十分强大的。但是它是否强大到能够覆盖我们想表达的所有不同类型的模型了呢?
事实是,因为Pyro植根于Python,随机函数可以包含任意复杂的确定性的Python代码,且随机性可以自由地影响流程控制语句。举例来说,我们可以构建一个递归函数,它是否结束递归是不确定的,前提是假设当它被调用时,我们能够传递pyro.sample
的唯一的取样名字。比如我们定义一个几何分布,它将累计第一个成功前的失败次数:1
2
3
4
5
6
7
8
9
10def geometric(p, t=None):
if t is None:
t = 0
x = pyro.sample("x_{}".format(t), pyro.distributions.Bernoulli(p))
if x.item() == 1:
return 0
else:
return 1 + geometric(p, t + 1)
print(geometric(0.5))
输出:0
注意到,在geometric()
中,命名变量x_0
,x_1
等是动态产生的,且不同地方的执行将有不同数量的命名随机变脸产生。
我们也可以定义一些接受随机函数为输入,或输出随机函数的随机函数。1
2
3
4
5
6
7
8
9
10
11
12def normal_product(loc, scale):
z1 = pyro.sample("z1", pyro.distributions.Normal(loc, scale))
z2 = pyro.sample("z2", pyro.distributions.Normal(loc, scale))
y = z1 * z2
return y
def make_normal_normal():
mu_latent = pyro.sample("mu_latent", pyro.distributions.Normal(0, 1))
fn = lambda scale: normal_product(mu_latent, scale)
return fn
print(make_normal_normal()(1.))
输出:2.1493
这里make_normal_normal()
就是一个接受一个参数,且产生三个命名随机变量的随机函数。
事实上,Pyro支持任意Python代码比如遍历,递归,高阶函数等等。与随机流程控制语句结合意味着Pyro的随机函数是普遍的,比方说,他们可以被用来表达任意可计算的概率分布。在之后的教程中,我们可以看到其强大之处。
关于为什么Pyro建立在PyTorch基础之上,值得强调的一点原因是:动态计算图(dynamic computational graphs)是对于普遍的模型而言是一个重要的元素,使得我们可以使用GPU加速的tensor计算。
下一步
我们已经展示了如何在Pyro使用随机函数和原始分布来表达模型。为了从数据中学习模型并理解他们,我们需要实现推断inference。请见下一章教程。