大数跨境

导师:“SVM、KNN、K-Means都调好了?”我:“Codex跑通了!”导师:“MinMaxScaler和StandardScaler,选哪个?为什么?”

导师:“SVM、KNN、K-Means都调好了?”我:“Codex跑通了!”导师:“MinMaxScaler和StandardScaler,选哪个?为什么?” 机器学习和人工智能AI
2026-11-02
2

哈喽,大家好~

很多同学在刚刚接触的时候,会有这么一个问题:为什么两个特征都很重要,模型却总盯着数值大的那个?

非常非常朴素的一个问题,很重要,今儿一文搞懂它~

而且这个问题在KNN、K-Means、神经网络中很常见。很多时候不是模型不够强,而是不同特征的量纲差距太大。

今天我们就聊一个简单又实用的预处理方法:最小-最大缩放,也就是Min-Max Scaling。

为什么要做归一化

假设我们用“年龄”和“年收入”判断用户是否购买产品:

  • 年龄范围是18~60;
  • 年收入范围是30000~500000。

计算两个用户的欧氏距离时,收入相差10000,年龄相差10。虽然年龄差异也很重要,但收入的数值更大,几乎会接管整个距离结果。

一句话概括的话,就是模型看到的是数字,不知道数字背后的单位。

最小-最大缩放做的事情,就是把不同特征压缩到相同范围。它不是机器学习模型,而是模型训练前的一步数据预处理。

核心原理

最常见的做法,是把数据缩放到 :

其中, 是原始值, 和 分别是该特征的最小值和最大值, 是缩放后的结果。

比如某个特征的范围是20~100,当 时:

也就是说,60正好位于整个范围的中间位置。

如果想缩放到任意区间 ,公式可以写成:

这里要注意,Min-Max缩放改变的是数值尺度,不会改变数据原来的大小顺序。

完整案例

我们生成两个特征。第一个特征保持正常尺度,第二个特征人为放大800倍,再使用KNN完成二分类。

KNN依赖样本之间的距离,因此特别适合观察归一化带来的变化。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

np.random.seed(42)

# 数据
n = 2700
z = np.random.normal(size=(n, 2))
noise = np.random.normal(0, 0.55, n)

score = z[:, 0] + 0.8 * z[:, 1] + \
        0.45 * np.sin(2 * z[:, 0]) + noise
y = (score > 0).astype(int)

# 第二个特征放大800倍
X = np.column_stack([z[:, 0], z[:, 1] * 800])

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, stratify=y, random_state=42
)

# 只使用训练集拟合缩放器
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

knn_raw = KNeighborsClassifier(n_neighbors=9)
knn_scaled = KNeighborsClassifier(n_neighbors=9)

knn_raw.fit(X_train, y_train)
knn_scaled.fit(X_train_scaled, y_train)

raw_acc = accuracy_score(y_test, knn_raw.predict(X_test))
scaled_acc = accuracy_score(
    y_test, knn_scaled.predict(X_test_scaled)
)

print(f"归一化前准确率:{raw_acc:.3f}")
print(f"归一化后准确率:{scaled_acc:.3f}")

关键点在这里:

scaler.fit(X_train)

最小值和最大值只能从训练集学习。如果在全部数据上执行fit_transform(),测试集信息就提前泄露给模型了。

决策边界发生了什么

下面画出归一化前后的KNN决策区域。背景代表模型预测类别,散点代表真实样本。

def draw_boundary(ax, model, X_data, y_data, title):
    x1_min, x1_max = X_data[:, 0].min(), X_data[:, 0].max()
    x2_min, x2_max = X_data[:, 1].min(), X_data[:, 1].max()

    xx, yy = np.meshgrid(
        np.linspace(x1_min, x1_max, 240),
        np.linspace(x2_min, x2_max, 240)
    )
    grid = np.c_[xx.ravel(), yy.ravel()]
    pred = model.predict(grid).reshape(xx.shape)

    bg = ListedColormap(["#28D7FE", "#FF4D8D"])
    points = ListedColormap(["#0066FF", "#FF005C"])

    ax.contourf(xx, yy, pred, alpha=0.28, cmap=bg)
    ax.contour(xx, yy, pred, colors="white",
               linewidths=1.2, alpha=0.9)
    ax.scatter(
        X_data[:, 0], X_data[:, 1],
        c=y_data, cmap=points,
        edgecolor="white", s=32, alpha=0.9
    )
    ax.set_title(title, fontweight="bold")
    ax.set_xlabel("Feature 1")
    ax.set_ylabel("Feature 2")

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

draw_boundary(
    axes[0], knn_raw, X_test, y_test,
    f"Before Scaling | Accuracy={raw_acc:.3f}"
)
draw_boundary(
    axes[1], knn_scaled, X_test_scaled, y_test,
    f"After Scaling | Accuracy={scaled_acc:.3f}"
)

plt.suptitle("KNN Decision Boundary Comparison",
             fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()

第一张图中,第二个特征跨度极大,距离计算被它主导,决策边界会更偏向单一方向。

第二张图中,两个特征都进入 ,KNN能够同时利用它们。注意,归一化没有创造新信息,只是让原有信息获得了更公平的参与机会。

再看模型稳定性

只比较一次准确率还不够。我们继续改变邻居数量 ,同时分析两个特征对距离的贡献。

ks = range(1, 32, 2)
raw_scores, scaled_scores = [], []

for k in ks:
    m1 = KNeighborsClassifier(n_neighbors=k).fit(
        X_train, y_train
    )
    m2 = KNeighborsClassifier(n_neighbors=k).fit(
        X_train_scaled, y_train
    )

    raw_scores.append(m1.score(X_test, y_test))
    scaled_scores.append(m2.score(X_test_scaled, y_test))

# 随机抽取样本对,计算各特征对平方距离的贡献
rng = np.random.default_rng(42)
i = rng.integers(0, len(X_train), 3000)
j = rng.integers(0, len(X_train), 3000)

raw_diff = (X_train[i] - X_train[j]) ** 2
scaled_diff = (
    X_train_scaled[i] - X_train_scaled[j]
) ** 2

raw_share = raw_diff.mean(axis=0)
raw_share /= raw_share.sum()

scaled_share = scaled_diff.mean(axis=0)
scaled_share /= scaled_share.sum()

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

axes[0].plot(
    ks, raw_scores, "o-", color="#FF4D8D",
    linewidth=2.5, label="Before scaling"
)
axes[0].plot(
    ks, scaled_scores, "o-", color="#00C9A7",
    linewidth=2.5, label="After scaling"
)
axes[0].set_xlabel("Number of Neighbors K")
axes[0].set_ylabel("Test Accuracy")
axes[0].set_title("Accuracy Under Different K")
axes[0].grid(alpha=0.25)
axes[0].legend()

x = np.arange(2)
width = 0.35
axes[1].bar(
    x - width / 2, raw_share, width,
    color="#FF8A00", label="Before scaling"
)
axes[1].bar(
    x + width / 2, scaled_share, width,
    color="#7B61FF", label="After scaling"
)
axes[1].set_xticks(x)
axes[1].set_xticklabels(["Feature 1", "Feature 2"])
axes[1].set_ylabel("Contribution to Squared Distance")
axes[1].set_title("Feature Distance Contribution")
axes[1].legend()

plt.tight_layout()
plt.show()

左图观察的是不同 值下的测试集准确率。如果归一化后的曲线更高、更平稳,说明模型对参数选择没那么敏感。

右图更直接:归一化前,第二个特征几乎承包了距离;归一化后,两个特征的距离贡献重新回到可比较的尺度。

注意项

Min-Max缩放适合KNN、K-Means、SVM和神经网络等对距离或梯度敏感的方法。决策树、随机森林主要比较特征切分点,对缩放并不敏感。

它还有一个明显短板:对异常值敏感。如果大部分数据在0~100之间,却混入一个10000,正常数据会被挤压到很小的区间。这时可以先处理异常值,或者改用RobustScaler。

训练集之外的新数据还可能超过原来的最小值和最大值,因此缩放结果不一定始终位于 。

需要强制截断时,大家可以使用:

MinMaxScaler(clip=True)

最后

最小-最大缩放的核心,就是把不同量纲的特征拉回同一条起跑线,让模型不会因为数值大小而偏爱某个特征。

大家可以继续尝试把它与StandardScaler、RobustScaler放在同一数据集上比较,也可以把案例中的KNN替换成SVM或神经网络,观察决策边界和收敛速度的变化。归一化不是固定流程,最终还是要结合模型原理和数据特点来选择。

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