混合高斯假设数据是几个高斯分布混出来的:每个成分有自己的中心、自己的形状、自己的份量。 它用 EM 算法交替算「每个点属于各成分的概率」和「按概率重算参数」。 Demo 里一团竖长条加一个小圆球: k-means 纯度只有 0.6925,混合高斯 0.9975——因为它认出了「一个细长、一个圆胖」, y 方向标准差差了 5.2 倍。
上一节末尾留了三个坑:k-means 只会直线切、假设簇大小相近、给硬编号。 这一节用一套概率语言把三个坑一起填了。 它和 k-means 的关系很紧: k-means 其实是混合高斯的一个极端特例——所有成分的协方差都相同且趋近于 0 时, EM 就退化成 k-means 的两步循环。 理解了这一节,等于同时理解了「软聚类」「EM 算法」「生成模型」三件事。
一、从「一个高斯」到「几个高斯」
单个高斯分布很好理解:一个中心 μ、一个协方差 Σ,数据以 μ 为中心、按 Σ 的形状散开。 它的等高线是椭圆。 混合高斯就是把几个这样的椭圆按不同份量叠在一起。 每个成分有三个参数: 权重 π(这个成分占多大比例)、中心 μ、协方差 Σ。所有成分的权重加起来等于 1。 它的表达能力比单个高斯强得多: 两个扁椭圆斜着叠,能拼出弯月形;三个椭圆能拼出一个圈。 而 k-means 只能拼出圆—— 这就是它在长条数据上失败的原因。 这里要说清一件事:混合高斯是「生成模型」。 它不只描述数据长什么样,还能「造」数据: 先从权重里抽一个成分,再从那个成分的高斯里抽一个点。 这个视角很重要,因为训练它的方法(最大似然)就是从这个假设推出来的。
二、EM 算法:先猜归属,再改参数
要是知道每个点属于哪个成分,问题就简单了——直接按成分分组、算均值和协方差。 要是知道参数,问题也简单——算每个点属于各成分的概率就行了。难的是两样都不知道。 EM 算法的办法是先随便猜一样,再交替。 E 步(Expectation):固定参数,算每个点属于每个成分的概率,这个概率叫责任度。 M 步(Maximization):固定责任度,按责任度加权重算每个成分的中心、协方差、权重。 关键在「加权」。 k-means 里一个点要么属于 A 要么属于 B,非黑即白; 混合高斯里一个点可能「七成属于 A、三成属于 B」,于是它在更新参数时, 同时以 0.7 的份量影响 A、以 0.3 的份量影响 B。 这就是「软分配」的实际含义。 EM 有个漂亮的性质:每一轮迭代,对数似然只增不减。 因为 E 步和 M 步各自都在最大化同一个下界, 交替进行下界只会往上走。Demo 里对数似然从 −1262.40 一路爬到 −1030.35,然后就平了。 和 k-means 一样, EM 也只保证局部最优。 而且它比 k-means 更挑初值——如果一开始就有个成分被塞在没人的角落, 它可能永远分不到点(权重趋近 0)。所以代码里拿 k-means 的结果打底,这是常见做法。
三、责任度:它到底在算什么
责任度的正式名字是「后验概率」: 在看到这个点之后,它属于成分 c 的概率。 用贝叶斯公式算——先验是权重 π, 似然是这个点在高斯 c 下的密度,两者相乘再归一化。 Demo 里挑了六个「最说不清」的点,责任度从 0.6962 / 0.3038 到 0.8834 / 0.1166。 注意最模糊的也到了 0.70 对 0.30,说明这份数据的两团其实分得很干脆。 换成边界犬牙交错的数据,你会看到成片的 0.5 对 0.5。 那时软分配的价值才真正显出来: k-means 会把这类点硬塞进某一类,一个点的归属就能让边界大幅摆动; 混合高斯则让它们「两边都算一点」,边界稳定得多。 责任度还有个实际用途: 它就是异常检测的分数。 一个点如果对每个成分的责任度都很低(也就是在所有高斯下密度都极小), 那它就是个离群点。这比「到中心的距离」稳健得多。
四、协方差矩阵:每个簇有自己的形状
这是混合高斯比 k-means 强的根本原因。
每个成分有自己的 Σ,而 Σ 是一个完整的协方差矩阵——它同时描述了「多散」和「朝哪个方向散」。
Demo 里两个成分的 y 方向标准差是 2.531 和 0.489,
差了 5.2 倍。 算法完全靠自己发现了「一个细长、一个圆胖」。
k-means 里没有这个概念,它只能假设所有簇的散度相同。
协方差的自由度是 d(d+1)/2,d 是维度。二维是 3 个参数,十维就是 55 个。
维度一高,参数就爆炸,而且很容易过拟合(某些成分的 Σ 会退化成一条线)。
所以工程上有几种简化,值得知道:球形(Σ = σ²I,等于 k-means 的软版本)、对角(各维独立)、
全协方差(最灵活也最贵)。
scikit-learn 里这个参数叫 covariance_type,数据量大、维度高时用对角更稳。
五、它和 k-means 的关系
把混合高斯的每个 Σ 都固定成同一个 σ²I,然后让 σ 趋近 0。 这时每个点的责任度会变成「一个接近 1、 其余接近 0」——软分配退化成硬分配。 E 步变成「找最近的中心」,M 步变成「算均值」。 这就是 k-means。 所以两者的关系是: k-means 是混合高斯的一个极端特例。 这解释了为什么 k-means 那么快——它少估了一大堆参数。 也解释了为什么混合高斯更慢但更准——它多花的钱买的是「形状」。 实践中的选择很直接:簇明显是圆的、数据量大、要快要稳,用 k-means; 簇形状各不相同、簇的大小差别大、需要「概率」这个输出,用混合高斯。 还有一个判据: 混合高斯能算「这个点有多异常」,k-means 只能算「离中心多远」。
六、动手 Demo:一团长条加一个圆球,k-means 切不动
两团点各 200 个:一团是竖着的长条(x 抖 0.25、y 抖 2.6), 另一团是小圆球(中心在 (1.7, 0)、两个方向都抖 0.5)。 人眼一看就分得开,但 k-means 分不开——它只会用直线切,而长条的重心在中段, 一刀下去必然把它劈成两半。 代码是一套完整的 EM:E 步算责任度,M 步按责任度加权重算中心、协方差、权重。 初值用 k-means 的结果打底,避免 EM 卡在奇怪的地方。
import numpy as np
for it in range(iters):
# E 步:每个点属于各成分的「责任度」(后验概率)
for c in range(k):
diff = X - mu[c]
R[:, c] = w[c] * np.exp(-0.5 * ((diff @ inv[c]) * diff).sum(axis=1)) / np.sqrt(det[c])
ll = np.log(R.sum(axis=1)).sum() # 对数似然,只增不减
R = R / R.sum(axis=1, keepdims=True)
# M 步:按责任度加权重算 中心 / 协方差 / 权重
nk = R.sum(axis=0)
mu = (R.T @ X) / nk[:, None]
for c in range(k):
diff = X - mu[c]
cov[c] = ((R[:, c][:, None] * diff).T @ diff) / nk[c]
w = nk / len(X)
实跑输出:
--------------------------------------------------------------------
混合高斯分布:用「软分配」补上 k-means 的短板
--------------------------------------------------------------------
造两团点,各 200 个。**一团是竖着的长条(x 只抖 0.25,y 抖 2.6),另一团是个小圆球。**
这两团人眼一看就分得开,但 k-means 分不开——因为它只会「用直线切」,而长条的重心在中段,
切一刀必然把它劈成两半。
【一】先看 k-means 栽在哪
方法 两个中心 纯度
--------------------------------------------------------
k-means (-0.04, -2.56) 与 (+1.04, +0.50) 0.6925
一个中心落在长条的下半截,另一个挤在长条中段和小球之间——
**小球被并进了长条,长条被拦腰劈成两段**。
纯度只有 0.69,比瞎猜(0.5)好一点,但两团根本没分开。
【二】混合高斯怎么想这件事
它假设数据是**几个高斯分布混在一起生成的**:
每个成分有自己的中心、自己的形状(协方差矩阵)、自己的份量(权重)。
长条的协方差在 y 方向大、x 方向小,**一个「竖着的高斯」就能罩住它,不必切成两半。**
成分 权重 中心 x 方向标准差 y 方向标准差
--------------------------------------------------------------------
1 0.499 (-0.01, -0.20) 0.265 2.531
2 0.501 (+1.68, +0.02) 0.472 0.489
两个成分的 y 方向标准差差了 5.2 倍——**算法自己认出了「一个细长、一个圆胖」。**
纯度 0.9975。这就是「让每个簇有自己的形状」带来的差别。
【三】软分配:每个点拿到的是「概率」,不是「编号」
点 属于成分 1 属于成分 2
--------------------------------------
第 33 个 0.6962 0.3038
第 220 个 0.7270 0.2730
第 178 个 0.8042 0.1958
第 137 个 0.8211 0.1789
第 28 个 0.8559 0.1441
第 74 个 0.8834 0.1166
这几个点是「最说不清」的:责任度最接近 0.5。
**k-means 会硬给它一个编号,混合高斯会告诉你「七成是这堆、三成是那堆」。**
这个责任度就是 E 步算出来的东西,它是下一轮更新参数的权重——
**边界上的点同时影响两个成分,这才叫「软」。**
顺带说一句:这次最模糊的点也到了 0.70 对 0.30,说明这份数据的两团确实分得干脆。
换成边界犬牙交错的数据,你会看到成片的 0.5 对 0.5——**那时「软分配」的价值才真正显出来。**
【四】EM 在最大化什么:对数似然一路往上
迭代 对数似然
----------------------
1 -1262.40
2 -1238.93
3 -1208.79
5 -1097.59
10 -1030.38
20 -1030.35
40 -1030.35
60 -1030.35
每一轮 E 步算责任度、M 步更新参数,对数似然只增不减——
**和 k-means 的惯性单调下降是同一件事的两面**:
都在保证「这一轮不会比上一轮差」。代价也一样:**只保证爬到某个局部最优,不保证是全局最优**
,所以初值仍然重要,代码里才拿 k-means 的结果打底。
先看 k-means 的两个中心:(−0.04, −2.56) 和 (+1.04, +0.50)。 一个在长条下半截, 另一个挤在长条中段和小球之间。结果是小球被并进长条、长条被拦腰劈开,纯度 0.6925。 混合高斯的参数表才叫「认出了形状」。成分 1 的中心 (−0.01, −0.20)、x 标准差 0.265、 y 标准差 2.531——这就是那条竖长条。成分 2 的中心 (1.68, 0.02)、 两个标准差 0.472 和 0.489——这就是那个小球。纯度 0.9975。 责任度表里最模糊的点是 0.6962 对 0.3038。换句话说,连最说不清的点都有七成把握。 这份数据太干净了,软分配的优势没充分体现,但机制是一样的。 对数似然那张表:−1262.40 → −1238.93 → −1208.79 → … → −1030.35。第 10 轮就到 −1030.38, 后面 50 轮只动了 0.03。EM 和梯度下降一样:前几步跑得快,越靠近谷底越磨蹭。
30 秒实验
- 把小球挪到 (1.2, 0)(更靠近长条),两团开始重叠,纯度会掉到多少?
- 把成分数改成 3(数据里只有两团),多出来的那个成分会分到什么?
- 把初值从「k-means 打底」改成纯随机,连跑五次,对数似然的终点一样吗?
常见误区
- 把混合高斯当成「更高级的 k-means」直接替换。它参数多得多,维度高时容易过拟合,而且更慢。 先确认簇真不是圆的,再换。
- 用全协方差处理高维数据。自由度是 d(d+1)/2,十维就 55 个参数, 数据不够时协方差会退化成奇异矩阵。高维改用对角协方差。
- 以为 EM 一定收敛到全局最优。它只保证对数似然单调不降,停在哪取决于初值。 多起几个初值取最好的,或者先用 k-means 打底。
自测三问(点开对答案)
1. E 步和 M 步分别在算什么? —— E 步固定参数,算每个点属于各成分的后验概率(责任度); M 步固定责任度,按它加权重算每个成分的权重、中心、协方差。 2. 为什么混合高斯能处理长条形的簇,k-means 不能? —— 因为每个成分有自己的完整协方差矩阵,能描述「朝哪个方向散」; k-means 隐含假设所有簇散度相同且各向同性,只能切出接近圆的区域。 3. 混合高斯和 k-means 是什么关系? —— k-means 是混合高斯在「所有协方差相同且趋于 0」时的极限特例,此时软分配退化成硬分配。 所以 k-means 更快,但表达能力更弱。到 3.6 节为止,所有算法都假设数据躺在「平面」上(哪怕是斜的平面)。 3.7 节要处理一张卷起来的曲面。
- ☐ (本节完,下一节 3.7 算法 16:LLE)