MCMC using Hamiltonian dynamics
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)