查看全课大纲(第 13 / 35 节)

特征幅度缩放

约 6 分钟

小象实战讲义 · 特征工程精讲与实践

在机器学习建模中,不同特征的数值范围可能差异巨大。例如,年龄在0-100之间,而收入可能高达数万。这种量级差异会影响许多模型(如逻辑回归、神经网络)的收敛速度与性能。本节我们将学习特征幅度缩放的核心方法,通过代码实战掌握如何将特征缩放到统一尺度,并理解训练集与测试集在特征工程中的正确处理流程。

💡 核心导读

  • 为什么需要幅度缩放:消除特征间量纲差异,加速模型收敛,提升模型性能。
  • 三种核心缩放方法:标准化(Z-Score Scaling)、归一化(Min-Max Scaling)、鲁棒缩放(Robust Scaling)。
  • Scikit-learn 标准流程:fit(在训练集学习统计量)→ transform(应用变换)。
  • 工业实践关键:必须用训练集学到的规则去变换测试集,防止数据泄漏。

幅度缩放的意义与场景

特征幅度缩放(Feature Scaling)是特征预处理的关键步骤。其核心目标是调整特征的数值范围,使其落入一个相对统一的区间。这并非改变数据的分布形态,而是对数值进行线性变换。

主要应用场景:

  1. 基于距离的模型:如K近邻(KNN)、支持向量机(SVM)。特征值范围过大时,数值大的特征会主导距离计算。
  2. 梯度下降优化的模型:如线性回归、逻辑回归、神经网络。特征尺度统一能大幅加快梯度下降的收敛速度。
  3. 正则化模型:如岭回归(Ridge)、Lasso。正则化项对系数施加惩罚,若特征尺度不一,系数的尺度也无法公平比较。

相反,树模型(如决策树、随机森林、XGBoost)基于特征阈值进行分裂,对特征幅度不敏感,通常不需要进行缩放。

核心缩放方法原理

1. 标准化 (Standardization / Z-Score Scaling)

标准化将数据线性变换为均值为0、标准差为1;它不改变分布形态——仅当原数据本身近似正态分布时,变换后的结果才近似标准正态分布。 z=x−μσz = \frac{x - \mu}{\sigma} 其中,μ\mu 是特征xx的均值,σ\sigma 是特征xx的标准差。

  • 优点:多数模型(如逻辑回归、SVM、KNN)的默认选择;变换后各特征尺度可比。
  • 注意:基于均值与标准差计算,仍受异常值影响(敏感性中等——标准差经平方运算会被极端值显著拉大);数据含显著异常值时应改用鲁棒缩放。
  • 结果范围:理论上无界,但大部分值落在[-3, 3]区间。

2. 归一化 (Normalization / Min-Max Scaling)

归一化将数据线性变换到[0, 1]的固定区间。 xscaled=x−xmin⁡xmax⁡−xmin⁡x_{\text{scaled}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}} 其中,xmin⁡x_{\min} 和 xmax⁡x_{\max} 分别是特征xx的最小值和最大值。

  • 优点:严格限定输出范围,解释性强。
  • 缺点:对异常值非常敏感。一个极端大的异常值会将其他正常数据压缩到很小的区间。

3. 鲁棒缩放 (Robust Scaling)

鲁棒缩放使用中位数和四分位距(IQR)进行缩放,对异常值不敏感。 xrobust=x−median(x)IQR(x)x_{\text{robust}} = \frac{x - \text{median}(x)}{\text{IQR}(x)} 其中,median(x)\text{median}(x) 是中位数,IQR(x)=Q3−Q1\text{IQR}(x) = Q_3 - Q_1(75%分位数 - 25%分位数)。

  • 优点:抗异常值能力强,适用于包含离群点的数据。
  • 结果范围:无界。

代码实战:Scikit-learn 缩放流程

我们将在一个完整的机器学习数据流中演示三种缩放方法。关键原则:所有变换规则(均值、标准差、最小最大值等)必须仅从训练集学习,然后同时应用于训练集和测试集。

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

# 1. 构造一个可复现的微型泰坦尼克数据集样例(自包含,硬编码)
# 包含Pclass, Sex, Age, Fare, SibSp, Survived字段
data = pd.DataFrame({
    'Pclass': [3, 1, 3, 1, 3, 2, 3, 3],
    'Sex': ['male', 'female', 'female', 'female', 'male', 'male', 'female', 'male'],
    'Age': [22.0, 38.0, 26.0, 35.0, 35.0, 54.0, 2.0, 27.0],
    'Fare': [7.2500, 71.2833, 7.9250, 53.1000, 8.0500, 51.8625, 21.0750, 11.1333],
    'SibSp': [1, 1, 0, 1, 0, 0, 3, 0],
    'Survived': [0, 1, 1, 1, 0, 0, 1, 0]
})
print("原始数据集:")
print(data)
print("\n" + "="*50 + "\n")

# 2. 划分训练集和测试集(模拟真实项目流程)
# 使用Survived作为标签y,其他作为特征X
X = data.drop(columns=['Survived'])
y = data['Survived']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

print("训练集形状:", X_train.shape)
print("测试集形状:", X_test.shape)
print("\n训练集特征数据(用于学习变换规则):")
print(X_train)
print("\n测试集特征数据(将应用学到的规则):")
print(X_test)
原始数据集:
   Pclass     Sex   Age     Fare  SibSp  Survived
0       3    male  22.0   7.2500      1         0
1       1  female  38.0  71.2833      1         1
2       3  female  26.0   7.9250      0         1
3       1  female  35.0  53.1000      1         1
4       3    male  35.0   8.0500      0         0
5       2    male  54.0  51.8625      0         0
6       3  female   2.0  21.0750      3         1
7       3    male  27.0  11.1333      0         0

==================================================

训练集形状: (6, 5)
测试集形状: (2, 5)

训练集特征数据(用于学习变换规则):
   Pclass     Sex   Age     Fare  SibSp
0       3    male  22.0   7.2500      1
7       3    male  27.0  11.1333      0
2       3  female  26.0   7.9250      0
4       3    male  35.0   8.0500      0
3       1  female  35.0  53.1000      1
6       3  female   2.0  21.0750      3

测试集特征数据(将应用学到的规则):
   Pclass     Sex   Age     Fare  SibSp
1       1  female  38.0  71.2833      1
5       2    male  54.0  51.8625      0

方法一:标准化 (Z-Score Scaling)

# 重新导入确保自包含(实际写作时可省略,这里为演示)
from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np

# 重新准备与 train_test_split(random_state=42) 完全一致的训练集和测试集(自包含原则)
X_train = pd.DataFrame({
    'Pclass': [3, 3, 3, 3, 1, 3],
    'Sex': ['male', 'male', 'female', 'male', 'female', 'female'],
    'Age': [22.0, 27.0, 26.0, 35.0, 35.0, 2.0],
    'Fare': [7.2500, 11.1333, 7.9250, 8.0500, 53.1000, 21.0750],
    'SibSp': [1, 0, 0, 0, 1, 3]
})
X_test = pd.DataFrame({
    'Pclass': [1, 2],
    'Sex': ['female', 'male'],
    'Age': [38.0, 54.0],
    'Fare': [71.2833, 51.8625],
    'SibSp': [1, 0]
})

# 1. 初始化标准化缩放器
zscore_scaler = StandardScaler()

# 2. 在训练集上拟合:计算训练集Fare特征的均值(mu)和标准差(sigma)
zscore_scaler.fit(X_train[['Fare']])

# 3. 查看学习到的参数
print("【标准化】学习到的参数:")
print("  均值 (mu):", zscore_scaler.mean_[0])
print("  标准差 (sigma):", zscore_scaler.scale_[0])

# 4. 对训练集进行变换
X_train_zscore = X_train.copy()
X_train_zscore['Fare_zscore'] = zscore_scaler.transform(X_train[['Fare']])
print("\n训练集标准化后的结果:")
print(X_train_zscore[['Fare', 'Fare_zscore']])

# 5. 验证标准化结果
mean_after = X_train_zscore['Fare_zscore'].mean()
std_after = X_train_zscore['Fare_zscore'].std()
print(f"\n验证:标准化后Fare_zscore的均值: {mean_after:.10f}")
print(f"验证:标准化后Fare_zscore的标准差: {std_after:.6f}")

# 6. 使用训练集学到的scaler对测试集进行变换
X_test_zscore = X_test.copy()
X_test_zscore['Fare_zscore'] = zscore_scaler.transform(X_test[['Fare']])
print("\n测试集标准化后的结果(使用训练集的mu和sigma):")
print(X_test_zscore[['Fare', 'Fare_zscore']])
【标准化】学习到的参数:
  均值 (mu): 18.088883333333335
  标准差 (sigma): 16.353595004403637

训练集标准化后的结果:
      Fare  Fare_zscore
0   7.2500    -0.662783
1  11.1333    -0.425324
2   7.9250    -0.621508
3   8.0500    -0.613864
4  53.1000     2.140882
5  21.0750     0.182597

验证:标准化后Fare_zscore的均值: -0.0000000000
验证:标准化后Fare_zscore的标准差: 1.095445

测试集标准化后的结果(使用训练集的mu和sigma):
      Fare  Fare_zscore
0  71.2833     3.252766
1  51.8625     2.065211

💡 口径说明:StandardScaler 按总体标准差(ddof=0)标准化,因此缩放后数据的总体标准差为 1.0;验证打印出的 1.095445 来自 pandas 的 .std()(默认样本标准差,ddof=1)——对 6 个样本即 6/5≈1.0954\sqrt{6/5}\approx 1.0954。这是自由度口径差异,不是计算错误。

方法二:归一化 (Min-Max Scaling)

from sklearn.preprocessing import MinMaxScaler
import pandas as pd

# 重新准备与上一块一致的数据(自包含)
X_train = pd.DataFrame({'Fare': [7.2500, 11.1333, 7.9250, 8.0500, 53.1000, 21.0750]})
X_test = pd.DataFrame({'Fare': [71.2833, 51.8625]})

# 1. 初始化归一化缩放器
minmax_scaler = MinMaxScaler()

# 2. 在训练集上拟合:计算训练集Fare特征的最小值(min)和最大值(max)
minmax_scaler.fit(X_train[['Fare']])

# 3. 查看学习到的参数
print("【归一化】学习到的参数:")
print("  最小值 (min):", minmax_scaler.data_min_[0])
print("  最大值 (max):", minmax_scaler.data_max_[0])

# 4. 对训练集进行变换
X_train_minmax = X_train.copy()
X_train_minmax['Fare_minmax'] = minmax_scaler.transform(X_train[['Fare']])
print("\n训练集归一化后的结果:")
print(X_train_minmax[['Fare', 'Fare_minmax']])

# 5. 验证归一化结果
min_after = X_train_minmax['Fare_minmax'].min()
max_after = X_train_minmax['Fare_minmax'].max()
print(f"\n验证:归一化后Fare_minmax的最小值: {min_after}")
print(f"验证:归一化后Fare_minmax的最大值: {max_after}")

# 6. 对测试集进行变换
X_test_minmax = X_test.copy()
X_test_minmax['Fare_minmax'] = minmax_scaler.transform(X_test[['Fare']])
print("\n测试集归一化后的结果(使用训练集的min和max):")
print(X_test_minmax[['Fare', 'Fare_minmax']])
【归一化】学习到的参数:
  最小值 (min): 7.25
  最大值 (max): 53.1

训练集归一化后的结果:
      Fare  Fare_minmax
0   7.2500     0.000000
1  11.1333     0.084696
2   7.9250     0.014722
3   8.0500     0.017448
4  53.1000     1.000000
5  21.0750     0.301527

验证:归一化后Fare_minmax的最小值: 0.0
验证:归一化后Fare_minmax的最大值: 1.0

测试集归一化后的结果(使用训练集的min和max):
      Fare  Fare_minmax
0  71.2833     1.396582
1  51.8625     0.973010

注意一个细节:测试集的 71.2833 超出了训练集的最大值 53.1,因此它被变换到了 1.3966——落在了 [0, 1] 区间之外。这正是「用训练集规则变换测试集」的真实写照:未来的数据完全可能超出历史范围,缩放器只保证训练集落在目标区间,测试值的外推是正常现象,而不是错误。

方法三:鲁棒缩放 (Robust Scaling)

from sklearn.preprocessing import RobustScaler
import pandas as pd
import numpy as np

# 重新准备与上一块一致的数据(自包含)
X_train = pd.DataFrame({'Fare': [7.2500, 11.1333, 7.9250, 8.0500, 53.1000, 21.0750]})
X_test = pd.DataFrame({'Fare': [71.2833, 51.8625]})

# 1. 初始化鲁棒缩放器
robust_scaler = RobustScaler()

# 2. 在训练集上拟合:计算训练集Fare特征的中位数和IQR
robust_scaler.fit(X_train[['Fare']])

# 3. 查看学习到的参数
print("【鲁棒缩放】学习到的参数:")
print("  中位数 (median):", robust_scaler.center_[0])
print("  四分位距 (IQR):", robust_scaler.scale_[0])

# 4. 对训练集进行变换
X_train_robust = X_train.copy()
X_train_robust['Fare_robust'] = robust_scaler.transform(X_train[['Fare']])
print("\n训练集鲁棒缩放后的结果:")
print(X_train_robust[['Fare', 'Fare_robust']])

# 5. 对测试集进行变换
X_test_robust = X_test.copy()
X_test_robust['Fare_robust'] = robust_scaler.transform(X_test[['Fare']])
print("\n测试集鲁棒缩放后的结果(使用训练集的中位数和IQR):")
print(X_test_robust[['Fare', 'Fare_robust']])
【鲁棒缩放】学习到的参数:
  中位数 (median): 9.591650000000001
  四分位距 (IQR): 10.633325

训练集鲁棒缩放后的结果:
      Fare  Fare_robust
0   7.2500    -0.220218
1  11.1333     0.144983
2   7.9250    -0.156738
3   8.0500    -0.144983
4  53.1000     4.091698
5  21.0750     1.079940

测试集鲁棒缩放后的结果(使用训练集的中位数和IQR):
      Fare  Fare_robust
0  71.2833     5.801727
1  51.8625     3.975318

方法对比与选择指南

方法公式输出范围对异常值敏感性适用场景
标准化(x−μ)/σ(x - \mu) / \sigma无界(通常≈[-3,3])中等(受均值、标准差影响)特征近似正态分布;大多数模型默认选择
归一化(x−xmin⁡)/(xmax⁡−xmin⁡)(x - x_{\min}) / (x_{\max} - x_{\min})[0, 1](固定)高(min/max直接受异常值影响)需要严格限定范围;图像像素值处理
鲁棒缩放(x−median)/IQR(x - \text{median}) / \text{IQR}无界低(基于中位数和IQR)数据包含显著异常值;金融、生物医学数据

工业实践建议:

  1. 默认尝试标准化:适用于大多数场景,尤其是后续使用线性模型、神经网络时。
  2. 数据有异常值时用鲁棒缩放:能保证变换后的数据更稳定。
  3. 谨慎使用归一化:除非你确信数据没有异常值,或者业务要求特征必须在[0,1]区间。
  4. 树模型通常不需要缩放:但进行缩放也不会损害性能,有时为了一致性也会统一处理。

📝 动手练一练

  1. 概念辨析:假设一个数据集中“年龄”特征的范围是[0, 100],“年薪”特征的范围是[20000, 200000]。如果直接使用原始数据训练一个逻辑回归模型,可能会遇到什么问题?为什么?
👉 点击查看参考答案

问题:模型训练过程(梯度下降)可能收敛缓慢或不稳定,且特征重要性的解读会被尺度差异扭曲。

原因:逻辑回归使用梯度下降优化损失函数。特征尺度差异大会导致损失函数的等高线呈椭圆形,梯度下降路径会剧烈震荡,需要更多迭代步数才能收敛。此外,许多正则化方法(如L1/L2)平等地惩罚所有系数,尺度大的特征对应的系数自然会变小,但这并非因为该特征不重要,而是尺度差异导致的,从而影响特征重要性的解读。

  1. 代码实践:上面鲁棒缩放的例子中,我们计算了训练集Fare特征的中位数和IQR。请根据打印出的参数(中位数≈9.59,IQR≈10.63),手动计算测试集中第一个样本Fare=71.2833的鲁棒缩放结果,验证是否与代码输出一致。
👉 点击查看参考答案

手动计算: 公式:xrobust=x−medianIQRx_{\text{robust}} = \frac{x - \text{median}}{\text{IQR}}

代入测试集第一个样本值: xrobust=71.2833−9.5916510.633325=61.6916510.633325≈5.8017x_{\text{robust}} = \frac{71.2833 - 9.59165}{10.633325} = \frac{61.69165}{10.633325} \approx 5.8017

代码输出结果为5.801727,两者一致。同时可以看到,这个测试样本远超训练集的取值范围(训练集Fare最大仅53.1),因此鲁棒缩放后的值也远大于训练集中任何一个变换后的值——变换规则只对「学到的数据」负责,未来数据超出历史范围时输出也会相应外推。

本章小结

本节我们深入学习了特征工程中的基础但至关重要的技术——特征幅度缩放。我们理解了不同缩放方法(标准化、归一化、鲁棒缩放)的数学原理、适用场景及其在Scikit-learn中的标准实现流程。最重要的是,我们掌握了工业实践中必须先在训练集上拟合(fit)变换器,再将同一规则应用于测试集(transform) 的核心原则,这是防止数据泄漏、保证模型评估可靠性的关键。

📋 行动清单

  • 在你的下一个机器学习项目中,对连续型特征有意识地进行幅度缩放,并观察其对模型训练速度的影响。
  • 使用Scikit-learn的StandardScaler、MinMaxScaler、RobustScaler分别对同一数据集进行变换,比较变换后数据的分布与范围。
  • 牢记并实践“fit仅用于训练集,transform用于训练集和测试集”的工作流,养成编写可复现特征工程管道的习惯。

—— 小象教研组