类别不平衡是机器学习中经常遇到的问题,有时候类别不平衡会直接影响到模型的训练结果。这里介绍几种常见的缓解类别不平衡问题的方法。假设样本数较少的类为正类,反之为负类。
比如逻辑回归可以写成如下形式,若 y 1 − y > m + m − \frac{y}{1-y}>\frac{m^{+}}{m^{-}} 1−yy>m−m+则预测为正例,实际上是对类别进行一个“再缩放”(传统的逻辑回归是假设 m + m − = 1 \frac{m^{+}}{m^{-}}=1 m−m+=1) y ′ 1 − y ′ = y 1 − y × m − m + \frac{y^{\prime}}{1-y^{\prime}}=\frac{y}{1-y} \times \frac{m^{-}}{m^{+}} 1−y′y′=1−yy×m+m−或者通俗来说,传统的逻辑回归中,如果输出大于0.5则判定为正类,反之为负类;如果类别不平衡,正类样本过少,我们可以把这个阈值调大一点,比如说只有输出大于0.8的时候才输出为正类,反之输出为负类。
可以看到调整前和调整后的输出是差不多的,这是因为我这里的数据并没有出现样本不平衡的情况。
再缩放的思想虽然简单,但是实际应用中却不一定很理想,这主要是因为真实样本的分布可能跟训练样本分布不一致,这样我们根据训练样本选定的阈值可能就不适合于真实样本。其实解决样本不平衡问题的最直观也是最常见的方法就是改变某一类的样本数量,使其变成平衡样本。改变样本数量使得样本变得平衡有两个思路:第一个是减少多数(负类)样本,这种叫欠采样;另一种是增加少数(正类)样本,这种叫过采样.
欠采样的思想是减少多数(负类)样本,使得正类和负类的样本数平衡。欠采样常见的具体实现方法有三种: (1)随机欠采样:随机选择部分多数样本参与模型训练 (2)先对多数样本进行聚类(Kmeans),然后分别从每个聚类簇中选择部分样本参与模型训练,类似于进行分层采样,它的主要思想是选出有代表性的多数样本参与模型训练。 (3)将负样本划分为k分,然后分别训练k个模型,再将这k个模型进行集成(EasyEnsemble) 随机欠采样由于随机选择了部分样本,所以模型可能会丢掉一些重要信息;先聚类再采样的方法虽然尽可能的选择了有代表性的样本,但是在模型训练的过程中还是会损失部分信息;EasyEnsemble利用集成学习机制,将反例划分为若干个集合供不同学习器使用,这样对每个学习器来看都进行了欠采样,但全局来看不会丢失重要信息,这个方法的缺点是会增加计算的复杂度。
过采样的思想是增加少数(正类)样本,使得正类和负类的样本数平衡。欠采样常见的具体实现方法主要有两种: (1)随机过采样:随机有放回的从少数样本中抽取样本,重复执行这一操作,直至正负样本数量平衡,类似与bootstrap抽样 (2)SMOTE过采样:对正类样本进行线性组合(插值),得到新的正样本。步骤可大致分为:首先随机选择一个正类样本 i i i,然后找出 i i i的k的邻居(假如k=3,找邻居可以用knn算法),其次从这k个邻居中随机选择一个 j j j ,最后将样本 i i i和 j j j进行线性组合就得到了新样本 z = λ i + ( 1 − λ ) j z=\lambda i + (1-\lambda)j z=λi+(1−λ)j,其中 λ \lambda λ的取值范围是(0,1) 随机过采样由于引入了很多重复的样本,所以比较容易过拟合;SMOTE是根据正样本之间的线性组合生成新样本,所以可能会扩大噪声对模型的影响。
所有X和y都跟上文相同(1是多数(负类)样本,0是少数(正类)样本) (1)随机欠采样
index = np.where(y==1)[0] # 找出全部负类样本的索引 index_sample = np.random.choice(index,size=len(np.where(y==0)[0]), replace=False) # 从负类样本中随机抽取一部分样本,抽取规模为正类的样本数量,replace=False表示无放回 X_sample = X.iloc[index_sample.tolist()+np.where(y==0)[0].tolist() , :] # 最终的X,不要忘了加上正样本的索引 y_sample = y[index_sample.tolist()+np.where(y==0)[0].tolist()] # 最终的y(2)聚类欠采样(分层采样)
from sklearn.cluster import KMeans index = np.where(y==1)[0] # 找出全部负类(多数)样本的索引 y_cluster = y[index] X_cluster = X.iloc[index,:] model = KMeans(n_clusters=3).fit(X_cluster) print(model.labels_) [2 1 0 1 0 1 2 1 2 1 1 1 0 0 0 0 0 1 0 1 0 1 1 1 1 1 1 1 2 2 2 2 1 0 1 0 1 0 0 1 1 1 0 1 2 0 0 1 0 1 2 1 2 2 1 2 1 1 0 0 1 1 0 1 2 2 2 1 0 0 0 2 1 2 1 1 1 1 2 0 2 1 0 0 0 0 1 1 1 0 1 1 1 1 0 1 1 1 0 1 2 1 1 0 2 2 0 2 2 0 2 1 1 1 0 2 2 2 1 1 2 0 1 1 0 1 1 0 2 1 0 2 1 0 1 1 2 2 1 1 1 1 1 0 1 2 2 1 1 1 2 0 2 0 1 0 1 1 1 0 2 2 1 2 1 1 0 1 1 0 1 0 1 1 1 2 1 1 0 1 1 1 0 2 0 0 1 0 1 2 1 1 1 1 1 1 2 0 0 1 1 1 2 2 1 2 2 2 0 2 2 1 0 1 1 1 1 2 1 0 0 1 2 2 1 1 1 1 1 1 1 1 2 1 1 1 1 0 2 1 0 1 1 1 2 1 2 0 0 0 1 0 1 1 2 1 2 2 2 1 2 0 1 2 2 1 1 2 1 2 1 1 1 0 2 1 2 2 2 0 1 0 1 2 1 0 1 2 2 1 1 2 2 2 2 1 2 1 1 2 1 1 2 1 1 2 1 0 0 1 0 2 1 2 2 1 1 1 0 0 2 0 0 2 1 2 1 1 1 2 0 1 0 0 1 2 2 1 2 2 0 0 0 1 0 0 1 0 1 0 0 0 2 1 2 0 0] index_sample = [] 然后从每个类中选出一部分样本 for i in range(3): temp_index = index[np.where(model.labels_==i)[0]] index_sample += np.random.choice(temp_index,size=int(len(np.where(y==0)[0])/3), replace=False).tolist() X_sample = X.iloc[index_sample.tolist()+np.where(y==0)[0].tolist() , :] # 最终的X,不要忘了加上正样本的索引 y_sample = y[index_sample.tolist()+np.where(y==0)[0].tolist()] # 最终的y(3)EasyEnsemble
index = np.where(y==1)[0] # 找出全部负类样本的索引 X_sample = [] y_sample = [] Len = len(index)// 10 for i in range(10): # 这里的10代表将负样本分为多少份,最终训练多少个模型 X_temp = np.vstack((X.iloc[index,:].values[i*Len:(i+1)*Len], X.iloc[np.where(y==0)[0]].values)) y_temp = y[index][i*Len:(i+1)*Len].tolist() + y[np.where(y==0)[0]].tolist() X_sample.append(X_temp) y_sample.append(y_temp)这里X_sample和y_sample都包含10组数据,并且每一组数据中正负样本是接近平衡的(通过10那个参数来调整),我们用这10组数据来训练一个集成模型,全局来看不会丢失重要信息。
所有X和y都跟上文相同 (1)随机过采样
index = np.where(y==0)[0] # 找出全部正类(少数)样本的索引 index_sample = np.random.choice(index, size=100) X_sample = X.iloc[index_sample] y_sample = [0] * 100 X_sample =np.vstack((X.values, X_sample.values)) # 最终的X,全样本加上新增的样本 y_sample = y.tolist() + y_sample # 最终的y(2)SMOTE采样
from sklearn.neighbors import KNeighborsClassifier X_sample,y_sample = [],[] index = np.where(y==0)[0] # 找出全部正类(少数)样本的索引 model = KNeighborsClassifier(n_neighbors=4).fit(X.iloc[index,:],y[index]) smote_index = np.random.choice(index, size=100,replace=False) # 这里100改成你想要增加的样本数 for ind in smote_index: neig_index = model.kneighbors(X.iloc[ind,:].values.reshape(1,-1),return_distance=False)[0] i = neig_index[0] # 本身 j = np.random.choice(neig_index[1:], size=1)[0] # 从他的邻居中选一个 z = 0.5* X.iloc[i,:].values + 0.5* X.iloc[j,:].values # 进行插值生产新的样本 X_sample.append(z.tolist()) y_sample.append(0) X_sample =np.vstack((X.values, np.array(X_sample))) # 最终的X,全样本加上新增的样本 y_sample = y.tolist() + y_sample # 最终的y第三种方法是概率样本的权重,按理说样本数量越少,那么它的权重应该越大。所以我们可以用样本频率的倒数作为样本的权重。 w e i g h t i = ∣ N ∣ ∣ N i ∣ weight_i=\frac{|N|}{|N_i|} weighti=∣Ni∣∣N∣表示第 i i i类样本的权重,其中 ∣ N ∣ |N| ∣N∣为样本总量, ∣ N i ∣ |N_i| ∣Ni∣为第 i i i类样本数量。这样的好处是对于样本数量比较少的类别,会得到更高的权重。
sklearn在训练模型的时候有一个权重参数可以选,我们在.fit()里面直接加上就好。下面以逻辑回归为例。
weight = np.array([len(y_train[y_train==0])/len(y_train), len(y_train[y_train==1])/len(y_train)]) sample_weight = y_train.copy() sample_weight[sample_weight==0] = weight[0] sample_weight[sample_weight==1] = weight[1] model = LogisticRegression().fit(X_train,y_train,sample_weight=sample_weight) y_pred = model.predict(X_test)本文主要介绍几种常用的解决类别不平衡问题的方法,各有优缺点,也可以结合使用。除此之外,还有其他的一些解决方法,比如说选择对类别不敏感的模型、把分类问题转变为异常检测问题(当样本极不平衡的时候可以考虑这种,因为这时候正类一般只有几个,上面提到的方法都会失效)等等