不均衡學習

本文是《智能風控、算法和工程實踐》第五章學習筆記。

倖存者偏差

  由於風控模型的拒絕屬性,會導致倖存者偏差和樣本不均衡兩個問題。 倖存者偏差是指,每次模型迭代時,使用的樣本都是被前一個模型篩選過的,只有高於前一版模型分數閾值的樣本,纔可以進入當前模型訓練,於是樣本空間就會不完備。進入模型的樣本就是倖存者,它們不攜帶或者很少攜帶被拒絕的樣本的信息,導致樣本逐漸偏離真實分佈。
  於是隨着模型迭代,區分能力強的特徵被弱化,甚至對模型起到完全相反的作用。想要修正有偏差的信息,就需要用無偏差的數據重新訓練模型。因此核心問題就是如何獲取無偏差的數據。
傳統的解決方法就是使用拒絕推斷爲拒絕樣本添加僞標記和權重。拒絕推斷的方法見: 拒絕推斷
  書中介紹的方法是一些其他的機器學習方法,具體有:

  1. 增量學習
  2. 生成對抗網絡——GAN模型
  3. 高斯混合模型——GMM算法

  這裏就不再深入學習以上的算法了,因爲實際工作中用不用得到拒絕推斷還是個問號。

樣本不均衡

  在風控場景下,負樣本的佔比要遠遠小於正樣本的佔比。當不同類別的樣本量有較大差異時,在梯度下降的過程中就會很難收斂到最優解。因爲在每一次梯度下降的過程中,負樣本所貢獻的信息有限,導致模型無法很好地學習負樣本。通俗說就是,負樣本越多,模型的區分效果通常會越好,KS值會越大。

代價敏感加權方案

  下探是解決不均衡問題最直接的方法。就是在拒絕域中隨機接收一些樣本,以此來積累負樣本。但這樣風險較高,會犧牲一部分收益,而且較難量化每次下探的量。
  對少數類樣本進行加權處理,使模型進行均衡訓練,即代價敏感加權方案,又叫作展開法。在邏輯迴歸中,可以通過參數class_weight="balanced"調整正負樣本的權重,大多數情況下樣本加權可以增強模型的表現。
  類權重的計算方法如下:
weight=n_samplesn_classesnp.bincount(y)weight=\frac{n\_samples}{n\_classes*np.bincount(y)}
  其中,n_samples爲樣本數,n_classes爲類別數量,np.bincount(y)會輸出每個類別的樣本數。

插值過採樣方案

  插值過採樣方案又叫作SMOTE算法,是機器學習中常用的採樣方法。基本思想就是在現有少數類樣本之間進行插值,人工合成新樣本。SMOTE算法的原理如下圖:

  爲了解決SMOTE算法的過擬合問題,Adaptive Synthetic Sampling方法被提出,主要包括:Borderline-SMOTE和Adaptive Synthetic Sampling(ADA-SYN)算法。

  1. Borderline-SMOTE
      對靠近邊界的minority樣本創造新數據。其與SMOTE的不同是:SMOTE是對每一個少數類樣本產生綜合新樣本,而Borderline-SMOTE僅對靠近邊界的少數樣本創造新數據。

  Borderline-SMOTE算法只對近鄰中多數類樣本大於少數類樣本的點進行合成新樣本。但是如果一個樣本的近鄰都是多數類樣本, 則會被認爲是噪聲,不合成新樣本。

  1. ADA-SYN
      根據多數類和少數類的密度分佈,動態改變權重,決定要生成多少少數類的新數據。

  2. 基於聚類的隨機採樣(CBO)

  可以用來解決類內不平衡問題,主要利用的聚類的方法。具體的過程如下:

隨機選擇K個樣本作爲K個簇,並且計算K類樣本在特徵空間的平均值,作爲聚類中心;
對於剩下的每一個樣本,計算它和K個聚類中心的歐氏距離,根據歐式聚類將其分配到最近的類簇中;
更新每個簇的聚類中心,直到所有的樣本都用完;

  CBO會使用過採樣的方法填充多數類和少數類的類簇,所以每個類的樣本數相同。少數類的樣本個數爲多數類樣本除以少數類的類別數。即上圖中60/2=30。

過採樣算法實戰

  由於SMOTE算法是基於樣本空間進行插值,會放大數據集中的噪聲和異常,因此要對訓練樣本進行清洗。這裏使用lightgbm對數據進行擬合,將預測結果較差的樣本權重降低,並且不參與SMOTE算法的插值過程。列出核心代碼:

def  lgb_test(train_x,train_y,test_x,test_y):
    clf =lgb.LGBMClassifier(boosting_type = 'gbdt',
                           objective = 'binary',
                           metric = 'auc',
                           learning_rate = 0.1,
                           n_estimators = 24,
                           max_depth = 4,
                           num_leaves = 25,
                           max_bin = 40,
                           min_data_in_leaf = 5,
                           bagging_fraction = 0.6,
                           bagging_freq = 0,
                           feature_fraction = 0.8,
                           )
    clf.fit(train_x,train_y,eval_set = [(train_x,train_y),(test_x,test_y)],eval_metric = 'auc')
    return clf,clf.best_score_['valid_1']['auc'],
lgb_model , lgb_auc  = lgb_test(train_x,train_y,test_x,test_y)
feature_importance = pd.DataFrame({'name':lgb_model.booster_.feature_name(),
                                   'importance':lgb_model.feature_importances_}).sort_values(by=['importance'],ascending=False)

pred = lgb_model.predict_proba(train_x)[:,1]
fpr_lgb,tpr_lgb,_ = roc_curve(train_y,pred)
print(abs(fpr_lgb - tpr_lgb).max())
    
pred = lgb_model.predict_proba(test_x)[:,1]
fpr_lgb,tpr_lgb,_ = roc_curve(test_y,pred)
print(abs(fpr_lgb - tpr_lgb).max())

pred = lgb_model.predict_proba(evl_x)[:,1]
fpr_lgb,tpr_lgb,_ = roc_curve(evl_y,pred)
print(abs(fpr_lgb - tpr_lgb).max())
sample = x[feature_lst]
sample['bad_ind'] = y
sample['pred'] = lgb_model.predict_proba(x)[:,1]
sample = sample.sort_values(by=['pred'],ascending=False).reset_index()

sample['rank'] = np.array(sample.index)/75522

def weight(x,y):
    if x == 0 and y < 0.1:
        return 0.1
    elif x == 1 and y > 0.7:
        return 0.1
    else:
        return 1

sample['weight'] = sample.apply(lambda x: weight(x.bad_ind,x['rank']),axis = 1)

def lr_wt_predict(train_x,train_y,evl_x,evl_y,weight):
    lr_model = LogisticRegression(C=0.1,class_weight='balanced')
    lr_model.fit(train_x,train_y,sample_weight = weight )
    
    y_pred = lr_model.predict_proba(train_x)[:,1]
    fpr_lr,tpr_lr,_ = roc_curve(train_y,y_pred)
    train_ks = abs(fpr_lr - tpr_lr).max()
    print('train_ks : ',train_ks)
    
    y_pred = lr_model.predict_proba(evl_x)[:,1]
    fpr_lr,tpr_lr,_ = roc_curve(evl_y,y_pred)
    evl_ks = abs(fpr_lr - tpr_lr).max()
    print('evl_ks : ',evl_ks)
    
lr_wt_predict(sample[feature_lst],sample['bad_ind'],evl_x,evl_y,sample['weight'])

  這裏的思想就是使用集成模型對數據做異常點檢測,將難以辨別的樣本視爲噪音。可以理解爲大神都做不對的題目,就不讓普通學員做了。異常點檢測的邏輯爲:低分段錯分比例和高分段錯分比例。
  下面做基於borderline1的smote算法做過採樣。只對weight=1的樣本進行過採樣。

osvp_sample = sample[sample.weight == 1].drop(['pred','index','weight'],axis = 1)
osnu_sample = sample[sample.weight < 1].drop(['pred','index',],axis = 1)

train_x_osvp = osvp_sample[feature_lst]
train_y_osvp = osvp_sample['bad_ind']
def lr_predict(train_x,train_y,evl_x,evl_y):
    lr_model = LogisticRegression(C=0.1,class_weight='balanced')
    lr_model.fit(train_x,train_y)
    
    y_pred = lr_model.predict_proba(train_x)[:,1]
    fpr_lr,tpr_lr,_ = roc_curve(train_y,y_pred)
    train_ks = abs(fpr_lr - tpr_lr).max()
    print('train_ks : ',train_ks)
    
    y_pred = lr_model.predict_proba(evl_x)[:,1]
    fpr_lr,tpr_lr,_ = roc_curve(evl_y,y_pred)
    evl_ks = abs(fpr_lr - tpr_lr).max()
    print('evl_ks : ',evl_ks)
    return train_ks,evl_ks

from imblearn.over_sampling import SMOTE,RandomOverSampler,ADASYN
smote = SMOTE(k_neighbors=15, kind='borderline1', m_neighbors=4, n_jobs=1,
              out_step='deprecated', random_state=0, ratio=None,
              svm_estimator='deprecated')
rex,rey = smote.fit_resample(train_x_osvp,train_y_osvp)
print('badpctn:',rey.sum()/len(rey))
df_rex = pd.DataFrame(rex)
df_rex.columns =feature_lst
df_rex['weight'] = 1
df_rex['bad_ind'] = rey
df_aff_ovsp = df_rex.append(osnu_sample)
lr_predict(df_aff_ovsp[feature_lst],df_aff_ovsp['bad_ind'],evl_x,evl_y)

  採樣後的KS值會有一定的提升,且過擬合的風險不會很大。

半監督學習方案

  SMOTE算法所產生的新樣本仍然是基於樣本產生的,並沒有爲模型引入拒絕樣本信息。通過半監督學習對拒絕樣本進行預測,自動利用無標籤樣本提升學習性能。
書中介紹的兩種半監督學習模型是半監督支持向量機和標籤傳播算法。

S3VM

  半監督支持向量機。基本思想是在不考慮無標籤樣本的情況下嘗試尋找最大間隔劃分超平面。

LP

  標籤傳播算法,是一種基於圖的半監督學習方式。LP算法包括兩個步驟:

  1. 構造相似矩陣
  2. 通過相似度進行傳播
      下面通過一個例子介紹LP算法的應用。首先自動生成200個弧形分佈的數據點,然後指定其中兩個點的標籤。使用LP算法根據有標籤樣本在樣本空間的位置進行標籤傳遞,最終使得每個樣本都有標籤。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.semi_supervised import label_propagation
from sklearn.datasets import make_moons

#生成弧形數據
n_samples=200
X,y=make_moons(n_samples,noise=0.04,random_state=0)
outer,inner=0,1
labels=np.full(n_samples,-1.)
labels[0]=outer
labels[-1]=inner

#使用LP算法實現標籤傳遞
label_spread=label_propagation.LabelSpreading(kernel='rbf',alpha=0.8)
label_spread.fit(X,labels)

#輸出標籤
output_labels=label_spread.transduction_
plt.figure(figsize=(8.5,4))
plt.subplot(1,2,1)
plt.scatter(X[labels==outer,0].X[labels==outer,1],color='navy',marker='s',lw=0,label="outer labeled",s=10)
plt.scatter(X[labels==inner,0].X[labels==inner,1],color='c',marker='s',lw=0,label="inner labeled",s=10)
plt.scatter(X[labels==-1,0].X[labels==-1,1],color='darkorange',marker='.',label="unlabeled",)
plt.legend(scatterpoints=1,shadow=False,loc='upper right')
plt.title("Raw data (2 classes=outer and inner)")

  左圖爲原始數據分佈,橘黃色的點代表無標籤樣本,深藍色的點爲負樣本,淺藍色爲正樣本;右圖爲標籤傳播的結果。

小結

  1. 代價敏感加權實現較爲簡單,使用廣泛;
  2. SMOTE算法需要進行樣本和特徵清洗,再進行過採樣;
  3. 半監督學習需要一定的無標籤樣本,泛化能力強,但精度無法保證。

  半監督學習比過採樣算法泛化能力更強,因爲是從無標籤樣本中召回負樣本。但準確率不好驗證,模型不夠穩定。

【作者】:Labryant
【原創公衆號】:風控獵人
【簡介】:某創業公司策略分析師,積極上進,努力提升。乾坤未定,你我都是黑馬。
【轉載說明】:轉載請說明出處,謝謝合作!~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章