☰
马氏距离:原理、几何意义与异常检测实践
2026/10/2 1:28:02 网站建设 项目流程

做多维数据分析和机器学习这几年,我越来越觉得“距离度量”这事儿被很多人低估了。模型调参调得再多,特征工程做得再花哨,底层如果用的是一种不适合场景的距离计算方式,那所有上层工作都等于在沙子上盖楼。欧氏距离大家都会用,但一旦数据各个维度之间不是独立的、方差还各不相同,欧氏距离给出的答案就可能错得离谱。这时候,马氏距离(Mahalanobis Distance)就派上用场了。

马氏距离这个名字你可能听过,但很多教程要么只丢出一个公式,要么把几何意义讲得云里雾里。这篇文章我想把马氏距离从定义到推导、再到几何意义和实际代码,完整地捋一遍。核心解决一个问题:当数据的各个维度存在相关性且尺度不一致时,如何正确地衡量一个点到一群数据之间的距离。适合正在做异常检测、分类聚类、或者对统计度量方法有需求的朋友阅读,尤其是那些已经用过欧氏距离,但总觉得哪里不对劲的读者。

1. 从欧氏距离说起:为什么需要马氏距离

1.1 欧氏距离的两个致命伤

先回忆一下欧氏距离的公式。在二维平面上,点 (A(x_1, y_1)) 和点 (B(x_2, y_2)) 之间的距离就是:

[ d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} ]

推广到 (n) 维就是各维度差值的平方和再开方。这个公式本身没有任何错误,但它隐含了两个非常强的假设,现实中经常不成立。

第一个假设是各维度之间相互独立。欧氏距离在计算时,每个维度是单独平方求和,完全忽略了维度与维度之间的关联。现实中的数据,比如身高和体重,这俩从来就不是独立的,个子高的人体重往往也更重。如果你直接拿欧氏距离去衡量两个人的“体型差异”,高瘦的人和一个矮壮的人,算出来距离可能很大,但在医学上,这两个人的体型特征可能非常接近“正常”。

第二个假设是各维度的尺度(方差)相同。还是举身高和体重的例子,一个维度单位是厘米,范围可能在150到190之间,另一个维度单位是公斤,范围可能在40到100之间。身高的数值天然就比体重大,欧氏距离计算的时候,身高那项的贡献会碾压体重那项。你相当于在做加权的时候,稀里糊涂地给方差大的维度赋予了更大的权重,这往往是反的。

1.2 一个直观的二维案例

我们用一组模拟数据说明。假设有两个特征 (x) 和 (y),它们之间有较强的正相关性,散点图呈现一个沿着45度方向拉长的椭圆分布。数据均值在 ((\mu_x, \mu_y))。

现在我来问你一个很实际的问题:均值点附近的这两类点,用欧氏距离看,哪个离中心更远?

  • 点 A 在 (x) 方向上偏离了1个标准差,在 (y) 方向上偏离了1个标准差,方向是沿着椭圆短轴。
  • 点 B 在 (x) 方向上偏离了2个标准差,在 (y) 方向上偏离了2个标准差,方向是沿着椭圆长轴。

如果把数据标准化到方差为1,点A的欧氏距离是 (\sqrt{1^2+1^2} \approx 1.41),点B的欧氏距离是 (\sqrt{2^2+2^2} \approx 2.83)。欧氏距离告诉你,点B离中心更远。但在真实的分布里,点B所在的椭圆长轴方向,数据本来就非常分散,偏离3个标准差在长轴方向上太常见了。而点A所在的短轴方向,数据极其紧凑,偏离1个标准差已经属于极端离群。因此,如果你要做异常检测,点A才是那个更该被标记的点。

这就是欧氏距离的盲区。它不关心数据的分布形状,只关心绝对坐标差。马氏距离要解决的,正是这个问题:它读懂了数据的“方差”和各维度的“相关性”,把分布的形状考虑进去之后,再来算距离。这才是它被称为“马氏距离”而不是马氏发明的欧氏距离的根本原因——它在做一件语义上完全不同的事情。

2. 马氏距离的数学定义与完整推导

2.1 定义式与符号约定

马氏距离的定义简洁得令人惊讶。对于一个均值为 (\mu)、协方差矩阵为 (\Sigma) 的多维数据集,任意一点 (x) 到该分布的马氏距离是:

[ D_M(x) = \sqrt{(x - \mu)^T \Sigma^{-1} (x - \mu)} ]

其中:

  • (x) 是一个 (n) 维列向量,是你要计算距离的样本点;
  • (\mu) 是数据集的均值向量,也是 (n) 维列向量;
  • (\Sigma) 是 (n \times n) 的协方差矩阵,刻画了各维度之间的相关性和各自的方差;
  • (\Sigma^{-1}) 是协方差矩阵的逆矩阵。

这个公式如果拿掉中间的 (\Sigma^{-1}),剩下的 ((x - \mu)^T (x - \mu)) 就是欧氏距离的平方。所以你可以把马氏距离理解为:在协方差矩阵的“逆”作为度量矩阵下的二次型距离。关键在于这个 (\Sigma^{-1}) 到底做了什么。

2.2 推导演练:从标准化到去相关

我们先来看一个特殊情况,假设数据各维度独立,且方差都为1,也就是 (\Sigma = I)(单位矩阵)。那么 (\Sigma^{-1} = I),马氏距离退化成欧氏距离。这很好理解,在标准化的独立空间中,两者没有区别。

再进一步,假设数据各维度独立,但方差不同,比如二维数据 (x) 方向的方差是4,(y) 方向的方差是1:

[ \Sigma = \begin{pmatrix} 4 & 0 \ 0 & 1 \end{pmatrix} ]

这时候 (\Sigma^{-1} = \begin{pmatrix} 1/4 & 0 \ 0 & 1 \end{pmatrix})。你看,逆矩阵的对角线元素其实就是在给每个维度的差值平方做“归一化”加权。(x) 方向的方差大,除以一个大的数,等于把该维度的贡献缩小了;(y) 方向的方差小,除以一个小的数,等于把该维度的贡献放大了。

这一步做完,相当于把原始数据在各维度上按照标准差进行标准化,转换到方差为1的“圆”形空间中。这就是马氏距离处理尺度差异的方式。但是,如果各维度之间存在相关性,协方差矩阵的非对角线元素不为0,光做标准化还不够。这时需要通过线性变换把数据去相关,这一步才是真正把马氏距离和欧氏距离拉开差距的地方。

我们做一次矩阵分解。因为马氏距离是二次型,(\Sigma^{-1}) 是实对称正定矩阵,可以进行 Cholesky 分解,或者从几何角度做特征值分解:

[ \Sigma = V \Lambda V^T ]

其中 (V) 是特征向量矩阵,(\Lambda) 是特征值对角矩阵。那么:

[ \Sigma^{-1} = V \Lambda^{-1} V^T ]

把这个代回马氏距离公式:

[ D_M(x) = \sqrt{(x - \mu)^T V \Lambda^{-1} V^T (x - \mu)} ]

令 (y = V^T (x - \mu)),也就是把 (x - \mu) 旋转到特征向量的坐标轴上。此时:

[ D_M(x) = \sqrt{y^T \Lambda^{-1} y} = \sqrt{\sum_{i=1}^{n} \frac{y_i^2}{\lambda_i}} ]

看到没有,这个式子就是若干个标准化后的分量的平方和再开方。整个过程分了两步:

  1. 旋转:用 (V^T) 把坐标轴旋转到与数据的主轴方向对齐;
  2. 缩放:除以对应主轴的 (\lambda_i),把各个方向的尺度归一化。

旋转把相关性去掉了,缩放把方差差异去掉了。做完这两步,数据在新空间里变成一团近似球形的分布,此时再用欧氏距离,就真正合理了。这就是马氏距离的全部推导核心:它等价于“白化”或“Sphere”变换之后的欧氏距离。

2.3 特殊情况:马氏距离如何退化为欧氏距离

理解退化条件,能帮你更清醒地判断该用哪个。

  • 当 (\Sigma = I) 时,协方差矩阵是单位阵,各维度独立且同方差,马氏距离 = 欧氏距离。
  • 当 (\Sigma) 是对角阵时,各维度独立但方差不同,马氏距离 =在各维度上用标准差标准化后的欧氏距离。
  • 当 (\Sigma) 是任意正定矩阵时,存在相关性,马氏距离带回相关性修正的欧氏距离。

这意味着欧氏距离其实是马氏距离在一个非常严格条件下的特例。如果你的数据维度之间强相关,直接用欧氏距离,本质上就是在用一个错误的度量矩阵 (I) 去替代真实的 (\Sigma),结果自然不准。反过来,如果你确定数据各维度独立且同方差,马氏距离没什么特别的优势,用欧氏距离反而省去了估计协方差矩阵的风险。

所以我的建议是:不确定就先算一下数据协方差矩阵,看一眼非对角元素多大,再决定距离度量的选型。这个习惯能帮你规避掉大量基础性的误差。

3. 几何意义:一句话讲透

3.1 白化变换视角

马氏距离的几何意义,用一句话概括:在原始坐标空间里,用一个与数据分布形状相匹配的椭圆(或椭球)来衡量距离,而不是用一个固定的圆。

我们回到那个椭圆分布的例子。数据分布拉成一个45度方向的椭圆,长轴方向方差大,短轴方向方差小。欧氏距离走的是圆——不管哪个方向,走相同的长度就是相同的距离,完全不理会椭圆的形状。马氏距离走的是椭圆——它定义了一个“与数据分布同形状”的椭圆,在这个椭圆内,马氏距离小于某个阈值,在椭圆外,马氏距离大于某个阈值。沿着短轴方向走一点点,就很容易跑出椭圆,因为椭圆在短轴方向很窄;沿着长轴方向走很多,可能还在椭圆内,因为椭圆在长轴方向很宽。

这个思路可以通俗地比作“鱼眼中的世界”。数据分布的大致范围是一块鱼塘,鱼塘是个狭长的湖。欧氏距离问你“从湖中心直线游到某个点有多远”,马氏距离问你“从湖中心到某个点,需要按湖的轮廓走多远”——在狭窄的方向上,稍微偏离就出界了。

3.2 特征分解视角

从线性代数的角度看,协方差矩阵的每个特征向量定义了椭球的一个主轴方向,特征值定义了该主轴方向的长度(平方)。 (\Sigma^{-1}) 的特征向量和 (\Sigma) 一致,但特征值是 (\Sigma) 特征值的倒数。

这个“取倒数”太关键了。特征值大的方向,原始数据方差大、分布散,这也是马氏距离认为“数据在这个方向上本来就会跑得远”的地方,所以对距离的惩罚反而小;特征值小的方向,数据分布紧凑,稍微偏离一点就非常反常,所以对距离的惩罚极大。于是马氏距离的等距面就是一个与数据分布同长轴、但长短轴互换过的椭圆——本质上是把原始椭圆校准成标准圆。

几何上我们经常这么说:马氏距离把原始数据空间映射到标准化的特征空间,在这个空间里,每一维的方差都是1,且维度之间正交(去相关),然后在这个规范空间里算欧氏距离。这正是白化(Whitening)变换。用任何工具做白化时,你实际上就是在算马氏距离的前半段。

3.3 椭球与协方差矩阵

等马氏距离面其实就是一个椭球。对于给定阈值 (c),满足:

[ (x - \mu)^T \Sigma^{-1} (x - \mu) = c^2 ]

的所有点构成了一个以为 (\mu) 中心的超椭球。这个超椭球在数据分布的形状(协方差矩阵)下,包含了数据点总数的某个百分比。

在多元高斯分布的假设下,这个 (c^2) 服从卡方分布(自由度 = 数据维度 (n)),所以可以通过卡方分布的分位数来确定异常阈值。比如二维数据,取 (c = \sqrt{\chi_{2, 0.95}^2} \approx \sqrt{5.991} \approx 2.448),马氏距离超过这个值的点,在95%置信度下可视为离群点。这就是马氏距离在统计学上比欧氏距离最大的优势——它不是拍脑门定的阈值,而是有严格概率分布的理论支撑。

3.4 与高斯分布的深度关联

如果 (x) 服从多元高斯分布 (N(\mu, \Sigma)),其概率密度函数的指数部分为:

[ -\frac{1}{2}(x - \mu)^T \Sigma^{-1} (x - \mu) ]

你发现了什么?括号里的部分就是马氏距离的平方。所以马氏距离的平方与高斯分布的指数项只差一个 (\frac{1}{2}) 的系数。换句话说,马氏距离越小,样本点在该分布下的概率密度就越高;马氏距离越大,密度就越低。在多个多维高斯分布做分类时(如高斯判别分析,GDA),比较“一个点到哪个类的马氏距离更小”,等于比较它在哪个高斯分布下的概率密度更大,这和贝叶斯决策规则是自洽的。

这也是为什么在马氏距离的监控中,我经常跟团队说:“马氏距离不是一个拍脑袋发明的距离公式,它是从高斯分布和统计推断中自然长出来的。” 理解了这层关系,你在做分类器、判别分析、异常检测时,才能真正把工具用对地方。

4. 实操:代码实现与应用要点

4.1 手写一个马氏距离函数

代码是最直接的检验方式。用 NumPy 十几行就能写出马氏距离的函数。第一步计算均值向量和协方差矩阵,第二步直接用 np.linalg.pinv 求伪逆(用伪逆而不是逆有讲究,见后文),第三步就是按公式批量计算。

import numpy as np from scipy.spatial.distance import mahalanobis def compute_mahalanobis_distance(X, x=None): """ 计算数据集中每个点相对于整体分布的马氏距离 X: (n_samples, n_features) 原始数据 x: 单个待测点,默认为None,即计算X中所有点的马氏距离 返回: (n_samples,) 或标量 """ mu = np.mean(X, axis=0) cov = np.cov(X, rowvar=False) cov_inv = np.linalg.pinv(cov) if x is not None: delta = x - mu return np.sqrt(delta @ cov_inv @ delta) deltas = X - mu # 批量计算 (n_samples, n_features) @ (n_features, n_features) @ (n_samples, n_features).T return np.sqrt(np.einsum('ij,jk,ik->i', deltas, cov_inv, deltas)) # 模拟数据:身高体重,存在正相关 rng = np.random.default_rng(42) height = rng.normal(170, 10, 500) weight = 0.6 * height + rng.normal(0, 8, 500) X = np.column_stack([height, weight]) # 用自己写的函数 dist_mine = compute_mahalanobis_distance(X) # 用scipy内置函数交叉验证 dist_scipy = np.array([mahalanobis(x, X.mean(axis=0), np.cov(X, rowvar=False)) for x in X]) print(np.allclose(dist_mine, dist_scipy)) # 输出 True,说明实现正确

这里有两个细节值得说说。第一,协方差矩阵计算用np.cov(X, rowvar=False),表示每一列是一个特征,千万别把rowvar参数搞反了,否则你算出来的是维度之间的协方差矩阵的转置,结果全错。第二,求逆用np.linalg.pinv(伪逆)而不是np.linalg.inv,是因为在实际数据中,特征之间若存在高度相关,协方差矩阵可能接近奇异,直接求逆会爆出无穷大的距离或者内存溢出,伪逆能对这种病态问题给出一个最小范数解,虽然不完美,但至少不会直接崩溃。

4.2 异常检测案例

马氏距离在异常检测里的用法非常成熟。我拿一个自己跑过的工业场景举例:某个设备有温度、振动幅度、转速三个传感器,每天采集24小时的均值,连续采集60天,形成一个60行3列的数据矩阵。目标:找出设备运行状态发生异常的日期。

具体操作步骤如下:

  1. 数据清洗:剔除明显超出物理量程的坏点;
  2. 计算均值向量与协方差矩阵:用全部60天的数据估算;
  3. 计算每天的马氏距离;
  4. 确定阈值:假设数据近似服从三维高斯分布,查卡方表,三维自由度下95%分位数约为 (\chi_{3, 0.95}^2 \approx 7.815),所以马氏距离阈值取 (\sqrt{7.815} \approx 2.795);
  5. 标记异常:马氏距离大于阈值的日期,标记为异常。

当我按照这个流程跑完一遍数据,发现第37天的马氏距离高达5.6,远远超过阈值。但如果用欧氏距离排序,第37天连前10都进不去。为什么?因为这设备在第37天,温度和振动都只是小幅波动,但两者原本强相关的模式被打破了。协方差矩阵捕捉到了两个特征之间的同步性,而欧氏距离完全感知不到这种“结构性异常”。这就是马氏距离在异常检测中不可替代的一个体现。

4.3 应用中的几个大坑

马氏距离虽好,远没有到无脑用的程度。实际操作中,我踩过不少坑,也见过团队里的同学踩过类似的,列出来给大家提个醒。

第一个坑是样本量不足导致协方差矩阵估计不可靠。协方差矩阵要估计的参数数量是 (n(n+1)/2) 个(对称矩阵的上三角加对角线),如果特征维数 (n) 是50,就意味着要估计1275个参数,而样本量如果只有80个,估计出来的矩阵噪声很大,算出的马氏距离不稳定。经验法则是:样本量至少要是特征维数的5到10倍,否则慎用马氏距离。

第二个坑是协方差矩阵奇异。当特征维数大于样本量(高维小样本),或者特征之间存在强共线性时,(\Sigma) 不可逆。前面提到用伪逆是一种权宜之计,但更稳的做法是先做主成分分析降维,保留主成分数不超过样本量,再用降维后的主成分算马氏距离。

第三个坑是异常值污染了协方差矩阵本身。你想用马氏距离找离群点,但如果数据本身就含有了大量离群点,它们会直接影响均值向量和协方差矩阵的估计,导致“掩盖效应”——异常值把协方差拉大,使得自己反而变成不异常。这种事情听着矛盾,实际中太常见了。解决方案是使用稳健协方差估计器,比如 FastMCD(scikit-learn 的MinCovDet类),它会用子集采样迭代拟合出不受离群点污染的协方差矩阵,再用这个稳健矩阵计算马氏距离。

第三个坑的延伸:如果你开发了一套异常检测系统,上线后需要定期用旧分布更新协方差矩阵。但设备工况缓变时,协方差矩阵跟着变,马氏距离的基准也在漂移。这时候需要引入滑动窗口或EWMA式的协方差估计,否则系统会越来越“宽容”。这个点我在生产环境里就吃过亏,一开始用全量历史估计,后来模型对轻微异常越来越不敏感,换成时间衰减加权之后才恢复正常敏感度。

5. 常见问题与排查技巧实录

5.1 问题速查表

整理一个速查表,基本覆盖了我这些年被问到的高频问题。建议收藏,真遇到问题了翻出来对照。

现象可能原因排查方向
马氏距离计算出负数或NaN协方差矩阵非正定或包含缺失值检查缺失值,检查协方差矩阵特征值是否为负
马氏距离整体偏大,几乎全部点都超阈值协方差矩阵被离群点污染,方差被高估改用稳健协方差估计,先剔除明显离群点
某些特征贡献异常大,距离主要由单个特征主导协方差矩阵对角线上该特征方差极小,逆矩阵对应元素极大检查特征是否包含非法范围的值或接近常数特征
特征维度高,协方差矩阵求逆报错样本量小于特征数或共线性严重先做PCA降维,再计算马氏距离
马氏距离和欧氏距离结果几乎一致各特征本身独立且方差相近核对协方差矩阵的非对角元素,如果近似为0,结果一致是正常的
用全部样本估计协方差后,用同一样本计算距离,发现部分点距离过大这是正常的,马氏距离近似服从卡方分布,约有一半的点在均值和分布中心之间,但也有百分之几的点超过95%分位阈值按卡方分布分位数设,也可以先排序取95%分位数
换了批次数据,阈值失效数据分布发生漂移(概念漂移)定期重新估计均值向量和协方差矩阵,或使用指数衰减更新

5.2 几次实战心得

我第一次真正用透马氏距离,是在给一批工业设备做健康度打分的时候。当时的困扰是:多个监测指标之间高度相关,直接加权求和做成“健康指数”,不同指标的权重怎么定都存在争议,业务方也不认可。后来把原始指标转为马氏距离,用马氏距离的大小作为健康度反向指标,加权那一步完全消失了,数据本身的协方差结构自动决定了一切。商务汇报时,解释路径也很简单:距离越大,说明设备状态偏离“正常群体”越远,对应健康度越低。这个方案落地后,业务方理解成本很低,因为这个分数背后有统计分布做支撑。

后来做金融反欺诈场景时,我又学到一个关键经验:马氏距离计算时到底该用整体数据的协方差矩阵,还是该用目标类别的协方差矩阵?在异常检测场景,如果目标是找“与绝大多数正常样本不同的点”,应该用全部数据(或者正常样本)的分布来算,这样离群点会被识别。但在分类场景,比如判断一个交易样本更接近“正常”还是“欺诈”,应该分别计算两类各自的马氏距离,然后比较谁更小,而不是用全局协方差。这两者的逻辑差异很细微,但在应用上是两个方向,很多新人会混淆。

另一个值得分享的心得是关于可视化。对于高维数据,马氏距离最后可以投影到一维做时序监控图,横轴是时间,纵轴是马氏距离,再加上一条阈值线。这个图是我在团队里最常用的监控看板形式,简单、直观、信息密度高。比起看一堆特征曲线自己去发现异常,看一条马氏距离曲线显然高效得多。有了这条曲线,我甚至可以做一个简单的自动化告警:只要马氏距离超过阈值,就触发工单,让业务人员去核实状态。这个流程上线后,省掉了大量人工盯盘的时间。

最后再分享一个小技巧:如果直接用np.cov得到的协方差矩阵和MinCovDet得到的稳健协方差矩阵,在马氏距离上差异很大,那么基本可以断定数据里存在离群点。你可以通过对比两个矩阵的特征值差异,反向定位是哪些样本在“拉偏”分布,这是排查脏数据的一个非常高效的手段,我经常用它来快速扫描一批没有标签的历史数据,帮我把精力集中在真正的可疑样本上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询