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 NearestNeighbors
from collections import Counter
value_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对应的feature
cur_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-neighbor
new_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的思路在类别内采样、线性合成。