简介这是一份基于MATLAB实现SMOTE算法合成少数类过采样技术的开源代码包面向处理不平衡数据集的数据科学学习者与机器学习工程师。SMOTE方法可有效缓解少数类样本不足导致的模型偏差问题资源包内含SMOTE主程序、测试脚本和说明文档源码约3KB结构精简便于直接运行或二次修改适合希望理解算法原理并快速上手应用的中级MATLAB用户。包内共5个文件其中2个m脚本为核心实现其余为文本说明与版本管理配置整体简洁清晰。目前该资源已被1321人浏览学习受到一定关注。借助该源码用户可掌握K近邻合成新样本的完整环节包括数据预处理、SMOTE操作、数据融合与后续模型评估思路并可将生成的平衡数据集接入SVM或决策树等分类器为实际项目中的类别不平衡问题提供可复现的解决路径。1. SMOTE算法MATLAB版把少数类样本从99比1拉回到能训练的样子做分类建模的人大概率撞过这堵墙正负样本比例悬殊模型把所有样本都判成多数类准确率照样99%上线之后才发现完全不能用。SMOTE就是专门治这个的它的思路不是复制少数类样本而是在少数类样本之间插值合成出全新的样本。这份MATLAB版SMOTE代码我拆了一遍核心逻辑完整、可以直接改参数跑通适合要在MATLAB里复现论文实验、做课程设计或者给毕业设计加一个过采样模块的从业者。下面从原理到代码再到踩坑一条线讲清楚。2. SMOTE算法原理为什么插值生成的样本比复制更有用2.1 过采样的本质问题复制样本只是把权重放大没有增加信息随机过采样做起来很简单把少数类样本复制几份让类别比例到1:1模型训练看起来没什么毛病。但实际跑出来的模型在真实数据上往往表现很差原因在于复制样本没有引入任何新信息。决策边界只是被重复样本“撑”了一下模型学到的只是少数类样本原来的分布密度对真实分布没有补充。SMOTE不一样它在特征空间里做线性插值等于在已有的少数类样本之间“猜”出一些位于流形中间的新样本让模型的决策边界能被真实地推向多数类一侧。SMOTE的全称是Synthetic Minority Over-sampling Technique最原始的论文由Chawla在2002年提出。它做的事情可以概括成三步先为每个少数类样本找到k个近邻再从这k个近邻里随机挑一个最后在这两个样本的连线上随机取一个点作为新样本。这个新样本的每个特征都是原有特征按同一个比例混合出来的。2.2 从近邻搜索到插值生成算法流程拆解具体展开说SMOTE对每一个少数类样本需要先算出它和其他所有少数类样本的距离。欧氏距离是最常用的度量公式是关注意义不大的常规数学表达式但实现时要注意特征量纲问题不然距离计算会完全失真。找到k个近邻之后算法会在这k个近邻里随机选一个然后生成一个0到1之间的随机数λ新样本的每个特征值等于原始样本的特征值加上λ乘以近邻样本和原始样本特征值的差。这个插值方式的妙处在于新样本不会偏离少数类分布太远。比如两个少数类样本在特征空间里相距比较远那么插值点也会落在这两点之间整体分布不会出现离谱的离群点。如果某些特征存在明显的边界范围插值也可能越界这是后话在第4章会专门讲。从计算的角度看整个SMOTE算法最核心的步骤是近邻搜索这也就是为什么在MATLAB里实现时优先用统计工具箱里的knnsearch而不是自己写双重循环。3. MATLAB代码落地主函数、调用方式与参数调优3.1 核心函数实现一份能直接跑的SMOTE主函数我在MATLAB里按标准流程实现了一个精简版主函数输入少数类样本矩阵、近邻数、目标生成数量输出合成样本矩阵。代码不长核心逻辑都在下面function X_new smote_light(X, k, n) % 输入 % X - 少数类样本矩阵m行d列每行一个样本 % k - 近邻数量默认取5 % n - 需要生成的合成样本数量 % 输出 % X_new - 合成样本矩阵n行d列 [m, d] size(X); if n 1 error(n必须大于等于1); end % 用knnsearch找每个样本的k1个近邻 % 加1是因为第一个近邻是样本自身需要去掉 idx knnsearch(X, X, K, k 1); idx(:, 1) []; % 去掉自身那一列 X_new zeros(n, d); for i 1:n % 随机选一个基样本 base randi(m); % 从该样本的k个近邻里随机选一个 neighbor idx(base, randi(k)); % 插值系数0到1之间均匀分布 lambda rand(); % 线性插值生成新样本 X_new(i, :) X(base, :) lambda * (X(neighbor, :) - X(base, :)); end end这段代码有几个地方值得说明。第一knnsearch返回的近邻索引第一列一定是样本自己所以K设置为k1再删掉第一列这才是真正要找的k个近邻。第二每次循环随机选基样本和近邻所以同一组输入运行两次生成的合成样本不完全一样这是随机过程的正常现象不影响整体分布。第三插值系数lambda用的是rand()生成的是0到1之间的均匀随机数决定新样本落在连线上的具体位置。3.2 典型调用与参数设置K值和生成数量怎么配假设现在有一个二分类训练集多数类样本有5000个少数类样本只有200个特征维度是30。常见做法是先取出少数类样本计算需要生成的样本数量然后调用上面这个函数% 读入原始训练数据X_all是全部样本y_all是对应标签 X_major X_all(y_all 1, :); % 多数类 X_minor X_all(y_all 0, :); % 少数类 % 目标让少数类数量达到多数类的一半即2500个 n_target 2500; n_to_generate n_target - size(X_minor, 1); % 调用SMOTE生成合成样本 X_syn smote_light(X_minor, 5, n_to_generate); % 合并原始少数类和合成样本 X_minor_balanced [X_minor; X_syn]; y_minor_balanced zeros(size(X_minor_balanced, 1), 1); % 重新拼接成完整训练集 X_train_new [X_major; X_minor_balanced]; y_train_new [ones(size(X_major, 1), 1); y_minor_balanced];参数选择上近邻数k一般取5少数类样本个数特别少的时候可以降到3样本量大且分布集中时可以取8到10。k越小合成样本越贴近某个原始样本周围多样性越低k越大插值范围越广合成样本覆盖的区域越大但也更容易跨越到多数类区域里去。生成数量不要追求绝对1:1尤其是少数类本身很少时硬拉到和多数类一样多会造成合成样本大量重叠模型过拟合风险反而上升。下面把主要参数的影响整理成一张表方便随手查参数推荐值影响调整方向近邻数k默认5k越小样本越局促k越大分布越广样本少用3分布密集用8目标比例1:1到1:3之间比例越极端过拟合风险越高多数类样本本身就多时别追求1:1距离度量欧氏距离特征量纲不同会严重干扰近邻选择先做zscore标准化再跑SMOTE插值系数0到1均匀分布决定新样本在连线上的位置不用改随机采样是标准做法参数调完先别急着训练模型前几批合成样本生成之后建议随手做个分布检查最简单的做法是把原始少数类和合成样本在二维散点图里画出来看合成样本是否落在原始样本连线的周围。如果发现合成样本全部挤在一个小区域通常是k值太大或者原始样本本身存在大量重复。3.3 环境与版本兼容性只依赖统计工具箱实现这个函数只依赖MATLAB统计和机器学习工具箱里的knnsearch不涉及其他第三方库。我用的是MATLAB R2023b环境R2018b以上的版本都能直接运行。如果用的是不带工具箱的纯基础版knnsearch会报未定义替代方案是先用pdist2算距离矩阵再手动排序取索引但那个做法只适合样本量在几千以内的场景样本量一大内存就撑不住了。3.4 运行效率千样本量级直接跑十万样本要谨慎样本量在5000以内时knnsearch的耗时基本在一秒以内可以直接跑。少数类样本超过两万时近邻搜索的计算量会明显上升这时建议改用分块策略每次只对随机抽出的子集做近邻搜索和插值而不是一次性把全量样本放进knnsearch。有一个比较取巧的做法是分batch循环生成每批处理500个基样本效果一样内存占用能降一个数量级。4. 避坑清单五个常见的SMOTE翻车场景4.1 合成样本挤成一团特征没有标准化现象画出来的合成样本全部堆在某个小范围里与原始少数类样本的分布明显脱节。原因knnsearch默认用欧氏距离如果某个特征的数值范围远大于其他特征距离计算被它主导近邻选择完全失真。比如一个特征是年龄0到100另一个特征是收入0到1000000收入特征一票否决年龄特征形同虚设近邻关系就错了。解决跑SMOTE之前先做标准化MATLAB里直接zscore(X_minor)或者用mapminmax把每个特征缩放到0到1之间。标准化之后再做近邻搜索和插值生成完新样本如果后续要还原到原始量纲再乘以原来的标准差加回均值。这个步骤是血泪经验我刚开始跑SMOTE时跳过了标准化生成的样本怎么看怎么不对劲排查了很久才找到根因。4.2 过采样倍率太猛类别比例反转后过拟合现象训练集上准确率接近满分但验证集、测试集上F1分数不升反降。原因把少数类强行过采样到和多数类一样多甚至更多合成样本在特征空间里大量重叠模型把注意力放在合成样本的细节上本质上是把噪声也给拟合进去了。解决别追求1:1。常见做法是把过采样后的比例控制在1:2到1:3即少数类数量达到多数类的三分之一到一半即可。如果原始少数类只有50个目标数量定在100到150之间就够了硬拉出来500个合成样本质量高不了。4.3 样本量一大就卡死近邻搜索内存爆掉现象少数类样本上万时程序运行越来越慢报内存不足错误。原因如果改用pdist2算全量距离矩阵样本量为m时矩阵大小是m乘m一万个样本就是1亿个浮点数光这个矩阵就要几百MB内存样本量翻倍内存直接爆炸。解决坚持用knnsearch而不是手写全量距离矩阵计算。knnsearch底层用kd树或穷举法内存占用小很多。如果knnsearch也慢按行分块调用knnsearch每块1000行结果累计起来再对插值步骤做循环。4.4 离散特征被插值破坏类别编码变成无意义小数现象合成样本里的性别、颜色、地区这类离散特征出现了0.3、1.7这样的小数。原因SMOTE是对所有特征统一做线性插值对连续数值特征有效但对类别型特征没有意义。比如性别编码为男0、女1插值得到0.4这个值不代表任何性别。解决把离散特征和连续特征分开处理。先只对连续特征做SMOTE插值离散特征用最近邻居的众数来填充或者直接用该基样本原始的特征值。如果离散特征编码是one-hot格式插值得到的小数可以阈值化成0或1但那样会在多个类别变量之间引入伪相关性不如分开处理干净。4.5 SMOTE放在全体数据集上执行数据泄露现象模型在测试集上表现好得异常但上线后效果暴跌。原因在划分训练集和测试集之前就对全量数据做了SMOTE合成样本同时出现在训练集和测试集里或者测试集被合成样本污染评估结果虚高。这种数据泄露是隐蔽的准确率摆在那里很漂亮但模型真正的泛化能力完全没被验证出来。解决SMOTE必须只针对训练集做测试集保持原始分布不动。交叉验证时SMOTE要放在每一折的交叉验证内部即在每次划分出训练折之后、训练模型之前执行具体做法在第6章展开讲。5. 进阶玩法从SMOTE变体到完整不平衡数据管线5.1 Borderline-SMOTE与ADASYN两个经典变体思路原始SMOTE对每个少数类样本一视同仁不管它在类别边界还是在少数类区域深处。实际应用中边界附近的样本往往携带更多信息深处那些几乎和多数类不重叠的样本对改善决策边界帮助有限。Borderline-SMOTE的思路是先识别出那些处在边界区域的少数类样本也就是其近邻中多数类占比较高但又不是纯多数类样本的那部分然后只对这部分样本做过采样。这个操作能在不增加太多样本的前提下更精准地推挤决策边界。ADASYN的侧重点不同它能自动为不同少数类样本分配不同的采样权重。核心逻辑是如果一个少数类样本的近邻中多数类很多说明这个样本很难学就多给它分配一些随机选择概率生成更多新样本。相比之下ADASYN更适应类别分布本身的复杂性。MATLAB里实现这两个变体并不复杂把第3章的smote_light主函数改一下替换基样本的选择逻辑就能从标准SMOTE变成ADASYN。5.2 评估口径变了别再看准确率不平衡数据场景下准确率是极具欺骗性的指标哪怕完全不做任何处理用全部预测为多数类这个最简单的策略在99:1的数据上都能拿到99%的准确率。评估过采样效果时我一般同时看三个指标F1分数、AUC值和混淆矩阵。F1综合了精确率和召回率能反映少数类被识别出来的能力AUC不依赖分类阈值可以在不同阈值下观察模型区分两类的能力混淆矩阵能直观看出漏报和误报分别有多少。5.3 一条完整的管线从原始数据到可部署模型基于前面的组件完整的不平衡数据建模流程我一般这样组织。第一步做数据清洗和去重第二步对连续特征做zscore标准化离散特征做编码第三步划分训练集和测试集第四步只在训练集上做SMOTE过采样第五步用交叉验证评估模型且在每一折内部重复执行标准化和SMOTE第六步在原始分布的测试集上做最终评估。% 完整管线示例以决策树分类器为例 rng(42); % 固定随机种子方便复现 % 第一步划分训练测试集 cv cvpartition(y_all, HoldOut, 0.3); X_train_raw X_all(cv.training, :); y_train y_all(cv.training); X_test X_all(cv.test, :); y_test y_all(cv.test); % 第二步在训练集上做标准化 [X_train_raw, mu, sigma] zscore(X_train_raw); X_test (X_test - mu) .* sigma; % 用训练集的均值标准差做测试集标准化 % 第三步分别取出多数类和少数类 X_minor X_train_raw(y_train 0, :); X_major X_train_raw(y_train 1, :); n_minor size(X_minor, 1); n_major size(X_major, 1); % 第四步生成目标数量的合成样本 n_target round(n_major * 1/3); X_syn smote_light(X_minor, 5, n_target - n_minor); % 第五步拼接成新训练集 X_train_bal [X_major; X_minor; X_syn]; y_train_bal [ones(n_major, 1); zeros(n_minor, 1); zeros(size(X_syn, 1), 1)]; % 第六步训练并评估 mdl fitctree(X_train_bal, y_train_bal); y_pred predict(mdl, X_test); confmat confusionmat(y_test, y_pred); f1_score 2 * confmat(2,2) / (2 * confmat(2,2) confmat(1,2) confmat(2,1));管线里每一步的顺序都不能乱。特别要注意的是测试集标准化必须用训练集的均值和标准差不能自己在测试集上重新算一遍否则会造成信息泄露。过采样倍率也不要拍脑袋定建议从1:3开始逐步调整到1:2观察验证集上的F1分数变化趋势找到拐点即可。6. 验证SMOTE效果的招数PCA可视化和交叉验证内部过采样SMOTE到底有没有用不能只看训练集上的数字。我习惯做的第一件事是PCA降维可视化。把原始少数类样本、SMOTE合成样本和多数类样本一起投影到二维平面用不同颜色标示一眼就能看出合成样本是贴合在少数类分布周围还是漫无边际地飘在特征空间里。MATLAB实现就几行用pca函数算出前两个主成分再scatter画散点% 合并三类样本做PCA投影 X_all [X_minor; X_syn; X_major]; [coeff, score] pca(X_all); figure; scatter(score(1:n_minor, 1), score(1:n_minor, 2), 10, b, filled); hold on; scatter(score(n_minor1:n_minorsize(X_syn,1), 1), ... score(n_minor1:n_minorsize(X_syn,1), 2), 10, r, filled); scatter(score(n_minorsize(X_syn,1)1:end, 1), ... score(n_minorsize(X_syn,1)1:end, 2), 5, k, filled); legend({原始少数类, SMOTE合成, 多数类});合成样本如果呈带状分布在少数类连线两侧说明插值逻辑正常如果合成样本像烟雾一样扩散到多数类区域就要检查k值是否过大或者标准化有没有出问题。第二件事是交叉验证内部做SMOTE。很多人的做法是先对全训练集过采样再交叉验证这会让同一批合成样本同时出现在不同折里造成严重的数据泄露。正确的做法是每一折交叉验证都重新做一次SMOTE且只在当前训练折内做验证折完全保持原始分布。这样评估出来的指标才是模型在真实数据上的表现而不是被合成样本粉饰过的假象。% 5折交叉验证内部SMOTE示意 cv_folds cvpartition(y_train, KFold, 5); f1_list zeros(cv_folds.NumTestSets, 1); for i 1:cv_folds.NumTestSets X_tr X_train(cv_folds.training(i), :); y_tr y_train(cv_folds.training(i)); X_va X_train(cv_folds.test(i), :); y_va y_train(cv_folds.test(i)); % 在第i折的训练折内部做SMOTE X_minor_fold X_tr(y_tr 0, :); X_major_fold X_tr(y_tr 1, :); n_syn round(size(X_major_fold, 1) / 3) - size(X_minor_fold, 1); X_syn_fold smote_light(X_minor_fold, 5, n_syn); X_bal [X_major_fold; X_minor_fold; X_syn_fold]; y_bal [ones(size(X_major_fold, 1), 1); zeros(size(X_minor_fold, 1), 1); zeros(size(X_syn_fold, 1), 1)]; % 训练与评估 mdl_fold fitctree(X_bal, y_bal); y_pred_fold predict(mdl_fold, X_va); cm confusionmat(y_va, y_pred_fold); f1_list(i) 2 * cm(2,2) / (2 * cm(2,2) cm(1,2) cm(2,1)); end fprintf(F1均值: %.3f\n, mean(f1_list));从那以后我每次拿到不平衡数据集都强制走一遍这套流程先zscore标准化再在交叉验证内部做SMOTE最后用PCA可视化和F1分数双重验证。踩过的坑多了反而觉得SMOTE这个算法本身不复杂真正决定成败的是预处理和评估方式。希望这篇拆解能帮你在自己的数据集上少走几段弯路。本文还有配套的精品资源点击获取