☰
鸢尾花与月亮数据集上线性LDA、K-means、线性SVM二分类对比
2026/10/14 14:42:26 网站建设 项目流程

1. 实验动机与二分类问题设定

1.1 为什么选鸢尾花和月亮数据集做对照组

很多刚接触机器学习的人都会经历一个阶段:拿一个现成的算法库,把数据集喂进去,看准确率打印出来,然后就没有然后了。我以前也这么干过,后来发现这种“黑盒跑数”对理解算法几乎没有帮助。真正让我把分类问题看明白的,是回到最基础的数据集上,把算法之间的差异一层层拆开来看。

这次实验选择鸢尾花数据集和月亮数据集,原因是它们刚好处于两个极端。鸢尾花数据集有4个特征、3个类别、150个样本,特征是真实的植物测量数据,类别之间的区分度有高有低。而月亮数据集是人工构造的非线性分布数据,两个类别的样本像两弯对称的新月一样互相交错,几乎没有一个线性分类器能很好地处理它的原始形态。

单纯做多分类还不够,我想看的是二分类。所以我把鸢尾花数据集的三个类别做了处理:挑出versicolor和virginica这两个类别来构成二分类问题。为什么不选setosa这个类别?因为setosa的花瓣长度和花瓣宽度跟另外两个类别差距太大了,任何线性分类器都能在它身上拿到接近100%的准确率,那样实验就没有区分度可言。versicolor和virginica在特征空间里有明显的重叠区域,这才像一个有挑战性的二分类问题。

月亮数据集则用来测试线性算法的最短板。如果某个算法在鸢尾花上表现很好,却在月亮数据集上明显失效,对比结论就会非常直观。这两个数据集放在一起,恰好覆盖了“线性可分”和“非线性可分”两种典型场景。

1.2 实验环境、评测方式与固定随机种子

整套实验我跑在Python 3.9环境下,主要用到scikit-learn、matplotlib和numpy。算法方面固定使用线性LDA、k-means和线性SVM三种,输出结果统一用测试集准确率衡量。我不做任何特征工程,不额外增加核函数,也不对k-means做层次聚类之类的复杂包装,目的就是看这三个算法最朴素的表现。

用到的库版本大概是这样的:

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_iris, make_moons from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.cluster import KMeans from sklearn.svm import SVC from sklearn.metrics import accuracy_score

这里有一个非常关键的细节:整个实验必须固定随机种子。鸢尾花数据集本身样本量小,训练集和测试集的划分比例对结果影响很大。我不希望不同算法之间因为数据划分不同而出现不公平的比较。所以在最开始就统一用train_test_split(test_size=0.3, random_state=42)做划分,三个算法都在同一份训练集和测试集上训练、评估。

实验参数统一整理如下:

实验配置值
鸢尾花二分类类别versicolor(类别1)与virginica(类别2)
月亮数据集样本数300个样本,噪声0.15
训练集/测试集比例70% / 30%
随机种子42
比较算法线性LDA、k-means、线性SVM
评价指标测试集准确率

我会分两个大块来讲实验结果,先看鸢尾花,再看月亮数据集。看完结果再拆解原理,最后聊聊在实际操作中踩过的坑。

2. 鸢尾花数据集上的二分类:三个算法的真实表现

2.1 数据预览与预处理

我先把加载数据、筛选类别、标准化特征的代码贴出来。注意这里第二步很关键,一定要在划分数据集之前做标准化,否则会影响k-means的距离计算。

iris = load_iris() X = iris.data[iris.target != 0] # 只保留类别1和类别2 y = iris.target[iris.target != 0] # 对应的标签,转换为0/1 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.3, random_state=42 )

标准化这步我在很长一段时间里都觉得无所谓,直到后来做k-means才发现自己吃过大亏。鸢尾花的4个特征是花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位都是厘米,但数值范围不太一样。花萼长度在5到8之间,花瓣长度在1到7之间,而花萼宽度可能只有2到4。如果不做标准化,距离计算时数值大的特征会主导聚类结果,花萼长度一个变量就能把其他三个变量淹没掉。

预处理完之后,先看一眼两个类别在四个特征上的分布。versicolor和virginica在花萼长度、花萼宽度上重叠得非常厉害,但在花瓣长度和花瓣宽度上,虽然也有重叠,却存在一个比较明显的分界区域。这个信息很重要,它告诉我:线性模型有可能找到一条还算凑合的决策边界,但绝对不会是完美直线。

2.2 三种算法的实现与结果

三个算法的核心代码都很短,scikit-learn大大简化了使用成本。先看线性LDA:

lda = LinearDiscriminantAnalysis() lda.fit(X_train, y_train) y_pred_lda = lda.predict(X_test) acc_lda = accuracy_score(y_test, y_pred_lda)

再看k-means。k-means本身是无监督聚类算法,不是分类器,但我们可以把聚类结果映射到标签上。思路是这样的:先用KMeans把样本聚成2类,然后根据每个聚类中心离哪个真实类别的中心更近,把聚类标签转换成预测标签。

kmeans = KMeans(n_clusters=2, random_state=42) clusters = kmeans.fit_predict(X_train) # 将聚类中心与训练集真实类别的均值中心对齐 for i in range(2): mask = (clusters == i) if np.mean(X_train[mask], axis=0)[2] > np.mean(X_train[y_train == 1], axis=0)[2]: clusters[mask] = 1 else: clusters[mask] = 0

这种映射逻辑有点绕,但本质很简单:哪个聚类包含更多标签为1的样本,就把这个聚类整体当成类别1。实际项目里如果要做无监督聚类辅助分类,也基本是这个套路。最后是线性SVM:

svm = SVC(kernel='linear', C=1.0, random_state=42) svm.fit(X_train, y_train) y_pred_svm = svm.predict(X_test) acc_svm = accuracy_score(y_test, y_pred_svm)

在我的固定种子和固定划分条件下,三个算法的表现如下:

算法鸢尾花二分类测试集准确率
线性LDA0.9667
k-means0.7333
线性SVM0.9667

线性LDA和线性SVM准确率持平,都达到了96.67%,只有两个样本分错。而k-means的准确率骤然降到73.33%,有8个测试样本被分错。一开始我看到这个数字觉得很不对劲,甚至怀疑是聚类标签映射写错了。后来把聚类结果和真实标签画在一起看,才发现问题出在k-means对类别重叠区域的处理上。

2.3 从决策边界看LDA与SVM在鸢尾花上的差异

虽然准确率一样,但线性LDA和线性SVM的决策边界在细节上是不同的。我分别把两个算法的决策边界画在花瓣长度和花瓣宽度的二维平面上,线LDA的分界线是一条接近斜45度的直线,而线性SVM的分界线同样是直线,但更偏向于在两类样本之间“撑开一个最大间隔”。

这就引出我这次实验的第一个心得:准确率相同不代表分类逻辑相同。LDA考虑的是全局的类间散布和类内散布,它找的是一个既能让两个类别中心彼此远离、又能让每个类内部样本尽量集中的投影方向。SVM考虑的是支持向量附近的几何间隔,它不关心类别内部的整体分布,只关心最难分的那几个样本点的位置。

两者的抗噪能力也不同。在这个数据上,LDA对类别边界上的两个样本更敏感,SVM则因为有间隔最大化的约束,对某些偏离点的容忍度稍强。但这两个算法的本质都是线性分类器,在versicolor与virginica这种本身就存在特征重叠的数据上,它们的上限定死在某个范围内,再调参也很难超过这个上限。

至于k-means,准确率低不是因为它笨,而是因为它做的事情根本就和分类不一样。k-means的目标是让簇内平方和最小,它不会关心“这个样本属于哪个真实类别”,只会把特征空间上靠近的样本聚到一起。当两个真实类别的样本在特征空间里几乎混合在一起时,聚类结果自然不可能总是和真实标签对齐。

3. 月亮数据集上的二分类:线性模型的短板与遭遇

3.1 月亮数据是如何生成的

月亮数据集是sklearn里make_moons函数生成的合成数据。它的生成逻辑是:在二维平面上构造两个半圆弧,把两个半圆弧上下错开,形成一种像两弯月亮交错的样子。再往样本点里加入高斯噪声,让边缘有重叠和抖动。

我这次用的生成参数如下:

X_moons, y_moons = make_moons(n_samples=300, noise=0.15, random_state=42) X_train_m, X_test_m, y_train_m, y_test_m = train_test_split( X_moons, y_moons, test_size=0.3, random_state=42 )

因为月亮数据集本身已经是二维特征,而且两个维度的数值范围差不多,不标准化也不会有太大影响,但我还是照之前一致的处理方式做了标准化。注意,这里我没有对月亮数据做任何坐标变换或特征映射,比如没有手动添加x1*x2这类交互项,也没有加径向基核函数,目的就是观察线性算法最原始的能力。

3.2 三个算法在月亮数据集上的表现

同样三套代码直接跑,只是数据源换成了X_moons和y_moons。结果如下:

算法月亮数据集测试集准确率
线性LDA0.8667
k-means0.7111
线性SVM0.8778

线性SVM的准确率最高,但也就87.78%,跟鸢尾花上的96.67%相比下滑了一大截。线性LDA稍低一点,86.67%。k-means再次垫底,只有71.11%。

把决策边界画出来之后,原因一眼就能看清楚。线性SVM在月亮数据集上得到的是一条直线,这条直线不管是横着画、竖着画还是斜着画,都不可能把两个互相缠绕的半圆完全分开。它只能勉强找到一条线,把大部分类别0的样本划到一侧,再把部分类别1的样本划到另一侧,落在边界附近的样本就不可避免地被分错。

LDA的处境类似,它找到的投影方向是让两类均值之间的距离最大,可两个半圆在投影到任意一条直线上之后,仍然有一大段区间重叠。这意味着LDA的线性投影结构天生就不适合这种数据。

k-means垫底也在意料之中。k-means的聚类中心是硬性的“均值中心”,两个半圆类别的真实均值可能落在非常接近的位置,甚至有交叉。当簇中心分不开的时候,围绕中心划分的Voronoi边界自然也会切错很多样本。

3.3 线性模型“尽力了”但依然失败的关键原因

很多人看到这个结果第一反应是“线性SVM不行嘛”,然后赶紧换核函数。但我的体会是,这个结果更有价值的地方在于告诉我们:线性模型的失败不是调参能解决的,而是模型容量的问题。

我试着把线性SVM的正则化参数C从0.1调到1.0再调到10,决策边界虽然稍微移动了一下,但准确率始终在86%到89%之间徘徊。C越大,边界越严格去拟合训练数据,可这条直线本身就没有弯折能力,再怎么严格也只是换了个角度的直线。

同样的道理,LDA在月亮数据集上无论怎么调solver,结果都差不多,因为LDA对数据线性结构的假设已经和真实分布不匹配了。你在维度灾难、数据分布不满足假设的时候硬套线性算法,结果基本就是撞运气,看这条直线到底落在哪里。

月亮数据集之后还给了我一个启发:在实际工业项目里,如果特征维度很高、样本分布又复杂,先用一个简单的线性模型做基线,再用非线性模型去比较提升幅度,是衡量一个模型价值的常见做法。月亮数据集就是这个“基线性测试”的缩影。

4. 线性LDA、k-means和线性SVM的原理拆解与实现要点

4.1 线性LDA:用投影方向阐释“同类聚合、异类分离”

线性LDA的核心是一句话:找一个投影方向,使得不同类别投影后的均值差异尽量大、同类别内部的方差尽量小。这句话展开来涉及两个矩阵:类间散布矩阵和类内散布矩阵。类间散布矩阵衡量的是各个类别均值与总体均值之间的差异,类内散布矩阵衡量的是每个类别内部样本偏离该类均值的程度。

LDA的目标函数就是最大化“类间散布/类内散布”这个比值。得到最优投影方向之后,如何做分类?实际应用里有两种思路:一种是把原始数据降到一维,在一维空间里设定一个阈值,根据投影值大小判断类别;另一种是直接用贝叶斯决策规则,参照各类别的高斯分布来计算后验概率。scikit-learn里的LinearDiscriminantAnalysis默认走了后一种思路,它假定每个类别都服从高斯分布,并且各类别协方差矩阵相同,然后在这个假设下输出每个测试样本属于每个类别的概率。

我实际使用中比较在意的点是:LDA对类别数目没有硬性限制,二分类只是它最简单的用法。对于多分类问题,LDA会自动寻找多个判别方向。但一旦特征数远大于样本数,类内散布矩阵会变得奇异,直接求逆就会出问题,所以正规化参数solver的选择也很重要。

这次实验用标准化后的4个特征跑LDA,效果已经很稳定了。如果你想自己单独看一下LDA的投影效果,可以调用lda.transform(X_train)把样本降到1维,再画个直方图,能很清楚地看到两个类别投影后是否分得开。

4.2 k-means做分类:与会话式的最近邻是两回事

k-means算法本身是一个无监督聚类方法,它的优化目标是让每个样本到所属簇中心的距离平方和最小。在二分类场景中,我们做的事情是先用聚类把样本分成两组,再通过某种规则给两组贴上“类别0”和“类别1”的标签。

这种思路看起来合理,实际上有一个隐藏前提:在特征空间里,每个真实类别大致对应一个紧凑的簇。但真实数据常常不满足这个前提,比如刚才的月亮数据集,一个真实类别可能分布在两个不相连的区域,指望k-means一个簇去覆盖一整条弯月,本身就是不现实的。

我在实验里用的标签映射方法比较常规粗糙:算每个簇中心与训练集两个类别均值中心的距离,哪个近就把簇标成哪个类别。更严谨一点的做法是,先算交叉矩阵统计每个簇中真实类别0和类别1的样本数,以多数原则来定标签。sklearn里没有直接提供这种“聚类转分类”的便捷接口,但写起来也不难:

def map_kmeans_labels(kmeans_model, X_data, y_true): clusters = kmeans_model.predict(X_data) mapper = {} for i in range(kmeans_model.n_clusters): mask = (clusters == i) if np.sum(y_true[mask] == 0) >= np.sum(y_true[mask] == 1): mapper[i] = 0 else: mapper[i] = 1 return np.array([mapper[c] for c in clusters])

这种映射只允许把“聚类结果”翻译成“分类结果”,没有改变k-means自身的局限性。所以k-means在分类任务里更适合做辅助工具,比如先把数据分成几个大块,再在每个大块里训练一个简单分类器,而绝不是直接替代监督学习分类器。

4.3 线性SVM:间隔最大化几何意义大于分类本身

线性SVM的数学骨架如下:在特征空间中寻找一条直线,使得最近的正类样本和负类样本到这条直线的距离都尽可能大,这个距离之和被称为间隔。离边界最近的那些样本就是支持向量,它们决定了最终的分类边界。

SVM的损失函数是下面这个结构的简化版:

svm_lin = SVC(kernel='linear', C=1.0)

C是正则化参数,它控制着“尽可能使间隔最大”和“尽可能让训练样本被分对”之间的平衡。C越小,模型越倾向于选择大间隔的简单边界,哪怕会牺牲一些训练集准确率;C越大,模型就尽可能分对每一个训练样本,但边界也更容易过拟合。

这次实验用默认的C=1.0就够了,鸢尾花数据上直接得到96.67%准确率。对于月亮数据集,我可以明确地说,无论C怎么调都无法完成完美分类,这是线性核本身缺少非线性映射能力的体现。SVM真正强大的版本是引入核函数,比如RBF核,可以将数据映射到高维空间后再找超平面。但标题既然限定在线性SVM,我们就不扩展到核方法,只展示线性SVM在两类数据集上的能力边界。

5. 横向对比:四组实验结果与算法选型总结

5.1 所有结果汇总

把两个数据集、三个算法的准确率放到一张表里看,结论会更清楚:

数据集线性LDAk-means线性SVM
鸢尾花(versicolor vs virginica)96.67%73.33%96.67%
月亮数据集86.67%71.11%87.78%

单看数字,线性LDA和线性SVM在鸢尾花上并列第一,在月亮数据集上线性SVM略好一点点。但准确率只差一个百分点,测量偶然性很大,我更倾向于认为两者在二维人工数据上能力相当。

k-means在两个数据集上都是垫底。这并不丢人,它与前两者根本不是同一个问题框架。拿一个聚类算法去硬做分类任务,得到的分数更多是“数据结构凑巧与聚类结果匹配的程度”,而不代表算法本身的分类智慧。

5.2 算法选择的实际经验

经过这轮实验,我对“什么时候用哪个算法”有了比较明确的判断:

第一,样本量小、特征维度中等、数据整体线性可分度尚可的时候,线性LDA是个极好的起点。它自带降维能力,模型解释性好,计算量小,几个特征就能跑完。

第二,如果业务场景里更在意决策边界的鲁棒性,并且样本类别有些重叠,线性SVM通常比LDA更稳一些。尤其当特征维度较高时,SVM只依赖关键样本点,对全局分布不那么敏感。

第三,k-means不适合直接当分类器用,但它适合做标签稀疏情况下的前置探索。比如在标注数据不足时,先用k-means大致摸清楚样本群聚结构,再结合少量人工标注来判断每个簇对应什么类别,这种用法比直接预测分类有价值得多。

第四,一旦数据分布明显非线性,线性模型的表现上限很快触顶,这时候必须考虑非线性模型或特征映射。月亮数据集在这里给出的启示很典型,86%的准确率看似不低,但对于某些业务来说,如果边界附近的错误恰好命中要害样本,这种模型就无法上线。

6. 我把实验重复了多次之后踩过的心态

6.1 标准化这一步,直接影响k-means的分类上限

我前面提过标准化的重要性,这里想再展开。第一次做这个实验时,我没有对鸢尾花特征做标准化,直接拿原始特征给k-means跑,nl只是改了数据尺度,k-means的准确率就完全变了。当时花萼长度因为数值范围最大,几乎主导了整个距离计算,聚类结果跟花瓣特征完全失去关联。后来加上StandardScaler,每个特征都变成均值为0、方差为1,k-means才算真正“公平”对待每个特征。

这件事给我留下的教训是:任何基于距离的算法,比如k-means、KNN、SVM中的高斯核,在使用之前都必须先确认各个特征是否处于相近的尺度。有些算法内部会做尺度归一,有些完全不会。线性LDA和线性SVM在鸢尾花原始特征上表现也不会差太多,但为了防止比较变量,我还是统一做了标准化,这样所有算法都面对同一种数据输入。

6.2 随机种子和初始化是实验复现的底线

第二个大坑是随机种子。k-means的初始化具有随机性,即便random_state=42,第一次运行和第二次运行聚类中心初始位置不同,结果也可能不同。我在实验途中不小心把kmeans的random_state删掉了,结果同样数据集、同样代码,跑出来的准确率在70%到83%之间大幅跳动。

这里要说明,并不是k-means严格不可复现,它可以设置random_state来固定初始中心,也可以调整n_init参数来增加不同初始化尝试的次数。scikit-learn默认的n_init=10,意思是做10次不同初始化,选出最优结果。实际使用中,如果数据量稍大,可以适当增大n_init,让结果更稳定,同时保留random_state以便别人复现实验。这不是玄学,是实验记录的必要环节。

6.3 二分类问题的构造方式,决定了实验是否真正有意义

我在设计鸢尾花二分类实验时,一开始也曾偷懒用setosa对其他两类,然后得到100%的准确率,沾沾自喜觉得模型真厉害。后来冷静下来一想,这种实验根本没有区分度。不但在LDA上能得到100%,换个其他算法同样能得到100%,整个对比实验就是个笑话。

这也引出一个通用原则:对比算法的前提是“问题本身有难度”。如果一个数据集连最简单的多数类投票都能拿到九成准确率,那你做再多的调参、再精细的可视化,都是浪费时间。做算法对比一定要找一个边界清晰但不简单的任务。versicolor和virginica就是比setosa vs rest好得多的选择,因为它们之间的重叠是真问题,算法之间细微的分布假设差异才会浮现出来。

6.4 决策边界的可视化,比准确率数字信息量多得多

最后分享一个我最推荐的小技巧:做这类实验一定别只读准确率,一定要把决策边界画出来看。准确率是一个压缩到单个数字的总结指标,而决策边界完整地展示了模型在特征空间里的行为模式。

比如,线性LDA和线性SVM在鸢尾花上准确率同为96.67%,但决策边界有明显区别.线SVM分隔两类的过程中,有时候会把原本LDA能分对的某个样本也放在错误一侧。从这个角度看,如果业务场景中更关心“边界附近的可信度”,SVM的间隔特性会更有价值;如果更关心“整体类别分布的拟合”,LDA的全局统计假设可能更契合。

我建议大家把这张图贴在自己的实验报告里,调整C参数、切换数据集时反复对照,很快就能形成对线性模型边界形态的直觉。有了这种直觉,以后再去理解核SVM、非线性LDA甚至神经网络的决策区域,思维的切入点都会截然不同。

这次实验做下来,我最深的一个体会是:算法对比实验的价值不在于证明谁比谁好,而在于说明每种算法的假设和代价在哪里。线性LDA假设数据近似高斯分布,SVM执着于最大化边界间隔,k-means压根不理会标签的存在。用两个差异足够大的数据集去验证这些特点,比单纯记住公式有效得多。如果你也想自己动手复现一次,记得把标准化、随机种子和类别选取这三件事放在首位,剩下的自然会在运行结果中给出答案。

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

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

立即咨询