哈喽,大家好~
今儿和大家聊聊:数据预处理方法,特征缩放。
特征缩放作为预处理的核心步骤之一,直接影响距离度量类模型、基于梯度下降的优化以及带正则化的线性模型的性能、稳定性与收敛速度。
简单来说,特征缩放的目标是控制不同特征量纲的影响,使模型的学习过程更稳健、更公平、更高效。
为什么需要特征缩放
距离度量的公平性:
多数算法隐含地使用欧氏距离或内积作为度量与相似的定义。
若不同特征的量纲差异巨大,例如一个特征以“千”为单位,另一个以“个位”为单位,欧氏距离:
将被高量纲的特征主导,致使模型忽略其他特征的重要信息。
这会导致:
-
kNN分类器错误地选择近邻; -
K-means聚类的质心与簇划分被“尺度最大”的特征严重影响; -
基于核函数的SVM在相似度计算中产生偏置。
缩放后,各特征在距离计算中权重更加均衡,提升模型对多维信息的敏感度。
梯度下降法的收敛与条件数:
考虑凸二次优化,如线性回归的最小二乘问题的目标函数近似为:
其中 是海森矩阵(例如 )。梯度下降的收敛速度与条件数 密切相关。若特征尺度差异巨大, 会很大,导致梯度下降沿某些方向过快、另一些方向过慢,训练过程振荡或缓慢收敛。
标准化后, 更接近单位矩阵,其条件数得到改善,从而显著提升优化的收敛速度与稳定性。
正则化的公平性(L2/L1):
带正则化的线性模型通过惩罚项约束参数大小:
-
L2正则: -
L1正则:
如果特征尺度不同,参数的相对大小并不能公平反映特征的“重要性”:为了匹配一个尺度大的特征,模型可能需要学到较小的参数;
相反,对于尺度小的特征,参数可能较大。
这会导致正则化在不同特征上施加不公平的惩罚。缩放后的各特征在近似同一尺度下,“统一的惩罚”机制才更合理。
SVM间隔与缩放不变性的破坏:
线性SVM的原始问题是:
若对特征进行缩放 ,为了得到相同的分类结果,需要相应调整权重为 。然而由于正则化项是 ,在不同缩放下该项的相对大小会改变,导致最优解发生变化。
因此,缩放影响SVM的“间隔”,以及最优解的形状与方向。这就是为什么在SVM中强烈建议先做适当的缩放。
常见特征缩放方法
Z-Score标准化:
对每个维度 ,定义均值 与标准差 ,标准化变换为:
在矩阵形式中,设数据矩阵 ,均值向量 ,标准差对角矩阵 ,标准化为:
性质:
-
使各特征均值约为0、方差约为1; -
不一定改变数据分布的形状(非高斯数据仍然非高斯); -
对于梯度法、正则化、距离度量较友好。
最小-最大缩放:
将每个特征线性映射到固定区间(通常是 ):
或映射到 :
性质:
-
保持相对顺序; -
对异常值敏感(max/min极端值会改变缩放范围); -
在使用激活函数有界模型(如某些神经网络)或需要标准范围时常用。
稳健缩放:
使用统计上更抗异常值的指标:中位数与四分位间距 :
性质:
-
稳健性强,降低异常值影响; -
使数据居中于0附近但不保证方差为1; -
更适合重尾分布或频繁异常值的场景。
单位向量缩放:
对样本向量进行归一化(常用于文本特征TF-IDF):
L2归一化:
L1归一化:
性质:
-
使每个样本的整体尺度一致; -
更适合基于余弦相似度的任务; -
不改变各特征的相对比率。
对数变换与幂变换:
对正值特征,为降低偏度与重尾:
-
对数变换:
-
Box-Cox(要求正值):
-
Yeo-Johnson(可处理零和负值,常见于scikit-learn的PowerTransformer)。
性质:
-
改善偏度、使分布更近似高斯; -
常与标准化组合使用。
白化:
白化将协方差矩阵变为单位阵,使特征去相关、方差为1。设数据零均值,协方差 ,其中 为特征向量, 是特征值。PCA白化:
可验证:
ZCA白化(保留原数据方向感):
性质:
-
去相关、单位方差; -
对噪声敏感,需要正则化(如加 ); -
常用于视觉特征的预处理、某些深度学习管线。
归一化层:
深度学习中,归一化层在训练过程中动态缩放:
BatchNorm(按批统计):
LayerNorm(按通道/特征统计),在NLP与Transformer中常见。
性质:
-
加速训练、稳定梯度; -
在推理阶段使用移动平均统计量; -
与传统预处理缩放思想一致,但融入网络参数学习与动态性。
完整案例
数据集
这里,我们构造一个三分类问题(类别A/B/C),包含6个特征,特征分布有意引入尺度差异与重尾分布,以及异常值。
-
f1:正态分布(中等尺度),不同类均值略有差异; -
f2:大尺度正态分布(均值在千级),作为“量纲巨大”的特征; -
f3:对数正态(重尾)并手动引入少量极端异常值; -
f4:Poisson计数型(非负,偏度较大); -
f5:二元特征(0/1),影响有限但与其他特征相关; -
f6:与f1线性相关(加入噪声),模拟共线性。
目标:比较不同缩放策略(无缩放、StandardScaler、MinMaxScaler、RobustScaler)对模型训练(SVM、KNN、逻辑回归)的影响,展示分布变化、决策边界与优化收敛。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.decomposition import PCA
from sklearn.metrics import accuracy_score, f1_score, classification_report, confusion_matrix
from sklearn.pipeline import Pipeline
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
np.random.seed(42)
torch.manual_seed(42)
# 1) 数据集:三类,六个特征
def generate_data(n_per_class=500):
# 类A
f1_A = np.random.normal(loc=50, scale=10, size=n_per_class)
f2_A = np.random.normal(loc=1200, scale=200, size=n_per_class) # 大尺度
f3_A = np.random.lognormal(mean=2.0, sigma=0.5, size=n_per_class)
f4_A = np.random.poisson(lam=8, size=n_per_class)
f5_A = np.random.binomial(n=1, p=0.3, size=n_per_class)
f6_A = f1_A * 0.8 + np.random.normal(loc=0, scale=5, size=n_per_class) # 与f1相关
# 类B
f1_B = np.random.normal(loc=60, scale=12, size=n_per_class)
f2_B = np.random.normal(loc=1500, scale=220, size=n_per_class)
f3_B = np.random.lognormal(mean=2.2, sigma=0.6, size=n_per_class)
f4_B = np.random.poisson(lam=12, size=n_per_class)
f5_B = np.random.binomial(n=1, p=0.5, size=n_per_class)
f6_B = f1_B * 0.85 + np.random.normal(loc=0, scale=6, size=n_per_class)
# 类C
f1_C = np.random.normal(loc=55, scale=9, size=n_per_class)
f2_C = np.random.normal(loc=1000, scale=180, size=n_per_class)
f3_C = np.random.lognormal(mean=1.8, sigma=0.7, size=n_per_class)
f4_C = np.random.poisson(lam=5, size=n_per_class)
f5_C = np.random.binomial(n=1, p=0.4, size=n_per_class)
f6_C = f1_C * 0.75 + np.random.normal(loc=0, scale=4, size=n_per_class)
# 汇总
X_A = np.vstack([f1_A, f2_A, f3_A, f4_A, f5_A, f6_A]).T
X_B = np.vstack([f1_B, f2_B, f3_B, f4_B, f5_B, f6_B]).T
X_C = np.vstack([f1_C, f2_C, f3_C, f4_C, f5_C, f6_C]).T
X = np.vstack([X_A, X_B, X_C])
y = np.array([0]*n_per_class + [1]*n_per_class + [2]*n_per_class)
# 注入异常值:在f2和f3中添加部分极端值
n_outliers = int(0.02 * X.shape[0])
outlier_idx = np.random.choice(X.shape[0], size=n_outliers, replace=False)
X[outlier_idx, 1] *= np.random.uniform(2.5, 4.0, size=n_outliers) # 放大f2
X[outlier_idx, 2] *= np.random.uniform(3.0, 5.0, size=n_outliers) # 放大f3
columns = ['f1', 'f2', 'f3', 'f4', 'f5', 'f6']
df = pd.DataFrame(X, columns=columns)
df['y'] = y
return df
df = generate_data(n_per_class=500)
X = df[['f1', 'f2', 'f3', 'f4', 'f5', 'f6']].values
y = df['y'].values
# 2) 划分训练和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
# 3) 定义缩放器
scalers = {
'none': None,
'standard': StandardScaler(),
'minmax': MinMaxScaler(),
'robust': RobustScaler()
}
# 4) 模型:SVM与kNN,比较不同缩放器
def evaluate_models_with_scalers(X_train, y_train, X_test, y_test, scalers):
results = []
for name, scaler in scalers.items():
if scaler isnotNone:
scaler.fit(X_train)
Xtr = scaler.transform(X_train)
Xte = scaler.transform(X_test)
else:
Xtr = X_train.copy()
Xte = X_test.copy()
# SVM
svm = SVC(kernel='rbf', gamma='scale', C=1.0, random_state=42)
svm.fit(Xtr, y_train)
y_pred_svm = svm.predict(Xte)
acc_svm = accuracy_score(y_test, y_pred_svm)
f1_svm = f1_score(y_test, y_pred_svm, average='macro')
# KNN
knn = KNeighborsClassifier(n_neighbors=7)
knn.fit(Xtr, y_train)
y_pred_knn = knn.predict(Xte)
acc_knn = accuracy_score(y_test, y_pred_knn)
f1_knn = f1_score(y_test, y_pred_knn, average='macro')
results.append({
'scaler': name,
'svm_acc': acc_svm, 'svm_f1': f1_svm,
'knn_acc': acc_knn, 'knn_f1': f1_knn
})
return pd.DataFrame(results)
results_df = evaluate_models_with_scalers(X_train, y_train, X_test, y_test, scalers)
print("缩放器对SVM与KNN性能的影响:")
print(results_df)
# 5) PCA降维用于决策边界可视化(2D)
def pca_2d_transform(X_train, X_test, scaler):
if scaler isnotNone:
scaler.fit(X_train)
Xtr = scaler.transform(X_train)
Xte = scaler.transform(X_test)
else:
Xtr = X_train.copy()
Xte = X_test.copy()
pca = PCA(n_components=2, random_state=42)
pca.fit(Xtr)
return pca.transform(Xtr), pca.transform(Xte), pca
# 6) PyTorch逻辑回归(多类)用于收敛曲线展示
class LogisticRegressionTorch(nn.Module):
def __init__(self, in_features, num_classes):
super().__init__()
self.linear = nn.Linear(in_features, num_classes)
def forward(self, x):
return self.linear(x)
def train_logreg_torch(X_train, y_train, X_test, y_test, scaler, num_epochs=100, lr=0.05, batch_size=64):
# 缩放
if scaler isnotNone:
scaler.fit(X_train)
Xtr = scaler.transform(X_train)
Xte = scaler.transform(X_test)
else:
Xtr = X_train.copy()
Xte = X_test.copy()
# 张量
Xtr_t = torch.tensor(Xtr, dtype=torch.float32)
ytr_t = torch.tensor(y_train, dtype=torch.long)
Xte_t = torch.tensor(Xte, dtype=torch.float32)
yte_t = torch.tensor(y_test, dtype=torch.long)
dataset = TensorDataset(Xtr_t, ytr_t)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
model = LogisticRegressionTorch(in_features=X_train.shape[1], num_classes=len(np.unique(y_train)))
optimizer = optim.SGD(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
losses = []
accs = []
for epoch in range(num_epochs):
model.train()
epoch_loss = 0.0
for xb, yb in loader:
optimizer.zero_grad()
logits = model(xb)
loss = criterion(logits, yb)
loss.backward()
optimizer.step()
epoch_loss += loss.item() * xb.shape[0]
epoch_loss /= len(loader.dataset)
losses.append(epoch_loss)
# 评估
model.eval()
with torch.no_grad():
logits_te = model(Xte_t)
preds_te = torch.argmax(logits_te, dim=1)
acc = (preds_te == yte_t).float().mean().item()
accs.append(acc)
return losses, accs
# 7) 可视化
def plot_complex_figure(df, X_train, y_train, X_test, y_test, scalers):
plt.figure(figsize=(9, 4))
# 子图1:原始数据的分布分析(选f2、f3、f6),各类别KDE叠加
ax1 = plt.subplot(2, 2, 1)
colors = ['#FF006E', '#3A86FF', '#FFBE0B']
for i, feat in enumerate(['f2', 'f3', 'f6']):
for cls in [0,1,2]:
sns.kdeplot(
df.loc[df['y']==cls, feat],
ax=ax1,
color=colors[cls],
lw=2,
label=f'{feat}-class{cls}'if i==0elseNone,
alpha=0.7
)
ax1.set_title('子图1:未缩放下的关键特征分布(f2、f3、f6)', fontsize=12)
ax1.set_xlabel('值域')
ax1.set_ylabel('密度')
ax1.legend(loc='upper right', ncol=2)
# 子图2:不同缩放器后的分布对比(箱线图,选f2、f3、f6)
ax2 = plt.subplot(2, 2, 2)
feat_sel = ['f2', 'f3', 'f6']
plot_data = []
for name, scaler in scalers.items():
if scaler isnotNone:
scaler.fit(X_train)
Xtr = scaler.transform(X_train)
else:
Xtr = X_train.copy()
df_tr = pd.DataFrame(Xtr, columns=['f1', 'f2', 'f3', 'f4', 'f5', 'f6'])
for f in feat_sel:
vals = df_tr[f].values
for v in vals:
plot_data.append({'scaler': name, 'feature': f, 'value': v})
box_df = pd.DataFrame(plot_data)
sns.boxplot(
data=box_df, x='feature', y='value', hue='scaler',
palette=['#8338EC', '#FB5607', '#00F5D4', '#FF006E'], ax=ax2
)
ax2.set_title('子图2:缩放器对分布的影响(箱线图比较 f2、f3、f6)', fontsize=12)
ax2.set_xlabel('特征')
ax2.set_ylabel('缩放后数值')
ax2.legend(loc='upper right')
# 子图3:PCA降至2D后的SVM决策边界(叠加不同缩放器)
ax3 = plt.subplot(2, 2, 3)
scaler_colors = {
'none': '#D00000',
'standard': '#3A86FF',
'minmax': '#FFBE0B',
'robust': '#2EC4B6'
}
# 显示训练样本散点(类别)
X_all = np.vstack([X_train, X_test])
y_all = np.hstack([y_train, y_test])
pca = PCA(n_components=2, random_state=42).fit(StandardScaler().fit_transform(X_all))
Xtr_std = StandardScaler().fit_transform(X_train)
Xte_std = StandardScaler().fit_transform(X_test)
Xtr_pca = pca.transform(Xtr_std)
Xte_pca = pca.transform(Xte_std)
# 用标准化+PCA的空间绘制点(保持视觉一致)
for cls, c in zip([0,1,2], colors):
idx_tr = (y_train == cls)
ax3.scatter(Xtr_pca[idx_tr,0], Xtr_pca[idx_tr,1], color=c, s=20, alpha=0.6, label=f'class{cls}-train')
ax3.set_title('子图3:PCA(2D)空间中的SVM决策边界(不同缩放器)', fontsize=12)
ax3.set_xlabel('PC1')
ax3.set_ylabel('PC2')
# 在同一个PCA坐标系中叠加不同缩放器训练出的边界(使用投影一致:先各自缩放->各自PCA->将网格通过逆变换映射)
# 简化:在标准化+PCA空间中画网格,分别训练各缩放器->用线性判别边界的轮廓近似叠加(采用SVM在该空间重新训练)
# 注意:此处为演示可比较性,严格对应空间略复杂;我们统一在标准化+PCA空间下训练SVM以对比缩放器带来的间接影响。
grid_x1 = np.linspace(Xtr_pca[:,0].min()-1, Xtr_pca[:,0].max()+1, 300)
grid_x2 = np.linspace(Xtr_pca[:,1].min()-1, Xtr_pca[:,1].max()+1, 300)
xx, yy = np.meshgrid(grid_x1, grid_x2)
grid = np.c_[xx.ravel(), yy.ravel()]
# 为了公平比较,我们对每个缩放器:先缩放原始数据,再用PCA(2D)拟合,并在对应PCA空间中训练SVM;
# 然后把它们的决策函数在各自PCA空间中求值。
for name, scaler in scalers.items():
if scaler isnotNone:
scaler.fit(X_train)
Xtr_s = scaler.transform(X_train)
else:
Xtr_s = X_train.copy()
pca_s = PCA(n_components=2, random_state=42).fit(Xtr_s)
Xtr_s_pca = pca_s.transform(Xtr_s)
svm = SVC(kernel='rbf', gamma='scale', C=1.0, random_state=42)
svm.fit(Xtr_s_pca, y_train)
# 为了叠加到统一图中,我们近似使用同一网格(标准化PCA空间的grid),
# 并用一个线性仿射对齐(近似):将标准化PCA空间中的grid点逆投影到原空间,
# 再用当前缩放器和PCA投影到当前空间下求决策函数。
# 简单近似:用标准化PCA的逆变换回到标准化空间,再逆标准化到原空间。
# 注意:此近似会引入变形,但利于直观对比。
# 逆标准化PCA -> 标准化空间
# 构造:grid_std = pca.inverse_transform(grid)
grid_std = pca.inverse_transform(grid) # 回到标准化后的原特征空间
# 逆标准化 -> 原空间
std = StandardScaler().fit(X_all) # 用所有样本估计标准化,仅用于近似可视化
grid_orig = std.inverse_transform(grid_std)
# 应用当前缩放器和PCA -> 当前空间
if scaler isnotNone:
grid_s = scaler.transform(grid_orig)
else:
grid_s = grid_orig.copy()
grid_s_p = pca_s.transform(grid_s)
# 决策函数
Z = svm.decision_function(grid_s_p)
if Z.ndim == 1:
Z = Z.reshape(xx.shape)
# 二分类等值线(但我们是三类,decision_function将是对每个类别的距离;此处绘制零等值线近似分类边界)
cs = ax3.contour(xx, yy, Z, levels=[0], colors=[scaler_colors[name]], linewidths=2, linestyles='solid')
else:
# 多类:绘制每两类之间的分隔线(找每个类相对于其他类的0等值线近似)
# 这里简单绘制对每个类的得分差的0等值线,用更稀疏绘制以避免太拥挤
for k in range(Z.shape[1]):
Zk = Z[:, k].reshape(xx.shape)
try:
ax3.contour(xx, yy, Zk, levels=[0], colors=[scaler_colors[name]], linewidths=1.5, linestyles='dashed', alpha=0.7)
except Exception:
pass
ax3.legend(loc='lower left')
# 子图4:PyTorch逻辑回归的训练收敛曲线(不同缩放器)
ax4 = plt.subplot(2, 2, 4)
for name, scaler in scalers.items():
losses, accs = train_logreg_torch(X_train, y_train, X_test, y_test, scaler, num_epochs=120, lr=0.08, batch_size=64)
ax4.plot(losses, label=f'loss-{name}', lw=2)
ax4.set_title('子图4:逻辑回归(SGD)训练收敛曲线(不同缩放器)', fontsize=12)
ax4.set_xlabel('Epoch')
ax4.set_ylabel('Loss')
ax4.legend(loc='upper right')
plt.tight_layout()
plt.show()
plot_complex_figure(df, X_train, y_train, X_test, y_test, scalers)
# 8) 进一步的性能输出
# 使用交叉验证比较不同缩放器+SVM、KNN的稳定性
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for name, scaler in scalers.items():
if scaler isnotNone:
pipeline_svm = Pipeline([('scaler', scaler), ('clf', SVC(kernel='rbf', gamma='scale', C=1.0, random_state=42))])
pipeline_knn = Pipeline([('scaler', scaler), ('clf', KNeighborsClassifier(n_neighbors=7))])
else:
pipeline_svm = Pipeline([('clf', SVC(kernel='rbf', gamma='scale', C=1.0, random_state=42))])
pipeline_knn = Pipeline([('clf', KNeighborsClassifier(n_neighbors=7))])
scores_svm = cross_val_score(pipeline_svm, X, y, cv=cv, scoring='accuracy')
scores_knn = cross_val_score(pipeline_knn, X, y, cv=cv, scoring='accuracy')
print(f"{name} -> SVM CV acc: {scores_svm.mean():.4f} ± {scores_svm.std():.4f}, KNN CV acc: {scores_knn.mean():.4f} ± {scores_knn.std():.4f}")
未缩放下的关键特征分布:
展示了不同类别在关键特征上的分布情况。f2(大尺度)与f3(重尾)显著影响整体度量,可能导致距离类算法不公平;f6与f1相关,说明共线性存在。
缩放器对分布的影响:
比较不同缩放器的缩放后分布形态,理解“标准化”“最小-最大”“稳健缩放”对偏度与离群点的不同处理方式。
PCA(2D)空间中的SVM决策边界:
直观展示不同缩放器对决策边界的影响。虽然同一数据集,但由于缩放改变了正则化与数据几何结构,SVM边界位置会发生改变。
逻辑回归(SGD)训练收敛曲线:
展示缩放对优化收敛速度与稳定性的影响。
总结
整体来看,缩放显著改善距离度量的公平性、优化稳定性与正则化的公平性;并非对所有模型无影响,尤其与SVM与梯度类模型结合时,缩放改变最优解的几何形状。
在应用中,通过一个有尺度差异、重尾与异常值的多类数据集,展示了不同缩放器(无缩放、Standard、MinMax、Robust)的效果,并通过SVM、KNN与PyTorch逻辑回归的训练与可视化进行验证。
最后

