首页 / 资讯中心 / 文章详情

K-Means聚类前必须做特征缩放与PCA降维,否则结果失真

K-Means聚类前必须做特征缩放与PCA降维,否则结果失真 ★ FEATURED ARTICLE
最近在做一个用户分层项目的时候碰到一个很典型的工程教训数据是年龄、年收入、月度消费金额三个数值特征一共两千多条样本直接开跑K-Meansk3结果出来三个簇基本就是“低收入、中等收入、高收入”的切条年龄和消费维度几乎没参与决策。把特征缩放加进去之后同一个k值、同一个数据集轮廓系数从0.22跳到了0.55聚类结果才变得像“行为分层”。这个对比让我意识到K-Means这种基于距离的聚类算法对数据预处理不是“锦上添花”而是“前置条件”。这篇文章想围绕特征缩放和降维把尺度敏感机制、缩放器怎么选、PCA该不该用、怎么落地一整套预处理链路完整地讲一遍。1. 为什么“年龄收入”这种常见组合一跑K-Means就怪1.1 距离公式里范围大的特征会把其他特征“淹没”K-Means在迭代分配样本时判断一个点属于哪个簇靠的是它到各个簇中心的欧氏距离。距离公式长这样[ d(p, q) \sqrt{\sum_{j1}^{d} (p_j - q_j)^2} ]这个公式本身没有错错的是我们喂进去的特征尺度。举个具体例子两个人年龄差5岁年收入差50000元。收入维度的差值平方是25亿的数量级年龄差的平方只有25。在根号里年龄贡献的那部分几乎可以忽略不计。于是算法在做距离比较时等于只使用了收入这一列年龄和消费金额都成了装饰品。K-Means的目标函数也可以解释这件事。它最小化的是每个点到所属簇中心的总欧氏距离平方和也就是簇内误差平方和SSE。一个特征如果取值范围大到数万它在目标函数里天然占据极大权重。优化算法为了让整体距离更小自然会优先在收入这个方向寻找分割线其它方向的差异再明显也进不了它的视野。这就是“尺度大的特征绑架聚类”的数学根源。1.2 各向同性假设K-Means眼里簇是“圆球”不是“扁饼”还有一层容易被忽视的几何原因K-Means假设每个簇在特征空间里大致是各向同性的也就是说簇的形状在不同方向上不能差别太大。如果原始特征里收入的范围是0到10万年龄的范围是18到60那数据空间横轴被拉得极长纵轴被压得极短。真实存在的簇可能是斜着的扁椭圆但K-Means在欧氏距离下只会按照横轴方向去切分因为沿着横轴方向移动一点点距离变化量就很大算法的着力点全部集中在这个方向。所以特征缩放不是为了“让数据服从正态分布”K-Means本来就不要求正态性。它要的是各个特征在讨论距离时有同等的发言权。把每个特征的标准差统一到相同量级、或者让取值范围落在一个可比区间等于把这个扁椭球空间重新拉回近似的球形聚类才有可能找回被淹没的维度结构。2. 特征缩放怎么选不同数据形态对应不同“配方”2.1 三种常用缩放器的行为差异不能只背口诀很多资料会告诉你“跑K-Means之前先用StandardScaler”。这个说法对了一半但实际工程中数据形态千差万别选错缩放器一样会翻车。我整理了一张自用的对比表缩放器核心公式输出范围适用的数据形态在K-Means里要注意的风险StandardScaler(x - μ) / σ无固定范围z值通常集中在[-3, 3]近似对称、无明显长尾的连续特征极端离群点会拉大σ导致其余样本被压缩到很小的z值MinMaxScaler(x - min) / (max - min)固定到[0, 1]已知上下界、无严重离群点的特征对离群点极敏感一个极端值就会压缩所有正常样本的差异RobustScaler(x - median) / IQR无固定范围受中位数和四分位距控制含离群点、偏态严重的连续特征IQR为0时会产生除零问题需要先检查离散程度这三种方法的核心区别在于它们用什么作为“中心”用什么作为“尺度”。StandardScaler用均值和标准差当数据分布对称时非常稳定MinMaxScaler用最小值和最大值适合自然有界的特征但离群值会破坏整个映射RobustScaler用中位数和四分位距相当于给特征缩放加了一层“抗干扰能力”适合那些有长尾、有异常记录的真实业务数据。2.2 偏态特征先做对数变换再谈缩放如果你直接对“年收入”这种极度右偏的特征做StandardScaler会出现一个很尴尬的情况因为大多数人的收入集中在低水平均值被少数高收入拉高标准差也被拉得很大缩放之后普通样本的z值全部挤在-0.5到0.5之间而高收入群体的z值可能直接到5以上。这并没有从根本上解决问题反而把“少数极端值”和“正常主体”的距离进一步放大。我现在的习惯是先看每个数值特征的分布形态。如果右偏明显就先做log1p变换把数据的量级差距压平再进缩放器。这样做之后收入特征从“幂律分布”变成“近似钟形”再进StandardScaler聚类稳定性和轮廓系数都会有可感知的提升。整个过程可以理解成先把数据从“参差不齐的楼层”压缩成“可比较的高度”再统一调整地板避免一层楼的高度变化影响到整栋楼的判断。2.3 类别型哑变量和稀疏特征别硬塞进StandardScaler做特征工程时很多人会把分类型变量做one-hot编码然后一股脑塞进缩放器。One-hot之后每一列只有0和1两个值均值是类别占比标准差也取决于类别比例。经过StandardScaler之后“0”变成了某个负值“1”变成了某个正值。表面上每个变量都被标准化了但在K-Means距离计算里这个被缩放后的哑变量会带来一笔人为的距离贡献尤其是当其它连续特征都已经缩放到z值量级时哑变量的一正一负反而成了主导。对于one-hot产生的稀疏列建议保留0/1不缩放或者用MinMaxScaler让0和1保持不变。稀疏特征需要的是“稀疏结构”而不是让每个列都进入标准水平。同理如果有一大堆0值的稀疏连续特征RobustScaler往往更安全因为IQR的鲁棒性能避免大量零值把数据形态压成奇怪的形状。2.4 什么时候可以跳过缩放虽然缩放是K-Means的默认前置步骤但确实存在可以跳过的情况比如所有特征本来就是同一个物理单位、取值范围和离散程度都接近或者你已经知道每个特征数值比较的优先级是等价的。再比如你的输入本身就是某个模型生成的embedding向量很多embedding在生成时已经被归一化到同一量纲这时再做StandardScaler反而可能破坏已经形成的几何语义。判断标准很简单当你没有把握每个特征在距离计算中的可比性时就老老实实做缩放。3. 降维在K-Means前的作用不是选美而是去掉噪音3.1 高维距离被噪声稀释聚类结构会被“淹没”特征多了以后K-Means的聚类效果并不会线性变好反而可能出现“维度灾难”。原因在于每个无意义的噪声维度都会给两两距离贡献一份随机增量维数越高这份随机增量累计得越多最后所有点之间的距离都变得差不多最近邻的距离和随机点之间的距离不再有明显差异。这时候K-Means找不到真正的边界只能凭运气切分。共线性也会带来类似问题。如果两个特征高度相关等于把同一个信息在距离公式里重复计算了两遍这会变相放大该方向的权重。降维的作用就是把重复表达的信息合并掉把噪声维度丢弃掉让距离计算重新聚焦在真正能区分样本的方向上。3.2 PCA的正确位置在特征缩放之后在K-Means之前PCA是目前最常用的线性降维方法。它会把原始特征变换成一组互相正交的主成分每个主成分对应数据方差最大的方向。关键点是如果不先做特征缩放PCA会因为尺度的差异自动把方差大的原始特征放到优先级更高的主成分上从而重蹈K-Means“大尺度特征绑架”的覆辙。所以PCA前必须做标准化。在操作层面我的常规做法是from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA scaler StandardScaler() X_scaled scaler.fit_transform(X) pca PCA(n_components0.95) X_pca pca.fit_transform(X_scaled)选择主成分数量时我会先看累计方差贡献率。常用标准是保留到90%到95%的方差但这不是铁律。如果数据本身维度不高比如只有20维也可以保留10个主成分如果主成分数量压缩到3个左右还有一个额外的好处是可以用三维散点图直接观察簇的分布大幅度降低排查难度。3.3 PCA不保留簇结构时会发生什么PCA并不认识簇它只关心方差。它默认的思路是“方差大的方向更重要”但这个假设并不永远成立。一个极端例子有两个分组靠一个低方差特征区分比如“是否开通某项服务”这类稀有属性而其余100个高方差特征都是无关噪声。PCA在挑选主成分时可能丢掉这个小方差方向于是降维后反而把真正区分样本的信息给删掉了。所以我的判断标准是先做一次PCA把前两三个主成分画出来肉眼看一下有没有明显的分簇结构。如果散点图是一片糊状说明数据要么本身没有簇要么簇方向被高方差噪声压制。这时候不要继续硬用PCA可以尝试保留更多主成分或者观察标准化后的原始特征里有没有局部明显分组的维度。PCA能提升聚类稳定性的前提是“结构方差大于噪声方差”这个前提一旦不满足降维反而会帮倒忙。3.4 t-SNE和UMAP只用于可视化不用于聚类前输入很多朋友喜欢用t-SNE把高维数据压到二维或三维再丢给K-Means。这种做法我强烈不建议。t-SNE的优化目标是让局部近邻在低维空间中保持邻近它会在不同区域之间产生伸缩变化压缩后的全局距离并不保真。更麻烦的是t-SNE的随机性和困惑度参数会直接影响输出同一份数据用不同参数跑两次得到的聚类结果可能完全不同。UMAP虽然速度更快但它同样依赖邻域图构建超参数不同同样会改变样本间距离的语义。我通常只在完成聚类之后用t-SNE把结果投影到二维平面做可视化解释给业务方看“簇大概长什么样”。真正参与聚类训练的特征还是以“可解释、可复用、可逆”的线性变换为主。4. 一套可直接复制的K-Means预处理流水线4.1 从原始特征到聚类标签端到端示例为了演示完整流程我用模拟数据跑一遍方便你直接复现和对照。这里用make_blobs生成一个五维模拟簇并人为把前两维放大几十倍上百倍模拟真实的尺度不平衡。import numpy as np from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score # 生成模拟数据5个维度4个簇 X, _ make_blobs(n_samples1500, centers4, cluster_std0.8, n_features5, random_state42) # 人为制造尺度失衡前两个特征量级被放大 X[:, 0] X[:, 0] * 100 X[:, 1] X[:, 1] * 1000 # 不做预处理的基线 k_bad KMeans(n_clusters4, n_init10, random_state42) labels_bad k_bad.fit_predict(X) print(未缩放 silhouette:, silhouette_score(X, labels_bad)) # 标准流程缩放 - PCA - K-Means scaler StandardScaler() X_scaled scaler.fit_transform(X) pca PCA(n_components0.95, random_state42) X_pca pca.fit_transform(X_scaled) k_good KMeans(n_clusters4, n_init10, random_state42) labels_good k_good.fit_predict(X_pca) print(缩放PCA silhouette:, silhouette_score(X_pca, labels_good))在这个模拟样例里未缩放数据的轮廓系数会因为尺度失衡明显偏低而缩放降维之后会回升。项目里的真实数据效果不一定这么夸张但流程就是这个流程先缩放再降维然后跑K-Means。PCA之后如果保留的主成分只能解释80%的方差而轮廓系数仍然不理想可以考虑调整主成分数量、换RobustScaler或者对偏态特征做对数变换后再试。4.2 固定随机种子同一个数据跑两次结果必须一致K-Means的初始化是随机的如果不固定随机种子哪怕同一份数据两次运行的结果也可能不同。过去很多同学会遇到一种情况昨天调好的聚类今天重新跑一遍簇标签编号变了甚至少数边缘样本被划到了另一个簇。这往往不是因为算法不稳定而是因为没有设置random_state。现在的KMeans默认初始化方式已经是k-means这比随机初始化稳健得多但为了工程可复现仍然必须显式设置随机种子。我的统一习惯是kmeans KMeans(n_clustersk, initk-means, n_init10, random_state42)n_init10表示算法会跑10次每次用不同的初始中心最后选择总误差最小的结果。这个参数能有效降低初始化对最终划分的干扰。如果你发现数据量特别大可以适度减少n_init但如果追求结果稳定不要小于5。4.3 k值选择和降维后的可视化验证聚类模型无法用真实标签直接评估所以k值选择除了看业务经验还需要结合指标和可视化。我常用的做法是把silhouette_score和肘部法则一起看计算不同k值下的轮廓系数和簇内误差平方和找轮廓系数较高且曲线变化平缓的区间。在降维后把样本投影到前两个主成分平面上用不同颜色标记聚类结果肉眼检查边界是否合理、有没有明显的簇重叠这一步值得花的时间比调参多得多。还要注意的一点是聚类结果的轮廓系数高不代表业务上有意义。最终验证标准是看聚类是否能指导决策比如每个簇的用户画像是否清晰、包围的样本是否有相似行为模式。5. 真实场景里最常踩的坑离群点、新样本和特征漂移5.1 离群点应该先处理再缩放离群点对K-Means的打击是双重的一是直接把簇中心拉向自己二是会破坏缩放器的参数估计。在真实业务数据里收入、消费金额、点击数这类字段经常出现数量级异常的值。如果你用MinMaxScaler最大值被离群点抬高后正常样本都会被压缩到接近0的区间如果用StandardScaler标准差被拉大后正常样本的z值普遍偏小特征区分度急剧下降。我的建议流水线是缺失值处理 → 离群点截断/剔除 → 非线性变换如log1p → 缩放 → 降维 → 聚类。离群点到底剔除还是截断取决于业务合理性但不要等到缩放之后再去处理那样离群点已经污染了缩放器的参数。5.2 新样本上线时必须保存和复用同一套预处理参数聚类模型上线后几乎一定会遇到“来了新用户该怎么打标签”的问题。很多新手这时候会重新对全量数据做一遍fit_transform这是典型的错误。因为新样本加入后均值和方差全部变了聚类边界也会变标签体系就无法稳定。正确做法是把已经训练好的scaler、pca和kmeans模型一起保存新样本来了之后只做transform不重新fit。import joblib # 训练完成后保存一整套预处理和聚类模型 joblib.dump({ scaler: scaler, pca: pca, kmeans: k_good, }, kmeans_pipeline.joblib) # 新样本上线时加载 pipeline joblib.load(kmeans_pipeline.joblib) scaler pipeline[scaler] pca pipeline[pca] kmeans pipeline[kmeans] new_sample np.array([...]) # 新样本原始特征 new_vec scaler.transform(new_sample.reshape(1, -1)) new_vec pca.transform(new_vec) label kmeans.predict(new_vec)[0]这一步看起来简单但实际项目里因为漏掉transform和fit_transform的区别导致的线上离线结果不一致真的很常见。fit只是学习参数transform才是应用参数线上预测永远只应transform。5.3 关注特征分布漂移定期重训即使整套流程都搭好了数据也不会一劳永逸地保持稳定。半年之后用户的消费结构变了收入分布可能整体右移PCA的主成分方向也可能发生偏移。这时候如果还在用旧的scaler和pca新样本会落到原本不存在的坐标区域聚类边界会越来越失真。我的做法是每个统计周期做一次分布对比计算最新特征均值、方差与历史均值和方差的偏差如果偏离比例超过阈值就重新拟合预处理和聚类模型。对于一个更简单实用的指标可以直接看“新样本被分配到各簇的比例”是否发生了显著变化。如果之前比例是20%、30%、50%现在变成了10%、40%、50%大概率是特征漂移导致聚类边界偏移了需要重新训练。最后说一点个人体会。每次拿到聚类需求我宁愿先在特征预处理上花70%的时间也不急着调k值和模型参数。大多数聚类效果差的项目根因不是聚类算法本身而是数据在进入算法之前没有处于公平、可比较的状态。先做缩放再考虑要不要降维最后才谈模型调参这个顺序能帮你少走至少三分之一的弯路。
阅读完成 · 觉得有帮助?
咨询建站