
简介合成少数类过采样技术SMOTE是处理类别不平衡数据的经典方法这份MATLAB实现版面向机器学习研究者和数据科学竞赛者可直接用于缓解少数类样本不足导致的模型偏向问题。压缩包共5个文件包含两个M函数源码核心过采样算法与配套测试脚本另有使用许可文本及版本控制忽略配置整包体积仅3KB轻量便于阅读。源码基于近邻与线性插值原理生成新样本测试脚本完整演示数据读取、参数设置、合成样本到结果输出的流程通过调节邻居个数和过采样倍率可控制新样本的数量与空间分布。无论是课程设计、论文复现或项目原型验证这份代码都能提供简明的起步参考。目前已有1321人学习适合具备MATLAB基础并希望从代码层面理解过采样原理的读者。1. SMOTE算法matlab版本把类不平衡数据拉到能学的地步先用一句话说清你可能正撞上的问题正负样本比例冲到 1:100 后绝大多数分类模型会交出一份“准确率 99%却一个正样本都抓不到”的答卷。这个现象在风控黑样本、设备故障报修、医疗影像罕见病灶里反复出现少数类恰恰是你在乎的那一类。SMOTE 算法matlab版本解决的就是这件事它不复制少数类样本而是在少数类样本之间的连线上插值生成新样本把不平衡数据集拉回模型还能学到东西的状态。Matlab 自带矩阵运算、KNN 搜索和分类器接口实现一套可复用的 SMOTE 并不需要外部依赖改动起来也比 Python 版更贴近矩阵思维。这篇笔记按“原理 → 最小实现 → 工程封装 → 踩坑 → 调参验证”的顺序讲适合正在做分类任务、手里已经有不平衡表格数据的工程师。2. SMOTE 的插值逻辑为什么 KNN 生成的是“合成样本”而不是复制2.1 SMOTE 的数学原理与关键公式先回到最原始的随机过采样。随机过采样就是把少数类样本原样复制几份模型会在完全相同的坐标点上反复训练决策边界越学越窄最后几乎是在“背样本”验证集一换就露馅。SMOTE 的思路完全不同它假设少数类样本附近仍然存在少数类因此可以在两个真实少数类样本的连线上随机取一个新点这个新点既继承了真实样本的特征组合又没有复制任何一行原始数据。具体数学表达不复杂。给定一个少数类样本 x_i先用欧氏距离在少数类集合中找到它的 k 个最近邻。每次生成新样本时从这 k 个近邻里随机抽一个记为 x_zi然后按下面公式生成合成样本x_new x_i λ × (x_zi − x_i)其中 λ 是 [0, 1] 区间里的均匀随机数。λ 越接近 0新样本越靠近 x_i越接近 1越靠近近邻 x_zi。换句话说SMOTE 是在“自己”和“某个长得像自己的邻居”之间做线性插值合成样本落在真实少数类分布的内部而不是跑到多数类区域里去。这个“只在局部近邻间插值”的设计就是它区别于简单复制和全局随机采样的关键。选择 KNN 而不是全局随机配对也是有理由的。少数类样本里可能有离群点如果允许任意两个少数类样本连线离群点和正常点之间的连线会生成大量跨区域垃圾样本把边界彻底搅浑。KNN 先圈定“谁和我是一伙的”插值就只发生在密度相近的局部。实际操作里 k 通常取 5太小容易受单个噪声点影响太大则会把不同簇的样本硬拉成近邻。2.2 一个最小 Matlab 实现从零写 SMOTE 核心循环理解了插值公式Matlab 实现就可以直接落到几十行代码上。下面的函数只做 SMOTE 最核心的事情输入少数类样本矩阵输出指定数量的合成样本。function [X_syn, y_syn] smote_core(X, y, k, N) % smote_core SMOTE核心函数 % 输入: % X - 少数类样本矩阵每行一个样本每列一个特征 % y - 少数类样本对应的标签该函数内只负责生成标签直接复用 % k - 近邻个数常见取5 % N - 每个原始少数类样本生成多少个新样本必须为正整数 % 输出: % X_syn - 合成样本矩阵大小为 (n*N) x d % y_syn - 合成样本标签全部等于原少数类标签 n size(X, 1); if n k error(少数类样本数必须大于k否则近邻搜索没有意义); end % knnsearch返回每个点到所有点的距离和索引 % 因为X里每个样本离自己最近所以K取k1再扔掉第一列 [idx, ~] knnsearch(X, X, K, k 1); idx idx(:, 2:end); X_syn zeros(n * N, size(X, 2)); y_syn ones(n * N, 1); % 单类别场景标签全部保持原少数类 row 1; for i 1:n for j 1:N % 随机选一个近邻 nn randi(k, 1); neighbor X(idx(i, nn), :); % 在 x_i 和 neighbor 之间线性插值 lambda rand(1, 1); X_syn(row, :) X(i, :) lambda .* (neighbor - X(i, :)); row row 1; end end end这段代码里knnsearch(X, X, K, k1)是找每个样本在 X 里的 k 个最近邻K必须写成k1因为结果的第一列永远是样本自己需要丢弃。randi(k, 1)负责从 0 到 k 之间等概率抽一个近邻编号实现“随机选一个邻居”。rand(1,1)生成 [0,1] 的均匀随机数作为插值比例 λ用X(i,:) lambda .* (neighbor - X(i,:))完成连线取点。注意这里用了.*而不是*因为 X 是矩阵的行向量逐特征相乘更稳妥。性能上第一次写会习惯用双重 for 循环数据量小没问题但少数类样本上万、特征上百时双重循环会明显变慢。改进方向是把内层循环向量化用rand(n, N)一次性生成所有 λ再用矩阵乘法批量插值。我一般先用循环版验证逻辑确认结果无误后再改写向量版。参数上N 别一上来就设成几十建议先生成少数类数量 50% 的新样本观察模型表现再逐步加。如果 N 太大smote_core 会在局部小范围反复插值造成合成样本大量重叠。3. 在 Matlab 工程里落地把 SMOTE 封装成可复用函数3.1 函数签名与输入输出设计核心函数只能处理少数类工程上还需要一个对外封装它自动拆出多数类和少数类、计算需要合成多少样本、调用 smote_core、最后拼回完整训练集。我建议把常用参数全部通过名值对传入避免调用方记错参数顺序。参数名默认值含义与建议K5近邻数先保持 5后面做敏感性测试Ratio0.5合成后少数类数量占多数类数量的比例0.5 比 1 更稳妥Standardizefalse是否先做 zscore 标准化特征量纲差异大时必须开 true封装函数的核心逻辑是先按标签拆出两个类别根据 Ratio 计算需要生成的新样本数量调用 smote_core最后用vertcat拼回多数类。这里需要特别处理一个边界少数类样本数如果小于等于 kKNN 搜索没有意义函数要主动报错而不是等 knnsearch 抛出一个看不懂的异常。另一个容易被忽略的点是Standardize 开关并不只是简单调用zscore因为后续训练分类器时也要用同样的均值和方差做标准化所以封装函数里我习惯把mu和sigma作为第二、第三个输出返回方便外部复用。function [X_aug, y_aug, mu, sigma] smote_balance(X, y, varargin) % smote_balance SMOTE工程封装 % 用法: % [X_aug, y_aug] smote_balance(X, y, K, 5, Ratio, 0.5) % [X_aug, y_aug, mu, sigma] smote_balance(X, y, Standardize, true) p inputParser; addParameter(p, K, 5, isscalar); addParameter(p, Ratio, 0.5, (x) x 0 x 1); addParameter(p, Standardize, false, islogical); parse(p, varargin{:}); k p.Results.K; ratio p.Results.Ratio; stdFlag p.Results.Standardize; % 分离多数类和少数类假设正类为1少数类就是label1 minority find(y 1); majority find(y ~ 1); X_min X(minority, :); y_min y(minority, :); X_maj X(majority, :); y_maj y(majority, :); if stdFlag [X_min, mu, sigma] zscore(X_min); % 多数类也要用同一组均值方差不能各自zscore X_maj (X_maj - mu) ./ sigma; else mu 0; sigma 1; end % 需要生成多少新样本 n_min size(X_min, 1); n_maj size(X_maj, 1); need max(0, round(n_maj * ratio - n_min)); if need 0 N ceil(need / n_min); % 每个样本平均生成的个数 [X_syn, y_syn] smote_core(X_min, y_min(1), k, N); % 如果多生成了随机截断到need X_syn X_syn(1:need, :); y_syn y_syn(1:need, :); else X_syn zeros(0, size(X, 2)); y_syn zeros(0, 1); end % 合并少数类、合成样本、多数类 X_aug [X_min; X_syn; X_maj]; y_aug [y_min; y_syn; y_maj]; end这个封装把决策权收敛到 Ratio 一个数字上Ratio 传 1 表示合成后少数类和多数类一样多传 0.3 表示只补到多数类的三成。ceil(need / n_min)保证每个原始样本至少被利用一次但因为循环里生成的是整数倍最后可能多出几个所以用1:need截断。这里的截断是随机的不会影响整体分布但如果你对随机性敏感建议在函数入口加rng(42)固定种子。3.2 用真实表格数据跑通 SMOTE 的完整流程工程调用通常不止做样本合成还要把读取数据、划分训练测试集、标准化、训练分类器、评估串起来。下面这段是我在信用评分类数据上常用的一套模板假设你手里已经有一张包含数值特征和标签的表格。% 读取表格数据假设最后一列是标签 data readtable(creditcard.csv); X data{:, 1:end-1}; y data{:, end}; % 先划分训练集和测试集再做SMOTE顺序不能反 cv cvpartition(y, HoldOut, 0.3); X_train X(training(cv), :); y_train y(training(cv), :); X_test X(test(cv), :); y_test y(test(cv), :); % 调用封装函数做SMOTE同时输出标准化参数 [X_bal, y_bal, mu, sigma] smote_balance(... X_train, y_train, K, 5, Ratio, 0.5, Standardize, true); % 训练分类器这里用SVM演示实际上换成fitcensemble也行 mdl fitcsvm(X_bal, y_bal, Standardize, false, KernelFunction, rbf); % 注意smote_balance里已经做过标准化所以训练时不再重复标准化 % 测试集用训练集的mu和sigma做变换 X_test_std (X_test - mu) ./ sigma; % 预测并输出混淆矩阵 ypred predict(mdl, X_test_std); confmat confusionmat(y_test, ypred); disp(confmat);这套流程里最关键的顺序是先cvpartition划分再对训练集做 SMOTE。如果先做 SMOTE 再划分合成样本里混杂了测试集的信息后续评估指标会虚高线上部署直接翻车。另外fitcsvm里的Standardize这一次要设成false因为smote_balance已经用训练集统计量把数据标准化过了如果你自己统一做标准化就要保证训练和测试共用同一组mu、sigma而不是各自zscore这是新手最容易踩的坑。跑通之后别急着调参先看两类各自的召回率。混淆矩阵对角线是正确分类第一行第二列是“把正类判成负类”的漏检数第二行第一列是“把负类判成正类”的误报数。不平衡场景里漏检正类的代价通常远高于误报所以我的习惯是先盯正类召回率再综合看精确率。注意Matlab 的fitcsvm输出概率需要额外设置ProbabilityEstimation如果后面要用 AUC 评估提前加上这个参数否则predict给不出稳定的后验概率。4. SMOTE 在 Matlab 中避坑指南5 个典型坑的现象、原因与解决4.1 坑 1特征尺度不统一插值样本大面积失真现象SMOTE 跑完合成样本画出来明显超出原始少数类分布范围分类器训练后反而更差。原因是欧氏距离对量纲敏感一个取值范围在 0 到 10000 的特征会碾压一个取值范围在 0 到 1 的特征KNN 找到的近邻根本不是“特征空间里真正接近”的样本而是被大数值特征绑架的样本插值自然失真。解决方法是 SMOTE 前对所有连续特征做 zscore 标准化让每个特征处于同一尺度。这一步不能只在 smote_balance 内部做而是要把训练集的均值和方差保存下来测试集进入模型前用同一组参数变换否则训练和测试的分布不一致预测结果全乱。4.2 坑 2离散特征被插值成“1.7 这种无意义数值”现象数据集里有“学历”“产品类型”这类取值只有 0、1、2 的离散特征SMOTE 生成的合成样本里出现了 1.7 这类中间值模型拿到后无法解释。原因是 SMOTE 对所有特征统一做线性插值它不知道哪些特征是类别型。解决思路分两种如果离散特征能变成有序数值插值后round取整再clip到合法区间如果只是无序类别正确做法是先把类别特征做 one-hot 编码对连续特征做 SMOTE合成后再把 one-hot 列变成整数或保持概率值。我更倾向于把离散列单独拎出来SMOTE 只跑在连续特征上离散列用原始样本的众数或按比例随机赋值简单且不会产生假类别。4.3 坑 3先 SMOTE 再划分数据验证集指标虚高现象测试集 AUC 高达 0.98上线后只有 0.72差距大到没法接受。原因是你在划分训练测试集之前就做了 SMOTE合成样本和原始样本一起参与划分测试集里混入了由训练样本插值出来的“近亲”数据模型在测试时相当于开了卷考试。解决方法是严格遵循“先划分后 SMOTE”的顺序而且交叉验证时也要把 SMOTE 放进每一折的训练侧不能先在完整数据集上合成再去做 K 折。Matlab 里可以用cvpartition的每个training(cv)和test(cv)分别取子集再对子集调用 smote_balance。4.4 坑 4高维稀疏数据让 KNN 距离失去意义现象特征维度上百、每列大量为 0 的数据集上knnsearch返回的近邻看起来毫无规律SMOTE 生成的样本也没带来任何提升。原因是高维空间里欧氏距离趋于均匀最近邻和最远邻的差越来越小KNN 等于随机选邻居。常见做法是先做 PCA 降维到 20 到 50 维再在降维后的空间做 SMOTE最后把合成样本变换回原始特征空间或者干脆换 Borderline-SMOTE、ADASYN 这类专门处理边界样本的变种。对做“深度学习 matlab 工作流”的同事我一般还要多提醒一句图像、文本这类原始高维输入SMOTE 不要直接作用在像素或词向量上而是作用在模型倒数第二层的嵌入特征上才有效。4.5 坑 5只用准确率评估模型“假阳性”泛滥现象训练完看准确率 99%很高兴一查混淆矩阵少数类的召回率只有 3%模型几乎把所有少数类都判成了多数类。原因是类别不平衡时准确率完全被多数类主导SMOTE 看似把比例拉平了评估指标却还用旧标准等于白做。解决方法是把评估指标换成少数类的召回率、精确率、F1 和 ROC 曲线的 AUC并且要求结果能同时看到两个类别的错判数量。Matlab 里可以直接用confusionmat输出混淆矩阵用rocmetrics计算 AUC这两样东西比一个孤零零的 accuracy 值信息量大得多。记住一句话SMOTE 做得好不好看的不是准确率涨了多少而是少数类召回率有没有结构性改善。5. 进阶SMOTE 的参数调优与效果验证技巧5.1 用 AUC 和混淆矩阵验证 SMOTE 的真实收益不加 SMOTE 的模型必须跑一份 baseline否则你无法判断合成样本到底是帮了忙还是帮倒忙。对比两份实验时推荐用同一个测试集、同一个随机种子记录模型对测试集的预测分数和标签然后一次性计算 AUC 和混淆矩阵。% baseline与SMOTE后的模型分别打分 [~, score_base] predict(mdl_base, X_test_std); [~, score_smote] predict(mdl_smote, X_test_std); % 用rocmetrics算AUC第二列是正类概率 roc_base rocmetrics(y_test, score_base(:, 2), [0 1]); roc_smote rocmetrics(y_test, score_smote(:, 2), [0 1]); disp([roc_base.AUC, roc_smote.AUC]); % 同时输出F1和混淆矩阵 confmat confusionmat(y_test, ypred_smote); recall confmat(2, 2) / sum(confmat(2, :)); precision confmat(2, 2) / sum(confmat(:, 2)); f1 2 * recall * precision / (recall precision);这里rocmetrics是较新版本里的函数如果你的 Matlab 版本比较老可以用perfcurve替代结果等价。AUC 提升幅度低于 0.02 时先不要急着换更大的过采样倍率优先检查特征工程和数据清洗SMOTE 不是万能药。5.2 参数调优顺序与经验值SMOTE 的核心参数就两个近邻数 k 和过采样倍率 Ratio也就是 N。我的调优顺序是先固定 k5把 Ratio 从 0.25、0.5、1 依次试一遍每次记录少数类 F1 和 AUC然后再把 k 从 3、5、10 扫一遍看结果波动大不大。经验上 k5 是最不容易出错的起点少数类样本少于 10 条时 k 要降到 3否则近邻搜索会直接报错或全是重复邻居。Ratio 不用一上来就拉到 1当少数类只有几十条、多数类上万条时强行 1:1 会产生大量合成样本把训练集撑到两倍大训练时间翻倍收益却可能下降。我习惯先补到多数类的 50%如果召回率上不去再往上加。这件事多少有点玄学但“小步快跑、对比着调”比一上来拉满参数靠谱得多。做每轮实验时固定随机种子是我吃了亏之后养成的习惯rng(42)放在脚本最前面randi和rand的序列就稳定了复现实验结果不再靠运气。SMOTE 本质上是一条“后悔药”路径——它只能缓解不平衡不能创造新信息如果你发现无论怎么调参模型都不收敛回头检查特征本身比继续堆合成样本更有价值。希望帮到你。本文还有配套的精品资源点击获取