4、数据预处理异常:归一化参数错误、缺失值处理不当、标签编码混乱、数据不平衡导致的训练震荡

数据预处理,说白了就是给模型喂饭前的洗菜切菜环节。菜没洗干净,或者切得乱七八糟,大厨手艺再好也白搭。我在排查训练流水线时,至少有一半的“玄学”问题,追根溯源都出在这一步。

今天咱们就聊聊四个最常见的坑:归一化参数怎么算、缺失值怎么填、标签怎么编、数据不平衡怎么治。每一个我都踩过,而且不止一次。

核心观点: 数据预处理不是“跑个脚本就行”的体力活。参数错了,模型可能直接不收敛;标签乱了,你训出来的就是个“睁眼瞎”。

4.1 归一化参数错误:你算的均值和方差,可能根本不对

归一化,最常见的就是 Z-score 标准化:(x - mean) / std。但问题往往出在 mean 和 std 怎么算。

典型错误一:用全量数据算,然后直接用于训练集和测试集。

这会导致数据泄露。测试集的信息提前混进了训练过程。我见过一个项目,模型在验证集上 AUC 高达 0.98,上线后直接崩到 0.6。查了两天,发现是归一化时把整个数据集混在一起算了均值和方差。

正确做法: 只用训练集计算 mean 和 std,然后保存下来。验证集和测试集都用同一组参数。

# 错误示范
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_all)  # 全量数据拟合,大忌!
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 正确做法
scaler = StandardScaler()
scaler.fit(X_train)  # 只用训练集
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 别忘了保存参数
import joblib
joblib.dump(scaler, 'scaler_params.pkl')

典型错误二:对稀疏特征做 Z-score。

比如用户 ID 的 one-hot 编码,或者文本的 TF-IDF 特征。这些特征大部分是 0,强行归一化会破坏稀疏性,而且计算出来的均值和方差毫无意义。我个人习惯,对这类特征直接跳过归一化,或者用 MaxAbsScaler 只缩放到 [-1, 1] 区间。

小技巧: 归一化后,检查一下训练集和测试集的均值、方差是否接近。如果差异很大,说明数据分布变了,模型泛化会出问题。

4.2 缺失值处理不当:填错了比不填更可怕

缺失值处理,看似简单,其实门道很多。填个均值?填个中位数?还是直接删掉?

我的经验法则:

  • 缺失率 < 5%: 直接删除该行,简单粗暴,影响不大。
  • 缺失率 5% - 30%: 用中位数或众数填充。数值型用中位数(抗异常值),分类型用众数。
  • 缺失率 > 30%: 别填了。把这个特征是否缺失单独作为一个新特征(0/1 标志位),然后用模型去学。

我曾经踩过一个坑: 用均值填充了一个严重右偏的特征。结果填充后,大量样本被拉到均值附近,模型学到的分布完全扭曲了。后来改用中位数填充,效果好了很多。

import pandas as pd
import numpy as np

# 数值型:中位数填充
df['age'].fillna(df['age'].median(), inplace=True)

# 分类型:众数填充
df['gender'].fillna(df['gender'].mode()[0], inplace=True)

# 高缺失率特征:新增标志位
df['income_missing'] = df['income'].isnull().astype(int)
df['income'].fillna(df['income'].median(), inplace=True)
注意: 填充值也必须从训练集计算,然后应用到验证集和测试集。和归一化一样,不能混用数据。

4.3 标签编码混乱:模型把“苹果”和“香蕉”当成了邻居

标签编码,就是把文字标签变成数字。但怎么变,差别很大。

两种常见方式:

编码方式 适用场景 潜在问题
Label Encoding 有序分类(如:小、中、大) 无序分类会被赋予大小关系,模型会认为“苹果(1) < 香蕉(2)”
One-Hot Encoding 无序分类(如:红、黄、蓝) 类别太多时,特征维度爆炸

我见过最离谱的错误: 有人把城市名做了 Label Encoding,结果模型学出来“北京(1) + 上海(2) ≈ 广州(3)”。这完全是无中生有的数学关系。

避坑指南:

  • 无序分类,优先用 One-Hot 或 Embedding。
  • 如果类别太多(比如几百个城市),可以考虑用目标编码(Target Encoding),但要注意防止过拟合。
  • 编码映射表一定要保存下来。线上推理时,新来的类别怎么处理?我习惯在训练时留一个“未知类别”的兜底编码。
from sklearn.preprocessing import LabelEncoder

# 保存编码映射
le = LabelEncoder()
le.fit(y_train)
y_train_encoded = le.transform(y_train)

# 保存映射关系
mapping = dict(zip(le.classes_, le.transform(le.classes_)))
print(mapping)  # {'cat': 0, 'dog': 1, 'bird': 2}

# 线上推理时,处理未知类别
def safe_transform(le, value):
    if value in le.classes_:
        return le.transform([value])[0]
    else:
        return -1  # 兜底编码

4.4 数据不平衡导致的训练震荡:模型学会了“偷懒”

正负样本比例 1:100,甚至 1:1000。模型会发现:只要全部预测为负类,准确率就能到 99%。于是 Loss 震荡,模型根本不学正类。

我常用的三种方法:

  1. 重采样:
    • 欠采样:从多数类中随机抽取,和少数类数量持平。适合数据量大的场景。
    • 过采样:复制少数类样本,或者用 SMOTE 生成新样本。适合数据量小的场景。
  2. 调整 Loss 权重: 给少数类更高的权重。比如 class_weight='balanced',让模型更关注少数类。
  3. 使用更适合的评估指标: 别只看准确率。用 Precision、Recall、F1-score,或者 AUC。

举个例子: 我在做信用卡欺诈检测时,正类(欺诈)只有 0.1%。直接用原始数据训练,模型 Recall 几乎为 0。后来用了 SMOTE 过采样,并把正类的 Loss 权重设为 10,Recall 才提升到 70% 以上。

from imblearn.over_sampling import SMOTE
from sklearn.utils.class_weight import compute_class_weight

# SMOTE 过采样
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

# 或者调整 Loss 权重
class_weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train)
class_weight_dict = dict(zip(np.unique(y_train), class_weights))

# 在模型训练时传入 class_weight
# model.fit(X_train, y_train, class_weight=class_weight_dict)
注意: 重采样只对训练集做。验证集和测试集必须保持原始分布,否则评估结果会失真。

4.5 本章知识体系总览

下面这张图,把数据预处理的四个核心异常和对应的排查思路串起来了。你可以把它当作一个快速检查清单。

数据预处理异常排查框架 数据预处理异常排查框架 数据预处理异常 归一化参数错误 缺失值处理不当 标签编码混乱 数据不平衡 常见错误 • 全量数据计算参数 • 稀疏特征做Z-score • 未保存参数用于推理 • 训练/测试分布不一致 常见错误 • 均值填充偏态分布 • 高缺失率特征硬填 • 填充值未从训练集计算 • 忽略缺失标志位特征 常见错误 • 无序分类用Label编码 • 未保存编码映射表 • 未处理未知类别 • 类别太多未降维 常见错误 • 只用准确率评估 • 未调整Loss权重 • 重采样后验证集失真 • 欠采样丢失重要信息 排查思路:从训练集出发,保存参数,验证集独立 数据预处理是训练流水线的第一道关卡,也是问题最多的地方。 每次排查,先从这四个维度过一遍,能解决 80% 的“玄学”问题。

数据预处理这块,说白了就是“细节是魔鬼”。归一化的参数、缺失值的填充策略、标签的编码方式、不平衡的处理手段,每一个环节都藏着坑。我个人的习惯是,每次跑新数据前,先花 10 分钟把这四个点过一遍,写一个简单的检查脚本。磨刀不误砍柴工,真的。