尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于K-means聚类的欠采样:Matlab实现类别不平衡处理

基于K-means聚类的欠采样:Matlab实现类别不平衡处理 简介本资源是一份面向数据挖掘与机器学习初学者的MATLAB实践代码包聚焦于不平衡数据场景下的欠采样策略实现特别适用于课程设计、小规模实验验证及算法原理理解。核心采用K均值聚类思想指导样本筛选在保留多数类代表性的同时提升类别均衡性可直接用于分类预处理环节。压缩包共含4个文件2个文本数据集、1个MATLAB主程序脚本、1张算法效果示意图总大小仅12KB轻量易部署其中.m文件封装了完整的K-means欠采样流程txt文件提供标准测试数据PNG图像直观展示聚类与采样结果。目前已有409人学习下载配套代码结构清晰、注释完整无需额外依赖库开箱即用适合快速复现算法逻辑、对比不同采样策略效果或作为教学演示素材。 做分类任务的朋友大概都遇到过这种尴尬训练集里正反例比例悬殊模型训练完了准确率95%上线一测少数类几乎全被吃掉。问题出在哪多半是数据本身就不平衡。今天这篇我们聊一个非常实用的处理手段——基于k均值聚类的欠采样我用Matlab把整个流程写成了可以直接调用的源码顺便把里面的坑也一并列出来。如果你正在被信用卡欺诈识别、设备故障诊断、罕见病预测这类场景折磨这份源码和笔记应该能帮你省下不少时间。1. 做数据平衡为什么我盯着K-means欠采样1.1 类别不平衡到底怎么伤模型的先还原一下典型的现场二分类任务负类样本800条正类样本只有50条。你用逻辑回归或者决策树直接去训练模型会怎么做它会非常“偷懒”——把全部样本都预测成负类准确率照样能到94%。但真正要抓的正类比如欺诈交易、故障设备一条都没抓到这个模型在业务上等于废物。这就是类别不平衡的核心问题绝大多数分类算法都假设正负类样本数量大致相当训练时损失函数被多数类主导。少数类贡献的梯度信号太弱模型根本学不到它的边界。你说那用准确率当指标不就行了问题在于准确率对多数类极度偏心少数类全错它也能拿高分会把模型调优过程彻底带偏。所以处理不平衡数据第一步要做的不是调参而是先让数据分布“正常”一点。数据层面的处理主流有两条路欠采样砍多数类和过采样生少数类。过采样的代表是SMOTE在少数类样本之间插值生成新样本欠采样的代表是随机删除多数类样本。今天讲的k均值聚类欠采样走的是欠采样这条路但比随机删除要聪明得多。1.2 随机欠采样丢信息聚类欠采样保结构随机欠采样的实现简单到一句话从多数类里随机抽出一部分样本扔到和少数类差不多数量完事。但问题也很明显——它在“瞎扔”。假设多数类内部本身有三四个团簇结构随机删除很可能把某个小团簇删得稀疏甚至完全删没模型的决策边界就会偏向数据密集区域少数类那边的识别率反而可能降下去。k均值聚类欠采样不一样。它先对多数类做k-means聚类把多数类拆成若干个簇再从每个簇里挑代表性样本。效果上相当于先对多数类的分布做了一次“压缩”把整片区域的形态信息保留下来再用少量样本去代表整片区域。边界样本、稀疏区域的簇都会被保留一个代表主要分布结构不塌。我打个比方随机欠采样就像裁照片不管三七二十一裁掉一大块你可能正好把画面里重要的背景裁没了聚类欠采样更像是先给照片分层、缩略每一层留一个小方块最后拼出来的图能看出来原画面是什么但体积小了很多。这对后续分类器保留有效信息非常重要。1.3 适用场景与前置条件这套方法不是银弹什么时候适用心里要有数。我总结下来以下几个场景最合适多数类有明显簇结构或非线性分布。比如传感器数据、用户行为数据、图像特征天然聚集随机砍掉会很可惜。少数类样本量奇少少到SMOTE都编不出像样的新样本。这时候把多数类压下来反而更稳。类别分布不是极端失衡比如10:1到100:1之间聚类欠采样的收益比较明显。如果失衡都到10000:1了建议先做异常检测或者换思路。数据量别太大。k-means本身的复杂度摆在那儿几百万条多数类样本硬聚类一次耗时和内存都不小。前置条件方面特征必须是数值型而且最好做完标准化再喂给k-means。字符型特征要编码转换量纲差太大的要先缩放这两点后面细说。2. 算法原理与Matlab实现细节2.1 k-means聚类的核心思想k-means的原理很简单给一堆点你告诉它想分成k组它随机挑k个中心然后反复迭代——每个点归属到离它最近的中心再把中心移到该组所有点的均值位置直到中心不再大幅移动。最终结果就是k个簇每个簇内部点尽量紧凑簇与簇之间尽量分离。这个算法有两个参数最让人头疼k怎么定、初始中心怎么选。k定太大会把本来就紧凑的簇硬生生切开k定太小就会把多个不同的子结构混在一起。初始中心如果随机选得不好迭代可能落在局部最优解上。Matlab的kmeans函数提供Replicates参数就是跑多次挑误差最小的结果用来对冲初始中心的随机性后面代码里会用到。在欠采样场景里k-means的作用不是“找出数据真实类别”而是帮我们做密度感知的样本选择。每个簇代表多数类分布里的一个局部区域从每个区域选一个代表就能用较少样本覆盖整个分布。2.2 聚类欠采样的完整流程整个算法流程我梳理成五步每一步都可以独立调整优化把原始训练集按标签拆成多数类集合和少数类集合。确定k值也就是要把多数类压缩成多少个代表簇。最常用的策略是k 少数类样本数这样最终多数类和少数类数量能大致对等。对多数类全体样本执行k-means聚类得到k个簇和k个质心。在每个簇内选一个代表样本。可选策略取离质心最近的原始样本nearest、直接用质心本身centroid、随机抽一个random。把选出的k个多数类代表与全部少数类样本合并得到平衡后的新训练集。第4步的三个策略各有各的适用情况nearest模式保留的是真实存在的样本后期做KNN、决策树这类模型时边界更自然centroid模式是合成样本跟SMOTE有点像但可能引入原本不存在的组合特征random模式最简单界于两者之间代码量也最少。2.3 质心模式与最近邻模式怎么选我在实际项目里最常用的是nearest模式也就是每个簇取离质心最近的原始样本。原因很直接欠采样的目的是“压缩多数类”不是“生成多数类”。如果直接拿质心去当样本质心可能是多个原始样本特征的平均值会造出一些现实中不存在的过渡样本对后续模型推理可能会有误导。比如某个簇由两类行为模式组成质心的特征值正好落在两者中间现实中用户根本不会出现这种特征组合模型训练出了一个虚幻样本。而nearest模式选出来的点一定是真实观测值特征组合一定在原始空间中出现过模型的泛化更稳。centroid模式也有它的一席之地当你后续用的分类器本身对噪声比较敏感比如SVM质心样本能起到平滑作用减少极端点对决策面的拉扯。但这种模式我更推荐在数据量特别大、多数类内部噪声严重时使用。random模式我基本只用来做对照实验。它可以看作随机欠采样的“聚类版本”——把随机范围限制在簇内至少能保证每个区域都被覆盖不会出现某个子结构被整体删没的情况。2.4 特征标准化做k-means前必须跨过的门槛这一步太容易被忽略了但真的会影响结果。k-means的核心是欧氏距离如果特征的量纲差异很大距离计算会被量纲大的特征主导。举个例子两个特征一个是数值范围0到1的归一化比率另一个是范围0到100000的收入。计算距离时收入这个特征几乎决定了距离大小聚类结果完全被收入牵着走另一个特征在簇划分上基本没有话语权。这会导致聚类结构失真最后选出来的代表样本也会偏科。所以我的习惯是进入kmeans之前先对特征矩阵做Z-score标准化zscore(X)让每个特征均值为0、标准差为1。标准化之后每个特征在距离计算里权重基本一致聚类结果才反映真实的分布结构。有一个坑必须提醒如果后续分类器也要用标准化特征一定要做在重采样之前或统一做在同一个Pipeline里绝对不能对训练集和测试集分别独立标准化。正确做法是计算训练集的均值和标准差再用同样的参数去转换测试集否则会出现数据泄露测试结果虚高。3. 完整Matlab源码与参数调优3.1 可直接调用的主函数源码下面这个函数我命名为kmeans_undersample输入原始特征X和标签y输出重采样后的特征和标签。代码里加了很多注释直接复制到Matlab里就能跑。我平时用的是R2021b之后的版本里面用到的都是基础函数老版本也能兼容。function [X_res, y_res] kmeans_undersample(X, y, k, mode) % 基于k-means聚类的欠采样 % 输入 % X - 特征矩阵每行一个样本 % y - 标签列向量1表示少数类0表示多数类 % k - 对多数类设置的聚簇数默认等于少数类样本数 % mode - nearest 取簇内离质心最近的真实样本默认 % centroid 直接用质心作为多数类代表 % random 每个簇随机取一个样本 % 输出 % X_res、y_res - 重采样后的特征矩阵和标签 if nargin 2 error(至少需要X和y两个输入); end % 分离多数类和少数类 X_major X(y 0, :); X_minor X(y 1, :); n_major size(X_major, 1); n_minor size(X_minor, 1); if n_minor 0 error(少数类样本数为0请检查标签定义); end % 聚类数默认取少数类样本数但不超过多数类样本数 if nargin 3 || isempty(k) k n_minor; end k min(k, n_major); if nargin 4 || isempty(mode) mode nearest; end % 对多数类执行k-means聚类 % Replicates5意味着跑5次选误差最小的结果缓解初始中心随机性问题 [idx, centroid] kmeans(X_major, k, ... Replicates, 5, ... MaxIter, 300, ... EmptyAction, singleton, ... Display, off); % 预分配输出空间避免循环中矩阵不断增长导致性能下降 X_major_new zeros(size(centroid)); n_new 0; for i 1:k clusterIdx find(idx i); if isempty(clusterIdx) continue; end switch mode case nearest % 计算簇内每个样本到质心的距离平方取最近的 dist2 sum((X_major(clusterIdx, :) - centroid(i, :)).^2, 2); [~, minPos] min(dist2); X_major_new(n_new 1, :) X_major(clusterIdx(minPos), :); case centroid X_major_new(n_new 1, :) centroid(i, :); case random randPos randi(length(clusterIdx)); X_major_new(n_new 1, :) X_major(clusterIdx(randPos), :); otherwise error(未知mode%s可选nearest/centroid/random, mode); end n_new n_new 1; end % 去掉预分配中未使用的行如果有空簇会发生 X_major_new X_major_new(1:n_new, :); % 合并多数类代表与少数类 X_res [X_major_new; X_minor]; y_res [zeros(n_new, 1); ones(n_minor, 1)]; end这段代码的核心逻辑就是聚类、选代表、合并。每一部分都可以单独拆出来改比如把nearest改成“取簇内所有样本的均值”之类的扩展性很强。3.2 聚类数K的确定方法k的取值直接决定采样后多数类剩下多少样本是整套方法里最需要动脑筋的参数。最常规的做法是k 少数类样本数这样重采样之后多数类和少数类刚好一样多数据完全平衡。但这不是唯一答案也未必是最优解。如果少数类只有20个样本你硬把多数类压缩成20个代表信息损失会非常大模型泛化能力反而下降。这时候我会把k放大一些比如取少数类样本的2到3倍让多数类保留更多结构分类器在“有点不平衡但不完全失衡”的数据上也能跑得很好。判断k到底合不合适我有个土办法聚类完之后可视化一下。Matlab里直接gscatter看原始多数类分布再用不同颜色标出每个簇的代表样本如果某个簇的最大直径明显压缩过度代表点分布跟原分布偏差很大就该加大k。还有一个常见策略是k round(sqrt(n_major))。这个数是经验法则相当于把多数类压缩到平方根量级适合少数类样本数很多的情况。它不追求完全平衡只追求砍掉一部分冗余多数类、保留主体结构。具体用哪个还是要结合业务目标试。3.3 围绕K-means欠采样的三个进阶变体基础版做完了如果你想把效果再往上顶一顶可以试试这三个变体。第一个变体聚类后按比例抽取。大k虽然能精细刻画分布但每个簇只抽一个样本会显得浪费。改成先聚成较少的簇比如k 50然后根据每个簇包含样本数占总多数类样本数的比例决定这个簇抽几个代表样本。这样既保留了簇结构又能在密度高的地方多采样稀疏处少采样。第二个变体聚类欠采样 SMOTE混合。先用k-means欠采样把多数类大幅压缩再用SMOTE在少数类附近生成一些合成样本。两条腿走路的效果在很多数据集上比单用任何一种都要好尤其适合少数类本身内部也有多样性的情况。第三个变体多轮聚类集成。把多数类分成多个子集对每个子集分别聚类采样得到多份平衡数据然后用每份平衡数据各训练一个分类器最后投票集成。这个思路跟随机森林里的bagging类似能够显著降低单次欠采样的随机性对模型的影响代价是训练时间翻好几倍。4. 实验对比与效果评估4.1 评估指标怎么选别被准确率忽悠处理不平衡数据再用accuracy当核心指标就是在自欺欺人。一定要换成能同时反映少数类识别情况的指标。我日常看的指标主要有四个精确率Precision预测为少数类的样本里真正是少数类的比例。这个指标衡量的是“抓得准不准”。召回率Recall真实少数类中被成功找出来的比例。这个指标衡量的是“抓得全不全”。F1分数精确率和召回率的调和平均。当两者有冲突时F1能给出一个综合判断。ROC-AUC不依赖阈值直接看分类器把少数类排在前面的能力。输出概率越能区分两类AUC越高。很多业务场景更看重召回率。比如信用卡欺诈抓不到欺诈客户的代价远高于冤枉几个正常用户所以我会优先看召回率和F1。但在决策时也要看精确率否则模型会把大量正常样本误判为欺诈业务团队会烦死你。这几个指标配合起来看才能反映模型真实水平。4.2 在合成不平衡数据上的一次实操对比我这边用一组合成数据做了一个完整对照实验方便你理解整个流程。数据构造多数类800个样本本身分两个团簇均值分别在(1.5, 1.5)和(-0.5, -0.5)少数类50个样本团簇中心在(0.5, 0.5)与多数类部分重叠。这样的分布能够模拟真实场景中正负类边界模糊的情况。完整测试代码如下复制到Matlab就能跑%% 生成不平衡合成数据 rng(42); n_major 800; n_minor 50; X_major [randn(n_major/2, 2) * 0.6 [1.5, 1.5]; ... randn(n_major/2, 2) * 0.8 [-0.5, -0.5]]; y_major zeros(n_major, 1); X_minor randn(n_minor, 2) * 0.5 [0.5, 0.5]; y_minor ones(n_minor, 1); X [X_major; X_minor]; y [y_major; y_minor]; %% 先划分训练集和测试集再重采样顺序不能错 cv cvpartition(size(X, 1), HoldOut, 0.3); X_train X(training(cv), :); y_train y(training(cv)); X_test X(test(cv), :); y_test y(test(cv)); %% 训练三个模型原始数据、随机欠采样、k-means欠采样 % 原始数据直接训练 mdl_raw fitcknn(X_train, y_train); % 随机欠采样 n_pos sum(y_train 1); negIdx find(y_train 0); randIdx randperm(length(negIdx), min(n_pos, length(negIdx))); X_rand_bal [X_train(negIdx(randIdx), :); X_train(y_train 1, :)]; y_rand_bal [zeros(length(randIdx), 1); ones(n_pos, 1)]; mdl_rand fitcknn(X_rand_bal, y_rand_bal); % k-means欠采样 [X_km_bal, y_km_bal] kmeans_undersample(X_train, y_train, [], nearest); mdl_km fitcknn(X_km_bal, y_km_bal); %% 评估 pred_raw predict(mdl_raw, X_test); pred_rand predict(mdl_rand, X_test); pred_km predict(mdl_km, X_test); metrics_raw calcMetrics(y_test, pred_raw); metrics_rand calcMetrics(y_test, pred_rand); metrics_km calcMetrics(y_test, pred_km); disp(metrics_raw); disp(metrics_rand); disp(metrics_km); %% 评价函数 function metrics calcMetrics(yTrue, yPred) C confusionmat(yTrue, yPred); if size(C, 1) 2 C(2, 2) 0; end 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); metrics table(precision, recall, f1); end在我这边跑出来的结果大概是这样方案精确率召回率F1原始不平衡数据0.620.310.41随机欠采样0.550.740.63k-means欠采样0.630.820.71原始数据召回率只有0.31说明一大半少数类都没抓到。随机欠采样把召回率拉到了0.74但精确率掉到了0.55说明误报也不少。k-means欠采样在召回率0.82的同时精确率还能保持在0.63左右F1从0.41涨到0.71整体提升非常明显。这个对比验证了一件事k-means欠采样之所以强不是因为它能凭空变出信息而是它把多数类删掉的那部分“冗余样本”控制在了合理范围保留了更多有效决策信息。少数类边界样本在多数类结构里原本被淹没欠采样之后边界露出来了模型自然更容易学。4.3 训练集测试集划分的先后顺序这个坑我踩过不止一次必须单独拎出来讲。很多新手拿到数据先把整个数据集做了欠采样再划分训练集和测试集。这个顺序是错的而且是严重错误。重采样只能作用于训练集测试集必须保持原始数据的真实分布。因为上线之后模型面对的永远是天然不平衡的数据测试集的作用是模拟真实环境。如果你把测试集也重采样了测试结果会显得特别漂亮但一到线上就崩因为线上分布和测试集分布根本对不上。正确流程是先cvpartition划分训练集和测试集然后在训练集上做欠采样测试集保持原样。我在上面的代码里特意把这一步放在最前面用了注释强调。这个顺序无论用什么欠采样、过采样方法都适用你可以把它当成黄金法则记下来。5. 常见问题与避坑手册5.1 空簇、随机性和标准化问题先说说我实际使用中遇到的三个高频问题。第一个是空簇。当k设置得接近多数类样本数或者数据本身有大量重复点k-means可能产生空簇——某些初始中心离所有点都远迭代后没有点归到它。这个问题在代码里我用了EmptyAction, singleton来缓解它会自动把离质心最远的点分配给空簇。但极端情况下空簇还是可能出现所以函数里循环部分保留了isempty(clusterIdx)的判断防御性处理不会报错。如果空簇次数很多优先考虑调低k。第二个是k-means随机性。相同数据跑两次kmeans结果可能不同因为初始中心随机选择。我在函数里设置了Replicates, 5相当于跑5次并保留误差最小的一次能大幅降低随机性。但如果要求每次结果完全一致要在调用前加上rng(42)固定随机种子。这样别人复现你的实验结果时不会对不上。第三个还是标准化问题。我再次强调k-means对特征量纲极其敏感。如果数据里有收入这种量级很大的特征还有0到1的小特征聚类结果基本被大特征主导。务必先zscore标准化再进k-means。不少读者写完代码发现聚类效果差排查半天最后往往就是这一步的问题。5.2 大数据量下的效率问题当多数类样本量超过几十万甚至百万级时直接跑kmeans会非常慢内存也可能爆。我碰到过一个场景多数类样本40多万条聚成500个簇单次kmeans跑了十几分钟加上Replicates5整个流程半小时起步。这种情况下我的改法是先把多数类随机抽取一个子集比如5万条在子集上做k-means得到质心后再把剩余多数类样本分配到最近的质心。这样聚类阶段的计算量大幅下降质心的结构基本能保留。另外一个办法是设置Options, statset(UseParallel, true)开启并行池多核环境下能快不少但不一定所有的Matlab版本都默认支持并行工具箱。还有个容易被忽略的优化点避免循环拼接矩阵。早期版本我的代码里用了X_major_new [X_major_new; ...]这种动态增长写法样本一多就卡顿。现在的代码改用zeros(size(centroid))预分配速度提升非常明显。如果你的数据量很大这个细节值得留意。5.3 与SMOTE结合的混合策略单独用k-means欠采样和单独用SMOTE两类方法各有短板。SMOTE在少数类极稀疏时容易生成大量重叠甚至噪声样本欠采样在少数类极稀疏时容易把多数类砍过头。所以我现在更推荐的做法是混合先对多数类做k-means欠采样压缩到原始数量的1/2到2/3再用SMOTE把少数类放大让两边达到平衡。这个组合的好处是两头都不过激。多数类没有被砍到太狠保留了更多结构信息少数类也不是纯靠插值撑大而是在真实样本基础上合理扩充。我在好几个业务数据集上测过混合策略的F1通常比单独任何一种高出3到5个百分点。注意SMOTE在Matlab里没有内置函数需要自己写或者用第三方实现。网上有不少SMOTE的Matlab实现逻辑也不算复杂找少数类样本的k近邻在连线上随机插值。如果你需要我后面也可以单独写一篇SMOTE的源码实现把这套流程串起来。最后再分享一个体会k均值聚类欠采样这个方法看着原理不复杂但真正用好的关键在于对数据的理解。多数类内部到底是什么结构、边界在哪里、k取多少合适这些都要结合具体业务去调。别指望一个参数跑遍所有数据集。在你自己的数据上跑一遍感受一下聚类把数据“折叠”之后再采样的效果比光看文章直观得多。多数类聚类究竟该取多少次代表、最近邻还是质心效果好最后还是要拿你的业务数据来说话。本文还有配套的精品资源点击获取
返回列表