ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SMOTE算法原理与Matlab实现:解决类别不平衡问题的实战指南

2026/9/9 21:08:50 拓冰建站 浏览量
SMOTE算法原理与Matlab实现:解决类别不平衡问题的实战指南 简介这份资源提供合成少数类过采样技术SMOTE的MATLAB完整实现面向机器学习、数据挖掘及金融风控等需要处理不平衡数据的开发者。该算法通过K近邻线性插值合成少数类样本能有效缓解类别失衡造成的模型偏向问题提升对少数类的识别与泛化能力。压缩包内共5个文件包括SMOTE.m主函数、SMOTETest.m测试脚本、LICENCE.txt许可说明及Git相关配置文件整体仅约3KB代码紧凑、结构清晰便于直接调用或按需修改。通过主函数与测试脚本对照阅读可快速理解抽样倍率、近邻个数等参数设置对合成样本的影响从而在实际项目中灵活调整。已有1321人学习下载覆盖了算法实现与测试调用的完整流程尤其适合刚接触不平衡学习、希望快速在MATLAB环境中完成数据平衡处理的读者参考。 做数据建模的时候类别不平衡这个问题真的太常见了。信贷欺诈样本、设备故障记录、罕见病诊断哪个场景里“好样本”都是一抓一大把“坏样本”却稀少得像大熊猫。如果你直接拿这种数据去训练分类模型模型大概率会变成“懒惰的学渣”——反正全预测成多数类准确率也能轻松到95%以上但你真的敢用这个模型去抓欺诈吗肯定不敢。解决这个问题的思路无非两条一是让模型自己学会关注少数类代价敏感学习、异常检测二是在数据层面动手脚人为地把少数类样本“变多”。数据层面最经典的方法就是SMOTE全称Synthetic Minority Over-sampling Technique合成少数类过采样技术。这篇博文就把SMOTE的原理和Matlab实现从头到尾掰开揉碎讲清楚同时给出可直接运行的代码、调参经验和避坑指南。不管你是刚接触机器学习的新手还是已经在用Matlab做数据建模的工程师看完都能直接用起来。1. SMOTE算法原理为什么它不是简单复制1.1 类别不平衡问题到底有多坑先说个典型的痛感场景。假设你手里有1万条银行贷款记录其中9500条是正常还款500条是逾期坏账。你吭哧吭哧训练了一个逻辑回归模型测试集准确率96%。老板问效果怎么样你沾沾自喜结果一看混淆矩阵傻眼了——500个坏账客户你一个都没识别出来模型把所有人都预测成“正常还款”。这不是模型坏了而是数据本身在“喂毒”。绝大多数分类算法都是通过最小化整体误差来学习的少数类样本占比太小对损失函数的贡献微乎其微算法优化的时候根本“看不上”它们。所以如果你的评估指标只有accuracy这种坑根本看不出来。这也是我一直强调的不平衡数据必须看Recall、Precision、F1或者AUC别死盯准确率。1.2 SMOTE的核心思路在样本之间“造”样本既然少数类样本太少最简单的想法就是随机复制几份原来的样本但这有个致命问题——复制出来的样本和原样本完全一样模型学到的还是同一个点相当于把一个人反复拉来面试本质上没有带来任何新信息反而容易过拟合。SMOTE的思路就巧妙得多。它不复制原始样本而是在少数类样本和它的近邻样本之间通过线性插值“合成”出全新的样本。打个比方随机过采样是让同一个演员反复换衣服上场SMOTE则是让两个演员各自贡献一点特征“生”出一个全新的角色。新样本保留了两个旧样本的局部特征但又和它们不完全一样等于在特征空间里真正地“扩军”了。1.3 SMOTE、随机过采样、欠采样一次说清先上一张对比表把三种常见的数据平衡方案放在一起看方法做法优点缺点随机欠采样随机丢弃多数类样本数据集变小训练快丢失大量信息模型易欠拟合随机过采样随机复制少数类样本实现最简单严重过拟合模型泛化差SMOTE在近邻之间线性插值生成新样本缓解过拟合信息增益大可能生成噪声样本K值敏感从表里能直观看出SMOTE的核心优势在于“创造新信息”。但它也不是万能的如果少数类样本本身分布就很稀疏、被多数类重重包围SMOTE插值出来的点可能落在多数类区域里反而成了噪声。这个问题后面我会细说怎么规避。2. Matlab环境下的实现思路拆解2.1 为什么在Matlab里实现而不是Python这问题几乎每次都会被人问。Python的imbalanced-learn库一条命令就能调SMOTE为什么要自己用Matlab写我的答案很简单第一很多做信号处理、图像处理、控制系统的工程师整个流程都在Matlab里就为调个SMOTE还要开Python环境数据还得来回导纯属给自己找事。第二Matlab的矩阵运算和向量化编程能力让SMOTE这种“批量插值”任务写起来非常清爽。第三自己写一遍SMOTE你会对整个算法的每个细节了如指掌以后想改成Borderline-SMOTE或者ADASYN也只是顺手的事。2.2 算法执行流程逐步拆解用标准的伪代码来说SMOTE执行一共五步输入少数类样本矩阵X维度是n×dn个样本每个样本d维特征设定近邻数K和过采样倍率NN为整数。计算X中每个样本到其他所有样本的欧氏距离构建距离矩阵。对每个样本找出距离最近的K个近邻。对于每个样本x从它的K个近邻里随机选一个近邻x在x和x的连线上随机取一个点公式是x_new x rand × (x - x)。其中rand是0到1之间的随机数。重复第4步N-1次因为原始样本还要保留1份把所有生成的新样本拼接回原始数据集。从公式也能看到SMOTE生成的新样本一定位于两个真实样本点的连线段上。这保证了新样本不会漂移到特征空间里完全没人的区域同时也决定了它无法扩展样本的全局分布边界——它只在已知样本之间的区域里“补充兵力”。2.3 距离矩阵计算一个隐藏的性能杀手SMOTE最耗时的地方在于距离矩阵计算。如果少数类样本有n个计算所有两两之间的距离复杂度是O(n²d)。在Matlab里很多新手会用双重for循环暴力计算样本量几千的时候就慢得让人想砸电脑。正确做法是直接用pdist2函数它是C语言底层实现计算速度比自己写循环快出几个数量级。如果是更大规模的数据还可以只把少数类样本的距离矩阵算出来就好不用碰多数类样本能省下大量内存。3. 实操过程一步一步写出可用的Matlab代码3.1 完整函数代码下面是我自己常用的SMOTE封装函数直接复制到Matlab里就能用function [X_syn, y_syn] smote(X, y, N, k) % SMOTE 合成少数类过采样技术 % 输入: % X - 少数类样本特征矩阵n×d % y - 少数类样本标签列向量n×1 % N - 过采样倍率整数最终少数类数量 N × n % k - 近邻数量默认5 % 输出: % X_syn - 合成样本特征 % y_syn - 合成样本标签全部为少数类标签 if nargin 4 k 5; end if nargin 3 error(至少要提供X、y、N三个参数); end [n, d] size(X); N round(N); if N 1 error(过采样倍率N必须大于等于1); end if k 1 || k n error(近邻数k必须大于1且不能超过样本数); end % 计算少数类样本之间的欧氏距离矩阵 D pdist2(X, X, euclidean); % 将每个样本到自身的距离设为无穷大避免被选为近邻 for i 1:n D(i, i) Inf; end % 按距离排序取前k个近邻的索引 [~, idx] sort(D, 2); knn_idx idx(:, 1:k); % 预分配合成样本矩阵 total_new n * (N - 1); X_syn zeros(total_new, d); cnt 0; % 对每个少数类样本循环生成N-1个新样本 for i 1:n for j 1:(N-1) % 随机选择一个近邻 neighbor knn_idx(i, randi(k)); % 随机插值系数0到1之间 gap rand; % 线性插值生成新样本 cnt cnt 1; X_syn(cnt, :) X(i, :) gap * (X(neighbor, :) - X(i, :)); end end y_syn repmat(y(1), total_new, 1); end3.2 验证实验造一个不平衡数据跑一遍光有函数不行得验证一下效果。我在Matlab里用mvnrnd函数构造了一个二维的两类分布数据一类3000个样本一类100个样本% 构造不平衡数据 rng(42); major mvnrnd([2, 2], [1, 0.2; 0.2, 1], 3000); minor mvnrnd([5, 5], [0.8, 0.1; 0.1, 0.8], 100); X [major; minor]; y [zeros(3000, 1); ones(100, 1)]; % 提取少数类样本 minor_idx find(y 1); X_minor X(minor_idx, :); % 调用SMOTE倍率4最终少数类变成400个 [X_syn, y_syn] smote(X_minor, y(minor_idx), 4, 5); % 合并回完整数据集 X_bal [X; X_syn]; y_bal [y; y_syn]; % 验证数量 fprintf(原始少数类:%d → 合成后:%d\n, sum(y1), sum(y_bal1));跑完之后可以看到少数类从100变成了400目标达成。我习惯在合成后画一张散点图对比原始样本和合成样本的分布。你会发现合成的样本确实沿着真实样本之间的连线分布不会乱跑但也会略微“侵入”到原样本分布的空隙区域这正是SMOTE扩充决策边界的机制。3.3 嵌入机器学习流程的完整示例拿到了平衡后的数据下一步自然是训练分类器。Matlab里最简单的例子是用fitcknnK近邻分类器对比平衡前后的效果% 划分训练测试集原始不平衡数据集 cv cvpartition(y, HoldOut, 0.3); X_train_orig X(training(cv), :); y_train_orig y(training(cv), :); X_test X(test(cv), :); y_test y(test(cv), :); % 原始数据训练 mdl_orig fitcknn(X_train_orig, y_train_orig, NumNeighbors, 5); pred_orig predict(mdl_orig, X_test); recall_orig sum(pred_orig1 y_test1) / sum(y_test1); fprintf(原始数据Recall: %.3f\n, recall_orig); % SMOTE平衡后训练注意只对训练集做SMOTE测试集保持原始分布 minor_train_idx find(y_train_orig 1); X_minor_train X_train_orig(minor_train_idx, :); [X_syn_train, y_syn_train] smote(X_minor_train, y_train_orig(minor_train_idx), 3, 5); X_train_bal [X_train_orig; X_syn_train]; y_train_bal [y_train_orig; y_syn_train]; mdl_bal fitcknn(X_train_bal, y_train_bal, NumNeighbors, 5); pred_bal predict(mdl_bal, X_test); recall_bal sum(pred_bal1 y_test1) / sum(y_test1); fprintf(SMOTE后Recall: %.3f\n, recall_bal);这里有个非常关键的细节必须强调对测试集绝不能做SMOTE。测试集是用来模拟真实场景的真实场景里少数类就是稀缺的SMOTE只能在训练集上做。我见过不少人把SMOTE放在整个数据集上处理之后再划分训练测试集这会造成数据泄露评估结果虚高得离谱一上真实场景就翻车。4. 参数调优、常见问题与排查技巧4.1 K值怎么选K值决定了每个样本参考多少个近邻来生成新样本默认取5但这不是死的。如果少数类样本量本身就很少比如只有10来个K值千万别设太大。K5的时候有些样本的近邻可能都混入了不同簇的样本插值出来的新样本会变得不伦不类反而破坏数据结构。这时候我建议K取3甚至2宁可比常量少一点也要保证样本“纯度”。反过来如果少数类样本量还可以100以上K值取5到7都行。遇到高维数据距离越来越不靠谱也就是维度灾难K值适量调大一点能平滑掉一部分随机性。4.2 过采样倍率N怎么定N是最终想要的少数类数量和原始少数类数量的比值。比如N3最终少数类数量变成原来的3倍。但这个值不是越极端的。多数类有9000个少数类有100个你是不是想直接把N干到90凑成1:1别急我踩过这个坑。当N特别大的时候你相当于在少数类样本之间疯狂插值生成的样本越来越像原始样本的“近亲”特征多样性急剧下降过拟合风险反而更大。我的经验是先过采样到少数类占多数类的30%到50%配合其他手段调整分类器权重、阈值优化一起用。如果你必须做到1:1建议同时考虑用集成方法比如EasyEnsemble配合SMOTE而不是单纯依赖过采样。4.3 常见报错与处理速查表整理几个我实际用Matlab写SMOTE时遇到的问题很多人应该也会碰到现象可能原因解决方案报错“索引超出矩阵维度”近邻数k设为n或超过nk必须小于少数类样本总数生成的新样本全是原始样本gap一直取到0或1代码逻辑错误检查rand调用位置确保每个样本每次生成都重新取随机数距离矩阵全是Inf或NaN特征中有NaN值先清洗数据用rmmissing或fillmissing处理运行极慢用双重for循环计算距离改用pdist2函数合成后分类器效果反而变差数据中存在大量离群点SMOTE插值引入了噪声先做离群点剔除或者改用Borderline-SMOTE4.4 高维数据和类别重叠严重时的应对SMOTE在高维数据上表现一般这个要提前做好心理建设。特征维度几十上百的时候两点之间的欧氏距离几乎都差不多找到的“近邻”可能并不可靠。我处理这类问题会先做降维PCA、t-SNE都行在降维后的特征空间里做SMOTE再回到原始空间训练模型。如果类别重叠严重SMOTE合成的样本落进多数类区域此时我应该用Borderline-SMOTE或者ADASYN——它们只会在“边界区域”的少数类样本附近生成新样本远离中心区域更克制噪声也少。5. 一些在实战中必须要说的坑5.1 SMOTE不是万能的我见过不少人把SMOTE当成万能药一遇到不平衡就无脑上SMOTE结果效果还不如什么都不做。原因是SMOTE本质上是在“造数据”它造出来的数据是否可信完全取决于原始少数类样本的质量和分布。如果少数类样本收集本身存在严重偏差或者样本量少到只有个位数SMOTE再怎么插值也变不出有效信息。这种情况下我更推荐改用异常检测思路或者用更先进的生成模型比如基于变分自编码器VAE来生成新样本。5.2 分类阈值和下采样可以一起上SMOTE解决了数据层面的不平衡但算法层面还能做优化。以二分类逻辑回归为例模型输出的概率默认以0.5为判定边界但平衡后的数据中少数类先验概率依然远低于0.5这时候把判断阈值往下降比如0.3往往能显著提升少数类召回率。Matlab里可以用fitclinear训练逻辑回归后遍历多个阈值找F1最高的点。另外也别忽略了多数类下采样的价值。当多数类样本量特别大的时候可以先用聚类把多数类分成几簇再从每个簇里按比例抽取一部分样本这样既能减少多数类数量又能保住多数类的分布结构和SMOTE配合使用效果相当不错。5.3 代码级别的细节优化再分享两个代码层面的小技巧。第一预分配矩阵空间。上面的代码里我已经用zeros提前分配好了X_syn如果不做这一步Matlab会在循环里动态扩展矩阵数据量大的时候内存碎片化严重会让程序变得极慢。第二随机数种子管理。如果你跑实验需要可复现的结果每次调用SMOTE之前用rng固定种子不然每次生成的合成数据都不一样对比实验就没法做了。最后说一句真心的整个SMOTE的实现其实并不复杂难的是理解它什么时候有效、什么时候无效、怎么和上下游衔接。希望这篇内容能让你不仅会用代码更懂背后的原理。如果你在跑代码的过程中遇到报错或者想交流调参经验欢迎在评论区留言我看到都会回复。本文还有配套的精品资源点击获取