数据预处理:让优化器「吃对饭」
优化不收敛,十次里有七八次是数据没喂对。我见过太多团队花几周调参数,最后发现是某个特征量级差了三个数量级——优化器直接懵了。数据预处理不是什么高大上的理论,它就是让优化器能正常工作的基本功。
说白了,你给优化器喂的数据质量,决定了它能跑多远。这一章我们就把数据归一化、缺失值处理、异常值检测这三件事聊透。
数据归一化与标准化:把特征拉到同一个尺度
优化器对特征的尺度极其敏感。一个特征取值 0~1,另一个特征取值 0~100000,梯度更新时大尺度特征会主导方向,小尺度特征几乎学不到东西。这就是不收敛的常见原因之一。
核心原则:让所有特征处于相近的量级,优化器才能「一视同仁」地更新参数。
两种主流方法
| 方法 | 公式 | 输出范围 | 适用场景 |
|---|---|---|---|
| Min-Max 归一化 | x' = (x - min) / (max - min) |
[0, 1] | 数据有明确边界,如像素值、分数 |
| Z-score 标准化 | x' = (x - μ) / σ |
均值为0,标准差为1 | 数据分布近似正态,或存在异常值 |
我个人习惯:如果数据分布比较规整,用 Min-Max;如果数据有长尾或异常值,用 Z-score 更稳。有一次我在做金融风控模型时,某个收入特征有极少数超高净值客户,Min-Max 直接把 99% 的数据压到了 0.01 以下,模型死活不收敛。换成 Z-score 后,问题立刻解决。
实用技巧:归一化/标准化的参数(min、max、μ、σ)一定要从训练集计算,然后应用到验证集和测试集。千万别在整个数据集上算——那叫数据泄露。
# Python 示例:两种方法对比
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import numpy as np
data = np.array([[1, 100],
[2, 200],
[3, 300],
[100, 50000]]) # 注意这个异常值
# Min-Max 归一化
mm_scaler = MinMaxScaler()
data_mm = mm_scaler.fit_transform(data)
print("Min-Max 结果:\n", data_mm)
# Z-score 标准化
z_scaler = StandardScaler()
data_z = z_scaler.fit_transform(data)
print("Z-score 结果:\n", data_z)
缺失值处理:别让「空」毁了你的梯度
缺失值在真实数据里太常见了。传感器掉线、用户没填信息、数据采集故障——原因五花八门。但优化器不认识 NaN,你给它 NaN,它就给你 NaN 的梯度,然后整个训练就崩了。
三种处理策略
- 直接删除:缺失比例很小(<5%)且随机分布时,删掉最省事。
- 统计填充:用均值、中位数、众数填充。我一般优先用中位数——它对异常值不敏感。
- 模型预测填充:把缺失特征当目标,用其他特征预测它。精度高,但计算成本也高。
避坑指南:我曾经在一个时间序列项目里用均值填充缺失值,结果把时序模式全破坏了。时间序列数据,请用前向填充或插值,别用全局统计量。
# 缺失值处理示例
import pandas as pd
import numpy as np
df = pd.DataFrame({
'feature_a': [1, 2, np.nan, 4, 5],
'feature_b': [10, np.nan, 30, np.nan, 50]
})
# 中位数填充
df['feature_a'] = df['feature_a'].fillna(df['feature_a'].median())
df['feature_b'] = df['feature_b'].fillna(df['feature_b'].median())
print(df)
异常值检测与剔除:别让「野点」带偏方向
异常值对优化的影响比缺失值更隐蔽。缺失值至少会报错,异常值会「安静地」把损失函数拉偏,让模型学到一个扭曲的映射关系。
你想想看,一个离群点产生的 loss 可能是正常点的几百倍,梯度更新时几乎完全被它主导。模型为了拟合这个野点,把决策边界扭得奇形怪状——收敛?不存在的。
常用检测方法
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 3σ 原则 | 超出均值±3倍标准差视为异常 | 数据近似正态分布 |
| IQR 方法 | 低于 Q1-1.5×IQR 或高于 Q3+1.5×IQR 视为异常 | 数据分布未知或偏态 |
| 孤立森林 | 基于树模型,异常点更容易被孤立 | 高维数据、复杂分布 |
我个人的经验:IQR 方法在工程实践中最好用。它不假设分布,对大多数场景都够用。3σ 原则对非正态数据容易误判,孤立森林效果好但解释性差——生产环境里你很难跟业务方解释「为什么删了这 3% 的数据」。
重要提醒:异常值不一定要删除。如果异常值代表真实的极端情况(比如金融欺诈、设备故障),你应该保留它,甚至专门建模。先问自己:这个异常是「噪声」还是「信号」?
# IQR 方法检测异常值
def detect_outliers_iqr(data):
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return (data < lower_bound) | (data > upper_bound)
# 示例
data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 100])
outliers = detect_outliers_iqr(data)
print("异常值索引:", np.where(outliers)[0])
print("异常值:", data[outliers])
数据预处理的执行顺序
顺序搞错了,效果会打折扣。我见过有人先归一化再处理异常值——结果异常值被缩放到正常范围里,根本检测不出来了。
正确的流程是:
- 先做异常值检测与剔除——把野点清掉
- 再做缺失值处理——把空位补上
- 最后做归一化/标准化——把尺度统一
小建议:每次做完预处理,都看一眼数据的统计摘要(describe())。均值、标准差、最大最小值有没有异常?这一步花不了 30 秒,但能帮你提前发现 80% 的数据问题。