大数跨境

导师皱眉:“KNN都要标准化,为什么随机森林不用?”我:“因为它不算距离”导师:“这是树模型真正的核心!”

导师皱眉:“KNN都要标准化,为什么随机森林不用?”我:“因为它不算距离”导师:“这是树模型真正的核心!” 机器学习和人工智能AI
2026-08-30
2

哈喽,大家好~

咱们来聊一个,最近大家提到的问题:决策树和随机森林为什么通常不需要标准化?

因为我们前面学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=(156)
)

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=(127)
)

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代码 → 数据实验 → 可视化分析 → 使用场景 → 常见坑

这样的思路去整理。

如果你现在正处在研究生阶段,或者刚开始系统学机器学习,经常有这种感觉:代码我会跑,但别人一问为什么,我就说不清楚。

扫码可以加入:

到了后面你会发现,不管是做科研、写论文、找实习,还是继续学深度学习、大模型,很多判断能力,其实都是从这些基础模型一点点练出来的。

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 381
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读5.0k
粉丝0
内容381