4、概率分布(下):多元分布与分布变换

好,咱们接着聊概率分布。上一章我们把单变量的各种分布捋了一遍,这一章要上点难度了——多元分布。说白了,就是从处理一个随机变量,变成同时处理多个随机变量。你想想看,现实世界里哪有那么多单一变量?图像是像素矩阵,文本是词向量序列,连天气预报都得同时报温度、湿度、风速。所以多元分布才是实战中的主角。

4.1 多元正态分布:MultivariateNormalDiag

多元正态分布,我愿称之为「概率分布界的瑞士军刀」。它扩展了单变量正态分布,能描述多个变量之间的联合分布。

在 TensorFlow Probability 里,最简单的实现是 MultivariateNormalDiag。名字里的 "Diag" 指的是协方差矩阵是对角矩阵——说白了就是假设各个维度之间相互独立。虽然这个假设有点强,但胜在计算快、参数少,很多场景下够用了。

核心参数:

  • loc:均值向量,形状为 [batch_shape, event_size]
  • scale_diag:标准差向量,形状与 loc 相同
import tensorflow as tf
import tensorflow_probability as tfp

tfd = tfp.distributions

# 定义一个二维正态分布
mvn = tfd.MultivariateNormalDiag(
    loc=[1.0, 2.0],
    scale_diag=[0.5, 1.0]
)

# 采样
samples = mvn.sample(5)
print(samples.shape)  # (5, 2)

# 计算对数概率
log_prob = mvn.log_prob([[0.5, 1.5], [1.0, 2.0]])
print(log_prob)  # 两个点的对数概率

我个人习惯在模型初期先用 MultivariateNormalDiag 做快速原型。等模型跑通了,再考虑换成更复杂的协方差结构。我在项目中遇到过一个问题:用全协方差矩阵做贝叶斯线性回归,结果训练时间从几分钟飙到几小时。后来换成对角矩阵,效果只差了 2%,但速度快了 20 倍。嗯,这里要注意——不是所有场景都需要复杂的协方差结构。

4.2 狄利克雷分布:Dirichlet

狄利克雷分布,说白了就是「分布之上的分布」。它是 Beta 分布在多维空间的推广,专门用来描述概率向量的分布。什么叫概率向量?就是元素非负且和为 1 的向量,比如 [0.2, 0.5, 0.3]。

狄利克雷分布有一个很直观的参数 concentration(浓度参数)。这个参数控制着概率向量的「集中程度」:

  • 浓度参数都大于 1:概率向量倾向于均匀分布
  • 浓度参数都小于 1:概率向量倾向于集中在某一个类别上
  • 浓度参数相等:分布是对称的
# 定义一个三维狄利克雷分布
dirichlet = tfd.Dirichlet(
    concentration=[2.0, 2.0, 2.0]
)

# 采样
samples = dirichlet.sample(3)
print(samples)
# 每行元素和为 1,例如 [0.3, 0.4, 0.3]

# 浓度参数不同时
dirichlet_skewed = tfd.Dirichlet(
    concentration=[0.1, 0.1, 5.0]
)
samples_skewed = dirichlet_skewed.sample(3)
# 第三个维度会明显偏大

实战技巧:狄利克雷分布是 LDA(隐含狄利克雷分配)主题模型的核心。我曾经用它做文本聚类,浓度参数设得太小,结果每个文档的主题分布都极端集中——说白了就是每个文档只属于一个主题,失去了混合的意义。后来把浓度参数调到 0.5 左右,效果才正常。

4.3 威沙特分布:Wishart

威沙特分布,是多元正态分布协方差矩阵的共轭先验。如果你在做贝叶斯多元分析,威沙特分布几乎是绕不开的。

它的参数有两个:自由度 df 和尺度矩阵 scale。自由度必须大于等于维度数,否则矩阵不是正定的。

# 定义一个 2x2 的威沙特分布
wishart = tfd.Wishart(
    df=5,
    scale=tf.eye(2)  # 单位矩阵
)

# 采样得到协方差矩阵
samples = wishart.sample(2)
print(samples.shape)  # (2, 2, 2)
# 每个样本是一个 2x2 的正定矩阵

注意:威沙特分布对自由度很敏感。自由度太小,采样出的矩阵可能接近奇异矩阵,导致数值不稳定。我建议自由度至少设为维度数 + 2。我曾经在这个坑里摔过——自由度设成 3,维度是 3,结果模型训练到一半就 NaN 了。排查了半天才发现是协方差矩阵不可逆。

4.4 分布变换与 Bijectors 基础

好,接下来是这一章的重头戏——分布变换。你想想看,很多时候我们需要的分布不是标准形式。比如我想定义一个在 [0, 1] 区间上的正态分布,或者一个对数正态分布。这时候就需要对基础分布做变换。

TensorFlow Probability 用 Bijectors(双射器)来实现分布变换。Bijector 说白了就是一个可逆的、可微的变换函数。它能把一个简单分布(比如标准正态)映射成复杂分布。

为什么叫「双射」?因为变换必须是一一对应的,这样才能保证概率密度可以正确计算。变换后的概率密度公式是:

p_Y(y) = p_X(f^{-1}(y)) * |det(J_{f^{-1}}(y))|

其中 J 是雅可比矩阵。别被公式吓到,TFP 已经帮你算好了。

# 用 Exp bijector 把正态分布变成对数正态分布
exp_bijector = tfb.Exp()
log_normal = tfd.TransformedDistribution(
    distribution=tfd.Normal(loc=0.0, scale=1.0),
    bijector=exp_bijector
)

# 采样
samples = log_normal.sample(5)
print(samples)  # 所有值都是正数

# 计算对数概率
log_prob = log_normal.log_prob([0.5, 1.0, 2.0])
print(log_prob)

常用的 Bijectors 有:

Bijector 变换 用途
Exp y = exp(x) 将实数映射到正数
Softplus y = log(1 + exp(x)) 平滑的正数映射
Sigmoid y = 1 / (1 + exp(-x)) 将实数映射到 (0, 1)
Affine y = scale * x + shift 线性缩放和平移
SoftmaxCentered Softmax 变换 将实数向量映射到概率单纯形

核心思想:Bijectors 可以链式组合。比如你想定义一个在 [a, b] 区间上的分布,可以先从标准正态开始,经过 Sigmoid 映射到 (0, 1),再经过 Affine 缩放到 [a, b]。这种组合方式让复杂分布的构建变得极其灵活。

4.5 自定义分布

有时候内置分布不够用,就得自己动手。TFP 提供了两种方式来自定义分布:

方式一:继承 tfd.Distribution

你需要实现 _log_prob_sample_n 这两个核心方法。其他方法(如 meanvariance)是可选的,但实现了能提高效率。

class MyCustomDistribution(tfd.Distribution):
    def __init__(self, alpha, beta):
        self._alpha = alpha
        self._beta = beta
        super().__init__(
            dtype=tf.float32,
            reparameterization_type=tfd.FULLY_REPARAMETERIZED,
            validate_args=False,
            allow_nan_stats=True
        )
    
    def _log_prob(self, x):
        # 自定义对数概率密度
        return self._alpha * tf.math.log(x) + self._beta * x
    
    def _sample_n(self, n, seed=None):
        # 自定义采样方法
        # 这里用 rejection sampling 或者直接调用底层采样
        pass

方式二:用 TransformedDistribution 组合

这种方式更推荐。你不需要从头实现采样和概率计算,只需要定义变换关系。

# 定义一个在 [0, 1] 区间上的 Beta 分布变换
base_dist = tfd.Beta(
    concentration1=2.0,
    concentration0=2.0
)

# 用 Affine 变换缩放到 [0, 5]
scaled_beta = tfd.TransformedDistribution(
    distribution=base_dist,
    bijector=tfb.Affine(scale=5.0)
)

# 采样
samples = scaled_beta.sample(5)
print(samples)  # 值在 [0, 5] 之间

我的建议:能用组合就别自己写。自定义分布容易在梯度计算和采样效率上出问题。我曾经手写了一个复杂的分布,结果反向传播时梯度全是 NaN,排查了两天才发现是雅可比矩阵算错了。用 Bijectors 组合,这些细节 TFP 都帮你处理好了。

小结

这一章我们聊了多元分布的核心内容:

  • MultivariateNormalDiag:快速原型首选,对角协方差省时省力
  • Dirichlet:概率向量的分布,LDA 等主题模型的基础
  • Wishart:协方差矩阵的先验,注意自由度别设太小
  • Bijectors:分布变换的瑞士军刀,链式组合威力无穷
  • 自定义分布:能组合就别手写,省心省力

下一章我们会把这些分布用到实际的贝叶斯模型中。到时候你会发现,理解了这些分布,贝叶斯推断就成功了一半。嗯,先消化一下,咱们下章见。