一天一个机器学习小知识——类别不平衡问题的解决方法


前言

类别不平衡是机器学习中经常遇到的问题,有时候类别不平衡会直接影响到模型的训练结果。这里介绍几种常见的缓解类别不平衡问题的方法。假设样本数较少的类为正类,反之为负类。

一、改变阈值

1.理论介绍

比如逻辑回归可以写成如下形式,若
y 1 − y > m + m − \frac{y}{1-y}>\frac{m^{+}}{m^{-}} 1yy>mm+则预测为正例,实际上是对类别进行一个“再缩放”(传统的逻辑回归是假设 m + m − = 1 \frac{m^{+}}{m^{-}}=1 mm+=1 y ′ 1 − y ′ = y 1 − y × m − m + \frac{y^{\prime}}{1-y^{\prime}}=\frac{y}{1-y} \times \frac{m^{-}}{m^{+}} 1yy=1yy×m+m或者通俗来说,传统的逻辑回归中,如果输出大于0.5则判定为正类,反之为负类;如果类别不平衡,正类样本过少,我们可以把这个阈值调大一点,比如说只有输出大于0.8的时候才输出为正类,反之输出为负类。

2.代码实现

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_breast_cancer
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np

data = load_breast_cancer() # 获取样例数据,这里的数据乳腺癌检测,y=0代表没有乳腺癌,y=1代表有
X = pd.DataFrame(data.data,columns=data.feature_names)
y = data.target
X_train, X_test, y_train, y_test =train_test_split(X,y,test_size=0.3,random_state=0)
model = LogisticRegression().fit(X_train,y_train)
y_pred0 = model.predict(X_test)  # 直接输出类别

res = model.predict_proba(X_test) # 先输出概率,进行缩放,再转化为类别
y_pred1 = []
r =  y_train.tolist().count(0)/y_train.tolist().count(1)        # 假设0是正类,1是负类,这里的r就代表 m+ / m-
for i in range(res.shape[0]):
    if res[i][0] / res[i][1] > r:
        y_pred1.append(0)
    else:
        y_pred1.append(1)
print('两者输出的相似率:%.3f'%(accuracy_score(y_pred2,y_pred)))
两者输出的相似率:0.994

可以看到调整前和调整后的输出是差不多的,这是因为我这里的数据并没有出现样本不平衡的情况。

二、抽样方法

1.理论介绍

再缩放的思想虽然简单,但是实际应用中却不一定很理想,这主要是因为真实样本的分布可能跟训练样本分布不一致,这样我们根据训练样本选定的阈值可能就不适合于真实样本。其实解决样本不平衡问题的最直观也是最常见的方法就是改变某一类的样本数量,使其变成平衡样本。改变样本数量使得样本变得平衡有两个思路:第一个是减少多数(负类)样本,这种叫欠采样;另一种是增加少数(正类)样本,这种叫过采样.

1.1 欠采样

欠采样的思想是减少多数(负类)样本,使得正类和负类的样本数平衡。欠采样常见的具体实现方法有三种:
(1)随机欠采样:随机选择部分多数样本参与模型训练
(2)先对多数样本进行聚类(Kmeans),然后分别从每个聚类簇中选择部分样本参与模型训练,类似于进行分层采样,它的主要思想是选出有代表性的多数样本参与模型训练。
(3)将负样本划分为k分,然后分别训练k个模型,再将这k个模型进行集成(EasyEnsemble)
随机欠采样由于随机选择了部分样本,所以模型可能会丢掉一些重要信息;先聚类再采样的方法虽然尽可能的选择了有代表性的样本,但是在模型训练的过程中还是会损失部分信息;EasyEnsemble利用集成学习机制,将反例划分为若干个集合供不同学习器使用,这样对每个学习器来看都进行了欠采样,但全局来看不会丢失重要信息,这个方法的缺点是会增加计算的复杂度。

1.2 过采样

过采样的思想是增加少数(正类)样本,使得正类和负类的样本数平衡。欠采样常见的具体实现方法主要有两种:
(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是根据正样本之间的线性组合生成新样本,所以可能会扩大噪声对模型的影响。

2.代码实现

2.1 欠采样

所有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组数据来训练一个集成模型,全局来看不会丢失重要信息。

2.1 欠采样

所有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

三、改变样本权重

1.理论介绍

第三种方法是概率样本的权重,按理说样本数量越少,那么它的权重应该越大。所以我们可以用样本频率的倒数作为样本的权重。
w e i g h t i = ∣ N ∣ ∣ N i ∣ weight_i=\frac{|N|}{|N_i|} weighti=NiN表示第 i i i类样本的权重,其中 ∣ N ∣ |N| N为样本总量, ∣ N i ∣ |N_i| Ni为第 i i i类样本数量。这样的好处是对于样本数量比较少的类别,会得到更高的权重。

2.代码实现

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)

总结

本文主要介绍几种常用的解决类别不平衡问题的方法,各有优缺点,也可以结合使用。除此之外,还有其他的一些解决方法,比如说选择对类别不敏感的模型、把分类问题转变为异常检测问题(当样本极不平衡的时候可以考虑这种,因为这时候正类一般只有几个,上面提到的方法都会失效)等等

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值