本文重點介紹貝葉斯推斷中的MCMC方法,這是眾多方法中的一個,具體的分類可以看圖。基于估計的呢,是點估計,求出目標分布的極值。這里呢多說一句,貝葉斯的點估計是對后驗概率求導(
),而頻率學派的點估計呢是對似然概率求導(
)。基于采樣的呢是,對目標分布采樣,用樣本來表示目標分布. 另外,MCMC的理解,要參考兩個非常形象的比喻,尋找山峰和利用隨機計算圓的面積。
import scipy.stats as stats
import matplotlib.pyplot as plt
import numpy as np
# 生成數據 - 有兩組數據X服從true_lambda_1 的指數分布,Y服從true_lambda_2的指數分布,我們要考察的是他們的聯合概率分布的更新過程。
#
N=10 # sample size
true_lambda_1=1
true_lambda_2=3
data=np.concatenate(
[
stats.poisson.rvs(true_lambda_1,size=(N,1)),
stats.poisson.rvs(true_lambda_2,size=(N,1)),
],
axis=1
) # x和y各自抽樣一次,樣本大小是N
x=y=np.linspace(.01,5,100) # 參數lambda的所有可能取值
likelihood_x=np.array(
[
stats.poisson.pmf(data[:,0],xi) for xi in x
]
).prod(axis=1) # 第一組10個樣本同時出現,lambda的概率分布!!!
likelihood_y=np.array(
[
stats.poisson.pmf(data[0,:],yi) for yi in y
]
).prod(axis=1) # 第二組10個樣本同時出現,lambda的概率分布!!
L=np.dot(likelihood_x.reshape((-1,1)),likelihood_y.reshape((1,-1))) # likelihood乘積的矩陣,矩陣的每一個元素都是p(x|lambda)p(y|lambda),用來更新先驗形成后驗,直接乘以先驗就行了
print(L.shape)
(100, 100)
# 先驗分布 - 均勻分布
plt.subplot(121)
uni_x=stats.uniform.pdf(x,loc=0,scale=5)
uni_y=stats.uniform.pdf(x,loc=0,scale=5)
M=np.dot(uni_x.reshape((-1,1)),uni_y.reshape((1,-1)))
im=plt.imshow(M,interpolation="none",origin="lower",cmap=plt.cm.jet,extent=(0,5,0,5))
plt.scatter(true_lambda_2,true_lambda_1,c="k",s=50) # 注意縱坐標是lambda_2的分布
plt.title("uniform prior")
plt.xlabel("lambda_2")
plt.ylabel("lambda_1")
# 更新分布
plt.subplot(122)
plt.contour(x,y,M*L) # 畫等高線
im=plt.imshow(M*L,interpolation="none",origin="lower",cmap=plt.cm.jet,extent=(0,5,0,5))
plt.scatter(true_lambda_2,true_lambda_1,c="k",s=50) # 畫出真實值的位置
plt.title("postieror after 1 sampling")
plt.xlabel("lambda_2")
plt.ylabel("lambda_1")
Text(0, 0.5, 'lambda_1')
上面這個過程呢,其實是貝葉斯推斷的標準過程。包含了幾個要素:確定模型(x,y來自兩個不同的泊松分布),確定模型參數(兩個泊松分布的參數lambda_1,lambda_2),確定先驗(均采用均勻分布),采樣,更新。
其實,有了這個標準過程就已經可以解決問題了。MCMC呢,對這個過程進行了改進,規避掉了計算復雜度最高的分母。MCMC結合了兩個東西,第一個MC是markov chain,指的是當前值只依賴上一步的值。第二個MC呢,指的是monte carlo這是一個用根據分布獲取隨機數的東西,同時獲取出來的隨機數呢,又能反向勾勒出原分布的情況。蒙地卡羅還可以利用隨機來測量圓形或者不規則圖形面積哦,很重要的一個技巧,我們不需要知道原到底長什么樣,我們只需要知道某個點到底來自不來自于這個圓內就行了。MCMC是這樣一個核心思想(書上看到的一段話,因為寫的太好了,所以直接搬運一下):
首先明確一點,MCMC返回值不是后驗分布,而是后驗分布上的一些樣本點。我們可以把MCMC的搜索過程看做是尋找正確的山峰的過程,上面的圖也能看出來,概率大的地方就是形成了一個山峰,我們要找的就是這樣一個山峰。但是我們返回的是這座上上的一些石頭,用來表示這座山。我們可以把MCMC的搜索過程,想象成不斷地問每一塊石頭(樣本):你是不是來自我要找的那座山?如果是,那么就會沿著這塊石頭所指引的方向前進,如果不是就丟掉。最后我們把所有是那座山的石頭都作為返回值給出。關于MCMC,我找到了一個很好的入門短視頻MCMC
MCMC的具體流程呢是這樣的,整個過程和利用隨機測圓面積很像:
- 對參數
,先給一個先驗分布,并且給出一個最開始的初始值
- 利用馬爾科夫鏈得到下一個參數
作為下一步的參數。具體做法是利用一個叫Proposal distribution的東西,這個分布呢其實是markov chain里的transition function,它結合當前值給出下一個值的分布,比如說我們在這里用的就是很簡單的正態分布
,可以看出其實就是以當前參數作為期望,然后給一個方差,得到這個分布,然后從這個分布上采樣出下一個參數。所以說呀,這個玩意用什么分布其實都不重要,感覺就其實有點隨機了,主要是后面有拒絕和接受那一步進行把關,這一步主要就是要隨機給點(對應到測面積里,就是在隨機產生點)
- 然后我們要判斷這個
是不是來自我們要找的那個山,我們要決定是接受還是拒絕。我們定義一個接受率
。這個接受率其實挺好理解的,如何判斷一個參數是好還是壞呢?我們就看這個參數下,針對指定的evidence X給出的后驗概率是大還是小,如果大那就毫無疑問就直接接受,如果小我們也不一定都拒絕,要以一定的概率拒絕。什么概率呢?我們用一個0到1之間的均勻分布采樣一個數
,如果這個采樣出來的數比
大,那就接受,如果比它小呢,就拒絕(對應到圓面積里,這一步就是在判斷隨機產生的點是不是在圓內)
- 重復上述過程
下面的內容呢,我將參考這篇教程,首先用dummy data跑一遍MCMC,然后再用一個實際的例子跑一遍MCMC。代碼只用到numpy,scipy, matplotlib,沒有用到任何成熟的庫
#“”“Dummy Data Example。注意我們對acceptance 函數等式兩邊求了個log,不然計算機沒辦法計算”“”
import numpy as np
import scipy.stats as stats
# prepare data
mod1=lambda t:np.random.normal(10,3,t)
population=mod1(30000) # 總數據是30000個
observations=population[np.random.randint(0,30000,1000)] #假設只觀察到1000個數據
fig=plt.figure(figsize=(10,10))
ax=fig.add_subplot(1,1,1)
ax.hist(observations,bins=35)
ax.set_xlabel("Value")
ax.set_ylabel("Frequency")
ax.set_title("Distribution of Dummy Data")
mu=observations.mean()
print("mu=",mu)
mu= 10.177366628047666
# 為了簡化問題,我們只求參數sigma,mu我們通過觀察這1000組數據,用平均值表示mu。其實mu也可以用mcmc求,但是這只是一個簡單的例子,不想搞得太復雜
# proposal distribution - 同樣,我們采用一個很常用的proposal distribution,就是正態分布N(mu=sigma_current,sigma=1)
sample_proposal_distribution=lambda mu: np.random.normal(mu,0.5,1)
# function to calculate prior
def get_log_prior(theta):
"""注意,我們對先驗沒有要求,認為就是個均勻分布,而且我們整個過程中也沒有更新這個先驗,始終認為先驗就是均勻分布"""
sigma=theta["sigma"]
if sigma>0:
return 0
else:
return -np.inf
# function to calculate posterior
def get_log_posterior(theta,evidence,log_prior):
sigma=theta["sigma"]
likelihood=stats.norm.pdf(evidence,loc=9.8,scale=sigma)
log_likelihood=np.log(likelihood).sum()
return log_likelihood+log_prior
# acceptance function
def acceptance(current_theta,proposed_theta):
current_log_prior=get_log_prior(current_theta)
proposed_log_prior=get_log_prior(proposed_theta)
current_log_posterior=get_log_posterior(current_theta,observations,current_log_prior)
proposed_log_posterior=get_log_posterior(proposed_theta,observations,proposed_log_prior)
log_r=proposed_log_posterior-current_log_posterior
if log_r>1:
return True
else:
alpha=np.random.uniform(0,1)
if alpha<np.exp(log_r):
return True
else:
return False
def metrolpolis_hastings(initial_theta,n_samples):
count_samples=0
accepted_samples=[]
rejected_samples=[]
current_theta=initial_theta
while count_samples <n_samples:
proposed_sigma=sample_proposal_distribution(current_theta["sigma"])
proposed_theta={"sigma":proposed_sigma[0]}
accept_or_reject=acceptance(current_theta,proposed_theta)
if accept_or_reject is True:
count_samples+=1
accepted_samples.append(proposed_theta)
current_theta=proposed_theta
else:
rejected_samples.append(proposed_theta)
return accepted_samples,rejected_samples
initial_theta={"sigma":0.5}
n_samples=10000
accepted_samples,rejected_samples=metrolpolis_hastings(initial_theta,n_samples)
# visualize the trace of sigma
accepted_sigmas=[s["sigma"] for s in accepted_samples]
rejected_sigmas=[s["sigma"] for s in rejected_samples]
steps=range(len(accepted_sigmas))
plt.subplot(121)
plt.hist(accepted_sigmas,bins=30)
plt.title("Accepted values")
plt.subplot(122)
plt.hist(rejected_sigmas,bins=30,color="red")
plt.title("rejected falues")
Text(0.5, 1.0, 'rejected values')
最后附上更簡潔的完整代碼
"""
Author: xuqh
Created on 2022/1/5
This is a complete implementation of Metropolis-Hasting Algorithm (MCMC)
"""
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
def sample_from_proposal_distribution(current_theta):
current_sigma = current_theta["sigma"]
return {
"sigma": np.random.normal(current_sigma, 1, 1)[0]
}
def get_log_prior(theta):
"""log(p(theta))"""
sigma = theta["sigma"]
if sigma > 0:
return 0
else:
return -np.inf
def get_log_likelihood(evidence, theta):
"""log(p(evidence|theta))"""
sigma = theta["sigma"]
likelihood = stats.norm.pdf(evidence, loc=9.8, scale=sigma)
log_likelihood = np.log(likelihood).sum()
return log_likelihood
def get_log_posterior(evidence, theta):
"""log(p(theta))+log(p(evidence|theta))"""
log_prior = get_log_prior(theta)
log_likelihood = get_log_likelihood(evidence, theta)
return log_prior + log_likelihood
def accept(current_theta, proposed_theta, evidence):
current_log_posterior = get_log_posterior(evidence, current_theta)
proposed_log_posterior = get_log_posterior(evidence, proposed_theta)
r = proposed_log_posterior - current_log_posterior
if r > 1:
return True
else:
alpha = np.random.uniform(0, 1)
if alpha < np.exp(r):
return True
else:
return False
def metropolis_hastings(initial_theta, n_samples, evidence):
current_theta = initial_theta
n_accepted_samples = 0
accepted_samples = []
rejected_samples = []
while n_accepted_samples < n_samples:
proposed_theta = sample_from_proposal_distribution(current_theta)
if accept(current_theta, proposed_theta, evidence):
n_accepted_samples += 1
current_theta=proposed_theta
accepted_samples.append(proposed_theta)
else:
rejected_samples.append((n_accepted_samples, proposed_theta))
return accepted_samples, rejected_samples
if __name__ == '__main__':
# generate data
observations = np.random.normal(10, 3, 1000)
# metropolis-hastings
initial_theta = {
"sigma": 1.0
}
n_samples = 10000
accepted_samples, rejected_samples = metropolis_hastings(initial_theta, n_samples, observations)
# plot trace & target distribution
plt.subplot(2, 1, 1)
plt.title("trace of sigma")
accepted_xs = range(len(accepted_samples))
accepted_ys = [s["sigma"] for s in accepted_samples]
plt.plot(accepted_xs, accepted_ys, color="green")
rejected_xs = [i for i, s in rejected_samples]
rejected_ys = [s["sigma"] for i, s in rejected_samples]
plt.scatter(rejected_xs, rejected_ys, marker="x", color="red")
plt.subplot(2, 1, 2)
plt.title("target distribution")
plt.hist(accepted_ys, bins=30)
plt.show()
再贈送一個實際的例子,數據來自這個網站的monthly sunspot數據
"""
Author: xuqh
Created on 2022/1/5
Applicationo of MCMC and Metropolis-hastings algorithm in real-world cases
Case: Sunspot number (monthly) X
Assumption: X~Gamma(a,b)
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
def sample_from_proposal_distribution(theta):
a = theta['a']
b = theta["b"]
proposed_a = np.random.normal(a, 0.05, 5)[0]
proposed_b = np.random.normal(b, 0.05, 5)[0]
return {
"a": proposed_a,
"b": proposed_b
}
def get_data():
fpath = "data/SN_m_tot_V2.0.csv"
pdf = pd.read_csv(open(fpath), header=None, sep=";")
data = pdf[3].to_numpy()
return data
def get_log_prior(theta):
a = theta['a']
b = theta["b"]
if a <= 0 or b <= 0:
return -np.inf
else:
return 0
def get_log_likelihood(theta, evidence):
a = theta['a']
b = theta["b"]
likelihood = stats.gamma.pdf(evidence,a=a, scale=b, loc=0)
log_likelihood = np.log(likelihood).sum()
temp=likelihood
return log_likelihood
def get_log_posterior(theta, evidence):
log_prior = get_log_prior(theta)
log_likelihood = get_log_likelihood(theta, evidence)
return log_prior + log_likelihood
def accept(current_theta, proposed_theta, evidence):
current_log_posterior = get_log_posterior(current_theta, evidence)
proposed_log_posterior = get_log_posterior(proposed_theta, evidence)
log_r = proposed_log_posterior - current_log_posterior
if log_r > 1:
return True
else:
alpha = np.random.uniform(0, 1)
if alpha < np.exp(log_r):
return True
else:
return False
def metropolis_hastings(initial_theta, n_samples, evidence):
current_theta = initial_theta
n_accepted_samples = 0
accepted_samples = []
rejected_samples = []
while n_accepted_samples < n_samples:
proposed_theta = sample_from_proposal_distribution(current_theta)
if accept(current_theta, proposed_theta, evidence):
n_accepted_samples += 1
current_theta = proposed_theta
accepted_samples.append(proposed_theta)
else:
rejected_samples.append(proposed_theta)
return accepted_samples, rejected_samples
if __name__ == '__main__':
data = get_data()
data+=1 # remove zeros
# data=data[-1000:]
initial_theta = {
"a": 4,
"b": 10
}
n_samples = 1000
accepted_samples, rejected_samples = metropolis_hastings(initial_theta, n_samples, data)
plt.title("trace of a and b")
accepted_xs=[s["a"] for s in accepted_samples]
accepted_ys=[s["b"] for s in accepted_samples]
plt.plot(accepted_xs,accepted_ys)
rejected_xs=[s["a"] for s in rejected_samples]
rejected_ys=[s["b"] for s in rejected_samples]
plt.scatter(rejected_xs,rejected_ys,marker="x",color="red")
plt.show()