TL;DR
- 场景:KNN/距离类模型在量纲不统一数据上效果失真,且归一化顺序易引入数据泄露
- 结论:先划分数据集;只用训练集拟合归一化参数;测试集只做transform;交叉验证用Pipeline才稳
- 产出:一套可复用的MinMaxScaler + KNN(含distance权重)工程流程与排错清单
归一化
距离类模型归一化要求
我们把X放到数据框中看一眼,是否观察到,每个特征值的均值差异很大?有的特征数值很大,有的特征数据很小,这种现象在机器学习中称为”量纲不统一”,KNN是距离类模型,欧式距离的计算公式中存在着特征上的平方和。
如果某个特征Xi的取值非常大,其他特征的取值和它比起来就不算什么,那么距离的大小很大程度上都会由这个Xi来决定,其他的特征之间的距离可能无法对d(A,B)的大小产生什么影响,这种现象会让KNN这样的距离类模型的效果大打折扣。
然而实际分析情况中,绝大多数数据集都会存在各特征量纲不同的情况,此时若要使用KNN分类器,则需要先对数据集进行归一化处理,即是将所有的数据压缩都同一个范围内。
当数据(X)按照最小值中心化后,再按极差(最大值-最小值)缩放,数据移动了最小值个单位,并且会被收敛到【0,1】之间,而这个过程,就称做数据归一化(Normalization,又称Min-Max Scaling)。
先分数据集 再做归一化
机器学习数据预处理中的归一化注意事项
错误做法分析
直接在全数据集X上进行归一化后再进行交叉验证绘制学习曲线的做法存在严重问题,这种操作会导致数据泄露(data leakage),具体表现在:
- 统计量计算错误:当使用全数据集计算归一化参数(如最小值和极差)时,这些参数实际上包含了未来测试集的信息
- 评估偏差:这种操作会使模型在交叉验证中表现异常好,但这种”好”是虚假的,因为测试集信息已经通过归一化过程泄露给了训练过程
正确做法详解
正确的数据预处理流程应该遵循以下步骤:
- 数据分割:首先将完整数据集分割为训练集和测试集(通常比例为7:3或8:2)
- 仅使用训练集计算归一化参数:在训练集上计算所需的归一化统计量(最小值、最大值、均值、标准差等)
- 应用归一化:
- 使用训练集计算的参数对训练集本身进行归一化
- 使用相同的参数对测试集进行归一化(绝对不要在测试集上重新计算归一化参数)
- 模型训练与评估:在归一化后的数据上进行模型训练和交叉验证
现实业务场景的重要性
在实际业务中,这种规范尤为重要,因为:
- 模拟真实场景:我们永远只能基于历史数据(训练集)来构建模型,无法预先知道未来数据(测试集)的分布
- 避免过度乐观:使用错误的归一化方法会导致模型评估结果过于乐观,从而可能选择在实际应用中表现不佳的模型
- 流程一致性:生产环境中,新数据到来时也必须使用训练阶段确定的归一化参数进行处理
示例说明
假设我们有一个包含1000个样本的数据集:
- 首先分割为700个训练样本和300个测试样本
- 在700个训练样本上计算最小值为10,最大值为90(极差为80)
- 使用这些参数:
- 训练集归一化:(每个值-10)/80
- 测试集归一化:(每个值-10)/80(即使测试集中出现值5或95)
- 这样得到的评估结果才能真实反映模型在未知数据上的表现
data = [[-1,2],[-0.5,6],[0,10],[1,18]]
data=pd.DataFrame(data)
(data-np.min(data,axis=0))/(np.max(data,axis=0)-np.min(data,axis=0))
通过 sklearn 实现
from sklearn.preprocessing import MinMaxScaler as mms
Xtrain,Xtest,Ytrain,Ytest=train_test_split(X,y,test_size=0.2,random_state=420)
# 归一化
# 求训练集最大/小值
MMS_01=mms().fit(Xtrain)
# 求测试集最大/小值
MMS_02=mms().fit(Xtest)
# 转换
X_train=MMS_01.transform(Xtrain)
X_test =MMS_02.transform(Xtest)
score=[]
var=[]
for i in range(1,20):
clf=KNeighborsClassifier(n_neighbors=i)
# 交叉验证的每次得分
cvresult=CVS(clf,X_train,Ytrain,cv=5)
score.append(cvresult.mean())
var.append(cvresult.var())
plt.plot(krange,score,color="k")
plt.plot(krange,np.array(score)+np.array(var)*2,c="red",linestyle="--")
plt.plot(krange,np.array(score)-np.array(var)*2,c="red",linestyle="--")
plt.show()
距离的惩罚
最近邻点距离远近修正在对未知样本分类过程中是一个重要的优化步骤。传统KNN模型采用”一点一票”的简单投票机制:在选取最近的K个邻居后,统计这些邻居的类别分布,每个邻居对分类结果的影响力相同。
然而这种简单投票机制存在明显缺陷。实际上,即使是K个最近邻点,它们与目标样本的距离也存在显著差异。根据KNN模型的基本假设——相似样本具有相似类别属性,距离更近的邻居往往与目标样本属于同一类别的概率更高。因此,距离较近的邻居理应比距离较远的邻居具有更大的投票权重。
常用的加权方式包括:
- 反比加权:权重=1/(距离+ε)
- 高斯加权:权重=exp(-距离²/σ²)
- 线性加权:权重=(最大距离-距离)/(最大距离-最小距离)
这种距离加权修正能更好地反映局部样本分布的实际情况,提高分类准确率,特别是在样本分布不均匀或存在噪声的情况下效果更为显著。
在sklearn中,我们可以通过参数weights来控制是否适用距离作为惩罚因子:
for i in range(1,20):
clf=KNeighborsClassifier(n_neighbors=i,weights='distance')
# 交叉验证的每次得分
cvresult=CVS(clf,X_train,Ytrain,cv=5)
score.append(cvresult.mean())
var.append(cvresult.var())
plt.plot(krange,score,color="k")
plt.plot(krange,np.array(score)+np.array(var)*2,c="red",linestyle="--")
plt.plot(krange,np.array(score)-np.array(var)*2,c="red",linestyle="--")
plt.show()
错误速查
| 症状 | 根因 | 定位方法 | 修复方案 |
|---|---|---|---|
| 交叉验证分数异常高、线上掉分明显 | 在全量X上fit归一化参数,产生data leakage | 检查是否在split/CV之前fit scaler | 先split;仅在训练集fit;CV用Pipeline(scaler+model) |
| 测试集表现不稳定、不同random_state波动大 | 测试集单独fit了scaler(训练/测试缩放标准不一致) | 代码里出现mms().fit(Xtest) | 测试集只用训练集scaler的transform,不允许对Xtest再fit |
| 训练/测试落在不同的[0,1]区间映射 | 分别拟合了两个MinMaxScaler | 发现MMS_01和MMS_02同时存在且都fit | 只保留一个scaler:fit(Xtrain);两边都transform |
| 最优K结论前后不一致(7/8/6混用) | 文本与代码的n_neighbors、index计算未同步 | 文中”最终值是7/最优K是8”,但建模用n_neighbors=6 | 统一口径:用同一套score列表与同一n_neighbors复现最终结论 |
| 绘图或循环报错(变量未定义/长度不匹配) | krange未定义或与score长度不一致;score/var未清空复用报NameError或plot维度错误 | 明确krange=range(1,20);每次实验前score=[]; var=[] | - |
| 加权KNN看似提升但不可复现 | 加权策略对异常点/噪声敏感,且评估流程不规范 | 对比不同split、不同CV结果差异大 | 用固定评估协议:Pipeline + StratifiedKFold;同时报告均值与方差 |
| 线上新数据出现<0或>1的缩放结果被误判为错误 | 测试/线上数据超出训练集min/max属正常现象 | 发现transform后有负数或大于1 | 保持训练期参数不变;必要时做异常值策略(截断/鲁棒缩放)并用验证集评估 |