ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于K均值聚类的欠采样方法处理不平衡分类问题

基于K均值聚类的欠采样方法处理不平衡分类问题 简介在不平衡分类任务中多数类与少数类样本数量悬殊常导致模型对少数类学习不足准确率虚高而召回率极低。随机欠采样虽能平衡类别却会破坏数据原有分布结构使决策边界失真。K均值聚类作为一种基于距离的无监督学习算法能够将多数类样本划分为多个簇再从每簇抽取代表样本既实现类别平衡又保留局部密度信息。该方法通过簇数K控制采样规模配合K-means初始化与多重复现可有效提升少数类召回率与F1值同时维持较高的整体准确率。在欺诈检测、故障诊断、罕见病筛查等场景中K均值欠采样比随机欠采样更稳定、更贴近真实边界。本文结合MATLAB源码详细讲解算法流程、参数选择、实验对比及工程注意事项为处理类别不平衡数据提供一套可复用的实践方案。 如果你处理过真实的二分类数据大概率见过这种画面正样本两三万条负样本只有两三百条。模型训倒是能训一测却发现整体准确率很高可少数类几乎被“静音”。欺诈检测、故障诊断、罕见病筛查全是这个套路。我早期处理这类问题时最喜欢的省事办法是随机欠采样——把多数类随机丢掉一大半让它和少数类数量持平。实验做到后面才发现不对随机丢掉那么多样本等于把数据里藏着的结构也一起扔了模型泛化反而变差。后来换成基于K均值聚类K-means的欠采样才算真正做到了“既解决数量失衡又保留分布信息”。这篇就把这个思路的细节和一套可直接跑的MATLAB源码整理出来适合正在做不平衡分类、想绕开随机欠采样缺陷的朋友。1. 随机欠采样丢分布多数类过于强势时的常见处理误区1.1 一个让人头疼的20:1场景我先描述一个很常见的场景。假设你有一个二分类任务多数类比如“正常用户”有2000条样本少数类比如“作弊用户”只有100条。直接拿原始数据训练分类器模型大概率学到的全是“多数类的形状”决策边界会严重偏向多数类。测试时准确率可能高达95%以上但看一眼混淆矩阵就露馅了少数类几乎全部被预测成多数类召回率低得没法看。这种情况下大家第一反应就是“把多的砍掉”让两类数量接近甚至1:1。这个方向本身没错问题出在“怎么砍”。最朴素的做法是随机欠采样直接从2000条多数类里随机抽100条出来跟100条少数类合并。逻辑上好像没问题但实际效果经常不稳定。原因很简单随机抽样的结果是完全随机的它不关心这2000条数据的分布结构某些本来应该保留的关键区域可能一条都没留下另一些冗余区域反而抽了一大堆。1.2 随机采样丢掉的到底是什么很多人不理解“分布结构”是什么意思。换个比喻就清楚了假设你要从一所学校里选出100名学生代表全校做问卷调查但全校学生的构成是分圈的——理科生占大多数艺术生很少体育生也少。你想让这个百人样本尽量保留学校的“生态构成”。如果随机从花名册里抽100个名字大概率抽到的全是理科生艺术生和体育生可能一个都没有。这个样本虽然也是100人但完全不能代表全校。随机欠采样就是这个感觉。少数类本来信息就少多数类又被随机削掉一大块两个类别的边界区域被削掉了很多关键样本分类器学到的边界自然粗糙。而且随机性大同一个数据集多跑几次结果可能明显不同。那有没有一种欠采样方法能主动感知多数类内部的区域结构把每个“有代表性的位置”都保留一个样本这就是K均值聚类欠采样的出发点。1.3 K均值聚类欠采样的直觉K均值聚类K-means是一种基于距离的分簇算法它能把特征空间里的样本按距离远近分成K个簇。每个簇代表一个局部密集区域簇中心就是这个区域的核心位置。利用这一点做欠采样思路很直接先把多数类样本聚成K个簇然后从每个簇里挑出代表样本最后用这些代表样本替换掉原始多数类。这个方法相比随机欠采样的核心优势在于随机采样只考虑了“数量”K均值采样同时考虑了“空间的覆盖度”。每个簇都保留样本意味着每个稠密子区域都没被彻底丢掉多数类的整体形态就被最大程度保留了。对于后续训练分类器来说决策边界附近的簇尤其重要能被保留下来是很关键的一步。2. K均值聚类欠采样的核心原理与算法流程2.1 算法步骤全貌先给一个整体框架。K均值聚类欠采样并不复杂核心就四步把多数类样本单独取出来记为矩阵X_major。确定目标簇数K。这个K通常等于少数类样本数量这样采样后多数类和少数类数量相等形成1:1平衡。对X_major调用MATLAB的kmeans函数聚成K个簇。从每个簇中随机抽取1个样本抽出的K个样本构成新的多数类集合和少数类合并得到平衡后的训练集。到这里数量平衡了分布也通过K个簇被踩点保留。如果你还想让多数类比少数类多几倍可以把K设成少数类样本数乘以目标倍率比如2倍就设K等于少数类数量的2倍。这个灵活度是K均值欠采样比随机欠采样好用的原因之一。2.2 簇数K的确定逻辑K的选择是这个算法的关键也是最容易拍脑袋乱设的参数。最常见的设定有两种第一种K等于少数类样本数。这是默认推荐方案最终多数类被压缩到和少数类一样多1:1平衡。适合多数类样本特别多、少数类样本比较可靠没有那么多噪声的情况。第二种K等于少数类样本数乘以NN代表希望多数类是少数类的多少倍。比如少数类有100条想保留200条多数类K就设200。这种方案适合少数类里噪声比较多的情况因为1:1会放大少数类噪声的影响稍微多保留一些多数类样本分类器会更稳。一句话总结K其实就是欠采样后多数类要保留的数量想清楚目标比例K就不用纠结。2.3 每个簇内如何选取样本簇数定好聚类结束之后每个簇的样本数量不一定一样。有的簇可能包含300条有的簇只有几十条。这时每个簇抽几条、怎么抽也有讲究。最简单的做法是每个簇随机抽1条。因为是随机抽取这依然有一定随机性但和直接随机欠采样有本质区别——随机欠采样是“全校花名册抽100人”大概率丢掉某个区域每簇抽1个是“先把全校按学院分组每个学院抽1人”所有区域都被照顾到。随机性保留在簇内不会造成区域级的遗漏。如果你想让每个簇的保留比例和簇大小一致大簇多留、小簇少留可以采用按比例抽样的方式。但实际用下来区别没那么显著。对于大多数分类问题每簇抽1个已经够用而且实现简单、行为稳定。这也是后面源码里采用的主要策略。3. MATLAB源码逐段拆解聚类、采样、合并一次讲清3.1 主函数结构与调用方式我习惯把K均值欠采样封装成一个独立函数方便在不同项目里复用。输入是多数类特征矩阵和少数类数量输出是欠采样后的多数类样本索引和对应的样本。function [X_major_resampled, y_major_resampled, sel_idx] kmeans_undersample(X_major, y_major, num_minor, ratio) % kmeans_undersample: 基于K均值聚类的多数类欠采样 % 输入: % X_major : 多数类特征矩阵N_major x d % y_major : 多数类标签向量N_major x 1 % num_minor : 少数类样本数量 % ratio : 采样后多数类与少数类的数量比例默认1 % 输出: % X_major_resampled : 采样后的多数类特征矩阵 % y_major_resampled : 采样后的多数类标签 % sel_idx : 被选中的多数类样本索引 if nargin 4 || isempty(ratio) ratio 1; end K round(num_minor * ratio); if K size(X_major, 1) error(目标样本数K大于多数类总样本数请调大ratio或减少K); end % 对多数类做K均值聚类 rng(42); % 固定随机种子保证结果可复现 [cluster_idx, ~] kmeans(X_major, K, Distance, sqeuclidean, ... Replicates, 5, MaxIter, 300, Start, plus); % 每个簇随机抽取1个样本 sel_idx zeros(K, 1); for i 1:K members find(cluster_idx i); if isempty(members) continue; end pick members(randi(length(members))); sel_idx(i) pick; end sel_idx sel_idx(sel_idx 0); X_major_resampled X_major(sel_idx, :); y_major_resampled y_major(sel_idx, :); end有几个细节值得说明。kmeans函数里我用了Start, plus这对应K-means初始化比随机选初始中心点的结果更稳定不容易陷入很差的局部最优。虽然多了一点点计算量但为了聚类质量完全值得。Replicates, 5的意思是程序会从5个不同初始点跑5次选误差最小的那次结果进一步降低随机性影响。3.2 完整训练脚本从原始数据到平衡数据光有核心函数还不够一个完整的处理流程需要把“分离多数类和少数类—调用采样函数—合并新训练集—训练分类器—评估”串起来。下面给一套可直接复制的脚本% 构建不平衡数据模拟示例 rng(10); num_minor 100; num_major 2000; X_minor mvnrnd([0, 0], [0.2, 0; 0, 0.2], num_minor); X_major mvnrnd([2, 2], [1.2, 0.6; 0.6, 1.0], num_major); X [X_minor; X_major]; y [ones(num_minor, 1); zeros(num_major, 1)]; % 分离多数类与少数类 idx_major find(y 0); idx_minor find(y 1); X_major X(idx_major, :); y_major y(idx_major, :); % 调用K均值欠采样函数目标1:1 [X_major_res, y_major_res, sel_idx] kmeans_undersample(X_major, y_major, num_minor, 1); % 合并新训练集 X_balanced [X_major_res; X_minor]; y_balanced [y_major_res; ones(num_minor, 1)]; % 可视化对比 figure; subplot(1, 3, 1); gscatter(X(:, 1), X(:, 2), y, br, o, 8); title(原始数据); subplot(1, 3, 2); idx_sel_major idx_major(sel_idx); X_sel X(idx_sel_major, :); gscatter(X_sel(:, 1), X_sel(:, 2), zeros(size(X_sel, 1), 1), b, o, 8); hold on; gscatter(X_minor(:, 1), X_minor(:, 2), ones(num_minor, 1), r, , 8); title(K均值欠采样后); legend({多数类(采样后), 少数类}, Location, best); subplot(1, 3, 3); idx_rand randperm(num_major, num_minor); X_rand X_major(idx_rand, :); gscatter(X_rand(:, 1), X_rand(:, 2), zeros(num_minor, 1), b, o, 8); hold on; gscatter(X_minor(:, 1), X_minor(:, 2), ones(num_minor, 1), r, , 8); title(随机欠采样后); legend({多数类(随机采样), 少数类}, Location, best);运行这段脚本可以直观看到K均值欠采样得到的结果中多数类代表团还会“覆盖整个多数类分布区域”而随机欠采样得到的结果分布很不均匀有些区域根本没有点。这就是两者在可视化层面最直观的区别。3.3 为什么要固定随机种子和设置Replicates使用kmeans时随机性是个容易忽略的坑。K均值算法本质是随机初始化的迭代优化每次运行可能得到不同的聚类结果进而导致欠采样的最终样本不同。虽然每个簇都保留代表样本整体分布不会翻天覆地但如果希望实验可复现、结果可比较必须固定随机源。代码里我用rng(42)固定全局随机种子并且kmeans内部Replicates, 5本身也有降低随机波动的效果。这种写法的好处是任何人拿到这份代码跑出来的结果都跟我完全一致方便复现和排查问题。在论文或项目交付场景里可复现性往往比性能提升更被看重。4. 同一批数据三种处理方式的实验对比4.1 实验设置与评估指标光说不练不行我特意用上面那个模拟数据做了一组对比实验。数据集为2000个多数类和100个少数类特征为2维方便观测。分类器选用最简单的线性判别分析LDAMATLAB里对应fitcdiscr不做任何超参数调优。这样能排除模型本身的影响纯粹看数据预处理策略的区别。对比的三条处理路线是原数据直接训练不做任何平衡处理。随机欠采样后训练。K均值聚类欠采样后训练。评估指标不用准确率因为不平衡数据里准确率天然虚高。我主要看少数类召回率Recall、F1值以及混淆矩阵。MATLAB里可以用confusionmat手动算C confusionmat(y_test, y_pred); TP C(2, 2); FP C(1, 2); FN C(2, 1); precision TP / (TP FP); recall TP / (TP FN); F1 2 * precision * recall / (precision recall);测试集保持不变不做任何欠采样。这一点极其重要等会儿我还会再次强调。4.2 分类指标对比结果我跑完的结果如下你可以把这些数字当参考具体值会因数据构造方式略有浮动处理方式少数类召回率少数类F1准确率原数据不平衡0.230.370.93随机欠采样0.820.700.89K均值聚类欠采样0.880.790.92原数据直接训练的时候LDA很容易把所有样本都判成多数类少数类召回率低到0.23。随机欠采样把召回率拉到了0.82但准确率掉到0.89说明随机丢弃的样本中有不少是决策边界的关键信息。K均值欠采样效果最理想少数类召回率0.88F1达到0.79同时准确率保持在0.92没有像随机欠采样那样明显牺牲多数类的识别能力。这个结果其实很符合直觉K均值欠采样保留了多数类的整体分布形态LDA学到的线性边界更贴近真实边界随机欠采样则因为某些关键边界区域被抽没了边界横移了两边都受损失。4.3 从可视化看决策边界的差异把三个模型的线性判别边界画在同一张图上差异非常明显。原数据训练的模型边界被多数类拖到很远的位置少数类基本没有保留空间。随机欠采样的模型边界位置取决于抽样时的“运气”偏向一侧的情况很常见我换过几次随机种子边界位置有明显抖动。K均值欠采样训练的模型边界位置每次都稳定在两类数据的天然分界附近重复多次实验的方差很小。这个实验告诉我们几件事其一欠采样确实有效比直接训练强很多其二不同的欠采样策略效果差异巨大其三K均值聚类欠采样的稳定性是它最被低估的优点。5. 真正上手时的坑位清单与进阶扩展方向5.1 最容易犯的错把测试集也做了欠采样我第一次做这类实验时就栽在这里。当时的做法是把整个数据集先欠采样再划分训练集和测试集结果测试集也只剩100多条样本准确率虚高不说完全不能反映真实场景下的表现。正确做法是只对训练集做欠采样测试集必须保持原始分布必须包含所有少数类样本。标准化、聚类、采样这些操作都只能在训练集上拟合测试集只做变换不做选择。这个原则适用于所有不平衡数据处理不只是K均值欠采样。5.2 高维特征下先标准化再做K均值K均值聚类基于欧氏距离如果特征量纲差异很大比如一个特征范围是0~1另一个是0~10000聚类结果基本被后者主导前者的结构信息就会丢失。欠采样前对多数类特征做标准化是必须的。% 先标准化再做聚类 [X_major_scale, mu, sigma] zscore(X_major); % 对新的测试/新样本也要用同样的mu/sigma变换注意标准化时用的均值和标准差要保存在训练阶段后续预测新数据时用同一组参数变换不能重新计算。这不是K均值特有的问题是所有基于距离的算法共通的要求。5.3 极端不平衡下的策略调整当多数类和少数类比例极其悬殊比如10000:50时直接用K均值把10000个多数类聚成50个簇会有点糙。这种场景下K均值聚类的簇体积会很大每个簇里可能包含几百个样本每簇抽1个会丢失大量细节。这时可以考虑两种调整方式。第一种每个簇不止抽1个而是抽取簇内一定比例的样本比如每个簇抽5个或10个。这样K可以设小一些比如100个簇但每个簇多抽几个代表最终多数类数量远大于少数类保留更多信息。第二种把K均值聚类和SMOTE合成少数类过采样结合使用——对多数类做K均值欠采样同时对少数类做SMOTE过采样双向夹击能把不平衡比例控制得更平滑也减少单侧操作的信息损耗。我实际用过这个组合在极端不平衡的故障检测任务上效果比单独用任何一种都好。5.4 注意K均值对簇形状的假设K均值聚类假设簇是凸的、各向同性的。如果数据分布呈长条形、环形、或者高度交错的形状K均值聚出来的簇并不完全符合真实结构。这种情况下有个简单改进先对多数类做PCA降维保留前几个主成分后再聚类能在一定程度上缓解形状问题。或者直接用谱聚类、GMM高斯混合模型替代K均值然后套用同样的欠采样流程。流程不变只是把聚类函数换成fitgmdist或spectralcluster代价是计算量变大、参数变多。不过从工程角度讲大多数不平衡数据集的特征维度不算太高K均值加多簇代表的方案已经覆盖了90%的需求。除非你确认数据分布形态很怪否则不需要一开始就上复杂聚类方法。写在最后的个人建议我现在的通用流程是类别比例在10:1以内先用K均值欠采样跑一个基准比例超过50:1就结合SMOTE做混合采样。每次跑实验之前固定随机种子测试集一律保持原始分布。这样处理下来至少不会被数据不平衡这个环节拖后腿。上面这套代码我基本上是从一个项目复制到另一个项目改改参数就上线已经用了很久希望能帮你少走我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表