MATLAB版SMOTE实战:解决不平衡数据分类的过采样方案
简介:这份资源是面向机器学习初学者与数据挖掘工程师的SMOTE算法MATLAB实现包,用于解决训练集中少数类样本过少导致模型偏向多数类、识别能力下降的不平衡数据问题。压缩包共5个文件,约3KB,以2个.m源码文件为核心,分别承载SMOTE算法主体实现与调用测试示例,另附LICENCE.txt授权说明及.gitignore、.gitattributes等版本管理配置,体量轻便、便于直接嵌入现有工程。已有1324人学习下载,说明其在中文社区具备一定参考价值。读者可从中获得可运行的合成少数类过采样代码,结合K近邻搜索与线性插值思路理解新样本生成过程,并借助测试脚本快速验证数据平衡前后的模型表现,为SVM、决策树、随机森林等分类任务提供预处理参考,同时可在此基础上调整K值与采样比例,探索与Tomek链接等方法的组合使用。
1. 少数类样本不够用?我用一份 MATLAB 版 SMOTE 把不平衡数据跑通了
手上有一批设备故障记录,正常样本几千条,故障样本只有几十条。直接丢进分类器训练,模型把所有样本都判成“正常”,准确率还挺好看,但真正要抓的故障一个没抓到。这种类别不平衡的场景,做工业检测、医疗辅助诊断、风控建模的开发者都绕不开。SMOTE 就是用来给少数类“造样本”的经典手段:在少数类样本之间做随机线性插值,合成新的少数类点,让分类器不再一边倒。这份 MATLAB 版本的 SMOTE 实现,适合两类人:一类是刚接触不平衡数据、想在 MATLAB 里快速跑通流程的新手;另一类是已有自己的分类脚本,想把过采样环节嵌进去做二次开发的熟手。下面我按“它怎么组织、怎么跑、怎么改”的顺序,把这份源码拆开讲。
2. 项目结构与算法骨架:SMOTE 在 MATLAB 里是怎么落地的
2.1 目录与文件组织
这份资源是一个围绕 SMOTE 算法组织的 MATLAB 工程,核心不是一个大而全的框架,而是“算法函数 + 演示脚本 + 数据”的轻量组合。常见做法是下面这种结构,我按实际拆包时最可能见到的形态说明:
smote_matlab/ ├── SMOTE.m % 核心过采样函数 ├── main_demo.m % 演示脚本:加载数据、调用SMOTE、训练分类器 ├── data/ │ └── imbalance_data.mat % 不平衡样本集(特征 + 标签) ├── utils/ │ ├── knn_search.m % 近邻查找(也可直接用内置knnsearch) │ └── plot_result.m % 可视化原始/合成样本分布 └── README.mdSMOTE.m是算法主体,main_demo.m是入口,data/放样本,utils/放辅助函数。这种拆法的好处是算法和演示解耦:你想把 SMOTE 用到自己的项目里,只需要拷SMOTE.m和它依赖的近邻函数,不用把整个演示脚本拖进去。新手先跑main_demo.m看效果,熟手直接读SMOTE.m改参数。
2.2 SMOTE 的核心逻辑
SMOTE 的思路不复杂,但几个参数决定了合成样本的质量。核心流程是:对每个少数类样本,找它的 k 个最近邻(同样是少数类),随机选一个邻居,在两点连线上随机取一个插值点。用公式说就是x_new = x_i + rand * (x_neighbor - x_i),rand是 0 到 1 之间的随机数。这样合成出来的点既靠近原始少数类分布,又不会简单复制原样本。
下面是我按这份工程风格重写的核心函数骨架,参数和注释都标清楚,方便你对照源码读:
function [X_new, y_new] = SMOTE(X, y, k, N) % SMOTE 对少数类做过采样 % 输入: % X - 特征矩阵, 每行一个样本, 每列一个特征 % y - 标签向量 (列向量) % k - 近邻个数, 常用 5 % N - 过采样倍率, 合成样本数 = 少数类样本数 * N % 输出: % X_new - 合成后的特征矩阵 % y_new - 合成后的标签向量 minority_idx = find(y == min(y)); % 定位少数类 X_min = X(minority_idx, :); n_min = size(X_min, 1); X_syn = zeros(n_min * N, size(X, 2)); % 预分配合成样本空间 cnt = 0; for i = 1:n_min % 在少数类内部找 k 近邻(排除自身) dist = pdist2(X_min(i,:), X_min, 'euclidean'); [~, idx] = sort(dist); neighbor_idx = idx(2:k+1); % 跳过第1个(自己) for n = 1:N nn = neighbor_idx(randi(k)); % 随机选一个近邻 gap = rand; % 插值系数 cnt = cnt + 1; X_syn(cnt,:) = X_min(i,:) + gap * (X_min(nn,:) - X_min(i,:)); end end X_new = [X; X_syn]; y_new = [y; repmat(min(y), n_min * N, 1)]; end逐段说下逻辑。minority_idx = find(y == min(y))用标签最小值定位少数类,这是演示代码里最常见的简化写法,前提是标签用 0/1 或 1/2 这种整数编码。pdist2算欧氏距离矩阵,sort后取第 2 到 k+1 个作为近邻,跳过自己。内层循环里randi(k)随机挑邻居,rand生成插值系数,两个随机性叠加保证每次合成的点都不一样。最后把合成样本拼回原矩阵,标签用repmat复制少数类标签。
参数上,k一般取 5,太小合成点容易贴着原样本、多样性不足,太大可能跨到类别边界外。N是倍率,少数类几十条、多数类几千条时,N取到让两类接近 1:1 或 1:2 就行,别盲目拉平,合成太多会引入噪声。这里有个血泪经验:N设太大,合成样本会把少数类的决策边界撑得很宽,分类器反而在真实测试集上翻车。
2.3 演示脚本怎么串起来
main_demo.m的职责是把“加载数据 → 过采样 → 训练 → 评估”串成一条线。典型写法如下:
% main_demo.m load('data/imbalance_data.mat'); % 载入 X 和 y fprintf('原始分布: 多数类 %d, 少数类 %d\n', ... sum(y==max(y)), sum(y==min(y))); [X_bal, y_bal] = SMOTE(X, y, 5, 10); % k=5, 倍率10 fprintf('过采样后: 多数类 %d, 少数类 %d\n', ... sum(y_bal==max(y_bal)), sum(y_bal==min(y_bal))); % 用简单的决策树做基线分类 mdl = fitctree(X_bal, y_bal); pred = predict(mdl, X); acc = mean(pred == y); fprintf('训练集准确率: %.2f%%\n', acc*100);这段脚本的价值在于给你一个可对照的基线。先看原始分布打印出来多悬殊,再看过采样后的分布,最后用fitctree快速验证。注意这里评估用的是原始X,不是合成后的X_bal,因为合成样本只该进训练集,不能污染评估。这一点新手很容易搞错,把合成样本也拿去算准确率,数字虚高,实际部署就露馅。
3. 跑起来:从零到看见合成样本分布
3.1 环境准备与数据格式
MATLAB 版本没有额外的工具箱硬依赖,pdist2在 Statistics and Machine Learning Toolbox 里,fitctree也在同一个工具箱。如果你装的是完整版 MATLAB,基本开箱即用;如果只装了基础版,pdist2可能报未定义,那就换成手写欧氏距离或者用knnsearch替代。数据格式上,imbalance_data.mat里通常是两个变量:X是n×d的数值矩阵,y是n×1的标签列。加载前先确认维度对得上,size(X,1)要等于length(y),否则后面索引会错位。
跑之前建议先做一次数据检查,把下面几行加在main_demo.m开头:
assert(size(X,1) == length(y), 'X 和 y 样本数不一致'); assert(all(isfinite(X(:))), 'X 中存在 NaN 或 Inf'); fprintf('特征维度: %d, 样本总数: %d\n', size(X,2), size(X,1));assert这两行是我每次接新数据必加的,NaN 和维度错位是最常见的翻车点,早报错比跑到一半崩强。
3.2 运行演示并观察结果
在 MATLAB 里把当前目录切到工程根目录,命令行输入main_demo回车。正常的话你会看到三行输出:原始分布、过采样后分布、训练集准确率。如果plot_result.m存在,还会弹出一张散点图,前两个特征做坐标轴,多数类一种颜色、少数类另一种、合成样本第三种。看这张图重点看两件事:合成点是不是落在少数类簇的范围内,有没有跑到多数类堆里。如果合成点大面积越界,说明k太大或者特征没做归一化。
特征量纲差异大时,欧氏距离会被大数值特征主导,近邻找得不准,合成点自然偏。常见做法是先标准化再喂给 SMOTE:
X_norm = normalize(X); % z-score 标准化 [X_bal, y_bal] = SMOTE(X_norm, y, 5, 10);normalize默认按列做 z-score,把每个特征拉到均值 0、方差 1。注意标准化参数要从训练集算,测试集用同一套均值和方差变换,别各算各的,否则评估会有偏差。
3.3 把 SMOTE 接进自己的分类流程
演示脚本只是样板,真正要用是把SMOTE.m嵌进你现有的训练脚本。关键原则只有一条:过采样只作用于训练集,验证集和测试集保持原始分布。常见做法是按比例先切分,再对训练部分过采样:
cv = cvpartition(y, 'HoldOut', 0.3); X_train = X(training(cv), :); y_train = y(training(cv)); X_test = X(test(cv), :); y_test = y(test(cv)); [X_train_bal, y_train_bal] = SMOTE(X_train, y_train, 5, 10); mdl = fitctree(X_train_bal, y_train_bal); pred = predict(mdl, X_test); recall_minority = sum(pred(y_test==min(y_test)) == min(y_test)) / sum(y_test==min(y_test)); fprintf('少数类召回率: %.2f%%\n', recall_minority*100);这里用cvpartition做留出切分,training和test取索引。评估指标换成少数类召回率,比整体准确率更能反映不平衡场景下的真实效果。整体准确率在不平衡数据上参考价值有限,这点我在多个项目里反复验证过。
4. 避坑与排查:SMOTE 用错比不用还糟
4.1 合成样本污染评估集
现象:过采样后准确率飙到 99%,换一批真实数据掉到 60%。原因:把合成样本混进了测试集,模型在“见过”的合成点上评估,数字虚高。解决:切分在前、过采样在后,测试集永远保持原始分布,评估只用原始样本。这条是最高频的坑,没有之一。
4.2 近邻跨类别边界
现象:合成点散落在多数类区域,分类边界变模糊。原因:k设得过大,或者少数类本身分布分散,近邻选到了远处的点。解决:k从 5 起步,观察散点图;少数类内部如果明显分簇,考虑按簇分别过采样,或者改用带边界清理的变体。别指望一个k通吃所有数据。
4.3 特征未归一化导致距离失真
现象:合成样本集中在某几个特征维度上,分布明显偏斜。原因:各特征量纲差异大,欧氏距离被大数值特征主导。解决:过采样前做 z-score 或 min-max 归一化,且归一化参数只在训练集上拟合。这条和 4.2 经常一起出现,排查时先看量纲再看k。
4.4 倍率设太大引入噪声
现象:少数类召回率上去了,但精确率暴跌,误报一堆。原因:N过大,合成样本数量超过真实少数类太多,噪声被放大。解决:把倍率控制在让两类比例接近 1:1 到 1:2 之间,配合交叉验证调。过采样不是越多越好,合成样本终究是插值出来的,不代表真实分布。
4.5 标签编码假设不成立
现象:min(y)定位少数类时选错了类。原因:标签不是 0/1 或 1/2 这种整数编码,比如用了字符串或多分类标签。解决:显式统计每类样本数,用tabulate(y)或groupcounts找真正的少数类,别默认min就是少数类。多分类场景下 SMOTE 要逐类处理,不能一把梭。
5. 二次开发:把 SMOTE 改成你能用的样子
拿到这份源码后,最值得动的几个地方我按优先级排一下。第一是近邻查找,pdist2在样本量大时是 O(n²) 的内存和计算开销,几千条以上就明显卡。换成knnsearch配合 KD 树能快不少,或者只对少数类内部建索引,因为 SMOTE 只在少数类里找邻居。第二是插值策略,原始 SMOTE 对每个少数类样本一视同仁,但靠近边界的样本合成风险高,可以给边界样本降权,或者只对“安全”样本过采样,这就是 Borderline-SMOTE 的思路。第三是倍率自适应,按当前类别比例动态算N,而不是写死。
改造时我一般会加一个验证闭环,改完参数立刻用交叉验证看少数类召回率和 F1 的变化,别凭感觉调。下面这个片段是我常用的快速对比框架:
k_list = [3, 5, 7]; N_list = [5, 10, 15]; for k = k_list for N = N_list [Xb, yb] = SMOTE(X_train, y_train, k, N); mdl = fitctree(Xb, yb); pred = predict(mdl, X_test); f1 = 2 * prec * rec / (prec + rec); % prec/rec 需先算 fprintf('k=%d N=%d F1=%.3f\n', k, N, f1); end end这个双层循环把k和N的组合扫一遍,输出对应的 F1,比单看准确率靠谱。参数网格不用太密,先粗扫再细调。改完记得把最优参数写回main_demo.m的调用处,别让演示脚本和实际流程脱节。
还有一个容易被忽略的点:合成样本的可解释性。工业场景里模型给出的判断往往要能追溯,合成点不是真实记录,解释时要说清楚哪些是原始样本、哪些是插值出来的。我一般会在输出里保留一个来源标记列,方便回溯。从那以后我每次改过采样逻辑,都强制先跑一遍“切分—过采样—评估”的完整闭环再提交,绝不跳过测试集验证这一步。希望帮到你。
本文还有配套的精品资源,点击获取