NumPy 数据预处理:归一化与标准化的深度指南
引言:数据预处理的重要性
在机器学习和深度学习的领域中,数据是驱动模型性能的核心。然而,原始数据往往存在着各种问题:格式不统一、缺失值、噪声,以及最重要的——特征尺度差异巨大。想象一下,一个数据集中同时包含以“厘米”为单位的身高和以“元”为单位的月收入。身高可能在几十到两百之间波动,而月收入可能从几千到几十万甚至更高。如果直接将这些数据输入到许多机器学习算法中(如梯度下降、支持向量机、聚类等),收入这一特征的巨大数值范围将会主导整个计算过程,使得身高这一特征的影响被大大削弱,即便身高对于预测结果同样重要。
这就是数据预处理的重要性所在。数据预处理旨在将原始数据转换成更适合模型处理的格式和尺度,从而提升模型的性能、收敛速度和稳定性。数据预处理包括多个环节,如数据清洗(处理缺失值、异常值)、特征选择、特征工程,以及我们本文将重点探讨的特征缩放(Feature Scaling)。
特征缩放是改变数据数值范围或分布的一系列技术。其中最常见的两种是归一化(Normalization)和标准化(Standardization)。本文将深入探讨这两种技术,并详细介绍如何使用 Python 中最核心的科学计算库 NumPy 来高效地实现它们。
为什么选择 NumPy?NumPy 是 Python 生态系统中进行数值计算的基础库。它提供了高性能的多维数组对象以及大量的数学函数来操作这些数组。虽然有更高级的库如 scikit-learn 提供了封装好的Scaler类(如 MinMaxScaler
和 StandardScaler
),但理解底层实现原理、掌握如何使用 NumPy 直接进行计算,对于深入理解数据处理过程、进行自定义操作以及在没有其他库依赖的场景下执行任务至关重要。NumPy 的操作是向量化和广播的,这使得批量处理数据非常高效,远超传统的 Python 循环。
本文将带领读者:
1. 理解归一化(Min-Max Scaling)和标准化(Z-score Scaling)的核心概念、数学原理及适用场景。
2. 学习如何使用 NumPy 提供的函数(如 min
, max
, mean
, std
)计算缩放所需的参数。
3. 通过详细的代码示例,展示如何使用 NumPy 实现单维和多维数据的归一化与标准化。
4. 探讨在实际应用中使用 NumPy 进行缩放时需要注意的高级问题,如处理多维数据、处理缺失值、避免数据泄露(在训练集和测试集上的应用)。
5. 简要对比使用 NumPy 手动实现与使用 scikit-learn 库的优缺点。
第一部分:理解归一化与标准化
尽管在中文语境中,“归一化”有时被广义地用来指代所有特征缩放方法,但在严格意义上,归一化(Normalization)通常特指 Min-Max Scaling,而标准化(Standardization)通常特指 Z-score Scaling。理解它们的区别对于选择合适的预处理方法至关重要。
1. 归一化(Normalization,Min-Max Scaling)
归一化(Min-Max Scaling)是将原始数据线性地映射到指定的一个范围内,通常是 [0, 1] 或 [-1, 1]。其目的是消除量纲影响,并确保所有特征值落在可比较的范围内。
其数学公式如下(映射到 [0, 1] 范围):
对于数据集中的任一特征 x,其归一化后的值 x_norm 计算为:
$$ x_{norm} = \frac{x – x_{min}}{x_{max} – x_{min}} $$
其中,$x_{min}$ 是该特征的最小值,$x_{max}$ 是该特征的最大值。
特点与适用场景:
* 特点: 将数据严格限定在一个固定范围内,例如 [0, 1]。这对于某些算法(如神经网络,激活函数可能对输入范围敏感)或需要将特征值解释为概率的场景很有用。它保留了原始数据的所有顺序关系。
* 敏感性: 对异常值非常敏感。如果数据中存在极端值,那么 $x_{max} – x_{min}$ 会非常大,导致大部分非异常值被压缩到一个非常小的范围内,区分度降低。
* 适用场景:
* 数据的分布范围已知且稳定,或者不包含极端异常值。
* 算法对输入范围有明确要求(如某些图像处理或像素值)。
* 数据分布不是高斯分布(正态分布)。
2. 标准化(Standardization,Z-score Scaling)
标准化(Z-score Scaling)是将原始数据转换成均值为 0、标准差为 1 的分布。它通过减去均值并除以标准差来实现。这个过程也被称为 Z-score 变换。
其数学公式如下:
对于数据集中的任一特征 x,其标准化后的值 x_std 计算为:
$$ x_{std} = \frac{x – \mu}{\sigma} $$
其中,$\mu$ 是该特征的均值(Mean),$\sigma$ 是该特征的标准差(Standard Deviation)。
特点与适用场景:
* 特点: 标准化后的数据分布中心在 0,标准差为 1。它不会将数据限定在某个固定范围内,转换后的数值可能大于 1 或小于 0。标准化后的数据保留了原始数据的相对差异和分布形状(不一定是正态分布,但如果是正态分布,标准化后仍是正态分布)。
* 敏感性: 虽然异常值会影响均值和标准差的计算,但标准化本身不会像归一化那样将非异常值压缩到很小的范围。标准化后的异常值仍然会是较大的或较小的数值。
* 适用场景:
* 算法假设数据是零均值和单位方差的(如线性回归、逻辑回归、支持向量机、主成分分析 PCA 等许多基于距离度量的算法)。
* 数据分布近似于高斯分布(虽然不是强制要求,但效果可能更好)。
* 存在一些极端值,且你不想让它们过度压缩正常数据的范围。
3. 何时选择哪种方法?
选择归一化还是标准化取决于具体的算法和数据的特点:
* 如果你的数据有明显的上下界,或者你需要将数据缩放到一个特定的范围(如神经网络的输入层),并且数据中没有极端异常值,归一化可能是更好的选择。
* 如果你的数据分布近似正态分布,或者算法对特征的绝对范围不敏感,但对它们的相对大小和分布形状更关注,并且可能存在异常值,标准化通常是更稳健的选择。
* 对于许多基于距离或梯度的算法(如 SVM、KNN、线性回归、逻辑回归、神经网络、PCA 等),标准化往往是首选,因为它可以避免某些特征由于其值域较大而对距离或梯度产生不成比例的影响。
* 基于树的模型(如决策树、随机森林、梯度提升树)通常对特征的尺度不敏感,因为它们依赖于特征值的顺序进行分裂,而不是其绝对数值。因此,对于这些模型,特征缩放通常不是必需的,尽管进行缩放也无害。
理解了这两种方法后,接下来我们将深入学习如何使用 NumPy 来实现它们。
第二部分:使用 NumPy 实现归一化与标准化
NumPy 提供了强大的数组操作和数学函数,使得实现归一化和标准化变得非常直接和高效。我们将从单维数据开始,然后扩展到更常见的二维数据(表示数据集,行代表样本,列代表特征)。
首先,我们需要导入 NumPy 库。
python
import numpy as np
1. NumPy 实现归一化(Min-Max Scaling)
假设我们将数据缩放到 [0, 1] 范围。公式是 $(x – x_{min}) / (x_{max} – x_{min})$。
NumPy 提供了 min()
和 max()
函数来方便地计算数组的最小值和最大值。
示例 1:单维数组的归一化
“`python
创建一个示例单维 NumPy 数组
data_1d = np.array([10, 20, 30, 40, 50])
1. 计算最小值和最大值
min_val = data_1d.min() # 或者 np.min(data_1d)
max_val = data_1d.max() # 或者 np.max(data_1d)
print(f”原始数据: {data_1d}”)
print(f”最小值: {min_val}”)
print(f”最大值: {max_val}”)
检查最大值和最小值是否相等,避免除以零
if max_val == min_val:
print(“警告:最大值和最小值相等,无法进行归一化。数据可能只有一个值。”)
normalized_data_1d = np.zeros_like(data_1d, dtype=float) # 或者其他处理方式,如保持原样或设为0
else:
# 2. 应用归一化公式
# 使用 NumPy 的广播特性,直接对整个数组进行操作
normalized_data_1d = (data_1d – min_val) / (max_val – min_val)
print(f”归一化 (到 [0, 1]) 后的数据: {normalized_data_1d}”)
验证结果
print(f”归一化后数据的最小值: {normalized_data_1d.min()}”)
print(f”归一化后数据的最大值: {normalized_data_1d.max()}”)
“`
解释:
* 我们使用 data_1d.min()
和 data_1d.max()
(或 np.min(data_1d)
和 np.max(data_1d)
)快速计算了数组的最小值和最大值。
* NumPy 允许我们直接对整个数组执行数学运算。data_1d - min_val
会将数组中的每一个元素都减去 min_val
。(data_1d - min_val) / (max_val - min_val)
会将上一步的结果中的每一个元素都除以 (max_val - min_val)
。这就是 NumPy 强大的广播(Broadcasting)功能的应用,避免了使用 Python 循环,效率更高。
* 我们添加了一个条件判断 if max_val == min_val:
来处理所有数据点都相同的情况,此时分母为零,无法进行归一化。在这种情况下,归一化后的数据通常全部变成 0 或保持原样(取决于需求)。
示例 2:二维数组(多特征)的归一化
在实际应用中,我们的数据通常是二维的,每一行是一个样本,每一列是一个特征。归一化或标准化通常是按列(即按特征)进行的,因为每个特征有自己的取值范围和分布。
NumPy 的 min()
和 max()
函数接受一个 axis
参数,用于指定计算的方向。axis=0
表示沿着行的方向计算,即对每一列进行操作(得到每列的最小值/最大值)。axis=1
表示沿着列的方向计算,即对每一行进行操作(得到每行的最小值/最大值,这在特征缩放中不常用)。
“`python
创建一个示例二维 NumPy 数组 (样本 x 特征)
假设有3个样本,4个特征
data_2d = np.array([
[10, 100, 0.5, 5000],
[20, 150, 0.1, 6000],
[5, 80, 1.0, 4000]
])
print(f”原始二维数据:\n{data_2d}”)
1. 计算每列(每个特征)的最小值和最大值
axis=0 计算每一列的统计量
min_vals_per_feature = data_2d.min(axis=0) # 得到 [min_col1, min_col2, min_col3, min_col4]
max_vals_per_feature = data_2d.max(axis=0) # 得到 [max_col1, max_col2, max_col3, max_col4]
print(f”每列的最小值: {min_vals_per_feature}”)
print(f”每列的最大值: {max_vals_per_feature}”)
2. 应用归一化公式到每一列
使用广播功能。 data_2d 的形状是 (3, 4),min_vals_per_feature/max_vals_per_feature 的形状是 (4,)
NumPy 会自动将形状为 (4,) 的数组广播到形状为 (1, 4),然后与 (3, 4) 进行逐元素操作。
效果就是每一列都减去/除以其对应的最小值/差值。
检查是否存在某列的最大值和最小值相等
range_per_feature = max_vals_per_feature – min_vals_per_feature
使用 np.where 处理分母为零的情况。
where condition is true, yield x, otherwise yield y.
Here, if range_per_feature is 0, yield 0; otherwise, yield the result of the division.
Note: This approach is a bit tricky for the numerator (data_2d – min_vals_per_feature).
A more robust way for potential division by zero across columns is to handle each column potentially differently,
or ensure the denominator is never zero by replacing zeros with a small value or infinity,
or setting the output to 0 for columns where min == max.
A simple approach: Handle division by zero explicitly for each column.
Or, using broadcasting with care:
denominator = range_per_feature
Replace zeros in denominator with 1 to avoid division errors,
as the numerator will be 0 for those columns anyway if min==max.
Alternatively, check before division.
Let’s do a safer version using broadcasting with zero handling:
normalized_data_2d = np.zeros_like(data_2d, dtype=float)
for i in range(data_2d.shape[1]): # 遍历每一列
col_data = data_2d[:, i]
min_val = min_vals_per_feature[i]
max_val = max_vals_per_feature[i]
col_range = max_val – min_val
if col_range == 0:
# 所有值都相同,归一化后全部为0
normalized_data_2d[:, i] = 0.0
else:
normalized_data_2d[:, i] = (col_data - min_val) / col_range
print(f”归一化 (到 [0, 1]) 后的二维数据:\n{normalized_data_2d}”)
验证结果(每列的最小值和最大值)
print(f”归一化后每列的最小值: {normalized_data_2d.min(axis=0)}”)
print(f”归一化后每列的最大值: {normalized_data_2d.max(axis=0)}”)
“`
解释:
* 我们使用 data_2d.min(axis=0)
和 data_2d.max(axis=0)
计算了每一列的最小值和最大值。结果是形状为 (4,)
的一维数组,包含了四个特征各自的最小值和最大值。
* 我们使用了 Python 循环结合 NumPy 的广播特性来安全地处理每一列。data_2d[:, i]
选择了第 i 列的所有行数据。(col_data - min_val) / col_range
对这一列数据应用了归一化公式。这种逐列处理的方式,结合了 NumPy 对单列的高效操作,并且可以清晰地处理分母为零的边缘情况。虽然也可以尝试更复杂的纯 NumPy 广播技巧来避免显式循环,但逐列处理在保证可读性和安全处理边缘情况方面通常更受欢迎。
* 归一化后的数据显示,每一列的数据都被独立地缩放到 [0, 1] 范围内。
缩放到其他范围(例如 [-1, 1]):
要将数据缩放到任意范围 $[a, b]$,可以使用以下公式:
$$ x_{scaled} = a + \frac{(x – x_{min})(b – a)}{x_{max} – x_{min}} $$
如果缩放到 [-1, 1],则 $a = -1, b = 1$,公式变为:
$$ x_{scaled} = -1 + \frac{(x – x_{min})(1 – (-1))}{x_{max} – x_{min}} = -1 + \frac{2(x – x_{min})}{x_{max} – x_{min}} = 2 \times \frac{x – x_{min}}{x_{max} – x_{min}} – 1 $$
这相当于先缩放到 [0, 1],然后乘以 2,再减去 1。
“`python
使用之前计算的 min_vals_per_feature 和 max_vals_per_feature
假设我们仍使用 data_2d
重新计算 range,处理分母为零的情况
range_per_feature = max_vals_per_feature – min_vals_per_feature
创建结果数组
normalized_data_neg1_1 = np.zeros_like(data_2d, dtype=float)
for i in range(data_2d.shape[1]):
col_data = data_2d[:, i]
min_val = min_vals_per_feature[i]
col_range = range_per_feature[i] # 使用预先计算好的范围
if col_range == 0:
# 所有值都相同,通常缩放到范围的中间点,例如 [-1, 1] 的中间点是 0
# 或者根据实际需求决定,这里设为 0
normalized_data_neg1_1[:, i] = 0.0
else:
# 缩放到 [0, 1]
scaled_0_1 = (col_data - min_val) / col_range
# 进一步缩放到 [-1, 1]
normalized_data_neg1_1[:, i] = 2 * scaled_0_1 - 1
print(f”归一化 (到 [-1, 1]) 后的二维数据:\n{normalized_data_neg1_1}”)
验证结果
print(f”归一化后每列的最小值: {normalized_data_neg1_1.min(axis=0)}”)
print(f”归一化后每列的最大值: {normalized_data_neg1_1.max(axis=0)}”)
“`
这个例子展示了如何灵活地使用 NumPy 实现不同范围的归一化。核心在于计算每列的最小值和最大值,然后应用相应的线性变换公式。
2. NumPy 实现标准化(Z-score Scaling)
标准化是将数据转换为均值为 0、标准差为 1 的分布。公式是 $(x – \mu) / \sigma$。
NumPy 提供了 mean()
和 std()
函数来计算数组的均值和标准差。同样,使用 axis=0
可以按列计算。
示例 3:单维数组的标准化
“`python
使用之前的单维 NumPy 数组
data_1d = np.array([10, 20, 30, 40, 50])
1. 计算均值和标准差
mean_val = data_1d.mean() # 或者 np.mean(data_1d)
std_val = data_1d.std() # 或者 np.std(data_1d)
print(f”原始数据: {data_1d}”)
print(f”均值: {mean_val}”)
print(f”标准差 (总体): {std_val}”) # NumPy std() 默认计算总体标准差 (ddof=0)
注意:如果计算样本标准差,需要指定 ddof=1
sample_std_val = data_1d.std(ddof=1)
print(f”标准差 (样本): {sample_std_val}”)
检查标准差是否为零,避免除以零
if std_val == 0:
print(“警告:标准差为零,无法进行标准化。数据可能只有一个值。”)
standardized_data_1d = np.zeros_like(data_1d, dtype=float) # 所有值相同,标准化后全部为0
else:
# 2. 应用标准化公式
standardized_data_1d = (data_1d – mean_val) / std_val
print(f”标准化后的数据: {standardized_data_1d}”)
验证结果
print(f”标准化后数据的均值 (接近0): {standardized_data_1d.mean()}”)
print(f”标准化后数据的标准差 (接近1): {standardized_data_1d.std()}”)
注意:浮点数计算可能导致均值和标准差非常接近0和1,而不是精确的0和1。
“`
解释:
* 使用 data_1d.mean()
和 data_1d.std()
计算了单维数组的均值和标准差。NumPy 的 std()
默认计算的是总体标准差(分母为 N),如果需要样本标准差(分母为 N-1),可以设置 ddof=1
参数。在特征缩放中,通常使用总体标准差,但根据具体情境也可以选择样本标准差。
* data_1d - mean_val
和 ... / std_val
利用了广播特性,对数组中的每个元素进行操作。
* 添加了标准差为零的检查,如果标准差为零,意味着所有数据点都相同,标准化后所有值应为 0。
示例 4:二维数组(多特征)的标准化
同样,我们按列(按特征)对二维数组进行标准化。
“`python
使用之前的二维 NumPy 数组
data_2d = np.array([
[10, 100, 0.5, 5000],
[20, 150, 0.1, 6000],
[5, 80, 1.0, 4000]
])
print(f”原始二维数据:\n{data_2d}”)
1. 计算每列(每个特征)的均值和标准差
axis=0 计算每一列的统计量
mean_vals_per_feature = data_2d.mean(axis=0)
std_vals_per_feature = data_2d.std(axis=0) # 默认 ddof=0 (总体标准差)
print(f”每列的均值: {mean_vals_per_feature}”)
print(f”每列的标准差: {std_vals_per_feature}”)
2. 应用标准化公式到每一列
使用广播功能。data_2d 的形状是 (3, 4),mean_vals_per_feature/std_vals_per_feature 的形状是 (4,)
NumPy 会将形状为 (4,) 的数组广播到形状为 (1, 4),然后与 (3, 4) 进行逐元素操作。
处理标准差为零的情况
denominator = std_vals_per_feature
将标准差为零的位置替换为 1,避免除以零。
在这些位置,分子 data_2d – mean_vals_per_feature 也会是零,结果仍然是 0 / 1 = 0。
这是一个安全的广播处理方法。
denominator[denominator == 0] = 1.0
standardized_data_2d = (data_2d – mean_vals_per_feature) / denominator
另一种更清晰的逐列处理方法(与归一化类似):
standardized_data_2d_explicit = np.zeros_like(data_2d, dtype=float)
for i in range(data_2d.shape[1]):
col_data = data_2d[:, i]
mean_val = mean_vals_per_feature[i]
std_val = std_vals_per_feature[i]
if std_val == 0:
standardized_data_2d_explicit[:, i] = 0.0
else:
standardized_data_2d_explicit[:, i] = (col_data – mean_val) / std_val
standardized_data_2d = standardized_data_2d_explicit # 使用这个结果
print(f”标准化后的二维数据:\n{standardized_data_2d}”)
验证结果(每列的均值和标准差)
print(f”标准化后每列的均值 (接近0): {standardized_data_2d.mean(axis=0)}”)
print(f”标准化后每列的标准差 (接近1): {standardized_data_2d.std(axis=0)}”)
“`
解释:
* data_2d.mean(axis=0)
和 data_2d.std(axis=0)
返回每列的均值和标准差。
* 通过检查并替换标准差为零的值,我们可以在利用 NumPy 广播功能进行高效计算的同时,优雅地处理所有值都相同(标准差为零)的列。对于标准差为零的列,所有原始值都等于均值,因此分子 (data_2d - mean_vals_per_feature)
在这些列上是零向量。将分母设为 1 使得结果为零向量,这正是标准化应有的结果。
* 标准化后的数据显示,每一列的数据都独立地被转换,其均值接近 0,标准差接近 1。不同特征原本巨大的数值差异被消除了,它们现在在同一个尺度上。
第三部分:高级注意事项与最佳实践
使用 NumPy 进行数据缩放不仅仅是应用公式那么简单,在实际机器学习项目中还需要考虑更多的问题。
1. 处理缺失值 (NaN)
如果你的数据中包含缺失值(用 np.nan
表示),直接使用 min()
, max()
, mean()
, std()
等 NumPy 函数可能会返回 NaN
,或者在计算过程中忽略 NaN
,这可能不是期望的行为。
NumPy 提供了 nan*
系列函数来处理包含 NaN
的数组,例如 np.nanmin()
, np.nanmax()
, np.nanmean()
, np.nanstd()
。这些函数在计算时会忽略 NaN
值。在进行缩放之前,通常需要先处理缺失值,例如使用均值、中位数填充,或者删除包含缺失值的样本/特征。
示例:计算包含 NaN 数据的统计量
“`python
data_with_nan = np.array([
[10, 100, np.nan, 5000],
[20, 150, 0.1, 6000],
[5, np.nan, 1.0, 4000],
[15, 120, 0.8, 5500]
])
print(f”包含 NaN 的数据:\n{data_with_nan}”)
使用常规 NumPy 函数计算统计量
print(f”常规 mean(axis=0): {data_with_nan.mean(axis=0)}”) # 包含 NaN 的列会是 NaN
print(f”常规 min(axis=0): {data_with_nan.min(axis=0)}”) # 包含 NaN 的列会是 NaN
print(f”常规 max(axis=0): {data_with_nan.max(axis=0)}”) # 包含 NaN 的列会是 NaN
print(f”常规 std(axis=0): {data_with_nan.std(axis=0)}”) # 包含 NaN 的列会是 NaN
使用 nan* 系列函数计算统计量(忽略 NaN)
nan_mean_vals = np.nanmean(data_with_nan, axis=0)
nan_min_vals = np.nanmin(data_with_nan, axis=0)
nan_max_vals = np.nanmax(data_with_nan, axis=0)
nan_std_vals = np.nanstd(data_with_nan, axis=0)
print(f”忽略 NaN 的 nanmean(axis=0): {nan_mean_vals}”)
print(f”忽略 NaN 的 nanmin(axis=0): {nan_min_vals}”)
print(f”忽略 NaN 的 nanmax(axis=0): {nan_max_vals}”)
print(f”忽略 NaN 的 nanstd(axis=0): {nan_std_vals}”)
在缩放前,通常需要先填充 NaN。例如使用均值填充:
data_filled = data_with_nan.copy()
for i in range(data_filled.shape[1]):
# 使用该列忽略 NaN 后的均值进行填充
col_mean = np.nanmean(data_filled[:, i])
data_filled[:, i] = np.nan_to_num(data_filled[:, i], nan=col_mean)
print(f”均值填充 NaN 后的数据:\n{data_filled}”)
现在可以在 data_filled 上应用之前的缩放方法了。
``
nan*` 函数在计算缩放参数时非常有用,但对数据本身进行填充或删除是必要的步骤。
处理 NaN 是数据预处理的重要一环,必须在缩放之前完成。NumPy 的
2. 避免数据泄露:训练集与测试集
在机器学习中,我们总是将数据分为训练集、验证集和测试集。特征缩放操作必须严格注意避免数据泄露(Data Leakage)。数据泄露是指在模型训练过程中使用了本不应该知道的信息,最常见的就是在整个数据集(包括训练集和测试集)上计算统计量(最小值/最大值、均值/标准差),然后用这些统计量来缩放所有数据。
正确的做法是:
1. 只在训练集上计算缩放所需的统计量(最小值/最大值、均值/标准差)。
2. 使用在训练集上计算出的这些统计量来对训练集进行缩放。
3. 使用同样的在训练集上计算出的统计量来对测试集(以及验证集)进行缩放。
这确保了测试集的处理模拟了未来未知数据到来的场景——我们只能使用在已知数据(训练集)上学到的信息进行处理。
使用 NumPy 实现时,这意味着你需要:
* 将数据分成 X_train
, X_test
。
* 在 X_train
上计算 min_train
, max_train
或 mean_train
, std_train
。
* 使用 (X_train - min_train) / (max_train - min_train)
或 (X_train - mean_train) / std_train
缩放 X_train
。
* 使用 (X_test - min_train) / (max_train - min_train)
或 (X_test - mean_train) / std_train
缩放 X_test
。
示例:训练集/测试集缩放(以标准化为例)
假设我们有以下数据,并已分割为训练集和测试集。
“`python
from sklearn.model_selection import train_test_split # 使用 sklearn 的分割函数方便演示
示例数据集
full_data = np.array([
[10, 100],
[20, 150],
[5, 80],
[15, 120],
[25, 180], # 训练集数据
[8, 90], # 测试集数据
[22, 160]
])
分割训练集和测试集 (通常比例是 70/30 或 80/20)
这里为了演示简单,手动指定分割点或使用一个小的测试集比例
X_train, X_test = train_test_split(full_data, test_size=0.3, random_state=42)
手动分割以便清晰看到效果
X_train = full_data[:5]
X_test = full_data[5:]
print(f”训练集 ({X_train.shape[0]} 样本):\n{X_train}”)
print(f”测试集 ({X_test.shape[0]} 样本):\n{X_test}”)
标准化 – 正确的方法
1. 只在训练集上计算统计量
train_mean = X_train.mean(axis=0)
train_std = X_train.std(axis=0)
print(f”训练集均值: {train_mean}”)
print(f”训练集标准差: {train_std}”)
处理训练集标准差为零的情况
train_std_for_division = train_std.copy()
train_std_for_division[train_std_for_division == 0] = 1.0
2. 使用训练集的统计量缩放训练集
X_train_scaled = (X_train – train_mean) / train_std_for_division
3. 使用训练集的统计量缩放测试集
X_test_scaled = (X_test – train_mean) / train_std_for_division
print(f”\n标准化后训练集:\n{X_train_scaled}”)
print(f”标准化后测试集:\n{X_test_scaled}”)
验证训练集缩放结果
print(f”标准化后训练集均值 (接近0): {X_train_scaled.mean(axis=0)}”)
print(f”标准化后训练集标准差 (接近1): {X_train_scaled.std(axis=0)}”)
验证测试集缩放结果 – 注意:测试集的均值和标准差不会是 0 和 1,这是正常的,
因为它们是使用训练集的统计量进行缩放的,它们自己的分布可能不同。
print(f”标准化后测试集均值: {X_test_scaled.mean(axis=0)}”)
print(f”标准化后测试集标准差: {X_test_scaled.std(axis=0)}”)
比较:如果在整个数据集上计算统计量 (错误的方法)
full_mean = full_data.mean(axis=0)
full_std = full_data.std(axis=0)
full_std_for_division = full_std.copy()
full_std_for_division[full_std_for_division == 0] = 1.0
X_train_scaled_wrong = (X_train – full_mean) / full_std_for_division
X_test_scaled_wrong = (X_test – full_mean) / full_std_for_division
print(f”\n错误方法(使用完整数据统计量)标准化后训练集:\n{X_train_scaled_wrong}”)
print(f”错误方法(使用完整数据统计量)标准化后测试集:\n{X_test_scaled_wrong}”)
可以看到结果与正确方法不同,且测试集的结果使用了未来信息。
``
fit
这个例子清晰地展示了在训练集上(计算统计量)并在训练集和测试集上
transform`(应用缩放)的过程。使用 NumPy 手动实现时,你需要显式地存储训练集的统计量并在测试集上重用。
3. 将缩放逻辑封装成函数
为了提高代码的可重用性和可读性,可以将归一化和标准化的逻辑封装成函数。
“`python
def min_max_scale(data, axis=0, feature_range=(0, 1)):
“””
对 NumPy 数组进行 Min-Max 归一化。
参数:
data (np.ndarray): 输入数据数组。
axis (int): 计算最小值/最大值的轴 (0 表示按列, 1 表示按行)。
feature_range (tuple): 目标范围 (min, max)。默认为 (0, 1)。
返回:
np.ndarray: 归一化后的数组。
tuple: 训练集计算得到的最小值和最大值 (min_vals, max_vals)。
方便应用于测试集。
"""
min_vals = np.min(data, axis=axis)
max_vals = np.max(data, axis=axis)
data_range = max_vals - min_vals
# 处理分母为零的情况
# 创建一个与 data_range 相同形状的数组用于存储分母,并将为零的位置替换为 1
denominator = data_range.copy()
zero_range_mask = (denominator == 0)
denominator[zero_range_mask] = 1.0
# 应用 [0, 1] 缩放
# 使用 np.where 在分母为零的位置直接生成 0.0,否则进行除法
# 这样比替换分母更直接处理了 numerator / zero_denominator = 0 的逻辑
scaled_0_1 = np.where(zero_range_mask, 0.0, (data - min_vals) / denominator)
# 应用目标范围缩放 [a, b]
a, b = feature_range
scaled_data = a + scaled_0_1 * (b - a)
# 对于原始范围为零的列,不管目标范围是什么,所有值都应该是目标范围的起始值a
# 因为 (x - min_val) / (max_val - min_val) = (min_val - min_val) / 0 无法计算
# 而我们上面的 np.where 已经将这种情况处理为 0.0 * (b - a) = 0
# 如果目标范围起始值 a 不是 0,需要修正。
# 对于原始范围为零的列,scaled_0_1 已经是 0.0。
# scaled_data = a + 0.0 * (b - a) = a
# 这样处理是对的,np.where 结合替换分母1.0并用0.0填充分子结果是正确的
return scaled_data, (min_vals, max_vals)
def z_score_scale(data, axis=0):
“””
对 NumPy 数组进行 Z-score 标准化。
参数:
data (np.ndarray): 输入数据数组。
axis (int): 计算均值/标准差的轴 (0 表示按列, 1 表示按行)。
返回:
np.ndarray: 标准化后的数组。
tuple: 训练集计算得到的均值和标准差 (mean_vals, std_vals)。
方便应用于测试集。
"""
mean_vals = np.mean(data, axis=axis)
std_vals = np.std(data, axis=axis) # 默认 ddof=0 (总体标准差)
# 处理标准差为零的情况
# 使用 np.where 在标准差为零的位置直接生成 0.0,否则进行除法
zero_std_mask = (std_vals == 0)
standardized_data = np.where(zero_std_mask, 0.0, (data - mean_vals) / std_vals)
return standardized_data, (mean_vals, std_vals)
示例使用封装的函数
X_train = full_data[:5]
X_test = full_data[5:]
使用 Min-Max 归一化函数
X_train_minmax, (train_min, train_max) = min_max_scale(X_train, axis=0, feature_range=(0, 1))
X_test_minmax, _ = min_max_scale(X_test, axis=0, feature_range=(0, 1)) # 注意:这里是错误的用法!_ 忽略了返回值
正确使用 Min-Max 归一化函数应用于训练集和测试集
在训练集上计算参数并缩放
X_train_minmax, (train_min, train_max) = min_max_scale(X_train, axis=0, feature_range=(0, 1))
使用训练集参数缩放测试集 (需要手动应用公式,或者修改函数签名使其接受外部参数)
这里我们手动应用公式来展示如何使用返回的参数
train_range = train_max – train_min
处理分母为零
train_range_for_division = train_range.copy()
train_range_for_division[train_range_for_division == 0] = 1.0
缩放测试集
X_test_minmax_correct = (X_test – train_min) / train_range_for_division # 缩放到 [0,1] 的中间结果
X_test_minmax_correct = 0 + X_test_minmax_correct * (1 – 0) # 缩放到目标范围 (这里是 [0,1])
对于目标范围不是 [0,1] 的情况,公式是 a + scaled_0_1 * (b-a)
a, b = (0, 1)
X_test_minmax_correct = a + ((X_test – train_min) / train_range_for_division) * (b-a)
再次处理原始范围为零的列在测试集上的情况
如果某一列在训练集上所有值都相同 (train_range_for_division 为 1.0),那么测试集该列的所有值都会被缩放到 a (目标范围的起始值)
zero_range_mask_train = (train_range == 0)
X_test_minmax_correct[…, zero_range_mask_train] = feature_range[0] # 目标范围的起始值
print(f”\n使用函数标准化后训练集:\n{X_train_minmax}”)
print(f”使用训练集参数标准化测试集:\n{X_test_minmax_correct}”)
使用 Z-score 标准化函数
在训练集上计算参数并缩放
X_train_std, (train_mean_std, train_std_std) = z_score_scale(X_train, axis=0)
使用训练集参数缩放测试集
处理训练集标准差为零的情况
train_std_std_for_division = train_std_std.copy()
train_std_std_for_division[train_std_std_for_division == 0] = 1.0
X_test_std_correct = (X_test – train_mean_std) / train_std_std_for_division
print(f”\n使用函数标准化后训练集:\n{X_train_std}”)
print(f”使用训练集参数标准化测试集:\n{X_test_std_correct}”)
``
fit
封装函数并返回计算得到的统计量,是使用 NumPy 实现缩放并应用于训练集/测试集的标准做法。请注意,为了将训练集参数应用于测试集,你需要在函数外部手动应用这些参数,或者设计一个更复杂的类来实现和
transform` 方法(类似于 scikit-learn),但这超出了纯 NumPy 函数的范畴。上面的例子演示了如何通过返回统计量并在测试集上再次应用公式来遵守训练/测试分割原则。
4. 与 Scikit-learn 的比较
NumPy 提供了进行数据缩放的基本构建块,让你完全控制计算过程。然而,对于大多数实际的机器学习任务,scikit-learn 的 preprocessing
模块提供了更方便、更健壮的解决方案,例如 MinMaxScaler
和 StandardScaler
。
优点 (NumPy):
* 基础理解: 帮助你深入理解缩放背后的数学原理和计算过程。
* 灵活性和控制: 可以根据特定需求进行高度自定义的缩放操作,不受库的限制。
* 无额外依赖: 如果你的项目只依赖 NumPy,不想引入 scikit-learn,NumPy 是唯一的选择。
* 性能: 对于基本的数学运算,NumPy 的向量化操作与 scikit-learn 底层调用的 NumPy 或其他优化库性能相当。
优点 (Scikit-learn):
* 便捷性: fit()
和 transform()
方法完美地封装了训练集/测试集处理逻辑,避免数据泄露。
* 健壮性: 内置处理了边缘情况(如标准差为零、范围为零),通常比手动实现更安全。
* 更多选项: 提供了除了 Min-Max 和 Z-score 外的其他缩放方法(如 RobustScaler, Normalizer)。
* Pipeline 集成: 可以轻松地集成到 scikit-learn 的 Pipeline 中,简化整个机器学习流程。
结论: 对于学习和理解,使用 NumPy 手动实现非常有益。但在生产环境或实际项目中,通常推荐使用 scikit-learn 或类似的成熟库,因为它们提供了更高级的抽象和更完善的鲁棒性处理。然而,NumPy 仍然是这些库的基础,理解 NumPy 的操作原理对于高效使用这些库以及调试问题至关重要。
结论
数据预处理,特别是特征缩放,是构建高性能机器学习模型的关键步骤。归一化(Min-Max Scaling)和标准化(Z-score Scaling)是两种最常用的缩放技术,它们通过不同的方式改变特征的尺度和分布,以适应不同算法的需求。
本文详细介绍了这两种方法,并使用 NumPy 库提供了从基本概念到具体实现的完整指南。我们学习了如何使用 NumPy 的 min()
, max()
, mean()
, std()
, nan*
函数以及广播、切片等特性来计算缩放参数并应用转换。我们还强调了在处理多维数据时 axis
参数的重要性,以及在实际应用中必须遵守的训练集/测试集分割原则以避免数据泄露。
掌握使用 NumPy 进行数据缩放的能力,不仅能让你在没有高级库时进行数据处理,更能加深你对数据处理底层机制的理解,这对于成为一名优秀的数据科学家或机器学习工程师来说是宝贵的基础。虽然像 scikit-learn 这样的库提供了更便捷的工具,但 NumPy 始终是 Python 数据生态的核心,理解并能直接使用 NumPy 进行基本操作,将使你在数据处理的道路上更加游刃有余。
通过本文的学习,你应该已经能够自信地使用 NumPy 对你的数据进行归一化和标准化处理,并理解其背后的原理和注意事项。现在,拿起你的数据,开始实践吧!