☰
SMOTE算法MATLAB实现:原理、代码与避坑指南
2026/11/6 6:55:03 网站建设 项目流程

简介:这份资源是面向机器学习初学者与数据挖掘实践者的SMOTE算法MATLAB实现包,用于解决训练集中少数类样本过少导致的类别不平衡问题,帮助提升模型对少数类的识别能力。压缩包共5个文件,约3KB,以.m源码文件为主,包含SMOTE核心实现与配套测试脚本,另附LICENCE.txt授权说明及.gitignore、.gitattributes等版本管理配置,结构精简,便于直接阅读与二次修改。已有1324人学习下载,说明其在同类实现中具有一定参考价值。读者可从中获得SMOTE的完整MATLAB代码逻辑,理解K近邻搜索、随机插值合成新样本的具体写法,并借助测试脚本快速跑通从数据加载、过采样到平衡数据集生成的流程,为后续结合SVM、决策树等模型验证效果提供可复用的代码基础。

1. SMOTE 的直觉陷阱:为什么“插值造样本”能救活一个濒死的分类器

少数类样本太少,分类器干脆学会了一件事——把所有样本都判成多数类,准确率还能有 95%。这不是模型坏了,是数据分布把损失函数带偏了。SMOTE 的做法反直觉:它不去改损失函数,也不去调阈值,而是直接在特征空间里“无中生有”地造出新样本。听起来像玄学,但背后是一条极朴素的假设——两个少数类样本连线上的点,大概率还是少数类。搞懂这条假设成立的条件、失效的边界,以及 MATLAB 里那几十行代码到底在做什么,比调一遍fitcknn的参数有用得多。这份 MATLAB 版本的 SMOTE 实现,恰好把“插值”这件事拆到了你能逐行验证的程度。

2. 从 k 近邻到合成样本:SMOTE 的机制拆解

2.1 为什么是“插值”而不是“复制”

最原始的过采样是随机复制少数类样本,也就是简单地把同一行数据抄若干遍。问题在于,复制不增加任何新的特征组合,分类器的决策边界不会因为多了几个一模一样的点而往外扩。更糟的是,如果少数类样本本身聚集在一个很小的区域,复制只会让这个区域更“重”,模型对边界附近的样本依然一无所知。

SMOTE 的出发点是把少数类样本当成一个点集,在点与点之间做线性插值。具体来说,对每一个少数类样本 $x_i$,从它的 $k$ 个最近邻里随机挑一个 $\hat{x}_i$,然后在新样本落在连线上:

$$x_{new} = x_i + \lambda \cdot (\hat{x}_i - x_i), \quad \lambda \sim U(0,1)$$

这个式子里有两个关键设计。第一,$\lambda$ 取 0 到 1 之间的随机数,保证新样本落在两个真实样本之间,不会跑到特征空间里没人去过的荒郊野岭。第二,近邻是在少数类内部找的,不是全局找的,这样插值方向始终沿着少数类自身的分布走向,而不是被多数类带偏。

常见的实现思路是:先算少数类样本两两之间的距离矩阵,对每一行排序取前 $k$ 个索引,再对每个样本随机选一个邻居做插值。MATLAB 里用pdist2算距离、sort取近邻、rand生成 $\lambda$,整个流程不超过二十行。但就是这二十行,藏着几个容易翻车的细节,后面单独说。

2.2 k 值的选择:不是越大越好

$k$ 是 SMOTE 唯一的超参数,也是最容易被随手设成 5 的那个数。它的物理含义是:插值时参考的“邻居范围”有多大。$k$ 太小,比如取 1,新样本只会落在最近的那一对样本连线上,合成样本的多样性极低,相当于在局部反复做线性组合,对决策边界的扩展有限。$k$ 太大,比如取到少数类样本总数的一半,邻居可能已经跨到了少数类分布的另一端,插值出来的点会横穿整个特征空间,落到多数类的地盘上,反而制造噪声。

一个经验性的取值区间是 5 到 10,但这背后有前提:少数类样本本身要有一定的数量。如果少数类只有 8 个样本,$k$ 取 5 已经接近“全局邻居”,插值的随机性会变得很大。更稳妥的做法是先看少数类样本量,$k$ 不超过 $\lfloor n_{minority}/2 \rfloor$,再在这个范围内试。MATLAB 实现里通常把 $k$ 作为函数入参暴露出来,而不是写死,就是为了让你能根据数据规模调整。

还有一个容易被忽略的点:距离度量。默认用欧氏距离没问题,但如果特征量纲差异很大,比如一个特征是年龄(0 到 100),另一个是收入(0 到 100000),欧氏距离会被收入主导,近邻实际上只反映了收入维度的接近程度。这时候要么先做标准化,要么换用标准化后的距离。SMOTE 本身不处理量纲,这个锅得调用方背。

2.3 合成比例:不是越多越好

SMOTE 的另一个入参是合成倍数,也就是少数类要扩到原来的几倍。很多人直觉上觉得“少数类越接近多数类越好”,直接把倍数拉到让两类完全平衡。但合成样本终究是插值产物,不是真实观测,它的信息量低于真实样本。过度合成会让少数类的分布变得“虚胖”,分类器在训练集上表现很好,一到真实数据上就露馅。

更合理的做法是分步走:先合成到少数类占比 20% 到 40%,看验证集上的召回率和精确率怎么变,再决定要不要继续加。MATLAB 实现里一般用N表示每个少数类样本要生成的新样本数,总合成量是 $N \times n_{minority}$。这个参数和 $k$ 是解耦的,可以单独调。如果发现合成后模型对少数类的召回率上去了但精确率掉得厉害,说明合成过头,边界被推得太远,把多数类样本也圈进来了。

提示:合成比例和 k 值要一起看。k 大、比例高,等于在更大的邻域里做更多次插值,噪声风险叠加。

3. MATLAB 实现里的关键代码:逐行看清插值过程

3.1 距离矩阵与近邻索引

SMOTE 的第一步是把少数类样本单独拎出来,算它们之间的距离。MATLAB 里最直接的是pdist2,返回一个对称的距离矩阵,对角线为 0。然后对每一行排序,取前 $k+1$ 个索引(第一个是样本自己,要跳过)。

% minority: 少数类样本矩阵,每行一个样本 % k: 近邻数 distMat = pdist2(minority, minority); % 对称距离矩阵 [~, idx] = sort(distMat, 2); % 每行按距离升序排列 neighborIdx = idx(:, 2:k+1); % 去掉自己,取前 k 个邻居

这段代码有两个隐含假设。第一,pdist2默认欧氏距离,如果特征需要标准化,得在调用前手动做,SMOTE 函数本身不管。第二,sort对每行独立排序,返回的idx是列索引,也就是邻居在minority里的行号。neighborIdx的每一行对应一个少数类样本的 $k$ 个邻居索引,后面插值时直接按这个索引取邻居向量。

一个常见的翻车点是少数类样本里有重复行。重复样本之间的距离为 0,排序后会占据近邻的前几位,导致插值时选到的“邻居”其实是自己,合成出来的新样本和原样本完全一样,等于白做。如果数据里可能有重复,去重要在算距离之前做,或者至少在选邻居时排除距离为 0 的项。

3.2 插值生成新样本

拿到邻居索引后,对每个少数类样本随机选一个邻居,生成 $\lambda$,做线性插值。MATLAB 里可以用循环,也可以向量化,但循环版本更容易看清每一步在干什么。

nMinority = size(minority, 1); nFeatures = size(minority, 2); N = 100; % 每个少数类样本生成的新样本数 synthetic = zeros(nMinority * N, nFeatures); for i = 1:nMinority for j = 1:N nn = neighborIdx(i, randi(k)); % 随机选一个邻居 lambda = rand(); % 0 到 1 之间的随机数 synthetic((i-1)*N + j, :) = minority(i, :) + ... lambda * (minority(nn, :) - minority(i, :)); end end

randi(k)从 $k$ 个邻居里等概率选一个,rand()生成插值系数。新样本的每一维都是两个真实样本对应维的线性组合,所以不会出现某一维超出原始范围的情况——这是插值相对于随机扰动的优势。synthetic预分配了 $n_{minority} \times N$ 行,避免循环里动态扩容,MATLAB 里这个细节对性能影响不小。

如果数据量很大,双重循环会慢。向量化的写法是一次性生成所有随机邻居索引和 $\lambda$,然后用矩阵运算算差值。但向量化版本的可读性差一些,调试时不容易定位是哪个样本出的问题。我的习惯是先用循环版本跑通、验证结果合理,再换向量化版本提速度。

3.3 合成样本的合并与标签

合成完之后,把新样本拼回原始训练集,标签统一标成少数类。这一步看起来简单,但有个顺序问题:合成样本必须在划分训练集和测试集之后做,不能先合成再划分。否则同一个原始样本插值出来的多个合成样本可能同时出现在训练集和测试集里,造成数据泄漏,测试集上的评估结果会虚高。

X_train_aug = [X_train; synthetic]; y_train_aug = [y_train; repmat(minorityLabel, size(synthetic, 1), 1)];

repmat把少数类标签复制成列向量,和合成样本行数对齐。如果原始标签是 categorical 类型,repmat也能正确处理,但要注意minorityLabel的类型和y_train一致,否则拼接时会报类型不匹配。这个错误在 MATLAB 里不算罕见,尤其是从 CSV 读进来的标签默认是 cell 或 string,而手动构造的是 double。

4. 避坑与排查:SMOTE 在 MATLAB 里最容易翻车的几件事

4.1 合成样本落在多数类区域

现象:合成之后少数类召回率上去了,但精确率暴跌,混淆矩阵里多数类被大量误判成少数类。

原因:$k$ 取得太大,或者少数类本身分布很散,插值时选到的邻居在特征空间里离得很远,连线穿过了多数类聚集区。SMOTE 的插值假设是“连线上大概率还是少数类”,但这个假设在少数类边界模糊时不成立。

解决:先把 $k$ 降到 3 到 5 试,观察合成样本在特征空间里的分布(可以用 PCA 降到二维画散点)。如果合成点明显混进了多数类区域,说明这批数据不适合纯 SMOTE,考虑用边界感知的变体,或者先做特征选择把区分度低的维度去掉。

4.2 距离计算被量纲绑架

现象:合成样本在某些特征上取值离谱,比如年龄变成了负数,或者某个二值特征出现了 0.37 这种值。

原因:没有标准化,欧氏距离被大量纲特征主导,近邻实际上只在那个维度上接近,其他维度完全随机。插值时 $\lambda$ 作用在所有维度上,小量纲特征被大量纲特征的差值带着跑,越界了。

解决:在调用 SMOTE 之前对特征做标准化(zscore或 min-max),合成完之后再反标准化回原始量纲。注意二值特征和类别特征不能直接插值,要么单独处理,要么在距离计算时给它们降权。

4.3 重复样本导致无效合成

现象:合成样本和原始少数类样本高度重合,扩增后模型表现几乎没变化。

原因:少数类里有重复行,距离矩阵里出现 0,排序后重复样本占据近邻前列,插值时选到的“邻居”距离为 0,$\lambda$ 乘上去还是 0,新样本等于原样本。

解决:合成前对少数类做unique去重,或者在选邻居时过滤掉距离小于某个阈值的项。去重会减少少数类样本量,需要相应调大合成倍数来补偿。

4.4 先合成再划分导致数据泄漏

现象:交叉验证或测试集上的指标好得不像话,换一批真实数据就崩。

原因:合成样本是在全量数据上做的,同一个原始样本的插值产物同时进了训练集和测试集,测试集里出现了训练集的“近亲”。

解决:严格按“先划分、后合成”的顺序,只在训练集内部做 SMOTE。如果要做交叉验证,每一折的训练部分单独合成,验证部分保持原始分布。这个坑在 MATLAB 里尤其容易踩,因为很多人习惯先把数据整理好再统一处理。

5. 设计取舍:SMOTE 的边界在哪里,什么时候该换思路

SMOTE 的核心假设是“少数类样本之间的连线仍属于少数类”,这个假设在低维、连续、类别边界清晰的数据上成立得很好。但它的局限也很明显:插值只在特征空间做线性组合,不关心样本的语义合理性。比如一个特征是“是否已婚”(0/1),插值出 0.5 没有任何物理意义;再比如高维稀疏数据,两个少数类样本在欧氏距离上很近,但中间区域可能全是多数类,插值直接制造噪声。

MATLAB 实现的好处是透明——你能看到每一行代码在做什么,能改距离度量、能换插值策略、能在合成前后加自己的过滤逻辑。代价是没有内置的自适应机制,$k$ 和合成比例得自己试。如果数据维度很高、类别边界复杂,纯 SMOTE 往往不够,需要考虑结合欠采样、代价敏感学习,或者用基于聚类的变体先识别出少数类的核心区域再插值。

我一般会在合成之后做一件事:把原始少数类、合成少数类、多数类用不同颜色画在同一张 PCA 图上,肉眼扫一遍合成点有没有跑到不该去的地方。这个习惯来自一次血泪经验——$k$ 设成 10 跑完指标很好看,画图才发现合成点把多数类的一个子群整个包了进去,模型学到的是一条虚假边界。从那以后我每次调完 SMOTE 参数都强制走一遍可视化,再决定要不要信指标。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询