资讯详情

SMOTE算法详解与Matlab实现:解决类别不平衡分类问题

📅 2026/9/9 21:03:53 | 华诺云谱 👁 阅读
SMOTE算法详解与Matlab实现:解决类别不平衡分类问题
简介面向不平衡数据分类场景的SMOTE算法MATLAB实现适合机器学习与数据挖掘开发者应对少数类样本不足的分类问题也适用于金融风控、医疗诊断等常见类别不均衡场景。压缩包仅3KB含5个文件其中2个M函数文件分别提供算法主体与可运行测试脚本另有许可证及Git版本管理配套说明。已有1321人学习使用。源码基于SMOTE原始思想完整呈现K近邻搜索、随机选邻、线性插值合成少数类样本的核心流程测试脚本便于直接复现效果通过对比合成前后数据集规模与模型指标可直观理解算法对平衡性的改善代码结构简洁、依赖低方便结合具体数据集调整近邻数和合成倍率也可作为组件集成到后续的分类模型中。整体小巧实用兼顾教学参考与工程二次开发。 在做分类任务的时候类别不平衡问题几乎躲不掉。我记得有一次做信贷风控项目好客户占比超过98%坏客户不到2%。第一版逻辑回归跑出来测试集准确率报出99%的漂亮数字可我盯了一眼混淆矩阵就沉默了——坏客户一个都没逮住全被模型当成了好人。准确率这指标在极度不平衡的数据上基本就是自欺欺人。那段时间我认真研究并落地了SMOTE算法全称 Synthetic Minority Over-sampling Technique合成少数类过采样技术也把它在Matlab里完整写了一遍、反复验证过。这篇就围绕SMOTE的Matlab实现展开从原理、代码、评估到踩坑和进阶思路一次性讲透。这篇文章的内容既适合正在处理不平衡分类问题、想在Matlab里直接复现SMOTE的同学也适合那些已经用过SMOTE但疑惑为什么我加了合成样本效果反而变差的人。我会把实现细节、参数选择、验证方法和常见的坑全部掰开来说。1. 先搞明白SMOTE到底在解决什么问题1.1 一个准确率99%的模型为什么会失效很多人刚接触分类任务时下意识就把准确率当成最重要的指标。而在类别极不平衡的场景下这个指标几乎没什么价值。设想一个场景10000条样本里只有200条是正类哪怕模型把所有样本都判成负类准确率依然有98%。但这样的模型在生产环境里等于废物因为它没有识别出任何正类样本该预警的没预警该拦截的没拦截。我当时排查那个信贷模型时把概率阈值从0.5一路往下调从0.3调到0.1模型才开始放出几个正类预测但精确率惨不忍睹。这说明模型根本没有从特征里学到正类样本的分布规律而是被大量负类样本带偏了。更本质的问题是数据量太少模型无法从200个正样本里提取出足够稳定的模式。1.2 SMOTE的设计思路合成样本不是复制粘贴对类别不平衡的处理思路粗分两大流派一是从算法层面做代价敏感学习给少数类更高的误判代价二是从数据层面做重采样。数据层面又分欠采样删负类和过采样增正类。最简单的过采样是直接复制少数类样本这种做法我一开始也试过模型确实会多关注正类但本质上只是在重复已有的信息容易导致过拟合泛化能力几乎没有提升。SMOTE的核心区别在于它不是复制现有样本而是在特征空间中通过插值合成全新的样本。它会选取少数类样本的K个近邻然后在这条连线上随机生成新样本点。换句话说SMOTE是在少数类样本的周围重新补充了一个分布上合理的新点这让后续训练的模型能看到更丰富的正类样本形态而不是反复背诵那几张旧面孔。从结果来看SMOTE能有效缓解少数类样本稀疏导致的学习不充分问题同时因生成样本不是简单的重复过拟合风险也比朴素过采样小得多。这也是它成为不平衡学习领域经典算法的根本原因。2. 核心原理拆解KNN找邻居与线性插值造数据2.1 三步流程从少数类样本出发造出新样本SMOTE的算法流程并不复杂一共三步。假设当前我们要对少数类样本集做 (N) 倍过采样每个原始样本需要生成 (N) 个新样本对少数类样本集合中的每个样本 (x_i)用欧氏距离找出它在少数类样本中的 (k) 个近邻从这 (k) 个近邻中随机抽取一个邻居 (x_{near})在 (x_i) 与 (x_{near}) 之间的线段上随机取一个插值点作为新样本 [ x_{new} x_i \lambda \cdot (x_{near} - x_i) ] 其中 (\lambda) 是 ([0, 1]) 区间上的均匀随机数。这一步的几何意义非常直观新样本一定落在少数类样本与其近邻连线之间的区域内不会跑到特征空间里离谱的位置。换句话说SMOTE是在少数类样本云团的内部和边缘做填充让这个类别的样本密度变大、形态更完整。这里的 KNN 近邻数是SMOTE的核心超参数日常默认取 (k5)。但如果少数类样本总量特别少比如只有十几条那 (k) 要相应调小甚至取2或3否则近邻中会混入距离很远的样本插值出来的点容易偏离真实分布。2.2 为什么插值参数要随机关于 (\lambda) 的取值我在初学的时候有个疑问为什么不固定取中点这样生成的样本看起来更居中、更稳定后来结合实验结果想明白了。如果每次插值都取固定比例生成的样本会全部落在一条条固定的连线上形态高度相似多样性不足模型照样容易过拟合。而 (\lambda) 随机从 ([0,1]) 中取值每次生成的样本在连线上的位置随机浮动同一对样本点可以派生出无数个位置不同的新样本。这不仅增加了样本数量还增加了样本多样性让模型在决策边界附近学到的形态更平滑。不过要注意论文原版里 (\lambda) 是完全均匀随机。实际工作中偶尔会把 (\lambda) 限制在 ([0.2, 0.8]) 之间避免新样本与某个原始样本靠得太近减少噪声。这个根据数据特点自行调整就好没有绝对的对错。2.3 k值和采样倍率N的实用取值建议SMOTE 有两个主要参数近邻数 (k) 和采样倍率 (N)。(k) 值的选择依赖少数类样本总量。经验上看当少数类样本只有几百条时(k) 取 5 是稳妥的起步值样本量特别少小于50条时建议降为 3如果数据噪声较大可以适当增大 (k)让插值样本更偏向密集区域减少随机性。(N) 的确定取决于你想把少数类样本扩到多少。假设少数类有 200 条多数类有 9800 条若 (N10)则少数类变成 2000 条正负比变成约 1:4.9已经比原来的 1:49 健康很多。通常建议过采样后的少数类占总体比例为 20% 到 40% 即可没有必要追求完全50%对50%的比例。过高的过采样比例会让模型死记少数类的局部噪声通常会带来过拟合。3. Matlab完整实现核心函数与调用示例3.1 基础版SMOTE函数pdist2加速距离计算Matlab 里实现 SMOTE 最方便的地方在于内置了pdist2函数可以一次性算出所有少数类样本两两之间的距离矩阵省去自己写双层遍历的麻烦。下面是可直接复制运行的函数function X_new smote_minority(X, k, N) % SMOTE 对某个类别做合成过采样 % 输入 % X - 该类样本矩阵m行d列每行是一个样本 % k - 近邻数量默认 5 % N - 每个原始样本要生成的新样本数量默认 1 % 输出 % X_new - 合成样本矩阵m*N行d列 if nargin 3 || isempty(N) N 1; end if nargin 2 || isempty(k) k 5; end [m, d] size(X); k min(k, m - 1); % 防止样本数少于 k1 X_new zeros(m * N, d); idx_new 1; % 计算所有样本两两之间的欧氏距离 D pdist2(X, X); for i 1:m % 距离从小到大排序索引第1位是样本自身跳过 [~, ord] sort(D(i, :)); neighbors ord(2:k1); for j 1:N % 随机选择一个近邻 nn neighbors(randi(k)); % 随机插值比例 lambda rand(); X_new(idx_new, :) X(i, :) lambda .* (X(nn, :) - X(i, :)); idx_new idx_new 1; end end end这段代码里有两个细节值得说明。第一k min(k, m - 1)这行非常重要因为如果少数类只有 3 条样本而你传了 (k5)排序之后根本找不到 5 个近邻代码直接报索引越界。这种边界处理属于平时用不上遇上就头疼的类型。第二使用pdist2(X, X)会在样本量大的时候占用较多内存但如果少数类样本量在几千以内完全没问题而且比每次调用knnsearch再两两查一遍要清爽。如果少数类样本实在太多超过万级pdist2生成的 (m \times m) 距离矩阵会非常吃内存那时建议改用knnsearch分批查询近邻核心逻辑不变只是性能优化层面的取舍。3.2 主脚本示例从数据划分到调用SMOTE下面用一个完整的主脚本演示SMOTE在分类流程里的位置。这里假设你已经有一个特征矩阵X_all和标签向量y_all多数类为0少数类为1。% 加载数据此处为示意 % load(your_data.mat); % 假设里面有 X_all 和 y_all rng(42); % 固定随机种子便于复现 % 划分训练集和测试集 cv cvpartition(y_all, Holdout, 0.3); X_train X_all(training(cv), :); y_train y_all(training(cv), :); X_test X_all(test(cv), :); y_test y_all(test(cv), :); % 提取训练集中的少数类样本 X_min X_train(y_train 1, :); num_min size(X_min, 1); num_maj sum(y_train 0); % 计算需要合成多少倍这里目标是把少数类扩到多数类的三分之一左右 target_min round(num_maj / 3); N ceil(target_min / num_min); % 调用SMOTE生成新样本 X_syn smote_minority(X_min, 5, N); y_syn ones(size(X_syn, 1), 1); % 合并原始训练集和合成样本 X_aug [X_train; X_syn]; y_aug [y_train; y_syn]; % 训练分类器示例用逻辑回归 mdl fitclinear(X_aug, y_aug, Learner, logistic); % 预测并评估 y_pred predict(mdl, X_test); confmat confusionmat(y_test, y_pred); disp(confmat);这里强调一点SMOTE 必须只在训练集上操作测试集保持原始分布不动。如果你把 SMOTE 应用到全量数据再划分训练测试集会导致测试集中也混入合成样本评估结果会虚高因为测试集不再是真实世界分布的反映。这个问题我在初学时踩过后面专门排查了很久才意识到是数据泄漏。3.3 参数选择先划分还是先过采样很多人会踩一个顺序坑先对整个数据集过采样再做训练测试集划分。这样做在代码执行上没有问题但逻辑上是错误的。测试集的意义在于模拟未来真实数据而真实数据中的少数类就是那么稀少不能人为把它变多。正确的流程是先把原始数据划分成训练集和测试集只在训练集上执行 SMOTE 或任何重采样操作用增强后的训练集训练模型用保持原始分布的测试集评估。我把这个顺序放在使用SMOTE前最需要确认的一件事因为它直接决定实验结论是否可信。4. 效果怎么验证别让准确率骗了你4.1 为什么准确率在类别不平衡下失去参考价值如果还是只看准确率那么SMOTE做与不做训练出来的模型准确率可能差别不大甚至会因为合成样本让模型误判一些多数类样本导致准确率轻微下降。但这不代表SMOTE没有价值而是评估指标选错了。在一个正负比 1:49 的数据集上一个全猜负类的模型准确率就有 98%。如果你改进后的模型准确率还是 98%但召回率从 0% 升到了 60%这就是巨大的提升。准确率完全体现不出这种进步。所以在评估SMOTE效果时我至少会看三个指标召回率Recall、精确率Precision和 F1-score。召回率回答真正的少数类里我抓住了多少精确率回答我预测为少数类的样本里有多少是对的F1 则是两者的调和平均适合在二者之间取平衡。4.2 用混淆矩阵、召回率和AUC做对比实验我在项目中通常做一组对比实验一组用原始训练集训练模型一组用SMOTE过采样后的训练集训练模型然后在同一个测试集上分别计算混淆矩阵、召回率、精确率、F1 和 AUC。这样能很直观地看到SMOTE带来了什么变化。Matlab 里计算这些指标很顺手% 原始训练模型 mdl_raw fitclinear(X_train, y_train, Learner, logistic); y_pred_raw predict(mdl_raw, X_test); confmat_raw confusionmat(y_test, y_pred_raw); % SMOTE训练模型 mdl_smote fitclinear(X_aug, y_aug, Learner, logistic); y_pred_smote predict(mdl_smote, X_test); confmat_smote confusionmat(y_test, y_pred_smote); % 从混淆矩阵计算指标 TP confmat(2,2); FP confmat(1,2); FN confmat(2,1); recall TP / (TP FN); precision TP / (TP FP); F1 2 * precision * recall / (precision recall); % ROC与AUC [~, score] predict(mdl_smote, X_test); [X_roc, Y_roc, ~, AUC] perfcurve(y_test, score(:,2), 1);perfcurve是 Matlab 里画 ROC 曲线、计算 AUC 的标准函数输出AUC就是曲线下面积。AUC 的好处是不需要设定分类阈值能从整体上评价模型的排序能力对于不平衡分类问题非常合适。我实测过多次在多数场景下SMOTE 后的模型 Recall 有明显提升AUC 也往往高于原始模型。但也有少数情况比如原始少数类样本噪声很大、重叠严重时SMOTE 后的 Precision 会下降这时需要结合 F1 来综合判断。5. 我踩过的坑与进阶方向5.1 三个典型坑先划分、连续特征假设和过采样比例失控第一个坑是数据泄漏前面已经提到不再赘述。第二个坑更隐蔽SMOTE 对离散特征和分类特征非常不友好。它的插值公式默认特征取值是连续实数如果某个特征是 0/1 的二值特征插值计算后可能得到 0.3、0.6 这样的中间值这在业务上完全没法解释。我在处理含大量哑变量的数据时吃过这个亏模型指标看着不错但一上业务系统规则解释全部乱套。解决思路是把连续特征和离散特征分开处理——只对连续特征做SMOTE离散特征从近邻样本中直接复制一份取值甚至放弃SMOTE改用 SMOTENC 这种专门处理混合数据类型的变体。第三个坑是过采样比例失控。有人为了追求完美平衡把少数类扩到和多数类一样多结果模型在训练集上表现极好测试集一验证立刻露馅——因为合成样本已经严重偏离真实分布模型学会了太多只属于合成区域的噪声。我通常会让少数类占训练集的 20% 到 35%先跑一版看 F1再微调比例而不是一上来就强行 1:1。5.2 进阶变体Borderline-SMOTE、ADASYN 和 SMOTE-ENN基础版 SMOTE 对所有少数类样本一视同仁不看它们处在什么位置。但在实际数据中处在类别边界的少数类样本往往比处在类别内部的样本更有学习价值。边界样本离多数类近模型容易分错恰恰是需要重点关注的区域。Borderline-SMOTE 的思路就是先把少数类样本分成边界样本和安全样本只对边界样本做插值合成。这样生成的样本集中分布在决策边界附近能显著提升模型在边界区域的分辨能力。实现时需要在SMOTE流程前面先算每个少数类样本的 m 个近邻中有多少个多数类样本按比例判定它是否处在边界。ADASYN 则是按难度自适应分配合成数量——每个少数类样本需要合成多少新样本取决于它周围多数类样本的密集程度周围多数类越多就给它分配越多的合成量。这个思路比 Borderline-SMOTE 更细腻实现也不算复杂。此外SMOTE 和清理技术组合的方案也值得提一下。SMOTE-ENN 是先 SMOTE 过采样再用 ENNEdited Nearest Neighbours把那些与多数类邻居混杂不清的样本删掉相当于先增后减把噪声清理出去。SMOTE-Tomek 也是类似思路通过删除 Tomek links 来降低类别重叠。这两个组合方法在样本噪声比较大的场景下表现往往比单纯 SMOTE 更稳定。我对 SMOTE 的定位是常用但不万能。它非常适合连续特征、样本量适中、类别边界比较清晰的数据。如果你的数据维度特别高、稀疏性强或者大量离散特征就需要对基础方法做调整。最终还是那句话SMOTE 不是越用越多越好参数调整要结合验证集结果来定别拍脑袋把少数类扩到天上去。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。