哈喽,大家好~
今儿和大家聊聊数据预处理中,非常重要的一个方面:数据标准化。
机器学习里的“标准化”(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(0, 1, 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'] = (12, 8)
# 图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(32, 16),
nn.ReLU(),
nn.Linear(16, 1)
)
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范围固定但容易受异常值影响,而标准化只是预处理手段,不能完全替代特征工程。

