SMOTE算法深度解析及代码实现
SMOTE算法介绍
SMOTE算法是较为常用的数据增广算法,其核心思路是在少数类别样本内部进行数据合成,更具体的说,其后隐藏的猜想是假定样本 x 0 , x 1 , . . , x N x_0,x_1,..,x_N x0,x1,..,xN都为同一类别,那么他们的线性组合 x 0 + a 1 ∗ x 1 + . . . + a N ∗ x N x_0+a_1*x_1+...+a_N*x_N x0+a1∗x1+...+aN∗xN也应该和它们属于同一类别。 给出SMOTE原论文中的算法伪代码如下:
其基本思路和刚刚所介绍的一致,但是引入了基于KNN检索的数据合成方案。让我们来一步步拆解。
SMOTE函数
函数SMOTE(T,N,K)三个参数含义为:
N
:int(N/100)生成数据的倍数,为100的倍数。距离而言,如果设置N=200,那么少数类别样本的数目就会增加2倍,变为原来的3倍;
T
:少数类别样本的数目
K
:用于合成的最近邻样本的数目
接下来我们分析一下SMOTE的运行逻辑:
- 首先会判断要增广的倍数是否大于1(即N是否大于100),基于此计算出增广倍数N=N/100;
- 遍历每个少数类别样本 x i , i ∈ 1 , . . , T x_i,i\in {1,..,T} xi,i∈1,..,T,对其进行增广,增广步骤如下:
- 计算出 x i x_i xi在同类样本(即少数类别样本) 的k个最近邻样本,记录这k个样本在样本集中索引,构成
nnarray
- 调用
Populate()
函数进行增广。
- 计算出 x i x_i xi在同类样本(即少数类别样本) 的k个最近邻样本,记录这k个样本在样本集中索引,构成
Populate函数
函数Populate(N,i,nnarray)的三个参数的含义为:
N
: 增广倍数
i
: 要进行增广的样本的索引
nnarray
:样本 x i x_i xi的最近邻样本的索引集合
Popluate函数会按照我们之前提过的SMOTE核心思路–同类样本的线性组合来进行样本的生成。具体而言,总共会执行N次生成过程,每一次生成过程中,我们都会从其最近邻样本中挑选一个,然后对特征进行线性组合作为合成样本。值得注意的是,在合成新样本过程中,不同特征的线性系数都是不同的,特征向量中含numattrs
个特征就会包含numattrs
个合成系数gap。
小结
SMOTE函数总结起来就是对每个少数类别样本都寻找其同类最相似的K个样本,然后进行N次样本合成,每次合成过程中都会随机从K个样本中采样一个,然后用采样的近邻样本和当前样本的特征进行线性组合得到新样本,最终得到T*K个增广样本。
代码实现
实际应用SMOTE算法时,调用imbalanced-learn
中的库即可实现,如果想要手动实现的话,可以参见笔者给出的实现方案如下:
def smote(feature,labels,K):'''这份代码实现的是最为简单的smote算法,也就是核心是同类样本特征的线性组合,:param feature: 特征matrix:param labels: 标签向量:param K: SMOTE算法中样本考量的近邻个数:return: new_features-生成的样本特征;new_labels-生成样本的标签'''from sklearn.neighbors import NearestNeighborsfrom collections import Countervalue_cnts=dict(Counter(labels))max_sampels=max(value_cnts.values())new_features=[]new_labels=[]for label,cnt in value_cnts.items():if cnt<=1:continue# 提取当前label对应的featurecur_feats=feature[np.where(labels==label)]if(cur_feats.shape[0]>K):Neighbors = NearestNeighbors(n_neighbors=K)Neighbors.fit(cur_feats)neighbor_index=Neighbors.kneighbors()[1]else:Neighbors = NearestNeighbors(n_neighbors=cur_feats.shape[0]-1)Neighbors.fit(cur_feats)neighbor_index=Neighbors.kneighbors()[1]# 采样max_samples-cnt个样本来进行增广base_sampe_idxs=np.random.choice(np.arange(cur_feats.shape[0]),max_sampels-cnt)for idx in base_sampe_idxs:base_sample=cur_feats[idx]# 随机采样一个邻居random_neighbor_idx=np.random.choice(neighbor_index[idx])neighbor=cur_feats[random_neighbor_idx]coff=np.random.rand(feature.shape[1]) # n_dim个位于0-1间的特征系数dif=base_sample-neighbornew_features.append((base_sample+coff*dif).reshape(1,-1))new_labels.append(label)new_features=np.vstack(new_features)new_labels=np.array(new_labels)return new_features,new_labels
需要注意的是,笔者实现的方案和原始SMOTE算法并不完全一致。具体而言,这份实现中舍弃了对增广倍数的控制,改为了强制要求所有类别样本数目相同,逐类别的进行样本合成,每次合成过程中都随机选取一个当前类的样本,然后再基于SMOTE的思路在类别内采样、线性合成。
参考
探索SMOTE算法