哈喽,大家好~
咱们来聊一个,最近大家提到的问题:决策树和随机森林为什么通常不需要标准化?
因为我们前面学KNN、SVM、逻辑回归的时候,老师经常强调一句,训练模型之前,最好先把数据标准化。
结果学到决策树、随机森林,突然又告诉你,不用标准化,直接扔进去训练就行。
很多同学第一反应都是,凭什么?
其实这件事没那么玄乎。
你只要搞明白决策树到底是怎么“看数据”的,就明白了。
决策树真正关心的,不是数值有多大
举个特别简单的例子。
现在有一个特征是年龄:
18
22
30
35
45
60
决策树训练的时候,可能找到一个切分点:
年龄 <= 32.5
然后把数据分成两边:
左边:18、22、30
右边:35、45、60
接下来比较这个切分以后,分类是不是更纯了。
比如分类问题里可能计算Gini:
或者计算信息增益。
注意这里有一个特别关键的地方:
决策树并没有计算“18和60差多少”。
它真正做的是:
18 < 22 < 30 < 35 < 45 < 60
然后不断寻找:
在哪个位置切一刀最好?
这就是核心。
标准化之后发生了什么?
假设我们把年龄做标准化:
原来的数据:
18
22
30
35
45
60
可能变成:
-1.21
-0.94
-0.40
-0.06
0.61
1.62
你会发现一个事情:
原来:
18 < 22 < 30 < 35 < 45 < 60
标准化以后:
-1.21 < -0.94 < -0.40 < -0.06 < 0.61 < 1.62
顺序完全没变。
所以原来最优切分如果发生在:
30 和 35 之间
标准化以后,最优切分依然发生在:
-0.40 和 -0.06 之间
只是阈值的数字变了。
数据被切成的两组样本,本质上没变。
这就是为什么决策树通常不需要标准化。
和KNN一对比就特别清楚了
假设一个数据集有两个特征:
年龄:18 ~ 60
收入:30000 ~ 1000000
如果使用KNN,它要计算两个样本之间的距离:
假设:
年龄差:5
收入差:100000
那计算距离的时候,收入这一项几乎完全碾压年龄。
所以KNN很在意:
每个特征到底是什么量纲。
但决策树不会把年龄和收入加在一起算距离。
它更像是在不断问:
年龄 <= 35?
或者:
收入 <= 300000?
哪个切法好,就用哪个。
所以:
年龄是几十,收入是几十万,对决策树来说并没有天然的问题。
那随机森林为什么也不需要?
这个就更好理解了。
随机森林本质上就是:
Decision Tree
+
Decision Tree
+
Decision Tree
+
......
训练很多棵不同的决策树,然后:
分类任务:
大家一起投票
回归任务:
大家取平均
单棵决策树本身就不依赖特征尺度,那么由大量决策树组成的随机森林,通常自然也不依赖标准化。
我们直接做一个实验
光讲理论其实没那么有意思。
下面我们自己造一个数据集。
故意让两个特征的尺度相差特别大:
特征1:大约 -3 ~ 3
特征2:直接放大1000倍
然后分别训练:
决策树
随机森林
比较:
不标准化
vs
标准化
看看最终到底有什么区别。
完整Python代码
这份代码可以直接运行,大家可以详细的学习一下~
-
图1:决策树标准化前后的决策边界; -
图2:决策树、随机森林在多次实验中的性能对比。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# 1. 构造数据
X, y = make_moons(
n_samples=1200,
noise=0.25,
random_state=42
)
# 故意制造严重的尺度差异
# 第一个特征大约是 -1 ~ 2
# 第二个特征放大1000倍
X[:, 1] = X[:, 1] * 1000
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.30,
random_state=42,
stratify=y
)
# 2. 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 3. 决策树
tree_raw = DecisionTreeClassifier(
max_depth=5,
random_state=42
)
tree_scaled = DecisionTreeClassifier(
max_depth=5,
random_state=42
)
tree_raw.fit(X_train, y_train)
tree_scaled.fit(X_train_scaled, y_train)
tree_raw_pred = tree_raw.predict(X_test)
tree_scaled_pred = tree_scaled.predict(X_test_scaled)
print("=" * 60)
print("Decision Tree")
print("=" * 60)
print(
"不标准化 Accuracy:",
accuracy_score(y_test, tree_raw_pred)
)
print(
"标准化 Accuracy:",
accuracy_score(y_test, tree_scaled_pred)
)
print(
"两种模型预测完全一致比例:",
np.mean(tree_raw_pred == tree_scaled_pred)
)
# 4. 随机森林
rf_raw = RandomForestClassifier(
n_estimators=300,
max_depth=6,
random_state=42,
n_jobs=-1
)
rf_scaled = RandomForestClassifier(
n_estimators=300,
max_depth=6,
random_state=42,
n_jobs=-1
)
rf_raw.fit(X_train, y_train)
rf_scaled.fit(X_train_scaled, y_train)
rf_raw_pred = rf_raw.predict(X_test)
rf_scaled_pred = rf_scaled.predict(X_test_scaled)
print()
print("=" * 60)
print("Random Forest")
print("=" * 60)
print(
"不标准化 Accuracy:",
accuracy_score(y_test, rf_raw_pred)
)
print(
"标准化 Accuracy:",
accuracy_score(y_test, rf_scaled_pred)
)
print(
"两种模型预测一致比例:",
np.mean(rf_raw_pred == rf_scaled_pred)
)
# 5. 图1:决策树标准化前后的决策边界
fig, axes = plt.subplots(
1,
2,
figsize=(15, 6)
)
fig.patch.set_facecolor("#F7F7FB")
-
# 原始模型决策边-
x1_min = X[:, 0].min() - 0.5
x1_max = X[:, 0].max() + 0.5
x2_min = X[:, 1].min() - 100
x2_max = X[:, 1].max() + 100
xx1, xx2 = np.meshgrid(
np.linspace(x1_min, x1_max, 500),
np.linspace(x2_min, x2_max, 500)
)
grid_raw = np.c_[xx1.ravel(), xx2.ravel()]
Z_raw = tree_raw.predict(grid_raw)
Z_raw = Z_raw.reshape(xx1.shape)
axes[0].contourf(
xx1,
xx2,
Z_raw,
levels=20,
cmap="RdYlBu",
alpha=0.72
)
axes[0].scatter(
X_test[:, 0],
X_test[:, 1],
c=y_test,
cmap="RdYlBu",
edgecolors="white",
linewidth=0.7,
s=42,
alpha=0.9
)
axes[0].set_title(
"Decision Tree · Raw Features",
fontsize=17,
fontweight="bold"
)
axes[0].set_xlabel("Feature 1")
axes[0].set_ylabel("Feature 2 × 1000")
axes[0].grid(
alpha=0.15,
linestyle="--"
)
# 标准化模型
#
# 注意:
# 为了方便比较,
# 我们仍然在“原始坐标空间”画图。
#
# 只是预测的时候把网格送进scaler
grid_scaled = scaler.transform(grid_raw)
Z_scaled = tree_scaled.predict(grid_scaled)
Z_scaled = Z_scaled.reshape(xx1.shape)
axes[1].contourf(
xx1,
xx2,
Z_scaled,
levels=20,
cmap="RdYlBu",
alpha=0.72
)
axes[1].scatter(
X_test[:, 0],
X_test[:, 1],
c=y_test,
cmap="RdYlBu",
edgecolors="white",
linewidth=0.7,
s=42,
alpha=0.9
)
axes[1].set_title(
"Decision Tree · Standardized Features",
fontsize=17,
fontweight="bold"
)
axes[1].set_xlabel("Feature 1")
axes[1].set_ylabel("Feature 2 × 1000")
axes[1].grid(
alpha=0.15,
linestyle="--"
)
plt.suptitle(
"Scaling Changes the Numbers, Not the Ordering",
fontsize=21,
fontweight="bold",
y=1.02
)
plt.tight_layout()
plt.show()
# 6. 图2:重复实验
#
# 看标准化前后模型表现是否存在系统性变化
records = []
for seed in range(50):
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.30,
random_state=seed,
stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Decision Tree
tree_raw = DecisionTreeClassifier(
max_depth=5,
random_state=42
)
tree_scaled = DecisionTreeClassifier(
max_depth=5,
random_state=42
)
tree_raw.fit(
X_train,
y_train
)
tree_scaled.fit(
X_train_scaled,
y_train
)
tree_raw_acc = accuracy_score(
y_test,
tree_raw.predict(X_test)
)
tree_scaled_acc = accuracy_score(
y_test,
tree_scaled.predict(X_test_scaled)
)
records.append({
"seed": seed,
"model": "Decision Tree",
"raw": tree_raw_acc,
"scaled": tree_scaled_acc
})
# Random Forest
rf_raw = RandomForestClassifier(
n_estimators=200,
max_depth=6,
random_state=42,
n_jobs=-1
)
rf_scaled = RandomForestClassifier(
n_estimators=200,
max_depth=6,
random_state=42,
n_jobs=-1
)
rf_raw.fit(
X_train,
y_train
)
rf_scaled.fit(
X_train_scaled,
y_train
)
rf_raw_acc = accuracy_score(
y_test,
rf_raw.predict(X_test)
)
rf_scaled_acc = accuracy_score(
y_test,
rf_scaled.predict(X_test_scaled)
)
records.append({
"seed": seed,
"model": "Random Forest",
"raw": rf_raw_acc,
"scaled": rf_scaled_acc
})
df_result = pd.DataFrame(records)
# 7. 统计差异
df_result["difference"] = (
df_result["scaled"]
-
df_result["raw"]
)
print()
print("=" * 60)
print("50次重复实验平均结果")
print("=" * 60)
print(
df_result
.groupby("model")[["raw", "scaled", "difference"]]
.mean()
)
# 8. 绘制性能差异图
fig, ax = plt.subplots(
figsize=(12, 7)
)
fig.patch.set_facecolor("#F7F7FB")
ax.set_facecolor("#F7F7FB")
models = [
"Decision Tree",
"Random Forest"
]
positions = np.arange(len(models))
width = 0.28
raw_mean = []
scaled_mean = []
raw_std = []
scaled_std = []
for model in models:
temp = df_result[
df_result["model"] == model
]
raw_mean.append(
temp["raw"].mean()
)
scaled_mean.append(
temp["scaled"].mean()
)
raw_std.append(
temp["raw"].std()
)
scaled_std.append(
temp["scaled"].std()
)
bars1 = ax.bar(
positions - width / 2,
raw_mean,
width,
yerr=raw_std,
capsize=7,
label="Raw Features",
color="#FF5A5F",
alpha=0.88
)
bars2 = ax.bar(
positions + width / 2,
scaled_mean,
width,
yerr=scaled_std,
capsize=7,
label="Standardized",
color="#3A86FF",
alpha=0.88
)
# 添加50次实验的散点
for i, model in enumerate(models):
temp = df_result[
df_result["model"] == model
]
jitter_raw = np.random.default_rng(42).normal(
positions[i] - width / 2,
0.025,
len(temp)
)
jitter_scaled = np.random.default_rng(100).normal(
positions[i] + width / 2,
0.025,
len(temp)
)
ax.scatter(
jitter_raw,
temp["raw"],
s=20,
alpha=0.35,
color="#9B2226"
)
ax.scatter(
jitter_scaled,
temp["scaled"],
s=20,
alpha=0.35,
color="#0057B8"
)
ax.set_xticks(
positions
)
ax.set_xticklabels(
models,
fontsize=13
)
ax.set_ylabel(
"Test Accuracy",
fontsize=13
)
ax.set_ylim(
min(
df_result["raw"].min(),
df_result["scaled"].min()
) - 0.03,
1.01
)
ax.set_title(
"Does Standardization Improve Tree Models?",
fontsize=20,
fontweight="bold",
pad=18
)
ax.text(
0.5,
1.01,
"50 different train/test splits",
transform=ax.transAxes,
ha="center",
fontsize=11,
alpha=0.65
)
ax.legend(
frameon=False,
fontsize=12
)
ax.grid(
axis="y",
linestyle="--",
alpha=0.2
)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.show()
第一张图我觉得是最重要的一张。
左边是:
原始数据
→ Decision Tree
→ 决策边界
右边是:
StandardScaler
→ Decision Tree
→ 决策边界
但为了公平比较,我们最后把两个模型都放回原始数据坐标系里看。
你会发现:
两个模型切数据的位置通常几乎一样。
这其实就是树模型不在乎尺度最直观的证据。
标准化确实把:
100000
变成了:
0.83
但它没有改变样本之间的顺序。
所以:
谁在阈值左边
谁在阈值右边
基本没变。
而这恰恰是决策树最关心的事情。
图2其实更接近真实科研场景
很多人跑一次实验以后会发现:
Random Forest
不标准化:0.91
标准化:0.915
然后马上得出结论,看吧,随机森林标准化以后效果更好。
这个结论其实有点着急了。
为什么我代码里面要重复50次?
就是想告诉大家:
不要拿一次数据划分的0.005差异去下结论。
我们真正应该看的是:
不同train/test划分
↓
重复几十次
↓
比较整体分布
如果你最后看到:
Decision Tree
Raw ≈ Scaled
Random Forest
Raw ≈ Scaled
这才真正符合理论预期。
也就是说:
标准化没有给树模型带来稳定、系统性的收益。
那为什么有时候标准化以后结果真的变了?
这个地方特别容易误解。
理论上,标准化属于线性单调变换:
如果 ,它不会改变数据大小关系。
所以理论上树的切分应该保持一致。
但是实际工程代码里,你还是可能看到:
0.9231
变成:
0.9204
这种轻微变化。
这时候不用慌。
实际模型训练还受到很多细节影响,比如:
浮点数计算精度
阈值候选点
重复值
随机采样
特征随机选择
并行计算
具体库的实现细节
尤其随机森林本身还有:
Bootstrap
+
随机特征子集
+
很多棵随机树
所以实验出现一点点变化,并不能证明:
随机森林需要标准化。
这两个概念一定要分开。
最后
我自己工作这么多年以后,越来越觉得,机器学习里最容易拉开差距的,其实不是会不会调一个API。
现在这些代码:
model.fit(X_train, y_train)
model.predict(X_test)
Codex、Claude Code随手就给你写出来了。
真正有价值的是看到一个现象以后,你能不能解释:
为什么?
比如今天这个问题。
如果只是背,随机森林不用标准化。
过几天基本就忘了。
但是如果你真正理解,树模型主要依赖样本的排序关系和切分位置,而标准化通常不会改变这种顺序关系。
以后再碰到Decision Tree、Random Forest、XGBoost、LightGBM,你其实都能自己判断。
机器学习真正难的,从来不是把模型代码跑起来,而是搞懂模型为什么这么做、什么时候能用、什么时候会失效。
像今天讲的“为什么决策树、随机森林通常不需要标准化”,代码可能几分钟就能写完,但背后的切分逻辑、特征尺度、模型差异,如果没人专门给你捋一遍,很容易学完还是一知半解。
之前给大家介绍的 「200个机器学习模型」 的系统内容~

目的不是让大家收藏200个模型名字,而是希望你以后再看到一个模型时,能够真正知道:
它解决什么问题、核心原理是什么、代码怎么写、参数怎么调、结果怎么看,以及它和其他模型到底有什么区别。
里面既有大家常见的:
线性回归、逻辑回归、KNN、SVM、决策树、随机森林、XGBoost、LightGBM……
也会继续往更丰富的机器学习模型和实际应用场景延伸。
每个模型我都尽量按照:
原理理解 → Python代码 → 数据实验 → 可视化分析 → 使用场景 → 常见坑
这样的思路去整理。
如果你现在正处在研究生阶段,或者刚开始系统学机器学习,经常有这种感觉:代码我会跑,但别人一问为什么,我就说不清楚。
扫码可以加入:
到了后面你会发现,不管是做科研、写论文、找实习,还是继续学深度学习、大模型,很多判断能力,其实都是从这些基础模型一点点练出来的。

