stan,pymc與edward都是基於一套自動求導機制(stan是自己實現的,pymc基於theano,edward利用tensorflow)在上面提供一些封裝來簡化概率推斷的描述。但它們同時也不那麼靈活,pymc和edward經常要和它們背後的theano,tensorflow的機制交互,而這兩個框架都不好調試。本文利用pytorch實現stan風格的幾個推斷方法來揭示它們內部做的類似的事情。

stan的模型簡介

stan的模型可以說最精簡的,如pystan例子的這段代碼

data {
int<lower=0> J; // number of schools
real y[J]; // estimated treatment effects
real<lower=0> sigma[J]; // s.e. of effect estimates
}
parameters {
real mu;
real<lower=0> tau;
real eta[J];
}
transformed parameters {
real theta[J];
for (j in 1:J)
theta[j] = mu + tau * eta[j];
}
model {
eta ~ normal(0, 1);
y ~ normal(theta, sigma);
}

可以說是十分直觀的。

stan的data段表示外部程序向其導入的數據,parameters和transformed parameters里聲明和計算的變數會被記錄下來,model則在用前面定義的參數和數據描述一個聯合概率函數。

其中類似

X ~ normal(mu,sigma)

之類的代碼都等價於向聯合對數概率target累加概率

target += normal_lp(X | mu,sigma)

由於這個target可以任意編寫,比pymc和edward的寫法要靈活得多。

X ~ normal(mu,sigma)這種所謂的採樣語句在任何時候都不對應著真正的隨機採樣,在MAP——極大後驗點估計中我們只是在極大化target,在ADVI自動微分變分推斷中,使用蒙特卡洛積分計算一個期望時,我們是在變分分布里隨機採樣而不是這裡的原分布,在HMC中,也不是直接用這個分布進行採樣。

事實上,為了在stan里進行真正的採樣,要調用normal_rng之類的函數,上述的sampling statement為了避免歧義被刻意限制為只能在model塊中使用,而XXX_rng函數則必須不能在model塊里使用。

stan里的模型只是花式寫一個聯合概率函數的本質使得這類框架變得十分清晰。pytorch也提供了一套自動求導機制,所以我們可以很容易平移它們的實現。

MAP點估計

下面用pystan實現一個簡單的正態模型:

Y sim N(mu,1^2)

import pystan
import numpy as np
import torch
from torch.autograd import Variable

ocode = """
data {
int<lower=1> N;
real y[N];
}
parameters {
real mu;
}
model {
y ~ normal(mu, 1);
}
"""

sm = pystan.StanModel(model_code=ocode)
np.random.seed(13)
y2 = np.random.normal(size=20)
np.mean(y2)
# 0.36640264498852165

op = sm.optimizing(data=dict(y=y2, N=len(y2)))
op
#OrderedDict([(mu, array(0.36640264))])

下面用pytorch實現基本等效的代碼(當然stan並不是直接用SGD優化的)

def normal_lp(x,mu,sigma):
#雖然在stan里這個函數是包含常數的,但這裡把它棄掉
return -((x - mu)**2/(2*sigma**2)).sum()

y = Variable(torch.from_numpy(y2).float())
mu = Variable(torch.zeros(1),requires_grad=True)

def model(y):
target = Variable(torch.zeros(1))
target += normal_lp(y,mu,1)
return target

optimizer = torch.optim.SGD([mu],lr=0.01)
for epoch in range(100):
optimizer.zero_grad()
y = Variable(torch.from_numpy(y2).float())
target = model(y)
loss = -target
loss.backward()
optimizer.step()

mu

Variable containing:
0.3664
[torch.FloatTensor of size 1]

自動微分變分推斷(AVDI)

這裡的代碼參考的Fully Automatic Variational Inference of Differentiable Probability Models和Automatic Differentiation Variational Inference的描述,stan的對應源碼參考github.

基本上AVDI就是把有約束變數變成無約束變數,用正態分布表示這個無約束變數,然後優化後者的參數。

stan提供平均場和滿秩設定,在我們的例子中是等價的。

下面的pystan的實現:

fit = sm.vb(data = dict(y=y2, N = len(y2)))
np.mean(fit[sampler_params][0]),np.std(fit[sampler_params][0]) #當然由於這個標準差不是直接從變分參數里拿出來而是按stan的設計莫名其妙又「估計」一次所以並不精確,在多維情況下會更明顯
#(0.30312868383719116, 0.2286080403322702)

值得注意的是這個「隨機優化」的隨機性確實很強:

ppost_stan = []
for i in range(1000):
fit = sm.vb(data = dict(y=y2, N = len(y2)))
ppost_stan.append([np.mean(fit[sampler_params][0]),np.std(fit[sampler_params][0])])

_ppost_stan = np.clip(ppost_stan,0.0,1.0)
np.mean(np.array(_ppost_stan),axis=0) # stan這種不靠譜的東西當然日常出一些溢出異常值
#array([0.37015113, 0.22651429])

1000次推斷之均值顯示變分分布期望大約為0.37,標準差約達0.22.這個均值是為了降低隨機優化的隨機性,一般用的只是一次的優化結果,如上面的0.303和0.22

下面為pytorch實現

mu_q_mu = 0.0
mu_q_omega = 1.0 # omega = log(sigma)

def grad_q(mu_q_mu, mu_q_omega, q_size = 10):

mu_q_samples_eta = np.random.normal(size=q_size) # theta(constrained) -> xi(unconstrained) ~ normal -> eta(standarded) ~ Normal(0,1)
mu_q_samples = mu_q_samples_eta*np.exp(mu_q_omega) + mu_q_mu

mu_q_mu_grad = 0.0
mu_q_omega_grad = 0.0

for mu_q_samples_eta,mu_q_sample in zip(mu_q_samples_eta,mu_q_samples):
mu_q = Variable(torch.ones(1)*mu_q_sample, requires_grad=True)
target = normal_lp(y,mu_q,1)
target.backward()
#loss = -target
#loss.backward()
mu_q_mu_grad += mu_q.grad.data.numpy()

#print(target.data[0],mu_q.grad.data.numpy(), mu_q_samples_eta, np.exp(mu_q_omega))
mu_q_omega_grad += mu_q.grad.data.numpy() * mu_q_samples_eta #*np.exp(mu_q_omega)

mu_q_mu_grad /= q_size
mu_q_omega_grad /= q_size
mu_q_omega_grad *= np.exp(mu_q_omega)
mu_q_omega_grad += 1.0
return mu_q_mu_grad,mu_q_omega_grad

lr = 0.001

for epoch in range(100):
mu_q_mu_grad, mu_q_omega_grad = grad_q(mu_q_mu, mu_q_omega)
mu_q_mu -= mu_q_mu_grad * lr
mu_q_omega -= mu_q_omega_grad * lr

mu_q_mu
#array([0.36268085], dtype=float32)
np.exp(mu_q_omega)
#array([0.12641455], dtype=float32)

ppost_torch = []
for i in range(100):
for epoch in range(100):
mu_q_mu_grad, mu_q_omega_grad = grad_q(mu_q_mu, mu_q_omega)
mu_q_mu += mu_q_mu_grad * lr
mu_q_omega += mu_q_omega_grad * lr
ppost_torch.append([mu_q_mu,np.exp(mu_q_omega)])

np.mean(np.array(ppost_torch),axis=0)

array([[0.37196007],
[0.23125193]], dtype=float32)

可以看到與stan的結果差不多。

HMC採樣

參考了The No-U-Turn Sampler: Adaptively Setting Path Lengths in Hamiltonian Monte Carlo。HMC哈密頓蒙特卡洛採樣法為

下面的pystan用的是nuts採樣,不過在這個簡單例子里兩者表現應該差不多。

fit = sm.sampling(data = dict(y=y2, N = len(y2)))

print(fit)

Inference for Stan model: anon_model_3aaa1aff3be33470f8a5bfa56085d51c.
4 chains, each with iter=2000; warmup=1000; thin=1;
post-warmup draws per chain=1000, total post-warmup draws=4000.

mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
mu 0.37 5.7e-3 0.22 -0.05 0.21 0.37 0.52 0.81 1494 1.0
lp__ -8.03 0.02 0.67 -9.95 -8.2 -7.76 -7.59 -7.53 1664 1.0

Samples were drawn using NUTS at Mon Apr 9 09:01:32 2018.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at
convergence, Rhat=1).

下面是pytorch實現

def hmc_grad(mu):
#mu_q = Variable(torch.ones(1)*mu, requires_grad=True)
mu_q = Variable(torch.from_numpy(mu).float(), requires_grad=True)
target = normal_lp(y,mu_q,1)
target.backward()
return mu_q.data.numpy()

def hmc_L(mu):
mu_q = Variable(torch.from_numpy(mu).float(), requires_grad=True)
target = normal_lp(y,mu_q,1)
return target.data.numpy()

def hmc(mu_q,epsilon=0.01,L=20,M=100):
mu_q = np.array(mu_q)
sample = [mu_q]
for m in range(1,M):
r = np.random.normal()
#sample[m] = sample[m-1]
sample.append(sample[m-1])
mu_q_ = sample[m-1]
r_ = r
for i in range(L):
r_ = r_ + epsilon/2.0 * hmc_grad(mu_q_)
mu_q_ = mu_q_ + epsilon * r_
r_ = r_ + epsilon/2.0 * hmc_grad(mu_q_)
p = np.exp(hmc_L(mu_q_) -0.5*r_*r_)/ np.exp(hmc_L(sample[m-1])-0.5*r*r)
alpha = np.min([1,p])
if np.random.random() < alpha:
sample[m] = mu_q_
return sample

fit2 = hmc([0.36],M=1000)
np.mean(fit2),np.std(fit2)
#(0.34214126220486607, 0.22397032206471484)

stan報告 mu 的期望為0.37,標準差為0.22,pystan的HMC為0.34,0.22。結果一致。


推薦閱讀:
相关文章