☰
六个机器学习算法简洁实现与调参避坑指南
2026/9/26 4:22:14 网站建设 项目流程

简介:这是一份面向机器学习初学者与快速实践者的算法简洁实现合集,覆盖决策树、随机森林、XGBoost、梯度提升、主成分分析、支持向量机、线性回归、逻辑回归、K近邻、朴素贝叶斯等常用模型,适合对照理论深入理解核心代码、快速搭建原型或作为课程作业参考。资源共四十七个文件,打包成约五十八KB的zip压缩包,包含二十六份Python源文件(算法实现与运行示例)、十个pyc预编译模块、十份Markdown说明文档和一个txt样例数据,整体小巧紧凑。目前已有185人学习浏览。内容按算法模块独立组织,每个模块均含模型代码、示例脚本和README,例如决策树同时提供分类与回归示例,支持向量机内置多种核函数,工具模块还汇集了损失函数、核函数、数据操作等常用函数。读者可按需查阅任一算法的完整实现,直接运行示例观察效果,也可复制代码结合自己的数据进行二次修改和实验。

1. 常用机器学习算法简洁实现:六个模型一个脚本跑完,别在调库上浪费生命

当你手里只有一份 CSV,老板要求今天下班前把决策树、随机森林、XGBoost、PCA、SVM、贝叶斯回归这几种方法的结果都跑出来对比,很多人会直接去网上找代码,结果被各种版本差异和报错困住。我先说结论:这六个算法在 scikit-learn 和 xgboost 里都有不超过 20 行的简洁实现,真正拉开差距的不是算法本身,而是特征预处理、参数边界和验证方式。这类打包好的常用机器学习算法实现脚本,本意就是把每个算法从黑匣子拉回到可复现的基线模型,让新手能直接跑通,让老手看到默认参数下的性能上限。这篇文章就按这条路径,把每个算法的原理选型、最小代码、参数说明和踩坑点全部拆开。

2. 决策树与随机森林:从单棵树到集成的简洁实现

2.1 决策树分类:默认参数能跑,但你要先调这几个

先放最常见做法。用 scikit-learn 的 DecisionTreeClassifier,一段代码跑通分类:

from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) clf = DecisionTreeClassifier( criterion='gini', # 分裂标准:gini 或 entropy max_depth=5, # 限制树深,防止过拟合 min_samples_leaf=5, # 叶节点最少样本数,调大能进一步剪枝 random_state=42 ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred))

这段代码的逻辑很直白:train_test_split 先把数据切成训练集和测试集,random_state=42 保证每次切分一致,模型可复现。criterion 选 gini 表示用基尼不纯度衡量分裂质量,选 entropy 用信息增益,实际效果经常没有显著差异,不必在这上面花太多时间。max_depth 和 min_samples_leaf 才是必须盯住的参数。

决策树能拟合非常复杂的边界,本质上是按特征取值把一个特征空间递归切分成矩形区域。搜索"决策树如何逼近真实曲线"的人,往往是想理解为什么一棵树在回归任务里会生成阶梯状逼近曲线——因为每个叶子节点输出该区域样本的均值,只要深度足够,它就能用分段常数逼近任意形状的真实函数。问题也伴随而来:深度越大,分段越细,训练集误差越来越小,测试集误差往往先降后升。这就是树模型的核心矛盾。

max_depth=5 意味着最多 5 层分裂,分割粒度有限,但已经足够解决大多数表格数据集。min_samples_leaf=5 强制每个叶子至少 5 个样本,等于提前剪枝,比直接限制深度更稳。如果你发现模型仍然过拟合,优先继续调大 min_samples_leaf,而不是把 max_depth 压到 2 或 3,因为太浅的树会直接欠拟合。我一般先固定 max_depth,再扫 min_samples_leaf,最后看训练集和测试集准确率的差值。

有些场景里你不只是要准确率,还要把决策树画出来给业务看。常见做法是用 sklearn.tree.export_text 输出文字树,或者用 plot_tree 画图。但 plot_tree 在深树时会画成一团密密麻麻的分支,所以可视化前先把 max_depth 调小到 3 或 4。另一个容易翻车的地方:如果某个特征跨越多个量级,树对它的切分点会偏向大数值,数值型特征最好先做标准化,或者至少分箱后再进树。

2.2 随机森林回归:把树集成起来,偏差与方差怎么权衡

随机森林的简洁实现同样在 scikit-learn 里,回归任务看这一份:

from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error rf = RandomForestRegressor( n_estimators=100, # 树的数量;100 是性价比最高的默认值 max_features='sqrt', # 回归任务常用 1/3,分类常用 sqrt max_depth=8, # 单棵树深度,随机森林可以浅一点 min_samples_leaf=3, n_jobs=-1, # 用满 CPU 核,训练速度快很多 random_state=42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) print('MSE:', mean_squared_error(y_test, y_pred))

随机森林在决策树的基础上做了两件随机化的事:bootstrap 采样样本,以及对每个分裂点随机挑一部分特征再找最优切分。这样每棵树都有差异,最后投票或取平均,把多棵高方差低偏差的树组合成低方差模型。它和单棵决策树的区别,用一个词概括就是"以偏差换方差"——单棵树可能过拟合,随机森林用集成把方差压下来,代价是树的整体拟合能力略有下降。

参数里最需要理解的是 max_features。分类任务一般取 'sqrt',即特征数的平方根;回归任务常见做法是取 1/3。这个值越小,树与树之间差异越大,集成效果越好,但单棵树太弱会拉起偏差。n_estimators 是树的数量,从 100 加到 200 收益已经很有限,再多只会让训练时间线性增加。n_jobs=-1 是很多人忽略的参数,随机森林的每棵树可以独立训练,不设这个参数默认单核跑,数据集稍大就会让你怀疑代码卡住了。

选随机森林还是决策树,我的判断标准是看特征噪声和样本量。如果样本只有几百条,特征关系比较干净,决策树加剪枝足够,随机森林反而可能因为 bootstrap 采样让某些小类样本反复出现,造成偏差。如果样本上万、特征几十个,直接上随机森林,配合 n_jobs=-1,训练时间通常只有几十秒。

随机森林另一个价值是免费的变量重要性。训练完后直接看 model.feature_importances_,它会给出每个特征在所有树上的平均不纯度下降。这里的坑是:相关性高的特征会把重要性互相摊薄,两个一模一样的特征,每个的重要性都会变成原来的一半。所以特征重要性适合做粗筛,不适合用来判断"这个特征真的没用"。和决策树不同,随机森林还有 oob_score_ 属性,用没进 bootstrap 样本的数据做验证,设置 oob_score=True 后可以打印出来,能省一次手动的 train_test_split。

3. XGBoost与贝叶斯回归:梯度提升和概率视角的两种极端

3.1 XGBoost二分类与回归:从sklearn接口到原生的差异

XGBoost 是梯度提升在工程上的一次成功落地,它和随机森林最直观的区别在于"串行":随机森林每棵树独立训练,而 XGBoost 每一棵新树都在拟合前面所有树的残差或梯度方向。它自带正则项,天生对缺失值敏感度低,而且可以把训练过程做得很快。做二分类或者回归,最省事的接口是 xgboost 包里的 sklearn 风格类:

import xgboost as xgb from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = xgb.XGBClassifier( n_estimators=200, # 树的数量,配合学习率调整 max_depth=4, # 树深,XGBoost 建议比随机森林小 learning_rate=0.05, # 学习率:调小会更稳,但要配合增加 n_estimators subsample=0.8, # 每棵树随机采样80%样本,防过拟合 colsample_bytree=0.8, # 每棵树随机选80%特征 scale_pos_weight=1, # 二分类样本不平衡时,设为负样本数/正样本数 eval_metric='auc', random_state=42 ) model.fit( X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=50, verbose=False )

这段代码的关键在 fit 里的 eval_set 和 early_stopping_rounds:每训练一轮,模型就在测试集上算一次 eval_metric,一旦连续 50 轮没有提升,训练就提前终止。这比硬编码 n_estimators 更可靠,也避免了过拟合。需要注意,XGBoost 1.6 之后不再需要 use_label_encoder=False,老代码带着这个参数反倒会报警告;如果你用的是旧版本,则要加上。

学习率和树深度是 XGBoost 最影响效果的两个旋钮。learning_rate 太大,训练很快但容易跳过最优区域;learning_rate 太小,又需要非常多的 n_estimators,训练时间成倍增加。我常用的组合是 learning_rate=0.05、max_depth=4、n_estimators=500,配合早停。subsample 和 colsample_bytree 都是随机化手段,数据量小的时候可以保持 0.9 以上,数据量大了再降到 0.8 以下。

训练完之后怎么查看模型?打印 model.get_params() 能看到当前所有参数,xgb.plot_importance(model) 能画特征重要性柱状图,plot_tree(model, num_trees=0) 可以画出第一棵树。这些在"怎么查看xgboost"相关的检索里是最常见的诉求。有一点要清楚:特征重要性图反映的是该特征在树中被用于分裂的次数和收益,不是因果关系的证明,相关性强的特征同样会互相分摊。

如果你不想用 sklearn 风格接口,XGBoost 原生接口 DMatrix + train 也可以,但和你自己的项目代码耦合会更深。我的习惯是:日常对比模型用 sklearn 风格,写参数解释也方便;等到需要自定义目标函数或频繁增量训练时,再切到原生接口。对大部分落地场景,sklearn 风格足够,而且能和 GridSearchCV、Pipeline 等工具无缝配合。

3.2 贝叶斯回归:用sklearn的BayesianRidge,别自己写后验

贝叶斯回归在标题里显得有点"理论",但其实落地很简单。它给回归系数加了先验分布,通过数据更新得到后验分布,效果上等价于一种自动调节强度的正则化。它最适用的场景是小样本、特征之间可能存在共线性、以及你需要预测置信区间。

from sklearn.linear_model import BayesianRidge br = BayesianRidge( alpha_1=1e-6, # 噪声方差的Gamma先验形状 alpha_2=1e-6, # 噪声方差的Gamma先验逆尺度 lambda_1=1e-6, # 系数方差的Gamma先验形状 lambda_2=1e-6, # 系数方差的Gamma先验逆尺度 n_iter=300, # 最大迭代次数 verbose=False ) br.fit(X_train, y_train) y_pred_mean, y_pred_std = br.predict(X_test, return_std=True) print('预测均值:', y_pred_mean[:5]) print('预测标准差:', y_pred_std[:5])

这里 alpha_1、alpha_2、lambda_1、lambda_2 是控制先验强弱的一组超参数,都设成 1e-6 表示让模型尽量从数据里学,不给太强的主观先验。n_iter 是迭代上限,不用刻意调大。predict 时加上 return_std=True,能得到每个预测值的标准差,这在业务上很有价值——比如库存预测里,你不仅想知道预测值,还想知道这个预测值有多不确定。

和普通线性回归、岭回归相比,BayesianRidge 不需要你手动交叉验证选正则化系数,它会在训练中自动更新噪声方差和系数方差。和 Lasso 相比,它是 L2 型收缩,不会把系数精确压成 0,因此不适合拿来降维筛选特征;如果你需要稀疏解,仍然要选 Lasso。很多声音说贝叶斯回归"起步困难",其实 scikit-learn 这个实现已经把所有贝叶斯计算细节封装好了,你只需要知道先验如何影响模型的行为边界。

4. PCA与SVM:降维和分界的组合拳

4.1 PCA主成分分析:先降维再去跑模型,特征脸怎么来

PCA 做的是线性变换:找到数据方差最大的若干个正交方向,把原始特征投影到这些方向上。它没有用到标签信息,所以属于无监督方法。常见误区是认为"PCA 一定能提升模型效果",事实不是这样——它只是把数据换个坐标系,会不会提升取决于原始特征里有多少噪声、以及后续模型对维度是否敏感。

from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA scaler = StandardScaler() X_std = scaler.fit_transform(X) pca = PCA(n_components=0.95) # 保留95%方差贡献,比指定个数更优雅 X_pca = pca.fit_transform(X_std) print('保留维数:', X_pca.shape[1]) print('各主成分解释方差比:', pca.explained_variance_ratio_) eigenfaces = pca.components_ # 每个主成分是输入维度的线性组合

先做 StandardScaler 是 PCA 的前置条件,因为 PCA 找的是最大方差的轴,如果某个特征单位是毫米、另一个单位是米,没标准化的结果会被量纲大的特征牵着走。n_components 可以传整数,也可以传 0 到 1 之间的小数,表示保留累计方差贡献率的阈值。传 0.95 的意思是自动选择让累计解释方差达到 95% 的最少维度数。

pca.components_ 是主成分矩阵,每一行是一个主成分,长度和原始特征数一样。如果你处理的是人脸图像,每个主成分还原成像素后看起来像一张"基脸",这就是常说的 PCA 特征脸。特征脸方法当年能做人脸识别,就是把人脸图像拉直成向量,用 PCA 降维后,拿低维向量在低维空间里做分类或距离度量。它只捕捉线性结构,对光照、表情变化并不鲁棒,但作为理解 PCA 的例子很有代表性。

使用 PCA 的一个现实价值是去相关。很多模型假设特征相互独立,但表格数据里特征往往高度相关。PCA 之后各主成分两两正交,这一步能消除共线性带来的数值不稳定。另一个价值是压缩:如果你的原始特征有上千维,而前 20 个主成分已经解释了 95% 方差,后续训练的时间开销会大幅下降。代价是每个主成分都是原始特征的线性组合,可解释性会变差,业务方不一定愿意接受"你让我看这个看不到原始意义的特征"。

4.2 SVM支持向量机:线性核与RBF核的选型,以及和PCA连用

SVM 的思想是找一个最大间隔超平面来分开两类样本。硬间隔要求所有样本都分对,现实中几乎不可能;软间隔引入松弛变量,允许少数样本犯错,通过参数 C 控制对错误的容忍度。线性 SVM 只适用于线性可分数据,遇到非线性边界就需要核函数,最常用的是 RBF 核,也就是高斯径向基核。

from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC pipe = make_pipeline( StandardScaler(), PCA(n_components=0.95), SVC(kernel='rbf', C=1.0, gamma='scale', probability=True, random_state=42) ) pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test)

这段代码直接把标准化、PCA、SVC 串成一个 Pipeline,fit 和 predict 都只需要调用 pipe 一次,非常干净。SVC 里的 C 是误分类惩罚:C 越大,模型越不愿意允许训练集出错,越容易过拟合;C 越小,间隔越宽,容忍错分越多。gamma 控制 RBF 核的单个样本影响半径,gamma='scale' 表示按特征标准差自动缩放,这是最省心的选择;手动设置时,gamma 太大每个样本只影响自己周围很小的一片区域,容易过拟合,gamma 太小所有样本都"糊"在一起,模型失去表达性。

这里有个很多新手的误解:搜索"SVM 的梯度下降"时,会看到有人想用梯度下降实现 SVM。实际上 scikit-learn 的 SVC 默认用的是 libsvm 的 SMO 算法,通过坐标轮换求解对偶问题,并不是梯度下降。梯度下降方式的 SVM 是可能的——把损失函数换成合页损失,然后用次梯度下降迭代,但收敛速度和对参数容忍度远不如成熟的 SMO,落地时不建议自己造这个轮子。

为什么要把 SVM 和 PCA 连用?RBF 核在高维稀疏空间里容易失效,特别是原始特征几百上千维、但很多特征本质是噪声时,PCA 先压缩出几十个主成分,能减少核函数在无意义维度上的计算量。另一个原因是 SVM 对特征尺度极其敏感,标准化是硬前提,而 PCA 也要求标准化,所以三者组成 Pipeline 是常见做法。在使用前先检查你训练集的分布,如果类别极度不平衡,SVC 的 class_weight='balanced' 是一个很实用的设置,它会自动为少数类加大权重。

5. 避坑:六个算法在落地时最常踩的 5 个坑

5.1 决策树过拟合:训练集 100%,测试集一塌糊涂

现象:决策树在训练集上准确率接近 1,测试集掉到 70% 甚至更低。你画出来的树非常深,很多叶子下面的样本数只有 1 或 2。

原因:默认的 DecisionTreeClassifier 不限制树的生长,它会递归切分到所有叶子都"纯"为止,等于把每个训练样本都记住了。测试集只要有一个样本落在训练样本没覆盖到的区域,模型就只能瞎猜。

解决:把 max_depth 限制在 5 到 8,min_samples_leaf 至少提到 5。调整时不要只看测试集准确率,还要对比训练集和测试集的差值,差值超过 10% 基本就是过拟合。更稳的做法是用 GridSearchCV 扫一遍 max_depth、min_samples_leaf、min_samples_split 的组合,但我建议先手动试两组再上网格,避免一次跑几十棵深树浪费时间。

5.2 随机森林训练慢且结果没提升

现象:n_estimators 从 100 加到 500,训练时间从几分钟变成几十分钟,准确率只提升了 0.1%。机器风扇狂转,代码仿佛卡死。

原因:随机森林的边际收益随树的数量快速衰减。500 棵树相比 100 棵,集成方差下降已经非常有限,但训练时间线性增长。还有一种情况是特征很多而你用的是默认 max_features='auto',每棵树都要评估大量特征组合,拖慢速度。

解决:n_estimators 设定 100 就够了,如果你需要更精确,先跑一边 n_estimators 的收敛曲线,从 20 到 200,看看在哪个区间指标不再上升,而不是直接拉满。max_features 用 'sqrt',回归用 1/3,n_jobs=-1 让所有核都干活。如果这样还慢,说明随机森林不适合你的数据规模,试试 HistGradientBoosting 这类更快的梯度提升实现。

5.3 XGBoost 在二分类上预测结果全是多数类

现象:模型训练完,predict 的结果几乎全是 0 或全是 1,auc 在 0.5 附近,print(classification_report) 里少数类的 precision 是 0。

原因:最常见的是正负样本严重不平衡,模型发现全部预测成多数类也能拿到很高的准确率。另一个原因是 learning_rate 设置过大,训练在早期就震荡甚至发散;还有人在 fit 里传 eval_set 的验证集分布和训练集不一致,导致早停被错误触发。

解决:在 XGBClassifier 里设 scale_pos_weight = 负样本数 / 正样本数,这是针对二分类不平衡最直接的杠杆。然后 learning_rate 降到 0.01 或 0.05,n_estimators 提到 500 以上配合 early_stopping_rounds=50。还要检查 eval_set 的选择,验证集必须来自同一分布,最好用 train_test_split 的 stratify 参数保证类别比例一致。

5.4 PCA 降维后模型效果反而变差

现象:PCA 之后维度从 20 降到 10,准确率掉了 5 个百分点。你以为是对的原理,结果数据越来越差。

原因:PCA 找的是方差最大的方向,不是分类最有效的方向。方差大的维度里可能混杂着大量与标签无关的噪声;或者你砍得太狠,把少量有价值的信息也删掉了。如果你设置了 n_components=0.9,剩下 10% 的方差里可能正藏着一个强分类特征。

解决:先打印 pca.explained_variance_ratio_ 的前 10 项,观察累计贡献曲线,通常保留到 0.95 到 0.99 比较稳。不要为了"降维"而硬降到某个整数。如果 PCA 之后效果仍然不好,说明这个数据集的特征相关性不强,或者关键信息在非线性的结构里,这时候换成有监督降维,比如 LinearDiscriminantAnalysis,或者直接不降维。

5.5 SVM 对特征尺度极其敏感,不同量纲下结果完全不一样

现象:同一份数据,标准化前跑 SVM 准确率 60%,标准化后 90%,代码没变,就多了几行 StandardScaler。你甚至怀疑是随机种子的问题。

原因:RBF 核计算的是样本间距离,如果一个特征是房价单位是万元、另一个特征是面积单位是平方米,面积的数量级会主导距离计算,SVM 就只看得到面积这个维度。决策树和随机森林对量纲不敏感,不需要标准化,但换成 SVM 很吃亏。

解决:把 StandardScaler 放进 Pipeline 中,在 SVC 之前执行。标准化之后,C 和 gamma 的搜索范围也要重新设定。我惯用 C=10**-2 到 102,gamma=10-3 到 10**1,用 GridSearchCV 做交叉验证。另一个技巧是 SVC 里的 probability=True 会触发额外的 Platt 缩放,训练时间变长,但能输出概率;如果你的下游任务只看排序不分,不一定需要这个参数,去掉能省不少时间。

6. 把六个算法装进一个脚本:Pipeline、交叉验证与结果对比的进阶玩法

当你已经跑通每个算法,下一步是把它们放进同一套验证框架里,用同一份切分数据对比效果。这是这类算法实现包最常见的落地姿势:先建基线,再决定哪个算法值得深调。

from sklearn.model_selection import cross_val_score, KFold from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.decomposition import PCA from sklearn.svm import SVC import xgboost as xgb import numpy as np models = { 'DecisionTree': make_pipeline( DecisionTreeClassifier(max_depth=5, random_state=42)), 'RandomForest': make_pipeline( RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=42)), 'XGBoost': make_pipeline( xgb.XGBClassifier(n_estimators=200, max_depth=4, learning_rate=0.05, eval_metric='auc')), 'SVM': make_pipeline( StandardScaler(), SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42)), 'PCA+SVM': make_pipeline( StandardScaler(), PCA(n_components=0.95), SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42)), } cv = KFold(n_splits=5, shuffle=True, random_state=42) for name, model in models.items(): scores = cross_val_score(model, X, y, cv=cv, scoring='roc_auc') print(f'{name}: {scores.mean():.4f} ± {scores.std():.4f}')

这里我用 KFold 固定了 5 折切分,确保每个模型见得训练数据完全一样,对比才有意义。scoring 选了 roc_auc,如果是回归任务就改成 neg_mean_squared_error,并把模型换成 BayesianRidge。你还可以在 models 字典里加一项 'BayesianRidge',不过它本质上是线性模型,跑之前最好先对特征做标准化,否则系数后验估出来会被量纲牵着走。

这个脚本的价值不是一次追求单模型 SOTA,而是让你在五分钟内看到:哪类算法在这个数据集上根本不行,哪类值得投入时间调参。我会把"调参直觉"写在注释里,比如 gamma='scale' 是自动缩放,比手动试 gamma 更安全;SVM 和 PCA 连用时,PCA 放在 StandardScaler 之后、SVC 之前,这个顺序不能颠倒。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询