《机器学习导论》第 14 章 -图方法

目录

正文

[14.1 引言](#14.1 引言)

[14.2 条件独立的典型情况](#14.2 条件独立的典型情况)

[典型场景(3 种核心结构)](#典型场景(3 种核心结构))

代码运行效果

[14.3 生成模型](#14.3 生成模型)

完整代码:贝叶斯网络生成样本

核心说明

[14.4 d 分离](#14.4 d 分离)

[完整代码:d 分离判断 + 可视化](#完整代码:d 分离判断 + 可视化)

核心比喻

[14.5 信念传播](#14.5 信念传播)

[14.5.1 链(最简单的 BP)](#14.5.1 链(最简单的 BP))

[14.5.2 树](#14.5.2 树)

[14.5.3 多树](#14.5.3 多树)

[14.5.4 结树](#14.5.4 结树)

完整代码:链式结构的信念传播

核心说明

[14.6 无向图:马尔科夫随机场](#14.6 无向图:马尔科夫随机场)

完整代码:马尔科夫随机场(图像去噪示例)

核心比喻

[14.7 学习图模型的结构](#14.7 学习图模型的结构)

完整代码:基于互信息的简单结构学习

核心说明

[14.8 影响图](#14.8 影响图)

完整代码:影响图(带伞决策示例)

[14.9 注释](#14.9 注释)

[14.10 习题](#14.10 习题)

[14.11 参考文献](#14.11 参考文献)

思维导图

总结


正文

大家好!今天我们来拆解《机器学习导论》第 14 章的核心内容 ------图方法 。图模型是机器学习中处理复杂概率关系的 "神器",它把变量之间的依赖关系用图形直观表示,就像给概率关系画 "思维导图",让复杂的条件独立、概率推理变得一目了然。

本文会避开繁杂公式,用通俗的语言讲解核心概念,每个关键知识点都搭配可直接运行的完整 Python 代码 +可视化对比图,Mac 系统也能完美显示中文,新手也能轻松上手!

14.1 引言

图方法本质是 "用图形表示概率模型":把每个随机变量当成节点,变量之间的依赖 / 独立关系当成,通过图的结构直观表达复杂的概率分布。

比如:

你今天是否 "下雨"(节点 A)会影响是否 "带伞"(节点 B),但不会影响 "明天吃什么"(节点 C)------ 这种依赖 / 独立关系,用图能一眼看明白。

图模型主要分两类:有向图(贝叶斯网络)无向图(马尔科夫随机场),是处理不确定性推理的核心工具(比如语音识别、疾病诊断)。

14.2 条件独立的典型情况

条件独立是图模型的 "灵魂":若在给定 C 的情况下,A 和 B 的概率互不影响,就称 A 和 B条件独立(记作 A⊥B∣C)。

典型场景(3 种核心结构)

我们用代码可视化这 3 种结构,并验证条件独立特性:

复制代码
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from scipy.stats import multinomial

# Mac系统Matplotlib中文显示配置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# 定义3种条件独立结构的可视化函数
def plot_conditional_independence_structures():
    # 创建画布,子图布局:1行3列
    fig, axes = plt.subplots(1, 3, figsize=(18, 6))
    fig.suptitle('条件独立的3种典型结构', fontsize=16, fontweight='bold')
    
    # 1. 链式结构(A→C→B):给定C,A和B独立
    G1 = nx.DiGraph()
    G1.add_nodes_from(['A', 'C', 'B'])
    G1.add_edges_from([('A', 'C'), ('C', 'B')])
    pos1 = {'A': (0, 0), 'C': (1, 0), 'B': (2, 0)}
    nx.draw(G1, pos1, ax=axes[0], with_labels=True, node_color='lightblue', 
            node_size=2000, font_size=14, arrowsize=20)
    axes[0].set_title('链式结构(A→C→B)\n给定C,A⊥B', fontsize=12)
    
    # 2. 分叉结构(C→A, C→B):给定C,A和B独立
    G2 = nx.DiGraph()
    G2.add_nodes_from(['C', 'A', 'B'])
    G2.add_edges_from([('C', 'A'), ('C', 'B')])
    pos2 = {'C': (1, 1), 'A': (0, 0), 'B': (2, 0)}
    nx.draw(G2, pos2, ax=axes[1], with_labels=True, node_color='lightgreen', 
            node_size=2000, font_size=14, arrowsize=20)
    axes[1].set_title('分叉结构(C→A, C→B)\n给定C,A⊥B', fontsize=12)
    
    # 3. 汇结构(A→C←B):未给C,A和B独立;给定C,A和B不独立
    G3 = nx.DiGraph()
    G3.add_nodes_from(['A', 'B', 'C'])
    G3.add_edges_from([('A', 'C'), ('B', 'C')])
    pos3 = {'A': (0, 0), 'B': (2, 0), 'C': (1, 1)}
    nx.draw(G3, pos3, ax=axes[2], with_labels=True, node_color='salmon', 
            node_size=2000, font_size=14, arrowsize=20)
    axes[2].set_title('汇结构(A→C←B)\n未给C,A⊥B;给定C,A不⊥B', fontsize=12)
    
    plt.tight_layout()
    plt.show()

# 验证链式结构的条件独立(数值验证)
def verify_chain_independence():
    # 定义链式概率:P(A)=0.5, P(C|A)=[0.8(A=0),0.2(A=1)], P(B|C)=[0.7(C=0),0.3(C=1)]
    P_A = np.array([0.5, 0.5])  # A的取值:0/1
    P_C_given_A = np.array([[0.8, 0.2], [0.2, 0.8]])  # 行:A的值,列:C的值
    P_B_given_C = np.array([[0.7, 0.3], [0.3, 0.7]])  # 行:C的值,列:B的值
    
    # 计算联合概率 P(A,B,C) = P(A)*P(C|A)*P(B|C)
    joint = np.zeros((2,2,2))  # (A,B,C)
    for a in [0,1]:
        for c in [0,1]:
            for b in [0,1]:
                joint[a,b,c] = P_A[a] * P_C_given_A[a,c] * P_B_given_C[c,b]
    
    # 计算给定C=0时,P(A|B,C=0) 和 P(A|C=0),验证是否相等(条件独立)
    C = 0
    P_A_given_C = np.sum(joint[:, :, C], axis=1) / np.sum(joint[:, :, C])
    for b in [0,1]:
        P_A_given_BC = joint[:, b, C] / np.sum(joint[:, b, C])
        print(f"给定C={C}, B={b}时,P(A|BC) = {P_A_given_BC.round(2)}")
        print(f"给定C={C}时,P(A|C) = {P_A_given_C.round(2)}")
        print(f"是否相等(条件独立):{np.allclose(P_A_given_BC, P_A_given_C)}\n")

# 运行可视化和验证
if __name__ == "__main__":
    plot_conditional_independence_structures()
    verify_chain_independence()
代码运行效果
  1. 可视化图:3 个子图分别展示链式、分叉、汇结构,标注条件独立特性;
  2. 数值验证:输出 "给定 C=0 时,P (A|B,C) = P (A|C)",验证链式结构的条件独立。

14.3 生成模型

图模型的生成模型,核心是 "通过图结构的条件独立特性,分解联合概率分布,从而高效生成样本"。

完整代码:贝叶斯网络生成样本
复制代码
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Mac字体配置(重复配置避免子图失效)
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# 定义贝叶斯网络结构:下雨(R) → 带伞(U),下雨(R) → 堵车(T)
class BayesianNetworkGenerator:
    def __init__(self):
        # 定义条件概率表(CPT)
        self.P_R = np.array([0.3, 0.7])  # P(R=0:不下雨)=0.3, P(R=1:下雨)=0.7
        self.P_U_given_R = np.array([[0.9, 0.1], [0.1, 0.9]])  # U=0:不带伞, U=1:带伞
        self.P_T_given_R = np.array([[0.2, 0.8], [0.8, 0.2]])  # T=0:不堵车, T=1:堵车
    
    # 生成单个样本
    def generate_sample(self):
        # 1. 先采样"下雨"(无父节点)
        R = np.random.choice([0, 1], p=self.P_R)
        # 2. 基于R采样"带伞"
        U = np.random.choice([0, 1], p=self.P_U_given_R[R])
        # 3. 基于R采样"堵车"
        T = np.random.choice([0, 1], p=self.P_T_given_R[R])
        return R, U, T
    
    # 生成N个样本
    def generate_samples(self, N):
        samples = []
        for _ in range(N):
            samples.append(self.generate_sample())
        return np.array(samples)

# 可视化生成样本的分布(对比理论分布)
def visualize_generated_samples():
    # 初始化生成器
    bn = BayesianNetworkGenerator()
    # 生成10000个样本
    samples = bn.generate_samples(10000)
    R_samples = samples[:, 0]
    U_samples = samples[:, 1]
    T_samples = samples[:, 2]
    
    # 计算经验概率
    P_R_emp = np.mean(R_samples)
    P_U_given_R1_emp = np.mean(U_samples[R_samples==1])
    P_T_given_R1_emp = np.mean(T_samples[R_samples==1])
    
    # 理论概率
    P_R_theo = 0.7
    P_U_given_R1_theo = 0.9
    P_T_given_R1_theo = 0.2
    
    # 可视化对比(经验 vs 理论)
    fig, axes = plt.subplots(1, 3, figsize=(18, 6))
    fig.suptitle('贝叶斯网络生成样本:经验分布 vs 理论分布', fontsize=16, fontweight='bold')
    
    # 1. 下雨概率对比
    axes[0].bar(['经验值', '理论值'], [P_R_emp, P_R_theo], color=['lightblue', 'salmon'])
    axes[0].set_title('P(下雨)')
    axes[0].set_ylim(0, 1)
    axes[0].text(0, P_R_emp+0.05, f'{P_R_emp:.2f}', ha='center')
    axes[0].text(1, P_R_theo+0.05, f'{P_R_theo:.2f}', ha='center')
    
    # 2. 下雨时带伞的概率对比
    axes[1].bar(['经验值', '理论值'], [P_U_given_R1_emp, P_U_given_R1_theo], color=['lightblue', 'salmon'])
    axes[1].set_title('P(带伞|下雨)')
    axes[1].set_ylim(0, 1)
    axes[1].text(0, P_U_given_R1_emp+0.05, f'{P_U_given_R1_emp:.2f}', ha='center')
    axes[1].text(1, P_U_given_R1_theo+0.05, f'{P_U_given_R1_theo:.2f}', ha='center')
    
    # 3. 下雨时堵车的概率对比
    axes[2].bar(['经验值', '理论值'], [P_T_given_R1_emp, P_T_given_R1_theo], color=['lightblue', 'salmon'])
    axes[2].set_title('P(堵车|下雨)')
    axes[2].set_ylim(0, 1)
    axes[2].text(0, P_T_given_R1_emp+0.05, f'{P_T_given_R1_emp:.2f}', ha='center')
    axes[2].text(1, P_T_given_R1_theo+0.05, f'{P_T_given_R1_theo:.2f}', ha='center')
    
    plt.tight_layout()
    plt.show()

# 运行生成和可视化
if __name__ == "__main__":
    visualize_generated_samples()
核心说明

1.生成模型的关键是 "按图的拓扑顺序采样":先采无父节点的变量(如下雨),再采依赖它的变量(带伞、堵车);

2.可视化对比 "经验分布(生成样本)" 和 "理论分布",验证生成模型的正确性。

14.4 d 分离

d 分离(directional separation)是判断图模型中 "两个节点是否条件独立" 的通用方法,核心是:在图中,若一条路径被 "阻断",则两个节点 d 分离(即条件独立)

可以把 d 分离理解为:"变量之间的信息传递路径是否被堵住"------ 比如链式结构中,给定中间节点 C,A 到 B 的信息路径被堵,A 和 B 就 d 分离。

完整代码:d 分离判断 + 可视化
复制代码
import networkx as nx
import matplotlib.pyplot as plt

# Mac字体配置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# 判断d分离的简易实现(核心逻辑)
def is_d_separated(G, X, Y, Z):
    """
    G: 有向图(nx.DiGraph)
    X: 节点1
    Y: 节点2
    Z: 条件节点列表
    返回:True(d分离)/False(不d分离)
    """
    # 简化版逻辑:仅处理简单路径(适合教学)
    # 1. 找到X到Y的所有无向路径
    undirected_G = G.to_undirected()
    all_paths = list(nx.all_simple_paths(undirected_G, source=X, target=Y))
    
    # 2. 检查每条路径是否被Z阻断
    blocked = True
    for path in all_paths:
        path_blocked = False
        # 遍历路径中的每个节点(排除首尾)
        for i in range(1, len(path)-1):
            c = path[i]
            prev = path[i-1]
            next_node = path[i+1]
            
            # 情况1:链式/分叉结构,c在Z中 → 阻断
            if (G.has_edge(prev, c) and G.has_edge(c, next_node)) or (G.has_edge(c, prev) and G.has_edge(c, next_node)):
                if c in Z:
                    path_blocked = True
                    break
            # 情况2:汇结构,c不在Z中 → 阻断
            elif G.has_edge(prev, c) and G.has_edge(next_node, c):
                if c not in Z:
                    path_blocked = True
                    break
        
        if not path_blocked:
            blocked = False
            break
    return blocked

# 可视化d分离效果
def visualize_d_separation():
    # 创建复杂一点的图:A→C→B,A→D←B,D→E
    G = nx.DiGraph()
    nodes = ['A', 'B', 'C', 'D', 'E']
    edges = [('A','C'), ('C','B'), ('A','D'), ('B','D'), ('D','E')]
    G.add_nodes_from(nodes)
    G.add_edges_from(edges)
    
    # 子图1:原始图
    # 子图2:给定C,A和B d分离(路径A-C-B被阻断)
    # 子图3:给定D,A和B 不d分离(路径A-D-B被激活)
    fig, axes = plt.subplots(1, 3, figsize=(20, 6))
    fig.suptitle('d分离判断示例', fontsize=16, fontweight='bold')
    
    # 子图1:原始图
    pos = {'A': (0, 1), 'B': (2, 1), 'C': (1, 2), 'D': (1, 0), 'E': (1, -1)}
    nx.draw(G, pos, ax=axes[0], with_labels=True, node_color='lightgray', 
            node_size=2000, font_size=14, arrowsize=20)
    axes[0].set_title('原始图:A和B有两条路径\nA-C-B 和 A-D-B', fontsize=12)
    
    # 子图2:给定C,A和B d分离(C标红,路径A-C-B阻断)
    node_colors = ['lightgray' if n != 'C' else 'red' for n in nodes]
    nx.draw(G, pos, ax=axes[1], with_labels=True, node_color=node_colors, 
            node_size=2000, font_size=14, arrowsize=20)
    axes[1].set_title(f'给定Z=[C],A和B d分离?{is_d_separated(G, "A", "B", ["C"])}', fontsize=12)
    
    # 子图3:给定D,A和B 不d分离(D标红,路径A-D-B激活)
    node_colors = ['lightgray' if n != 'D' else 'red' for n in nodes]
    nx.draw(G, pos, ax=axes[2], with_labels=True, node_color=node_colors, 
            node_size=2000, font_size=14, arrowsize=20)
    axes[2].set_title(f'给定Z=[D],A和B d分离?{is_d_separated(G, "A", "B", ["D"])}', fontsize=12)
    
    plt.tight_layout()
    plt.show()

# 运行
if __name__ == "__main__":
    visualize_d_separation()
核心比喻

d 分离就像 "查酒驾":

  • 路径是 "道路" ,条件节点 Z 是**"交警"**;
  • 链式 / 分叉结构:交警站在中间节点,道路被封(阻断);
  • 汇结构:交警不在中间节点,道路通(不阻断);交警在中间节点,道路通(激活)。

14.5 信念传播

信念传播(BP)是图模型中 "概率推理" 的核心算法,目的是:在已知部分节点观测值的情况下,计算其他节点的后验概率

可以把信念传播理解为:"节点之间互相传递'消息',最终达成对概率的一致判断"。

14.5.1 链(最简单的 BP)

链式结构的 BP 是 "双向传递":从链的一端传到另一端,再传回来,最终计算每个节点的后验概率。

14.5.2 树

树结构的 BP 是链式 BP 的扩展:每个节点只向父 / 子节点传递消息,无环,能高效计算所有节点的后验。

14.5.3 多树

多树是 "有多个根的树",本质还是无环,BP 算法依然适用。

14.5.4 结树

结树(Junction Tree)是 "有环图" 的解决方案:把有环图转化为无环的结树,再用 BP 算法推理(核心是 "消环")。

完整代码:链式结构的信念传播
python 复制代码
import numpy as np
import matplotlib.pyplot as plt

# Mac字体配置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'


# 链式信念传播类(A→B→C→D)
class ChainBP:
    def __init__(self):
        # 定义条件概率表(CPT)
        self.P_A = np.array([0.6, 0.4])  # A=0的概率0.6,A=1的概率0.4
        # P_B_given_A[i,j] = P(B=j | A=i)
        self.P_B_given_A = np.array([[0.9, 0.1], [0.2, 0.8]])  
        # P_C_given_B[i,j] = P(C=j | B=i)
        self.P_C_given_B = np.array([[0.8, 0.2], [0.1, 0.9]])  
        # P_D_given_C[i,j] = P(D=j | C=i)
        self.P_D_given_C = np.array([[0.7, 0.3], [0.2, 0.8]])  

    # 前向传播(从A到D):计算各节点的先验信念和传递的消息
    def forward(self):
        # A节点无父节点,信念就是自身先验概率
        bel_A = self.P_A.copy()
        # 消息:A→B(直接传递A的信念)
        msg_A_to_B = bel_A
        # 计算B的信念(未归一化):sum_A P(A) * P(B|A)
        bel_B = np.dot(msg_A_to_B, self.P_B_given_A)
        # 消息:B→C(传递B的信念)
        msg_B_to_C = bel_B
        # 计算C的信念:sum_B P(B) * P(C|B)
        bel_C = np.dot(msg_B_to_C, self.P_C_given_B)
        # 消息:C→D(传递C的信念)
        msg_C_to_D = bel_C
        # 计算D的信念:sum_C P(C) * P(D|C)
        bel_D = np.dot(msg_C_to_D, self.P_D_given_C)
        
        # 修正:补充bel_A的定义后返回
        return msg_A_to_B, msg_B_to_C, msg_C_to_D, bel_A, bel_B, bel_C, bel_D

    # 后向传播(从观测节点反向修正各节点信念)
    # obs_node:观测节点(如'D'),obs_val:观测值(0/1)
    def backward(self, obs_node='D', obs_val=1):
        # 先执行前向传播,获取初始信念和消息
        msg_A_to_B, msg_B_to_C, msg_C_to_D, bel_A, bel_B, bel_C, bel_D = self.forward()

        # 初始化观测节点的信念(硬证据:观测值概率为1,其他为0)
        if obs_node == 'D':
            bel_obs = np.zeros(2)
            bel_obs[obs_val] = 1.0
            # 消息:D→C(简化版,添加极小值避免除零)
            denom = np.dot(msg_C_to_D, self.P_D_given_C) + 1e-8
            msg_D_to_C = bel_obs / denom
            # 修正C的信念并归一化
            bel_C_obs = bel_C * msg_D_to_C
            bel_C_obs /= np.sum(bel_C_obs)
            
            # 消息:C→B
            denom = np.dot(msg_B_to_C, self.P_C_given_B) + 1e-8
            msg_C_to_B = bel_C_obs / denom
            # 修正B的信念并归一化
            bel_B_obs = bel_B * msg_C_to_B
            bel_B_obs /= np.sum(bel_B_obs)
            
            # 消息:B→A
            denom = np.dot(msg_A_to_B, self.P_B_given_A) + 1e-8
            msg_B_to_A = bel_B_obs / denom
            # 修正A的信念并归一化
            bel_A_obs = bel_A * msg_B_to_A
            bel_A_obs /= np.sum(bel_A_obs)
            
            # 观测节点的信念固定为观测值
            bel_D_obs = bel_obs.copy()
            
            return bel_A_obs, bel_B_obs, bel_C_obs, bel_D_obs
        else:
            raise NotImplementedError("当前仅支持观测D节点的后向传播")


# 可视化信念传播前后的概率对比
def visualize_bp_results():
    bp = ChainBP()
    # 无观测时的信念(前向传播结果)
    _, _, _, bel_A, bel_B, bel_C, bel_D = bp.forward()
    # 观测D=1后的信念(后向传播修正)
    bel_A_obs, bel_B_obs, bel_C_obs, bel_D_obs = bp.backward(obs_node='D', obs_val=1)

    # 整理数据:提取各节点取1的概率
    nodes = ['A', 'B', 'C', 'D']
    bel_no_obs = [bel_A[1], bel_B[1], bel_C[1], bel_D[1]]  # 无观测时P(节点=1)
    bel_obs = [bel_A_obs[1], bel_B_obs[1], bel_C_obs[1], bel_D_obs[1]]  # 观测D=1后P(节点=1)

    # 可视化对比
    fig, ax = plt.subplots(figsize=(12, 6))
    x = np.arange(len(nodes))
    width = 0.35  # 柱状图宽度

    # 绘制无观测/有观测的对比柱状图
    ax.bar(x - width / 2, bel_no_obs, width, label='无观测(D未知)', color='lightblue')
    ax.bar(x + width / 2, bel_obs, width, label='有观测(D=1)', color='salmon')

    # 图表样式设置
    ax.set_title('链式信念传播:观测D=1前后的节点概率对比', fontsize=14, fontweight='bold')
    ax.set_xlabel('节点', fontsize=12)
    ax.set_ylabel('P(节点=1)', fontsize=12)
    ax.set_xticks(x)
    ax.set_xticklabels(nodes, fontsize=11)
    ax.legend(fontsize=11)
    ax.set_ylim(0, 1)  # 概率范围限制在0-1

    # 添加数值标签(保留2位小数)
    for i, v in enumerate(bel_no_obs):
        ax.text(i - width / 2, v + 0.02, f'{v:.2f}', ha='center', fontsize=10)
    for i, v in enumerate(bel_obs):
        ax.text(i + width / 2, v + 0.02, f'{v:.2f}', ha='center', fontsize=10)

    plt.tight_layout()
    plt.show()


# 运行主函数
if __name__ == "__main__":
    visualize_bp_results()
核心说明

1.信念传播分两步:前向(从根到叶)后向(从叶到根)

2.观测到某个节点后,会通过 "消息传递" 修正所有节点的概率 ------ 比如观测到 D=1,A、B、C 为 1 的概率都会升高。

14.6 无向图:马尔科夫随机场

马尔科夫随机场(MRF)是无向图模型,核心是 "团势能":把联合概率分解为 "团"(完全连通的子图)的势能函数乘积。

可以把 MRF 理解为:"无向图中,相邻节点互相影响,每个团的'能量'决定了联合概率的大小"

完整代码:马尔科夫随机场(图像去噪示例)
复制代码
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import gaussian_filter

# Mac字体配置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# 马尔科夫随机场图像去噪
class MRFImageDenoising:
    def __init__(self, beta=2.0, eta=1.0):
        self.beta = beta  # 相邻节点的势能系数(越大,越倾向于相邻像素相同)
        self.eta = eta    # 观测值的势能系数(越大,越倾向于接近观测值)
    
    # 势能函数
    def potential(self, x, y):
        # 一元势能(和观测值的相似度)
        unary = -self.eta * (x - y)**2
        # 二元势能(相邻节点的相似度)
        binary = -self.beta * (x - y)**2
        return unary, binary
    
    # 迭代条件模式(ICM)算法去噪
    def denoise(self, noisy_img):
        denoised_img = noisy_img.copy()
        h, w = noisy_img.shape
        
        # 迭代10次
        for _ in range(10):
            for i in range(h):
                for j in range(w):
                    # 计算当前像素取0/1的总势能
                    total_0 = 0
                    total_1 = 0
                    
                    # 一元势能(和观测值的匹配)
                    unary_0, _ = self.potential(0, noisy_img[i,j])
                    unary_1, _ = self.potential(1, noisy_img[i,j])
                    total_0 += unary_0
                    total_1 += unary_1
                    
                    # 二元势能(和相邻像素的匹配)
                    neighbors = [(i-1,j), (i+1,j), (i,j-1), (i,j+1)]
                    for ni, nj in neighbors:
                        if 0<=ni<h and 0<=nj<w:
                            val = denoised_img[ni,nj]
                            _, binary_0 = self.potential(0, val)
                            _, binary_1 = self.potential(1, val)
                            total_0 += binary_0
                            total_1 += binary_1
                    
                    # 选择势能更大的取值
                    denoised_img[i,j] = 0 if total_0 > total_1 else 1
        return denoised_img

# 生成噪声图像并去噪,可视化对比
def visualize_mrf_denoising():
    # 生成原始图像(黑白块)
    original = np.zeros((32, 32))
    original[8:24, 8:24] = 1.0
    # 添加高斯噪声
    noisy = original + np.random.normal(0, 0.5, (32, 32))
    noisy = np.clip(noisy, 0, 1)
    noisy = (noisy > 0.5).astype(float)  # 二值化
    
    # MRF去噪
    mrf = MRFImageDenoising(beta=2.0, eta=1.0)
    denoised = mrf.denoise(noisy)
    
    # 可视化对比
    fig, axes = plt.subplots(1, 3, figsize=(18, 6))
    fig.suptitle('马尔科夫随机场(MRF)图像去噪效果', fontsize=16, fontweight='bold')
    
    axes[0].imshow(original, cmap='gray')
    axes[0].set_title('原始图像')
    axes[0].axis('off')
    
    axes[1].imshow(noisy, cmap='gray')
    axes[1].set_title('噪声图像')
    axes[1].axis('off')
    
    axes[2].imshow(denoised, cmap='gray')
    axes[2].set_title('MRF去噪后')
    axes[2].axis('off')
    
    plt.tight_layout()
    plt.show()

# 运行
if __name__ == "__main__":
    visualize_mrf_denoising()
核心比喻

MRF 去噪就像 "邻居之间互相提醒":

  • 每个像素是 "居民",噪声是 "居民记错了自己的颜色";
  • 相邻居民会互相确认颜色(二元势能),同时参考自己的记忆(一元势能);
  • 最终达成一致,修正错误(去噪)。

14.7 学习图模型的结构

学习图模型的结构,就是 "从数据中自动发现变量之间的依赖关系(边)",核心方法有两类:

  1. 评分搜索法 :给不同的图结构打分(比如 BIC 评分),找最高分的结构;
  2. 约束法 :从数据中发现条件独立关系,再根据独立关系构建图。
完整代码:基于互信息的简单结构学习
复制代码
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from scipy.stats import chi2_contingency

# Mac字体配置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# 计算互信息(衡量变量之间的依赖程度)
def mutual_information(x, y):
    # 构建联合频次表
    c_xy = np.histogram2d(x, y, bins=(2,2))[0]
    # 计算互信息
    mi = 0.0
    n = np.sum(c_xy)
    for i in range(2):
        for j in range(2):
            if c_xy[i,j] > 0:
                p_xy = c_xy[i,j] / n
                p_x = np.sum(c_xy[i,:]) / n
                p_y = np.sum(c_xy[:,j]) / n
                mi += p_xy * np.log2(p_xy / (p_x * p_y))
    return mi

# 基于互信息学习图结构
def learn_graph_structure(data, threshold=0.1):
    """
    data: 二维数组,每行样本,每列变量
    threshold: 互信息阈值,大于阈值则加边
    """
    n_vars = data.shape[1]
    G = nx.DiGraph()
    G.add_nodes_from([f'X{i}' for i in range(n_vars)])
    
    # 计算所有变量对的互信息
    mi_matrix = np.zeros((n_vars, n_vars))
    for i in range(n_vars):
        for j in range(n_vars):
            if i != j:
                mi_matrix[i,j] = mutual_information(data[:,i], data[:,j])
    
    # 根据阈值加边
    for i in range(n_vars):
        for j in range(n_vars):
            if i != j and mi_matrix[i,j] > threshold:
                G.add_edge(f'X{i}', f'X{j}')
    
    return G, mi_matrix

# 生成模拟数据并学习结构
def visualize_structure_learning():
    # 生成模拟数据:X0→X1→X2,X0和X3独立
    n_samples = 1000
    X0 = np.random.choice([0,1], size=n_samples, p=[0.6,0.4])
    X1 = np.where(X0==0, np.random.choice([0,1], size=n_samples, p=[0.9,0.1]),
                  np.random.choice([0,1], size=n_samples, p=[0.2,0.8]))
    X2 = np.where(X1==0, np.random.choice([0,1], size=n_samples, p=[0.8,0.2]),
                  np.random.choice([0,1], size=n_samples, p=[0.1,0.9]))
    X3 = np.random.choice([0,1], size=n_samples, p=[0.5,0.5])
    data = np.column_stack([X0, X1, X2, X3])
    
    # 学习图结构
    G, mi_matrix = learn_graph_structure(data, threshold=0.1)
    
    # 可视化
    fig, axes = plt.subplots(1, 2, figsize=(16, 6))
    fig.suptitle('图模型结构学习(基于互信息)', fontsize=16, fontweight='bold')
    
    # 子图1:互信息矩阵热力图
    im = axes[0].imshow(mi_matrix, cmap='hot', vmin=0, vmax=np.max(mi_matrix))
    axes[0].set_title('变量对互信息矩阵')
    axes[0].set_xticks(range(4))
    axes[0].set_yticks(range(4))
    axes[0].set_xticklabels(['X0', 'X1', 'X2', 'X3'])
    axes[0].set_yticklabels(['X0', 'X1', 'X2', 'X3'])
    # 添加数值标签
    for i in range(4):
        for j in range(4):
            axes[0].text(j, i, f'{mi_matrix[i,j]:.2f}', ha='center', va='center', color='white')
    plt.colorbar(im, ax=axes[0])
    
    # 子图2:学习到的图结构
    pos = nx.spring_layout(G)
    nx.draw(G, pos, ax=axes[1], with_labels=True, node_color='lightblue', 
            node_size=2000, font_size=14, arrowsize=20)
    axes[1].set_title('学习到的图结构(互信息>0.1)')
    
    plt.tight_layout()
    plt.show()

# 运行
if __name__ == "__main__":
    visualize_structure_learning()
核心说明

1.互信息越大,变量之间的依赖关系越强;

2.代码中,X0 和 X1、X1 和 X2 的互信息大于阈值,会生成边;X0 和 X3 互信息小,无连接,符合数据生成逻辑。

14.8 影响图

影响图是 "带决策节点和效用节点的图模型",核心是 "在不确定性下做最优决策"。

比如:

  • 节点分三类:机会节点(随机变量,如天气)决策节点(可选择的行动,如是否带伞)效用节点(收益 / 损失,如是否淋雨)
  • 影响图的目标是 "选择决策节点的取值,最大化效用节点的期望"。
完整代码:影响图(带伞决策示例)
复制代码
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx

# Mac字体配置
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# 影响图决策计算
def influence_diagram_decision():
    # 定义参数
    P_rain = 0.3  # 下雨概率
    # 效用表:U(决策, 天气)
    # 决策:0=不带伞,1=带伞;天气:0=不下雨,1=下雨
    U = np.array([
        [10, 0],  # 不带伞:不下雨(10分),下雨(0分)
        [8, 9]    # 带伞:不下雨(8分),下雨(9分)
    ])
    
    # 计算期望效用
    EU_no_umbrella = P_rain * U[0,1] + (1-P_rain) * U[0,0]
    EU_umbrella = P_rain * U[1,1] + (1-P_rain) * U[1,0]
    
    # 最优决策
    optimal_decision = "带伞" if EU_umbrella > EU_no_umbrella else "不带伞"
    
    # 可视化影响图和决策结果
    fig, axes = plt.subplots(1, 2, figsize=(16, 6))
    fig.suptitle('影响图:带伞决策示例', fontsize=16, fontweight='bold')
    
    # 子图1:影响图结构
    G = nx.DiGraph()
    # 节点:天气(机会节点)、决策(决策节点)、效用(效用节点)
    G.add_nodes_from(['天气', '决策', '效用'])
    G.add_edges_from([('天气', '效用'), ('决策', '效用')])
    # 节点样式
    node_colors = {'天气': 'lightblue', '决策': 'lightgreen', '效用': 'salmon'}
    pos = {'天气': (0, 1), '决策': (2, 1), '效用': (1, 0)}
    nx.draw(G, pos, ax=axes[0], with_labels=True, node_color=[node_colors[n] for n in G.nodes()],
            node_size=3000, font_size=14, arrowsize=20)
    axes[0].set_title('影响图结构', fontsize=12)
    
    # 子图2:期望效用对比
    axes[1].bar(['不带伞', '带伞'], [EU_no_umbrella, EU_umbrella], color=['lightgray', 'orange'])
    axes[1].set_title(f'期望效用对比(最优决策:{optimal_decision})', fontsize=12)
    axes[1].set_ylabel('期望效用')
    # 添加数值标签
    axes[1].text(0, EU_no_umbrella+0.2, f'{EU_no_umbrella:.2f}', ha='center')
    axes[1].text(1, EU_umbrella+0.2, f'{EU_umbrella:.2f}', ha='center')
    
    plt.tight_layout()
    plt.show()

# 运行
if __name__ == "__main__":
    influence_diagram_decision()

14.9 注释

本文所有代码均基于 Python 3.8+,依赖库安装命令:

复制代码
pip install numpy matplotlib networkx scipy seaborn
  • networkx:图结构的构建和可视化;
  • scipy:概率计算和统计检验;
  • seaborn:辅助可视化(可选)。

14.10 习题

  1. 修改链式信念传播代码,观测 B=1,计算 A、C、D 的后验概率;
  2. 调整 MRF 去噪的 beta 参数(如 0.5、5.0),观察去噪效果的变化;
  3. 基于结构学习代码,生成包含 5 个变量的模拟数据,学习其图结构。

14.11 参考文献

  1. 《机器学习导论》(原书);
  2. 《概率图模型:原理与技术》(Koller 著);
  3. NetworkX 官方文档:https://networkx.org/
  4. Scipy 统计模块文档:https://docs.scipy.org/doc/scipy/reference/stats.html

思维导图

总结

1.图方法的核心是 "用图形表示变量间的依赖 / 独立关系",有向图(贝叶斯网络)和无向图(MRF)是两大核心类型;

2.条件独立和 d 分离是判断变量关系的基础,信念传播是图模型推理的核心算法;

3.所有代码均可直接运行(Mac 系统兼容),通过可视化对比能直观理解核心概念,建议动手修改参数(如 MRF 的 beta、BP 的观测值)加深理解。

如果有问题,欢迎在评论区交流~

相关推荐
九.九9 小时前
ops-transformer:AI 处理器上的高性能 Transformer 算子库
人工智能·深度学习·transformer
春日见9 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
恋猫de小郭9 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio9 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
deephub9 小时前
Agent Lightning:微软开源的框架无关 Agent 训练方案,LangChain/AutoGen 都能用
人工智能·microsoft·langchain·大语言模型·agent·强化学习
偷吃的耗子10 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
l1t10 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
大模型RAG和Agent技术实践10 小时前
从零构建本地AI合同审查系统:架构设计与流式交互实战(完整源代码)
人工智能·交互·智能合同审核
老邋遢10 小时前
第三章-AI知识扫盲看这一篇就够了
人工智能