8大机器学习算法横向评估:从逻辑回归到MLP的完整对比实战
2026/9/8 2:26:35 网站建设 项目流程

1. 为什么要做这次8种算法的横向评估

我先说个自己踩过的坑。早年在项目里训练完模型,汇报的时候被领导问“你这个模型跟别的算法比过吗”,我当时只能拿出一个准确率数字硬撑。后来面试被人追问“为什么选随机森林而不是逻辑回归,SVM在你这个场景的优势是什么”,我发现自己其实说不清楚。这个痛点相信很多人都有过:我们习惯于调库、调参、看loss,却很少认真思考“我的模型在算法谱系里处于什么位置”。

所以这篇文章做了一件基础但重要的事:用同一份标准数据集,把机器学习里最常被提到的8种算法放在同一套评估流程下,统一跑分、统一看指标、统一对比。这8种分别是逻辑回归、决策树、随机森林、梯度提升树、支持向量机、K近邻、朴素贝叶斯、多层感知机(MLP)。

这篇文章适合谁看?两类人。一类是刚学完机器学习基础、准备做实战项目或应付期末的初学者,你需要建立起“怎么评估模型”的完整方法论,而不是只会跑fit和predict。另一类是工作一段时间、在选型时缺乏参考依据的工程师,你可以把本文的对比框架直接复用到自己的数据集上,换数据、换算法,流程完全通用。

先说结论:不同算法在同一份数据上的效果差距,往往比很多人想象的要小,但它们的“性格”差异非常大——有的快、有的稳、有的对特征尺度敏感、有的天生适合高维稀疏。评估模型不能只看一两个指标,你得知道每个指标在说什么,也要知道你的业务到底关心什么。接下来我把整个项目的设计思路、评估方法和实操代码拆开讲。

2. 项目设计与算法选型思路

2.1 为什么恰好是这8种算法

算法库里有几十种分类器,我选这8种不是因为它们最“高级”,而是因为它们覆盖了机器学习里最核心的几类建模思想。

  • 逻辑回归:线性模型的代表,也是很多工业场景的默认基线。它的训练快、可解释性好,适合作为“下限参照物”。
  • 决策树:非线性、非参数模型的代表,是后面所有树模型的基石。单独用容易过拟合,但作为对比能看出“单棵树”和“集成树”的差距。
  • 随机森林:Bagging集成思路的代表。通过多棵树投票来降低方差,是“开箱即用”的典型。
  • 梯度提升树(GBDT):Boosting集成思路的代表。通过串行拟合残差来降低偏差,是Tabular数据竞赛的常胜将军。
  • 支持向量机(SVM):核方法的代表,擅长在高维空间里找最大间隔超平面,适合样本量不算大的场景。
  • K近邻(KNN):基于距离的“懒惰学习”代表,不需要训练,但预测时要遍历全部样本。
  • 朴素贝叶斯:基于概率的生成式模型代表,假设特征独立,计算最快。
  • 多层感知机(MLP):神经网络的最简形态,虽然不算深,但能让你在“传统机器学习”和“深度学习”之间找到一个衔接点。

选择这8种还兼顾了另一个考虑:它们在scikit-learn里都是成熟实现,代码写起来非常顺手,不需要自己写底层逻辑,非常适合做一场“公平对决”。

2.2 为什么用交叉验证而不是一次性切分

很多人评估模型时习惯用train_test_split切一次数据,跑出来一个准确率就完事。这种做法的问题在于:单次切分的偶然性太大。如果随机种子碰巧把难分类的样本都分到测试集里,分数就会虚低;反过来如果测试集太“简单”,分数就会虚高。你拿这一个数字去给别人汇报,其实是站不住脚的。

交叉验证的思路是把数据切成了k份,每次拿其中1份做验证、剩下k-1份做训练,轮流k次,最后把k次结果的平均值和标准差一并汇报。这样做有三个好处:

第一,所有样本都有机会出现在训练集和验证集中,避免了单次划分的运气成分。第二,你可以看到模型的稳定性——如果5次结果的标准差很大,说明这个模型对数据波动很敏感,换一批数据可能就翻车。第三,它更诚实地反映了模型在“未见数据”上的泛化能力,因为每一折的验证集在训练时都没有参与拟合。

我在这篇文章里用的是分层5折交叉验证(StratifiedKFold,n_splits=5)。分层的意思是每一折里正负样本的占比都保持和原始数据一致。这在不平衡数据上尤其重要,否则可能会出现某一折验证集里几乎没有正样本,算出来的召回率直接崩掉。

2.3 数据选择与预处理策略

这次实战选的数据集是scikit-learn自带的乳腺癌数据集(Breast Cancer Wisconsin)。它只有569个样本、30个特征,是一个二分类问题。选它的理由很简单:数据干净、加载一行代码搞定、体积小到可以忽略训练时间,非常适合用来展示评估流程。

但“干净”不代表可以不做预处理。这30个特征的量纲差异很大,有的特征均值在几百,有的只有个位数。对于逻辑回归、SVM、KNN、MLP这类依赖距离或梯度计算的模型,如果不做标准化,那些数值范围大的特征会把模型主导掉,结果会奇差无比。而树模型不受这个影响,因为它们做的是特征值切分,不涉及距离度量。

所以在代码里我会分两条路走:对需要标准化模型的用StandardScaler,对树模型直接喂原始特征。这种“区别对待”不是偷懒,而是理解算法本质后的合理设计。

3. 评估指标的选择与计算细节

3.1 准确率为什么不能作为唯一标准

先说一个脑筋急转弯:如果数据集里98%是负样本、2%是正样本,我把所有样本都预测为负样本,准确率是多少?98%。看起来很厉害,但这个模型毫无业务价值,因为它把一个正样本都识别不出来。

这种场景在现实中太普遍了:信贷违约很少、机器故障很少、疾病发生很少。所以准确率只能作为第一眼看整体水平,绝不能作为唯一依据。我这次选的乳腺癌数据集本身还算平衡(良性357例、恶性212例),准确率还算有参考意义,但它依然掩盖了很多细节。

3.2 精确率、召回率、F1和AUC各自在说什么

精确率(Precision)和召回率(Recall)是一对天生的矛盾。

精确率回答的是“你预测为正类的那些样本,有多少是真正类”。它高,意味着误报少。召回率回答的是“真正的正类样本,你找回了多少”。它高,意味着漏报少。

在医疗场景里,我们宁可误报也不愿意漏报,所以召回率更重要。在垃圾邮件过滤场景里,误杀一封正常邮件损失很大,所以精确率更重要。两个指标的矛盾本质是阈值问题:门槛放低,召回率上升但精确率下降;门槛收紧,反过来。

F1分数是精确率和召回率的调和平均。调和平均对“一个指标特别低”非常敏感,所以F1更像是两个指标的“及格线检查器”,只有当两边都不错时,F1才会高。这比算术平均更严格,也更公平。

AUC则是另一个角度:它衡量的是模型把正样本排在负样本前面的能力,不受阈值影响。AUC等于0.5相当于瞎猜,等于1是完美。它的好处是在类别不平衡时依然稳定,坏处是它关心排序不关心具体分数,有些业务场景(比如需要概率值做风控定价)就不能只看AUC。

这些指标我在代码里全部计算,每折算一次,最后报告均值加减标准差。看表的时候,不要只盯一个数字,而是要横向看整套指标。

3.3 分层K折交叉验证的实现要点

使用分层K折时有几个容易踩的坑,我提前说清楚:

第一,shuffle要设为True并且固定random_state。不洗牌的话,如果原始数据按类别顺序排列,每一折的分布就会严重偏移。固定随机种子是为了让实验结果可复现,否则你下一次跑结果就变了。

第二,任何预处理步骤都要在每一折内部完成,也就是先用训练折fit标准化器,再用它transform验证折。这个点非常重要,属于“数据泄露”的高发区。如果先在整个数据集上做标准化再划分,每一折的验证集信息其实已经通过均值和方差泄露给了训练过程,评估结果会偏乐观。

第三,预测概率和预测标签要分开拿。计算AUC需要predict_proba输出的概率,计算准确率、精确率等需要predict输出的标签。很多人漏掉predict_proba,导致AUC算不出来。

4. 实战:完整评估流程跑分

4.1 环境准备

代码跑在Python 3.9,核心库是scikit-learn 1.3以上版本。如果你用的是Notebook环境,直接安装运行即可:

pip install scikit-learn numpy pandas

还用到了time模块统计训练耗时,这是Python自带的,不需要额外安装。数据加载直接用scikit-learn内置的load_breast_cancer,不需要手动下载文件,这保证了实验的可复现性。

4.2 完整评估代码

下面这段代码,复制到Jupyter Notebook或Python脚本里就能直接跑。

import time import warnings import numpy as np from sklearn.datasets import load_breast_cancer from sklearn.model_selection import StratifiedKFold from sklearn.preprocessing import StandardScaler from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, roc_auc_score ) from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.neural_network import MLPClassifier warnings.filterwarnings("ignore") RANDOM_STATE = 42 N_SPLITS = 5 # 加载数据 data = load_breast_cancer() X, y = data.data, data.target print(f"数据集大小: {X.shape[0]} 样本, {X.shape[1]} 特征") print(f"类别分布: 类别0={np.sum(y==0)}, 类别1={np.sum(y==1)}") # 定义8种算法 classifiers = { "Logistic Regression": LogisticRegression(max_iter=2000, random_state=RANDOM_STATE), "Decision Tree": DecisionTreeClassifier(random_state=RANDOM_STATE), "Random Forest": RandomForestClassifier(n_estimators=100, random_state=RANDOM_STATE), "Gradient Boosting": GradientBoostingClassifier(random_state=RANDOM_STATE), "SVM": SVC(probability=True, random_state=RANDOM_STATE), "KNN": KNeighborsClassifier(), "Naive Bayes": GaussianNB(), "MLP": MLPClassifier(hidden_layer_sizes=(64, 32), max_iter=500, random_state=RANDOM_STATE), } # 需要做标准化的模型 need_scaling = ["Logistic Regression", "SVM", "KNN", "MLP"] # 分层K折 skf = StratifiedKFold(n_splits=N_SPLITS, shuffle=True, random_state=RANDOM_STATE) def run_evaluation(name, model): scaler = StandardScaler() acc_list, prec_list, recall_list, f1_list, auc_list, time_list = [], [], [], [], [], [] for train_idx, test_idx in skf.split(X, y): X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx] # 标准化处理 if name in need_scaling: X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) start_time = time.time() model.fit(X_train, y_train) train_time = time.time() - start_time y_pred = model.predict(X_test) y_proba = model.predict_proba(X_test)[:, 1] acc_list.append(accuracy_score(y_test, y_pred)) prec_list.append(precision_score(y_test, y_pred, zero_division=0)) recall_list.append(recall_score(y_test, y_pred, zero_division=0)) f1_list.append(f1_score(y_test, y_pred, zero_division=0)) auc_list.append(roc_auc_score(y_test, y_proba)) time_list.append(train_time) return { "name": name, "accuracy": f"{np.mean(acc_list):.4f}±{np.std(acc_list):.4f}", "precision": f"{np.mean(prec_list):.4f}±{np.std(prec_list):.4f}", "recall": f"{np.mean(recall_list):.4f}±{np.std(recall_list):.4f}", "f1": f"{np.mean(f1_list):.4f}±{np.std(f1_list):.4f}", "auc": f"{np.mean(auc_list):.4f}±{np.std(auc_list):.4f}", "train_time": f"{np.mean(time_list)*1000:.2f}ms", } results = [] for name, model in classifiers.items(): print(f"正在评估: {name}") result = run_evaluation(name, model) results.append(result) print(f" 完成, 耗时 {result['train_time']}") # 打印汇总表 print("\n" + "="*100) print(f"{'算法':<24}{'准确率':<16}{'精确率':<16}{'召回率':<16}{'F1':<16}{'AUC':<16}{'训练耗时'}") print("="*100) for r in results: print(f"{r['name']:<24}{r['accuracy']:<16}{r['precision']:<16}{r['recall']:<16}{r['f1']:<16}{r['auc']:<16}{r['train_time']}")

代码里有一个细节:precision_score等指标我加了zero_division=0参数。别小看这个参数,在极端情况下,如果某一折验证集里模型没有预测出任何正类,精确率就是除零错误,加了这个参数可以避免程序中断。

4.3 跑分结果参考

在我的机器上(普通笔记本电脑,没有GPU),跑出来的结果大概是这样的:

算法准确率精确率召回率F1AUC训练耗时(单折)
逻辑回归0.97±0.010.97±0.020.97±0.020.97±0.020.99±0.013.5ms
决策树0.94±0.020.94±0.030.93±0.040.93±0.030.95±0.022.1ms
随机森林0.97±0.010.97±0.020.96±0.030.96±0.020.99±0.0118.6ms
梯度提升树0.97±0.010.97±0.020.96±0.030.97±0.020.99±0.0122.3ms
SVM0.97±0.010.98±0.020.97±0.020.97±0.010.99±0.017.8ms
KNN0.96±0.010.96±0.020.96±0.030.96±0.020.98±0.020.1ms
朴素贝叶斯0.94±0.020.94±0.030.93±0.040.93±0.030.97±0.020.6ms
MLP0.97±0.010.97±0.020.97±0.020.97±0.020.99±0.0115.2ms

说三点解释:

第一,不同机器、不同scikit-learn版本跑出来的具体数字会有一两分的浮动,这是正常的,不要过度纠结绝对值。真正重要的是不同算法之间的相对位置和趋势。第二,AUC普遍比准确率高,这是一个常见现象,因为在AUC视角下模型对样本的排序能力比“踩在某个固定阈值上”更容易达到高分。第三,朴素贝叶斯和单棵决策树在这份数据上明显落后于其他算法,这并非偶然,后面会具体分析原因。

5. 结果解读与业务选型建议

5.1 各算法表现差异背后的原因

先看两类“掉队”的算法:决策树和朴素贝叶斯。

单棵决策树容易过拟合,虽然它在训练集上可以做到几乎100%准确,但在验证集上会损失泛化能力。交叉验证恰好把这个弱点暴露出来了。它在这个数据集上拿到94%的准确率并不算差,但和随机森林的97%一对比,就能看到“单棵树”和“装袋后的多棵树”之间的稳定性差距。随机森林通过随机选择特征子集和样本子集,把多棵高方差树的结果平均起来,方差降低了,泛化能力自然更强。

朴素贝叶斯的问题在于它的核心假设:特征之间相互独立。乳腺数据集里的30个特征,很多是高度相关的(比如肿瘤面积的周长、半径、面积本身就有很强相关性),这个假设在这里明显不成立。所以它的表现受限是合理的。

再看高分组。逻辑回归、SVM、MLP都拿到了高分,原因它们都能很好地利用特征的线性组合。逻辑回归本身就是线性边界,SVM通过核技巧可以构造非线性边界,MLP则通过隐藏层的非线性激活函数拟合复杂关系。三个算法原理不同,但在这个中等规模、线性可分性较强的数据集上,效果殊途同归。

KNN的表现也值得注意。它没有任何训练过程,预测的时候靠的是“距离最近的k个邻居投票”。它的优点是不需要对数据分布做假设,缺点有两个:一是样本量大时预测慢,因为要算每个样本到所有训练样本的距离;二是对特征尺度极其敏感,所以我必须对它做标准化。在乳腺癌这种低维小样本数据上,它表现不错,但换个高维稀疏场景,它很可能会垫底。

梯度提升树和随机森林都在同一水平,但在更大、更复杂的数据集上,两者会拉开差距。简单说:如果数据特征和标签之间存在复杂的非线性交互,GBDT这类Boosting方法往往更占优势;如果你的数据包含大量噪声,随机森林因为平均了多棵树的结果,反而更稳定。

5.2 评估结果到底该怎么落地到业务里

拿到表格之后,最重要的事不是“选第一名”,而是结合你的业务场景去考虑成本和风险。

先看训练耗时。在这个数据规模下,8个算法都是毫秒级,差距可以忽略。但换成百万条数据、上千个特征,逻辑回归和朴素贝叶斯依然是毫秒到秒级,SVM就会开始让人等得发慌,MLP在CPU上也会明显变慢。如果你的业务要求频繁重新训练模型,训练速度可能比精度值差那一两个点更重要。

再看业务对错误的容忍方向。还是以医疗场景为例,漏诊一个癌症患者的代价远大于误诊一次,所以你要重点看召回率。如果两个模型的F1相近,但一个召回率高、一个精确率高,选召回率高的那个,然后通过调整判断阈值来优化精确率。垃圾邮件识别则是反过来的逻辑,误伤正常邮件的用户体验成本极高,精确率优先。

还有一类场景你必须冷静:比如银行风控。除了模型精度,你还要考虑可解释性。逻辑回归可以把每个特征的权重直接拿出来汇报给监管;随机森林和XGBoost虽然效果更好,但解释起来要依赖SHAP值等工具,复杂度高很多。这种时候,模型评估就不只是算法问题,而是整个系统设计的一部分。

我个人还有一个习惯:表格里的标准差是决策的重要参考。如果一个模型的准确率均值比另一个高0.5%,但标准差是对方的2倍,我更愿意选那个更稳的。因为真实业务里的数据漂移比你想象中严重,一个对数据波动敏感模型,上线后可能随时给你惊喜。

6. 常见问题与避坑实录

6.1 数据泄露这个坑最容易踩

我在帮人看评估代码的时候,发现最常犯的错就是数据预处理没有放进交叉验证循环里。很多人会先写这样两行:

scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 错误:先用全量数据拟合scaler

然后再去做train_test_split或交叉验证。这种写法是典型的数据泄露。fit_transform在全体数据上计算了均值和方差,再切分的时候,验证集的信息已经通过这两个统计量混进了训练过程。模型在验证集上看起来表现很好,实际上是被“剧透”了。真实线上环境里,你面对新样本时,用的是训练集学到的均值和方差去做transform,所以评估流程也必须模拟这个过程,只能在每一折内部调用fit。

顺带一提,不只是标准化,特征选择、缺失值填充、降维这类包含“从数据中学习统计规律”的步骤,都应该放进交叉验证流程里,而不是先做完再切分。这条规则是评估流程的红线。

6.2 随机性导致你复现不了“别人的分数”

很多人在跑公开数据集的时候会疑惑:为什么我的随机森林准确率跟博客里写的差了2%?原因大概率出在随机种子上。

随机森林里每个样本的选择是随机的,决策树分裂时特征子集的选择是随机的,SVM的求解过程也有随机性,更不用说神经网络了。所以代码里一定要统一设置random_state=42这类固定值。我习惯把所有随机种子定义在脚本顶部,而不是散落各处,方便统一修改。

如果你有两行代码用的是同一个随机种子,它们的随机结果会是序列完全一致的,这保证了可复现。如果某天你发现实验无法复现,第一反应就是检查有没有漏设random_state,或者训练数据顺序发生了改变。

另外,cross_val_score这个函数有个特点:它每次调用会重新生成种子,所以即使你设置了全局随机种子,连续跑两次结果也可能会略有差异。这也解释了为什么有人用同一个脚本跑两次,分数却对不上。

6.3 什么时候调参,什么时候别调参

很多初学者一上来就用GridSearchCV疯狂调参,这是本末倒置的。调参的前提是你已经确定了模型的基本类型,并且这个模型类型在你的数据场景下是合理的。如果你连逻辑回归和随机森林的差距都没评估过就直接对XGBoost调三个小时参,那就是用战术上的勤奋掩盖战略上的懒惰。

正确的顺序是:先跑一批默认参数的模型做横向对比,看谁值得深挖。如果默认参数的随机森林已经达到97%,GBDT也是97%,你再花时间调参的边际收益可能很有限,不如把精力放在特征工程或错误分析上。反过来,如果所有模型都停在90%左右,那你应该先分析数据质量、特征表达、标签准确性,而不是急着调参。

我的判断标准很简单:调参只追求1%的提升,但要付出数倍调参时间的话,不如先去看看那写错的2%样本到底长什么样。错误分析带来的收益,往往比调参大得多。

6.4 评估报告应该包含哪些信息能拿得出手

如果你想把这次评估整理成一份能交付的评估报告,除了上面那张大表,建议再补三样东西:

第一,每个算法的ROC曲线叠在一张图上。线条越多越靠近左上角,说明模型排序能力越强,比单纯看AUC数字直观得多。第二,每个算法预测错误的样本索引清单。把预测错的样本捞出来看看,能帮你判断是特征缺失、标注错误还是边界样本本身无法区分。第三,算法运行时间随数据量增长的趋势。这一项可以用不同子数据集规模测试,对工业场景的扩容预估很有价值。

这三项凑齐之后,你的报告就不再是一堆数字,而是一套有对比、有分析、有结论的完整实验记录。带这种报告去参加评审或汇报,每次都不会被问倒。

我在实际做这种评估时最深的体会是:模型的排名远没有评估流程的严谨性重要。一次精心设计的交叉验证,哪怕结论是“所有模型都一样好”,也比一次随手切分出来的“SVM碾压全场”有价值得多。因为严谨的流程能保证你的结论在换一批数据后依然成立,而随手的结论往往只属于那一次运行。这个横评框架你完全可以带走,把你的数据替换进去,逻辑回归到MLP整整齐齐跑一遍,你会对自己手头的模型有完全不一样的认识。

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

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

立即咨询