SMOTE算法MATLAB实现:原理、代码与避坑指南
简介这份资源是面向机器学习初学者与数据挖掘实践者的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。然后对每一行排序取前 $k1$ 个索引第一个是样本自己要跳过。% minority: 少数类样本矩阵每行一个样本 % k: 近邻数 distMat pdist2(minority, minority); % 对称距离矩阵 [~, idx] sort(distMat, 2); % 每行按距离升序排列 neighborIdx idx(:, 2:k1); % 去掉自己取前 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 endrandi(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 参数都强制走一遍可视化再决定要不要信指标。希望帮到你。本文还有配套的精品资源点击获取