资讯详情 MATLAB实现SMOTE:类别不平衡数据增强与过采样实战指南
📅 2026/10/15 17:21:43
简介SMOTE算法的MATLAB实现源码包面向机器学习从业者与数据科学研究者用于解决分类任务中少数类样本不足导致的模型偏差问题。源自Chawla等2002年提出的经典过采样方法通过K近邻线性插值合成新样本有效提升模型对少数类的识别能力。包体包含5个文件以两个.m源码文件为核心——SMOTE.m为主算法实现SMOTETest.m为测试示例另附LICENCE.txt许可证说明及Git相关配置文件整体仅3KB轻量易读适合直接阅读源码理解算法细节或嵌入现有项目。目前已有1321人学习使用。读者可获得完整的SMOTE算法原生MATLAB实现包括K近邻搜索、插值合成等核心逻辑配合测试脚本快速验证效果为不平衡数据预处理提供可扩展的基础工具。1. 类目不平衡SMOTE 为什么在 MATLAB 里值得花时间做过实际分类任务的人基本都撞过同一个问题正样本几千条、负样本几十条模型训练出来全预测成多数类准确率 98% 却一点用没有。类不平衡是工程技术里最常见的“假高分”来源金融欺诈、设备故障、罕见病诊断、质检缺陷识别这类场景尤其严重。SMOTE Synthetic Minority Over-sampling Technique 这个名字在论文里出现频率极高核心思路不是复制少数类样本而是在少数类样本之间合成新的样本让模型能见到的少数类分布更完整。这份 MATLAB 版本资源把整个算法封装成可读、可改、可复现的源码包适合两类人一类是把 MATLAB 当主力工具、不想跨语言做数据增强的工程师另一类是正在做毕业设计或竞赛、需要把 SMOTE 原理和实现逐行搞懂的同学。接下来按“原理 → 实现 → 参数 → 排错 → 进阶”的顺序拆。2. SMOTE 合成样本的数学内核公式、距离度量和边界条件2.1 合成公式到底在算什么从两个样本之间插值说起SMOTE 原本来自论文 “SMOTE: Synthetic Minority Over-sampling Technique”它做合成的基础假设是少数类样本附近的样本仍然是少数类因此在少数类样本之间的连线上插值得到的新样本仍然属于少数类。合成公式非常朴素x_new x_i δ × ( x_j - x_i )其中 x_i 是当前选择的少数类样本x_j 是从 x_i 的 K 近邻里随机挑出的一个少数类样本δ 是 0 到 1 之间的随机数。这个公式的本质是在 x_i 和 x_j 之间做线性插值。实现时候最常见的 MATLAB 写法是% 对每个少数类样本索引 i随机选一个近邻索引 j % 生成 0~1 之间均匀分布的随机数 delta % 合成样本原样本 delta * (近邻样本 - 原样本) delta rand(1, 1); syn_sample minority_data(i, :) delta * (minority_data(j, :) - minority_data(i, :));逻辑说明这条语句直接对应合成公式本身把当前样本和近邻样本之间的差值乘以一个随机比例再加回当前样本。delta 越小合成点越靠近 x_idelta 越大越靠近 x_j。MATLAB 的 rand(1,1) 生成一个标量随机数如果改成 rand(size(minority_data(i,:))) 则每个特征维度使用不同的随机比例使合成样本在特征空间里的移动方向更丰富适用于特征之间相关性不强的场景。参数说明delta 的取值范围强制在 [0,1) 之间如果数据经过了标准化这个操作等价于在标准化空间内插值合成后再逆标准化回原始尺度即可。K 近邻在 n 维空间中用欧氏距离计算MATLAB 里直接用 vecnorm 函数一次取整行距离% 计算每个少数类样本到所有少数类样本的欧氏距离矩阵 % 使用 vecnorm 对每行做差后取 2-范数 dist_matrix vecnorm(minority_data - minority_data(i, :), 2, 2);逻辑说明minority_data 是少数类样本矩阵每行一个样本每列一个特征。minority_data - minority_data(i,:) 用到了 MATLAB 的隐式扩展把这个矩阵的每一行都减去当前行向量得到的是“每个样本与当前样本的差向量”vecnorm 的第三参数 2 表示沿行方向求 2-范数最终得到一列距离值。如果样本量较大可以不计算全量矩阵而是用 knnsearch 直接找最近邻这是更省内存的做法。2.2 距离度量不只有欧氏距离卡方距离和余弦距离什么时候更合适上一节的 vecnorm 默认用欧氏距离这对连续型数值特征最直观。但现实里很多数据集带着明显的分布差异比如频数型特征、稀疏特征、周期性特征。我在做工业传感器的异常检测时遇到过一组振动信号特征其中几个维度是频域能量值直接算欧氏距离会把低频和高频的能量差放大到离谱导致近邻选得不合理合成样本落在事实上不存在的区域里。对于稀疏高维特征余弦距离比欧氏距离更稳。MATLAB 里计算两两余弦距离不需要自己写公式可以直接调用 pdist% 计算成对距离可选的 distance 参数有 euclidean、cosine、cityblock 等 pair_dist pdist(minority_data, cosine); % 将距离向量转成方阵方便按行找近邻 dist_square squareform(pair_dist);逻辑说明pdist 返回的是上三角距离向量不占用 n×n 矩阵的空间对样本量大的情况更友好。squareform 把向量还原成对称方阵这样你可以直接在每一行用 sort 排序取前 K 个索引。参数说明K 值选取在 SMOTE 里默认是 5对应原论文的默认配置。但更合理的做法是先看少数类总量如果少数类样本数少于 20 条K 值最好压到 3 以内因为近邻数过大时挑出的 j 样本可能已经偏离本簇的中心结构合成样本的噪声会被放大。余弦距离适合文本特征、用户行为特征这类几乎不含负值的场景卡方距离适合直方图类型的特征但 MATLAB 的 pdist 里没有内置卡方距离需要自己写先对每行做归一化然后套用卡方公式。2.3 合成数量的分配策略两倍、五倍还是动态比例SMOTE 有个参数 N表示需要合成的样本倍数也就是每一条少数类样本要额外生成的合成样本数。如果少数类有 100 条N3则最终生成 300 条新样本。实际工程里N 的取值并不是拍脑袋而是由目标平衡比决定。比如多数类有 1000 条想要让少数类达到多数类的 60%需要新增 500 条那么 N 500 / 100 5。这里有个关键的实现细节N 在代码里通常是整数如果 500/100 不是整数比如目标是 65%需要合成 550 条直接取 N5 会缺 50 条取 N6 又会超出 50 条。常见做法有两种第一种是把少数类样本全部复制一遍先保证至少达到目标量然后随机删除超出的合成样本第二种是遍历少数类样本前若干条样本多做一次合成其余样本少做一次。我一般用第二种因为随机删除会让某些簇被破坏。落到 MATLAB 代码控制每个样本的合成次数可以用 repmat 配合 randperm 实现% 需要合成的总数 total_to_syn % 少数类样本数 num_min % 基础每样本合成次数 base_n floor(total_to_syn / num_min) % 余数 extra_n rem(total_to_syn, num_min) % 前 extra_n 个被打乱顺序的样本各多合成一次 syn_counts base_n * ones(num_min, 1); extra_idx randperm(num_min, extra_n); syn_counts(extra_idx) syn_counts(extra_idx) 1;逻辑说明base_n 保证总量足够接近目标extra_idx 从全部少数类样本里随机抽取 extra_n 条每条多合成一条这样分配不会把额外合成量集中在某一个簇里。参数说明randperm(num_min, extra_n) 在 MATLAB 里的含义是从 1 到 num_min 的随机排列中取前 extra_n 个值天然不重复不需要额外去重。如果不需要精确控制合成总量直接把 N 设定为整数倍即可代码更简洁泛化能力也够。3. 手写 MATLAB 版 SMOTE完整函数、脚本执行与数据标准化3.1 从零手写一个可供调用的 smote 函数输入输出与校验逻辑网上能找到的 SMOTE 代码不少但很多存在细节问题有的直接用矩阵减法忘了隐式扩展维度有的没处理 K 大于样本数的情况有的对标签变量不做过滤。下面这个函数是我在实际项目中改过几轮后沉淀下的版本特点是每一步都有校验输入矩阵和标签分离允许用户传入任意距离度量方式function [syn_data, syn_label] smote_minority(minority_data, K, num_syn) % SMOTE_MINORITY 对少数类样本执行 SMOTE 合成 % 输入: % minority_data - 少数类样本特征矩阵每行一个样本每列一个特征 % K - 近邻数量默认 5 % num_syn - 需要合成的总样本数默认 size(minority_data,1) * 5 % 输出: % syn_data - 合成样本特征矩阵 % syn_label - 合成样本标签列向量值与少数类标签一致 if nargin 2 || isempty(K) K 5; end if nargin 3 || isempty(num_syn) num_syn size(minority_data, 1) * 5; end num_min size(minority_data, 1); num_dim size(minority_data, 2); % 确保 K 不超过少数类样本数减 1否则近邻选择没有意义 K min(K, num_min - 1); if K 1 error(少数类样本数过少无法执行 SMOTE); end % 按比例分配每个样本的合成数量 base_n floor(num_syn / num_min); extra_n rem(num_syn, num_min); syn_counts base_n * ones(num_min, 1); if extra_n 0 extra_idx randperm(num_min, extra_n); syn_counts(extra_idx) syn_counts(extra_idx) 1; end % 预分配合成样本矩阵 syn_data zeros(sum(syn_counts), num_dim); syn_label zeros(sum(syn_counts), 1); % 距离计算与近邻选择 dist_sq squareform(pdist(minority_data, euclidean)); [~, sorted_idx] sort(dist_sq, 2); % 第一列是自己排除掉取 2 到 K1 列作为近邻索引 neighbor_idx sorted_idx(:, 2:K1); row_pos 1; for i 1:num_min for c 1:syn_counts(i) % 从当前样本的近邻中随机选一个 j neighbor_idx(i, randi(K, 1)); delta rand(1, 1); syn_data(row_pos, :) minority_data(i, :) ... delta * (minority_data(j, :) - minority_data(i, :)); syn_label(row_pos) 1; % 标签由调用方在外部统一替换 row_pos row_pos 1; end end end逻辑说明函数先做参数校验再计算距离矩阵最后两层循环完成合成。neighbor_idx 的取法利用了 sort 后每行从小到大排列的特点排除自身后取前 K 个索引。syn_label 默认赋值为 1实际应用时调用方应替换为对应少数类的真实标签值。合成样本总数是 sum(syn_counts)也就是 num_syn与输入完全一致。参数说明pdist 计算欧氏距离用 euclidean如果需要换成余弦或曼哈顿距离替换这一行的距离参数即可。这里的预分配矩阵方式比在循环里不断 append 效率高出很多样本上万时差距尤其明显是一个值得记住的习惯。3.2 把 SMOTE 嵌入完整分类流程训练前处理的标准顺序手写函数本身只能合成样本真正要落地还需要一套标准处理流程。我在项目中通常按这个顺序组织脚本先读入原始数据并分离特征与标签再对特征矩阵做标准化然后调用 smote_minority 合成少数类样本把合成样本与原始多数类合并最后重新打乱顺序并划分训练集和测试集。这里有个容易被忽略的细节标准化必须在 SMOTE 之前做而且标准化参数只能从训练集里算出不能用全量数据的均值和方差。原因在于合成样本不能参与标准化参数的计算否则会造成数据泄漏。以下是一段可运行的脚本% 读取原始数据data 为特征矩阵label 为二分类标签0/1 % 分离多数类与少数类索引 idx_min find(label 1); idx_maj find(label 0); X_min data(idx_min, :); X_maj data(idx_maj, :); % 标准化只基于少数类和多数类合并后的训练集计算均值和标准差 mu mean([X_min; X_maj], 1); sigma std([X_min; X_maj], 0, 1); X_min_norm (X_min - mu) ./ sigma; X_maj_norm (X_maj - mu) ./ sigma; % 调用 SMOTE 合成少数类样本 [SYN_norm, SYN_labels] smote_minority(X_min_norm, 5, 300); % 合并并还原到原始尺度 X_all [X_min_norm; X_maj_norm; SYN_norm]; X_all_orig X_all .* sigma mu; y_all [ones(size(X_min, 1), 1); zeros(size(X_maj, 1), 1); SYN_labels];逻辑说明这里的标准化和合并顺序是有讲究的。X_min_norm 与 X_maj_norm 是标准化后的特征合成在标准化空间内完成最后通过 X_all .* sigma mu 还原回原始尺度这样标准化前后的合成样本在特征空间中的相对位置一致不会因为尺度问题产生偏移。参数说明mu 和 sigma 是 1×n 的行向量MATLAB 的隐式扩展让减法、除法和乘法都自动逐列对齐不需要用 repmat 手工复制匹配维度。如果使用 fitcsvm 或 fitcTree 这类分类器建议用标准化后的 X_all 直接训练效果通常优于在原始尺度训练。3.3 随机种子、可复现性与交叉验证的连带问题MATLAB 的 rand 和 randi 默认使用全局随机流直接复用我之前写的代码会出现一个常见问题每次运行生成的合成样本都不同。对论文或竞赛来说这会导致结果不可复现。解决方法是设置随机种子但这里有个坑——不是只设一次种子就完事还要考虑交叉验证中的每一折都需要独立采样。我见过不少人在脚本开头写一行 rng(42)然后整个流程跑下来结果固定但交叉验证时每一折的数据划分其实已经带了随机性SMOTE 内部的随机合成在同一折内又叠加了一次随机性整体来看每次运行仍然不一致。严谨做法是在每次调用 SMOTE 之前显式重置随机流% 固定随机种子保证合成样本可复现 rng(2024, twister); % 在交叉验证循环内部每个 fold 重新设置一次种子 for fold 1:5 rng(fold * 1000, twister); % 划分训练测试集、执行 SMOTE、训练模型、评估 end逻辑说明rng 的第两个参数 twister 指定随机数生成器算法为 Mersenne Twister是 MATLAB 默认且最稳定的生成器。fold * 1000 让每个折的种子不同但可推导这样别人拿到代码后只需要修改种子起始值就能复现全部实验结果。参数说明若在并行环境中使用 parfor每个 worker 需要独立设置随机流仅仅在客户端设置 rng 不会影响 worker。正确做法是用 RandStream 配合 parfor 的 spmd 或直接在每个 worker 内调用 rng。需要强调的是SMOTE 是数据层面的随机过程它与模型训练的随机性比如 SVM 的求解器内部随机初始化是独立的必须分别固定才能做到完全可复现。4. 边界与缺陷SMOTE 失效的五个常见坑4.1 合成样本越界到多数类区域模型反而更混乱现象用 SMOTE 扩样后分类器的精确率下降少数类召回率虽然提升了但误报率显著上升。原因SMOTE 只在少数类样本之间做线性插值当少数类样本分布非常分散、样本量极其稀少时某些少数类样本的近邻中混入了实际靠近多数类簇的样本插值结果落到了多数类密度较高的区域。这种情况下新样本实际上是“披着少数类标签的多数类样本”模型被误导。解决在合成前先剔除孤立点或噪声样本常见的做法是先用 KNN 对少数类样本做一轮筛选如果一个样本的 K 近邻里超过一半属于多数类就把它标记为噪声并剔除不参与合成。MATLAB 里可以用 fitcknn 训练一个临时分类器对少数类样本调用 predict把误分类到多数类的样本过滤掉。4.2 K 值设得过大合成样本绕过数据流形结构现象合成样本看起来和现有样本都很远PCA 降维后明显偏离原有数据分布。原因K 过大时每个样本的近邻集合里可能包含离自己较远的跨簇样本插值结果跨越了原始分布的“空洞”。特征维度较高时欧氏距离对远邻的容忍度会进一步放大。解决把 K 设置在 3 到 8 之间并且优先使用每个样本自己的局部密度来动态调整 K。一种更稳妥的方式是对每个少数类样本先统计其与全部少数类样本的距离分布把 K 设置为其第 30 百分位距离对应的样本数这样不同簇的样本使用不同的近邻数能够保留局部的流形结构。4.3 离散特征被线性插值成无意义的小数现象数据集中包含性别、故障码、城市 ID 这类整数分类特征SMOTE 合成后这些特征变成了 0.37 或 1.62 之类的小数模型无法解释。原因SMOTE 对连续特征和离散特征一视同仁地做插值离散特征本身没有连续语义插值破坏其类别完整性。解决将离散特征从参与 SMOTE 的特征矩阵中剥离数值特征单独合成离散特征则使用近邻样本的众数填充或者从近邻样本中随机抽取一个值作为合成样本的离散特征值。更推荐的做法是把离散特征先做 one-hot 编码再作为一个整体参与 SMOTE但要注意 one-hot 编码后的插值结果不是 0/1需要设定阈值把它重新二值化。4.4 少数类样本太少SMOTE 直接报错或合成的样本严重重复现象少数类只有 3 条样本K 设为 5运行到 min(K, num_min-1) 后变成 2合成出来的样本几乎与原始样本重合模型的提升效果接近于零。原因样本数太少时可供选择的近邻集合只有两个随机挑选的组合极其有限合成样本的多样性大幅下降。更深层的问题是少数类中真实的子类结构可能只有一两个SMOTE 无法在如此少的样本中“发现”更多结构。解决此时不应继续堆 SMOTE而应使用数据层面更强的增强手段对每个少数类样本做小幅的噪声扰动比如在特征上加一个服从 N(0, 0.01) 的高斯噪声用多个扰动版本替代 SMOTE。另一条路线是把 SMOTE 与随机过采样结合先对少数类复制若干份再在复制的数据上做 SMOTE虽然会引入一定的重复样本但至少不会崩溃。4.5 分箱后做 SMOTE 却被数据泄漏反噬现象在训练集上做了分箱归一化或标准化然后用 SMOTE 合成样本最后评估指标看起来很好但上线后模型表现崩盘。原因分箱参数比如 each feature 的箱边界使用了训练集的全部数据计算包括验证集或测试集的信息这属于经典的数据泄漏。SMOTE 合成样本本身不是泄漏源泄漏发生在预处理参数的计算范围上。解决先在训练集内部做交叉验证来估计分箱边界应用固定边界到验证和测试集。更严格的做法是对训练集划分出内部验证集所有预处理参数只从内部训练子集拟合然后把参数应用于内部验证子集和最终测试集。在代码实现上把预处理、SMOTE、模型训练全部封装进一个函数保证没有任何一步先看了全量数据。5. 把 SMOTE 变成可视化工具合成样本分布诊断与参数自检SMOTE 的合成质量不能只靠分类准确率来验证因为准确率对类不平衡问题本来就有遮蔽效应。我后来养成的一个习惯是每次跑完 SMOTE先画一张合成样本与原始样本的降维分布图用 t-SNE 或 PCA 把高维特征投影到二维肉眼观察合成样本是否嵌入了原始数据的簇结构内。如果合成样本明显散成一片或者聚成与原始簇分离的孤立团说明 K 值或 delta 的随机性出了问题这时候先调整参数再谈模型训练。下面是一段完整可运行的绘图脚本基于我常用的诊断流程写出来% 合并原始少数类、合成样本和多数类样本到同一个矩阵 X_vis [X_min; SYN_data; X_maj]; labels_vis [ones(size(X_min, 1), 1); 2 * ones(size(SYN_data, 1), 1); zeros(size(X_maj, 1), 1)]; % 先用 PCA 降到 50 维再用 t-SNE 降到 2 维 % 这样既保留主要结构又不让 t-SNE 计算量过大 [coeff, score] pca(X_vis, NumComponents, 50); Y_tsne tsne(score(:, 1:50), Perplexity, 30); % 绘图三类点分别着色 figure; gscatter(Y_tsne(:, 1), Y_tsne(:, 2), labels_vis, [0.7 0.7 0.7; 1 0 0; 0 0 1], ., 15); legend({多数类, 原始少数类, 合成少数类}, Location, best); title(SMOTE 合成样本分布诊断); xlabel(t-SNE 维度 1); ylabel(t-SNE 维度 2); grid on;逻辑说明这段代码的关键点在于把原始少数类标记为红色、合成样本标记为蓝色多数类设为灰色。合成样本如果显示为蓝色且多数位于红色点聚集的范围内说明 SMOTE 在这份数据上行为正常。如果蓝色点散布到灰色区域深处就得回头查 K 值和训练集划分。参数说明PCA 降到 50 维是经验值如果原始特征少于 50 维则直接使用全部维度。tsne 的 Perplexity 参数控制每个点在降维空间中的近邻平衡样本量小的时候设置 10 到 20 更稳定样本量大则设置在 30 到 50。需要注意的是 tsne 函数在 MATLAB R2024a 之后对 GPU 有内置加速数据量超过五万行时建议开启 GPU。还有一个我常用的自检技巧合成前先固定一个随机种子跑第一次 SMOTE把合成样本保存为 baseline然后换个种子跑第二次对比两次合成样本的分布重心和平均距离。如果两次合成结果的平均欧氏距离差异超过原始样本标准差的 30%说明合成稳定性不足模型的训练结果也会随随机性波动。这种波动在实际项目里是致命的因为别人复现你的实验时结果对不上白白消耗信任。我的经验是SMOTE 不是一把万能钥匙它是一个需要围绕数据特点做配置的工具。常见做法是先对少数类做一次 KNN 密度估计再决定是上 SMOTE 还是改用 Borderline-SMOTE 甚至 ADASYN而不是拿到不平衡数据就无脑调用 smote_minority。从那以后我每次在 MATLAB 里做不平衡数据处理都强制走一遍分布诊断图并对比两个种子的合成结果确认近邻质量和稳定性之后再进入模型训练环节踩坑的概率确实小了很多。希望帮到你。本文还有配套的精品资源点击获取