十大机器学习算法实战指南:从原理到代码落地与避坑
算法这东西,光看公式和论文很容易产生一种“我懂了”的错觉,真到要动手解决问题的时候,面对一堆数据却不知道从哪儿下手。我见过太多人把十大算法背得滚瓜烂熟,面试也能说个大概,但一让他选型、调参、处理脏数据,立刻就卡壳。这篇内容就是冲着这个痛点来的——把人工智能领域最常被提及的十类算法拉出来,逐个讲清楚它到底在解决什么问题、核心思路是什么、代码怎么落地、实际用的时候会在哪里翻车。不管你是刚入门想建立全局认知,还是已经做过几个项目想系统梳理一遍,下面这些从实战里磨出来的细节应该都能帮上忙。我会尽量避开那种教科书式的罗列,每个算法都配上能直接跑起来的代码骨架,再补上文档里通常不会写的坑点。
1. 线性回归:最朴素却最容易被低估的起点
1.1 它到底在拟合什么
线性回归的核心假设非常直接:目标值可以表示成输入特征的加权和,再加一个偏置项。写成式子就是 y = w₁x₁ + w₂x₂ + … + wₙxₙ + b。很多人觉得这东西太简单,不屑于用,但实际项目中它往往是第一个该跑的基线模型。原因很实在——它的可解释性极强,每个特征的权重直接告诉你这个特征对结果的影响方向和大致幅度,业务方听得懂,你自己也容易判断数据里有没有明显的信号。
从几何角度看,你是在高维空间里找一个超平面,让所有样本点到这个超平面的残差平方和最小。这个“最小二乘”的目标函数是凸的,意味着有全局唯一最优解,不会陷入局部极小。这是它相比神经网络的一大优势:训练过程稳定、结果可复现。我在做特征工程验证的时候,经常先扔一个线性回归进去,看 R² 能到多少,如果连线性关系都捕捉不到,说明特征构造可能有问题,或者问题本身高度非线性,需要换思路。
代码实现上,用 scikit-learn 几行就能跑通:
from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score import numpy as np X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = LinearRegression() model.fit(X_train, y_train) pred = model.predict(X_test) print("MSE:", mean_squared_error(y_test, pred)) print("R2:", r2_score(y_test, pred)) print("系数:", model.coef_) print("截距:", model.intercept_)1.2 多重共线性这个坑几乎人人踩过
线性回归最隐蔽的问题不是过拟合,而是多重共线性。当两个或多个特征高度相关时,权重估计会变得极不稳定——数据稍微扰动一下,系数可能从正的大数跳到负的大数。表面上看模型预测效果还行,但你没法解释系数,业务方问“这个特征到底是不是正向影响”你答不上来。检测方法很简单,算方差膨胀因子(VIF),一般 VIF 超过 10 就说明共线性严重。
处理手段有几种:直接删掉相关性高的特征之一、用主成分分析降维、或者改用带正则化的岭回归。岭回归在损失函数里加了 L2 惩罚项,会把系数往零的方向压,虽然牺牲了一点无偏性,但换来了方差的大幅降低。我个人的经验是,只要特征数量超过二十个,默认先上岭回归,别用纯线性回归硬扛。
提示:标准化对线性回归不是必须的(它不影响拟合优度),但如果你要比较不同特征的重要性,或者后续要加正则项,务必先做标准化,否则惩罚项对不同量纲的特征不公平。
还有一个常被忽略的点:线性回归对异常值非常敏感,因为残差是平方的,一个极端离群点能把整条线拽偏。实操中先画残差图看看有没有明显的离群样本,必要时做缩尾处理或者用 Huber 损失替代平方损失。
2. 逻辑回归:名字带回归,干的却是分类的活
2.1 从线性输出到概率的桥梁
逻辑回归名字里有“回归”,但它解决的是分类问题,这个命名历史遗留问题坑过不少人。它的做法是在线性组合外面套一个 Sigmoid 函数,把任意实数映射到 (0,1) 区间,解释成样本属于正类的概率。Sigmoid 的形式是 σ(z) = 1 / (1 + e⁻ᶻ),这个函数有个漂亮的性质:导数可以用自身表示,σ'(z) = σ(z)(1 - σ(z)),这让梯度计算非常简洁。
为什么不用线性回归直接做分类?因为线性输出没有边界,也没法解释成概率,而且用平方损失去优化分类问题会导致梯度消失或者爆炸。逻辑回归用的是交叉熵损失,它对错误预测的惩罚是指数级的,梯度信号更强,收敛也更稳。这个选择背后有信息论的解释——交叉熵衡量的是预测分布和真实分布之间的差异,最小化它就等价于最大化似然。
from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) clf = LogisticRegression(penalty='l2', C=1.0, max_iter=1000) clf.fit(X_train_s, y_train) pred_proba = clf.predict_proba(X_test_s)[:, 1] print(classification_report(y_test, clf.predict(X_test_s))) print("AUC:", roc_auc_score(y_test, pred_proba))2.2 正则化参数 C 的调法
逻辑回归的C参数是正则化强度的倒数,C 越小正则越强。很多人直接默认 C=1.0 就跑了,但在特征维度高、样本量不大的场景下,这个默认值往往不是最优的。我的习惯是先在 [0.001, 0.01, 0.1, 1, 10, 100] 这个量级上做网格搜索,配合交叉验证看 AUC 的变化曲线。如果曲线在某个区间很平,说明模型对正则化不敏感,选中间值就行;如果曲线陡峭,说明数据对复杂度很敏感,得仔细挑。
另一个实操细节是class_weight参数。遇到类别不平衡(比如正样本只占 5%),不加权的话模型会倾向于全预测成多数类,准确率看着高但召回惨不忍睹。设成'balanced'会让损失函数按类别频率反比加权,通常能把召回拉上来。但要注意,加权之后预测概率的绝对值会偏移,如果下游需要校准过的概率,得额外做 Platt 校准或者等渗回归。
注意:逻辑回归假设特征对 log-odds 是线性的。如果某个特征和标签的关系是 U 型的,直接放进去会欠拟合。解决办法是做分箱离散化或者加多项式特征,别指望模型自己学出来。
3. 决策树:把判断逻辑摊开给你看
3.1 分裂准则背后的信息论
决策树的本质是一系列 if-else 规则的嵌套,每个内部节点选一个特征和一个阈值,把数据切成两份,直到满足停止条件。关键在于“怎么选特征和阈值”——分类树常用基尼不纯度或信息增益,回归树用方差缩减。基尼不纯度衡量的是随机抽两个样本类别不一致的概率,值越小越纯;信息增益则是分裂前后熵的减少量。
这两个准则在实际效果上差别不大,基尼计算少一个 log,速度略快,所以 scikit-learn 默认用基尼。但有个细节值得注意:信息增益对取值多的特征有偏好(比如用户 ID 这种),容易过拟合,基尼相对好一些。如果特征里有类别数很多的离散变量,要么做目标编码,要么限制树的深度。
from sklearn.tree import DecisionTreeClassifier, export_text tree = DecisionTreeClassifier( criterion='gini', max_depth=5, min_samples_leaf=20, min_samples_split=50, random_state=42 ) tree.fit(X_train, y_train) print(export_text(tree, feature_names=list(X_train.columns)))3.2 预剪枝比后剪枝更实用
决策树最大的问题是容易长成一棵“记住”了训练集的巨树,泛化能力差。剪枝分预剪枝和后剪枝,预剪枝在生长过程中就限制条件(最大深度、叶子最小样本数、分裂最小增益),后剪枝是长完了再往回砍。实操中我几乎只用预剪枝,因为后剪枝需要留验证集做代价复杂度剪枝,流程繁琐,而且预剪枝的几个参数已经够用了。
min_samples_leaf是我调得最多的参数。设太小(比如 1),树会为每个样本单独建叶子,完全过拟合;设太大,树欠拟合。经验起点是样本量的 0.5% 到 1%,然后上下浮动试。max_depth反而不用太纠结,配合min_samples_leaf一起调就行。另外,决策树对特征缩放不敏感,这是它相比 SVM、KNN 的一大便利——不用标准化,原始数据直接扔进去。
单棵决策树的预测能力通常不如集成模型,但它的价值在于可解释性。把树的结构打印出来,业务方能看到具体的判断路径,这在风控、医疗等需要解释决策的场景里非常重要。我经常用单棵树做初步的特征筛选,看哪些特征被高频使用,再把这些特征喂给更强的模型。
4. 随机森林:把多棵树的意见综合起来
4.1 双重随机性带来的稳定性
随机森林的思路很直白:训练很多棵决策树,每棵树用不同的数据子集和特征子集,最后投票或取平均。这里的“双重随机”是关键——行采样(bootstrap 有放回抽样)让每棵树看到不同的样本,列采样(每次分裂只考虑随机选的一部分特征)让树之间产生差异。树之间的相关性越低,集成的方差缩减效果越好。
为什么 bagging 能降低方差?假设有 n 棵独立同分布的树,每棵方差是 σ²,平均之后的方差是 σ²/n。但树之间不可能完全独立(因为它们看的是同一批数据的不同子集),所以实际方差介于 σ²/n 和 σ² 之间。列采样就是用来进一步降低相关性的,这也是随机森林比单纯 bagging 效果好的原因。
from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=300, max_features='sqrt', min_samples_leaf=5, n_jobs=-1, random_state=42, oob_score=True ) rf.fit(X_train, y_train) print("OOB Score:", rf.oob_score_) print("特征重要性:", dict(zip(X_train.columns, rf.feature_importances_)))4.2 袋外误差和特征重要性的正确读法
随机森林有个白送的好处:每棵树训练时约有 36.8% 的样本没被抽到((1-1/n)^n 当 n 趋于无穷时的极限是 1/e ≈ 0.368),这些袋外样本可以当验证集用,不用额外切分就能评估泛化性能。oob_score=True打开后直接能读,这在数据量紧张的时候特别有用。但要注意,如果开了max_features很小或者树很深,OOB 估计可能偏乐观,别完全信它。
特征重要性有两种算法:基于不纯度减少的和基于排列的。前者算得快但有偏差,对取值多的连续特征和高基数类别特征会高估重要性;后者更可靠但计算量大。我的做法是先用不纯度重要性粗筛,把明显不重要的特征去掉,再用排列重要性在精简后的特征集上复核。如果两种方法给出的排序差异很大,说明数据里有共线性或者特征交互,得进一步排查。
提示:随机森林的
n_estimators不是越大越好,超过一定数量后性能提升边际递减,但训练时间线性增长。一般 200 到 500 之间就够,配合n_jobs=-1并行训练,速度可以接受。
随机森林的另一个实用场景是缺失值填充。它自带对缺失值的处理能力(虽然 scikit-learn 的实现不支持原生缺失,但可以先用中位数粗填,再用随机森林预测缺失值做迭代填充),比简单均值填充效果好不少。
5. 梯度提升树:串行纠错的集成哲学
5.1 残差学习的直观理解
梯度提升树(GBDT)和随机森林的思路完全相反:随机森林是并行训练很多独立树再平均,GBDT 是串行训练,每棵新树专门去拟合前面所有树留下的残差。打个比方,随机森林像一群专家各自独立判断然后投票,GBDT 像一个学生每次考试后专门复习错题,下一棵树就是针对上一轮的“错题”训练的。
数学上,GBDT 在函数空间里做梯度下降。每一轮计算损失函数对当前预测值的负梯度(对于平方损失就是残差),然后训练一棵树去拟合这个负梯度,再把树的输出乘以学习率加到预测值上。学习率(shrinkage)控制每棵树的贡献,设小一点(比如 0.05 到 0.1)能让模型更稳,但需要更多树来收敛。
from sklearn.ensemble import GradientBoostingClassifier gbdt = GradientBoostingClassifier( n_estimators=200, learning_rate=0.05, max_depth=3, subsample=0.8, random_state=42 ) gbdt.fit(X_train, y_train)5.2 学习率和树数量的跷跷板关系
GBDT 调参最核心的一对参数就是learning_rate和n_estimators,它们是一对跷跷板:学习率小,需要更多树;学习率大,树可以少但容易过拟合。我的经验是先固定一个较小的学习率(0.05 左右),把树的数量调到验证集误差不再下降为止,然后再微调学习率看能不能用更少的树达到同样效果。subsample小于 1 引入行采样,相当于随机梯度下降的效果,能提升泛化并加速训练,一般设 0.7 到 0.9。
max_depth对 GBDT 的影响和随机森林不同。随机森林里树可以很深(因为平均能抵消过拟合),但 GBDT 里每棵树是串行叠加的,深树会让单棵树的拟合能力过强,整体容易过拟合。通常max_depth设 3 到 5 就够了,配合较小的学习率,效果比深树加大的学习率好。
注意:GBDT 对异常值比随机森林敏感,因为残差会被异常值放大,后续的树会拼命去拟合这些异常点。如果数据里离群值多,先做处理或者改用 Huber 损失之类的鲁棒损失函数。
6. 支持向量机:用核函数把低维难题搬到高维
6.1 最大间隔的几何直觉
SVM 的目标是找一个超平面,不仅能把两类分开,还要让离超平面最近的样本点(支持向量)到超平面的距离最大。这个“最大间隔”的约束让 SVM 对噪声有较好的容忍度——决策边界只由支持向量决定,其他样本点挪一挪不影响结果。数学上这是个带约束的凸二次规划问题,用拉格朗日乘子法转成对偶形式后,求解只涉及样本之间的内积。
核技巧是 SVM 的精髓。当数据在原始空间线性不可分时,用一个核函数隐式地把样本映射到高维空间,在高维空间里找线性超平面。常用的核有线性核、多项式核、RBF 核。RBF 核(高斯核)最常用,它把每个样本映射到无穷维空间,理论上能拟合任意复杂的边界,但参数gamma控制单个样本的影响范围,设大了容易过拟合,设小了欠拟合。
from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) svm = SVC(kernel='rbf', C=1.0, gamma='scale', probability=True) svm.fit(X_train_s, y_train)6.2 标准化和参数搜索是绕不开的
SVM 对特征尺度极其敏感,因为间隔的计算依赖欧氏距离,量纲大的特征会主导距离。所以用 SVM 之前必须标准化,这是硬性要求,不是可选项。我见过有人忘了标准化,结果模型效果还不如逻辑回归,排查半天才发现是这个问题。
C和gamma的联合搜索是 SVM 调参的标准动作。C是惩罚系数,越大越不允许误分类,容易过拟合;gamma是 RBF 核的宽度参数,越大决策边界越弯曲。用网格搜索在 log 尺度上扫,比如 C 取 [0.1, 1, 10, 100],gamma 取 [0.001, 0.01, 0.1, 1],配合交叉验证。数据量超过几万条时 SVM 训练会很慢(复杂度大致是 O(n²) 到 O(n³)),这时候要么换线性核加更多特征,要么直接上树模型。
SVM 还有个probability=True的坑:打开后会用内部交叉验证做概率校准,训练时间显著增加,而且校准后的概率不一定比直接输出决策值更可靠。如果下游只需要类别标签,别开这个选项。
7. K 近邻:最简单的思路,最麻烦的距离度量
7.1 懒惰学习的代价
KNN 是“懒惰学习”的代表——训练阶段什么都不做,只是把数据存起来,预测时才去算待预测样本和所有训练样本的距离,找最近的 K 个邻居投票。这种设计的好处是概念极其简单,没有显式的训练过程,新增数据直接加进去就行。但代价是预测慢,每次预测都要遍历整个训练集,数据量大时延迟很高。
KNN 的效果高度依赖距离度量和特征缩放。默认用欧氏距离,但如果特征量纲不同,距离会被大量纲特征主导,所以标准化是必须的。另外,高维空间里欧氏距离会失效——所有样本对之间的距离趋于相等,这叫“维度灾难”。特征维度超过几十维时,KNN 通常表现很差,得先降维或者做特征选择。
from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) knn = KNeighborsClassifier(n_neighbors=5, weights='distance', metric='euclidean') knn.fit(X_train_s, y_train)7.2 K 值选择和加权策略
K 的选择是个偏差-方差权衡:K 小,模型复杂,对噪声敏感,方差大;K 大,决策边界平滑,偏差大。一般从 3 到 15 之间试,用交叉验证选。有个经验法则:K 取训练样本量的平方根附近,但这只是起点,实际还得看数据分布。二分类问题里 K 最好取奇数,避免平票。
weights='distance'让近的邻居投票权重更大,通常比等权投票效果好,尤其是在类别边界附近。但要注意,如果数据里有噪声点,距离加权会放大噪声的影响。另一种改进是限制邻居的搜索范围(用 KD 树或球树加速),scikit-learn 的algorithm参数可以设成'kd_tree'或'ball_tree',数据维度低时能显著加速预测。
提示:KNN 对不平衡数据很敏感,多数类会在邻居投票中占优。解决办法是对多数类下采样,或者用距离加权并调大 K,让少数类的近邻有更多话语权。
8. 朴素贝叶斯:假设很强,但常常够用
8.1 条件独立假设的威力与局限
朴素贝叶斯的核心假设是:给定类别的前提下,各个特征相互独立。这个假设在现实中几乎从来不成立(比如文本分类里,“机器”和“学习”两个词明显相关),但奇妙的是,即使假设不成立,朴素贝叶斯在很多任务上仍然表现不错,尤其是文本分类。原因在于分类任务只关心哪个类别的后验概率最大,不需要概率估计绝对准确,只要排序对就行。
根据特征分布的不同,朴素贝叶斯有几个变体:高斯朴素贝叶斯假设特征服从正态分布,适合连续特征;多项式朴素贝叶斯适合计数特征(如词频);伯努利朴素贝叶斯适合二值特征。文本分类里最常用的是多项式版本,配合 TF-IDF 特征效果稳定。
from sklearn.naive_bayes import MultinomialNB from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X_train_vec = vectorizer.fit_transform(train_texts) X_test_vec = vectorizer.transform(test_texts) nb = MultinomialNB(alpha=1.0) nb.fit(X_train_vec, train_labels)8.2 平滑参数 alpha 的作用
alpha是拉普拉斯平滑参数,用来处理训练集里没出现过的特征值。如果不平滑,某个特征在某个类别下没出现过,条件概率就是零,连乘之后整个后验概率归零,这叫“零概率问题”。alpha=1是标准拉普拉斯平滑,alpha<1是 Lidstone 平滑。文本分类里alpha通常设 0.1 到 1 之间,数据量大时影响不大,数据量小的时候调一调能提升效果。
朴素贝叶斯的优势是训练极快、对小数据友好、不怕高维特征(特征维度远大于样本量也能跑)。缺点是特征间强相关时概率估计不准,而且它输出的是相对概率,不是校准过的绝对概率。如果下游需要可靠的概率值,得做额外的校准。我在做文本分类基线的时候,朴素贝叶斯永远是第一个跑的,几秒钟出结果,给后续复杂模型提供一个参照。
9. K 均值聚类:无监督里最常用的那把锤子
9.1 迭代优化的收敛逻辑
K 均值是最经典的划分聚类算法,目标是把 n 个样本分成 K 个簇,使得簇内样本到簇中心的距离平方和最小。它的求解是迭代式的:先随机选 K 个初始中心,然后把每个样本分配给最近的中心,再根据分配结果重新计算中心,重复直到中心不再移动或达到最大迭代次数。这个交替优化的过程保证目标函数单调下降,最终收敛到局部最优。
K 均值快、简单、可解释,但有几个硬伤:需要预先指定 K、对初始中心敏感、只能发现球形簇、对异常值敏感。初始中心的选择影响很大,糟糕的初始化可能收敛到很差的局部最优。scikit-learn 默认用 k-means++ 初始化,它让初始中心尽量分散,比纯随机好很多。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score kmeans = KMeans(n_clusters=5, init='k-means++', n_init=10, random_state=42) labels = kmeans.fit_predict(X_scaled) print("轮廓系数:", silhouette_score(X_scaled, labels)) print("簇中心:", kmeans.cluster_centers_)9.2 K 的确定和结果评估
确定 K 是 K 均值最头疼的问题。肘部法是最常用的:画 K 从 1 到 10 的簇内平方和(inertia)曲线,找拐点。但拐点经常不明显,这时候配合轮廓系数(silhouette score)一起看,轮廓系数越接近 1 说明簇内紧凑、簇间分离好。业务约束也是重要参考——如果聚类结果要对应实际的业务分群,K 可能由业务方直接给定。
n_init参数控制用不同初始化跑几次取最好的,默认 10 次,数据量大时可以适当减少。K 均值假设簇是凸的、各向同性的,如果数据是长条形或者密度差异大,它会把一个自然簇切成两半。这种时候考虑 DBSCAN 或者高斯混合模型。另外,K 均值对特征尺度敏感,聚类前必须标准化,否则量纲大的特征会主导距离计算。
注意:K 均值的结果受随机种子影响,虽然 k-means++ 降低了这种敏感性,但不同种子跑出来的簇标签可能不同(簇的编号会变),做结果对比时要用调整兰德指数之类的指标,别直接比标签。
10. 主成分分析:降维是为了看得更清
10.1 方差最大化的投影方向
PCA 的目标是找一组正交的投影方向,让数据投影到这些方向后方差最大。第一个主成分是方差最大的方向,第二个主成分在与第一个正交的前提下方差最大,依此类推。数学上,主成分就是协方差矩阵的特征向量,对应的特征值就是该方向上的方差。做降维时保留前 k 个主成分,就相当于把数据投影到方差最大的 k 维子空间。
PCA 的前提是数据已经中心化(每个特征减去均值),否则第一主成分会被均值方向带偏。标准化也是常见操作,尤其是特征量纲不同的时候——不标准化的话,方差大的特征会主导主成分,降维结果反映的是量纲差异而不是数据结构。
from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X) pca = PCA(n_components=0.95) X_pca = pca.fit_transform(X_scaled) print("保留主成分数:", pca.n_components_) print("累计解释方差比:", pca.explained_variance_ratio_.cumsum())10.2 解释方差比和重构误差
n_components设成小于 1 的浮点数时,表示保留多少比例的方差,比如 0.95 表示保留 95% 的方差。这个设法的好处是不用手动试 k,但要注意,保留 95% 方差不一定对应最优的下游任务性能——有时候丢掉的那 5% 方差里恰好包含了对分类有用的信号。我的做法是先按 0.95 或 0.99 降维,跑一遍下游模型,再对比不降维的效果,如果降维后效果下降明显,说明丢掉的成分里有信息。
PCA 的可解释性是个问题。主成分是原始特征的线性组合,系数可正可负,很难直接解释成业务含义。如果降维后需要向业务方解释每个维度的意义,PCA 不太合适,得考虑用特征选择或者因子分析。另外,PCA 是线性方法,如果数据流形是非线性的(比如瑞士卷数据集),PCA 会失效,这时候用 t-SNE 或 UMAP 做可视化可以,但它们不适合做特征预处理喂给下游模型。
提示:PCA 对异常值敏感,因为方差计算会被极端值放大。做 PCA 之前先检查离群点,必要时做缩尾或者用鲁棒 PCA 变体。
11. 算法选型的实战判断框架
11.1 先看数据形态再看任务类型
十个算法摆在这儿,实际项目里怎么选?我的判断顺序是:先看有没有标签,有标签是监督学习,没标签是无监督。监督里再看目标是连续值还是离散类别,连续值走回归(线性回归、回归树、GBDT 回归),离散类别走分类(逻辑回归、SVM、树模型、朴素贝叶斯)。无监督里看目标是分组还是降维,分组用聚类(K 均值、DBSCAN),降维用 PCA 或流形学习。
数据量是个硬约束。样本量几千条以下,逻辑回归、SVM、朴素贝叶斯这些"轻量"模型往往比深度学习更靠谱,因为复杂模型在小数据上容易过拟合。样本量几万到几十万,随机森林和 GBDT 是主力。特征维度极高(比如文本的稀疏特征),线性模型加正则化(逻辑回归、线性 SVM)通常比树模型更合适,因为树模型在高维稀疏数据上分裂效率低。
| 场景特征 | 优先尝试 | 备选 | 慎用 |
|---|---|---|---|
| 小样本、需要可解释 | 逻辑回归、单棵决策树 | 朴素贝叶斯 | 深度学习 |
| 中等样本、追求精度 | GBDT、随机森林 | SVM(RBF 核) | KNN |
| 高维稀疏(文本) | 逻辑回归 + L2、朴素贝叶斯 | 线性 SVM | 树模型 |
| 无标签分群 | K 均值 | 高斯混合、DBSCAN | PCA(单独用) |
| 特征降维可视化 | PCA | t-SNE、UMAP | 线性回归 |
11.2 基线模型和迭代路径
我习惯的上手路径是:先跑一个最简单的基线(分类用逻辑回归,回归用线性回归),记录指标;然后上树模型(随机森林或 GBDT)看能提升多少;如果树模型明显更好,说明数据里有非线性关系或特征交互,再针对性地做特征工程;如果树模型和线性模型差不多,说明信号主要是线性的,没必要上复杂模型,把精力放在特征上更划算。
这个路径的好处是每一步都有对照,不会一上来就调复杂模型然后不知道瓶颈在哪。另外,别忘了留一个"傻瓜基线"——比如分类问题全预测成多数类,回归问题全预测成均值。如果复杂模型连这个基线都打不过,说明数据或者流程有问题,先排查再说。
提示:模型选型不是一锤子买卖,上线后要持续监控数据分布漂移和模型性能衰减。树模型对分布漂移相对鲁棒,线性模型在特征分布变化时系数解释会失效,得定期重训。
12. 那些文档里不会写的踩坑记录
12.1 数据泄漏:最隐蔽也最致命
数据泄漏是指训练时用到了预测时拿不到的信息,导致离线指标虚高、上线后崩盘。最常见的泄漏场景是特征里包含了未来信息(比如用当天的收盘价预测当天涨跌),或者预处理在划分训练测试集之前就做了(比如先对全量数据标准化再切分,测试集的均值方差泄漏到了训练过程)。正确做法是先切分,再在训练集上 fit 预处理器,然后 transform 测试集。
另一个隐蔽的泄漏是目标编码。做类别特征的目标编码时,如果用全量数据算类别的目标均值,测试集的标签信息就泄漏了。正确做法是在训练集内部做交叉验证式的目标编码,或者加噪声和平滑。时间序列问题里,划分不能用随机切分,必须按时间切,否则未来信息会泄漏到训练集。
12.2 评估指标选错,努力全白费
分类问题默认看准确率,但类别不平衡时准确率会骗人——99% 的负样本,全预测成负类也有 99% 准确率。这种场景要看精确率、召回率、F1 或者 AUC。具体选哪个取决于业务代价:漏检代价高(比如故障检测)优先召回,误报代价高(比如营销推送)优先精确率。回归问题默认看 MSE,但如果异常值多,MAE 更稳健;如果大误差代价特别高,用 MSE 或 Huber。
交叉验证的切分方式也有讲究。普通随机 K 折在样本独立时没问题,但如果样本有分组结构(比如同一个用户的多条记录),必须用 GroupKFold,否则同一用户的数据同时出现在训练和验证集里,指标会虚高。时间序列用 TimeSeriesSplit,保证验证集永远在训练集之后。
12.3 超参数搜索的效率和陷阱
网格搜索在小参数空间里很稳,但参数一多就组合爆炸。随机搜索在同样预算下通常能找到更好的解,因为不是所有参数都同等重要。贝叶斯优化更省评估次数,但实现复杂一些。我的经验是:参数少于 4 个用网格,多于 4 个用随机,评估一次很贵(比如深度学习)才上贝叶斯优化。
搜索空间的范围设定比搜索方法更重要。很多人把学习率设成 [0.001, 0.01, 0.1, 1],但实际最优值可能在 0.03 这种中间位置,网格扫不到。正确做法是在 log 尺度上均匀取点,或者先粗扫确定量级再细扫。另外,交叉验证的折数不是越多越好,5 折是性价比最高的选择,10 折提升有限但计算量翻倍。
注意:调参时别在测试集上反复试,测试集只能用一次。调参用验证集或交叉验证,最终模型确定后再在测试集上评估一次,这个数字才是可信的泛化估计。
13. 写在最后的几句实在话
这十个算法没有哪个是"最好"的,只有合不合适。我见过有人拿着 GBDT 硬套小样本数据,过拟合到没法看;也见过有人用逻辑回归加精心构造的特征,在业务指标上吊打复杂模型。算法是工具,对数据的理解、对业务目标的拆解、对评估方式的把握,这些才是决定项目成败的东西。
代码实现上,scikit-learn 的 API 设计高度一致,fit、predict、transform 三个方法走天下,学会一个就基本会了全部。但别停留在调包层面,至少把这十个算法的损失函数和优化过程搞明白,遇到效果不对的时候才知道往哪个方向排查。调参之前先看数据,特征分布、缺失情况、异常值、类别平衡,这些基础工作做到位,比任何高级技巧都管用。
最后分享一个我自己的习惯:每接一个新问题,先花半天时间做探索性分析,画特征分布图、算相关性、看标签分布,然后用最简单的模型跑一个基线。这个基线不一定是最好的,但它给了我一个锚点,后面所有复杂模型的提升都能量化,也能防止我在错误的路上越走越远。算法清单可以背,但判断力只能靠一个个项目磨出来。