☰
随机森林实战指南:sklearn实现、参数调优与过拟合避坑
2026/10/4 3:24:32 网站建设 项目流程

简介:这是一份面向Python初学者的随机森林二分类实战示例,适合正在学习scikit-learn机器学习库、希望理解RandomForestClassifier调用流程的读者,也可用于课程实验或快速原型验证。资源共2个文件,包含1个Python脚本和1个data.csv数据文件。脚本演示了从CSV读取数据、将每行四列特征与二分类标签分开、切分为训练集和测试集、拟合随机森林分类器并在测试集上检验分类效果的全过程;数据文件则直接提供规范化的四特征一标签样本,无需额外构造数据,拷入脚本同级目录即可运行。压缩包整体仅974B,轻量小巧,适合携带与在线预览,是入门随机森林建模的高性价比选择。目前已有1486人学习,学习者可从中掌握数据加载、特征/标签分离、模型训练与验证的基本套路,并能在此基础上改写自己的数据集或调整模型参数。对快速上手sklearn随机森林算法的初学者有较高参考价值。

1. 随机森林不是玄学:先从一次“过拟合翻车”说起

当你在搜索框里输入“Python随机森林算法sklearn代码 RandomForestClassifier示例”,多半是手头已经攒了一批特征和标签,想用随机森林快速拿到一个能用的模型。这个方法确实皮实:对缺失值不敏感,对量纲不敏感,几乎不用做特征缩放就能跑出像样的结果。但我第一次用 RandomForestClassifier 时也翻过车——训练集 AUC 高达 0.99,换到测试集直接掉到 0.72。当时我以为随机森林是黑匣子,后来才明白是参数和数据出了问题。这篇笔记不是把官方文档复述一遍,而是按我实际把随机森林用出真东西的顺序,讲清楚它为什么难坏、最小可运行代码怎么组织、参数怎么调、哪些坑必须绕开。适合刚用 Python 做过一点数据分析、想让随机森林真正落地到分类或回归任务的人。

2. 随机森林为什么“难坏”:从决策树到集成学习的三个关键点

2.1 决策树是基座:分裂条件、叶子与剪枝

随机森林不是凭空出现的算法,它的基学习器是决策树。决策树的目标,是通过一系列“特征值是否大于某个阈值”的判断,把样本逐步分成类别更纯的组。sklearn 的 RandomForestClassifier 使用的是 CART 决策树,分类场景下分裂质量默认用基尼不纯度衡量,你也可以把 criterion 设为 entropy 用信息熵。无论用哪个指标,本质上都在回答同一个问题:当前节点用哪个特征的哪个阈值切下去,能让下一层的不纯度下降最多。

决策树的优点是解释性强、对特征尺度不敏感、可以处理数值和类别混合特征。但它有一个致命短板:不做限制的决策树会一直分裂下去,直到每个叶子节点只剩一种类别,这等于把训练集背了下来,换新样本就翻车。因此实际使用决策树时总需要剪枝。sklearn 里有两种路径:一种是预剪枝,也就是在生长前限制 max_depth、min_samples_split、min_samples_leaf 这些参数;另一种是后剪枝,比如代价复杂度剪枝 cost_complexity_pruning。在随机森林里,我们更依赖预剪枝,因为集成本身能抵消一部分过度生长带来的风险,而且后剪枝会让每棵树之间的差异性变小。

这里需要区分一个常见误解:随机森林中的树通常没有刻意做深度限制,默认 max_depth=None,单棵树完全是高方差模型。但正是这种高方差、低偏差的单棵树,配合后面的随机化,才能在平均之后获得低方差。如果一开始就把每棵树剪得很浅,随机森林的偏差会变大,反而不一定更好。所以调参时不要一上来就限制深度,先跑默认参数,再根据交叉验证结果决定剪枝力度。

2.2 集成核心:Bagging 与特征随机化

随机森林的“随机”来自两个互相独立的抽样机制。第一是样本抽样:每棵树从原始训练集中有放回地抽取同样大小的样本集,这叫 bootstrap 抽样。有放回意味着某些样本会被抽到多次,另一些样本一次也抽不到,约 36.8% 的样本不在某个 bootstrap 样本里,这些样本被称为袋外样本。第二是特征抽样:每次节点分裂时,不是从全部特征里选最优分裂,而是先从全部特征中随机抽一个子集,再从子集里找最优分裂。分类模型默认的特征子集大小是 sqrt(n_features),这就是 max_features 参数的含义。

这两个随机化的目的都是降低树之间的相关性。统计学上,集成模型的方差由单棵树方差和树间协方差共同决定。如果每棵树都一样,投票和平均只是放大了同一棵树的错误,无法降低方差;只有让树之间足够“不同”,平均之后错误才能互相抵消。Bagging 对高方差模型最有效,对本身低方差高偏差的模型反而增益有限。你可以把随机森林想象成“一群各有性格的专家”,而不是“同一个专家反复投票”。

特征随机化还有一个额外好处:它让每棵树只依赖部分特征,从而在特征维度上也能形成多样性。如果数据里有一两个极强的特征,没有特征随机化的 Bagging 树几乎都会先用这两个特征分裂,树之间的相关性依然很高;而 max_features 的随机抽取强制它们去关注不同特征,这在特征维度很高时非常关键。这也是随机森林在“特征数量多于样本量”的基因数据上还能稳定工作的原因之一。

2.3 随机森林在 sklearn 里的真实结构:n_estimators 与 bootstrap

用 sklearn 时,你不需要手写上述流程。RandomForestClassifier 内部封装了完整的 Bagging 过程:创建 n_estimators 棵决策树、每棵树用独立 bootstrap 样本训练、预测时综合所有树的投票或概率平均。默认 n_estimators=100 是 sklearn 在速度和效果之间的平衡点。在这点上,200 棵可能只比 100 棵好一点点,但训练时间和内存翻倍。想确认当前环境里的默认参数,可以直接打印:

from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier() print(rf.get_params())

get_params 返回模型所有可调参数和默认值。你会发现 max_features 默认是 "sqrt"、bootstrap 默认是 True、oob_score 默认是 False。在训练前看一眼这个输出,能避免你基于旧版本的记忆去调参。

bootstrap 参数默认是 True,也就是启用样本有放回抽样。如果你把它改成 False,每棵树训练时都使用完整训练集,随机性只剩特征抽样,这其实是 ExtraTrees 的随机性来源的一半,但纯粹的 RandomForestClassifier 建议保留 bootstrap=True。另外,当 bootstrap=True 时,模型会自动记录哪些样本没进入每棵树,可以用 oob_score=True 来启用袋外评估。袋外评估是一种几乎不额外消耗数据的交叉验证,它用每棵树没见过的样本来测试该树,最后把所有树的袋外预测汇总成整个模型的袋外分数。

理解了这个结构,你就能解释很多现象:为什么 n_estimators 增加后模型不会明显过拟合,因为每棵树都是独立训练的,多树平均只会让预测更稳定;为什么单棵树训练时间不长但随机森林很慢,因为要反复做采样和特征子集搜索;为什么随机森林在几千条数据上比深度学习更省心,因为它不需要调试学习率、批大小、网络层数,只需要把特征清理干净并喂进去。

这个结构也决定了它适合哪些任务:数据量中小规模、特征维度中等或偏高、存在非线性关系且不需要强解释性的场景。如果数据量达到百万级别,或者特征之间主要是顺序结构,梯度提升树或深度学习往往是更好的选择。

3. 跑通最小示例:RandomForestClassifier 的完整训练与预测

3.1 数据准备:用 sklearn 自带数据集,避免第一步就踩坑

正式业务里,数据可能充满字符串、缺失值、异常样本,如果直接用,RandomForestClassifier 会报错或给出奇怪结果。为了把注意力集中在算法本身,我通常先用 sklearn 自带的数据集跑通流程。最常用的是乳腺癌数据集 load_breast_cancer,它包含 569 个样本、30 个数值特征,类别标签是二分类(恶性/良性)。特征全部是实数且没有缺失,不需要做标准化,因为随机森林是树模型,对特征尺度不敏感。如果你还没装 scikit-learn,按环境惯例用 pip install -U scikit-learn 装一下即可。

这里给出数据加载和划分的代码:

from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split import numpy as np data = load_breast_cancer() X, y = data.data, data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print("训练集大小:", X_train.shape, "测试集大小:", X_test.shape) print("训练集正负样本数:", np.bincount(y_train))

逻辑说明:train_test_split 按 8:2 划分;stratify=y 表示按原始标签比例分层抽样,这样即使数据不平衡,训练集和测试集中的正负比例也保持一致;random_state=42 固定随机种子,保证每次运行得到的划分一样。np.bincount(y_train) 快速统计 0 和 1 两类样本数量,帮助你快速发现类别是否失衡。

需要注意:有些人习惯在划分前用 StandardScaler 做标准化,对随机森林来说没必要,反而增加无谓的代码。真正需要检查的是特征里是否含有 NaN 或无穷大,虽然新版本 sklearn 的某些集成模型允许缺失值,但传统 RandomForestClassifier 遇到 NaN 会直接报错。如果数据存在缺失,常见做法是用 SimpleImputer 填充中位数或众数。

3.2 核心代码:训练、预测、评估一条龙

这是全文最核心的一段代码,建议直接复制在自己的脚本里跑一遍:

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, confusion_matrix, classification_report clf = RandomForestClassifier( n_estimators=100, max_depth=None, max_features="sqrt", random_state=42, n_jobs=-1, ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) y_prob = clf.predict_proba(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print("混淆矩阵:\n", confusion_matrix(y_test, y_pred)) print("分类报告:\n", classification_report(y_test, y_pred)) print("前5条概率:\n", y_prob[:5])

参数说明:n_estimators=100 是随机森林里树的数量,一般 100 到 500 之间性价比最高;max_depth=None 表示不限制树的深度,让单棵树充分生长,依靠随机森林的机制去控制过拟合,如果数据量小或者发现过拟合,再改成 10 或 20;max_features="sqrt" 是分类任务默认也是常用的特征子集大小,即每次分裂随机考虑 sqrt(特征数) 个特征;random_state=42 固定随机种子,让结果可复现;n_jobs=-1 表示使用所有 CPU 核心并行训练。

fit 是训练过程,predict 返回每个样本的类别标签。predict_proba 返回一个形状为 (n_samples, n_classes) 的数组,每一行是样本属于各类的概率。很多业务场景不建议直接使用 predict 的硬标签,而应该保留概率再做阈值决策,比如在风控中设定“概率大于 0.7 才放行”。

评估部分:准确率只适合类别平衡的数据;confusion_matrix 能告诉你具体错在哪一类;classification_report 给出 precision、recall、f1-score,分别衡量“预测为正的可信度”“正类被找出来的比例”“两者的调和平均”。如果数据不平衡,重点看少数类的 recall 和 f1,不要只看准确率。

3.3 结果解读与 model.feature_importances_

训练完成后,一个非常有价值的事情是查看特征重要度。sklearn 的随机森林通过训练好的树直接暴露 feature_importances_ 属性,表示每个特征对模型预测的贡献度之和(归一化到 1)。代码:

import pandas as pd importance_series = pd.Series( clf.feature_importances_, index=data.feature_names ) print(importance_series.sort_values(ascending=False).head(10))

输出会显示哪些乳腺细胞指标对判断恶性肿瘤影响最大。注意:feature_importances_ 是基于平均不纯度减少计算的,它偏向取值多、基数高的数值特征,不能完全等同于“特征的真实因果影响”。举个例子,某医院 ID 列虽然是随机号码,但由于取值很多,可能被反复选中用来分裂,基尼重要度高,但它毫无业务含义。因此更稳健的做法是使用排列重要性 permutation_importance,它通过在测试集上打乱某列特征、观察模型性能下降程度来衡量特征贡献,代码会在后面的避坑章节里给出。

在解读随机森林结果时,我建议大家先看一个东西:训练准确率与测试准确率的差距。如果训练 99%、测试 80%,说明明显过拟合;如果训练和测试都在 95% 附近,则说明模型泛化良好。然后是混淆矩阵中的错误类型:如果正类错判为负类的代价高,就适当提高决策阈值;反之降低。最后再看 feature_importances_,用它来缩小特征集合,再训练一个更轻量的模型用于上线。

4. 参数调优与验证:从默认参数到可用模型

4.1 关键参数逐个说透

随机森林的参数不算多,但每个都会影响模型行为和资源消耗。我把最常用的几个整理成下面这张表,再逐个解释。

参数默认值作用调参思路
n_estimators100树的数量树越多越稳定,但收益递减,注意内存
max_depthNone每棵树的最大深度过拟合时从 10~30 开始限制
max_features"sqrt"(分类) / 1.0(回归)每次分裂的随机特征子集大小分类用 sqrt 或 log2,回归用 1.0 或 None
min_samples_split2节点再分裂所需最小样本数增大到 5~20 可抑制过拟合
min_samples_leaf1叶子节点最少样本数增大到 5~50,模型更平滑
criterion"gini"分裂质量指标gini 与 entropy 差异不大,少调
class_weightNone类别权重不平衡时用 "balanced" 或手动指定
oob_scoreFalse是否计算袋外分数数据量不大时建议 True
random_stateNone随机种子固定它才能复现

逐个说:n_estimators 主要受计算资源约束,500 以后收益极其微小,我一般用 200 作为默认上限。max_depth 是最直接的复杂度和过拟合控制项,数据量几千条时,None 往往没问题,数据量大或特征噪音多就限制在 10~30。max_features 控制每次分裂时随机挑选的候选特征数:越小,树之间的差异性越大、方差越低,但单棵树能力变差,偏差升高;越大,单棵树拟合能力越强,但树间相关性变高。几乎所有分类任务里 sqrt 都是稳妥起点。min_samples_split 和 min_samples_leaf 是正则化手段,其中 min_samples_leaf 比 min_samples_split 更常用,因为它保证了叶子有足够样本支持。criterion 一般不调,gini 和 entropy 在随机森林里差异不大。class_weight 在类别不均衡且不想做重采样时非常管用。oob_score 是白送的交叉验证,不开白不开。

4.2 用 GridSearchCV 找参数:一个可复制的调参流程

调参最怕拍脑袋。我会先用 GridSearchCV 跑一个较小的网格,覆盖 max_depth、max_features、min_samples_leaf、min_samples_split 这几个最值得调的参数。代码:

from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestClassifier param_grid = { "n_estimators": [100], "max_depth": [None, 10, 20], "max_features": ["sqrt", "log2"], "min_samples_split": [2, 5, 10], "min_samples_leaf": [1, 2, 4], } grid = GridSearchCV( RandomForestClassifier(class_weight="balanced", random_state=42), param_grid, cv=5, scoring="roc_auc", n_jobs=-1, ) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_) print("最优AUC:", grid.best_score_)

逻辑说明:GridSearchCV 会对 param_grid 里的参数组合逐个做 5 折交叉验证,scoring="roc_auc" 指定用 AUC 作为评分指标,对不平衡分类更合理。这里 n_estimators 固定为 100,是为了先调更影响泛化的结构参数;等结构参数确定后,再单独把 n_estimators 的范围扩到 [100, 200, 300] 验证收益。

参数说明:这个网格的组合数是 1 × 3 × 2 × 3 × 3 = 54 组,每组 5 折,也就是 270 次模型训练。如果数据集在万条级别、特征几百个,这个规模已经有点压力。遇到这种情况,我一般用 RandomizedSearchCV 或先粗后细的两阶段搜索。粗调时每个参数给 3 个跨度大的值,锁定区间后再细化。

调参有一个小技巧:先固定 n_estimators=100 和 max_features="sqrt",只调 max_depth 和 min_samples_leaf,因为它们主要控制过拟合;再用验证曲线观察 max_features,最后回头看 n_estimators。不要在第一次就开满全部维度,那样不仅慢,还容易把随机噪声当最优。

4.3 随机森林回归 RandomForestRegressor 的快速对照

热词里频繁出现“随机森林回归算法”,很多人会误以为分类跟回归差别很大。其实在 sklearn 里,回归版本 RandomForestRegressor 几乎一样的用法,差异主要在三点:max_features 默认是 1.0(即用全部特征),criterion 默认是 squared_error(均方误差),评估指标从准确率换成 MSE 或 R2。下面用合成数据演示:

from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score X_reg, y_reg = make_regression( n_samples=1000, n_features=20, noise=0.1, random_state=42 ) Xr_train, Xr_test, yr_train, yr_test = train_test_split( X_reg, y_reg, test_size=0.2, random_state=42 ) reg = RandomForestRegressor( n_estimators=200, max_depth=10, max_features=1.0, random_state=42, n_jobs=-1, ) reg.fit(Xr_train, yr_train) yr_pred = reg.predict(Xr_test) print("MSE:", mean_squared_error(yr_test, yr_pred)) print("R2:", r2_score(yr_test, yr_pred))

make_regression 生成线性带噪的回归数据集,噪声系数 0.1 让它不完美,方便看到模型在训练和测试上的差距。回归随机森林的 predict 返回连续数值,而不是类别标签;predict_proba 不存在,对应的是不确定性估计,sklearn 1.0 之后可以用 predict(X, return_std=True) 获得每棵树叶子的标准差。回归任务的调参方法与分类相同,只是评分指标换成 neg_mean_squared_error 或 r2。

如果数据里有时间顺序,做回归或分类预测时要注意不能随机划分,要按时间切分训练集和测试集,否则你会用未来信息预测过去,得到虚高的分数。这一点在量化交易、销量预测场景里特别容易翻车。

5. 随机森林避坑指南:5 个我踩过的真实问题

5.1 类别不均衡导致模型“摆烂”

现象:用乳腺癌这类平衡数据没问题,换到真实风控数据后,负样本只占 5%。模型训练完,predict 出来的标签几乎全为正类,准确率有 95%,但一看分类报告,负类 recall 是 0。

原因:随机森林的损失函数在分裂时关注整体纯度,多数类的样本量大,贡献的不纯度下降占绝对主导。少数类的分裂收益被淹没了,树根本学不到少数类的分界。

解决:优先设 class_weight="balanced",按类别样本数反比加权,让少数类在分裂收益中被放大。代码就一行:

clf = RandomForestClassifier(class_weight="balanced", random_state=42) clf.fit(X_train, y_train)

如果还是不行,下一步用 imbalanced-learn 库的 BalancedBaggingClassifier,它会在每个 bootstrap 样本内对少数类过采样。注意,修改类别权重后,predict_proba 输出的概率不再是真实概率,这在做阈值决策时要重新校准。另外,网格搜索时要在 param_grid 里同时加入 class_weight,比如 ["balanced", None],而不是写死。

5.2 n_estimators 越大越好?内存先崩了

现象:为了追求精度,我把 n_estimators 设为 1000,训练到一半内存 90%,模型保存后占 1.2GB,预测一个样本都要几十毫秒。

原因:每棵决策树都完整保存在内存里,树的数量线性增加内存;默认不限深度时,每棵树可能有几千个节点,1000 棵树就是上百万个节点对象。

解决:先把 n_estimators 控制在 100~200,观察验证曲线。如果从 100 加到 200,交叉验证得分上升不超过 0.005,就没必要继续加。同时限制 max_depth=15 或 min_samples_leaf=5,能显著减少节点数量。另外,可以考虑用 HistGradientBoostingClassifier(直方图梯度提升)替代,它内存占用小很多,适合大数据集。

补充:sklearn 没有直接删掉部分树的方法,但你可以对 predict_proba 的树概率平均做临时处理,比如只取前 80 棵树的预测平均。生产环境中更合理的是用 warm_start=True 渐增式训练并保存中间模型。

5.3 特征工程没做,重要度全是噪音

现象:我把用户 ID、注册时间戳直接丢进模型,feature_importances_ 显示这两个特征排前二。模型上线前筛选特征时差点把它们留下。

原因:基尼重要度更偏好取值种类多、数值范围大的特征。用户 ID 每个样本都不同,树很容易靠它分裂,但分裂对泛化毫无意义;时间戳同样是高基数特征。

解决:删除无业务含义的唯一标识列;把时间戳解析成年、月、星期、小时等周期性特征;用排列重要性做二次确认。排列重要度代码:

from sklearn.inspection import permutation_importance perm = permutation_importance( clf, X_test, y_test, n_repeats=5, random_state=42, scoring="roc_auc" ) print(perm.importances_mean)

permutation_importance 的做法是打乱某一列测试集数据,然后看模型性能下降多少。如果打乱用户 ID 后性能几乎不变,说明它不重要,即使基尼重要度高。注意排列重要度在特征强相关的数据里会被低估,建议只用于筛选明显假特征。

5.4 训练集得分高、测试集拉胯

现象:训练集 AUC 0.97,测试集 0.78,我一度怀疑是不是测试集划分有问题。

原因:这是过拟合的典型症状。随机森林虽然抗过拟合,但如果你把 max_depth 设为 None、min_samples_leaf 设为 1,且数据里存在强记忆特征,个别树会把训练样本完全背下来。投票平均后,树的整体方差是低了,但偏差和控制不足时仍会过拟合。

解决:先用 min_samples_leaf 让叶子有更多样本,比如从 1 提到 5;同时限制 max_depth=10~20。然后是交叉验证:不要只看一次划分,用 5 折交叉验证取平均。还有一招是看 oob_score_:

clf = RandomForestClassifier(oob_score=True, random_state=42) clf.fit(X_train, y_train) print(clf.oob_score_)

oob_score 接近测试集得分时,说明模型的袋外泛化能力稳定;如果 oob_score 很高而测试集低,往往是测试集选取有问题;如果 oob_score 也低,那就是参数过拟合了。

5.5 随机种子不固定,复现不了结果

现象:同一份代码、同一份数据,我连续跑两次 GridSearchCV,最优参数不一样;再跑一次预测,AUC 有小幅浮动。调参调得像玄学。

原因:随机森林的 bootstrap 抽样和特征子集随机都依赖随机数生成器;GridSearchCV 在并行时,不同 CPU 线程取随机数的顺序也可能不同,导致结果不确定。

解决:在所有模型和交叉验证器上固定 random_state。在脚本开头加:

import numpy as np np.random.seed(42)

但这只影响 numpy 的随机流,无法完全控制 sklearn 内部所有随机源。最稳妥的做法是把 random_state 传进 RandomForestClassifier、train_test_split、GridSearchCV 里。如果要在完全确定的环境下复现,可以把 PYTHONHASHSEED 设为 0,或者用 joblib 保存调参结果。另外,不要因为 0.001 的 AUC 波动而纠结于某组参数,随机森林的随机性决定了它天然有微小噪声,选交叉验证得分整体稳定的参数区间即可。

6. 进阶技巧:把随机森林用成生产级模型的两个习惯

6.1 先看 OOB 分数,再动参数

训练时开 oob_score=True,模型的 oob_score_ 就是每棵树在袋外样本上的汇总表现。它不需要额外留验证集,适合数据集不大、不想再切一次训练集的情况。我的习惯是:默认参数跑完后,先看 oob_score_ 和测试集得分,如果两者明显不一致,说明划分策略或参数有问题,先解决这个,再去 GridSearchCV。

6.2 joblib 保存模型与 warm_start 增量扩展

模型训练完,用 joblib 保存,避免每次启动都重新训练:

import joblib joblib.dump(clf, "rf_model.joblib")

新数据来了,如果不想重新训练,把 n_estimators 加到原来的 150,设置 warm_start=True 再次 fit。warm_start 会保留原来 100 棵树,只新增 50 棵,训练时间更短。但要注意:新树在 bootstrap 时来自新数据,模型对旧数据的记忆会逐渐淡化,所以它适合数据流稳定、周期性追加的场景。

6.3 概率输出后接阈值决策

业务里最值钱的不是 predict 的硬标签,而是 predict_proba 的概率。比如在放贷场景,宁可错过也不误判,就把阈值提高到 0.8;在召回场景,需要多捞人,就降到 0.3。阈值怎么选?用验证集的混淆矩阵,算不同阈值下的利润或 F1,找到业务最优解。这一步做完,随机森林才真正接进业务系统。

我自己用随机森林最深的教训是:先看 oob_score,再动参数,最后信 feature_importances_。大多数项目里默认参数已经能跑到七八十分,真正的收益来自特征清理和阈值选择,而不是堆树。希望帮到你。

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

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

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

立即咨询