数值特征处理:标准化、归一化、离散化

开始讲数值特征之前,我先问你一个问题:你给模型喂过身高、收入、点击率这种数据吗?它们的量纲可能差几个数量级。如果不做处理,模型会把数值大的特征当成更重要的特征——其实只是单位没选对而已。

我早期做一个推荐系统项目,跑了三天发现模型完全不收敛。排查半天,用户年龄(0-100)和消费金额(几千到几十万)直接拼在一起喂给神经网络,梯度更新被金额特征带偏了。从那以后,数值特征处理成了我建模流程里雷打不动的第一步。

这一节我们讲三个最常用的数值特征处理方法:标准化、归一化和离散化。它们在底层原理和适用场景上各有不同,但目标都是让模型更稳定、更高效地学习。

数值特征处理概览 原始数值特征 标准化 (Standardization) 归一化 (Normalization) 离散化 / 分箱 Z-Score Robust Min-Max 均值归一化 等宽分箱 等频分箱 选择依据 • 标准化:数据服从正态分布或对异常值不敏感时常用 • 归一化:有明确上下界、无需正态假设的场景 • 离散化:需要处理非线性关系、降低噪声或 增强可解释性时使用 Tip:GPU 训练时优先选标准化,对张量运算友好

标准化:把数据拉回标准正态

标准化的核心公式你肯定见过:z = (x - μ) / σ。结果变成均值为0、标准差为1的分布。它不改变数据的形状,只改变位置和尺度。

什么时候用?我个人习惯是:当后续要用线性模型、SVM、PCA或者神经网络时,标准化几乎是必选项。因为这些方法依赖距离或梯度,数值范围不同会导致某些特征支配学习过程。

代码示例:使用 scikit-learn 的 StandardScaler

from sklearn.preprocessing import StandardScaler
import numpy as np

# 模拟特征矩阵:行=样本,列=特征
X = np.array([[1.0, 200.0],
              [2.0, 500.0],
              [3.0, 800.0],
              [4.0, 1100.0]])

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print("均值:", scaler.mean_)      # [2.5, 650.0]
print("标准差:", scaler.scale_)  # [1.118, 335.41]
print("标准化后:\n", X_scaled)

跑完这段,你会发现变换后的第一列和第二列的量级完全一致了。我再提一点:fit_transform 用在训练集上,验证集和测试集只能用 transform,不能重新 fit。我见过有人把整个数据集拿去 fit,那验证集的分布信息就泄露了。

避坑指南: 我曾经在一个时序项目里对全部历史数据做了标准化,结果验证集用了未来信息的均值和方差。模型上线后表现暴跌10个点。对时序数据,正确的做法是滑动窗口滚动标准化。

归一化:缩放到固定区间

归一化的经典做法是 Min-Max 缩放:x' = (x - min) / (max - min),结果落在 [0,1] 之间。也有均值归一化,把数据搬到 [-1,1] 范围。

归一化不改变数据分布的形状,但会把异常值压扁。如果数据有极端离群点,Min-Max 会让大多数正常数据挤在很窄的区间里,模型几乎学不到区分度。

我推荐在图像像素处理、或者需要特征值有物理意义上下界时用归一化。 比如神经网络输出层用 sigmoid,输入归一化到 [0,1] 能加速收敛。

代码示例:MinMaxScaler

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler(feature_range=(0, 1))
X_norm = scaler.fit_transform(X)

print("归一化后:\n", X_norm)
# 第一列: [0, 0.333, 0.667, 1.0]
# 第二列: [0, 0.333, 0.667, 1.0]

看着结果,两种特征完美对齐了。但要注意:如果测试集出现比训练集更大的值,会被 cap 到 1 或超出范围,需要预先处理边界。

离散化(分箱):把连续变离散

离散化就是把连续数值切分成几个区间,每个区间变成一个类别。说白了,就是把“年龄=25岁”变成“年龄在20-30岁这个箱子里”。

我遇到过做信用评分模型需求,原始收入分布严重右偏,模型总学不好。后来用分箱变成“低收入、中低收入、中等收入、高收入”4个等级,AUC 直接涨了3个点。为什么?因为连续值里面的非线性关系被分箱后,模型更容易捕捉到分段效应。

分箱方法 原理 适用场景
等宽分箱 将值域等分成固定宽度的区间 数据分布均匀时简单有效
等频分箱 每个箱子包含相同数量的样本 偏态分布、长尾分布数据
基于聚类分箱 用k-means聚类后分配箱号 有自然簇状结构的数据
最优分箱 基于目标变量进行卡方或信息增益分割 风控评分卡等需要强可解释性的场景

我的经验: 分箱数目别太多,通常5~10个就够了。太多箱跟直接用连续值没区别,还增加了稀疏性。另外分箱后一定要做 one-hot 编码或 label encoding,别直接把箱号当连续变量喂给模型,除非箱号本身有顺序意义。

代码实现:自己动手做分箱

下面我用 pandas 演示等宽和等频分箱,再对比一下效果。

import pandas as pd
import numpy as np

# 生成偏态数据:收入
np.random.seed(42)
income = np.random.lognormal(mean=3, sigma=1.0, size=1000)
df = pd.DataFrame({'income': income})

# 等宽分箱:4个箱
df['equal_width_bin'] = pd.cut(df['income'], bins=4, labels=['低', '中低', '中高', '高'])

# 等频分箱:每个箱约250个样本
df['equal_freq_bin'] = pd.qcut(df['income'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

print(df.head())
print("\n等宽分箱样本数:\n", df['equal_width_bin'].value_counts().sort_index())
print("\n等频分箱样本数:\n", df['equal_freq_bin'].value_counts().sort_index())

输出会显示等宽分箱下,第一个箱(低)可能只有几十个样本,最后一个箱(高)可能有几百个样本,因为数据右偏。而等频分箱每个箱严格250个左右,更均衡。具体选哪个,看你的下游任务——如果不希望某个箱样本太少导致过拟合,优先等频。

GPU 优化的小贴士

最后聊一下 GPU 上的考虑。标准化和归一化都是逐元素操作,GPU 可以并行处理,计算开销几乎可以忽略。但分箱涉及排序或聚类,可能会触发 CPU 上的预处理,尤其是等频分箱需要对全数据排序。我建议在数据加载阶段(比如 PyTorch Dataset 的 __init__ 里)提前算好分箱边界,然后在训练时只做查表映射。

GPU 友好的实现方式: 把分箱边界作为张量上传到 GPU,然后用 torch.bucketize 进行快速分箱映射。

import torch

# 假设你在 GPU 上计算
boundaries = torch.tensor([0.0, 100.0, 500.0, 2000.0, 1e10], device='cuda')
data = torch.tensor([30.0, 250.0, 1500.0, 8000.0], device='cuda')

bins = torch.bucketize(data, boundaries)
print(bins)  # tensor([1, 2, 3, 4], device='cuda:0')

这样整个分箱过程都在 GPU 上完成,不需要 CPU-GPU 数据传输,速度能快一个数量级。