简介: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_i;delta 越大,越靠近 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 条,N=3,则最终生成 300 条新样本。实际工程里,N 的取值并不是拍脑袋,而是由目标平衡比决定。比如多数类有 1000 条,想要让少数类达到多数类的 60%,需要新增 500 条,那么 N = 500 / 100 = 5。这里有个关键的实现细节:N 在代码里通常是整数,如果 500/100 不是整数,比如目标是 65%,需要合成 550 条,直接取 N=5 会缺 50 条,取 N=6 又会超出 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 到 K+1 列作为近邻索引 neighbor_idx = sorted_idx(:, 2:K+1); 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 里做不平衡数据处理,都强制走一遍分布诊断图并对比两个种子的合成结果,确认近邻质量和稳定性之后再进入模型训练环节,踩坑的概率确实小了很多。希望帮到你。
本文还有配套的精品资源,点击获取