简介SMOTE算法MATLAB版是一份面向机器学习初学者与数据分析人员的算法实现资源主要解决分类任务中少数类样本不足导致的模型偏差问题适用于金融风控、医疗诊断等类别不平衡场景。资源包内含5个文件体积仅3KB其中两个m文件分别对应SMOTE核心算法与测试脚本另有txt说明文档及gitattributes、gitignore工程配置文件结构精简便于直接阅读和二次修改。已有1321人学习下载适合希望快速理解过采样原理并在MATLAB中动手实践的读者。通过此资源可掌握SMOTE为少数类样本寻找K个近邻并插值生成新样本的完整逻辑同时了解数据预处理、样本合成与融合的操作流程也可以直接调用函数完成数据集平衡为后续训练SVM、决策树等模型提供更均衡的输入从而提升对少数类的识别效果和模型泛化能力。1. 类不平衡数据里最常见的“后悔药”SMOTE 算法在 Matlab 里怎么用起来二分类任务里正负样本比例悬殊到 1:50 时模型会直接躺平——把所有样本都判成多数类训练准确率照样有 98%但少数类一个都抓不住。SMOTE 算法就是在这种场景下反复被提及的名字它不靠简单复制少数类样本而是在特征空间中沿着样本连线插值生成一批人工少数类样本把失衡的训练集掰回相对平衡的状态。这篇笔记把 SMOTE 在 Matlab 环境下的实现从原理到落地讲清楚新手能跟着把代码跑通熟手可以直接参考边界条件和参数经验。2. SMOTE 的原理拆解线性插值为什么比盲目复制更有效2.1 过采样的痛点复制样本只是把噪声也复制了一遍最直观的过采样做法是随机复制少数类样本把训练集数量凑齐。这个思路在数据量小的时候似乎没问题但模型训练时会发现问题复制出来的样本和原始样本完全重叠相当于给同一个点叠加了更高的权重。决策树这类模型会因为这个重复样本把分裂点的选择带偏支持向量机则会在重复样本区域形成过大的间隔压力结果往往是训练集表现很好验证集召回率不升反降。SMOTE 的思路完全不同。它不是在原始样本上原地复制而是把少数类样本看成一个点在这个点和它的近邻点之间连一条线段再在线段上随机取一个新的点作为合成样本。这样做出来的样本和原始样本不完全相同但又在特征分布上和少数类处在同一片区域给分类器提供了“新的证据”而不是“重复的旧证据”。这也是把 SMOTE 称作过采样里“后悔药”的原因——在不丢弃任何多数类样本的前提下通过补充少数类样本把决策边界往中间推。2.2 合成样本的数学过程连线、分段、取点SMOTE 的合成过程可以拆成三步。第一步对于每个少数类样本 (x_i)用 KNN 找出它在少数类集合里的 k 个近邻这里要注意近邻只在少数类内部找不会把多数类样本拉进来。第二步从这 k 个近邻里随机选出一个 (x_{nn})。第三步在 (x_i) 和 (x_{nn}) 之间做线性插值用公式表示为[ x_{new} x_i \delta \cdot (x_{nn} - x_i), \quad \delta \in (0,1) ]这里的 (\delta) 是 0 到 1 之间均匀分布的随机数。当 (\delta) 接近 0 时合成样本靠近原始样本接近 1 时合成样本靠近被选中的近邻在 0.5 附近时合成样本落在两点中间。这个“在两点之间取随机位置”的操作让每个合成样本都不是固定的从同一对原始样本出发每次运行都可能得到不同的合成样本这也是 SMOTE 能带来多样性而不是简单复制的原因。这个公式在二维特征空间里很好理解两个样本各有两个坐标连成一条直线新样本就是直线上随机取的一个点。高维情况下数学本质一样只不过在两个样本的 n 维向量之间做逐维线性插值。2.3 为什么 SMOTE 能改善决策边界随机复制样本不会改变少数类样本在空间里的分布范围只是加重了已有点位的权重。SMOTE 生成的样本落在少数类样本的连线上实际上是在扩大少数类样本的“覆盖范围”让分类器在原本没有样本的空白区域也能看到少数类的存在从而把决策边界往多数类方向推移。需要注意的风险是如果少数类样本本身很分散、彼此距离很远SMOTE 生成的新样本会落在两者连线的中间可能横跨多数类密集的区域甚至产生“混合样本”的噪声。这也是为什么 SMOTE 并不总是优于随机过采样——在少数类内部存在明显子簇的情况下盲目连线会织出一片不属于任何一类的中间地带。理解这个前提是后面落地时判断“什么时候该用 SMOTE”的基础。3. 在 Matlab 里手写 SMOTE核心代码与逐行讲透3.1 主函数框架输入输出怎么设计Matlab 自带的 Statistics and Machine Learning Toolbox 里并没有名为smote的官方函数但有一个从 R2021a 开始提供的smote方法可以在fitcnet等模型的训练流程里用。实际项目里更多人选择手写一个函数原因很直接手写版本可以完全控制近邻数、采样倍率、距离度量还能帮理解原理。这里给出一个可以直接复制的函数头建议保存成smote_oversample.m。完整函数框架如下function [synX, synY] smote_oversample(X, label, minClass, N, k) % SMOTE_OVERSAMPLE 手动实现 SMOTE 过采样 % 输入 % X : 特征矩阵size [m, n]m 为样本数n 为特征数 % label : 标签向量size [m, 1]取值可以是数值或逻辑 % minClass : 少数类的标签值例如 0 或 minority % N : 每个少数类样本需要生成的合成样本个数 % k : 近邻个数默认取 5 % 输出 % synX : 生成的合成特征矩阵size [numMin * N, n] % synY : 对应的标签向量全部等于 minClass if nargin 5 k 5; end if nargin 4 error(至少需要传入 X, label, minClass, N 四个参数); end minIdx (label minClass); X_min X(minIdx, :); numMin size(X_min, 1); if numMin 2 error(少数类样本数量不足无法进行 KNN 搜索); end synX zeros(numMin * N, size(X, 2)); synY zeros(numMin * N, 1); ... end这里把输入的校验放在最前面目的是避免在循环深处才暴露问题。numMin 2的判断很关键因为 KNN 搜索至少需要两个样本才能找到近邻如果少数类只有一个样本SMOTE 就无从谈起。synX用zeros预分配内存在样本量上千、特征维度几十上百时能明显提升运行速度。3.2 核心计算KNN 搜索与新样本生成主函数里的核心循环如下每一行都做了注释方便对照公式理解。% 对每个少数类样本分别找近邻并生成合成样本 for i 1:numMin % 计算当前样本到所有其他少数类样本的欧氏距离 diff X_min - X_min(i, :); dist sqrt(sum(diff.^2, 2)); dist(i) inf; % 排除自己 % 按距离升序排序取前 k 个作为近邻下标 [~, idxSorted] sort(dist); nnIdx idxSorted(1:k); % 每个原始样本生成 N 个合成样本 for j 1:N % 从 k 个近邻中随机挑一个 pick nnIdx(randi(k)); % 生成 0 到 1 之间的随机数向量长度等于特征数 gap rand(1, size(X, 2)); % 线性插值生成新样本 synX((i-1)*N j, :) X_min(i, :) gap .* (X_min(pick, :) - X_min(i, :)); synY((i-1)*N j) minClass; end enddiff是广播减法把当前样本的坐标从所有样本坐标里减掉得到的是一个numMin x n的矩阵sum(diff.^2, 2)对每一行求和得到距离平方再开根号得到欧氏距离。这里用sort(dist)而不是mink是因为排序结果里除了距离本身还有下标方便统一取前 k 个。如果样本量特别大可以考虑用分块距离计算减少内存占用。gap一次性生成一个和特征维度等长的随机向量这比逐维生成随机数再组装要快。X_min(i, :) gap .* (X_min(pick, :) - X_min(i, :))就是向量形式的插值公式和数学表达式完全对应。3.3 调用方式与输出校验写完成函数后用一个无标签偏移的简单数据验证逻辑。下面的脚本生成两类样本一类 200 个一类 40 个把少数类倍率设为 3k 设为 5调用后统计合成样本数量。% 生成示例数据 rng(42); X_major randn(200, 2) * 0.8 [2, 2]; X_minor randn(40, 2) * 0.5 [-1, -1]; X [X_major; X_minor]; label [zeros(200, 1); ones(40, 1)]; % 调用 SMOTE每个少数类样本生成 3 个新样本 [synX, synY] smote_oversample(X, label, 1, 3, 5); fprintf(原始少数类样本数: %d\n, sum(label 1)); fprintf(合成样本数: %d\n, size(synX, 1));运行后应该看到原始少数类样本 40 个合成样本 120 个。拿到合成样本后可以先做一步可视化验证在二维特征空间里把原始样本和合成样本同时画出来肉眼确认合成样本是否落在少数类样本的连线附近。如果合成样本明显跑到了多数类区域就要回头检查特征是否需要归一化以及 k 值是否选得太大。rng(42)这一行虽然不起眼但对复现实验非常关键。SMOTE 内部的randi和rand都依赖随机数流固定随机种子可以保证同一份代码在重跑时得到相同的合成样本这在对比实验里是基本要求也是调试时的心定丸。4. 参数怎么设k 值、采样倍率、距离度量的边界4.1 k 近邻个数的选择与影响k 是 SMOTE 里最敏感的参数。k 太小比如 k1合成样本只会在每一对最近的样本之间产生多样性受限生成结果基本都挤在少数类的局部密集区。k 太大比如 k20近邻范围扩大插值时可能连接到离当前样本很远的样本合成样本容易跨越到多数类的地盘。实际使用经验是默认 k5在样本量小于 100 时考虑k min(5, numMin-1)防止 k 超过少数类样本总数导致下标越界。样本量较大且少数类内部结构清晰时可以把 k 调到 37 之间做一轮 grid search评估指标优先看少数类的召回率而不是整体准确率。Matlab 里做这个搜索并不复杂写一个 for 循环遍历 k每次调用smote_oversample后训练分类器并计算召回率即可。另一个容易被忽略的坑是特征维度。当特征维度超过几十甚至上百时高维空间里的欧氏距离区分度会下降k 近邻的质量随之变差。这种情况靠调 k 解决不了需要先降维或者换距离度量。4.2 采样倍率 N 与平衡目标设计N 是每个少数类样本生成的合成样本数量最终少数类的总样本数是原始数 × (N1)。这里的 N 可以是小数吗在多数实现里 N 用整数。如果要生成 1.5 倍也就是让少数类从 40 变成 100可以先按 N2 生成 80 个再加 20 个原始样本凑出目标比例。在 Matlab 里更简洁的做法是把 N 直接设为(目标多数类数量 - 原始少数类数量) / 原始少数类数量的向上取整值生成后再随机抽掉多余的合成样本。平衡目标不是固定的 1:1。1:1 不代表最优尤其是在多数类本身信息量不足的时候强行 1:1 会让合成样本占比过高模型容易过拟合到合成样本的模式上。常见的做法是往 1:2 或 1:3 调也就是多数类保持两倍或三倍于少数类既缓解不平衡又不至于让合成样本主导训练集。这种比例在实际项目里往往比 1:1 更稳推荐作为默认起点。4.3 归一化、距离度量与离散特征处理SMOTE 的插值没有范围限制如果特征量纲差异大距离计算会被量纲大的特征主导插值结果也会集中在那几个维度上。比如年龄的范围是 0100收入的范围是 500050000收入维度几乎完全决定了距离。解决办法是先对特征做 z-score 归一化再进 SMOTE生成完样本后再把合成样本的对应特征按训练时记录的均值和标准差还原回去。距离度量方面默认欧氏距离在绝大多数场景够用但如果特征里混着大量二值特征可以用曼哈顿距离换一下试试。在 Matlab 里要换距离度量只需要把核心循环里的sqrt(sum(diff.^2, 2))替换成sum(abs(diff), 2)改动成本很低。离散特征的问题更绕。如果特征是类别型比如“颜色”取值为 1/2/3插值后可能出现 2.3 这种荒谬取值。常见的处理有两个方向一是把类别特征做 one-hot 后参与插值合成样本落在超平面内部不再对应某一类这个问题依然存在二是在插值后对类别特征做就近取整再映射回原来的类别值。这是个没有完美解的环节实际项目中可以接受 one-hot 后的非整数作为“软类别”也可以做四舍五入关键是要始终用相同的方式处理训练集和后续测试集不要两边不一致。5. 落地 SMOTE 的常见坑与排查现象、原因、解决5.1 合成样本全是 NaN现象smote_oversample运行后synX里出现大量 NaN分类器训练直接报错。原因特征矩阵里本身含有 NaN或者少数类样本在某些维度上标准差为 0导致插值过程中gap .* (X_min(pick, :) - X_min(i, :))里的减法出现 NaN 传播。解决进入 SMOTE 前先删掉或填充 NaN。Matlab 里最粗暴的做法是X(any(isnan(X), 2), :) []删掉包含 NaN 的行如果特征有实际业务意义可以用fillmissing按列填充均值或中位数。另外插值后可以加一行验证assert(~any(isnan(synX(:)), all))提前拦截问题省得等到训练才爆雷。5.2 少数类样本数量小于等于 k程序直接下标越界现象idxSorted(1:k)报错提示索引超出数组边界或者生成了空白样本。原因少数类只有 4 个样本k 设为 8idxSorted只有 4 个元素取前 8 个自然越界。解决把 k 动态裁剪到min(k, numMin-1)并且在函数入口加一行校验逻辑。上面第 3 章代码里的框架就有这条判断实际项目中常有人把 k 写死成配置文件里的 5换到小样本数据集上就翻车。更稳妥的做法是在调用处就根据少数类样本数计算 kk_effective min(k, sum(label minClass) - 1)。5.3 把 SMOTE 应用在测试集上造成信息泄漏现象模型在交叉验证里召回率奇高但线上评估时表现断崖式下降。原因SMOTE 的合成样本携带了训练集少数类的分布信息如果对包含测试集在内的整个数据集做 SMOTE测试集的原始信息会以合成样本的形式参与训练相当于把考试答案提前塞进了复习资料。解决SMOTE 只能在训练集上执行验证集和测试集保持原始分布。在 Matlab 里先划分训练测试集再对训练集的少数类部分做 SMOTE合成样本只和训练合的样本混合。这一步在代码上就是把smote_oversample的调用放在cvpartition之后而不是全局执行这个顺序不要写反。5.4 原始特征数值范围小合成样本却跑出离谱范围现象合成样本里的个别特征值比训练集里的最大最小值大出好几倍分类器在验证集上方差很大。原因当 k 值偏大、两个被选中的近邻之间距离很远时插值点会落在线段的极端位置尤其是当gap接近 1 且两个近邻跨在多数类两侧时合成样本会进入多数类区域。解决限制插值范围。常见做法是把gap限制在 0.2 到 0.8 之间不让新样本落在距离任一原始样本过近的位置。也可以对合成样本做后处理把每个维度 clip 回训练集该维度的取值范围。这两种做法都会带来一定的偏差但从实测效果看前者更自然后者更容易实现且不会破坏样本真实度。5.5 离散特征插值出非整数无效值现象类别型特征生成出 2.7 这样的数值转成整数后和原始类别分布对不上。原因SMOTE 的线性插值逻辑天然是为连续特征设计的对类别特征没有保护机制。解决分两步走。首先类别特征在参与距离计算前一般是 one-hot 编码插值后保留连续值其次若业务要求生成样本必须是合法类别就在生成后对类别列做就近取整并对 one-hot 列做 argmax 回映射。这套做法不是完美的但至少保证合成样本能进入下游模型而不报错。针对这个问题也有很多变体算法比如 SMOTE-NC专门处理类别特征Matlab 里没有现成函数要自己补一个后处理函数代码量和上面提到的就近取整差不多。6. 进阶用法把 SMOTE 嵌入真实建模流程的三个技巧第一个技巧是始终在交叉验证内部做 SMOTE。用cvpartition划分 K 折每折里只在训练部分执行 SMOTE验证部分不碰。这样评估出的泛化能力才和线上真实场景一致。在 Matlab 里可以写一个闭包函数输入原始训练集输出增强后的训练集再放到fitcsvm或fitcensemble的循环里调用。这个习惯能避免很多在 5.3 里提到的信息泄漏问题。第二个技巧是针对结合分类器训练做增强。SMOTE 生成的合成样本和原始样本数量一旦失衡分类器会拟合出偏向合成样本分布的模式。可以在合成样本上乘一个 0.7~0.9 的权重降低它们在目标函数里的影响或者用fitcsvm的Weights参数给原始样本和合成样本赋予不同权重。配合成本敏感学习的代价矩阵使用通常比单独用 SMOTE 的效果更好。第三个技巧是识别样本中的边界区域。先把少数类样本按到多数类样本的平均距离排序距离较大的样本本身就处在危险地带对这些样本做 SMOTE 时生成的合成样本更容易引入噪声。可以在 SMOTE 前只保留距离中位数以内的少数类样本再对这些样本进行插值能减少跨界噪声。这种做法是 Borderline-SMOTE 的核心思想Matlab 实现时只需增加一个距离过滤的步骤改动量不大。最后提一句我在项目里经常犯的错是通篇默认 SMOTE 一定比随机过采样好后来发现少数类样本密度极高、分布区间非常紧凑时SMOTE 反而会把合成样本铺到空白区域破坏原有密度。现在我的做法是先跑一版随机过采样做 baseline再跑 SMOTE比较两者在验证集的结果差异差异不大就选简单的差异明显才深入调 SMOTE。这个习惯帮我省了不少调试时间也希望帮到你。本文还有配套的精品资源点击获取