(已弃用) Pyro 中的模型简介

警告

本教程已被弃用,请参阅更新的 Pyro 入门。本教程未来可能会被移除。***

概率程序的基本单元是随机函数。这是一个任意的 Python 可调用对象,它结合了两种成分

  • 确定性 Python 代码;以及

  • 调用随机数生成器的原始随机函数

具体来说,随机函数可以是任何具有 __call__() 方法的 Python 对象,例如函数、方法或 PyTorch 的 nn.Module

在整个教程和文档中,我们通常将随机函数称为模型,因为随机函数可以用来表示数据生成过程的简化或抽象描述。将模型表示为随机函数意味着模型可以像常规的 Python 可调用对象一样进行组合、重用、导入和序列化。

[1]:
import torch
import pyro

pyro.set_rng_seed(101)

原始随机函数

原始随机函数,或分布,是一类重要的随机函数,我们可以明确计算给定输入时输出的概率。自 PyTorch 0.4 和 Pyro 0.2 起,Pyro 使用 PyTorch 的分布库。你还可以使用变换 (transforms) 创建自定义分布。

使用原始随机函数很简单。例如,要从单位正态分布 \(\mathcal{N}(0,1)\) 中抽取样本 x,我们可以这样做

[2]:
loc = 0.   # mean zero
scale = 1. # unit variance
normal = torch.distributions.Normal(loc, scale) # create a normal distribution object
x = normal.rsample() # draw a sample from N(0,1)
print("sample", x)
print("log prob", normal.log_prob(x)) # score the sample from N(0,1)
sample tensor(-1.3905)
log prob tensor(-1.8857)

这里,torch.distributions.NormalDistribution 类的一个实例,它接受参数并提供 sample 和 score 方法。Pyro 的分布库 pyro.distributionstorch.distributions 的一个薄封装,因为我们希望在推断过程中利用 PyTorch 快速的张量计算和自动求导能力。

一个简单模型

所有概率程序都是通过组合原始随机函数和确定性计算构建起来的。由于我们最终对概率编程感兴趣是因为我们想对现实世界中的事物进行建模,所以我们从一个具体事物的模型开始。

假设我们有一堆包含每日平均气温和云量的数据。我们想探究气温与晴天或阴天之间的关系。一个描述这些数据可能如何生成的简单随机函数如下所示:

[3]:
def 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()

我们逐行来看。首先,在第 2 行,我们定义了一个二元随机变量 'cloudy',它来自于参数为 0.3 的 Bernoulli 分布。由于 Bernoulli 分布返回 01,因此在第 3 行我们将 cloudy 的值转换为字符串,以便更易于解析 weather 的返回值。所以根据这个模型,30% 的时间是阴天,70% 的时间是晴天。

在第 4-5 行,我们定义了用于在第 6 行采样温度的参数。这些参数取决于我们在第 2 行采样的 cloudy 的特定值。例如,阴天时的平均温度是 55 华氏度,晴天时是 75 华氏度。最后,我们在第 7 行返回这两个值 cloudytemp

然而,weather 完全独立于 Pyro——它只调用 PyTorch。如果除了采样假数据之外还想用这个模型做点别的,我们需要把它变成一个 Pyro 程序。

pyro.sample 原语

要将 weather 变成一个 Pyro 程序,我们将把 torch.distribution 替换为 pyro.distribution,并将 .sample().rsample() 调用替换为 pyro.sample 的调用,这是 Pyro 中的核心语言原语之一。使用 pyro.sample 和调用原始随机函数一样简单,但有一个重要的区别

[4]:
x = pyro.sample("my_sample", pyro.distributions.Normal(loc, scale))
print(x)
tensor(-0.8152)

就像直接调用 torch.distributions.Normal().rsample() 一样,这会返回一个来自单位正态分布的样本。关键的区别在于这个样本是命名的。Pyro 的后端使用这些名称来唯一标识采样语句,并根据包含的随机函数的使用方式,在运行时改变它们的行为。正如我们将看到的,这就是 Pyro 如何实现支撑推断算法的各种操作。

现在我们已经介绍了 pyro.samplepyro.distributions,我们可以将简单模型重写为一个 Pyro 程序

[5]:
def 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())
('cloudy', 64.5440444946289)
('sunny', 94.37557983398438)
('sunny', 72.5186767578125)

从过程上看,weather() 仍然是一个非确定性的 Python 可调用对象,它返回两个随机样本。然而,由于随机性现在是通过 pyro.sample 调用的,它的意义远不止于此。特别是,weather() 指定了两个命名随机变量 cloudytemp 的联合概率分布。因此,它定义了一个概率模型,我们可以使用概率论的技术对其进行推理。例如,我们可以问:如果我观察到温度是 70 度,那么是阴天的可能性有多大?如何提出和回答这类问题将是下一篇教程的主题。

通用性:随机递归、高阶随机函数和随机控制流

现在我们已经看到了如何定义一个简单模型。在此基础上构建模型也很容易。例如

[6]:
def 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 时传递唯一的样本名称。例如,我们可以像这样定义一个几何分布,它计算直到第一次成功之前的失败次数

[7]:
def 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_0x_1 等是动态生成的,并且不同的执行可以具有不同数量的命名随机变量。

我们还可以自由地定义接受其他随机函数作为输入或将其作为输出的随机函数

[8]:
def 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.))
tensor(2.1493)

这里 make_normal_normal() 是一个随机函数,它接受一个参数,并在执行时生成三个命名随机变量。

Pyro 支持任意 Python 代码(例如迭代、递归、高阶函数等)与随机控制流相结合,这意味着 Pyro 随机函数是通用的,也就是说它们可以用来表示任何可计算的概率分布。正如我们在后续教程中将看到的,这非常强大。

值得强调的是,这也是 Pyro 构建在 PyTorch 之上的一个原因:动态计算图是实现可以从 GPU 加速的张量计算中受益的通用模型的重要组成部分。

下一步

我们已经展示了如何使用随机函数和原始分布来表示 Pyro 中的模型。为了从数据中学习模型并对其进行推理,我们需要能够进行推断。这是下一篇教程的主题。