数据预处理:让优化器「吃对饭」

优化不收敛,十次里有七八次是数据没喂对。我见过太多团队花几周调参数,最后发现是某个特征量级差了三个数量级——优化器直接懵了。数据预处理不是什么高大上的理论,它就是让优化器能正常工作的基本功。

说白了,你给优化器喂的数据质量,决定了它能跑多远。这一章我们就把数据归一化、缺失值处理、异常值检测这三件事聊透。

数据预处理核心流程 原始数据 数据清洗与检查 缺失值处理 删除 / 填充 / 插值 异常值检测 统计法 / IQR / 模型法 归一化 & 标准化 Min-Max / Z-score 干净、规整的训练数据 顺序很重要:先清异常 → 再补缺失 → 最后归一化

数据归一化与标准化:把特征拉到同一个尺度

优化器对特征的尺度极其敏感。一个特征取值 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 的梯度,然后整个训练就崩了。

三种处理策略

  1. 直接删除:缺失比例很小(<5%)且随机分布时,删掉最省事。
  2. 统计填充:用均值、中位数、众数填充。我一般优先用中位数——它对异常值不敏感。
  3. 模型预测填充:把缺失特征当目标,用其他特征预测它。精度高,但计算成本也高。

避坑指南:我曾经在一个时间序列项目里用均值填充缺失值,结果把时序模式全破坏了。时间序列数据,请用前向填充或插值,别用全局统计量。

# 缺失值处理示例
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])

数据预处理的执行顺序

顺序搞错了,效果会打折扣。我见过有人先归一化再处理异常值——结果异常值被缩放到正常范围里,根本检测不出来了。

正确的流程是:

  1. 先做异常值检测与剔除——把野点清掉
  2. 再做缺失值处理——把空位补上
  3. 最后做归一化/标准化——把尺度统一

小建议:每次做完预处理,都看一眼数据的统计摘要(describe())。均值、标准差、最大最小值有没有异常?这一步花不了 30 秒,但能帮你提前发现 80% 的数据问题。