大数跨境

面试官追问:“为什么KNN训练前必须先标准化?”我:“只是让数据更好看?”面试官:“距离尺度不统一,结果可能从第一步就错了!”

面试官追问:“为什么KNN训练前必须先标准化?”我:“只是让数据更好看?”面试官:“距离尺度不统一,结果可能从第一步就错了!” 机器学习和人工智能AI
2026-07-29
1
导读:哈喽,大家好~今儿和大家聊聊数据预处理中,非常重要的一个方面:数据标准化。

哈喽,大家好~

今儿和大家聊聊数据预处理中,非常重要的一个方面:数据标准化

机器学习里的“标准化”(standardization)就是把每一列特征都按“平均值为 0、方差为 1”的尺度拉平,让不同量纲、不同分布的特征变得“同一口径”来参与模型训练。

这样做能让模型训练更稳定、更快、参数更新更合理,同时也能提升一些基于距离或正则化方法的效果。

为什么要做标准化?

不同特征的量级可能差很多,比如“年龄”只在几十范围,而“收入”可能是上万,上亿。若不标准化,数值大的特征会主导梯度与距离,模型会偏向“看数大”的特征。

很多优化算法(比如梯度下降、Adam)在不同维度上学习率表现不同,标准化能让每个维度都在相似尺度上更新,训练更快更稳定。

一些方法(如 KNN、KMeans、PCA、正则化模型等)非常依赖尺度,标准化能让距离和正则项有意义。

逐特征(按列)计算训练集上的均值   和标准差 

标准化公式(Z-score):

实际实现中常加一个很小的数   防止除以 0:

还原(inverse transform):

大家要注意,均值与方差只用训练集算,然后把这组参数( )应用于验证集/测试集,不能把验证/测试信息泄露到训练中。

对于含异常值(outliers)特别多的特征,标准差敏感;可考虑使用更稳健的缩放器(median & IQR)——RobustScaler。

实战案例

我们构造一个合成数据集,多维特征,尺度差异大且含少量异常值,比较“未标准化/标准化”下的数据分布、PCA 投影和模型训练曲线。

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import seaborn as sns

np.random.seed(42)
torch.manual_seed(42)

# 1) 数据集:两类分类问题,3 个特征:尺度差异大
N = 1000
# 基本特征:X1 在 [0,100], X2 在 [0,1], X3 在 [0,10000]
x1 = np.random.normal(loc=50, scale=10, size=(N,1))           # 中等尺度
x2 = np.random.normal(loc=0.5, scale=0.1, size=(N,1))        # 小尺度
x3 = np.random.normal(loc=2000, scale=500, size=(N,1))       # 大尺度

# 将特征合并
X = np.hstack([x1, x2, x3])

# 构造标签:一个非线性决策边界(使问题有一定难度)
# 例如通过线性组合 + 非线性噪声判定类别
scores = 0.03 * x1 - 2.0 * x2 + 0.0004 * x3 + 0.5 * np.sin(0.02 * x3)
# 转化为概率并取阈值
y = (scores.ravel() + np.random.normal(01, size=N) > np.median(scores)).astype(int)

# 人为加入少量异常值到 X 的第三列(大尺度特征)以观察对标准差的影响
n_outliers = 10
outlier_indices = np.random.choice(N, n_outliers, replace=False)
X[outlier_indices, 2] *= 6.0  # 放大一些点

# 划分训练/测试
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 转为 torch tensors
X_train_t = torch.from_numpy(X_train).float()
X_test_t = torch.from_numpy(X_test).float()
y_train_t = torch.from_numpy(y_train).float().unsqueeze(1)
y_test_t = torch.from_numpy(y_test).float().unsqueeze(1)

# 2) 计算训练集上的均值和标准差(按列)
eps = 1e-8
train_mean = X_train_t.mean(dim=0, keepdim=True)
train_std = X_train_t.std(dim=0, unbiased=False, keepdim=True)  # 用 N(非 N-1)

# 标准化函数
def standardize(tensor, mean, std, eps=1e-8):
    return (tensor - mean) / (std + eps)

# 对训练/测试分别做标准化(注意只用训练集的 mean/std)
X_train_std = standardize(X_train_t, train_mean, train_std, eps)
X_test_std = standardize(X_test_t, train_mean, train_std, eps)

# 3) 绘图准备:为了更直观显示,我们将做多个图
import matplotlib
matplotlib.rcParams['figure.figsize'] = (128)

# 图1:原始数据的 PCA 投影(用于可视化高维数据) - 原始尺度
pca = PCA(n_components=2)
proj_orig = pca.fit_transform(X)  # 使用全部数据做展示
plt.figure()
plt.scatter(proj_orig[:,0], proj_orig[:,1], c=y, cmap='Spectral', s=30, alpha=0.8)
plt.title("图1: 原始数据 PCA 投影(未标准化)")
plt.xlabel("PC1"); plt.ylabel("PC2")
plt.colorbar()
plt.tight_layout()
plt.show()

# 图2:每个特征的直方图(原始 vs 标准化对比)
fig, axes = plt.subplots(3,2, figsize=(14,10))
features = ['X1 (age-like)''X2 (ratio-like)''X3 (income-like)']
for i in range(3):
    # 原始
    axes[i,0].hist(X_train[:,i], bins=40, color='tomato', alpha=0.8)
    axes[i,0].set_title(f"{features[i]} - 原始分布")
    # 标准化后
    axes[i,1].hist(X_train_std[:,i].numpy(), bins=40, color='royalblue', alpha=0.8)
    axes[i,1].set_title(f"{features[i]} - 标准化后 (mean={train_mean[0,i]:.2f}, std={train_std[0,i]:.2f})")
plt.tight_layout()
plt.show()

# 图3:箱线图(展示异常值和尺度)
plt.figure(figsize=(12,5))
plt.subplot(1,2,1)
plt.boxplot([X_train[:,0], X_train[:,1], X_train[:,2]], labels=features)
plt.title("图3a: 原始特征箱线图(尺度差异明显,异常值可见)")
plt.subplot(1,2,2)
plt.boxplot([X_train_std[:,0].numpy(), X_train_std[:,1].numpy(), X_train_std[:,2].numpy()], labels=features)
plt.title("图3b: 标准化后特征箱线图(尺度统一,异常值仍存在但更显著于 z-score)")
plt.tight_layout()
plt.show()

# 图4:PCA 投影:比较原始 vs 标准化
pca_orig = PCA(n_components=2).fit_transform(X_train)
pca_std = PCA(n_components=2).fit_transform(X_train_std.numpy())
plt.figure(figsize=(12,5))
plt.subplot(1,2,1)
plt.scatter(pca_orig[:,0], pca_orig[:,1], c=y_train, cmap='Accent', s=30, alpha=0.8)
plt.title("图4a: 原始尺度下的 PCA 投影(训练集)")
plt.subplot(1,2,2)
plt.scatter(pca_std[:,0], pca_std[:,1], c=y_train, cmap='Accent', s=30, alpha=0.8)
plt.title("图4b: 标准化后的 PCA 投影(训练集)")
plt.tight_layout()
plt.show()

# 4) 简单模型训练:比较未标准化 vs 标准化
class SimpleNet(nn.Module):
    def __init__(self, in_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, 32),
            nn.ReLU(),
            nn.Linear(3216),
            nn.ReLU(),
            nn.Linear(161)
        )
    def forward(self, x):
        return self.net(x)

def train_model(Xtr, ytr, Xte, yte, epochs=100, lr=1e-3):
    model = SimpleNet(Xtr.shape[1])
    opt = optim.Adam(model.parameters(), lr=lr)
    criterion = nn.BCEWithLogitsLoss()
    history = {'train_loss':[], 'test_loss':[], 'train_acc':[], 'test_acc':[]}
    for ep in range(epochs):
        model.train()
        logits = model(Xtr)
        loss = criterion(logits, ytr)
        opt.zero_grad(); loss.backward(); opt.step()
        # 评估
        model.eval()
        with torch.no_grad():
            train_pred = (torch.sigmoid(model(Xtr))>0.5).float()
            test_pred = (torch.sigmoid(model(Xte))>0.5).float()
            train_acc = (train_pred==ytr).float().mean().item()
            test_acc = (test_pred==yte).float().mean().item()
            test_loss = criterion(model(Xte), yte).item()
        history['train_loss'].append(loss.item())
        history['test_loss'].append(test_loss)
        history['train_acc'].append(train_acc)
        history['test_acc'].append(test_acc)
    return history

# 训练两个模型
hist_raw = train_model(X_train_t, y_train_t, X_test_t, y_test_t, epochs=200, lr=1e-3)
hist_std = train_model(X_train_std, y_train_t, X_test_std, y_test_t, epochs=200, lr=1e-3)

# 图5:训练损失与准确率对比(标准化前后)
plt.figure(figsize=(12,8))
plt.subplot(2,1,1)
plt.plot(hist_raw['train_loss'], color='coral', label='train loss (raw)')
plt.plot(hist_raw['test_loss'], color='orangered', linestyle='--', label='test loss (raw)')
plt.plot(hist_std['train_loss'], color='royalblue', label='train loss (std)')
plt.plot(hist_std['test_loss'], color='navy', linestyle='--', label='test loss (std)')
plt.legend(); plt.title("图5a: 训练/测试损失对比(原始 vs 标准化)")
plt.subplot(2,1,2)
plt.plot(hist_raw['train_acc'], color='coral', label='train acc (raw)')
plt.plot(hist_raw['test_acc'], color='orangered', linestyle='--', label='test acc (raw)')
plt.plot(hist_std['train_acc'], color='royalblue', label='train acc (std)')
plt.plot(hist_std['test_acc'], color='navy', linestyle='--', label='test acc (std)')
plt.legend(); plt.title("图5b: 训练/测试准确率对比(原始 vs 标准化)")
plt.tight_layout()
plt.show()

原始数据 PCA 投影

对高维(3 维)数据做 PCA,投影到二维以便观察整体类分布。

由于一个特征的尺度非常大(X3),PCA 的主成分很可能被该特征主导,导致投影方向主要反映大尺度特征的变化。颜色代表真实类别,可以观察类别是否可分,但投影可能受尺度偏倚影响。

每个特征的直方图

对比展示原始值分布和标准化后的分布。

  • 原始图显示三个特征尺度差异巨大:X1 ~ 50、X2 ~ 0.5、X3 ~ 2000。
  • 标准化后,三者都被拉到均值约 0,方差约 1。对于含异常值的特征(X3),标准化后会出现较大的偏移或尾部仍存在,但尺度统一了。

箱线图

展示每列数据的中位数、IQR(四分位间距)和异常点(outliers)。

  • 原始箱线图中 X3 的尺度和离群点非常明显。
  • 标准化后箱体在 y 轴上的范围统一到了大约 [-3, 3],但异常值仍存在(只不过更容易以标准差单位被检测出来)。这说明标准化不能消除异常值,只是让尺度一致,异常值的“相对大小”会更突出。

PCA 投影

比较在做 PCA 时是否先做标准化对主成分方向的影响。

  • 原始尺度下 PCA 可能被大尺度特征(X3)主导,两个类别在投影上可能不是最理想的分离。

  • 标准化后 PCA 能更公平地考虑每个特征的贡献,投影方向会改变,类别可能更易被分开或展示了其他结构。

  • 要点:在做 PCA、聚类或任何基于方差/距离的方法时,通常先做标准化,除非特征本身有物理意义并应保留尺度。

训练损失与准确率

训练同样结构的神经网络,比较在未标准化与标准化数据下训练曲线(损失与准确率)。

  • 通常你会看到标准化后的模型训练更快、损失下降更稳定,测试准确率也可能更高或更平滑。未标准化时梯度可能震荡、收敛慢,或需要更小的学习率才能稳定训练。
  • 在我们的示例中(实际结果受随机性影响),通常标准化曲线比原始更平滑且早期达到更好准确率。

总结

总的来说,标准化时一定只用训练集计算均值和标准差,再复用到验证集、测试集和部署阶段,避免数据泄露。

Z-score更适合连续数值特征,异常值较多时可换成RobustScaler,偏态明显的特征可以先做对数或Box-Cox变换。Min-Max范围固定但容易受异常值影响,而标准化只是预处理手段,不能完全替代特征工程。

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