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 震荡,模型根本不学正类。
我常用的三种方法:
- 重采样:
- 欠采样:从多数类中随机抽取,和少数类数量持平。适合数据量大的场景。
- 过采样:复制少数类样本,或者用 SMOTE 生成新样本。适合数据量小的场景。
- 调整 Loss 权重: 给少数类更高的权重。比如
class_weight='balanced',让模型更关注少数类。 - 使用更适合的评估指标: 别只看准确率。用 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 本章知识体系总览
下面这张图,把数据预处理的四个核心异常和对应的排查思路串起来了。你可以把它当作一个快速检查清单。
数据预处理这块,说白了就是“细节是魔鬼”。归一化的参数、缺失值的填充策略、标签的编码方式、不平衡的处理手段,每一个环节都藏着坑。我个人的习惯是,每次跑新数据前,先花 10 分钟把这四个点过一遍,写一个简单的检查脚本。磨刀不误砍柴工,真的。