
简介这份资源是面向机器学习初学者与数据挖掘工程师的MATLAB版SMOTE算法实现包用于解决分类任务中少数类样本不足导致的模型偏置问题。压缩包共5个文件以2个.m脚本为核心分别承担SMOTE主算法实现与测试调用另附LICENCE.txt、.gitignore、.gitattributes等工程配置文件整体仅3KB轻量易读便于直接嵌入现有MATLAB工程。代码覆盖数据加载、标准化预处理、K近邻搜索、线性插值合成少数类样本及数据融合等环节并配有测试脚本演示完整调用流程读者可据此快速复现算法并观察平衡前后样本分布变化。目前已有1321人学习下载适合希望理解SMOTE原理、在MATLAB环境中处理不平衡数据集并提升模型召回率与F1分数的读者参考。1. SMOTE 算法在 MATLAB 里到底解决什么问题从一份 300 行 12 个正样本的表格说起手上拿到一份设备故障记录表300 行数据其中「故障」标签只有 12 条剩下 288 条全是「正常」。直接丢进fitcsvm或者fitcensemble训练集准确率能到 96%可一旦拿新数据一测故障样本几乎全被漏判——这就是典型的类别不平衡把分类器带偏了。SMOTESynthetic Minority Over-sampling Technique要干的事很直接不去复制那 12 条少数类样本而是在它们之间的连线上插值造出一批「像故障又没见过」的合成样本让模型在决策边界附近有足够多的少数类证据可学。MATLAB 里做这件事最省事的路径是 Statistics and Machine Learning Toolbox 自带的smote函数一行就能返回过采样后的特征矩阵和标签但真实项目里往往不能只靠这一行——特征里有类别型变量、有量纲差三个数量级的传感器读数、有缺失值直接调smote会翻车。这篇笔记按「先搞懂插值逻辑 → 手写一版可控的 SMOTE → 用官方函数对照验证 → 处理混合类型和边界样本 → 排查常见报错」的顺序展开适合正在用 MATLAB 做故障诊断、信用评分、医学信号分类这类不平衡任务的工程师也适合刚接触matlab深度学习与matlab优化工具箱、想把这套流程接进自己 pipeline 的人。2. SMOTE 的插值逻辑与 MATLAB 实现选型为什么不能直接复制少数类2.1 从一条合成样本的生成公式说起SMOTE 的核心动作只有三步对每一个少数类样本 $x_i$在同类样本里找它的 $k$ 个最近邻随机挑一个邻居 $x_{i}^{nn}$在两点连线上随机取一个位置生成新样本$$x_{new} x_i \delta \cdot (x_{i}^{nn} - x_i), \quad \delta \in [0,1]$$$\delta$ 是 0 到 1 之间的随机数所以合成样本一定落在两个真实少数类样本的连线上不会跑到多数类区域里去。这跟「随机复制」有本质区别复制只是让损失函数里少数类的权重变大决策边界位置不变插值是把少数类的分布「撑开」让分类器看到少数类在特征空间里其实占了一片区域而不是几个孤点。理解这一点就能明白 SMOTE 的两个前提少数类样本之间要有可插值的「空隙」且特征空间的距离度量要有意义。如果少数类只有 3 个样本$k$ 近邻根本凑不齐如果特征里混着「设备型号」这种类别编码欧氏距离算出来就是玄学。2.2 MATLAB 里三条可选路径的取舍在 MATLAB 里落地 SMOTE常见做法有三条选哪条取决于你对可控性和依赖项的要求。路径依赖可控性适用场景官方smote函数Statistics and Machine Learning Toolbox中参数有限纯数值特征、快速验证手写函数仅基础 MATLAB高可改距离、可处理混合类型特征复杂、需要嵌入自定义流程第三方工具箱需额外获取视实现而定一般不必要我一般会先用手写版本跑通逻辑确认合成样本的分布合理再换官方smote做交叉验证。原因很实际官方函数对类别型特征和缺失值的处理是黑匣子出问题时你只能猜手写版本每一行都在你眼皮底下调参和排错都快。2.3 手写一版最小可用的 SMOTE下面这段代码假设输入X是数值特征矩阵每行一个样本y是标签向量1 表示少数类0 表示多数类目标是把少数类扩到指定数量。function [X_new, y_new] smote_simple(X, y, k, targetRatio) % SMOTE_SIMPLE 基础版 SMOTE 过采样 % X: 特征矩阵 n x d % y: 标签向量 n x 11 为少数类 % k: 近邻数一般取 5 % targetRatio: 少数类目标数量 / 多数类数量 X_min X(y 1, :); X_maj X(y 0, :); n_min size(X_min, 1); n_maj size(X_maj, 1); % 需要生成的合成样本数 n_syn round(targetRatio * n_maj) - n_min; if n_syn 0 X_new X; y_new y; return; end % 少数类样本两两距离找 k 近邻 D pdist2(X_min, X_min); D(1:n_min1:end) inf; % 排除自身 [~, idx] sort(D, 2); knn idx(:, 1:min(k, n_min-1)); % 近邻索引 X_syn zeros(n_syn, size(X, 2)); for i 1:n_syn % 随机选一个少数类样本 base randi(n_min); % 从它的 k 近邻里随机选一个 nb knn(base, randi(size(knn, 2))); delta rand(); X_syn(i, :) X_min(base, :) delta * (X_min(nb, :) - X_min(base, :)); end X_new [X; X_syn]; y_new [y; ones(n_syn, 1)]; end逻辑说明pdist2算出少数类样本间的欧氏距离矩阵把对角线设成inf是为了排除「自己跟自己最近」这个无意义结果。sort后取前 $k$ 列就是每个样本的 $k$ 近邻索引。生成循环里base是插值起点nb是插值终点delta控制新样本在连线上的位置。参数说明k取 5 是文献里的常见默认值样本量小于 20 时建议降到 3 甚至 1targetRatio控制过采样强度取 1 表示少数类补到和多数类一样多取 0.5 表示补到多数类的一半——不是补得越多越好后面避坑章节会讲。3. 用官方 smote 函数对照验证参数怎么设、结果怎么看3.1 官方函数的最小调用与参数含义MATLAB 的smote函数签名大致是[X_res, y_res] smote(X, y, K, k, Ratio, ratio)不同版本参数名可能有差异用help smote确认。下面是一个完整可跑的对照脚本% 构造不平衡数据300 个多数类20 个少数类 rng(42); X_maj randn(300, 2) [2, 2]; X_min randn(20, 2) [-1, -1]; X [X_maj; X_min]; y [zeros(300,1); ones(20,1)]; % 官方 smote 过采样 [X_res, y_res] smote(X, y, K, 5, Ratio, 1.0); % 手写版本对照 [X_my, y_my] smote_simple(X, y, 5, 1.0); % 可视化对比 figure; subplot(1,3,1); gscatter(X(:,1), X(:,2), y); title(原始数据); subplot(1,3,2); gscatter(X_res(:,1), X_res(:,2), y_res); title(官方 smote); subplot(1,3,3); gscatter(X_my(:,1), X_my(:,2), y_my); title(手写 smote);逻辑说明rng(42)固定随机种子保证每次跑出来的合成样本一致方便对比。gscatter按标签着色画散点能直观看出合成样本是否落在少数类区域。参数说明K是近邻数Ratio是目标比例。官方函数返回的X_res里既包含原始样本也包含合成样本y_res对应标签不需要自己拼接。3.2 合成样本质量怎么判断三个可量化的检查点光看图不够我一般会算三个指标第一合成样本到最近真实少数类样本的平均距离。这个值应该和少数类样本之间的平均最近邻距离在同一量级如果大出一个数量级说明插值跑到了不该去的地方。第二合成样本被多数类 $k$ 近邻包围的比例。对每个合成样本看它最近的 $k$ 个真实样本里多数类占多少占比过高说明合成样本侵入了多数类区域。第三过采样前后用同一分类器做 5 折交叉验证比较少数类的召回率和精确率。召回率应该明显上升精确率可能略降——如果精确率暴跌说明合成样本质量有问题。% 检查合成样本是否侵入多数类区域 k_check 5; D_syn pdist2(X_res(y_res1 (1:length(y_res)) length(y), :), X); % 对每个合成样本看最近 k 个真实样本的标签分布 [~, nn_idx] sort(D_syn, 2); nn_labels y(nn_idx(:, 1:k_check)); invasion_rate mean(sum(nn_labels 0, 2) / k_check); fprintf(合成样本被多数类包围的平均比例: %.2f\n, invasion_rate);逻辑说明D_syn算的是合成样本到所有真实样本的距离nn_idx取最近邻索引nn_labels查这些近邻的真实标签。invasion_rate超过 0.4 就值得警惕。参数说明k_check可以和生成时的k不同检查用的近邻数取大一点更保守。3.3 和分类器串起来一个完整的交叉验证流程SMOTE 不能在全量数据上做完再过采样那样会造成数据泄漏——合成样本的信息会渗进验证集。正确做法是在每一折的训练集内部做过采样验证集保持原始分布。cv cvpartition(y, KFold, 5); recall zeros(5,1); precision zeros(5,1); for i 1:5 X_train X(training(cv, i), :); y_train y(training(cv, i)); X_test X(test(cv, i), :); y_test y(test(cv, i)); % 只在训练集上过采样 [X_bal, y_bal] smote(X_train, y_train, K, 5, Ratio, 1.0); mdl fitcsvm(X_bal, y_bal, KernelFunction, rbf); y_pred predict(mdl, X_test); tp sum(y_pred 1 y_test 1); fp sum(y_pred 1 y_test 0); fn sum(y_pred 0 y_test 1); recall(i) tp / (tp fn); precision(i) tp / (tp fp); end fprintf(平均召回率: %.3f, 平均精确率: %.3f\n, mean(recall), mean(precision));逻辑说明cvpartition分层划分保证每折里都有少数类样本。过采样只作用于X_trainX_test保持原始不平衡比例这样评估结果才反映真实场景。参数说明KernelFunction换成linear可以对比线性边界下的效果fitcsvm之外也可以换fitcensemble或fitcknnSMOTE 对分类器类型不敏感但对距离度量敏感。4. 混合类型特征与边界样本SMOTE 在真实数据上的四个改造点4.1 类别型特征不能直接算欧氏距离真实表格里常有「设备型号」「区域编码」这类列。如果直接 one-hot 编码后算欧氏距离一个型号差异会被放大成 $\sqrt{2}$ 的距离压过传感器读数的差异。常见做法是数值列做标准化后算欧氏距离类别列单独用汉明距离最后加权合并。function D mixed_distance(X_num, X_cat, w_num, w_cat) % 数值列欧氏距离 类别列汉明距离的加权组合 D_num pdist2(X_num, X_num) / max(pdist2(X_num, X_num(:))); D_cat pdist2(X_cat, X_cat, hamming); D w_num * D_num w_cat * D_cat; end逻辑说明D_num归一化到 [0,1] 避免量纲主导pdist2的hamming选项直接算类别列的不匹配比例。w_num和w_cat按业务重要性调默认可以取 0.7 和 0.3。参数说明归一化用max而不是mean是因为距离矩阵的最大值更能代表「最远两点」的尺度。4.2 边界少数类样本要区别对待SMOTE 对所有少数类样本一视同仁地插值但靠近多数类边界的少数类样本插值后很容易生成「骑墙」样本反而干扰分类器。Borderline-SMOTE 的思路是只对「近邻里多数类占多数」的边界样本做插值且插值方向偏向多数类一侧把决策边界往外推。在 MATLAB 里实现这个变体只需要在生成循环前加一层筛选% 筛选边界少数类样本 D_all pdist2(X_min, X); [~, nn_all] sort(D_all, 2); m 10; % 看最近 10 个邻居 nn_labels_all y(nn_all(:, 1:m)); is_border sum(nn_labels_all 0, 2) m/2; X_min_border X_min(is_border, :);逻辑说明is_border标记出近邻里多数类超过一半的少数类样本后续只对这些样本做插值。如果边界样本太少可以放宽到 m/2。参数说明m取 10 是经验值样本量小的时候降到 5。4.3 缺失值处理先补再插还是先插再补特征里有 NaN 时pdist2会返回 NaN整个距离矩阵就废了。两条路一是先做缺失值填补fillmissing再跑 SMOTE二是把缺失值当成一个额外的类别用混合距离处理。我一般选第一条因为填补方法可控而且 SMOTE 本身不负责处理缺失。X_filled fillmissing(X, knn, NumNeighbors, 5);逻辑说明fillmissing的knn方法用最近邻的均值填补比全局均值更贴近局部分布。参数说明NumNeighbors取 5 到 10 之间太大容易把多数类的值填到少数类样本上。4.4 过采样之后要不要做欠采样少数类补到和多数类一样多训练集规模翻倍训练时间也翻倍。如果多数类样本本身有大量冗余可以在 SMOTE 之后对多数类做随机欠采样把总规模压下来。常见组合是 SMOTE Tomek Links先过采样再删掉那些和少数类互为最近邻的多数类样本清理决策边界。% SMOTE 后用 Tomek Links 清理 [X_bal, y_bal] smote(X, y, K, 5, Ratio, 0.8); % 找互为最近邻的异类样本对 D_bal pdist2(X_bal, X_bal); D_bal(1:size(X_bal,1)1:end) inf; [~, nn_bal] min(D_bal, [], 2); tomek_idx false(size(X_bal,1), 1); for i 1:size(X_bal,1) j nn_bal(i); if y_bal(i) ~ y_bal(j) nn_bal(j) i tomek_idx(i) true; end end X_clean X_bal(~tomek_idx, :); y_clean y_bal(~tomek_idx);逻辑说明互为最近邻且标签不同的样本对就是决策边界上的「钉子」删掉它们能让边界更干净。tomek_idx标记要删除的样本。参数说明Ratio取 0.8 而不是 1.0是给后续清理留出余量避免清理后少数类又不够。5. SMOTE 在 MATLAB 里的避坑清单五条血泪经验5.1 现象smote报错「K must be less than the number of minority samples」原因少数类样本数小于等于K值近邻凑不齐。比如少数类只有 4 个样本K设了 5。解决把K降到n_min - 1或者先检查sum(y1)再决定参数。手写版本里min(k, n_min-1)就是干这个的。5.2 现象过采样后交叉验证准确率反而下降原因在全量数据上做了 SMOTE 再划分训练验证集合成样本同时出现在训练和验证里评估结果虚高或者反过来合成样本质量差把噪声带进了训练集。解决严格在每折训练集内部过采样验证集保持原始分布。同时用 3.2 节的入侵率指标检查合成样本质量。5.3 现象合成样本的某些特征值超出物理范围原因插值公式在两个样本连线上取点如果两个样本的某个特征差异很大中间值可能落在合理范围之外。比如温度特征两个样本是 20 和 80插值出 50 没问题但如果是「开关状态」0 和 1插值出 0.5 就没有物理意义。解决对有明显边界的特征插值后做截断或取整类别型特征不要参与插值用 4.1 的混合距离方案。5.4 现象pdist2内存溢出原因样本量上万时pdist2返回的 $n \times n$ 距离矩阵占用内存是 $n^2$ 量级10000 个样本就是 800MB。解决用knnsearch替代pdist2它只返回 $k$ 个近邻而不存全矩阵或者分批计算距离。[idx, ~] knnsearch(X_min, X_min, K, k1); knn idx(:, 2:end); % 第一列是自身去掉5.5 现象过采样后模型对多数类的识别能力下降原因少数类补得太多模型偏向少数类多数类的召回率被牺牲。Ratio设成 1.0 甚至更高时尤其明显。解决Ratio从 0.3 开始试逐步加到 0.8 左右用验证集上的 F1 或 AUC 选最优点。不要默认补到 1:1那个比例只适合少数类极度稀缺的场景。6. 把 SMOTE 接进 MATLAB 分类 pipeline 的两个进阶技巧6.1 用自定义距离函数替换欧氏距离smote官方函数不支持自定义距离但手写版本可以。如果你的特征里有周期性变量比如小时、角度欧氏距离会把 23 点和 0 点算成很远实际上它们只差 1 小时。这时候可以传入一个自定义距离函数function d circular_distance(X1, X2, period) % 周期性特征的距离取环形最短路径 diff abs(X1 - X2); d min(diff, period - diff); end逻辑说明对每个周期性维度算环形距离再和其他维度的欧氏距离合并。period是周期长度小时就是 24。参数说明这个函数要嵌进pdist2的自定义距离接口或者直接在knnsearch里用Distance参数指定。6.2 用 AUC 而不是准确率来选过采样强度不平衡数据里准确率是个误导性指标。300 个样本里 12 个少数类全预测多数类就有 96% 准确率。选Ratio参数时我一般跑一组值画 AUC 曲线ratios 0.2:0.2:1.5; aucs zeros(size(ratios)); for i 1:length(ratios) [X_bal, y_bal] smote(X_train, y_train, K, 5, Ratio, ratios(i)); mdl fitcsvm(X_bal, y_bal, KernelFunction, rbf); [~, scores] predict(mdl, X_test); [~, ~, ~, aucs(i)] perfcurve(y_test, scores(:,2), 1); end plot(ratios, aucs, -o); xlabel(Ratio); ylabel(AUC);逻辑说明perfcurve返回的第四个输出是 AUC 值scores(:,2)是预测为正类的分数。曲线峰值对应的Ratio就是当前数据下的较优值。参数说明ratios范围根据数据不平衡程度调极端不平衡可以从 0.1 开始试。perfcurve需要预测分数而不是硬标签所以用predict的两个输出。这套流程跑下来SMOTE 在 MATLAB 里就不再是一个黑匣子函数而是一个你能控制插值方向、能检查合成质量、能接进交叉验证的组件。我自己的习惯是任何过采样结果先画散点图看一眼再算入侵率最后跑 AUC 曲线定参数——三步都过了才敢往生产模型里放。希望帮到你。本文还有配套的精品资源点击获取