ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

多元正态分布实战:协方差矩阵、马氏距离与异常检测

多元正态分布实战:协方差矩阵、马氏距离与异常检测 做数据建模的人,迟早会撞上多元正态分布这座山。它不像线性回归那么直白,也不像决策树那样好向业务方解释,但只要你碰过异常检测、高斯过程、卡尔曼滤波、隐马尔可夫或者生成模型,几乎每一条路都绕不开它。我第一次真正认真啃这个东西,是在处理一批工业传感器时序数据的时候——单个维度的分布看着都挺规矩,该正态的正态,该平的平,但把几个维度拼到一起做联合分析时,总会出现一些说不通的样本点。后来才弄明白,问题根本不在单变量,而在于变量之间的联合结构,也就是协方差这个层级的信息,而多元正态分布恰好是描述这种联合结构最自然、数学性质又最完整的一把工具。这篇文章我打算按一个实际从业者的路径来讲,不堆公式吓人,也不假装它有多神秘。我会先说清楚它到底解决了什么类型的问题,再把概率密度、协方差矩阵、马氏距离这些核心部件拆开揉碎地讲一遍,然后落到实操——参数怎么估、样本怎么采、条件分布怎么推,最后把我踩过的坑和常见报错整理成一份速查表。无论你是刚学概率论的学生,还是需要在工程里落地高斯类模型的老手,都能从里面拿走可以直接抄的东西。1. 多元正态分布到底在解决哪类问题1.1 从单变量到多变量的思维跨越单变量正态分布大家都很熟,一个均值 μ 定中心,一个标准差 σ 定胖瘦,概率密度函数就那么一条钟形曲线,画在纸上闭着眼都能描出来。它的威力在于中心极限定理大量独立小扰动叠加,结果趋近正态。但现实世界里,一个样本往往不是一个数,而是一组数——一个人的身高体重、一台设备的温度振动电流、一只股票的开盘收盘成交量。这些维度不是各玩各的,它们之间有联动。最典型的坑就是分别处理每个维度然后拼起来。假设你有一批二维数据,横轴和纵轴各自都服从标准正态分布,听上去很正常对吧但如果我告诉你,这两个维度是强正相关的——横轴一大,纵轴几乎必然也大——那么像 (5, 5) 这种点,虽然两个坐标单看都不算离谱,但联合起来看,它出现的概率几乎为零,因为现实中这两维根本不会一起跑那么远。反过来,如果你只用两维各自的边缘分布去做异常检测,这类点就会被漏掉,这才是多变量分析真正要处理的东西。所以多元正态分布的第一个价值,就是把维度之间的联动这件事,从定性描述变成了可以计算的定量模型。它用一个协方差矩阵把各维度之间的相关关系全部编码进去,让你能够回答这个样本点在联合意义下到底有多罕见这种问题。这个能力,单变量正态分布给不了。1.2 协方差矩阵才是真正的主角如果把多元正态分布比作一个人,均值向量 μ 是它的位置,协方差矩阵 Σ 才是它的性格。均值决定这个分布的中心落在空间里的哪个点,而协方差矩阵决定它长什么样——是圆是扁、朝哪个方向扁、有没有倾斜。一个 k 维的协方差矩阵是个 k×k 的对称矩阵,对角线元素是每个维度自己的方差,非对角线元素是两两之间的协方差。这里有个特别容易忽略的点协方差矩阵必须是对称正定的(至少是半正定)。为什么直觉上很好理解,因为从任何一个方向去看这个分布,你看到的投影都应该是个合法的正态分布,方差必须大于零,不能出现负的宽度。数学上,这就是要求对任意非零向量 a,都有 a^T Σ a 0。这条约束在实操中经常被违反——你用样本估出来的协方差矩阵,在维度接近或超过样本数时,极容易变成奇异矩阵,后面做求逆直接爆炸。这是新手最常翻车的地方之一,后面第 4 章我会专门讲怎么救。另外要提的一点是,协方差矩阵的行列式 |Σ| 有很明确的几何意义,它正比于这个分布散布云团的体积。行列式越大,分布越散;越小,分布越集中,像一个被压紧的橄榄。这个量在概率密度函数里直接出现在归一化常数中,理解它对后面看密度公式帮助很大。1.3 几个绕不开的经典应用场景说它重要,不是空口白话,我列几个实打实会用到它的方向。第一是异常检测用正常数据拟合一个多元正态分布,新样本来了算它的马氏距离,超过某个阈值就判为异常。这个方法在工业设备监控、金融欺诈识别里用得非常广,原因就是它简单、可解释、算得快。第二是高斯判别分析(GDA)分类任务里假设每个类别内部的数据服从多元正态分布,然后用贝叶斯公式推后验,属于生成式分类的经典套路。第三是高斯过程它是一个函数空间上的分布,但任意有限个点上的取值合起来就是个多元正态分布,是贝叶斯优化、小样本回归的顶梁柱。第四是卡尔曼滤波状态估计里那一堆预测和更新公式,本质上全是多元正态分布之间的运算——两个高斯相乘还是高斯,线性变换后的高斯还是高斯。这四个方向只是入门,真正用起来你会在信号处理、计量经济、机器人定位里反复见到它的身影。理解它,等于拿到了一把能开很多门的钥匙。2. 概率密度、协方差与马氏距离的内核拆解2.1 概率密度函数逐项拆解先把公式摆出来,一个 k 维的多元正态分布记为 N(μ, Σ),它的概率密度是$$f(x) \frac{1}{(2\pi)^{k/2} |\Sigma|^{1/2}} \exp\left(-\frac{1}{2}(x-\mu)^T \Sigma^{-1} (x-\mu)\right)$$看着吓人,咱们一项一项拆。前面的 (2π)^{k/2} 是维度带来的归一化因子,维度越高这个数越大,保证整个密度在空间里积分为 1。|Σ|^{1/2} 是协方差矩阵行列式的平方根,它其实在补偿分布体积的影响——分布越散,密度峰值就越低。真正有意思的是指数部分那个二次型 (x-μ)^T Σ^{-1} (x-μ),它衡量的就是样本到中心 μ 的距离,但这个距离不是我们熟悉的欧氏距离,而是被 Σ^{-1} 加权后的距离,也就是马氏距离的平方。把这几个部件连起来看,你就明白密度函数在做什么了它在每个点上计算你离这个分布的中心有多远(按分布本身形状来量),然后把它塞进一个负指数里,越远概率越低。当 Σ 退化成单位矩阵时,这个公式就退化成标准的、各维独立的标准正态分布乘积,马氏距离也就退化成欧氏距离。这个退化视角特别有用,每当公式看着晕的时候,就把它想象成一个被拉伸旋转过的标准正态球,思路一下就通了。2.2 协方差矩阵的几何直觉怎么建立很多人卡在协方差上,是因为它抽象。我给你一个可视化的抓手对协方差矩阵做特征分解 Σ QΛQ^T,这里 Q 是正交矩阵(特征向量),Λ 是对角矩阵(特征值)。这个分解的几何含义是,多元正态分布其实就是一个标准正态球,先沿各坐标轴按特征值的平方根伸缩,再旋转到特征向量的方向上得到的。换句话说,特征向量给出了这个分布椭圆的主轴方向,特征值给出了每条轴的长度。第一特征向量是椭圆最长的那条轴,对应的方向上方差最大,也就是数据最分散的方向。特征值小的方向上,数据被压得很扁,稍有偏离就会显得很异常。这解释了为什么在异常检测里,同样的欧氏距离,沿着方差小的方向往往更可疑——因为那个方向本来就窄,你跑那么远是真的不寻常。理解了这个几何图像,后面很多操作就有依据了。比如为什么在相关系数很大的时候,不能直接对每个维度做标准化然后算欧氏距离因为标准化只处理了对角线,没动非对角线,维度之间的相关性还在,椭圆还是斜的。只有用马氏距离把整个椭圆的形状考虑进去,才能公平地衡量远近。2.3 马氏距离为什么比欧氏距离更合理马氏距离的定义很简单d(x) sqrt((x-μ)^T Σ^{-1} (x-μ))。它跟欧氏距离的本质区别,是把分布的协方差结构当成了度量尺。欧氏距离假设所有方向的重要性相同、尺度相同,这在一个各维独立同方差的世界里没问题,但现实数据里维度和维度之间量纲都不一样——温度用摄氏度、压力用帕斯卡、流量用立方米每小时,直接算欧氏距离等于让量纲大的维度主导了整个距离,结论会完全被带跑偏。马氏距离解决了三件事。第一是消除量纲,因为除以了各维的方差,相当于自动做了标准化。第二是考虑相关性,Σ^{-1} 这个逆矩阵会把相关的维度解耦,让距离计算基于真正独立的成分。第三是给出统一的异常判定尺度,因为当 x 服从 N(μ, Σ) 时,d(x)^2 恰好服从自由度为 k 的卡方分布。这意味着你可以直接查卡方分布表定阈值——比如二维数据取 95% 置信,对应的马氏距离平方阈值就是 5.99,大于这个值就落在最外层的 5% 区域里,可以判定为异常。注意很多教程教你马氏距离阈值取 3,这是想当然。阈值应该由维度 k 和置信水平决定,二维和十维的阈值差得很远,一定要查卡方分布分位数。我实测下来,在传感器异常检测里,马氏距离配合卡方阈值,召回率和误报率的平衡往往比简单设定固定欧氏距离阈值要好很多,原因就在这个自动校准的尺度上。当然它也有前提,就是正常数据要尽量接近高斯,如果分布严重偏斜或者多峰,得先做变换(比如 Box-Cox)再上马氏距离,否则阈值同样会失真。3. 从参数估计到采样与条件分布的实操3.1 参数估计:极大似然怎么做拿到一批数据,第一步永远是估参数。多元正态的极大似然估计公式干净得让人舒服$$\hat{\mu} \frac{1}{n}\sum_{i1}^{n} x_i, \qquad \hat{\Sigma} \frac{1}{n}\sum_{i1}^{n}(x_i-\hat{\mu})(x_i-\hat{\mu})^T$$均值就是样本均值,没啥好说的,跟单变量一样。协方差这块有个细节要做笔记极大似然估计用的是分母 n,但如果你想要无偏估计,分母得换成 n-1。维度一高、样本一少,这个 n 和 n-1 的差别就不是小事了,直接影响到协方差矩阵是否满秩。我在处理小样本数据时,习惯用 n-1,虽然严格说那不是 MLE,但工程上更稳。Python 里几行就能写出来,这个我基本不用现成函数,自己写一遍心里有数import numpy as np def fit_mvn(X, unbiasedTrue): # X 形状为 (n_samples, n_dims) n, k X.shape mu X.mean(axis0) Xc X - mu denom n - 1 if unbiased else n Sigma (Xc.T Xc) / denom return mu, Sigma注意Xc.T Xc这一步,它把每个样本的外积 (x_i - μ)(x_i - μ)^T 全部累加起来了,这就是协方差定义式的向量化写法,比手写双循环快几个数量级。估完之后一定要自查两件事矩阵是否对称、是否正定。因为浮点误差可能让它轻微不对称,做求逆前用(Sigma Sigma.T) / 2强制对称一下,能避开不少奇怪的数值问题。3.2 采样:Cholesky 分解为什么是最佳选择如果你想从一个指定均值协方差的多元正态里生成样本,标准套路是先生成 k 个独立标准正态数 z~N(0, I),再做变换 x μ L z,其中 L 满足 L L^T Σ。这里的 L 通常用 Cholesky 分解得到,也就是把 Σ 分解成一个下三角矩阵和它的转置的乘积。为什么偏偏选 Cholesky 而不是特征分解原因有两个。一是计算快,Cholesky 只需要大约 k³/3 次运算,而完整的特征分解要好几倍的开销。二是稳定,只要 Σ 是正定的,Cholesky 一定能跑,而且数值误差小。特征分解虽然也能用(取 L Q Λ^{1/2}),但它更贵,而且在特征值接近零时容易出负数的平方根问题。我自己写采样代码,默认就上 Cholesky,除非遇到矩阵不正定才退而求其次。import numpy as np def sample_mvn(mu, Sigma, n_samples1000, seed42): rng np.random.default_rng(seed) k len(mu) L np.linalg.cholesky(Sigma) # Sigma L L.T z rng.standard_normal((n_samples, k)) return mu z L.T # 每行一个样本跑完之后强烈建议做一次自检,把采样结果的样本协方差跟目标比一比,数值接近才说明采样正确。这个习惯帮我抓到过好几次维度或转置写错的低级 bug。另外,如果 Σ 不是严格正定(比如存在重复维度导致特征值为零),Cholesky 会直接抛错,这时候你得先做降维或者加一点对角正则,别硬算。3.3 条件分布与边缘分布:高斯家族最优雅的性质多元正态最让人喜欢的地方,就是切一刀之后还是正态。把变量分成两块 X1 和 X2,均值对应分成 μ1、μ2,协方差分块成 Σ11、Σ12、Σ21、Σ22。那么边缘分布 X1 ~ N(μ1, Σ11) 直接取分块就行,简单得像切蛋糕。条件分布稍微绕一点但同样漂亮:X1 | X2 x2 的分布是$$\mu_{1|2} \mu_1 \Sigma_{12}\Sigma_{22}^{-1}(x_2-\mu_2), \qquad \Sigma_{1|2} \Sigma_{11} - \Sigma_{12}\Sigma_{22}^{-1}\Sigma_{21}$$均值那项是看已知变量修正推测,协方差那项是看到已知变量后不确定性下降。你会发现 Σ_{1|2} 跟 x2 无关,只跟协方差分块有关,这意味着不管你观测到多少个具体值,观测之后剩下多少不确定性是固定的。这个性质在高斯过程回归里就是那个著名的后验均值与后验方差的来源。def conditional_mvn(mu, Sigma, idx1, idx2, x2): mu1, mu2 mu[idx1], mu[idx2] S11 Sigma[np.ix_(idx1, idx1)] S12 Sigma[np.ix_(idx1, idx2)] S21 Sigma[np.ix_(idx2, idx1)] S22 Sigma[np.ix_(idx2, idx2)] S22_inv np.linalg.inv(S22) mu_cond mu1 S12 S22_inv (x2 - mu2) Sigma_cond S11 - S12 S22_inv S21 return mu_cond, Sigma_cond这个函数我几乎每次做缺失值填补、传感器融合都会复用。它的价值在于,当你有一部分维度缺失时,可以用条件分布的均值去补,而且这个填补不是简单平均,是带着相关结构的最优猜测,比插值法讲究得多。实测在补传感器偶尔掉线的数据时,效果比线性插值稳。4. 工程落地中最容易翻车的典型问题4.1 协方差矩阵奇异或根本不是正定怎么办这是投产阶段最常见、最要命的问题。典型触发条件是样本数 n 小于等于维度数 k,或者某些维度之间存在精确的线性关系(比如你把总额和各项明细之和同时放进模型)。这时的协方差矩阵会变成奇异矩阵,行列式为 0,求逆直接报 LinAlgError。即便不奇异,在样本少维度高时也常常接近奇异,矩阵条件数巨大,求逆结果全是噪声,模型彻底不可用。我救这类问题有三招,按顺序上。第一招是加对角正则,也就是所谓的收缩估计,把 Σ 换成 (1-λ)Σ λ·tr(Σ)/k·I,这里 λ 是一个小的收缩系数,比如 0.01 到 0.1。这个操作把协方差矩阵往单位矩阵方向拉一点,保证正定,代价是引入一点偏差,但换来的是数值稳定,非常划算。第二招是降维,先用 PCA 把方差接近零的方向砍掉,再在低维空间里拟合,把冗余维度提前清出去。第三招是直接换成对角协方差,也就是假设各维独立,只保留方差。虽然丢掉了相关性信息,但很多时候比一个病态的全协方差模型表现更好。注意判断条件数是否过大,可以直接算np.linalg.cond(Sigma),如果结果超过 1e6 量级,基本就得考虑正则或降维了,别指望它算出来的逆是准的。4.2 高维下的数值稳定性坑维度一上去,还有几个隐蔽的坑。第一个是浮点溢出。密度公式里的指数部分如果马氏距离很大,exp 的负指数会下溢到 0,虽然理论上概率接近零,但多个样本比较时容易出现全变 0 无法排序的问题。解决办法是做对数化处理,直接算 log 密度:$$\log f(x) -\frac{k}{2}\log(2\pi) - \frac{1}{2}\log|\Sigma| - \frac{1}{2}(x-\mu)^T \Sigma^{-1}(x-\mu)$$ 这样比较就安全了。算 log|Σ| 的时候也别先算行列式再取对数,那样容易上下溢出,直接用np.linalg.slogdet,它内部靠 LU 分解稳定地给出符号和对数行列式。第二个坑是重复维度。如果你不小心把方差为零的常量列塞进数据,协方差矩阵对应那一行一列全是零,行列式为零,Cholesky 报错。上线前养成一个固定动作检查每一列的标准差,把标准差小于一个极小阈值的列干掉。第三个坑是数值求逆不划算,当维度上百时,np.linalg.inv又慢又不稳,应该改用np.linalg.solve或np.linalg.cholesky配合三角求解,尤其在做批量马氏距离时,用 Cholesky 分解一次然后解线性方程,比反复求逆快很多。4.3 常见问题速查表现象可能原因排查方法解决方案Cholesky 报错非正定样本少维度高、存在重复列算 cond(Σ),查各列方差加对角正则或降维,PCA 预筛求逆结果全是 NaNΣ 奇异、条件数过大看行列式是否为零换solve,加收缩估计密度计算全变 0指数下溢看 log 密度全程对数化异常检测误报一堆阈值拍脑袋定的检查阈值来源用卡方分位数查表定阈采样结果偏斜变换矩阵写错比对样本协方差确认z L.T而非L z条件分布方差为负Σ 不正定导致检查 Σ_{12} 对角这张表基本覆盖了我这两年在项目里遇到的大部分报错。有了这张表,大多数问题你对着现象就能定位,不用每次从头 Debug。5. 三个我能直接复用的落地方案5.1 基于马氏距离的多维异常检测这套方案的思路是先用健康的正常数据训一个多元正态分布,估出 μ 和 Σ,给 Σ 加一层收缩正则防病态;来了新样本,算它的马氏距离平方,跟卡方阈值比,超了就报警。阈值我用scipy.stats.chi2.ppf(0.997, dfk)取,大概对应 3σ 的水平。import numpy as np from scipy.stats import chi2 class MVNAnomaly: def __init__(self, shrink0.05): self.shrink shrink def fit(self, X): self.mu X.mean(axis0) Xc X - self.mu n, k X.shape Sigma (Xc.T Xc) / (n - 1) target np.trace(Sigma) / k * np.eye(k) self.Sigma (1 - self.shrink) * Sigma self.shrink * target self.threshold chi2.ppf(0.997, dfk) def score(self, X): Xc X - self.mu # 用 Cholesky 解线性方程,避免求逆 L np.linalg.cholesky(self.Sigma) y np.linalg.solve(L, Xc.T) return (y ** 2).sum(axis0) def predict(self, X): return (self.score(X) self.threshold).astype(int)这套东西我丢到线上跑,单机每秒处理几万个样本毫无压力。要注意的是,正常数据的分布形态最好先扫一眼,确认单峰且大致对称,如果是双峰的,得分段建多个模型,硬套一个高斯会漏报中间的异常。5.2 高斯判别分析做小样本分类分类场景里,如果每个类别样本都不多,判别式模型容易过拟合,这时候生成式的 GDA 就很香。它假设每个类别内部服从多元正态,共享或各自独立的协方差,然后比较各类别的对数后验,谁大判谁。关键是它在小样本下很稳,而且给出的后验概率有实际含义,不像某些模型输出一堆没校准的分数。def gda_predict(X_test, classes_data, priors): # classes_data: {label: (mu, Sigma)} scores {} for label, (mu, Sigma) in classes_data.items(): L np.linalg.cholesky(Sigma) Xc X_test - mu y np.linalg.solve(L, Xc.T) maha (y ** 2).sum(axis0) log_det 2 * np.log(np.diag(L)).sum() log_lik -0.5 * (maha log_det len(mu) * np.log(2 * np.pi)) scores[label] log_lik np.log(priors[label]) return max(scores, keylambda k: scores[k][0])实践中我一般会先试共享协方差(线性判别分析),只有在各类别散布形态差异明显时才放开成各自的协方差。共享协方差参数少,小样本下更不容易崩,很多时候差别不大但稳定太多。5.3 用条件分布做缺失值填补传感器掉线、用户没填字段这类问题,用条件分布补是最优解之一。做法是把完整数据估出 μ 和 Σ,面对一个有缺失的样本,把已知维度当条件,缺失维度当待推,用前面 3.3 节的公式算出条件均值作为填补值。这个填补不是简单均值,而是根据已知的强相关维度做的最合理猜测,比如你知道某设备的振动高了,即使温度读数缺失,也能推一个偏高的温度出来。def impute_missing(x_partial, mu, Sigma, observed_idx, missing_idx): x2 x_partial[observed_idx] mu_cond, _ conditional_mvn(mu, Sigma, missing_idx, observed_idx, x2) filled x_partial.copy() filled[missing_idx] mu_cond return filled一个经验之谈如果缺失比例超过三成,条件填补的优势就明显了,但如果缺失的是方差极小的维度,推出来的值几乎就是均值,这时候就该接受它信息量低的事实,别过度解读填补结果。6. 我在实际使用中总结的几点体会前面讲了一整套从理论到落地的路径,最后再补几个我反复验证过、但很少在教科书里看到的心得。第一个是关于假设检验。多元正态分布是个很强的假设,数据不是高斯的时候硬套,后果比想象中严重。养成一个习惯建模前先做正态性检验,单变量用偏度和峰度粗看,多变量用马氏距离平方是否近似卡方分布来判断。如果 QQ 图明显偏离,先考虑换模型或做变换,别硬上。第二个是关于维度诅咒。协方差矩阵的参数个数是 k(k1)/2,维度从 10 涨到 100,参数从 55 涨到 5050,涨了近两个数量级,而样本量往往跟不上。所以维度一高,收缩、降维、因子模型这些正则手段基本是必需品而不是可选项。我见过太多人直接把 200 维数据往多元正态里塞,结果模型完全学不到东西,还以为是高斯不行,其实是样本不够撑起那个协方差矩阵。第三个是关于为什么用它。我越来越觉得,多元正态分布的价值不只在它本身,更在于它是一个很好的基线——结构简单、可解释、有闭式解、计算高效。当你要上更复杂的模型时,先拿它跑一遍,得到一个 baseline,再对比复杂模型有没真正带来提升。很多号称高级的方法,在数据本身接近高斯时,并不比一个干净的多元正态模型表现好,反而贵得多、难解释得多。先跑基线,再谈复杂,这个习惯帮我省下过不少算力和时间。最后提一句,真正把多元正态用熟的人,不会把它当成一个孤立公式,而是会把它和协方差、马氏距离、条件分布、卡方分布这几块联系在一起思考。当你看到一个多维样本异常判定或者不确定性量化的需求时,脑子里能立刻调出这套组合拳,那基本就算过关了。剩下的就是在具体场景里调参数、做检验、堆经验,这些没有捷径,只能靠一次次实操累积。
返回列表