简介:这份资源是面向机器学习初学者与数据挖掘工程师的SMOTE算法MATLAB实现包,用于解决分类任务中少数类样本不足导致的模型偏置问题。压缩包共5个文件,以2个.m脚本为核心,分别承载SMOTE主算法与测试调用逻辑,另附LICENCE.txt、.gitignore、.gitattributes等辅助配置,整体仅3KB,轻量易读,便于直接嵌入现有工程。代码围绕K近邻与线性插值合成少数类样本展开,涵盖数据加载、预处理、过采样、数据融合到模型训练评估的完整链路,并提示K值选择、过采样与欠采样结合、防止过拟合等注意事项。已有1324人学习下载,适合希望快速理解SMOTE原理、在MATLAB环境中动手复现并调参的读者参考。
1. 当类别样本只有几十条时,我靠这份 MATLAB 版 SMOTE 把模型从“全猜多数类”拉了回来
前阵子帮某实验室处理一批工业传感器故障数据,正负样本比例接近 1:40,模型训练完混淆矩阵里少数类召回率直接是 0——它学聪明了,全猜多数类也能拿 97% 的准确率。这种场景下,过采样几乎是绕不开的一步,而 SMOTE(合成少数类过采样技术)又是最常被拿来当 baseline 的算法。这份 MATLAB 版本的 SMOTE 实现,就是我在那次踩坑之后翻出来重新拆解的一份代码,它不依赖任何工具箱,纯矩阵运算,适合需要在 MATLAB 环境里快速做数据平衡、又不想装一堆 Python 依赖的开发者。如果你手头正好有类别极不平衡的分类任务,或者想搞懂 SMOTE 到底在特征空间里做了什么,这份代码值得跟着跑一遍。
2. 算法骨架拆解:从 K 近邻到合成样本的完整链路
2.1 核心思路:为什么不是简单复制少数类
很多人第一次接触过采样,直觉就是“把少数类样本复制几份”。但随机复制有个致命问题:它不增加任何新的特征空间信息,只是让决策边界更偏向少数类,模型容易过拟合到那几个重复点。SMOTE 的做法是在两个少数类近邻之间做线性插值,生成“看起来合理”的新样本。
具体流程分三步:先对每个少数类样本,在同类样本里找 K 个最近邻;然后随机挑一个近邻,在两者连线上随机取一点作为新样本;重复这个过程直到少数类数量达到目标比例。这份 MATLAB 实现把这三步拆得很清楚,没有用任何黑箱函数,全部是显式的距离计算和矩阵索引。
2.2 距离矩阵计算:MATLAB 向量化的关键几行
整个算法最耗时的部分就是找 K 近邻。如果写双重循环逐个算欧氏距离,样本量一上来就卡死。这份代码用了矩阵化写法,核心逻辑如下:
function D = pairwise_dist(X) % X: n x d 矩阵,n 个样本,d 维特征 % 利用 (a-b)^2 = a^2 + b^2 - 2ab 展开,避免循环 n = size(X, 1); sq = sum(X.^2, 2); % 每行的平方和,n x 1 D2 = sq + sq' - 2 * (X * X'); % 平方欧氏距离矩阵 D2(D2 < 0) = 0; % 数值误差可能导致微小负值 D = sqrt(D2); % 开方得到真实距离 D(1:n+1:end) = inf; % 对角线置 inf,排除自身 end这段代码有几个细节值得说。sq + sq'是广播加法,得到 n×n 矩阵;X * X'是 Gram 矩阵,一次矩阵乘法搞定所有内积。对角线置inf是为了后面排序时把自己排除掉,不然每个样本的最近邻永远是它自己。数值误差那行也不能省,浮点运算下D2可能出现 -1e-12 这种值,开方直接变复数,后面全乱。
参数上,X的每一行是一个样本,列是特征维度。如果你的数据是特征在行、样本在列,记得先转置。这个函数返回的是完整距离矩阵,内存占用是 O(n²),样本量超过两万就要考虑分块或者换 KD 树了。
2.3 合成样本生成:随机插值的边界控制
拿到距离矩阵后,对每个少数类样本找 K 个近邻,然后随机选一个做插值:
function X_new = generate_samples(X_min, k, N) % X_min: 少数类样本,m x d % k: 近邻数 % N: 需要生成的样本总数 m = size(X_min, 1); D = pairwise_dist(X_min); [~, idx] = sort(D, 2); % 每行按距离升序排列 knn = idx(:, 2:k+1); % 取前 k 个近邻(跳过自身) X_new = zeros(N, size(X_min, 2)); for i = 1:N base = randi(m); % 随机选一个少数类样本作为基准 nb = knn(base, randi(k)); % 从它的 k 个近邻里随机挑一个 gap = rand(); % 0 到 1 之间的随机插值系数 X_new(i, :) = X_min(base, :) + gap * (X_min(nb, :) - X_min(base, :)); end endsort(D, 2)对每一行独立排序,idx(:, 2:k+1)跳过第一列(自身,距离为 inf 排最后?这里要注意:因为对角线置了 inf,排序后 inf 会在最后,所以前 k 个就是真正的近邻)。插值系数gap用rand()生成,范围是 [0,1),新样本落在两个少数类样本的连线上。如果gap取 0 或 1,新样本就和原样本重合,所以有些改进版会用rand()加一个小偏移避免边界值,这份代码没做这个处理,后面改造章节我会提怎么加。
N是你需要生成的数量,通常由目标比例决定。比如少数类原有 50 条,多数类 2000 条,你想让少数类达到多数类的 50%,就需要生成 950 条。
3. 跑通这份代码:数据准备、参数设置与结果验证
3.1 数据格式与目录组织
这份代码对输入数据的要求很宽松,核心就是一个特征矩阵和一个标签向量。我一般会这样组织工作目录:
smote_project/ ├── main.m % 入口脚本 ├── smote.m % 核心函数 ├── pairwise_dist.m % 距离计算 ├── data/ │ └── dataset.mat % 你的数据 └── output/ └── balanced.mat % 平衡后的数据数据文件里至少要有两个变量:X是 n×d 的特征矩阵,Y是 n×1 的标签向量(假设少数类标签为 1,多数类为 0)。如果你的原始数据是 CSV,用readmatrix读进来后手动拆一下就行。
3.2 主脚本调用示例
入口脚本的逻辑很直白:读数据、分少数类多数类、调 SMOTE、合并、保存。
% main.m load('data/dataset.mat'); % 载入 X 和 Y minority_idx = find(Y == 1); % 少数类索引 majority_idx = find(Y == 0); % 多数类索引 X_min = X(minority_idx, :); X_maj = X(majority_idx, :); k = 5; % 近邻数,常用 5 或 3 ratio = 0.5; % 目标:少数类达到多数类的 50% N = round(ratio * size(X_maj, 1)) - size(X_min, 1); X_syn = generate_samples(X_min, k, N); X_balanced = [X_maj; X_min; X_syn]; Y_balanced = [zeros(size(X_maj,1),1); ones(size(X_min,1),1); ones(N,1)]; save('output/balanced.mat', 'X_balanced', 'Y_balanced');k的选择有个经验范围:太小(比如 1)生成的样本多样性不够,太大(比如 20)可能跨越类别边界引入噪声。我一般从 5 开始试,看后续分类器的召回率和精确率平衡点。ratio不是越高越好,把少数类硬拉到和多数类 1:1 有时反而伤精确率,0.3 到 0.5 之间往往更稳。
3.3 怎么验证生成结果靠不靠谱
跑完之后别急着训模型,先做两件事。第一,画一下生成前后少数类在二维 PCA 投影下的分布,看看新样本是不是落在原有少数类簇的合理范围内,有没有跑到多数类堆里去。第二,用生成前后的数据分别训一个简单的分类器(比如 MATLAB 自带的fitcsvm或fitctree),对比少数类召回率的变化。
% 快速验证:用决策树对比 tree_before = fitctree(X, Y); tree_after = fitctree(X_balanced, Y_balanced); % 在留出测试集上算混淆矩阵 pred_before = predict(tree_before, X_test); pred_after = predict(tree_after, X_test); % 重点看少数类的 recall 和 precision如果召回率上去了但精确率崩了,说明k或ratio设得太激进,生成的样本侵入了多数类区域。这时候把k降到 3,或者把ratio降到 0.3,重新跑一遍。
4. 避坑与排查:SMOTE 在 MATLAB 里最容易翻车的几个点
4.1 距离矩阵内存爆炸
现象:样本量到一万以上时,pairwise_dist直接报Out of memory。原因很简单,n×n 的 double 矩阵,一万条就是 800MB,两万条直接 3.2GB。解决办法有两个:一是对多数类先做随机下采样,把总样本量压到五千以内再跑 SMOTE;二是改成分块计算,每次只算一批样本的距离,用pdist2配合循环。我一般选第一种,因为多数类下采样对最终效果的影响通常比少数类过采样小。
4.2 近邻数 k 大于少数类样本数
现象:少数类只有 3 条样本,你设k=5,代码在knn = idx(:, 2:k+1)这里直接索引越界。原因就是近邻数不能超过同类样本总数减一。解决:在调用前加一行k = min(k, size(X_min,1)-1),保证不会越界。如果少数类少到只有一两条,SMOTE 本身就不适用了,得换 ADASYN 或者直接收集更多数据。
4.3 特征量纲不统一导致距离失真
现象:生成的新样本全挤在某个特征维度附近,分类器效果没提升。原因是欧氏距离对量纲敏感,如果一个特征范围是 0-1,另一个是 0-10000,距离几乎完全由大范围特征主导。解决:在跑 SMOTE 之前对所有特征做标准化(z-score 或 min-max),生成完再反标准化回去。这一步很多人会忘,但影响很大。
4.4 类别标签不是 0/1 导致索引错乱
现象:find(Y == 1)返回空值,后面全报错。原因是你的标签可能是 1/2、'yes'/'no' 或者字符串。解决:先看一眼unique(Y),把少数类标签映射成 1,多数类映射成 0,再往下走。这个坑在新手手里出现频率极高,因为大家拿到数据就急着跑算法,忽略了标签编码。
4.5 生成样本与原始样本完全重合
现象:检查生成结果时发现有些新样本和原样本一模一样。原因是插值系数rand()理论上可以取到 0,虽然概率极低,但在某些随机种子下确实会出现。解决:把gap = rand()改成gap = 0.1 + 0.8 * rand(),强制插值系数落在 [0.1, 0.9] 之间,保证新样本和两个父样本都有区分度。
5. 二次开发改造:从能跑到好用,我习惯加的三个扩展
这份代码本身是个干净的 baseline,但直接拿去处理真实项目,我一般会做三处改造。第一处是加自适应近邻数:不再对所有少数类样本用同一个k,而是根据每个样本周围多数类的密度动态调整——密度高的区域用更小的k,避免生成样本侵入多数类。实现上就是在generate_samples里对每个基准样本算一下它到最近多数类样本的距离,如果这个距离小于到第 k 个少数类近邻的距离,就把k降到 3 或者 2。
第二处是加边界样本过滤。SMOTE 有个已知问题:如果少数类样本本身就落在多数类区域里(噪声点),以它为基准生成的样本也是噪声。我习惯在生成前先跑一遍 Tomek links 或者简单的近邻清洗,把那些近邻全是多数类的少数类样本剔掉,再拿剩下的干净样本去生成。代码改动不大,在main.m里加一个清洗步骤就行。
第三处是加生成质量评估。每次跑完 SMOTE,自动算一下生成样本到最近多数类样本的平均距离,以及生成样本之间的平均距离。如果前者太小,说明生成样本太靠近边界;如果后者太小,说明生成样本多样性不够。这两个指标可以做成一个简单的报告输出,帮你快速判断参数是否合理。
% 生成质量快速评估 dist_to_maj = min(pdist2(X_syn, X_maj), [], 2); dist_within = pdist(X_syn); fprintf('生成样本到多数类平均距离: %.4f\n', mean(dist_to_maj)); fprintf('生成样本内部平均距离: %.4f\n', mean(dist_within));从那以后我每次跑 SMOTE 都强制走一遍“标准化 → 清洗 → 生成 → 质量评估”这四步,再也没出现过生成完效果反而变差的情况。希望这份拆解能帮到你,代码跑起来之后,先从k=5、ratio=0.3这组保守参数开始试,稳了再往上调。
本文还有配套的精品资源,点击获取