Neal R. M. , MCMC Using Hamiltonian Dynamics[J]. arXiv: Computation, 2011: 139-188.
@article{neal2011mcmc,
title={MCMC Using Hamiltonian Dynamics},
author={Neal, Radford M},
journal={arXiv: Computation},
pages={139--188},
year={2011}}
算法
先把算法列一下.
Input: 初始值, 步长
与leapfrog迭代次数
.
- 令
;
- 循环迭代直到停止条件满足(以下第
步):
- 从标准正态分布中抽取
,
,
.
- 重复
:
- 如果
:
- 计算接受概率
- 从均匀分布
中抽取
, 如果
,
, 否则
.
output: .
注: 1中的标准正态分布不是唯一的, 但是文中选的便是这个. 4中的在实际编写程序的时候可以省去.
符号说明
因为作者从物理方程的角度给出几何解释,所以这里给出的符号一般有俩个含义:
符号 | 概率 | 物理 |
---|---|---|
随机变量,服从我们所在意的分布 | 冰球的位置 | |
用以构造马氏链的额外的变量 | 冰球的动量(mv) | |
...与我们所在意的分布有关 | 冰球的势能 | |
... | 冰球的动能 | |
与我们所在意的分布有关 |
Hamilton方程
物理解释
Hamilton方程(,
是维度):
这个东西怎么来的, 大概是因为, 如果机械能守恒, 那么随着时间
的变化
应该是一个常数, 所以其关于t的导数应该是0.
其中.
而, 左边是速度
, 右边
不过,估计是先有的(2.2)再有的H吧, 就先这么理解吧. 需要一提的是, 通常定义为
其中是对称正定的, 后面我们可以看到, 这种取法与正态分布相联系起来.
此时:

一些性质
可逆 Reversibility
映射是一一的,这个我感觉只能从物理的解释上理解啊, 一个冰球从一个点到另一个点, 现在H确定, 初值确定, 不就相当于整个轨迹都确定了吗, 那从哪到哪自然是一一的, 也就存在逆
, 且只需:
H的不变性
即

当然, 因为我们的算法是离散化的, 所以这个性质只是在比较小的时候近似保持.
保体积 Volume preservation
即假设区域在映射
的作用下为
, 则二者的体积相同, 均为
.
定义
其中. 又
其中
所以
又对于任意方阵,

所以
且, 于是
又
对于, 我们都可以类似的证明, 所以是常数.
这部分的证明参考自
辛 Symplecticness
离散化Hamilton方程 leapfrog方法
下面四幅图, 是, 起始点为
.
Euler's method
如果假定,
Modified Euler's method
仅有一个小小的变动,
Leapfrog method
注意到, 在实际编写程序的时候, 除了第一步和最后一步, 我们可以将的俩个半步合并成一步.
另外从右下角的图可以发现, 因为离散化的缘故, 的值是有偏差的. 但是Leapfrog 方法和 modified Euler方法都是保体积的, 因为每步更新都只改变一个量, 可以验证其雅可比行列式为1.
MCMC
概率与Hamiltonian, 正则(canonical)分布
如何将分布于Hamilton方程联系在一起? 假如, 我们关心的是的分布
, 则我们构造一个容易采样的分布
,
其中是规范化的常数,
一般取1. 从(3.2)中容易得到
. 事实上此时
是独立的(这么写是说明直接构造
也是可以的), 则可以分别
在贝叶斯统计中, 有
其中为数据,
为似然函数, 与文章中不同, 文章中是
, 应该是笔误.
HMC算法
就是开头提到的算法, 但是其中有一些地方值得思考. (alg.4)我们令, 这一步在实际中是不起作用的, 既然
而且在下轮中我们重新采样
, 我看网上的解释是为了理论, 取反这一部分使得proposal是对称的, 是建议分布
? 不是很懂.
有点明白了, 首先因为Leapfrog是确定的, 所以非0即1:
为了, 如果不取反肯定不行, 因为他就会往下走, 取反的操作实际上就是在可逆性里提到的, 在同样的操作下,
会回到
. 于是MH接受概率就退化成了M接受概率. 但是前文也提到了, 取反的操作, 只有在
的情况下是成立的.
HMC保持正则分布不变的证明 detailed balance
假设是
空间的一个分割, 其在L次leapfrog的作用, 及取反的操作下的像为
, 由于可逆性,
也是一个分割, 且有相同的体积
(保体积性),则
实际上是时候是显然的, 因为二者都是0. 因为
是连续函数, 当
变得很小的时候,
在
区域上的值相当于常数
, 于是

所以(3.7)成立.
Detailed balance:
其中是当前状态属于, 拒绝提议的的概率. 注意. 看上面的连等式可能会有点晕, 注意到, 左端实际上是概率, 最右端是, 这样就能明白啥意思了.
遍历性 Ergodicty
马氏链具有遍历性才会收敛到一个唯一的分布上(这部分不了解), HMC是具有这个性质的, 只要和
选的足够好. 但是如果选的不过也会导致坏的结果, 比如上面的图,
, 如果我们选择了
, 那么我们的Leapfrog总会带我们回到原点附近, 这就会导致比较差的结果.
HMC的一个例子及优势
下图是:

.
相关系数由改为
,
, 随机游走取的协方差矩阵为对角阵, 标准差为
, HMC生成
的为标准正态分布.
文章中提到, HMC较Randomwalk的优势在于,Randomwalk对协方差很敏感, 而且太大会导致接受率很低, 太小俩俩之间的相关性又会太高.
HMC在实际中的应用和理论
线性变换
有些时候, 我们会对变量施加线性映射(
非奇异方阵), 此时新的密度函数
, 其中
是
的密度函数, 相应的我们需要令
.
如果我们希望线性变化前后不会是的情况变得"更糟", 一个选择是,则
, 如果
, 则

其中. 此时的更新会使得原先的的更新保持, 即
所以本质上按照原来的轨迹发生着变化.
设想, 我们对的协方差矩阵有一个估计
, 且近似服从高斯分布, 我们可以对其做Cholesky分解
, 并且令
, 则
的各分量之间就相互独立了, 那么我们很自然的一个选择是
, 那么
的各分量的独立性能够保持.
另一个做法是, 保持不变, 但是
, 此时
, 则相当于
所以俩个方法是等价的.
HMC的调整
HMC对的选择比较严苛.
预先的实验
我们可以对一些进行实验, 观察轨迹, 虽然这个做法可能产生误导, 另外在抽样过程中随机选择
是一个不错的选择.
stepsize
的选择很关键, 如果太大, 会导致低的接受率, 如果太小, 不仅会造成大量的计算成本, 且如果此时
也很小, 那么HMC会缺乏足够的探索.
考虑下面的例子:

每一次leapfrog将映射为, 则

是否稳定, 关键在于系数矩阵的特征值的模(?还是实部)是否小于1, 特征值为

当的时候,(4.6)有一个实的大于1的特征值, 当的时候, 特征值是复制, 且模为:

所以, 我们应当选择.
在多维问题中, ,如果
且协方差矩阵为
, 我们可以取协方差矩阵的最小特征值(非零?)作为步长. 如果
, 我们可以通过线性变化将其转换再考虑.
tracjectory length
如何选择也是一个问题, 我们需要足够大的
使得每次的探索的足够的, 以便能够模拟出独立的样本, 但是正如前面所讲, 大的
不仅会带来计算成本, 而且可能会导致最后结果在起点附近(由周期性带来的麻烦). 而且
没法通过轨迹图正确的选择. 一个不错的想法是在一个小的区间内随机选择
, 这样做可能会减少由于周期性带来的麻烦.
多尺度
我们可以利用的缩放信息, 为不同的
添加给予不同的
. 比方说在
的前提下, 应该对
放大
倍, 即
(
不变).
等价的, 可以令(
不变), 相当于
, 则
, 所以
. 这么做就相当于一次leapfrog为:
结合HMC与其它MCMC
当我们所关心的变量是离散的, 或者其对数概率密度()的导数难以计算的时候, 结合其它MCMC是有必要的.
Scaling with dimensionality
的情况
如果, 且
之间相互独立(?), 这种假设是可行的, 因为之前已经讨论过, 对于
其协方差矩阵为
, 我们可以通过线性变化使其对角化, 且效能保持.
Cruetz指出, 任何的Metropolis形式的算法在采样密度函数的时候都满足:

其中表现在的状态, 而表提议. 则根据Jensen不等式可知:
所以,
在的情况下, 令
,
或者
. 对于整个状态, 我们则用
表示, 则
是所有
的和. 既然
的平均值均为正, 这会导致接受概率
的减小(随着维度的增加), 除非以减小步长作为代价, 或者建议分布的宽度进一步降低(即
尽可能在一个区域内).
因为, 再根据(4.17)得:
故的方差约是均值的两倍(
足够小的时候), 类似的也作用与
. 为了有一个比较好的接受率, 我们应当控制
的均值在1左右(小于?), 此时
.
HMC的全局误差(标准差)在级别, 所以
应当在
级别, 所以
也应当在
级别, 则
在
级别上, 所以为了保持均值为1左右, 我们需要令
正比于
, 相应的
为
.
文章中还有关于Randomwalk的分析, 这里不多赘述了.
HMC的扩展和变种
这个不一一讲了, 提一下
分割
假如能够分解成:

那么我们可以一个一个来处理, 相当于. 这个做法依旧是保体积的(既然每一个算子都是保体积的), 但是如果希望其可逆(对称), 这就要求 这个有很多应用场景:
比如, 其中计算
如果是容易的, 我们可以作如下分解:

此时有项, , , . 此时对于中间部分, 相当于步长变小了,误差自然会小.
当处理大量数据, 并用到似然函数的时候:
不过文章中说这个分解是不对称的, 可明明是对称的啊.
处理约束
有些时候, 我们对有约束条件, 比方
等等, 直接给算法:
Langevin method, LMC
假设我们使用,
从标准正态分布中采样, 每次我们只进行一次leapfrog变计算接受概率, 即
以及其接受概率:

Windows of states
这个方法试图将的曲线平滑来提高接受率.
我们可以通过任意构造一列
. 首先从
中等概率选一个数, 这个数代表
在序列中的位置, 记为
, 则其前面的可以通过leapfrog (
)产生, 后面的通过leapfrog(
)产生. 所以任意列的概率密度为:

然后从出发, 通过L-W+1步leapfrog(
)获得
并定义提议序列为
,计算接受概率:

其中. 设拒绝或者接受后的状态为:, 依照概率

抽取, 这个就是后的下一个状态.
文中说这么做分布不变, 因为:

如果没理解错, 前面的部分就是出现在最开始的序列中的概率, 但是中间的接受概率哪里去了? 总不能百分百接受吧...
代码
import numpy as np
from collections.abc import Iterator, Callable
from scipy import stats
class Euler:
"""
Euler方法
"""
def __init__(self, grad_u, grad_k, epsilon):
self.grad_u = grad_u
self.grad_k = grad_k
self.epsilon = epsilon
def start(self, p, q, steps):
trajectory_p = [p]
trajectory_q = [q]
for t in range(steps):
temp = p - self.epsilon * self.grad_u(q) # 更新一步P
q = q + self.epsilon * self.grad_k(p) # 更新一步q
p = temp
trajectory_p.append(p)
trajectory_q.append(q)
return trajectory_p, trajectory_q
def modified_start(self, p, q, steps):
"""
启动!
:param p:
:param q:
:param steps: 步数
:return: p, q
"""
trajectory_p = [p]
trajectory_q = [q]
for t in range(steps):
p = p - self.epsilon * self.grad_u(q) #更新一步P
q = q + self.epsilon * self.grad_k(p) #更新一步q
trajectory_p.append(p)
trajectory_q.append(q)
return trajectory_p, trajectory_q
class Leapfrog:
"""
Leapfrog 方法
"""
def __init__(self, grad_u, grad_k, epsilon):
self.grad_u = grad_u
self.grad_k = grad_k
self.epsilon = epsilon
self.trajectory_q = []
self.trajectory_p = []
def start(self, p, q, steps):
self.trajectory_p.append(p)
self.trajectory_q.append(q)
for t in range(steps):
p = p - self.epsilon * self.grad_u(q) / 2
q = q + self.epsilon * self.grad_k(p)
p = p - self.epsilon * self.grad_u(q) / 2
self.trajectory_q.append(q)
self.trajectory_p.append(p)
return p, q
class HMC:
"""
HMC方法
start为进行一次
accept_prob: 计算接受概率
hmc: 完整的过程
acc_rate: 接受概率
"""
def __init__(self, grad_u, grad_k, hamiltonian, epsilon):
assert isinstance(grad_u, Callable), "function needed..."
assert isinstance(grad_k, Callable), "function needed..."
assert isinstance(hamiltonian, Callable), "function needed..."
self.grad_u = grad_u
self.grad_k = grad_k
self.hamiltonian = hamiltonian
self.epsilon = epsilon
self.trajectory_q = []
self.trajectory_p = []
def start(self, p, q, steps):
self.trajectory_p.append(p)
self.trajectory_q.append(q)
p = p - self.epsilon * self.grad_u(q) / 2
for t in range(steps-1):
q = q + self.epsilon * self.grad_k(p)
p = p - self.epsilon * self.grad_u(q)
q = q + self.epsilon * self.grad_k(p)
p = p - self.epsilon * self.grad_u(q) / 2
p = -p
return p, q
def accept_prob(self, p1, q1, p2, q2):
"""
:param p1: 原先的
:param q1:
:param p2: 建议的
:param q2:
:return:
"""
p1 = np.exp(self.hamiltonian(p1, q1))
p2 = np.exp(self.hamiltonian(p2, q2))
alpha = min(1, p1 / p2)
return alpha
def hmc(self, generate_p, q, iterations, steps):
assert isinstance(generate_p, Iterator), "Invalid generate_p"
self.trajectory_q = [q]
p = next(generate_p)
self.trajectory_p = [p]
count = 0.
for t in range(iterations):
tempp, tempq = self.start(p, q, steps)
if np.random.rand() < self.accept_prob(p, q, tempp, tempq):
p = tempp
q = tempq
self.trajectory_p.append(p)
self.trajectory_q.append(q)
count += 1
p = next(generate_p)
self.acc_rate = count / iterations
return self.trajectory
@property
def trajectory(self):
return np.array(self.trajectory_p), \
np.array(self.trajectory_q)
class Randomwalk:
"""
walk: 完整的过程, 实际上Metropolis更新似乎就是start中steps=1,
一开始将文章的意思理解错了, 不过将错就错, 这样子也能增加一下灵活性.
"""
def __init__(self, pdf, sigma):
assert isinstance(pdf, Callable), "function needed..."
self.pdf = pdf
self.sigma = sigma
self.trajectory = []
def start(self, q, steps=1):
for t in range(steps):
q = stats.multivariate_normal.rvs(mean=q,
cov=self.sigma)
return q
def accept_prob(self, q1, q2):
"""
:param q1: 原始
:param q2: 建议
:return:
"""
p1 = self.pdf(q1)
p2 = self.pdf(q2)
alpha = min(1, p2 / p1)
return alpha
def walk(self, q, iterations, steps=1):
self.trajectory = [q]
count = 0.
for t in range(iterations):
temp = self.start(q, steps)
if np.random.rand() < self.accept_prob(q, temp):
q = temp
count += 1
self.trajectory.append(q)
self.acc_rate = count / iterations
return np.array(self.trajectory)
网友评论