ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

机器学习四大基础分类算法:KNN、朴素贝叶斯、决策树与随机森林实战指南

机器学习四大基础分类算法:KNN、朴素贝叶斯、决策树与随机森林实战指南 周末两天把机器学习经典入门算法过了一遍第二天正好卡在四个基础又核心的模型上KNN、朴素贝叶斯、决策树以及由多棵树组成的决策森林和随机森林。这四个算法几乎是所有分类问题的起手式后面再玩什么提升树、堆叠模型很多核心思想都是从它们身上长出来的。如果你正在自学机器学习或者已经会调包但总觉得原理糊了一层纸这篇笔记应该能帮你把那层窗户纸捅破。内容全程来自我自己的复现代码和踩坑记录没有晦涩推导只有怎么用、为什么这么用、坑在哪里。先说我那天的场景手头有一个模拟的用户流失预测Demo特征有数值型使用时长、消费金额有离散型渠道来源、套餐类型样本量大概两万。我要求自己用这四种算法分别去跑一遍分类任务对比精度、训练时间、可解释性。这一跑四个算法的脾气基本就摸清了。这篇文章就按“从直观到集成”的顺序来写每一步都带执行动作和避坑经验新手可以直接按着思路来一遍。1. 四种算法先混个脸熟1.1 它们各自解决什么问题凡是拿到一张带标签的数据表你第一反应就是找一个分类器。KNN是“看邻居”朴素贝叶斯是“算概率”决策树是“问问题”随机森林是“让一群决策树投票”。四种做法背后的逻辑完全不同但都解决同一类问题给定样本的特征预测它的标签是A还是B或者多分类。KNN的思路最简单粗暴一个样本的类别由离它最近的K个样本投票决定。它没有任何训练过程属于“惰性学习”模型其实就是那堆训练数据本身。朴素贝叶斯则是直接算后验概率在已知特征取值的情况下样本属于某个类别的概率有多大。它核心依赖贝叶斯公式再加一个非常强的假设——特征之间条件独立。这个假设在现实里几乎不成立但它往往还能给出不错的结果属于“人傻但运气好”的类型。决策树则是一连串if-else规则构成的树状结构每一步问一个问题比如“使用时长大于30天吗”不断把数据分割成越来越纯的子集。回答路径就是预测结果天然具有可解释性。决策森林就是把多棵树组在一起通常指用Bagging或类似思路训练出的树集合。随机森林就是决策森林里最出名的一个变种用有放回抽样训练很多棵决策树每棵树在分裂时只随机看一部分特征最后让所有树投票得到结果。它把“单棵易过拟合的树”变成了“一群不容易过拟合的树”。1.2 一张表看懂适用场景表里这几项是我实践中比较看重的维度维度KNN朴素贝叶斯决策树随机森林训练速度无训练过程很快快快预测速度慢要算全部距离极快快快高维稀疏文本很吃力特别适合比较吃力尚可特征数值范围敏感非常敏感几乎不敏感不敏感不敏感可解释性差一般很好一般是否需特征缩放需要不需要不需要不需要小样本表现不错很不错容易过拟合容易欠拟合常见硬伤维度灾难独立性假设切分规则太机械超参数多选型口诀我自己的版本是文本分类先上朴素贝叶斯样本少、维度低、数据干净试试KNN要跟业务方解释逻辑时用单棵决策树要精度、又不想太折腾时直接用随机森林。没说到的场景随机森林是你能做出的最稳妥默认选择之一。2. KNN把离你最近的朋友的话当真2.1 核心思想与距离度量KNN的全称是K-Nearest Neighbors直译就是K个最近的邻居。假设你有一个未知类别的样本算法会在全量训练数据里找离它最近的K个样本这K个样本里哪个类别多预测结果就是哪个。若K3找最近3个点两个说A类、一个说B类那就判为A类。“离它最近”怎么定义这就引出了距离度量。最常用的是欧氏距离也就是几何空间里的直线距离import numpy as np def euclidean_distance(a, b): return np.sqrt(np.sum((np.array(a) - np.array(b)) ** 2))实践中还有个常用选项是曼哈顿距离绝对值之和它更适合特征维度高但稀疏的情况但说实话默认用欧氏距离已经能覆盖绝大多数场景。文本向量这类稀疏场景则常用余弦相似度。距离度量选错会带来非常微妙的影响。欧氏距离对异常值特别敏感某个特征出现一个离谱的极值距离直接被它主导。所以做KNN之前特征工程比算法本身更重要。2.2 K值要命特征缩放更要命K值的选择是整个KNN里最核心的超参数。K太小模型只看极少几个点对噪声极其敏感K太大把离得很远的样本也拉进来边界被抹得一塌糊涂。我那时图省事直接取了K1做了个Demo准确率看起来还行但一画混淆矩阵就发现问题预测结果在边界上东一块西一块完全跟着个别异常点跑。换成交叉验证后K从1调到11模型才稳定下来。做法很简单把训练集切出验证集或者直接用交叉验证遍历K1到K30看哪个K的验证精度最高。别偷懒拍脑袋定K。特征缩放是我那天真正踩的第一个坑。当时数据里有“使用时长”和“月消费金额”两个特征前者在0到60之间后者动辄几百上千。KNN是基于距离计算的量纲大的特征直接把量纲小的特征给淹没了算出来所谓的“最近邻居”全是消费金额相近的使用时长几乎不参与决策。标准化之后两个特征才真正平等地被考虑。所以但凡用基于距离的算法第一件事就是做标准化或者归一化。from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import cross_val_score scaler StandardScaler() X_scaled scaler.fit_transform(X) k_values range(1, 31) scores [] for k in k_values: knn KNeighborsClassifier(n_neighborsk) score cross_val_score(knn, X_scaled, y, cv5).mean() scores.append(score) best_k k_values[int(np.argmax(scores))] print(f最优K值: {best_k}, 交叉验证精度: {max(scores):.4f})2.3 用KNN跑通一次小分类Demo拿我手头那个流失预测DataFrame来演示。特征做标准化后先划分训练测试集from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42, stratifyy ) knn KNeighborsClassifier(n_neighborsbest_k) knn.fit(X_train, y_train) print(训练集精度:, knn.score(X_train, y_train)) print(测试集精度:, knn.score(X_test, y_test))注意一点标准化是在全部训练数据上fit的然后再transform测试集。千万不能在训练和测试上分别做标准化那样会让测试集信息泄露到训练流程里。我见过好多新手在这里出错最后验证集精度看起来虚高上线就崩。2.4 KNN的坑与补救KNN最大的坑是预测慢。训练什么都不干一预测就要遍历全部训练样本算距离。样本量一上万预测过程肉眼可见地变慢。解决办法要么用KDTree或Ball Tree结构加速要么直接换模型。sklearn里algorithmkd_tree可以开但高维数据下KDTree优势会消失。第二个坑是样本不平衡。如果A类占90%B类占10%KNN天然更倾向把边界样本判成A类。补救方式有采用加权投票距离越近权重越高、对少数类样本过采样比如SMOTE、或者调整分类阈值。我自己实践时加权投票是成本最低的改进sklearn里weightsdistance就是干这个的。3. 朴素贝叶斯不查资料就敢下判断的概率党3.1 贝叶斯公式与那个“朴素”的假设朴素贝叶斯的出发点非常聪明与其直接判断“这个样本是什么类别”不如反过来问“如果是这个类别它长这样的概率有多大”再结合每个类别本身出现的概率算出后验概率。公式是P(类别|特征) P(特征|类别) × P(类别) / P(特征)做分类时分母P(特征)对所有类别都一样所以只需比较分子大小P(特征|类别)乘以P(类别)。问题来了一个样本往往有几十个特征联合概率P(特征1, 特征2, ..., 特征n|类别)没法直接从有限样本里估计出来因为特征组合数量会爆炸。“朴素”的假设在这里登场假设所有特征在给定类别条件下相互独立。这样一来联合概率就变成了各个特征条件概率的乘积P(特征|类别) ≈ P(特征1|类别) × P(特征2|类别) × ... × P(特征n|类别)生活化理解就是判断一封邮件是不是垃圾邮件把“出现了‘打折’”、“出现了‘发票’”、“出现‘点击链接’”这几个词各自与垃圾邮件的相关度相乘乘出来的数越高越可能是垃圾邮件。虽然词跟词之间明明有关联但这个假设保证了计算可行。3.2 拉普拉斯平滑防止零概率翻车假设某个词在训练集里从未出现在垃圾邮件中那么P(这个词|垃圾邮件)直接等于0。乘积里有一个0整个概率就变成0导致后验概率为0。这个结果显然不合理没见过的词不代表永远不出现。解决办法是拉普拉斯平滑本质是给每个特征取值加一点“保底计数”。公式中的P(特征|类别)变成P (该类别下该特征出现次数 α) / (该类别下总特征出现次数 α × 特征取值总数)α通常取1称为拉普拉斯平滑。这个技巧在文本分类里尤其重要因为词汇量大没见过的组合实在太多了。这也是为什么用朴素贝叶斯做文本分类前一定要保证平滑参数被正确设置。3.3 连续特征怎么处理朴素贝叶斯原始形态处理的是离散特征词语出现或不出。但流失预测里的“消费金额”是连续值怎么处理常见做法有两种。一种是把连续值离散化比如按分位数切分成“低、中、高”三段再当离散特征统计频次。这种方式的缺点是对切分边界很敏感信息损失也大。另一种更优雅假设连续特征在某个类别下服从某个概率分布通常是正态分布然后用数据估计这个分布的参数均值和方差预测时把特征值代入概率密度函数算出“概率”。这个变体叫高斯朴素贝叶斯是sklearn里处理连续特征最常用的选择。如果你发现特征分布明显偏态可以试试对数变换后再拟合。from sklearn.naive_bayes import GaussianNB gnb GaussianNB() gnb.fit(X_train, y_train) print(测试集精度:, gnb.score(X_test, y_test))3.4 用一个文本分类小例子验证威力为了验证朴素贝叶斯在文本上有多能打我额外构造了一个模拟的短文本分类任务给一组评论句子打好评差评标签。用CountVectorizer把句子变成词频向量然后直接用多项式朴素贝叶斯from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB corpus [这个产品真好用, 太差了退货, 服务态度很不错, 垃圾再也不买了, ...] labels [1, 0, 1, 0, ...] vectorizer CountVectorizer() X_vec vectorizer.fit_transform(corpus) mnb MultinomialNB(alpha1.0) mnb.fit(X_vec, labels) sample [东西还行吧] sample_vec vectorizer.transform(sample) print(预测类别:, mnb.predict(sample_vec)[0])这个例子我只用了几十条样本分类效果已经像模像样。朴素贝叶斯在小样本、高维稀疏场景下的性价比是神经网络都比不了的。3.5 边界与硬伤朴素贝叶斯的硬伤就是独立性假设。真实数据里特征几乎总有关联在流失预测里消费金额高的用户往往使用时长也长。当特征强相关时独立假设会把概率算得过分乐观最终分类边界比贝叶斯最优边界要差。第二个问题它适合用概率阈值做决策的场景但输出的概率值本身不准。如果你想用预测概率做风险排序最好先做概率校准用CalibratedClassifierCV包一层。我在某次风险名单排序中试过校准前后的排名差异非常明显直接影响到后续人工审核的工作量。4. 决策树与决策森林从一堆if-else到一群if-else4.1 决策树怎么选特征决策树的训练过程就是不断挑选特征和切分阈值的过程。标准是每次切分之后子节点的数据要比父节点“更纯”也就是尽量让同一类的样本落在一起。衡量“纯”的指标常见有两个信息熵和基尼系数。信息熵衡量不确定性熵越高越混乱。切分前后的熵差叫信息增益选信息增益最大的特征来切分。这就是ID3算法的思路。C4.5换成信息增益率避免倾向取值多的特征。CART则用基尼系数公式为1减去各类别样本占比平方之和每次选基尼系数下降最大的切分方式。ETL现场实操里我一般不会自己算这些sklearn的DecisionTreeClassifier直接包好了。但理解切分逻辑很重要决策树本质上在特征空间里画矩形边界所以它对线性可分问题的表达本身就吃亏它擅长的是捕捉高维特征的交互关系if A and B then C这类结构。4.2 剪枝防止树长疯了单棵决策树不设限制时会一路生长到每个叶子节点都纯为止。训练集精度可以做到100%但测试集惨不忍睹这就是过拟合。树的深度越深边界越碎噪声被刻进去得越彻底。控制过拟合的方法是剪枝分两种预剪枝在生长过程中就限制深度、最小样本量后剪枝是先让树长满再自底向上评估合并叶子节点是否让验证集精度提升能提升就合并。sklearn里默认不给后剪枝但我可以手动设置多项预剪枝参数from sklearn.tree import DecisionTreeClassifier dt DecisionTreeClassifier( max_depth5, min_samples_split20, min_samples_leaf10, criteriongini ) dt.fit(X_train, y_train)这里面min_samples_leaf是我最常用的控制项。只要叶子样本量够大边界就稳。深度限制只是兜底实际调参时我会优先调叶子节点最少样本数。4.3 什么是决策森林“决策森林”在正规教材里不是一个标准算法名称更多是一种口语化的说法指把多棵决策树凑在一起使用。为什么要凑一起因为单棵树过拟合太严重而且方差大训练集换一批样本树结构可能完全不一样。如果同时训练多棵存在差异的树让它们投票方差就能被平均掉。决策森林是通向随机森林的过渡概念。袋装思路Bagging把训练数据做有放回抽样得到多份不同的训练子集每份子集训练一棵树。抽样带来的随机性让每棵树长得不一样最终投票决策。不过有一个问题如果某些特征特别强几棵树在分裂时都优先用这个特征树和树之间仍然很像差异不够大。4.4 单树与森林的直观对比我在模拟项目X上做了个实验同一份数据单棵深度为10的决策树测试精度大约0.76训练精度0.99明显过拟合。换一个简单的带100棵树的决策森林每棵树用不同的抽样子集测试精度升到0.82训练精度反而降到0.91。这就是典型的“以微小偏差换巨大方差下降”。如果你需要向业务方解释模型逻辑单棵浅树依然有不可替代的地位。深度为3或4的树可以画成规则表每个叶子给出一条“如果...那么...”的规则这对风控场景特别友好。而森林模型更像一个黑箱失去了可解释性。5. 随机森林一群模型打群架果然更猛5.1 Bagging与特征随机让每棵树不一样随机森林是集成的集大成者。它凭借两个机制保证每棵树之间足够有差异。第一个机制是Bagging对原始训练集进行有放回抽样每次抽与训练集同样大小的样本子集每棵树只在自己的子集上训练。大概会有约36.8%原始样本没被抽到这部分样本称为袋外样本OOB可以直接用来做测试误差的估计省了单独划分验证集。第二个机制是特征随机化每棵树在每次分裂时并不是从全部特征里挑最优而是先随机选一部分特征默认是特征总数的开方再从这部分里选最优切分。这样进一步增大了树之间的差异性。两个机制加起来随机森林里的树既“数据不同”又“视角不同”投票结果稳定得多。这个设计里的直觉我特别喜欢单棵决策树容易偏见和钻牛角尖但一群出发点不同的树做决策任何一棵树的毛病都会被其他树的判断给稀释掉。5.2 三个关键参数怎么调随机森林调参不复杂关键是抓住主要矛盾。n_estimators树的数量直接决定模型稳定性和训练成本。树太少方差没有完全压平树太多训练时间线性增长但精度提升趋于边际递减。我的经验是先用500棵试观察精度随树数增长的收敛曲线。如果200棵就到瓶颈后面再加树基本无意义。max_features每次分裂随机抽取的特征数控制单棵树的能力。默认值是特征数的平方根分类问题一般够用。特征很多比如上百维时可以往大调一点让每棵树更强一些。min_samples_leaf叶子最小样本数依然是我最优先调的正则化参数。设得太小比如1单棵树过拟合设得太大树太粗糙模型整体会欠拟合。我在两万样本的流失预测里设成20左右效果还不错。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_featuressqrt, min_samples_leaf20, max_depth15, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) print(OOB精度:, rf.oob_score_) print(测试集精度:, rf.score(X_test, y_test))注意OOB精度是随机森林特有的一项免费评估指标不需要额外留验证集就能估出泛化能力。训练过程中它已经被内部算出来了我每次都会先看它再决定要不要继续调参。5.3 用随机森林做特征重要性分析随机森林一个很值钱的能力是输出特征重要性。原理是某特征在树分裂中造成的基尼系数下降总和越大或对预测精度贡献越大它就越重要。这个结果可以直接用于特征筛选帮我把两万样本里的三十多个特征砍到只剩十来个核心特征建模速度大幅提升精度几乎不掉。import pandas as pd importance rf.feature_importances_ feature_names X.columns imp_df pd.DataFrame({特征: feature_names, 重要性: importance}) imp_df imp_df.sort_values(重要性, ascendingFalse) print(imp_df.head(10))用特征重要性时注意一个陷阱当两个特征高度相关时重要性会在它们之间被分散导致结果虚低。做筛选的时候最好先看一眼相关性矩阵把高相关的特征先合并或删掉一边再做重要性排序才靠谱。5.4 随机森林的克制与边界随机森林不是万能药。一来它对噪声特征多的数据依然会受影响虽然比单棵树抗噪但噪声比例很高时依然被带偏。二来它不适合高维稀疏数据文本词频这种上万维的输入随机森林效果远不如朴素贝叶斯或线性模型。三来它预测速度比单棵树慢每个样本要走全部树再合并结果实时性要求高的场景要谨慎。还有一点随机森林输出概率的校准程度也一般。如果需要精确概率而不只是类别标签依然需要概率校准。6. 实战踩坑记录与经验速查6.1 经典错误与排查思路数据泄漏导致虚高精度。我某次把标准化fit过整个数据集后才切分训练测试集测试精度从0.74直接跳到0.88。排查方式是手动创建一条“明显不合理样本”放进测试集看它是否被模型“认出”。防止这类问题的核心原则所有统计变换只能从训练集获取参数测试集只能被transform。类别不平衡让多数类躺赢。流失率只有8%时模型全预测“不流失”就能拿92%的准确率。看准确率毫无意义要重点看召回率和ROC-AUC。处理方法是调class_weightbalanced或采样少数类。KNN在量纲未统一时的结果不可解释。我试过用原始特征直接跑KNNK5的邻居里几乎全是某个特征的极端值样本而且这种错误很难通过常规指标暴露因为accuracy可能并不低但业务逻辑明显不对劲。一定先做特征标准化。决策树深度过大画不出规则。一开始我把树深度设为15模型倒是精确但导出的规则有几千条叶子业务方根本没法用。后来改成深度4到5模型虽然损失了一点精度但规则可读性极大提升项目推进反而更快。6.2 我的几个固定习惯每一个算法Demo我都要跑三件套打印训练集精度、测试集精度、交叉验证均值和标准差。只看单次测试集精度极其容易自欺欺人标准差大说明模型对数据划分方式太敏感。对随机森林我固定打印OOB精度。它帮我省掉的验证集时间加起来已经足够一次完整的调参迭代。对朴素贝叶斯我固定检查各类别下特征的条件概率分布而不是只看精度。比如高斯朴素贝叶斯如果某些特征的条件方差被估算得极大大概率是异常值没处理干净这种情况精度看不出来但上线后预测概率会漂。选择算法时我现在的顺序是先想清楚业务需要的是“解释清楚”还是“精度优先”再考虑样本量、特征类型、预测实时性最后才决定用哪个模型。四个算法里随机森林和决策树是解释需求的分岔口朴素贝叶斯和KNN则是特征类型的适配结果。6.3 我的个人体会四个算法学完最大的感触是算法本身都不复杂真正的功夫全在数据处理、特征工程和模型评估的纪律性上。KNN和朴素贝叶斯属于“道理一听就懂但数据稍微脏一点就翻车”的模型决策树和随机森林对数据预处理要求低一些但你对模型内部发生了什么的理解程度决定了你能把它用得多好。我后来再给别人讲这四个算法时都会用一句话串起来先用朴素贝叶斯兜底再上随机森林提精度需要解释时换深一点的决策树KNN则永远是那个用来理解“距离”和“样本相似性”的最佳入门模型。机器学习进阶之路有很多条但这四条是值得每个实践者走一遍的必经之路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表