概率图模型深度实战:从贝叶斯网络、马尔可夫随机场与因子图到变量消去、置信传播与 Chow-Liu 树状结构学习的完整工程链路

概率图模型(Probabilistic Graphical Models, PGM)是不确定性的"通用语言":它用一张图把高维联合分布 $P(x_1,\dots,x_n)$ 拆解成若干局部因子的乘积,既压缩了参数、又显式编码了条件独立性。从贝叶斯网络(有向)、马尔可夫随机场(无向)到把二者统一起来的因子图,PGM 是现代统计推理与机器学习的共同底座——VAE 的变分下界、扩散模型的马尔可夫链、图神经网络的信息传递、乃至 Transformer 的注意力,都可以在图模型的镜头下被重新理解。本文从第一性原理出发,用可运行的纯 NumPy 代码把精确推断(变量消去、信念传播)、近似推断(吉布斯采样)与结构学习(Chow-Liu 树)串成一条完整工程链路。

一、为什么需要图模型:用图表达条件独立性

直接建模 $n$ 个变量的联合分布需要 $2^n-1$ 个参数(二值情形),这很快变得不可行。图模型的全部价值在于:用图的拓扑替我们"记住"哪些变量相互独立,从而把联合分布写成局部因子的乘积。

  • 有向图(贝叶斯网络):每个节点给定其父节点后条件独立于所有非后代,于是 $$P(x_1,\dots,x_n)=\prod_{i=1}^n P(x_i\mid \mathrm{pa}(x_i))$$ 独立性由 d-分离(d-separation) 判定:一条路径被"阻断"当且仅当它经过一个给定节点的"汇聚(v-结构除外)"或其父节点已被观测。
  • 无向图(马尔可夫随机场):用团(clique)上的势能函数 $\phi_c$ 定义吉布斯分布 $$P(\mathbf{x})=\frac{1}{Z}\prod_{c}\phi_c(\mathbf{x}_c),\qquad Z=\sum_{\mathbf{x}}\prod_c\phi_c(\mathbf{x}_c)$$ 局部条件独立性由 Hammersley-Clifford 定理保证:正概率分布满足马尔可夫性 当且仅当 它可写成如上团势能乘积。
  • 因子图:把变量节点与因子节点画成二分图,是上述两者的统一表示,也是消息传递算法的天然舞台。

下面我们用一个经典小网络把"有向 → 联合分布 → 精确推断"跑通。

二、贝叶斯网络:有向因式分解与 Sprinkler 实例

Pearl 的 Sprinkler 网络是入门标准案例:天气阴(C)同时影响洒水器(S)与降雨(R),而草地是否湿(W)由 S 与 R 共同决定($C\to S,\; C\to R,\; S\to W,\; R\to W$)。我们先用一个 Factor 数据结构装载条件概率表(CPT),再暴力枚举验证联合分布归一化、并算出一个具体边缘:


import itertools

class Factor:
    def __init__(self, vars_, table):
        self.vars = list(vars_)
        self.table = {tuple(k): float(v) for k, v in table.items()}
    def get(self, assign):
        return self.table.get(tuple(assign[v] for v in self.vars), 0.0)

# Pearl 经典 Sprinkler 网络: C->S, C->R, S->W, R->W
pC    = Factor(('C',), {(1,):0.5, (0,):0.5})
pSgC  = Factor(('S','C'), {(1,1):0.1,(0,1):0.9,(1,0):0.5,(0,0):0.5})
pRgC  = Factor(('R','C'), {(1,1):0.8,(0,1):0.2,(1,0):0.2,(0,0):0.8})
pWgSR = Factor(('W','S','R'), {
    (1,1,1):0.99,(0,1,1):0.01,(1,1,0):0.90,(0,1,0):0.10,
    (1,0,1):0.90,(0,0,1):0.10,(1,0,0):0.01,(0,0,0):0.99})
factors = [pC, pSgC, pRgC, pWgSR]
allvars = ['C','S','R','W']
joint = lambda a: pC.get(a)*pSgC.get(a)*pRgC.get(a)*pWgSR.get(a)

pw1 = sum(joint({v:val for v,val in zip(allvars,s)})
          for s in itertools.product([0,1], repeat=4) if s[3]==1)
tot = sum(joint({v:val for v,val in zip(allvars,s)})
          for s in itertools.product([0,1], repeat=4))
print("P(W=1) brute = %.4f" % pw1)
print("sum joint = %.6f (should be 1)" % tot)

运行结果:P(W=1)=0.6500、联合分布求和恰为 1.000000。这说明我们装进去的因子确实是合法的联合分布——草地湿的概率是 65%,符合直觉(阴天既增降雨又减洒水,但降雨对 W 的影响占主导)。

三、变量消去:精确推断的通用引擎

暴力枚举随变量数指数爆炸。变量消去(Variable Elimination, VE)是精确推断的核心:对要消去的变量 $z$,先把所有涉及 $z$ 的因子相乘做一次"联结(join)",再沿 $z$ 求和(边际化)得到新因子,如此反复。关键在于因子相乘时必须只合并共享变量取值一致的条目,否则会把不一致的组合错误混入:


# 沿用第二节的 Factor 类与 pC/pSgC/pRgC/pWgSR/factors 定义

def factor_mul(f1, f2):
    vars_ = list(dict.fromkeys(list(f1.vars)+list(f2.vars)))
    shared = set(f1.vars) & set(f2.vars)
    table = {}
    for a1, v1 in f1.table.items():
        d1 = dict(zip(f1.vars, a1))
        for a2, v2 in f2.table.items():
            d2 = dict(zip(f2.vars, a2))
            if all(d1[s] == d2[s] for s in shared):   # 仅合并共享变量一致的条目
                d = {**d1, **d2}
                k = tuple(d[v] for v in vars_)
                table[k] = table.get(k, 0.0) + v1*v2
    return Factor(vars_, table)

def sum_out(f, var):
    newvars = [v for v in f.vars if v != var]; table = {}
    for a, v in f.table.items():
        k = tuple(a[i] for i,vn in enumerate(f.vars) if vn != var)
        table[k] = table.get(k, 0.0) + v
    return Factor(newvars, table)

def variable_elimination(factors, query, evidence=None):
    evidence = evidence or {}; fs = []
    for f in factors:
        if any(v in evidence for v in f.vars):
            nt = {a:v for a,v in f.table.items()
                  if all(dict(zip(f.vars,a))[v2]==evidence[v2] for v2 in evidence if v2 in f.vars)}
            f = Factor(f.vars, nt)
        fs.append(f)
    elim = [v for v in set(sum([f.vars for f in fs], []))
            if v not in query and v not in evidence]
    for v in elim:
        rel = [f for f in fs if v in f.vars]; fs = [f for f in fs if v not in f.vars]
        g = rel[0]
        for f in rel[1:]: g = factor_mul(g, f)
        fs.append(sum_out(g, v))
    res = fs[0]
    for f in fs[1:]: res = factor_mul(res, f)
    for v in evidence:                # 证据变量也需消去,得到纯 query 边际
        if v in res.vars: res = sum_out(res, v)
    z = sum(res.table.values())
    return {k:v/z for k,v in res.table.items()}, z

dist, _ = variable_elimination(factors, query=['W'])
print("P(W=1) VE = %.4f" % dist[(1,)])
d1, _ = variable_elimination(factors, query=['W'], evidence={'C':1})
d0, _ = variable_elimination(factors, query=['W'], evidence={'C':0})
print("P(W=1|C=1) = %.4f   P(W=1|C=0) = %.4f" % (d1[(1,)], d0[(1,)]))

消去顺序不影响最终结果:VE 给出 P(W=1)=0.6500,与上一节的暴力枚举逐位吻合;而带入证据后 P(W=1|C=1)=0.7470(阴天 → 更易降雨 → 更湿)、P(W=1|C=0)=0.5530(晴天 → 更易洒水但仍更干),方向完全合理。VE 的代价由消元诱导宽度(induced width)决定——最坏仍是树宽的指数级,这正是下一节消息传递算法的动机。

四、马尔可夫随机场与势能函数:无向图的 Hammersley-Clifford

无向图不区分"因果方向",改用团势能表达"相容性"。最典型的实例是 Ising 模型:每个节点取 $\pm1$,相邻节点同号被奖励、外场 $h_i$ 偏向某符号。其配分函数 $Z$ 与边缘概率原则上需枚举全部 $2^N$ 个状态——我们用一个 8 节点链做暴力基准(之后用来校验消息传递):


import itertools, numpy as np
N = 8
h = np.array([0.5,-0.3,0.4,0.0,0.2,-0.4,0.3,-0.2])   # 非均匀外场,便于观察边缘差异
J = 1.0
vals = [-1, 1]
states = list(itertools.product(vals, repeat=N))
def energy(s):
    e = 0.0
    for i in range(N):
        e -= h[i]*s[i]
        if i < N-1: e -= J*s[i]*s[i+1]
    return e
Z = sum(np.exp(-energy(s)) for s in states)
marginals = [sum(np.exp(-energy(s)) for s in states if s[i]==1)/Z for i in range(N)]
print("Z = %.4f" % Z)
print("brute marginals P(s_i=+1):", [round(m,4) for m in marginals])

暴力枚举得到 Z=6386.6336 以及随外场变化的 8 个节点边缘概率(从 0.7284 到 0.4941)。只要节点数稍大,这一步就会因 $2^N$ 而立刻不可行——于是需要更聪明的办法。

五、因子图与和积算法:把信念传播统一起来

因子图把"变量"和"因子(势能)"画成二分图;和积(sum-product)算法就是在这张图上做消息传递:每条边上的消息是"把发送方一侧的子图边际掉、再沿边传递"的函数。对树(含链)结构,消息传递一次正反向即可给出所有节点的精确边缘——这正是信念传播(Belief Propagation, BP)。

我们在上面那条 Ising 链上实现 BP,并与第四节的暴力基准逐位对照:


# 沿用第四节的 h, J, vals, N
def node_pot(x, i): return np.exp(h[i]*x)
E = np.array([[np.exp(J*vals[a]*vals[b]) for b in range(2)] for a in range(2)])
mL2R = [None]*N; mR2L = [None]*N   # mL2R[i]: 消息 i->i+1 ; mR2L[i]: 消息 i->i-1
# 正向(左->右)
for i in range(N-1):
    inc = np.ones(2) if i == 0 else mL2R[i-1]
    mL2R[i] = np.array([sum(node_pot(vals[a],i)*inc[a]*E[a,b] for a in range(2)) for b in range(2)])
# 反向(右->左)
for i in range(N-1, 0, -1):
    inc = np.ones(2) if i == N-1 else mR2L[i+1]
    mR2L[i] = np.array([sum(node_pot(vals[b],i)*inc[b]*E[a,b] for b in range(2)) for a in range(2)])
# 汇总信念
bp = []
for i in range(N):
    left  = mL2R[i-1] if i>0   else np.ones(2)
    right = mR2L[i+1] if i<N-1 else np.ones(2)
    b = np.array([node_pot(vals[a],i)*left[a]*right[a] for a in range(2)]); b /= b.sum()
    bp.append(b[1])
print("BP marginals P(s_i=+1):", [round(m,4) for m in bp])
print("max|BP-brute| = %.2e" % max(abs(a-b) for a,b in zip(marginals, bp)))

BP 边缘与暴力基准的差距仅 6.66e-16——在链(树)上信念传播给出逐位精确的边缘,却把复杂度从 $O(2^N)$ 降到 $O(N)$。把求和换成取最大值,同一套消息框架立刻变成 max-product / 最大后验(MAP)推断,用于寻找最可能的全局配置;这正是后文与深度学习映射的钥匙。

六、Chow-Liu 树:从数据中学出树状贝叶斯网络

前面都是"结构已知、求推断"。现实中结构也要学。当假设依赖结构是一棵树时,Chow-Liu 算法给出最优树:任意两两变量的互信息 $I(X;Y)$ 作为边权,取最大生成树即得。直觉是"信息量最大的边最该相连"。我们用一个已知链状网络 $A\to B\to C\to D$ 造数据来检验:


import numpy as np
rng = np.random.default_rng(0)
def sample():
    a = int(rng.random()<0.5)
    b = int(rng.random()<(0.9 if a else 0.1))
    c = int(rng.random()<(0.9 if b else 0.1))
    d = int(rng.random()<(0.9 if c else 0.1))
    return (a,b,c,d)
data = np.array([sample() for _ in range(30000)])
vars_ = ['A','B','C','D']
def mi(i,j):
    cxy=np.zeros((2,2)); cx=np.zeros(2); cy=np.zeros(2)
    for r in data:
        cxy[r[i],r[j]]+=1; cx[r[i]]+=1; cy[r[j]]+=1
    n=len(data); I=0.0
    for a in range(2):
        for b in range(2):
            if cxy[a,b]>0:
                pxy=cxy[a,b]/n; px=cx[a]/n; py=cy[b]/n
                I += pxy*np.log(pxy/(px*py))
    return I
edges = sorted(((mi(i,j),i,j) for i in range(4) for j in range(i+1,4)), reverse=True)
parent=list(range(4))
def find(x):
    while parent[x]!=x: parent[x]=parent[parent[x]]; x=parent[x]
    return x
mst=[]
for w,i,j in edges:
    if find(i)!=find(j):
        parent[find(i)]=find(j); mst.append((vars_[i],vars_[j],round(float(w),4)))
print("pairwise MI:", [(vars_[i]+'-'+vars_[j], round(float(w),3)) for w,i,j in edges])
print("Chow-Liu tree:", mst)

互信息排序显示沿链的三条边($A\!-\!B,\;B\!-\!C,\;C\!-\!D$,约 0.37 bit)显著高于跨节点边($A\!-\!C,\;B\!-\!D$ 约 0.22,由 Markov 性介导)。最大生成树精确还原出 A-B / B-C / C-D 这条链——证明 Chow-Liu 能从纯数据中恢复出真实依赖骨架(再选一个根定向即得到树状贝叶斯网络)。

七、吉布斯采样:当精确推断不可行时

对一般的(含环)图,精确推断是 #P 难的,于是退而求其次做近似推断。马尔可夫链蒙特卡洛(MCMC)通过在状态空间上游走、使稳态分布等于目标分布来采样。其中 吉布斯采样最简:每个变量在其马尔可夫毯(Markov blanket)条件下依次重采样。回到 Sprinkler 网络,我们用吉布斯采样估计 P(W=1) 并与 VE 精确值对照:


# 沿用第二节的 Factor 类与 pC/pSgC/pRgC/pWgSR 定义
import numpy as np
def unnorm(s): return pC.get(s)*pSgC.get(s)*pRgC.get(s)*pWgSR.get(s)
rng = np.random.default_rng(1)
def gibbs(iters, burn=3000):
    st = {'C':1,'S':1,'R':1,'W':1}; w=0; tot=0
    for t in range(iters):
        for v in ['C','S','R','W']:
            s1=dict(st); s1[v]=1; s0=dict(st); s0[v]=0
            p1=unnorm(s1); p0=unnorm(s0)
            st[v]=1 if rng.random()<p1/(p1+p0) else 0
        if t>=burn: w+=st['W']; tot+=1
    return w/tot
print("Gibbs P(W=1) = %.4f   exact VE = 0.6500" % gibbs(25000))

吉布斯估计 0.6365,与精确值 0.6500 仅差 0.0135——在 25000 步(含 3000 步 burn-in)内已相当接近。吉布斯采样的精度由混合速度(mixing)决定:变量间耦合越强、环越多,链越需要更久才能遍历稳态,这正是近似推断的工程痛点(可换用折叠吉布斯、切片采样或变分推断缓解)。

八、与 AI / 深度学习的精确映射表

概率图模型不是"另一种模型",而是深度学习诸多组件的图论母题:

深度学习概念 概率图模型对应
VAE / 扩散模型 隐变量图模型 + 变分推断(ELBO = 负自由能)
扩散模型去噪 沿时间的马尔可夫随机场(链式 MRF)
图神经网络 (GNN) 信念传播的消息传递 = sum-product
Transformer 注意力 在 token 图上跑 BP / 能量模型
玻尔兹曼机 / RBM Ising / Potts 势能网络
贝叶斯深度学习 带神经网络势能的 PGM
归一化流 图上变量的可逆变量替换
结构学习 从数据学图结构(Chow-Liu / 打分搜索)

关键洞察:深度学习训练与推理的许多"黑箱"动作,本质都是图模型里"求和/求最大/采样"三种原语的特例——注意力是消息传递,扩散是链式 MRF 的逐帧条件采样,VAE 是带神经势能的变分推断。理解了 PGM,就把这些看似分散的技术收束到了"图 + 因子 + 消息"的统一框架里。

九、工程边界与陷阱

  • 配分函数 $Z$ 难以计算:无向图 $Z$ 是 $2^N$ 求和,需用 BP / 重要性采样 / 退火近似;
  • 环上的 BP 不保证收敛:loopy BP 在含环图上只是近似,可能振荡或不收敛;
  • 消元顺序决定复杂度:VE 代价取决于诱导树宽,需先做三角化(triangulation)选优序(junction tree);
  • MCMC 混合瓶颈:强耦合 / 多模分布下吉布斯链混合极慢,需更聪明的提议分布;
  • 结构学习不可辨识:等价类(I-equivalence)下多个图给出相同分布,Chow-Liu 仅覆盖树假设;
  • 条件独立性不可验证:d-分离给的是"图蕴含的"独立性,真实数据未必满足。

小结

概率图模型用"图 = 条件独立性"这一条原则,统一了贝叶斯网络、马尔可夫随机场与因子图,并派生出变量消去、信念传播、吉布斯采样与 Chow-Liu 结构学习四条主线。从 Sprinkler 网络的联合分布与精确边缘(0.6500,VE 与暴力逐位吻合),到 Ising 链上 BP 与基准误差 6.66e-16 的精确验证,再到从数据还原 A-B-C-D 链、用吉布斯采样逼近精确值,本文用纯 NumPy 把整条工程链路跑通。它既是已发的凸优化(推理即优化)、信息论(互信息 / KL)与数值分析的方法支柱,也是理解 VAE、扩散模型、GNN 与注意力的统一透镜——当你下次调用 model.sample() 或 attention() 时,本质上是在一张看不见的因子图上做求和、求最大与采样。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部