news 2026/11/6 4:08:35

随机森林Python实战:手写与sklearn对照及避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
随机森林Python实战:手写与sklearn对照及避坑指南

简介:面向机器学习初学者、课程实验与算法复现场景,这份随机森林实践材料围绕 Python 实现与小数据集验证展开,适合希望读懂分类流程并动手跑通代码的读者。压缩包共 4 个文件,包含 2 份 Python 源码、1 份 CSV 数据集和 1 份 TXT 阅读说明,整体约 34KB,体积轻巧,便于本地解压后直接查看与运行。两份源码均配有详尽中文注释且已调试通过,一份来自网络、一份为整理后重写,可对照比较建树、投票与预测等环节的实现差异,帮助梳理随机森林的关键函数与编码思路。附带的小型数据集可用于分类实验,读者能观察特征输入、模型训练与结果评估的完整链路,并在此基础上调整参数、记录正确率变化,积累调参与排错经验。目前已有 15746 人学习,适合作为入门练习与代码阅读样本;因数据规模较小且调参未深入,结果更偏向流程演示,若追求更高精度还需自行优化。

1. 解压之后先别急着跑:这份随机森林 Python 代码包到底能给什么

同事扔过来一个压缩包,说“随机森林 Python 的代码和数据都在里面,你跑一下看看准不准”。解压出来是一堆 csv、两套实现,外加一个空的 outputs 目录,没有 README。这种资源包最容易卡住人的地方不是算法难,而是你根本不知道该先跑哪个文件、跑完该看什么、结果不对该怀疑谁。它的价值恰好也在这儿:手写版和 sklearn 版跑的是同一份数据,能互相对照,哪一步抄错了立刻能暴露。适合两类人,一类是刚学完决策树、想看清 Bagging 到底怎么落地的新手,另一类是要把现成模型塞进业务、但不想只当一个fit/predict调包侠的开发者。往下从目录结构一路拆到你自己改分裂准则。

2. 目录结构与技术选型:手写版和 sklearn 版为什么都要留

2.1 目录结构:先认清哪些是入口,哪些是被 import 的库

拿到这类资源包,我会先花两分钟把目录画出来,标清楚“能直接 run 的”和“只能被 import 的”。代码包大体是这个形状:

random-forest-demo/ ├── data/ # 原始数据,只读,任何清洗都写到 outputs 里 │ ├── iris.csv │ ├── winequality_red.csv │ ├── titanic_train.csv │ └── housing_small.csv ├── rf_scratch/ # 纯 numpy 手写实现 │ ├── criteria.py # 基尼 / 熵 / 方差 │ ├── tree.py # 单棵 CART 树 │ ├── forest.py # Bootstrap + 特征子采样 + 集成 │ └── metrics.py # 准确率、混淆矩阵 ├── rf_sklearn/ # sklearn 对照、调参、解释 │ ├── train.py # 基线训练,程序入口 │ ├── tune.py # 网格搜索 │ └── explain.py # 特征重要性、置换重要性 ├── notebooks/ │ └── 01_eda.ipynb # 先看数据长什么样 ├── outputs/ # 图、模型、日志全部落这里 └── requirements.txt
路径职责什么时候需要动它
notebooks/01_eda.ipynb分布、缺失、类别不平衡一眼看清换新数据集时第一个改
rf_sklearn/train.py跑基线、出指标、存模型想要一个能交差的结果时
rf_sklearn/tune.py网格 / 随机搜索基线不够用时
rf_scratch/tree.py单棵树的分裂与递归想改准则、加剪枝时
rf_scratch/forest.py采样、集成、OOB想改并行、改聚合方式时
data/*.csv原始数据只读,不要就地覆盖

这里有个容易忽略的约定:rf_scratch/下面没有if __name__ == "__main__",它是一组库,直接python tree.py什么都不会发生。真正的入口只有两个,rf_sklearn/train.py和 notebook。新手最常干的事是挨个文件双击运行,然后以为代码坏了。

2.2 技术选型:numpy 打底,sklearn 只当标尺

手写版刻意只依赖 numpy,连 sklearn 的DecisionTreeClassifier都不 import。原因很实际:一旦允许调 sklearn,你就会忍不住用它的树去对照,然后跳过最该看的那段递归。所有矩阵运算用 ndarray,连续特征的分裂点搜索靠“排序 + 累积类别计数”向量化,纯 Python 的双层 for 循环在几千行数据上要跑好几分钟,这是血泪经验。

pandas 的角色被限制在“读数据”这一步,fit之前统一.values转成 ndarray。原因有两个:手写树里大量用布尔索引X[mask],DataFrame 上做这个会退化成iloc,慢一个量级;另外 sklearn 内部也是 ndarray,两边用同一种数据表示,结果对齐时才不会掺进 dtype 和列顺序的干扰。

sklearn 版存在的意义是标尺,不是替代品。它的作用是三件事:给一个可信的准确率上界、给一组调好的参数、给置换重要性这类手写版没实现的诊断工具。常见做法是先把rf_sklearn/train.py跑通拿到基线数字,再拿手写版去逼近它,差得离谱就说明某一环写错了。

2.3 数据集:四份 csv 各管一件事,别拿错

数据集不是随便凑的,每份负责验证一条链路。拿错数据集去调参,会得到一堆看起来合理但没法解释的结论。

数据集量级任务在这份代码里负责验证什么
iris.csv150 行 4 特征 3 类多分类冒烟测试、手写版与 sklearn 逐棵对齐
winequality_red.csv千余行 11 特征回归 / 二分类阈值切分、类别不平衡下的评估
titanic_train.csv数百行混合类型二分类缺失值、类别编码的预处理链路
housing_small.csv数百行连续特征回归手写版把“投票”换成“均值”的改造点

iris 用来做对齐是有讲究的:特征少、树浅、类别均衡,两套实现只要随机种子一致,准确率应该一模一样,差 0.01 都能立刻定位。titanic 则相反,它脏得很有代表性——Age有缺失、Embarked有缺失、Sex是字符串,预处理顺序写反就会静默产生 NaN,模型照样能训,只是结果莫名其妙地低。winequality 那种连续标签,直接当分类做会切出七八个类别,通常的做法是先定一个阈值二值化,再谈准确率。

3. 环境搭建与第一次跑通:从建虚拟环境到出第一张特征重要性图

3.1 环境:三条命令,别往全局环境里装

python -m venv .venv source .venv/bin/activate # Windows 下换成 .venv\Scripts\activate pip install numpy pandas scikit-learn matplotlib jupyter pip freeze > requirements.txt # 锁版本,别让自己三天后复现不出来

核心依赖就四个,不需要额外的东西。不建议直接在全局或某个大而全的发行版环境里跑,sklearn 跨版本有参数改名和默认值变更,报出来的TypeError往往和你的代码逻辑毫无关系,排查方向直接跑偏。

提示:pip freeze出来的文件建议提交到版本库。调参调了两天结果复现不出来,八成是这一步偷懒了。

激活成功的标志是命令行前面出现(.venv)。接下来验证一下环境是否完整:

python -c "import numpy, pandas, sklearn, matplotlib; print('ok')"

打印ok再往下走。如果卡在sklearn上,通常是编译依赖的问题,换一个干净的虚拟环境重装比在原环境里修快得多。

3.2 数据加载:把 csv 变成模型能吃的 X / y

先写一个只服务 iris 的 loader,把约定固定下来:最后一列是标签,列名统一小写去空格。

import pandas as pd from sklearn.model_selection import train_test_split def load_iris(path="data/iris.csv", seed=42): df = pd.read_csv(path) # 列名统一:有些导出工具会带空格或大写,后面按名字取列会翻车 df.columns = [c.strip().lower().replace(" ", "_") for c in df.columns] y = df.iloc[:, -1].values # 约定最后一列是标签 X = df.iloc[:, :-1].values # 其余全是特征,且都已是数值 # 小数据集必须分层,否则某一类可能整体落进测试集 return train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)

iloc[:, -1]依赖列顺序,不是列名,所以读进来第一件事是确认列顺序没被 Excel 打乱。stratify=y在 150 行这种量级上是必须的:不分层的话,3 类里数量最少的那一类有可能测试集里一个样本都没有,classification_report直接给你一片nan。X.values转 ndarray 是为了后面手写树里做布尔索引。

titanic 那份要复杂一些,缺失值和编码的顺序不能反:

def load_titanic(path="data/titanic_train.csv", seed=42): df = pd.read_csv(path) cols = ["Pclass", "Sex", "Age", "Fare", "Embarked", "Survived"] df = df[cols].copy() # 1. 先填缺失,再编码。顺序反了 fillna 会把 NaN 变成字符串 "nan" df["Age"] = df["Age"].fillna(df["Age"].median()) # 年龄长尾,中位数比均值稳 df["Embarked"] = df["Embarked"].fillna("S") # 登船港是类别,填众数 # 2. 编码成整数,随机森林不需要 one-hot,但需要数值 df["Sex"] = df["Sex"].map({"male": 0, "female": 1}) df["Embarked"] = df["Embarked"].map({"S": 0, "C": 1, "Q": 2}) y = df.pop("Survived").values return train_test_split(df.values, y, test_size=0.2, random_state=seed, stratify=y)

Age用中位数而不是均值,是因为船上年龄分布右偏,均值会被少数高龄乘客拉高。Embarked必须先fillna再map,反过来写的话map遇到NaN会原样返回NaN,fillna("S")再去填就已经晚了,整列变成 object 类型,sklearn 会在fit时报“无法把 string 转成 float”。这个坑没有报错提示指向它,只能靠看 dtype 发现。

注意:编码完之后跑一句df.dtypes检查,只要有一列是 object,就别急着训练。

3.3 第一次训练:参数先给一组能跑的默认值

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = load_iris() clf = RandomForestClassifier( n_estimators=300, # 树的数量,边际收益递减,300 在这个量级足够 max_depth=None, # 先不限深,让树自己长 min_samples_leaf=2, # 叶子最少 2 个样本,抑制单样本叶子带来的过拟合 max_features="sqrt", # 每次分裂随机看 sqrt(p) 个特征,分类默认值 oob_score=True, # 用袋外样本做一次免费验证 n_jobs=-1, # 用满所有核 random_state=42, # 固定随机性,否则两次结果对不上 ) clf.fit(X_train, y_train) print("OOB 分数:", round(clf.oob_score_, 4)) print(classification_report(y_test, clf.predict(X_test)))

n_estimators从 100 加到 300,准确率通常涨一两个点,再往上加基本是浪费电。min_samples_leaf=2是为了避免树长到每个叶子只剩一个样本,那种树在训练集上完美、在测试集上抖得厉害。oob_score=True是随机森林白送的一份验证:每棵树训练时有约 37% 的样本没被抽到,用这些样本给这棵树打分再汇总,等于不切验证集也能估一个泛化误差。OOB 和测试集准确率差三个百分点以内算正常,差十几个点就要怀疑数据泄漏或者切分方式有问题。

拿到基线后接着出图,这张图比准确率更能说服人:

import numpy as np import matplotlib.pyplot as plt imp = clf.feature_importances_ order = np.argsort(imp) plt.barh(np.array(["f0", "f1", "f2", "f3"])[order], imp[order]) plt.xlabel("importance") plt.tight_layout() plt.savefig("outputs/feature_importance.png", dpi=150)

feature_importances_是归一化的不纯度下降量,加起来等于 1。它快,但对高基数特征有偏好,只能当参考,真正的结论我会用rf_sklearn/explain.py里的置换重要性再核一遍。

4. 核心模块拆解:手写随机森林的三个关键实现

4.1 分裂准则:基尼、熵、方差三件套

准则函数是整棵树唯一需要“可插拔”的部分。分类用基尼或熵,回归用方差,签名统一成“输入一维标签数组,返回一个标量”。

import numpy as np def gini(y): """基尼不纯度:1 - sum(p_k^2),越小越纯""" if y.size == 0: return 0.0 _, counts = np.unique(y, return_counts=True) p = counts / y.size return 1.0 - np.sum(p ** 2) def entropy(y): """信息熵:-sum(p_k * log2 p_k),比基尼慢在 log 上""" if y.size == 0: return 0.0 _, counts = np.unique(y, return_counts=True) p = counts / y.size return float(-np.sum(p * np.log2(p))) def variance(y): """回归树的 MSE 版本,叶子值取均值""" return float(np.var(y)) if y.size else 0.0

这三个函数只在“评估某个节点有多纯”时调用,不能直接拿去做分裂搜索——每个候选切点都调一次np.unique的话,时间复杂度会从 O(n log n) 涨到 O(n² log n)。所以真正的实现里,基尼是用累积类别计数向量化算出来的,见下一节。熵和基尼在分类效果上的差距通常很小,选哪个更像玄学,但熵的可解释性好一点,报告里好写。

4.2 递归建树:一次分裂搜索的完整链路

先解决搜索。核心思路是把特征排一次序,然后扫一遍所有合法切点,用前缀和算出左右两侧的类别分布。

def best_split(X, y, features, min_samples_leaf): """在所有候选特征上找加权不纯度下降最大的切点""" n = X.shape[0] parent = gini(y) best = {"gain": 0.0, "feature": None, "threshold": None} for f in features: col = X[:, f] order = np.argsort(col, kind="mergesort") # 稳定排序,重复值不被打乱 xs, ys = col[order], y[order] # 只在相邻不同值之间切;相同值中间切没有意义 cand = np.where(xs[1:] != xs[:-1])[0] if cand.size == 0: continue # 叶子样本数约束,两侧都得满足 left_n = cand + 1 right_n = n - left_n ok = (left_n >= min_samples_leaf) & (right_n >= min_samples_leaf) cand, left_n, right_n = cand[ok], left_n[ok], right_n[ok] if cand.size == 0: continue # 累积类别计数:cum[k] 表示前 k+1 个样本的类别分布 classes, yi = np.unique(ys, return_inverse=True) onehot = np.zeros((n, classes.size)) onehot[np.arange(n), yi] = 1 cum = np.cumsum(onehot, axis=0) left = cum[cand] # (m, C) right = cum[-1] - left p_left = left / left_n.reshape(-1, 1) p_right = right / right_n.reshape(-1, 1) imp_left = 1.0 - np.sum(p_left ** 2, axis=1) imp_right = 1.0 - np.sum(p_right ** 2, axis=1) # 加权不纯度下降 gain = parent - (left_n / n) * imp_left - (right_n / n) * imp_right i = int(np.argmax(gain)) if gain[i] > best["gain"]: best = {"gain": float(gain[i]), "feature": f, "threshold": float((xs[cand[i]] + xs[cand[i] + 1]) / 2)} return best

几个参数值得说清楚。min_samples_leaf同时约束左右两侧,只约束一边会切出只有一个样本的叶子。阈值取相邻两个不同值的中间点,这样x <= threshold和x > threshold能把训练集里的样本严格分开。kind="mergesort"是稳定排序,特征里有大量重复值时,非稳定排序会让同一份数据两次训练得到不同的树,固定种子也救不回来。

然后是递归本身。停止条件必须写三重,少一个就会出现无限递归或者树深到爆栈:

class Node: __slots__ = ("feature", "threshold", "left", "right", "value") def __init__(self, value=None): self.feature = self.threshold = None self.left = self.right = None self.value = value # 叶子节点的预测值 def majority(y): vals, counts = np.unique(y, return_counts=True) return vals[np.argmax(counts)] def build_tree(X, y, depth, max_depth, min_samples_split, min_samples_leaf, n_features, rng): node = Node(value=majority(y)) # 三个停止条件:到深度上限 / 样本太少 / 已经纯净 if depth >= max_depth or y.size < min_samples_split or np.unique(y).size == 1: return node # 每个节点重新抽一批特征,这是随机森林和普通 Bagging 的关键区别 feats = rng.choice(X.shape[1], size=n_features, replace=False) split = best_split(X, y, feats, min_samples_leaf) if split["feature"] is None: # 找不到合法切点,退化成叶子 return node mask = X[:, split["feature"]] <= split["threshold"] node.feature, node.threshold = split["feature"], split["threshold"] node.left = build_tree(X[mask], y[mask], depth + 1, max_depth, min_samples_split, min_samples_leaf, n_features, rng) node.right = build_tree(X[~mask], y[~mask], depth + 1, max_depth, min_samples_split, min_samples_leaf, n_features, rng) return node

n_features在每次递归时重新抽,跟 sklearn 的行为一致。有些教材把它写成“每棵树抽一次然后固定”,两种都能跑,但结果对不上,做对齐验证时这是第一个要统一的点。__slots__是顺手优化,上完棵树之后节点数量能到几十万,省下的内存很可观。

4.3 Bootstrap 与特征子采样:随机性加在哪一层

森林层只做三件事:有放回抽样、并发地建树、收集袋外样本。

class RandomForest: def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2, min_samples_leaf=1, max_features="sqrt", oob_score=True, random_state=None): self.n_estimators = n_estimators self.max_depth = max_depth if max_depth is not None else 10 ** 9 self.min_samples_split = min_samples_split self.min_samples_leaf = min_samples_leaf self.max_features = max_features self.oob_score = oob_score self.random_state = random_state def fit(self, X, y): rng = np.random.default_rng(self.random_state) # 独立 Generator,别用全局种子 n, p = X.shape self.classes_ = np.unique(y) k = int(np.sqrt(p)) if self.max_features == "sqrt" else p self.trees_, self.y_train_ = [], y # 袋外投票累积:不用硬投票计数,累积概率再取 argmax 更稳 self.oob_proba_ = np.zeros((n, self.classes_.size)) self.oob_count_ = np.zeros(n) for _ in range(self.n_estimators): idx = rng.integers(0, n, n) # 有放回,约 36.8% 的样本一次都没被抽到 oob = np.ones(n, dtype=bool) oob[np.unique(idx)] = False tree = build_tree(X[idx], y[idx], 0, self.max_depth, self.min_samples_split, self.min_samples_leaf, k, rng) self.trees_.append(tree) if self.oob_score and oob.any(): self.oob_proba_[oob] += predict_proba_tree(tree, X[oob], self.classes_) self.oob_count_[oob] += 1 return self

rng.integers(0, n, n)比rng.choice(n, n, replace=True)快,两者等价。1 - 1/e ≈ 0.632 这个数字就是袋外样本的比例来源,样本量越大越接近它。OOB 这里用概率累加而不是多数投票,是因为同一棵树的概率分布可以直接相加,而投票计数在类别不均衡时会偏向多数类。

4.4 预测与 OOB 打分:把一棵树的结果变成森林的结果

def predict_proba_tree(node, X, classes_): """把样本推到叶子,返回 one-hot 形式的概率(单棵树是硬输出)""" out = np.zeros((X.shape[0], classes_.size)) for i in range(X.shape[0]): cur = node while cur.feature is not None: cur = cur.left if X[i, cur.feature] <= cur.threshold else cur.right out[i, np.searchsorted(classes_, cur.value)] = 1.0 return out def predict_proba(self, X): proba = np.zeros((X.shape[0], self.classes_.size)) for tree in self.trees_: proba += predict_proba_tree(tree, X, self.classes_) return proba / len(self.trees_) def predict(self, X): return self.classes_[np.argmax(self.predict_proba(X), axis=1)] def oob_score(self): has = self.oob_count_ > 0 pred = self.classes_[np.argmax(self.oob_proba_[has] / self.oob_count_[has, None], axis=1)] return float(np.mean(pred == self.y_train_[has]))

单棵树输出的是 one-hot,森林把它们平均成软概率再取最大值,这叫软投票。硬投票(每棵树各投一票,看谁票多)在类别均衡时两者结果差不多,不均衡时软投票通常更好一点。回归任务把predict_proba换成每棵树叶子值的均值就行,聚合逻辑只改一行。OOB 打分里has这个掩码不能省,没被任何一棵树漏掉的样本,分母是 0。

5. 避坑与排查:跑这套代码最常见的 5 个翻车现场

5.1 训练十分钟不出结果,或者直接 RecursionError

现象:python rf_sklearn/train.py跑了很久没动静,换成手写版直接抛RecursionError: maximum recursion depth exceeded。

原因:max_depth=None加min_samples_leaf=1,连续特征上每次都能找到让不纯度下降的切点,树会一直长到每个叶子只剩一个样本,深度接近样本量。手写版是递归实现,Python 默认递归上限一千层,直接撞墙。

解决:给min_samples_leaf一个下限(分类 2 到 5,回归 3 到 10),同时给max_depth一个兜底值。手写版里我把None显式转成10 ** 9,是故意的——让它靠样本约束停下来,而不是靠深度。

5.2 手写版和 sklearn 版准确率差一大截

现象:同一份 iris,手写版 0.90,sklearn 0.97,怎么调都对不上。

原因:三个地方最容易出偏差。一是分裂阈值搜索粒度不同,手写版扫全部相邻值,sklearn 在特征基数很大时会做近似分箱;二是max_features的抽样时机,我写在每个节点,有些实现写成每棵树;三是随机种子不在同一条链上,sklearn 的random_state和 numpy 的 Generator 是两套独立的状态。

解决:做对齐验证时把所有随机性关掉——max_features=p(用全部特征)、bootstrap=False、random_state固定,先让单棵树和DecisionTreeClassifier(criterion="gini")完全一致,再逐层放开。

5.3 特征重要性全是 0,或者和业务直觉反着来

现象:明明是最相关的那个特征,feature_importances_打出来是 0。

原因:feature_importances_是训练集上的不纯度下降累加,如果某个特征在树的上层从没被抽中过,它一次都不会贡献。特征之间高度共线时更明显,两个强相关特征会互相“抢”重要性,一个拿满另一个归零。另外特征方差接近 0 时,它也永远切不出有效分裂。

解决:换sklearn.inspection.permutation_importance,在测试集上打乱单列看指标跌多少,结论更贴近业务。看到 0 先查这一列的标准差,再查它和其他特征的相关系数。

5.4 Windows 上n_jobs=-1卡死或者进程无限增殖

现象:脚本刚跑起来,任务管理器里冒出一堆 python 进程,内存爆掉,或者干脆卡住不动。

原因:Windows 用 spawn 方式启动子进程,子进程会重新 import 主模块。顶层代码没有if __name__ == "__main__":保护时,子进程一 import 就又去建森林,无限递归。

解决:把训练代码全部塞进main()并用if __name__ == "__main__":守住;调试阶段先把n_jobs设成 1,确认逻辑没问题再开多核。

5.5 明明固定了random_state,结果还是每次都不一样

现象:random_state=42写死了,两次运行准确率差一个点。

原因:只固定了 sklearn 的种子,手写版里还在用全局的np.random.seed,而其他地方(比如train_test_split、你自己写的采样)会消耗全局状态,顺序一变结果就变。另外一种情况是train_test_split在设种子之前就被调用了。

解决:全部改用np.random.default_rng(seed)生成独立 Generator,把 rng 一路传进build_tree,禁止任何模块碰全局np.random。这条我踩过不止一次,改完之后两次运行的输出能逐字节一致。

6. 二次开发:换数据、加准则、导出模型的改造路径与验证方法

这份代码包真正能被用起来的地方,是它把“改哪儿”标得很清楚。换自己的数据基本只动 loader,rf_sklearn/train.py里的load_iris()换成你的函数即可,只要保证返回四个 ndarray 且标签是数值。改完先跑 100 棵树看 OOB 稳不稳,OOB 抖动超过两个点,说明数据预处理有问题,别急着调参。

改造目标改哪个文件验证方式
接入自有数据loader 函数OOB 与测试集差距在 3 个点内
增加分裂准则rf_scratch/criteria.py单棵树与 sklearn 对应 criterion 完全一致
导出模型rf_scratch/tree.py加序列化导出再加载后预测结果逐元素相同
加速分裂搜索best_split里的 onehot 缓冲单次fit耗时下降且预测不变
改成回归森林聚合处换均值、准则换方差与RandomForestRegressor的 R² 接近

加一种准则是最小改动,注册进去就行:

CRITERIA = {"gini": gini, "entropy": entropy} def make_criterion(name): if name not in CRITERIA: raise ValueError(f"unknown criterion: {name}") return CRITERIA[name]

但要注意best_split里现在硬编码了基尼的向量化公式,加准则时得同时把这段改成按名字分支,否则你只是换了个评估函数,搜索逻辑还在算基尼,结果看起来“没变化”。这就是那种不报错、但结论完全错的黑匣子,只能靠对齐测试抓出来。

对齐测试我一般写成这样,跑通它再做任何改动:

from sklearn.tree import DecisionTreeClassifier # 关掉所有随机性,单棵树必须和 sklearn 一模一样 tree = DecisionTreeClassifier(criterion="gini", max_depth=4, random_state=0) tree.fit(X_train, y_train) mine = build_tree(X_train, y_train, 0, 4, 2, 1, X_train.shape[1], np.random.default_rng(0)) assert np.array_equal(tree.predict(X_test), predict_tree(mine, X_test))

改完之后,我还会跑一条学习曲线:把n_estimators从 10 扫到 500,看准确率在哪一段变平。这份代码包的默认值是 300,换到你的数据上不一定够,尤其样本上万之后,曲线的拐点会往右移。

从那以后,我每次动这类“算法 + 数据”的资源包,都会先把单棵树对齐跑通,再谈森林和调参——跳过这一步,后面所有数字都是玄学,调出来的东西也没法复现。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 4:07:48

Git入门指南:从下载安装到基础配置全解析

1. Git入门&#xff1a;从零开始掌握版本控制工具 作为一个在软件开发行业摸爬滚打多年的老手&#xff0c;我深知版本控制系统的重要性。Git作为目前最流行的分布式版本控制系统&#xff0c;已经成为程序员必备的技能之一。今天我就来分享一下Git最基础的入门知识——下载与安…

作者头像 李华
网站建设 2026/11/6 4:07:18

Apache Doris在医疗数据分析中的应用与优化

1. Doris在医疗数据分析中的独特价值医疗科技领域正面临数据爆炸式增长的挑战。某三甲医院信息科主任曾告诉我&#xff1a;"我们医院的PACS系统每天新增影像数据就超过2TB&#xff0c;传统数据库根本扛不住这种增长。"这正是Apache Doris这类MPP分析型数据库大显身手…

作者头像 李华
网站建设 2026/11/6 4:07:16

Windows 下 Hydra 配 txt 字典做口令基线核查实战

简介&#xff1a;hydra Windows 版及配套 txt 字典集合&#xff0c;面向需要在 Windows 环境开展口令安全测试、渗透测试学习与实验复现的安全从业者和进阶学习者&#xff0c;可解决 hydra 在 Windows 下运行依赖与字典准备两类问题。压缩包共 99 个文件&#xff0c;约 120.9MB…

作者头像 李华
网站建设 2026/11/6 4:04:34

MAC地址查询方法与网络管理实战指南

1. 什么是MAC地址及其应用场景MAC地址&#xff08;Media Access Control Address&#xff09;是网络设备的唯一硬件标识符&#xff0c;由48位二进制数组成&#xff0c;通常表示为12位十六进制数字&#xff08;如00-1A-2B-3C-4D-5E&#xff09;。这个烧录在网卡芯片上的物理地址…

作者头像 李华
网站建设 2026/11/6 4:04:33

RabbitMQ消息队列:从原理到实践的分布式解耦方案

1. 为什么我们需要告别传统HTTP通信在分布式系统架构中&#xff0c;服务间通信通常采用HTTP协议作为默认选择。这种同步阻塞式的通信方式虽然简单直接&#xff0c;但随着系统复杂度提升&#xff0c;其局限性日益明显&#xff1a;强耦合性&#xff1a;调用方必须知道被调用方的确…

作者头像 李华
网站建设 2026/11/6 4:04:15

Python+SnowNLP中文短评情感分析与词云实战

简介&#xff1a;这份资源面向具备一定Python基础、希望入门自然语言处理与文本挖掘的学习者&#xff0c;围绕豆瓣《肖申克救赎》影评展开情感分析与词云可视化实践&#xff0c;重点演示如何调用SnowNLP库完成中文情感倾向判定&#xff0c;并配合分词、词频统计等环节形成完整分…

作者头像 李华