简介:这份资源是面向机器学习初学者与数据挖掘工程师的SMOTE算法MATLAB实现包,用于解决分类任务中少数类样本不足导致的模型偏置问题。压缩包共5个文件,以2个.m脚本为核心,分别承载SMOTE主算法与测试调用逻辑,另附LICENCE.txt、.gitignore、.gitattributes等辅助配置,整体仅3KB,轻量易读,适合直接嵌入现有MATLAB工程。代码围绕K近邻与线性插值合成少数类样本,覆盖数据加载、预处理、过采样、数据融合到模型训练评估的完整链路,并提示K值选择、过拟合控制及与Tomek链接等方法结合使用的注意事项。已有1324人学习下载,可作为不平衡数据实验的入门参考与二次开发起点。
1. SMOTE 算法:为什么“造样本”比“调模型”更关键
拿到一份类别极度不平衡的数据集时,多数人的第一反应是换模型、调超参、加正则。但真正让召回率翻车的,往往不是模型不够强,而是少数类样本太少,少到模型根本“没见过”足够的决策边界形态。SMOTE(Synthetic Minority Over-sampling Technique)解决的就是这个问题:它不复制少数类样本,而是在少数类样本之间做插值,合成出新的、不在原始数据中出现过的样本点。这件事听起来简单,但背后涉及一个核心判断——少数类的局部线性假设是否成立。如果成立,插值出来的样本就落在真实的少数类流形附近;如果不成立,合成样本就是噪声。matlab 版本的 SMOTE 实现,恰好把这个问题暴露得最清楚,因为 matlab 的矩阵运算风格让“近邻选择”和“插值生成”这两步的底层逻辑变得可读、可改、可验证。搞懂它,你就不只是会调一个函数,而是能判断什么时候该用、什么时候用了反而掉点。
2. SMOTE 的底层机制:从 k 近邻到合成样本的完整链路
2.1 为什么不能简单复制少数类样本
最朴素的过采样是随机复制少数类样本,让正负比例接近 1:1。这种做法在逻辑回归这类对类别先验敏感的模型上确实能提升召回,但它有一个致命缺陷:复制不产生新信息。模型看到的少数类样本点还是那几个,决策边界只会向多数类方向平移,而不会变得更精细。更糟的是,如果少数类内部本身有子簇结构,复制会让模型过度拟合到那几个被反复复制的点上,导致在真实少数类分布的其他区域表现更差。
SMOTE 的设计动机就在这里:它假设少数类样本在特征空间中不是孤立的点,而是分布在某个连续流形上。既然流形连续,那么两个少数类样本之间的连线上的点,大概率也属于少数类。于是它用 k 近邻找到每个少数类样本的邻居,在样本与邻居的连线上随机取点,生成新样本。这个假设在多数实际场景中近似成立,比如信用欺诈检测中欺诈行为的特征组合、医学图像中病灶区域的纹理特征,都具有一定的局部连续性。
2.2 k 近邻选择:谁有资格当“插值参考”
SMOTE 的第一步是为每个少数类样本找到它的 k 个最近邻。这里的“最近”通常用欧氏距离度量,但在 matlab 实现中,距离计算直接决定了后续插值的质量。常见的实现思路是:先提取所有少数类样本,构成一个矩阵X_minority,然后计算每个样本与其他所有少数类样本的成对距离。matlab 的pdist2函数可以一次性算出距离矩阵,但要注意内存开销——如果少数类样本数超过几千,距离矩阵会迅速膨胀。
% 假设 X_minority 是少数类样本矩阵,每行一个样本 % k 是近邻数,通常取 5 k = 5; num_minority = size(X_minority, 1); % 计算成对欧氏距离 dist_matrix = pdist2(X_minority, X_minority); % 对每一行,排除自身(距离为0),取前 k 个最小距离的索引 [~, sorted_idx] = sort(dist_matrix, 2); neighbor_idx = sorted_idx(:, 2:k+1); % 跳过第1列(自身)这段代码的关键在于sorted_idx(:, 2:k+1)。排序后第一列是样本自身,距离为 0,必须跳过。如果忘了这一步,插值时会用样本和自己做插值,生成的就是原样本本身,等于没做 SMOTE。另一个容易翻车的地方是距离度量:如果特征量纲差异大,欧氏距离会被大量纲特征主导,导致近邻选择完全偏离。常见的做法是在 SMOTE 之前做标准化,但 matlab 的 SMOTE 实现通常不内置这一步,需要手动处理。
2.3 插值生成:随机数背后的边界控制
找到近邻后,SMOTE 在样本与每个近邻的连线上随机取一个点。具体公式是:
x_new = x_i + rand(0,1) * (x_neighbor - x_i)其中rand(0,1)是 0 到 1 之间的随机数。这个随机数决定了新样本在连线上的位置:接近 0 时偏向原样本,接近 1 时偏向邻居。matlab 实现中,这一步通常用循环或向量化完成。
% 假设 neighbor_idx 是 num_minority x k 的矩阵 % 为每个少数类样本生成 1 个合成样本(可根据需要调整生成数量) num_synthetic = num_minority; % 生成数量等于少数类样本数 synthetic_samples = zeros(num_synthetic, size(X_minority, 2)); for i = 1:num_synthetic % 随机选一个少数类样本 idx = randi(num_minority); % 随机选它的一个近邻 nn = neighbor_idx(idx, randi(k)); % 随机插值系数 lambda = rand(); % 生成合成样本 synthetic_samples(i, :) = X_minority(idx, :) + lambda * (X_minority(nn, :) - X_minority(idx, :)); end这里有几个设计取舍值得注意。第一,randi(k)表示每个近邻被选中的概率相同,但实际中距离更近的邻居可能更应该被选中,有些变体(如 Borderline-SMOTE)会调整这个概率。第二,lambda = rand()是均匀分布,意味着合成样本在连线上均匀分布,但有些实现会用高斯分布让样本更集中在原样本附近。第三,生成数量num_synthetic通常设为少数类样本数,使正负比例接近 1:1,但具体设多少取决于你的目标——如果只是想让模型“看到”更多少数类形态,生成 50% 就够了;如果少数类极度稀缺,可能需要生成 200% 甚至更多。
2.4 合成样本的标签与后续处理
合成样本的标签直接继承少数类标签,这一点没有歧义。但合成之后的数据集需要重新划分训练集和验证集,而且划分必须在 SMOTE 之前做。如果先 SMOTE 再划分,合成样本可能同时出现在训练集和验证集中,导致验证集上的性能虚高。这是实际项目中最常见的翻车点之一:验证集里混入了训练集样本的“插值亲戚”,模型在验证集上看起来很好,上线后直接崩盘。
正确的流程是:先划分训练集和验证集,只在训练集上做 SMOTE,验证集保持原始分布。这样验证集才能真实反映模型在未见过数据上的表现。matlab 实现中,这一步通常需要手动控制,因为 SMOTE 函数本身只负责生成样本,不负责数据划分。
3. matlab 实现中的关键细节:向量化、内存与随机种子
3.1 向量化加速:从双重循环到矩阵运算
上面的插值代码用了 for 循环,在 matlab 中这是性能瓶颈。当少数类样本数达到几万时,循环会慢到无法接受。向量化的思路是:一次性为所有样本生成随机近邻索引和随机插值系数,然后用矩阵运算完成插值。
% 向量化版本 num_synthetic = num_minority; % 为每个合成样本随机选一个原样本 base_idx = randi(num_minority, num_synthetic, 1); % 为每个合成样本随机选一个近邻(从 base_idx 对应的近邻里选) nn_offset = randi(k, num_synthetic, 1); % 获取近邻索引:需要把 base_idx 和 nn_offset 映射到 neighbor_idx 矩阵 nn_idx = neighbor_idx(sub2ind(size(neighbor_idx), base_idx, nn_offset)); % 随机插值系数 lambda = rand(num_synthetic, 1); % 矩阵运算生成合成样本 synthetic_samples = X_minority(base_idx, :) + lambda .* (X_minority(nn_idx, :) - X_minority(base_idx, :));这段代码的核心是sub2ind,它把二维索引(base_idx, nn_offset)转换成线性索引,从而一次性取出所有近邻。lambda .* (...)利用了 matlab 的广播机制,每一行乘以对应的插值系数。向量化后,生成十万级合成样本通常在一秒内完成,而循环版本可能需要几十秒。
但向量化也有代价:内存占用。X_minority(base_idx, :)会复制一份原样本矩阵,如果少数类样本本身很大,内存可能吃紧。常见的折中方案是分块处理,比如每次生成 10000 个合成样本,循环几次完成。
3.2 随机种子:可复现性的双刃剑
matlab 的随机数生成器默认以当前时间为种子,这意味着每次运行 SMOTE 生成的合成样本都不同。对于调试和对比实验,这是灾难——你无法判断性能变化是来自模型改进还是随机样本的差异。解决办法是在 SMOTE 之前固定随机种子:
rng(42); % 固定种子,保证每次生成相同的合成样本但固定种子也有风险:如果种子选得不好,生成的合成样本可能恰好集中在某些区域,导致模型在验证集上表现异常。更稳妥的做法是跑多次不同种子的实验,取平均性能。我一般会跑 5 次,种子分别设为 1、2、3、4、5,然后看均值和方差。如果方差很大,说明 SMOTE 对随机性太敏感,可能需要调整 k 值或生成数量。
3.3 k 值的选择:5 是万能解吗
k 近邻数 k 是 SMOTE 最重要的超参数。k 太小,插值范围窄,合成样本多样性不足;k 太大,可能选到距离很远的邻居,插值出来的样本落在多数类区域,变成噪声。常见的默认值是 5,但这并不是万能解。
如果少数类样本本身很少(比如只有几十个),k=5 可能已经覆盖了大部分少数类样本,插值范围过大。这时应该减小 k,比如 k=3 甚至 k=2。如果少数类样本较多且分布均匀,k=5 到 k=10 都可以。判断方法是:观察合成样本与多数类样本的重叠程度。如果合成样本大量落在多数类区域,说明 k 太大或插值系数范围太宽。
matlab 实现中,k 值通常作为参数传入,但很多开源实现没有做自适应 k 值。一个实用的技巧是:先用 k=5 跑一遍,然后用简单的分类器(如 kNN)检查合成样本的类别纯度——如果合成样本的 k 近邻里多数类占主导,说明这些合成样本质量差,需要调整。
4. 避坑指南:SMOTE 在实际项目中的五个翻车现场
4.1 现象:验证集性能飙升,上线后暴跌
原因:先做了 SMOTE 再划分训练集和验证集,导致验证集中混入了合成样本。合成样本是从训练集样本插值出来的,与训练集样本高度相关,模型在验证集上相当于“见过”这些样本的亲戚。
解决:严格先划分数据集,只在训练集上做 SMOTE。验证集和测试集必须保持原始分布。如果数据量允许,最好用独立的测试集做最终评估。
4.2 现象:合成样本全是噪声,模型性能反而下降
原因:少数类样本本身分布不连续,或者特征空间中存在大量离群点。SMOTE 在离群点和正常点之间插值,生成的样本落在多数类区域,标签却是少数类,直接污染训练集。
解决:先做离群点检测,剔除少数类中的极端离群点,再做 SMOTE。或者改用 Borderline-SMOTE,它只对边界附近的少数类样本做插值,避免在离群点周围生成样本。
4.3 现象:高维数据上 SMOTE 完全失效
原因:在高维空间中,欧氏距离失去区分度,所有样本对的距离都趋于相近,k 近邻选择变得随机。这是维度灾难的典型表现。
解决:先做降维(如 PCA 或 t-SNE),在低维空间做 SMOTE,再映射回原空间。或者改用基于特征子空间的 SMOTE 变体,在随机选择的特征子集上计算距离。
4.4 现象:类别不平衡比例没变,模型还是偏向多数类
原因:SMOTE 生成样本后,没有调整模型的类别权重或决策阈值。很多模型(如 SVM、逻辑回归)默认假设类别均衡,即使训练集平衡了,模型的决策边界仍然偏向多数类。
解决:SMOTE 之后,配合使用类别权重调整或阈值移动。比如在逻辑回归中设置class_weight='balanced',或者在预测时调整概率阈值,让少数类的召回率提升。
4.5 现象:生成样本导致过拟合,训练集准确率 99%,测试集 60%
原因:生成数量过多,合成样本远多于原始少数类样本,模型过度拟合到合成样本的局部模式。或者 k 值太小,合成样本多样性不足,模型看到的少数类形态仍然单一。
解决:控制生成数量,通常生成到正负比例 1:1 或 1:2 即可,不要盲目生成到 1:10。同时增大 k 值或使用 SMOTE 的变体(如 ADASYN),让合成样本更分散。
5. 设计取舍:SMOTE 的边界与替代思路
SMOTE 的核心假设是“少数类样本在局部线性可插值”,这个假设在多数场景下近似成立,但它不是万能药。当少数类样本极度稀缺(比如只有个位数)、或者特征空间高度非线性时,SMOTE 的合成样本可能完全偏离真实分布。这时候,更好的选择可能是代价敏感学习(直接调整损失函数)、集成方法(如 EasyEnsemble)、或者数据增强(针对图像、文本等非结构化数据)。
matlab 版本的 SMOTE 实现,最大的价值在于它的可读性和可修改性。你可以清楚地看到每一步在做什么,然后根据具体问题调整。比如,如果你发现合成样本总是落在多数类区域,可以修改插值系数的分布,让它更集中在原样本附近;如果你发现 k 近邻选择被某个特征主导,可以在距离计算前做特征加权。
我自己的习惯是:每次用 SMOTE 之前,先画一张少数类样本的二维投影图(用 PCA 或 t-SNE),肉眼判断少数类是否连续。如果图上少数类明显分成几个孤立的簇,SMOTE 大概率会翻车,这时候我会先做聚类,然后在每个簇内分别做 SMOTE。这个习惯帮我省了很多次“调参调到怀疑人生”的时间。
从那以后,我每次做不平衡数据项目,都强制走一遍“先划分、再可视化、后 SMOTE”的流程,再也没出现过验证集虚高的情况。希望帮到你。
本文还有配套的精品资源,点击获取