两团高斯数据为何产生直线边界?从共享协方差到 LDA
从朴素贝叶斯忽略特征相关性出发,推导类高斯分布、马氏距离与共享协方差消去二次项,并实现可诊断的线性判别分析。
上一篇的多项式朴素贝叶斯用条件独立假设,把高维词频联合分布拆成许多一维计数。这让模型在小数据、稀疏文本中极其高效,却会把“优惠”和“领取”这类相关特征重复当成独立证据。
在体检或传感器数据中,相关性更难忽略:身高与体重通常一起变化,温度与压力可能沿某个斜方向波动。若模型只看每一维自己的均值和方差,就会把数据云的方向丢掉。
线性判别分析(Linear Discriminant Analysis,LDA)保留生成式分类框架,用多元高斯分布显式描述特征共同变化。本文只回答三个紧密问题:每一类的高斯数据云怎样写成概率、共享协方差为何让二次项消失,以及马氏距离怎样变成可计算的线性分类分数。
01 独立方差为什么看不见“斜着的数据云”?#
设两个类别的二维测量数据如下:
x₂
▲ 类别 1
│ ● ●
│ ● ●
│ ●
│ ○ ○ 两类数据都沿右上方向延展
│ ○ ○
│ ○ 类别 0
└──────────────────────────────────► x₁text每一维的方差只能回答沿坐标轴有多分散;协方差(Covariance)还回答两个特征是否一起增减。二维协方差矩阵为:
对 维输入,。对角线保存各特征方差,非对角线保存成对协方差。高斯朴素贝叶斯相当于只保留对角线;LDA 则保留完整矩阵,但要求各类别共享它。
02 LDA 对数据分布作了什么假设?#
训练集为:
LDA 假设类别 的特征服从多元高斯分布(Multivariate Gaussian Distribution):
密度为:
- :类别 的均值向量;
- :所有类别共享的类内协方差矩阵;
- :协方差行列式,反映数据云总体体积;
- :精度矩阵(Precision Matrix);
- :类别先验,全部先验组成 。
模型训练后保存的主要状态是:
class means mu [K,D]
shared covariance Sigma [D,D]
class priors pi [K]text与朴素贝叶斯一样,LDA 先建模 与 ,再用贝叶斯定理计算 ;它不是把标签作为连续值做最小二乘回归。
03 马氏距离为什么比欧氏距离多看了一层?#
高斯指数中的二次型:
叫作平方马氏距离(Squared Mahalanobis Distance)。欧氏距离把每个方向视为同等尺度;马氏距离先按协方差“白化”数据:高方差方向上的同样位移惩罚较小,低方差方向上的位移惩罚较大,并考虑特征相关导致的椭圆方向。
原空间:共享斜椭圆 白化空间:协方差变成单位阵
╱╱ ● 类1 ○ ● 类1
○ ╱╱ 类0
类0 距离可直接用圆形欧氏距离
按 Sigma^{-1} 拉伸、旋转 ───────────────────────────────►text所以 LDA 可以直观理解为:在共享协方差定义的几何中,选择马氏距离更近的类中心,同时用类别先验修正答案。
04 从高斯后验推导线性分数#
贝叶斯分类选择最大后验类别:
取对数并去掉对所有类别相同的常数:
展开二次项:
关键在第一项 :因为所有类别共享同一个 ,它对每个 完全相同,比较类别时消去。剩下:
定义:
便得到熟悉的线性分数:
任意两个类别 的边界满足 ,也就是:
这是一条超平面。LDA 的“线性”不是先拍脑袋规定边界必须直,而是共享协方差使所有类别共同的输入二次项在后验比较中抵消。
05 用两个二维高斯手算边界#
设两个类别参数已经由训练数据估计为:
因为 :
令两分数相等,公共先验抵消,决策边界为:
对查询点 ,两类分数差为:
因此预测类别 1。二分类时,将两个联合分数归一化等价于对差值做 Sigmoid:
交互手算:类别 1 先验降为 0.2 会怎样?
先验差变为 。查询点原来的分数差 0.5 变为 ,预测翻转为类别 0。先验改变只平移边界,不改变法向量 。
06 均值、先验和共享协方差怎样从数据得到?#
类别 有 个样本,样本集合记为 。均值与先验的自然估计是:
每类先计算围绕本类均值的残差,再把全部类内散布汇总为共享协方差:
这里写的是有偏最大似然形式,分母为 ;不同统计语境可能使用自由度修正。scikit-learn 的 covariance_ 是按类别先验加权的类内协方差,并使用有偏估计。不要把它与围绕全局均值计算的总协方差混淆,否则类间均值差会被错误计入类内噪声。
训练与推理的数据流是:
训练 X [N,D] + y [N]
│ 按类别分组
├──► means [K,D]
├──► priors [K]
└──► 类内残差 ─► shared covariance [D,D]
│ 线性求解 Sigma w_k = mu_k
▼
weights [K,D] + bias [K]
推理 Xq [Q,D]
│ Xq @ weights.T + bias
▼
scores [Q,K] ─► softmax ─► probability [Q,K]
│ argmax
▼
prediction [Q]text07 不显式求逆:用 NumPy 写出 LDA 核心#
公式里出现 ,实现时不应先计算完整逆矩阵。求解线性方程 通常更稳定:
import numpy as np
X = np.array([
[0.0, 0.0],
[0.2, -0.1],
[-0.2, 0.1],
[2.0, 1.0],
[2.2, 0.9],
[1.8, 1.1],
], dtype=np.float64) # [N=6, D=2]
y = np.array([0, 0, 0, 1, 1, 1]) # [N]
classes = np.unique(y) # [K=2]
means = np.stack([X[y == c].mean(axis=0) for c in classes]) # [K,D]
priors = np.array([(y == c).mean() for c in classes]) # [K]
residual_blocks = [X[y == c] - means[i] for i, c in enumerate(classes)]
residuals = np.concatenate(residual_blocks, axis=0) # [N,D]
covariance = residuals.T @ residuals / X.shape[0] # [D,D]
# 极小样本使协方差接近奇异;仅为示例加入很小的岭项
ridge = 1e-6
regularized_covariance = covariance + ridge * np.eye(X.shape[1])
weights = np.linalg.solve(regularized_covariance, means.T).T # [K,D]
bias = -0.5 * np.sum(means * weights, axis=1) + np.log(priors) # [K]
X_query = np.array([[1.0, 1.0]]) # [Q=1,D=2]
scores = X_query @ weights.T + bias # [Q,K]
prediction = classes[np.argmax(scores, axis=1)]
shifted = scores - scores.max(axis=1, keepdims=True)
probability = np.exp(shifted)
probability /= probability.sum(axis=1, keepdims=True) # [Q,K]
assert means.shape == weights.shape == (2, 2)
assert covariance.shape == (2, 2)
assert np.allclose(covariance, covariance.T)
assert np.all(np.linalg.eigvalsh(regularized_covariance) > 0)
assert np.allclose(probability.sum(axis=1), 1.0)python这段代码的 ridge 只用于展示数值保护,不是完整的 shrinkage 选择策略。生产中应在训练数据内部用交叉验证评估正则化,而不是遇到报错就不断增大常数。
08 用 scikit-learn 1.9 正确选择求解器#
当前 LinearDiscriminantAnalysis 提供 svd、lsqr 和 eigen 三种求解器。先用默认 svd 建立基线:
import numpy as np
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
pipeline = make_pipeline(
StandardScaler(),
LinearDiscriminantAnalysis(
solver='svd',
store_covariance=True,
tol=1e-4,
),
)
pipeline.fit(X_train, y_train)
scores = pipeline.decision_function(X_val) # 二分类 [Q],多分类 [Q,K]
probability = pipeline.predict_proba(X_val) # [Q,K]
prediction = pipeline.predict(X_val) # [Q]
model = pipeline.named_steps['lineardiscriminantanalysis']
print('classes:', model.classes_)
print('means:', model.means_.shape) # [K,D]
print('priors:', model.priors_.shape) # [K]
print('covariance:', model.covariance_.shape) # [D,D]
print('coef:', model.coef_.shape)
positive_column = np.flatnonzero(model.classes_ == 1).item()
positive_probability = probability[:, positive_column]python三种求解器不是单纯的速度开关:
| 求解器 | 是否显式依赖协方差 | shrinkage | transform | 适用提醒 |
|---|---|---|---|---|
svd | 默认不显式计算 | 不支持 | 支持 | 默认选择;高维时避免显式协方差 |
lsqr | 计算并求解线性系统 | 支持 | 不支持 | 只做分类,可配 shrinkage='auto' |
eigen | 显式计算 | 支持 | 支持 | 可分类和投影,但高维协方差成本高 |
当特征数接近或超过样本数,经验协方差会不稳定。可比较:
regularized_pipeline = make_pipeline(
StandardScaler(),
LinearDiscriminantAnalysis(
solver='lsqr',
shrinkage='auto',
),
)pythonshrinkage 只支持 lsqr 与 eigen;若传 covariance_estimator,必须让 shrinkage=None,两者不能同时控制协方差。n_components 只影响 transform 的监督降维输出,不会改变 fit 和 predict 的分类结果。
09 怎样诊断协方差而不是只看准确率?#
先用分层交叉验证评价模型,再检查训练状态:
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
result = cross_validate(
regularized_pipeline,
X,
y,
cv=cv,
scoring=['balanced_accuracy', 'neg_log_loss'],
return_train_score=True,
)python遇到不稳定或概率极端时,按下面顺序排查:
- 检查每类样本数。 某类样本少于特征维数时,均值与协方差方向都很不可靠;
- 检查秩与条件数。 重复列、线性组合列和常数列会使协方差奇异或病态;
- 画每类均值与协方差椭圆。 若各类椭圆方向、长短明显不同,共享协方差假设可能不成立;
- 比较训练与验证概率。 极端分数但验证对数损失很差,常提示协方差估计或分布假设有问题;
- 核对
classes_。predict_proba列顺序按类别排序,二分类decision_function是classes_[1]对classes_[0]的对数后验差。
最小数值检查:
assert np.isfinite(model.means_).all()
assert np.isfinite(model.priors_).all()
assert np.isclose(model.priors_.sum(), 1.0)
assert np.allclose(model.covariance_, model.covariance_.T)
print('covariance eigenvalues:', np.linalg.eigvalsh(model.covariance_))python10 常见错误与失败场景#
- 把总协方差当成类内协方差。 必须先减各自类均值,不能只减全局均值;
- 显式调用
np.linalg.inv。 直接解线性方程或使用稳定求解器,避免放大数值误差; - 在全数据上标准化后交叉验证。 预处理必须置于 Pipeline,防止验证折泄漏;
- 高维小样本仍使用未经收缩的经验协方差。 比较
svd与带 shrinkage 的lsqr/eigen; - 盲信高斯后验概率。 异常值、重尾、类别混合和分布漂移都会让概率失真;
- 把 LDA 投影当成无监督降维。 它使用标签寻找判别方向,不能在划分前对全数据拟合;
- 把系数解释为因果作用。 同时受相关特征与量纲影响,只表达模型中的分类关联。
共享协方差假设尤其容易在以下情况失败:不同类别数据云形状明显不同;每类本身由多个簇组成;边界是同心圆或 XOR;存在强异常值;部署数据的均值、协方差或类别先验发生变化。
11 LDA、QDA、高斯 NB 和逻辑回归的边界#
| 方法 | 类条件分布 | 协方差假设 | 边界 | 参数与数据需求 |
|---|---|---|---|---|
| 高斯 NB | 每类高斯 | 每类对角协方差 | 通常二次、轴对齐 | 参数少,忽略相关性 |
| LDA | 每类高斯 | 各类共享完整协方差 | 线性 | 共享信息,较省样本 |
| QDA | 每类高斯 | 每类独立完整协方差 | 二次 | 更灵活,也更需样本 |
| 逻辑回归 | 不建模 | 无高斯要求 | 线性 | 直接优化条件似然 |
LDA 与逻辑回归都能产生线性边界,但来源不同。LDA 假设每类特征高斯且共享协方差,在假设接近真实且样本不多时可高效利用结构;逻辑回归只规定后验对数几率线性,分布假设更弱,数据足够时通常更稳健。
LDA 还有监督降维用途:transform 最多输出 个方向,因为 个类中心在中心化后最多张成 维。PCA 则完全不看标签,只保留总体方差最大的方向;高方差方向未必最能分开类别。本文的核心仍是分类器,不要把两种用途混成同一件事。
12 今天真正需要记住什么?#
- LDA 为每个类别拟合一个均值,并让所有类别共享完整类内协方差;
- 马氏距离用协方差修正尺度和相关性,相当于在白化空间比较类中心;
- 共享协方差让后验中的 对所有类别相同并消去,因此边界是线性的;
- 实现时求解 ,不要先显式求逆;高维小样本应评估稳定求解与 shrinkage;
n_components只控制监督投影,不改变分类拟合与预测,验证时仍要防止标签泄漏。
13 思考题与小练习#
练习 1:写出不等先验下的新边界
在手算例中令 。边界满足 ,即 。稀有的类别 1 需要更强特征证据才会被预测。
练习 2:共享协方差为什么消二次项?
分别展开 与 ,相减后标出 。它在两式中完全相同;若改为 与 ,两项不能抵消,边界便保留二次项。
练习 3:制造病态协方差
在代码中新增第三列 x3=x1+x2,计算协方差特征值与条件数。比较未经正则化的手写求解、solver='svd' 和 solver='lsqr', shrinkage='auto',解释完全线性相关为何导致零特征值。
相关工作#
- Fisher: The Use of Multiple Measurements in Taxonomic Problems ↗:线性判别思想的奠基论文。
- Rao: The Utilization of Multiple Measurements in Problems of Biological Classification ↗:多变量分类与判别函数的经典拓展。
- Ledoit & Wolf: Honey, I Shrunk the Sample Covariance Matrix ↗:高维小样本协方差收缩的重要工作。
- Hastie, Tibshirani & Buja: Flexible Discriminant Analysis by Optimal Scoring ↗:将判别分析扩展到非线性基函数与更灵活边界。
- scikit-learn: LinearDiscriminantAnalysis ↗:当前求解器、shrinkage、属性与输入输出语义。
14 下一篇预告#
到这里,经典监督学习已经从线性概率、局部邻域、树集成、最大间隔推进到生成式判别。下一步将移除标签:只有一堆样本而没有类别答案时,怎样自动寻找代表性中心?下一篇进入无监督学习,从 K 均值的“分配—更新”循环与失败几何开始。