尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

决策树、随机森林与KNN:三种经典机器学习算法实战全解析

决策树、随机森林与KNN:三种经典机器学习算法实战全解析 这两年经常有人问我深度学习这么火入门机器学习还有必要从决策树、随机森林、KNN这些传统算法开始吗我的回答一直很坚决——太有必要了。这三个算法恰好代表了机器学习里三种完全不同的建模思想决策树是规则派随机森林是集成派KNN是距离派。把它们彻底吃透你才算真正理解了模型在做什么这件事后面再碰深度学习、大模型思路都会清晰很多。这篇文章我就按自己带新人时的完整路径来写先讲清楚每个算法的核心逻辑再给出可以直接跑的Python代码最后把我在项目里踩过的坑一并交代。文中的例子全部用Scikit-Learn实现不需要GPU不需要云计算你手头任何一台笔记本都能跟着跑完。1. 为什么是三剑客三种完全不同的建模思想1.1 三个算法分别代表了什么先说结论这三个算法放在一起学不是因为它们长得像而是因为它们恰好覆盖了监督学习的三种典型路径。决策树解决的是规则从哪来的问题。它的学习过程本质上是在自动找一套if-else规则让这套规则能最大化地把数据分对。你不给它任何先验知识它自己根据数据分布去选特征、切阈值最终长成一棵从根到叶的树。每个叶子节点对应一个预测结果路径上的每个分支就是一条判断条件。这种模型的好处是透明人能看懂它在做什么这也是它在风控、医疗等强解释性场景里至今无法被替代的原因。随机森林解决的是单个模型不稳定的问题。它的思路特别朴素一棵树容易出错那我就种几百棵树每棵树用不同的数据子集和不同的特征子集来训练最后让大家投票。任何一个单棵树的错误会被多数派冲掉整体预测的稳定性大幅提升。这背后的思想叫集成学习它是机器学习里最重要的套路之一后面所有厉害的模型几乎都继承了这种一群弱模型拼成强模型的理念。KNN解决的是相似的人会有相似的结果这个问题。它不训练参数把所有训练样本直接记住来一个新样本时找出离它最近的K个已知样本用它们的标签做投票分类或取平均回归。这种思路叫基于实例的学习也是懒惰学习——训练阶段几乎什么也不做真正的工作全部发生在预测那一刻。这三种思路一个管规则的自动提取一个管模型的稳定性提升一个管距离度量的灵活运用。学完它们你对模型如何从数据中获取知识这件事就有了一个完整的坐标系。1.2 用哪个数据集作为贯穿全文的主线为了让你能边读边跟着跑我这篇文章统一用Scikit-Learn内置的数据集不需要额外下载任何文件。分类部分用鸢尾花数据集Iris150条样本、4个特征、3个类别数据干净、量级适中非常适合观察算法之间的差异。回归部分我会用到波士顿房价数据集的替代品——如果你用的是新版Scikit-Learn波士顿数据集已经被移除了建议直接用内置的糖尿病数据集Diabetes或者自己造一个人工数据集来演示回归。KNN部分我会额外用一下手写数字数据集Digits因为它的样本量更大、特征维度更高能让你直观感受到KNN在高维下的表现变化。用内置数据集还有一个好处所有实验都是可复现的。你跑出来的结果和我文中写的数字可能略有浮动但趋势一定是一致的。等你自己换到真实业务数据时方法照搬只是特征工程的部分要花更多心思。2. 决策树实操从猜动物游戏理解特征划分逻辑2.1 信息增益到底在算什么决策树的核心问题是每一步该用哪个特征来切分数据答案由信息增益来定。这词听起来唬人其实背后的直觉特别简单——我们要选一个特征让切完之后的数据纯度提升最大。纯度在信息论里用熵Entropy来量化。熵的公式长这样H(X) -Σ p(x) * log2(p(x))这里的p(x)是每个类别的占比。熵越大表示数据越混乱熵为0表示所有样本都属于同一类。假如一组数据里有9个A类和5个B类那这组数据的熵就是H - (9/14)*log2(9/14) - (5/14)*log2(5/14) ≈ 0.940你可以把熵理解为系统的不确定程度。划分前数据有一个熵值用某个特征划分后各个子节点的熵按照样本量加权求和得到一个划分后的总熵。两者相减就是信息增益信息增益 划分前的熵 - 划分后各子节点熵的加权和信息增益越大说明该特征把数据分得越干净。决策树在每个节点做的就是遍历所有特征、所有可能的切分阈值找到信息增益最大的那个组合。这就是训练过程的全部本质。提示除了信息增益Scikit-Learn里还支持用基尼系数Gini作为划分依据原理类似。实践中基尼系数算得更快而且通常结果差异很小所以很多人默认就用Gini。2.2 用Scikit-Learn训练并可视化决策树下面这段代码可以直接跑我加了必要的注释import matplotlib.pyplot as plt from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier, plot_tree from sklearn.model_selection import train_test_split # 加载数据 iris load_iris() X, y iris.data, iris.target # 划分训练集和测试集random_state固定下来保证结果可复现 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 创建决策树分类器限制深度防止过拟合 clf DecisionTreeClassifier(max_depth3, random_state42) clf.fit(X_train, y_train) # 输出测试集准确率 print(决策树测试集准确率{:.4f}.format(clf.score(X_test, y_test))) # 把树画出来看结构 plt.figure(figsize(14, 8)) plot_tree(clf, filledTrue, feature_namesiris.feature_names, class_namesiris.target_names, roundedTrue) plt.show()如果你的环境不显示中文画图的文字框里会变成英文标签这个不影响使用。准确率一般在0.93到1.0之间别慌这个数据集小且干净决策树很容易做到高准确率。建议你亲手改一下max_depth的值分别用2、3、5、None去跑对比测试集准确率的变化。你会看到深度太小欠拟合深度太大过拟合总有一个甜点区间。这个体验过就忘不掉的感觉是读十遍理论都比不上的。2.3 决策树最大的坑是过拟合决策树是机器学习里最容易过拟合的模型没有之一。原因很直白如果不限制生长条件它可以把训练集里每一个样本都分到单独的叶子节点上训练集准确率100%但一到新数据上就崩。我在项目里见过最典型的情况是这样的某控系统的特征有几十个维度建模的人直接拿默认参数的决策树去跑训练集准确率99%测试集只有72%。这就是教科书级的过拟合。决策树会把训练数据里的噪声当成规律学进去——比如某人恰好因为某天心情好而点击了某个按钮这本来只是个随机事件但树会把它当成一条真规则记下来。控制过拟合的手段主要有三种限制树深max_depth最直接有效建议先小后大网格搜索。限制叶子节点最小样本数min_samples_leaf保证每个叶子至少覆盖一定数量的样本避免学出只为一两个样本服务的规则。限制分裂所需的最小样本数min_samples_split样本太少就不许再划分了。这三板斧用上决策树基本就能从学渣变成稳扎稳打的老实人。但说实话单棵决策树的稳定性天然有限你换个随机种子、重采一批数据训练出来的树可能长得完全不一样。这也是为什么我们需要随机森林。3. 随机森林实操为什么多棵树的投票更稳3.1 装袋思想的本质随机森林的核心技术叫Bagging全称是Bootstrap Aggregating中文常译作自助聚合。它的操作分三步第一步从原始训练集中有放回地随机抽样抽出若干个大小相同但内容略有不同的子数据集。有放回意味着同一条样本可能被抽到多次也可能一次都没被抽到。第二步分别在每个子数据集上训练一棵决策树。这里还有一个关键改动每棵树分裂时不是从所有特征里找最优而是先从全部特征里随机抽取一个子集再在这个子集里找最优。这个特征随机化很关键——如果所有树都用同样的最优特征去分裂那几百棵树会长得高度相似投票就失去了多样性集成效果大打折扣。第三步预测时让所有树投票分类问题取多数票回归问题取平均值。为什么这一套下来效果变好了核心原因是降低了方差。单棵决策树对数据扰动非常敏感训练数据稍微变化整棵树就面目全非。但几百棵不那么相关的树放在一起投票个别树因数据扰动产生的偏差会被群体平均掉。这就像你问一个问题一个专家可能因为当天状态不好给个离谱答案但问100个独立的普通选手他们的答案会收敛到正确方向。3.2 随机森林代码与关键参数解读随机森林的代码和决策树几乎一样简单from sklearn.ensemble import RandomForestClassifier # 创建随机森林200棵树 rf RandomForestClassifier( n_estimators200, max_depth5, random_state42, n_jobs-1, # 用所有CPU核心加速训练 oob_scoreTrue # 启用袋外样本评分 ) rf.fit(X_train, y_train) print(随机森林测试集准确率{:.4f}.format(rf.score(X_test, y_test))) print(袋外样本评分{:.4f}.format(rf.oob_score_))参数逐个说n_estimators树的数量不是越多越好。树多了训练时间和推理时间都会涨但准确率的提升曲线很快进入平台期。我一般先设100看准确率变化趋势如果还在明显上升就加到200、300直到增益变得微乎其微再停。max_depth树深随机森林里单棵树不需要特别深。因为集成本身已经能抵消一部分偏差过深的树反而浪费算力。通常限制在5~15之间比较划算。oob_score袋外评分这个参数强烈建议打开。Bagging采样时每条样本大约有37%的概率没被抽到当前这棵树的训练集里这些没被抽到的样本就是袋外样本。用它们来评估模型等效于免费的验证集不需要再额外切数据。训练完之后看oob_score就能大致估计模型在新数据上的表现。我在实际项目里的经验是随机森林是先跑通再调参的最佳模型。不管数据多脏、特征多乱先拿默认参数跑一个随机森林出来基本都能得到一个不差的基线结果。这也是它在比赛里被广泛当baseline的原因。3.3 特征重要性随机森林的隐藏福利随机森林做预测只是基本功它的另一个价值是能告诉你谁重要。Scikit-Learn里用feature_importances_属性就能查看每个特征的重要性分数背后原理是某个特征在树的分裂中被使用的次数越多、带来的不纯度下降越大它就越重要。import numpy as np importance rf.feature_importances_ indices np.argsort(importance)[::-1] print(特征重要性排序) for i in indices: print(特征{}{}: {:.4f}.format( i, iris.feature_names[i], importance[i] ))这个信息在真实业务里极其宝贵。比如你做用户流失预测特征有几十个维度跑完随机森林发现最近30天登录次数的重要性远远甩开其他特征那你的运营策略就能聚焦了——只针对这个关键特征做干预成本最低、效果最明显。需要注意特征重要性是相对指标而不是绝对贡献值。它受特征数量、特征之间的相关性影响如果两个特征高度相关重要性会被分散到两者头上。所以看排序就好别把具体数值当精确度量。4. KNN实操最朴素的近朱者赤4.1 KNN分类和回归其实是同一件事KNN的思想简单到你甚至可以不用机器学习库自己用几行Python就能实现分类。但先用Scikit-Learn体验标准流程再自己动手写理解会更深刻。分类时KNN找离新样本最近的K个已知样本然后数这K个样本里哪个类别最多就预测为哪个类别。回归时同样的逻辑只是最后一步改成把这K个样本的目标值取平均。from sklearn.neighbors import KNeighborsClassifier # 创建KNN分类器K5 knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train, y_train) print(KNN测试集准确率{:.4f}.format(knn.score(X_test, y_test)))就这么简单。你可能会觉得这也太偷懒了——确实KNN不需要训练过程fit这一步本质上是把训练数据存起来。真正的计算发生在predict那一刻你要为每个新样本算它和所有训练样本之间的距离。所以KNN有两个特别明显的短板一是预测速度慢样本量一大就扛不住二是存储开销大所有训练样本都得留在内存里。线上系统如果要求毫秒级响应KNN直接用往往够呛需要用KD-Tree、Ball-Tree这类数据结构做加速。4.2 距离度量与数据标准化KNN最容易翻车的地方KNN的价值取决于距离算得准不准而距离能不能算准很大程度取决于特征是否在同一个量纲上。举个例子假设你在做用户画像分类一个特征是年龄取值范围0~100另一个特征是年收入取值范围0~2000000。计算欧氏距离时年收入的差值会完全淹没年龄的影响。一个30岁、年薪30万的人和一个60岁、年薪29万的人算出来距离几乎只由收入决定年龄特征等于白费。解决办法是对所有特征做标准化Standardization让每个特征的均值变为0、方差变为1from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) knn_scaled KNeighborsClassifier(n_neighbors5) knn_scaled.fit(X_train_scaled, y_train) print(标准化后的KNN准确率{:.4f}.format(knn_scaled.score(X_test_scaled, y_test)))注意一个关键细节scaler必须只用训练集的数据来fit再对测试集做transform。如果先对整个数据集做标准化再划分训练测试集会发生数据泄露——测试集的信息流进了训练集你的验证结果会虚高。这个错误非常隐蔽我见过不少初学者栽在这上面。你可以在鸢尾花数据集上分别用标准化前和标准化后的数据跑一次KNN观察准确率的差异。因为鸢尾花特征量纲差距不太大差异可能不明显。我建议你换到那个手写数字数据集8x8像素的灰度值取值范围0~16上试试效果差异会更直观。4.3 K值到底怎么选K值的选择直接决定KNN的偏差-方差取舍。K太小比如K1模型只参考最近一个邻居容易被噪声带偏决策边界非常复杂K太大参考样本范围太广会把远处的不相关样本也算进来决策边界过于平滑丢失局部规律。选K的经验做法是候选范围从小到大比如1到30用交叉验证逐个评估挑准确率最高的那个K。代码如下from sklearn.model_selection import cross_val_score import numpy as np best_k 1 best_score 0 for k in range(1, 31): knn_cv KNeighborsClassifier(n_neighborsk) scores cross_val_score(knn_cv, X_train_scaled, y_train, cv5) mean_score scores.mean() if mean_score best_score: best_score mean_score best_k k print(交叉验证选出的最佳K值{}得分{:.4f}.format(best_k, best_score))这个循环跑完你就得到了一个相对可靠的K值。跑完之后我建议你再思考一个问题为什么随着K增大准确率不是单调变化的原因在于不同K值对应不同的决策边界复杂度而特定数据集的真实规律所在复杂度是固定的K只要偏离这个最优复杂度误差就会增大。5. 三剑客对比与应用选型5.1 一张表看明白三者的强弱项把三个算法放在同一张表里对比选型时一眼就能找到方向维度决策树随机森林KNN训练速度快中等几乎无训练预测速度快中等慢样本量大时很慢可解释性强规则直观中等可看特征重要性弱只能看邻居对数据量需求小中等大需要足够样本覆盖空间对特征缩放敏感度不敏感不敏感非常敏感对异常值敏感度中等低多树投票抵消高单个异常邻居会干扰抗过拟合能力弱强中等典型适用场景风控、医疗诊断、需要解释的场景工业默认首选baseline、特征筛选小样本快速原型、推荐系统协同过滤5.2 工程落地时我怎么做选择如果是做项目而不是做作业我的决策路径一般是这样的第一优先级看需不需要解释。银行风控、医疗诊断、法律辅助这类场景模型必须给出人类能听懂的理由直接选决策树。如果树太深导致解释困难就用浅层决策树深度3~5或者提取规则集牺牲一点精度换合规性。如果没有任何解释要求第一版模型我永远先跑随机森林。不是因为KNN和决策树不好而是随机森林在大多数表格数据上表现最稳、调参成本最低。先把随机森林的结果作为基线后续再尝试别的模型你才有一个判断标准新模型到底有没有比基线更好KNN在什么时候用我主要在两个场景用它一是数据量不大但特征空间比较规整、需要快速出原型的场景二是做推荐系统里相似用户或相似物品的检索候选集。另外值得提的是KNN作为一个非参数模型偶尔会打败各种复杂模型——比如数据分布极度不规则时KNN的局部适应性比全局模型更强。还有一个候选策略把三个模型组合起来投票。这听起来有点作弊但确实是我在一些比赛里的常规操作——决策树提供规则视角随机森林提供集成视角KNN提供局部视角三者投票表决往往比单个模型更稳。不过缺点也很明显推理时三个模型都要跑一遍成本和复杂度都上去了业务上能不能接受得自己权衡。6. 我踩过的几个坑与实操建议6.1 标准化里的数据泄露前面提到了标准化必须在划分数据集之后再单独fit训练集这个坑我再多强调几句。有一次我用KNN做某个工业设备的故障预测数据包含几十个传感器的时间序列特征我先对整个数据集做了一遍标准化然后才划分训练集和测试集。结果测试集准确率高达94%我一度以为模型已经能上线了。后来在试运行时发现现场效果完全对不上回头查才发现问题就出在数据泄露上——测试集的均值和方差已经提前参与了训练。正确的流程是先用训练集fit这个scaler然后分别transform训练集和测试集。所有涉及用全局数据计算统计量的预处理标准化、PCA、缺失值填充等都要遵循这个原则。6.2 交叉验证比单次划分更可靠train_test_split一次划分的结果有很大的随机性。如果你的训练集和测试集划分方式恰好让测试集偏向于容易分类的样本准确率就会虚高反之则虚低。我在实际项目里基本不会只看一次划分结果而是用交叉验证把数据切成K份通常5或10份轮流拿其中一份做验证、其余做训练最后把K次结果平均。这样得到的是一个更接近模型真实泛化能力的分值。cross_val_score这个函数直接封装了完整流程几个参数就能跑完。用它选K值、选树深度、比较模型比你自己反复切分数据要高效得多。6.3 从baseline起步别想一口吃成胖子很多新手一上来就追求最复杂的模型把深度学习、XGBoost一股脑往上堆结果数据一跑全是问题最后也不知道是数据的问题、参数的问题还是模型的问题。我的习惯是拿到一份新数据先跑一个最简单的随机森林甚至先用常数模型比如全部预测为多数类当作地板基准看看数据本身到底有多难分。然后依次尝试决策树、KNN、随机森林记录各自的交叉验证分数。这个多模型快速对比的过程一般十分钟内就能完成但它能告诉你的信息量极大数据质量如何、哪些特征在起作用、哪个方向值得深挖。等确定方向之后再去精调某一个模型而不是一开始就陷入调参泥潭。这三剑客之所以被我称为玩转机器学习的第一块跳板就是因为它们各自足够简单又覆盖了建模最核心的思维范式。最后再分享一个小技巧给随机森林开oob_scoreTrue配合cross_val_score做双重校验是我每次做项目时的固定操作。一个免费的袋外评分一个稳健的交叉验证评分两者相差不大基本就可以放心进入下一阶段了。
返回列表