简介:面向机器学习与数据挖掘场景,这份压缩包针对类别不平衡问题提供MATLAB环境下的SMOTE插值实现,适合需要处理数据不平衡的算法工程师、数据科学家及科研人员。在医疗诊断、信用欺诈等任务中,少数类样本往往承载关键价值,SMOTE通过挑选少数类样本、计算k近邻并沿连线线性插值合成新样本,从而改善分类模型对少数类的识别能力。资源共2个m文件,大小约1KB,一个为核心算法代码,另一个为可直接运行的示例脚本,便于在自带数据上快速验证效果。已有340人学习下载。代码同样适用于数值分析中的缺失值补全,可在保持原始分布特征的前提下填充空缺,避免简单删除带来的信息损失。读者可对照代码理解过采样原理,并根据自身任务调整近邻数、采样倍率等参数,灵活运用于不平衡分类、数据增强或数据清洗流程。合成样本与原始数据可直接合并构成平衡数据集,用于后续模型训练与评估,提升整体预测性能。
1. 从类别不平衡到 SMOTE 插值:一张混淆矩阵引发的数据补全需求
第一次面对混淆矩阵里几乎被吞掉的少数类时,第一反应通常是复制少数样本。复制不产生新信息,模型只是把同样的点学得更重;真正该做的是在特征空间中“补”出未被观测到的合理位置。SMOTE 干的正是这件事:对某个少数类样本,找出同类的 k 个近邻,再在样本与近邻的连线上按比例取点合成新样本。从数值分析角度看,这是标准线性插值;从机器学习角度看,这是目前最常用的过采样方法。下面用 MATLAB 实现一套可运行的 SMOTE 流程,把插值参数、数据补全边界和验证方式一次讲清。适合在做分类模型、处理不平衡数据集或补全稀疏样本的工程师阅读。
2. 插值视角下的 SMOTE 算法机制与参数选择
2.1 SMOTE 的插值本质和数学表达
SMOTE 全称是 Synthetic Minority Over-sampling Technique。设少数类样本集合为 X_min = {x_1, x_2, ..., x_m},其中每个 x_i 是一个 d 维特征向量。对每个 x_i,先计算它与集合内其他样本的欧氏距离,取最小的 k 个作为近邻;随后从 k 个近邻里等概率挑一个 x_zi,沿两点连线生成新样本:
x_new = x_i + λ(x_zi − x_i),λ ∈ [0, 1]
代码实现里 λ 用 rand 生成,服从均匀分布。这个式子和数值分析里线性插值的标准写法只差一个名字:线性插值是在已知函数值的两点之间估计中间位置的函数值,SMOTE 则是在两个已知样本坐标之间生成一个新的样本坐标。差别在于 SMOTE 生成的坐标没有“真实值”可以做误差检验,所以它不能被当作函数插值器,而是样本分布插值器。
还需要注意 λ 的边界。λ = 0 时新样本就是 x_i 本身,等价于复制少数类样本;λ = 1 时新样本等于近邻样本本身。SMOTE 的原版做法把 λ 限制在 [0, 1] 内,不做外推。一旦让 λ 大于 1,合成样本会跑到两点连线的延长线上,那个位置完全依赖两个样本张成的方向推断,分类器会把这种合成点当成强判决区域,直接放大过拟合。在数据补全场景里,外推点尤其危险,因为补全的目的是让缺失区域有合理覆盖,而不是制造新边界。所以参数设置的第一原则是:λ 保持均匀分布随手写合理,但永远不要让它越界。
2.2 三个必须调的核心参数
| 参数 | 设置位置 | 典型值 | 对插值结果的影响 |
|---|---|---|---|
| k(近邻数) | 算法入参 | 5 | 决定插值线段来自多大局部范围,k 太小方向过窄,k 太大跨过簇结构 |
| N(过采样倍率) | 算法入参 | 多数类数量 / 少数类数量 | 控制合成样本总量,决定少数类在训练集里的权重 |
| λ 分布 | 随机数生成处 | U[0,1] | 控制新样本在线段上的位置,影响合成样本与原样本的相似度 |
k = 5 是原版论文的默认值,也是绝大多数工程场景的首选。当少数类样本非常少,比如只有 15 条,k 最大值不能超过 14,否则近邻计算会拿到重复或无效索引。特征维度较高时,欧氏距离会出现“球面集中”现象,任何两点之间的距离都趋于相等,近邻失去区分度。这时候应该先生成距离矩阵看分布,发现区分度不行就先用 PCA 降维到 20 维左右再做 SMOTE,而不是硬调 k。
N 的取值要分场景讨论。多数类 800 条、少数类 40 条时,N = 19 可以把少数类补到 800 条,与多数类完全平衡。但一次补到位会让合成样本主导少数类区域,尤其是原始少数类本身就分布不均匀的时候。常见的做法是分轮补:第一轮按 N = 5 补,训练模型看少数类召回率,不足再补第二轮。对数据补全来说,目标是让模型对少数类有稳定识别能力,不是强制数学意义上的数量完全相等。
λ 的采样方式在 MATLAB 里常见两个变体:rand 均匀采样和 randn 带噪声偏移。后者生成的新样本会偏离线段本身,适合特征带噪的数据,但偏离程度不可控,交叉验证里方差会变大。稳健做法是平时用 rand,只在特征噪声明显时才引入高斯偏移。
2.3 和经典数值插值方法的对照
数值分析里最常听到的插值应用是地面站点中的风场数据插值到格点,这是典型空间插值:站点观测值已知,格点坐标已知,用线性插值或克里金估计格点处的风速。这种插值有明确的空间邻居关系和物理约束,误差可以用留出站点验证。
SMOTE 插值不是这种用途。它的输入没有“坐标-数值”二元结构,只有样本在特征空间里的位置;输出也不是某个物理量的估计值,而是一组新的样本点。下表给出四类插值器的差异:
| 插值器 | 已知量 | 输出 | 误差可校验性 |
|---|---|---|---|
| 线性插值 | 两点及函数值 | 两点间函数估计 | 有真实函数可对比 |
| 多项式插值 | n+1 个节点 | 高次多项式全覆盖 | 有龙格振荡风险 |
| 样条插值 | 节点与端部条件 | 分段多项式 | 逐段误差可控 |
| SMOTE 插值 | 少数类样本坐标 | 线段上的新样本坐标 | 只有模型指标间接验证 |
这个差异直接决定了后续的做法:对 SMOTE 生成的坐标,无法像数值分析那样计算绝对误差,更不能用某个经典插值公式去“验证”它。评估只能落在两条路上:合成样本与真实样本的距离分布是否一致,以及训练出来的模型在测试集上的指标是否稳定。这两个方向在第四章和第五章会分别展开。
3. 在 MATLAB 里从零实现 SMOTE 插值
3.1 构造一份可复现的不平衡数据
先用 randn 生成一份二维数据,多数类 200 条、少数类 40 条。为了模拟真实场景,少数类里混入一部分与多数类重叠的样本,这些边界样本最容易在插值后把新样本送到多数类区域。
% 固定随机种子,保证每次运行得到相同数据 rng(42); % 多数类:以 (3,3) 为中心的 200 条二维正态样本 X_major = randn(200, 2) + [3, 3]; % 少数类:以 (1,1) 为中心的 30 条二维正态样本 X_minor = randn(30, 2) + [1, 1]; % 再补 10 条靠近多数类区域的边界样本,模拟真实场景里的重叠 X_minor = [X_minor; X_major(1:10, :) + 1.2 * randn(10, 2)]; X = [X_major; X_minor]; y = [zeros(200, 1); ones(40, 1)]; % 看一眼类别分布 tabulate(y);代码里 rng(42) 是关键,数值分析性质的代码必须可复现;后续调参时如果每次数据都变,没法判断指标变化来自 SMOTE 还是来自随机波动。tabulate 需要统计工具箱,不想依赖时可以直接用 histcounts(y, [0, 1, 2])。少数类里那 10 条重叠样本很值得保留,后续验证时你会发现,SMOTE 的插值线一旦跨越这些点,合成样本就会大量落在多数类一侧。
3.2 SMOTE 主函数:距离矩阵、近邻搜索与插值生成
以下函数是完整的 SMOTE 实现,不算工具箱内置函数,直接用 pdist2 和 mink 组合完成。
function [X_aug, y_aug] = my_smote(X_minor, N, k) % my_smote: 对少数类样本做线性插值过采样 % 输入: % X_minor : m×d 矩阵,少数类特征 % N : 正整数,每条原样本生成 N 条新样本 % k : 正整数,近邻个数 % 输出: % X_aug : (m + m*N)×d 矩阵,原始少数类 + 合成样本 % y_aug : 全 1 标签 m = size(X_minor, 1); if m < k + 1 error('样本量不足:至少需要 %d 条少数类样本才能计算 k=%d 个近邻', k + 1, k); end % 1. 全距离矩阵 D = pdist2(X_minor, X_minor, 'euclidean'); % 2. 主对角线置为 inf,避免样本把自己当成邻居 D(1:m+1:m*m) = inf; % 3. 每行取 k 个最小距离,mink 返回升序排列的索引 [~, neighIdx] = mink(D, k, 2); % 4. 逐样本生成 N 条插值样本 X_new = zeros(m * N, size(X_minor, 2)); row = 0; for i = 1:m for j = 1:N n = randi(k); % 随机挑一个近邻 neighbor = neighIdx(i, n); lambda = rand(); % 插值比例 U[0,1] row = row + 1; X_new(row, :) = X_minor(i, :) + lambda * (X_minor(neighbor, :) - X_minor(i, :)); end end % 5. 组装结果 X_aug = [X_minor; X_new]; y_aug = ones(size(X_aug, 1), 1); end逻辑上最重要的三处:距离矩阵预计算、主对角线处理、λ 的位置。距离矩阵一次性算完,避免内层循环里反复调用 pdist2,数据量到几千条时性能差异非常明显。D(1:m+1:m*m) = inf 这行写成线性索引,等价于把每个样本到自己的距离设为无穷大,确保近邻列表里不混入自己。λ 直接乘在 (邻居 − 原样本) 方向上,等价于线段上的线性插值。
如果你的 MATLAB 没有统计工具箱,pdist2 和 mink 会报错。可以用两层 for 循环加 sort 替换:先算 D,再用 sort(D, 2) 得到索引,取每行第 2 到第 k+1 列。速度慢一些,但结果一致。生成新样本的循环里,randi(k) 可能多次选中同一个邻居,这是允许的,因为 λ 不同,生成的样本位置仍然不同;但如果少数类只有很少几条,同一个邻居被反复选中会让合成样本集中在那几个方向上,此时建议把 k 调小。
3.3 调用函数后先确认合成样本的位置
在二维数据上,画图是最直接的数值分析式验证。
% 调用 my_smote:原始 40 条少数类,每条生成 3 条新样本 [X_aug, ~] = my_smote(X_minor, 3, 5); X_syn = X_aug(41:end, :); % 前 40 行是原始少数类 figure; plot(X_major(:, 1), X_major(:, 2), 'k.', 'MarkerSize', 6); hold on; plot(X_minor(:, 1), X_minor(:, 2), 'bo', 'MarkerSize', 8); plot(X_syn(:, 1), X_syn(:, 2), 'r+', 'MarkerSize', 6); legend('多数类', '原始少数类', 'SMOTE 新样本');画完图之后重点看两类位置:红点是否大致落在蓝色圆点连成的狭窄区域内,以及有没有大量红点越过黑色点群。如果红点大规模混入多数类区域,说明少数类样本本身重叠度过高,需要减少边界样本的权重,或者改用过滤边界样本的策略。如果红点全部集中在少数几个方向,说明某个邻居被反复选中,这时把 k 调大能增加方向多样性。
4. 把 SMOTE 与数据补全接进不平衡分类建模流程
4.1 数据补全前先做缺失值填补和标准化
SMOTE 用的距离是欧氏距离,对特征尺度极其敏感。特征 A 取值范围 0 到 1,特征 B 取值范围 0 到 1000 时,近邻选择几乎完全由特征 B 主导,插值出来的新样本在特征 B 上的差异会被放大,在特征 A 上的差异几乎为零。所以 SMOTE 必须放在缺失值填补和标准化之后。
一个通用的数据补全流程是:读取数据 → fillmissing 填补缺失值 → zscore 标准化 → SMOTE → 训练模型 → 交叉验证。缺失值落在少数类样本上时尤其麻烦,因为欧氏距离无法处理 NaN,距离矩阵会出现整行 Inf。用中位数填补是最稳妥的做法,对边界样本的扰动最小。
% 替换每一列中的 NaN 为该列中位数 for j = 1:size(Xraw, 2) col = Xraw(:, j); idxNaN = isnan(col); if any(idxNaN) col(idxNaN) = median(col(~idxNaN), 'omitnan'); Xraw(:, j) = col; end end % 标准化:零均值单位方差 mu = mean(Xraw); sd = std(Xraw); sd(sd == 0) = 1; % 零方差特征直接置为 1,避免产生 NaN X_std = (Xraw - mu) ./ sd; % 提取少数类并设置倍率 minor_idx = find(y == 1); N = floor(sum(y == 0) / sum(y == 1)) - 1; if N < 1, N = 1; end [X_aug, y_aug] = my_smote(X_std(minor_idx, :), N, 5);填补缺失值时优先中位数而不是均值,因为少数类样本的分布往往有偏,均值会拉低边界样本的位置。标准化之后,SMOTE 生成的新样本落在标准化空间里,训练出的模型参数也是在标准化空间里的,预测新数据时要用同一组 mu 和 sd 做变换。这是最容易被忽略的步骤:预测阶段忘掉标准化的一致处理,等效于把插值结果直接扔进一个错误坐标系里。
4.2 用交叉验证评估 SMOTE 的真实增益
SMOTE 是数据补全过程,评估它是否有效,必须做交叉验证。这里有一个关键禁忌:SMOTE 绝对不能放在划分数据集之前执行。如果把全量数据做了 SMOTE 再划分训练集和测试集,测试集会被合成样本污染,模型是在包含测试集分布信息的数据上训练出来的,指标虚高且无法复现。
正确的做法是在每一折的训练子集内部执行 SMOTE,测试子集保持原始样本不动。
rng(7); cv = cvpartition(y, 'KFold', 10); auc_org = zeros(cv.NumTestSets, 1); auc_smt = zeros(cv.NumTestSets, 1); % 用 AdaBoostM2 做基分类器,弱学习器为深度 4 的决策树模板 mdlSpec = @(Xt, yt) fitcensemble(Xt, yt, 'Method', 'AdaBoostM2', ... 'Learners', templateTree('MaxNumSplits', 4)); for f = 1:cv.NumTestSets tr = training(cv, f); te = test(cv, f); % 对照组:原始训练子集 mdl1 = mdlSpec(X_std(tr, :), y(tr)); [~, s1] = predict(mdl1, X_std(te, :)); [~, ~, ~, auc_org(f)] = perfcurve(y(te), s1(:, 2), 1); % 实验组:对训练子集内的少数类做 SMOTE cnt1 = sum(tr & y == 1); cnt0 = sum(tr & y == 0); X_aug = X_std(tr, :); y_aug = y(tr); if cnt0 > cnt1 N = floor(cnt0 / cnt1) - 1; if N < 1, N = 1; end [X_b, ~] = my_smote(X_std(tr & y == 1, :), N, 5); X_syn = X_b(cnt1 + 1:end, :); % 只取合成样本,跳过原始少数类 y_syn = ones(size(X_syn, 1), 1); X_aug = [X_aug; X_syn]; y_aug = [y_aug; y_syn]; end mdl2 = mdlSpec(X_aug, y_aug); [~, s2] = predict(mdl2, X_std(te, :)); [~, ~, ~, auc_smt(f)] = perfcurve(y(te), s2(:, 2), 1); end fprintf('原始 AUC: %.4f ± %.4f\n', mean(auc_org), std(auc_org)); fprintf('SMOTE AUC: %.4f ± %.4f\n', mean(auc_smt), std(auc_smt));代码里的核心点在X_b(cnt1 + 1:end, :)这一行。my_smote 返回的前 cnt1 行是原始少数类样本,如果把这部分再追加进 X_aug,少数类会被重复一遍,指标变好不能说明 SMOTE 有效,只能说明重复数据对模型的加权有效。这里只追加合成部分,训练集保证每条真实样本只出现一次。
实际工程里 AUC 提升一般不会太大,尤其是边界样本本来就多的数据集。更应该关注的是少数类召回率。把 perfcurve 的输出改成 [~, ~, ~, tpr, fpr] 可以单独看召回率;如果 SMOTE 后召回率上升但精度明显下降,说明合成样本把多数类区域吞掉了,这时要做的是调小 N 或者过滤边界样本。
4.3 数据补全策略选择:什么时候用 SMOTE,什么时候改用类别权重
SMOTE 不是所有不平衡场景的最优解。补全数据的代价是额外引入合成样本,而这些样本没有真实观测支撑。下面这组选择标准来自实际项目经验:
| 场景 | 做法 | 理由 |
|---|---|---|
| 少数类只有 5 到 10 条 | 先聚类确认不是离群点,再做 SMOTE,k 取 3 | 样本过少时距离不稳定,小 k 降低跨簇插值风险 |
| 多数类 / 少数类比例超过 100 | 分轮 SMOTE 加成本敏感学习 | 一次拉到平衡会让合成样本主导决策面 |
| 缺失值集中在少数类特征列 | 先 fillmissing 再 SMOTE | SMOTE 的线路上存在 NaN 时距离矩阵直接失效 |
| 特征全部是类别型 | 改用 SMOTE-NC 或类别权重 | 连续距离不适合类别特征,直接插值会产生不存在的组合 |
类别权重方案比 SMOTE 省事,做法是给少数类样本更高的误分类惩罚,fitcsvm 可以传 'Cost' 参数,fitcensemble 可以调 'Prior'。SMOTE 的优势在于生成的新样本让模型的局部几何信息更丰富,对树模型和线性模型都有帮助;代价是合成样本可能泄漏到测试集区域,导致验证指标失真。用第四章开头提到的数据补全流程再配合交叉验证,是判断该不该用的唯一标准。
5. 验证 SMOTE 插值效果的三种边界测试
5.1 合成样本距真实样本的最近距离检验
对每个合成样本计算到最近真实少数类样本的欧氏距离,再计算真实少数类样本之间的最近邻距离,比较两个分布的 90 分位数。若合成样本的 90 分位数明显偏高,说明插值线段跨越了簇边界,合成样本进入了多数类地域。
D_real = pdist2(X_minor, X_minor); D_real(1:size(D_real,1)+1:end) = inf; real_nn = min(D_real, [], 2); D_syn = pdist2(X_syn, X_minor); syn_nn = min(D_syn, [], 2); disp(prctile(real_nn, [10 50 90])); disp(prctile(syn_nn, [10 50 90]));经验判断是:合成样本的 90 分位数超过真实样本的 1.5 倍时,优先调小 k,再看是否需要做边界采样。
5.2 过采样倍率 N 的敏感性测试
对 N 做网格扫描,N 分别取 1、2、3、5、8,在固定交叉验证折上记录 AUC、少数类召回率和精度。N 增大到某个临界点后,召回率可能不再上升,精度反而快速下跌,这个临界点就是这个数据集的合理补全上限。
5.3 边界样本过滤的快速实验
提前计算每个少数类样本的 k 近邻中多数类占比,占比超过一半的样本标记为边界样本,生成合成样本时跳过它们。这个过滤法不需要额外工具箱,直接在近邻索引上统计即可。
| 边界占比 | 0 到 0.3 | 0.3 到 0.5 | 大于 0.5 |
|---|---|---|---|
| 处理方式 | 正常插值 | 插值但不作为线段端点 | 跳过不做插值 |
最终判断不看单一指标,而是看少数类召回率与精度的平衡是否比原始数据更好;如果 SMOTE 后召回率提升的同时精度没有塌缩,说明插值确实补上了有效区域。
本文还有配套的精品资源,点击获取