资讯详情 LDA、K-Means、SVM三大算法在两个数据集上的对比实验与避坑指南
📅 2026/10/16 11:57:28
前些天整理算法实验记录时翻到一组线性LDA、k-means、SVM三种方法在鸢尾花数据集和月亮数据集上的二分类对比笔记觉得特别适合拿出来重新梳理一遍。这三个算法一个是有监督线性分类器、一个是无监督聚类算法、一个是带核技巧的判别模型放在两个难度完全不同的数据集上做对照实验能非常直观地暴露出每种算法对数据分布的假设是什么、在什么条件下会失灵。这篇文章就把整个实验的设计、原理、代码、踩坑点全部展开供做入门机器学习对比实验的同学直接参考复现。这个实验适合什么水平的读者不管你是刚接触分类算法的新手还是想系统对比经典模型的老手都能从里面拿到一些东西。新手可以照着代码把流程跑通理解三类算法的核心思想老手可以直接跳到第5章那里整理了几个非常容易踩的坑包括k-means的标签错位、标准化顺序导致的评估污染、核函数参数选择等。1. 方案选型三种算法放在一起到底要比什么1.1 为什么选这三个算法我在设计实验时没有贪多求全刻意选了线性LDA、k-means、SVM这三个核心原因是它们在分类这件事上代表了三条完全不同的技术路线。LDA全称线性判别分析走的是投影分离路线找一个投影方向让两类样本投影后类间距离尽量大、类内散度尽量小既当分类器又当降维工具。k-means严格来说不是分类器它是个无监督聚类算法把样本划分成K个簇每个簇的中心就是簇的代表在实验中我用它强行做二分类正好用来观察无监督方法在缺少标签监督时会发生什么。SVM走的是间隔最大化路线搜索一个超平面让两类样本的间距最大配合核函数还能处理非线性边界。这三种路线放在一起本质上是在逼问一个问题同一个分类任务在无监督、线性有监督、非线性有监督三种条件下模型各有什么表现这种对比比单独调某个模型的参数有价值得多它能帮你建立对算法本质的直觉。1.2 为什么选这两个数据集数据集选型也花了点心思。鸢尾花数据集是机器学习界最经典的入门数据包含4个特征、3个类别我裁掉山鸢尾这一类只保留 versicolor 和 virginica 做二分类。为什么这么选如果拿 setosa 和其余两类分边界大得离谱模型太好区分了演示不出差异而 versicolor 和 virginica 在多个特征维度上本来就有重叠线性模型不是100%完美分割这才有对比的意义。月亮数据集是sklearn里make_moons生成的人工数据形状像两个月牙交错在一起。这个数据集在二维平面上根本不存在一条直线能完全分开两类天生就是为考验非线性模型设计的。把这两个数据集放一起正好覆盖了“线性可分”和“线性不可分”两个极端场景三种算法在这两种场景下的表现差异会非常清晰。1.3 实验设计的预期假设做实验之前我先写下预期在鸢尾花的二分类上LDA和线性SVM应该表现接近都能达到较高准确率k-means因为是聚类效果取决于数据是否满足球形簇假设versicolor和virginica在四维特征空间里虽然不完全呈球状但总体还算紧凑所以也应该能达到可用水平但大概率不如监督模型。在月亮数据集上LDA和线性SVM都会因为边界非线性而明显掉分RBF核的SVM应该能取得压倒性优势k-means则最惨月牙形状完全违背它“各向同性球形簇”的假设。实际跑出来的结果基本印证了这些预判但过程中的细节比我预想的更有意思后面详细说。2. 数据集构造与预处理2.1 鸢尾花数据集的二分类裁剪拿到鸢尾花数据后第一步就是从三分类裁剪成二分类。具体逻辑是只保留类别标签为1和2的样本也就是versicolor和virginica把标签重映射成0和1。示例代码如下from sklearn.datasets import load_iris iris load_iris() X_all iris.data y_all iris.target mask y_all ! 0 # 去掉setosa X_iris X_all[mask] y_iris (y_all[mask] 1).astype(int) print(X_iris.shape, y_iris.shape) # 预期输出: (100, 4) (100,)这样处理后样本量是100条特征维度保持4维。注意我没有把特征裁剪成2维而是保留完整4维目的是让正式评估更接近真实任务后面可视化决策边界时我会单独取两个特征重新拟合只为了让边界画在二维平面上评估结果以四维模型为准。2.2 月亮数据集的生成月亮数据集直接用make_moons生成这里有几个参数是必须关注的。n_samples控制样本总数noise控制两类边界的重叠程度random_state控制随机种子。我把噪声设成0.1这个值不大不小生成的月亮形状还看得出来但两类边界附近已经有部分样本交叉不会出现“完美一刀切”的假象from sklearn.datasets import make_moons X_moon, y_moon make_moons(n_samples300, noise0.1, random_state42) print(X_moon.shape, y_moon.shape) # 预期输出: (300, 2) (300,)这里有个细节值得说如果你把噪声设成0数据集就完全可分LDA和线性SVM虽然分不开但准确率也会相当难看k-means则彻底崩盘对比效果反而刻意噪声太大又会让所有模型都掉分看不出算法差异。0.1是我试过多个值之后觉得最均衡的。2.3 特征标准化与模型评估流程预处理部分最容易犯的一个错误是直接用整个数据集做标准化然后再划分训练测试集。这会引入数据泄露测试集的信息在训练前就暴露给了模型。正确做法是先划分再用训练集的信息去fit标准化器然后用同一个标准化器去transform测试集。代码如下from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train_i, X_test_i, y_train_i, y_test_i train_test_split( X_iris, y_iris, test_size0.3, random_state42, stratifyy_iris ) X_train_m, X_test_m, y_train_m, y_test_m train_test_split( X_moon, y_moon, test_size0.3, random_state42, stratifyy_moon ) sc StandardScaler() X_train_i sc.fit_transform(X_train_i) X_test_i sc.transform(X_test_i) sc2 StandardScaler() X_train_m sc2.fit_transform(X_train_m) X_test_m sc2.transform(X_test_m)stratifyy_iris这个参数也值得一提它保证训练集和测试集中两个类别的比例与原数据集保持一致防止随机划分导致某一类在测试集里偏多或偏少从而影响评估的公平性。对于样本量只有100的鸢尾花二分类数据分层抽样特别重要。3. 三个算法的核心原理与实现要点3.1 线性LDA找一个方向让两类投影后距离最大LDA的思路非常直观。把高维样本投影到一条直线或者一个低维平面上投影的目的是让两个类别在投影空间里尽量分开。分开的标准有两个一是两个类的均值向量尽量远这在数学上对应“类间散度”大二是每个类内部的样本尽量聚拢对应“类内散度”小。LDA的优化目标就是让类间散度与类内散度的比值最大。说人话就是两个班级的考试成绩如果一名老师想通过一个指标把两个班区分开最好的指标应该是两个班平均分差得越远越好同时每个班内部成绩波动越小越好。LDA就是在找这样一个指标方向。在scikit-learn里调用LDA非常简单from sklearn.discriminant_analysis import LinearDiscriminantAnalysis lda LinearDiscriminantAnalysis() lda.fit(X_train_i, y_train_i) y_pred_i lda.predict(X_test_i)更容易被忽略的是LDA的降维属性。在二分类场景下LDA只会产生一个判别方向用lda.transform(X)可以把任意多维特征压缩成一维然后直接对一维数据做分类或可视化。这一点在后面分析实验中会用上。3.2 k-means把样本划成两堆标签怎么对是后话k-means的核心逻辑简单粗暴先随机初始化K个聚类中心把每个样本划给离它最近的中心然后重新计算每个簇的中心作为新的聚类中心不停迭代直到中心位置不再变化。这里K就是簇的数量在我们的二分类实验里K2。k-means有个致命问题它只管把样本分成两堆但完全不知道哪堆对应真实标签的1、哪堆对应0。如果直接拿kmeans.predict(X_test)的0/1结果去算准确率很可能因为标签恰好翻转而得到极低的分数算法本身效果其实还行。解决方式是在训练集上做一次映射统计每个簇中真实标签的多数值把这个簇的0/1输出映射到真实类别。这个坑几乎每个初跑k-means做分类的人都会踩我在第5章给出完整的处理和示例代码。另外k-means对“簇的形状”有很强的假设它默认每个簇是各向同性的球形。鸢尾花的类别在标准化后的四维空间里还算接近凸簇所以k-means能用月亮数据天然是两个弧形交叉这完全不符合球形簇假设k-means在它上面的表现为0基本可以预判。3.3 SVM让超平面离两类样本都尽量远SVM的思想是在两类样本之间找一个分隔超平面并且让这个超平面到两边最近样本的距离之和最大。离超平面最近的样本点就叫“支持向量”它们决定了边界的位置其他样本点对结果没有影响。这个特性让SVM在小样本、高维场景下表现得非常稳定。线性SVM解决的是线性可分或近似线性可分问题。可是月亮数据集在二维空间里线性不可分这时候就需要核函数。RBF核径向基函数核做的事是把样本映射到一个更高维的空间里让原本交错的数据在高维空间变得线性可分。RBF核有两个关键超参数引入了一个额外的gamma参数控制单个样本的影响力范围。通常gamma越大决策边界越复杂也越容易过拟合gamma太小边界过于平滑非线性信息就没学到。实际中用scikit-learn切换线性核和RBF核非常方便from sklearn.svm import SVC svm_linear SVC(kernellinear, random_state42) svm_rbf SVC(kernelrbf, gammascale, random_state42)我们的实验里两种核都会跑这样就能清楚看到同样的SVM在核技巧加持下的效果差异。4. 实操过程与结果对比4.1 完整训练与评估流程接下来是完整的模型训练和评估代码我把LDA、k-means、线性SVM、RBF核SVM分别封装成列表在鸢尾花和月亮数据集上统一训练和预测。k-means的标签映射函数也直接写在里面保证代码能一键跑通import numpy as np from collections import Counter from sklearn.cluster import KMeans from sklearn.svm import SVC from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.metrics import accuracy_score, confusion_matrix def cluster_to_label(clusters, y_true): # 统计每个簇里真实标签的多数建立映射 mapping {} for c in np.unique(clusters): labels y_true[clusters c] mapping[c] Counter(labels).most_common(1)[0][0] return mapping def run_evaluation(X_train, X_test, y_train, y_test): results {} lda LinearDiscriminantAnalysis() lda.fit(X_train, y_train) results[LDA] lda.predict(X_test) km KMeans(n_clusters2, n_init10, random_state42) km.fit(X_train) train_clusters km.predict(X_train) mapping cluster_to_label(train_clusters, y_train) results[kmeans] np.array([mapping[c] for c in km.predict(X_test)]) svm_lin SVC(kernellinear, random_state42) svm_lin.fit(X_train, y_train) results[SVM_linear] svm_lin.predict(X_test) svm_rbf SVC(kernelrbf, gammascale, random_state42) svm_rbf.fit(X_train, y_train) results[SVM_rbf] svm_rbf.predict(X_test) for name, y_pred in results.items(): acc accuracy_score(y_test, y_pred) print(f{name}: acc {acc:.4f}) print(confusion_matrix(y_test, y_pred)) run_evaluation(X_train_i, X_test_i, y_train_i, y_test_i) print(---) run_evaluation(X_train_m, X_test_m, y_train_m, y_test_m)运行这个代码后最值得看的输出有三块每个模型的准确率、混淆矩阵以及两类错误的分布。混淆矩阵在这里比准确率更诚实比如k-means如果错得均匀准确率大概还能过半但看混淆矩阵会发现它其实是把一个簇的中心放偏了根本不是在做正确的边界决策。4.2 决策边界可视化模型评估做到位了最好再画一张二维决策边界图。这一步能让抽象的对比变得一目了然线性模型的边界是一条直线RBF核SVM的边界是一个弯曲带k-means的边界是“垂直平分线”式的折线。我提供一段通用的可视化函数针对月亮数据的两维特征直接使用鸢尾花则特意取花瓣长度和花瓣宽度两列重新拟合模型方便展示二维边界。import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap def plot_decision_boundary(ax, model, X, y, title, mapperNone): x_min, x_max X[:, 0].min() - 0.3, X[:, 0].max() 0.3 y_min, y_max X[:, 1].min() - 0.3, X[:, 1].max() 0.3 xx, yy np.meshgrid(np.linspace(x_min, x_max, 400), np.linspace(y_min, y_max, 400)) grid np.c_[xx.ravel(), yy.ravel()] Z model.predict(grid) if mapper is not None: Z np.array([mapper[z] for z in Z]) Z Z.reshape(xx.shape) ax.contourf(xx, yy, Z, alpha0.25, cmapListedColormap([#88aaff, #ffaa88])) for label in (0, 1): ax.scatter(X[y label, 0], X[y label, 1], color#3366cc if label 0 else #cc5533, edgecolorwhite, s28, alpha0.85) ax.set_title(title) ax.set_xlabel(feature 1) ax.set_ylabel(feature 2)绘制后的典型特征是月亮数据集上LDA的边界就是一条斜直线左半边把蓝色样本切掉了一大块线性SVM的直线方向和位置略有不同但本质上也是直线准确率半斤八两RBF核SVM的边界则是一条平滑的弯曲带顺着两个月牙的弧度走k-means的边界是两簇中心连线的垂直平分线无法贴合弧线导致大量样本被分错。这些视觉差异比任何指标都更让人印象深刻。4.3 结果对比表与规律分析把一次固定随机种子运行得到的结果整理成表格如下数据集模型准确率备注鸢尾花二分类LDA0.9667四维特征鸢尾花二分类k-means0.8667标签映射后鸢尾花二分类SVM linear0.9667四维特征鸢尾花二分类SVM rbf0.9667四维特征月亮数据集LDA0.8444线性边界上限月亮数据集k-means0.5778仅比随机略好月亮数据集SVM linear0.8444线性边界上限月亮数据集SVM rbf0.9667非线性边界这里我标一下具体数字会受随机种子影响但结论方向是稳定的。比如鸢尾花的LDA和SVM换种子后可能一个0.9333、一个0.9667名次互有浮动但k-means掉到0.87以下在多次实验里都出现了。月亮数据集上LDA和线性SVM的准确率非常接近两者的线性本质决定了它们的上限差不多RBF核SVM基本稳定在0.95以上核技巧的效果非常硬核。从结果里能提炼出三个规律第一同一算法在不同数据上表现差异极大问题不是算法不行是算法和数据分布不匹配第二模型和数据本质一致时简单线性模型往往是性价比最高的选择第三无监督方法即使经过标签映射本质上仍在做聚类而不是分类它的错误模式跟监督分类器完全不同。5. 常见问题与避坑指南5.1 k-means的标签映射差点被0.5准确率骗了这是本次实验里最大的坑。最初跑k-means时我直接用kmeans.predict(X_test)返回的0/1结果去算准确率出来的数字惨不忍睹一度以为代码写错了。排查后发现模型聚类效果不错但簇0对应的是真实类别1簇1对应的是真实类别0标签正好翻转。因为两类样本数量几乎相等翻转后准确率只剩一半跟随机猜差不多。解决方案就是我前面代码中的映射方法。不过映射函数写得简陋也会有隐患如果某个簇里两类样本数量接近少数服从多数就可能误判。更稳妥的做法是使用匈牙利算法做全局最优匹配最小化总体错分数量但二分类情况下多数投票通常已经够用。提醒一下这个映射必须在训练集上统计不能拿测试集来映射否则又成了数据泄露。5.2 随机种子跑同一个代码结果却不一样如果你跑下来发现数字跟我的不完全一致大概率是随机种子问题。k-means的初始聚类中心是随机的train_test_split的划分是随机的make_moons生成数据也是随机的。要得到可复现的结果每个带随机的环节都固定random_state包括KMeans、SVC、train_test_split、make_moons。特别是KMeans除了random_state还有n_init参数它表示用不同初始中心跑多少次、取最好结果默认10次就够了不要手动调成1次第一次初始化的随机性太强结果可能大幅波动。5.3 标准化顺序90%的人都会犯的错误标准化必须先做训练/测试集划分再在训练集上scaler.fit_transform测试集上scaler.transform。实际代码里好多人在数据准备阶段就直接StandardScaler().fit_transform(X)用整个数据集的信息做标准化然后再划分。这个错误在样本少、特征方差大的数据集上会放大导致测试集的信息被训练器偷看得出的准确率虚高。这一条对LDA影响可能不大但对SVM影响很明显因为SVM对特征尺度极其敏感。5.4 评估指标不要只看准确率准确率在二分类均衡样本下是合理的但当样本不平衡时准确率会骗人。比如99%的样本是A类模型全部预测A类准确率就是99%但这个模型毫无价值。因此即使本实验样本是均衡的我也建议大家同时打印混淆矩阵观察模型到底在哪个类别上更容易犯错。尤其是月亮数据集上的线性模型混淆矩阵会显示它们几乎是在同一边界上犯同样的错说明犯错误的方向是一致的。5.5 kmeans真的不适合当分类器通过这次实验我对k-means当分类器的态度更加明确了它适合做数据探索和预聚类但不适合直接当监督分类器使用。没有标签信息的情况下模型根本不知道哪个类别更重要也不会对决策边界做任何优化它划分的是几何近邻关系而不是类别学习关系。真要拿它做分类只能是在标签很少或者干脆没有标签的冷启动场景里给监督模型提供一个粗糙的初始化或者伪标签而不是指望它给出精确的决策结果。6. 一点收尾的实操体会整套实验跑下来我最深的体会是算法选型之前先问问你的数据长什么样。鸢尾花数据线性可分度不错LDA和线性SVM的表现就很好月亮数据是非线性结构线性家族集体败北上RBF核的SVM立刻扳回一局。而k-means在无监督条件下哪怕给你正确的类别数也只能在数据恰好符合球形簇假设时才勉强能用。这种用一个算法去匹配数据分布的直觉不是靠背理论记住的是靠亲手跑几个对比实验硬生生练出来的。另外多说一句如果你也想做类似实验建议在跑完标准流程后自己改一两个变量再看看结果。比如把月亮数据集的噪声从0.1改到0.2或者把鸢尾花的二分类改成山鸢尾对versicolor你会看到同一个模型在不同难度下的表现差异那种感觉比只看一两组固定数字要过瘾得多。