數據不平衡與Smote算法

本文來自OPPO互聯網技術團隊,轉載請註名做者。同時歡迎關注咱們的公衆號:OPPO_tech,與你分享OPPO前沿互聯網技術及活動。html

在實際生產中,可能會碰到一種比較頭疼的問題,那就是分類問題中類別型的因變量可能存在嚴重的不平衡,即類別之間的比例嚴重失調。git

爲了解決數據的非平衡問題,2002年Chawla提出了SMOTE算法,並受到學術界和工業界的一致認同。本文將對Smote算法,Smote算法的幾個變形,以及一個smote算法的主流開源實現的源碼進行分析。github

1. 數據不均衡問題概述

1.1 常見的數據不平衡場景

醫學影像:癌細胞識別,健康細胞和癌細胞的比例20:1算法

保險:車險報銷數據集,報銷和未報銷比例26:1

天文學:其餘記錄和太陽風記錄,比例26 : 1數組

CTR:未點擊和點擊記錄比例57:1bash

除了以上例子之外,數據不平衡問題的場景還有:欺詐性交易,識別客戶流失率(其中絕大多數顧客都會繼續使用該項服務),天然災害,例如地震等等。markdown

1.2 不平衡場景下的準確率評估

當面臨不平衡的數據集的時候,機器學習算法傾向於產生不太使人滿意的分類器。對於任何一個不平衡的數據集,若是要預測的事件屬於少數類別,而且事件比例小於 5%,那就一般將其稱爲罕見事件(rare event)。在不平衡領域時,準確率並非一個用來衡量模型性能的合適指標。架構

在一個公用事業欺詐檢測數據集中,有如下數據:併發

總觀測 = 1000dom

欺詐觀測(正樣本) = 20

非欺詐觀測 (負樣本)= 980

罕見事件比例 = 2%

在這個例子中,若是一個分類器將全部屬於大部分類別的實例都正確分類,實現了 98% 的準確率;而把佔 2% 的少數觀測數據視爲噪聲並消除了。但實際上咱們真正關心的是被消除的那2%的少數類樣本。

因此這種狀況下,準確率嚴重高估了模型的表現。

1.3 不平衡場景下的AUC評估

在評估模型在不平衡數據集上的表現時,應該使用AUC,即ROC曲線的面積。ROC曲線是指一個模型的threshold在連續變換時對應的真正例率和假正例率圍成的曲線。

假正例率:FPR = FP/(TN+FP),把一個負樣本錯當成正樣本的機率。

真正例率:TPR= TP/(TP +FN),把一個正樣本分對的機率。

固然是FPR越低越好,TPR越高越好。

AUC值是一個機率值,當你隨機挑選一個正樣本以及負樣本,當前的分類算法根據計算獲得的Score值將這個正樣本排在負樣本前面的機率就是AUC值,AUC值越大,當前分類算法越有可能將正樣本排在負樣本前面,從而可以更好地分類。

AUC的通常判斷標準:

  • 0.1 - 0.5:模型的表現比隨機猜想還差

  • 0.5 - 0.7:效果較低,但用於預測股票已經很不錯了

  • 0.7 - 0.85:效果通常

  • 0.85 - 0.95:效果很好

  • 0.95 - 1:效果很是好,但通常不太可能

下面幾個小動畫能夠幫助你們更好得理解AUC:

左圖中橙色的曲線是指負樣本(多數類)的分佈,紫色的曲線是指正樣本(少數類)的分佈,兩條曲線中間透明的邊界就是模型用於把機率轉換爲0/1的閾值(e.g,邏輯迴歸中的默認的0.5),右圖就是在正負樣本分佈以及閾值變化時不一樣的ROC曲線。

分類器的分類性能越好,即把正負樣本分開能力越強時,真正例率越高,假正例率越低,這樣ROC曲線就越往左上方偏斜,當ROC曲線爲一條45度直線時,表示模型的區分能力等同於隨機亂猜。

ROC曲線的繪製和PR曲線同樣,即遍歷模型的全部Threshold,計算當前Threshold下的FTR和PTR,而後把全部Threshlod下的(FPR, TPR)連成一條曲線。能夠看出,閾值變化時不會影響模型的AUC值的。

當面臨不平衡的數據集的時候,ROC曲線無視樣本不均衡的狀況,只考慮模型的分類能力,如圖,正負樣本的比例變化時,模型的區分能力不變,ROC曲線的形狀是不會變的。因此對模型在不平衡數據集上的性能評估最好使用AUC而不是Accuray。

1.4 小實驗:不平衡數據對模型表現的影響

這裏用一個小實驗測試模型在正負樣本不一樣比例下的表現。

使用sklearn.datasets make_classification生成試驗用的二分類數據,正樣本/整體的比例分別爲[0.01,0.05,0.1,0.2,0.5],數據爲二維,樣本數500個。使用默認值的決策樹模型進行擬合,計算其AUC值,畫出分離邊界。

from sklearn.datasets import *
from sklearn.model_selection import *
from sklearn import tree
from sklearn.metrics import *
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
#決策邊界繪製函數
def plot_decision_boundary(X_in,y_in,pred_func):
    x_min, x_max = X_in[:, 0].min() - .5, X_in[:, 0].max() + .5
    y_min, y_max = X_in[:, 1].min() - .5, X_in[:, 1].max() + .5
    h = 0.01
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
    Z = pred_func(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    plt.contourf(xx, yy, Z,cmap=plt.get_cmap('GnBu'))
    plt.scatter(X_in[:, 0], X_in[:, 1], c=y_in,cmap=plt.get_cmap('GnBu'))
#在不一樣的正負樣本比例下訓練決策樹,並畫出決策邊界,計算AUC
for weight_minority in [0.01,0.05,0.1,0.2,0.5]:
X,y=make_classification(n_samples=500,n_features=2,n_redundant=0,random_state=2,n_clusters_per_class=1,weights=(weight_minority,1-weight_minority))
    plt.scatter(X[:,0],X[:,1],c=y)
    X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,random_state=6)
    clf = tree.DecisionTreeClassifier()
    clf = clf.fit(X_train, y_train)
    plot_decision_boundary(X,y,lambda x: clf.predict(x))
    plt.ion()
    plt.title("Decision Tree with imbalance rate: "+str(weight_minority))
    plt.show()
    print("current auc:"+str(roc_auc_score(y_test, clf.predict(X_test))))
    print("----------------------------------")

複製代碼

正負樣本比例1:100,AUC:0.5

正負樣本比例1:20,AUC:0.8039

正負樣本比例1:10,AUC:0.9462

正負樣本比例1:5,AUC:0.9615

正負樣本比例1:1,AUC:0.9598

可見在樣本不平衡的時候,模型的表現較差,當少數類的比例升高後,模型的auc迅速提高,分離邊界也更加合理,在比例繼續升高的時候auc有少量降低是由於正負樣本發生了重疊。

2. 應對數據不均衡問題的經常使用方法

這一節會對經常使用的應對數據不均衡的方法進行一個簡單的總結,按照樣本處理方式能夠分爲4類:過採樣,欠採樣,過採樣+欠採樣以及異常檢測。

2.1 過採樣

隨機過採樣:隨機抽取負樣本複製多份。

Smote:採用插值方法生成新樣本,具體原理會在第三節詳述。

Adasyn:原理與Smote相似,只是選取種子樣本時會用一個KNN分類器選擇那些更加容易分錯的樣本。

2.2 欠採樣

隨機欠採樣

隨機從多數類樣本中抽取一部分數據進行刪除,隨機欠採樣有一個很大的缺點是未考慮樣本的分佈狀況,而採樣過程又具備很大的隨機性,可能會誤刪多數類樣本中一些重要的信息。

EasyEnsemble

將多數類樣本隨機劃分紅n個子集,每一個子集的數量等於少數類樣本的數量,這至關於欠採樣。接着將每一個子集與少數類樣本結合起來分別訓練一個模型,最後將n個模型集成,這樣雖然每一個子集的樣本少於整體樣本,但集成後總信息量並不減小。

BalanceCascade

採用了有監督結合Boosting的方式。在第n輪訓練中,將從多數類樣本中抽樣得來的子集與少數類樣本結合起來訓練一個基學習器H,訓練完後多數類中能被H正確分類的樣本會被剔除。

在接下來的第n+1輪中,從被剔除後的多數類樣本中產生子集用於與少數類樣本結合起來訓練,最後將不一樣的基學習器集成起來。BalanceCascade的有監督表如今每一輪的基學習器起到了在多數類中選擇樣本的做用,而其Boosting特色則體如今每一輪丟棄被正確分類的樣本,進然後續基學習器會更注重那些以前分類錯誤的樣本。

NearMiss

一種原型選擇(prototype selection)方法,即從多數類樣本中選取最具表明性的樣本用於訓練,主要是爲了緩解隨機欠採樣中的信息丟失問題。NearMiss採用一些啓發式的規則來選擇樣本,根據規則的不一樣可分爲3類:

NearMiss-1:選擇到最近的K個少數類樣本平均距離最近的多數類樣本。

NearMiss-2:選擇到最遠的K個少數類樣本平均距離最近的多數類樣本。

NearMiss-3:對於每一個少數類樣本選擇K個最近的多數類樣本,目的是保證每一個少數類樣本都被多數類樣本包圍。

Tomek Link

Tomek Link表示不一樣類別之間距離最近的一對樣本,即這兩個樣本互爲最近鄰且分屬不一樣類別。這樣若是兩個樣本造成了一個Tomek Link,則要麼其中一個是噪音,要麼兩個樣本都在邊界附近。這樣經過移除Tomek Link就能「清洗掉」類間重疊樣本,使得互爲最近鄰的樣本皆屬於同一類別,從而能更好地進行分類。

Edited Nearest Neighbours (ENN)

對於屬於多數類的一個樣本,若是其K個近鄰點有超過一半都不屬於多數類,則這個樣本會被剔除。這個方法的另外一個變種是全部的K個近鄰點都不屬於多數類,則這個樣本會被剔除。

最後,數據清洗技術最大的缺點是沒法控制欠採樣的數量。因爲都在某種程度上採用了K近鄰法,而事實上大部分多數類樣本週圍也都是多數類,於是能剔除的多數類樣本比較有限。

2.3 過採樣+欠採樣

不少實驗代表結合過採樣和欠採樣比單獨使用這兩種方法會有更好的效果,經常使用的過採樣和欠採樣組合包括SMOTE + ENN、SMOTE + Tomek。

2.4 異常檢測方法

當少數類的樣本並不屬於同一種分佈時,能夠考慮使用異常檢測方法區分多數類和少數類。

統計方法檢測

統計方法也比較簡單,通常分兩步:

  • 先假設全量數據服從必定的分佈,好比常見的正太分佈,泊松分佈等;

  • 再計算每一個點屬於這個分佈的機率,也就是常見的以平均值和方差肯定密度函數的問題。

OneClassSVM

只有一類的信息是能夠用於訓練,其餘類別的(總稱outlier)信息是缺失的,也就是區分兩個類別的邊界線是經過僅有的一類數據的信息學習獲得的。尋找一個超平面將樣本中的正例圈出來,預測就是用這個超平面作決策,在圈內的樣本就認爲是正樣本。

孤立森林

在孤立森林(iForest)中,異常被定義爲「容易被孤立的離羣點 (more likely to be separated)」,能夠將其理解爲分佈稀疏且離密度高的羣體較遠的點。 在特徵空間裏,分佈稀疏的區域表示事件發生在該區域的機率很低,於是能夠認爲落在這些區域裏的數據是異常的。

孤立森林是一種適用於連續數據(Continuous numerical data)的無監督異常檢測方法,即不須要有標記的樣原本訓練,但特徵須要是連續的。對於如何查找哪些點容易被孤立(isolated),iForest使用了一套很是高效的策略。在孤立森林中,遞歸地隨機分割數據集,直到全部的樣本點都是孤立的。在這種隨機分割的策略下,異常點一般具備較短的路徑。

3. Smote算法及其變形的原理

3.1 什麼是Smote

2002年Chawla提出了SMOTE (synthetic minority oversampling technique) 算法,並受到學術界和工業界的一致認同,SMOTE 的思想歸納起來就是在少數類樣本之間進行插值來產生額外的樣本。

爲何要用SMOTE,直接隨機過採樣不行嗎?下面這個例子來自於SMOTE原始論文。

數據取自一個乳腺癌X光片數據集,紅色爲少數樣本,綠色爲多數樣本,上圖爲直接隨機過採樣後訓練的決策樹分離邊界,可見隨機過採樣存在嚴重過擬合。模型只學習到了隨機過採樣的那幾個種子樣本,而把其餘少數類樣本都當成了噪聲!

具體地,對於一個少數類樣本X_i使用K近鄰法(k值須要提早指定),求出離X_i距離最近的k個少數類樣本,其中距離定義爲樣本之間n維特徵空間的歐氏距離。而後從k個近鄰點中隨機選取一個,使用下列公式生成新樣本:

X_{(new)}=X_i+(\hat X-X_I)\times \delta

其中\hat X_i爲選出的k近鄰點,δ∈[0,1]是一個隨機數。下圖就是一個SMOTE生成樣本的例子,使用的是3-近鄰,能夠看出SMOTE生成的樣本通常就在X_i\hat X_i相連的直線上:

下面是原始論文中的僞代碼,感興趣的同窗能夠仔細研究一下,其實其中的核心代碼就是第22行:插值生成的邏輯。

3.2 Smote算法的變形

Border-line SMOTE

SMOTE會隨機選取少數類樣本用以合成新樣本,而不考慮周邊樣本的狀況,這樣容易帶來兩個問題:

  1. 若是選取的少數類樣本週圍也都是少數類樣本,則新合成的樣本不會提供太多有用信息。這就像支持向量機中遠離margin的點對決策邊界影響不大。

  2. 若是選取的少數類樣本週圍都是多數類樣本,這類的樣本多是噪音,則新合成的樣本會與周圍的多數類樣本產生大部分重疊,導致分類困難。

總的來講咱們但願新合成的少數類樣本能處於兩個類別的邊界附近,這樣每每能提供足夠的信息用以分類。而這就是下面的 Border-line SMOTE 算法要作的事情。

這個算法會先將全部的少數類樣本分紅三類,以下圖所示:

  • "noise" : 全部的k近鄰個樣本都屬於多數類

  • "danger" : 超過一半的k近鄰樣本屬於多數類

  • "safe": 超過一半的k近鄰樣本屬於少數類

Border-line SMOTE算法只會從處於」danger「狀態的樣本中隨機選擇,而後用SMOTE算法產生新的樣本。處於」danger「狀態的樣本表明靠近」邊界「附近的少數類樣本,而處於邊界附近的樣本每每更容易被誤分類。於是 Border-line SMOTE 只對那些靠近」邊界「的少數類樣本進行人工合成樣本,而 SMOTE 則對全部少數類樣本一視同仁。

SVM SMOTE

使用一個SVM分類器尋找支持向量,而後在支持向量的基礎上合成新的樣本。相似Broderline Smote,SVM smote也會根據K近鄰的屬性決定樣本的類型(safe,danger,noice),而後使用danger的樣本訓練SVM。

Kmeans SMOTE

在合成樣本以前先對樣本進行聚類,而後根據簇密度的大小分別對不一樣簇的負樣本進行合成。在聚類步驟中,使用k均值聚類爲k個組。過濾選擇用於過採樣的簇,保留具備高比例的少數類樣本的簇。而後,它分配合成樣本的數量,將更多樣本分配給少數樣本稀疏分佈的羣集。最後,過採樣步驟,在每一個選定的簇中應用SMOTE以實現少數和多數實例的目標比率。

SMOTE-NC

以上的Smote方法都不能處理分類變量,SMOTE-NC因爲分類變量沒法計算插值,SMOTE-NC會在合成新樣本的時候參考新樣本最近鄰的該特徵,而後區其中出現次數最多的值。

下圖是Smote幾種變形的過採樣結果可視化:

可見使用原始Smote的話,插值產生的新樣本極可能會穿過正確的分離邊界,進入其餘類的區域,而其餘變形則很好地解決了這個問題。

4. 開源實現Imbalanced_learn中的smote實現

市面上Smote的一個主流實現是來自於sklearn的contrib項目imbalanced_learn,使用imbalanced_learn的smote符合sklearn的API規範,下面是一段使用smote的示例代碼:

>>> from collections import Counter
>>> from sklearn.datasets import make_classification
>>> from imblearn.over_sampling import SMOTE
>>> X, y = make_classification(n_classes=2, class_sep=2,
... weights=[0.1, 0.9], n_informative=3, n_redundant=1, flip_y=0,
... n_features=20, n_clusters_per_class=1, n_samples=1000, random_state=10)

>>> print('Original dataset shape %s' % Counter(y))
Original dataset shape Counter({1: 900, 0: 100})

>>> sm = SMOTE(random_state=42)
>>> X_res, y_res = sm.fit_resample(X, y)
>>> print('Resampled dataset shape %s' % Counter(y_res))

Resampled dataset shape Counter({0: 900, 1: 900})
複製代碼

源碼中的類繼承關係以下圖:

下面咱們以boardline_smote爲例分析源碼,咱們將按照從下到上(SMOTE->BorderlineSMOTE->BaseSMOTE)的順序對幾個核心類的關鍵代碼進行分析。

SMOTE

在使用過程當中,最基礎的類就是SMOTE,SMOTE類便可以使用原始的SMOTE算法,也能夠經過傳參使用BoardlineSmote和SVMSmote。

合成樣本的方法就是SMOTE的fit_resample,而fit_resample方法作了兩件事:

第一件事就是作一件模型驗證的工做,包括於檢測KNN模型實例是否被串用,還有檢測當前的Smote實例的類型,若是當前算法是BoardLineSMOTE或者SVMSmote,則把對應算法的_sample方法綁定到當前對象上;

第二件事就是調用_sample方法,其中基本原始SMOTE的_sample方法訓練了一個KNN分類器,經過KNN獲得全部少數類的K近鄰,而後調用了_make_samples方法,咱們在以前提到的插值邏輯就是在_make_samples中實現的,_make_samples是由SVMSMOTE和BorderlineSMOTE的父類BaseSMOTE實現,咱們稍後會講到。

若是算法是BoardLineSMOTE或者SVMSMOTE,則會調用SMOTE的父類,SVMSMOTE和BorderlineSMOTE中實現的_sample方法。

class SMOTE(SVMSMOTE, BorderlineSMOTE):
    
    def __init__(self,
                 sampling_strategy='auto',
                 random_state=None,
                 k_neighbors=5,
                 m_neighbors='deprecated',
                 out_step='deprecated',
                 kind='deprecated',
                 svm_estimator='deprecated',
                 n_jobs=1,
                 ratio=None):
        # FIXME: in 0.6 call super()
        BaseSMOTE.__init__(self, sampling_strategy=sampling_strategy,
                           random_state=random_state, k_neighbors=k_neighbors,
                           n_jobs=n_jobs, ratio=ratio)
        self.kind = kind
        self.m_neighbors = m_neighbors
        self.out_step = out_step
        self.svm_estimator = svm_estimator
        self.n_jobs = n_jobs

   #主要用於檢測KNN模型實例是否被串用,還有檢測當前的Smote實例的類型
    def _validate_estimator(self):
        # FIXME: in 0.6 call super()
        BaseSMOTE._validate_estimator(self)
        # FIXME: remove in 0.6 after deprecation cycle

        #判斷模型類型,把具體類型模型的sample方法綁定到當前對象上
        if self.kind != 'deprecated' and not (self.kind == 'borderline-1' or
                                              self.kind == 'borderline-2'):
            if self.kind not in SMOTE_KIND:
                raise ValueError('Unknown kind for SMOTE algorithm.'
                                 ' Choices are {}. Got {} instead.'.format(
                                     SMOTE_KIND, self.kind))
            else:
                warnings.warn('"kind" is deprecated in 0.4 and will be '
                              'removed in 0.6. Use SMOTE, BorderlineSMOTE or '
                              'SVMSMOTE instead.', DeprecationWarning)
            #以BorderLine爲例:若是類型是borderline的話,就是用BorderlineSMOTE類的sample方法
            if self.kind == 'borderline1' or self.kind == 'borderline2':
                self._sample = types.MethodType(BorderlineSMOTE._sample, self)
                self.kind = ('borderline-1' if self.kind == 'borderline1'
                             else 'borderline-2')

            elif self.kind == 'svm':
                self._sample = types.MethodType(SVMSMOTE._sample, self)

                if self.out_step == 'deprecated':
                    self.out_step = 0.5
                else:
                    warnings.warn('"out_step" is deprecated in 0.4 and will '
                                  'be removed in 0.6. Use SVMSMOTE class '
                                  'instead.', DeprecationWarning)

                if self.svm_estimator == 'deprecated':
                    warnings.warn('"svm_estimator" is deprecated in 0.4 and '
                                  'will be removed in 0.6. Use SVMSMOTE class '
                                  'instead.', DeprecationWarning)
                if (self.svm_estimator is None or
                        self.svm_estimator == 'deprecated'):
                    self.svm_estimator_ = SVC(gamma='scale',
                                              random_state=self.random_state)
                elif isinstance(self.svm_estimator, SVC):
                    self.svm_estimator_ = clone(self.svm_estimator)
                else:
                    raise_isinstance_error('svm_estimator', [SVC],
                                           self.svm_estimator)

            if self.kind != 'regular':
                if self.m_neighbors == 'deprecated':
                    self.m_neighbors = 10
                else:
                    warnings.warn('"m_neighbors" is deprecated in 0.4 and '
                                  'will be removed in 0.6. Use SVMSMOTE class '
                                  'or BorderlineSMOTE instead.',
                                  DeprecationWarning)

                self.nn_m_ = check_neighbors_object(
                    'm_neighbors', self.m_neighbors, additional_neighbor=1)
                self.nn_m_.set_params(**{'n_jobs': self.n_jobs})

    # FIXME: to be removed in 0.6
    def _fit_resample(self, X, y):
        self._validate_estimator()
        return self._sample(X, y)

    #採樣關鍵函數
    def _sample(self, X, y):
        # FIXME: uncomment in version 0.6
        # self._validate_estimator()
        X_resampled = X.copy()
        y_resampled = y.copy()

        for class_sample, n_samples in self.sampling_strategy_.items():
            if n_samples == 0:
                continue
            target_class_indices = np.flatnonzero(y == class_sample)
            X_class = safe_indexing(X, target_class_indices)
      # 訓練一個KNN分類器獲得少數類的K近鄰
            self.nn_k_.fit(X_class)
            nns = self.nn_k_.kneighbors(X_class, return_distance=False)[:, 1:]
      # 真正進行過採樣的方法_make_samples,由BaseSMOTE實現
            X_new, y_new = self._make_samples(X_class, y.dtype, class_sample,
                                              X_class, nns, n_samples, 1.0)
      # 支持對稠密數據和稀疏數據的過採樣 
            if sparse.issparse(X_new):
                X_resampled = sparse.vstack([X_resampled, X_new])
                sparse_func = 'tocsc' if X.format == 'csc' else 'tocsr'
                X_resampled = getattr(X_resampled, sparse_func)()
            else:
                X_resampled = np.vstack((X_resampled, X_new))
            y_resampled = np.hstack((y_resampled, y_new))

        return X_resampled, y_resampled
複製代碼

BorderlineSMOTE,這個類能夠在使用SMOTE時傳入Boardline參數時被調用,或者直接調用,這個類中最核心的函數就是_sample方法,_sample作了兩件事:

第一件事和SMOTE中的相似,檢查KNN分類器是否被串用,還有就是檢查用戶使用的是borderline-1仍是borderline-2算法。

第二件事就是生成新樣本的邏輯,其中插值的邏輯一樣調用BaseSMOTE的_make_samples方法,Borderline和原始SMOTE不同的地方就在於須要把樣本劃分爲safe和danger(這個方法一樣在BaseSMOTE中實現)選取種子樣本,再按照borderline-1/2中不一樣的策略生成新樣本。

詳細邏輯見代碼中的註釋。

class BorderlineSMOTE(BaseSMOTE):
       def __init__(self,
                 sampling_strategy='auto',
                 random_state=None,
                 k_neighbors=5,
                 n_jobs=1,
                 m_neighbors=10,
                 kind='borderline-1'):
        super().__init__(
            sampling_strategy=sampling_strategy, random_state=random_state,
            k_neighbors=k_neighbors, n_jobs=n_jobs, ratio=None)
        self.m_neighbors = m_neighbors
        self.kind = kind

    def _validate_estimator(self):
        super()._validate_estimator()
        self.nn_m_ = check_neighbors_object(
            'm_neighbors', self.m_neighbors, additional_neighbor=1)
        self.nn_m_.set_params(**{'n_jobs': self.n_jobs})
        if self.kind not in ('borderline-1', 'borderline-2'):
            raise ValueError('The possible "kind" of algorithm are '
                             '"borderline-1" and "borderline-2".'
                             'Got {} instead.'.format(self.kind))

    # FIXME: rename _sample -> _fit_resample in 0.6
    def _fit_resample(self, X, y):
        return self._sample(X, y)

    def _sample(self, X, y):
        self._validate_estimator()
         #拿到copy
        X_resampled = X.copy()
        y_resampled = y.copy()

        for class_sample, n_samples in self.sampling_strategy_.items():
            if n_samples == 0:
                continue

            #獲得少數類的索引
            target_class_indices = np.flatnonzero(y == class_sample)

            #獲得少數類樣本列表
            X_class = safe_indexing(X, target_class_indices)

            #使用全量樣本訓練KNN模型(這個模型是用來計算危險樣本的)
            self.nn_m_.fit(X)

            #獲得危險樣本的索引列表
            danger_index = self._in_danger_noise(
                self.nn_m_, X_class, class_sample, y, kind='danger')

            #若是沒有危險樣本就跳過
            if not any(danger_index):
                continue

            #使用少數類訓練一個KNN模型
            self.nn_k_.fit(X_class)
 
            #獲得危險樣本的近鄰
            nns = self.nn_k_.kneighbors(safe_indexing(X_class, danger_index),
                                        return_distance=False)[:, 1:]

            # divergence between borderline-1 and borderline-2
            #borderline-1 採樣作插值的近鄰只屬於少數類
            if self.kind == 'borderline-1':
                # Create synthetic samples for borderline points.
                X_new, y_new = self._make_samples(
                    safe_indexing(X_class, danger_index), y.dtype,
                    class_sample, X_class, nns, n_samples)
                if sparse.issparse(X_new):
                    X_resampled = sparse.vstack([X_resampled, X_new])
                else:
                    X_resampled = np.vstack((X_resampled, X_new))
                y_resampled = np.hstack((y_resampled, y_new))

            #borderline-2 採樣作插值的近鄰可能屬於任何一個類
            elif self.kind == 'borderline-2':
                random_state = check_random_state(self.random_state)
                fractions = random_state.beta(10, 10)

                # only minority
                X_new_1, y_new_1 = self._make_samples(
                    safe_indexing(X_class, danger_index),
                    y.dtype,
                    class_sample,
                    X_class,
                    nns,
                    int(fractions * (n_samples + 1)),
                    step_size=1.)

                # we use a one-vs-rest policy to handle the multiclass in which
                # new samples will be created considering not only the majority
                # class but all over classes.
                X_new_2, y_new_2 = self._make_samples(
                    safe_indexing(X_class, danger_index),
                    y.dtype,
                    class_sample,
                    safe_indexing(X, np.flatnonzero(y != class_sample)),
                    nns,
                    int((1 - fractions) * n_samples),
                    step_size=0.5)

                if sparse.issparse(X_resampled):
                    X_resampled = sparse.vstack(
                        [X_resampled, X_new_1, X_new_2])
                else:
                    X_resampled = np.vstack((X_resampled, X_new_1, X_new_2))
                y_resampled = np.hstack((y_resampled, y_new_1, y_new_2))
        return X_resampled, y_resampled
複製代碼

BaseSMOTE

它是SVMSMOTE和BoardlineSMOTE的父類,主要實現了幾個子類須要用到的方法,就是剛纔提到的_make_samples_in_danger_noise_generate_sample

  • _make_samples主要實現了樣本遍歷的邏輯。

  • _in_danger_noise主要實現了判斷樣本是danger/safe/noise的邏輯。

  • _generate_sample就是以前提到的插值邏輯。

詳細代碼分析見註釋。

SMOTE_KIND = ('regular', 'borderline1', 'borderline2', 'svm')
class BaseSMOTE(BaseOverSampler):
    """Base class for the different SMOTE algorithms."""
    def __init__(self,
                 sampling_strategy='auto',
                 random_state=None,
                 k_neighbors=5,
                 n_jobs=1,
                 ratio=None):
        super().__init__(
            sampling_strategy=sampling_strategy, ratio=ratio)
        self.random_state = random_state
        self.k_neighbors = k_neighbors
        self.n_jobs = n_jobs

    def _validate_estimator(self):
        """Check the NN estimators shared across the different SMOTE algorithms. """
        self.nn_k_ = check_neighbors_object(
            'k_neighbors', self.k_neighbors, additional_neighbor=1)
        self.nn_k_.set_params(**{'n_jobs': self.n_jobs})

    #製造合成樣本的函數
    def _make_samples(self,
                      X,
                      y_dtype,
                      y_type,
                      nn_data,
                      nn_num,
                      n_samples,
                      step_size=1.):
        """A support function that returns artificial samples constructed along the line connecting nearest neighbours. Parameters ---------- X : {array-like, sparse matrix}, shape (n_samples, n_features) Points from which the points will be created. y_dtype : dtype The data type of the targets. y_type : str or int The minority target value, just so the function can return the target values for the synthetic variables with correct length in a clear format. nn_data : ndarray, shape (n_samples_all, n_features) Data set carrying all the neighbours to be used nn_num : ndarray, shape (n_samples_all, k_nearest_neighbours) The nearest neighbours of each sample in `nn_data`. n_samples : int The number of samples to generate. step_size : float, optional (default=1.) The step size to create samples. Returns ------- X_new : {ndarray, sparse matrix}, shape (n_samples_new, n_features) Synthetically generated samples. y_new : ndarray, shape (n_samples_new,) Target values for synthetic samples. """
         #獲得當前的Random_state實例
        random_state = check_random_state(self.random_state)
        #獲得一個數組,長度爲生成樣本數,每一個值是生成樣本使用的哪一個近鄰(使用一個數字存儲行列座標,要用的時候在拆出來)
        samples_indices = random_state.randint(
            low=0, high=len(nn_num.flatten()), size=n_samples)
        #步長,默認爲1,若是超過一,那生成的樣本就是在原始樣本和近鄰的延長線上
        steps = step_size * random_state.uniform(size=n_samples)
        #除近鄰個數獲得行數
        rows = np.floor_divide(samples_indices, nn_num.shape[1])
        #求近鄰個數求餘獲得列數
        cols = np.mod(samples_indices, nn_num.shape[1])
        #建立生成樣本的標籤列
        y_new = np.array([y_type] * len(samples_indices), dtype=y_dtype)
 
        #若是輸入的X是稀疏矩陣
        if sparse.issparse(X):
            #記錄行列
            row_indices, col_indices, samples = [], [], []

            for i, (row, col, step) in enumerate(zip(rows, cols, steps)):
                #若是當前樣本非空
                if X[row].nnz:
                    #生成合成樣本
                    sample = self._generate_sample(X, nn_data, nn_num,
                                                   row, col, step)
                    #記錄行列indece
                    row_indices += [i] * len(sample.indices)
                    col_indices += sample.indices.tolist()
                    #記錄樣本
                    samples += sample.data.tolist()
             #返回生成樣本構成的稀疏矩陣csr_matrix
            return (sparse.csr_matrix((samples, (row_indices, col_indices)),
                                      [len(samples_indices), X.shape[1]],
                                      dtype=X.dtype),
                    y_new)

 #若是不是稀疏矩陣
        else:
 #構造返回結果集ndarray
            X_new = np.zeros((n_samples, X.shape[1]), dtype=X.dtype)
            for i, (row, col, step) in enumerate(zip(rows, cols, steps)):
 #對當前的行列生成一個樣本,並放到第i個位置
                X_new[i] = self._generate_sample(X, nn_data, nn_num,
                                                 row, col, step)
            return X_new, y_new‘

 #生成樣本的函數
    def _generate_sample(self, X, nn_data, nn_num, row, col, step):
        r"""Generate a synthetic sample. The rule for the generation is: .. math:: \mathbf{s_{s}} = \mathbf{s_{i}} + \mathcal{u}(0, 1) \times (\mathbf{s_{i}} - \mathbf{s_{nn}}) \, where \mathbf{s_{s}} is the new synthetic samples, \mathbf{s_{i}} is the current sample, \mathbf{s_{nn}} is a randomly selected neighbors of \mathbf{s_{i}} and \mathcal{u}(0, 1) is a random number between [0, 1). Parameters ---------- X : {array-like, sparse matrix}, shape (n_samples, n_features) Points from which the points will be created. nn_data : ndarray, shape (n_samples_all, n_features) Data set carrying all the neighbours to be used. nn_num : ndarray, shape (n_samples_all, k_nearest_neighbours) The nearest neighbours of each sample in `nn_data`. row : int Index pointing at feature vector in X which will be used as a base for creating new sample. col : int Index pointing at which nearest neighbor of base feature vector will be used when creating new sample. step : float Step size for new sample. Returns ------- X_new : {ndarray, sparse matrix}, shape (n_features,) Single synthetically generated sample. """
        #X[row]爲原始樣本
        #step是隨機比值
        #nn_num[row, col]是當前所用的近鄰在全量樣本中的索引值
        #nn_data[nn_num[row, col]]爲近鄰樣本
        return X[row] - step * (X[row] - nn_data[nn_num[row, col]])

    #檢查樣本是否處於危險之中或者是噪聲
    def _in_danger_noise(self, nn_estimator, samples, target_class, y,
                         kind='danger'):
        """Estimate if a set of sample are in danger or noise. Used by BorderlineSMOTE and SVMSMOTE. Parameters ---------- nn_estimator : estimator An estimator that inherits from :class:`sklearn.neighbors.base.KNeighborsMixin` use to determine if a sample is in danger/noise. samples : {array-like, sparse matrix}, shape (n_samples, n_features) The samples to check if either they are in danger or not. target_class : int or str The target corresponding class being over-sampled. y : array-like, shape (n_samples,) The true label in order to check the neighbour labels. kind : str, optional (default='danger') The type of classification to use. Can be either: - If 'danger', check if samples are in danger, - If 'noise', check if samples are noise. Returns ------- output : ndarray, shape (n_samples,) A boolean array where True refer to samples in danger or noise. """
        #拿到目標樣本的K近鄰放進一個矩陣
        x = nn_estimator.kneighbors(samples, return_distance=False)[:, 1:]
 
        #多數類的標籤列表,多數類爲1,少數類爲0
        nn_label = (y[x] != target_class).astype(int)

        #多數類的個數
        n_maj = np.sum(nn_label, axis=1)

        #若是是求危險值
        if kind == 'danger':
            # Samples are in danger for m/2 <= m' < m
            #若是多數類的數量大於大於一半切小於K,那就是危險樣本、
            #這裏使用bitwise對條件值進行按位與,效率比循環高
            return np.bitwise_and(n_maj >= (nn_estimator.n_neighbors - 1) / 2,
                                  n_maj < nn_estimator.n_neighbors - 1)
        elif kind == 'noise':
            # Samples are noise for m = m'
            #全部的近鄰都是多數類,那就是噪聲
            return n_maj == nn_estimator.n_neighbors - 1
        else:
            raise NotImplementedError
複製代碼

源碼連接:github.com/scikit-lear…

5. Smote實際應用案例

5.1 Imbalanced_learn中的Smote參數

SMOTE(ratio='auto', random_state-None, k_neighbors=5, m_neighbors=10,
  out_step=0.5, kind="regular", svm_estimator=None, n_jobs=1)
複製代碼

ratio: 用於指定重抽樣的比例,若是指定字符型的值,能夠是’minority’,表示對少數類別的樣本進行抽樣、’majority’,表示對多數類別的樣本進行抽樣、’not minority’表示採用欠採樣方法、’all’表示採用過採樣方法,默認爲’auto’,等同於’all’和’not minority’;若是指定字典型的值,其中鍵爲各個類別標籤,值爲類別下的樣本量;

random_state: 用於指定隨機數生成器的種子,默認爲None,表示使用默認的隨機數生成器;

k_neighbors: 指定近鄰個數,默認爲5個;

m_neighbors: 指定從近鄰樣本中隨機挑選的樣本個數,默認爲10個;

kind: 用於指定SMOTE算法在生成新樣本時所使用的選項,默認爲’regular’,表示對少數類別的樣本進行隨機採樣,也能夠是’borderline1’、’borderline2’和’svm’;

svm_estimator: 用於指定SVM分類器,默認爲sklearn.svm.SVC,該參數的目的是利用支持向量機分類器生成支持向量,而後再生成新的少數類別的樣本;

n_jobs: 用於指定SMOTE算法在過採樣時所需的CPU數量,默認爲1表示僅使用1個CPU運行算法,即不使用並行運算功能。

5.2 德國電信CHURN用戶流失數據集的SMOTE應用

該數據集來源於德國某電信行業的客戶歷史交易數據,該數據集一共包含條5000記錄,17個特徵,其中標籤churn爲二元變量,yes表示客戶流失,no表示客戶未流失;

剩餘的自變量包含客戶的是否訂購國際長途套餐、語音套餐、短信條數、話費、通話次數等。接下來就利用該數據集,探究非平衡數據轉平衡後的效果。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import model_selection
from sklearn import tree
from imblearn.over_sampling import SMOTE
from sklearn.metrics import *
from sklearn.linear_model import LogisticRegression
#清洗數據
churn=pd.read_csv(r'C:\Users\Administrator\Desktop\Work\data\churn_all.txt',sep='\t')
churn=churn.drop(['Instance_ID'],axis=1)
col=['State', 'Account Length', 'Area Code', 'Phone', 'Intl Plan', 'VMail Plan', 'VMail Message', 'Day Mins', 'Day Calls', 'Day Charge', 'Eve Mins', 'Eve Calls', 'Eve Charge', 'Night Mins', 'Night Calls', 'Night Charge', 'Intl Mins', 'Intl Calls', 'Intl Charge', 'CustServ Calls', 'churn'] 
churn.columns=col
churn['churn'].value_counts()
#正負樣本比例5:1
plt.rcParams['font.sans-serif']=['Microsoft Yahei']
plt.axes(aspect='equal')
counts=churn.churn.value_counts()
plt.pie(x=counts,labels=pd.Series(counts.index))
plt.show()
#數據清洗
churn.drop(labels=['State','Area Code','Phone'],axis=1,inplace=True)
churn['Intl Plan']=churn['Intl Plan'].map({' no':0,' yes':1})
churn['VMail Plan']=churn['VMail Plan'].map({' no':0,' yes':1})
churn['churn']=churn['churn'].map({' False.':0,' True.':1})
#構建訓練集和測試集
predictors=churn.columns[:-1]
X_train,X_test,y_train,y_test=model_selection.train_test_split(churn[predictors],churn.churn,random_state=12)
#使用不平衡數據訓練lr模型,查看AUC
lr=LogisticRegression()
lr.fit(X_train,y_train)
pred=lr.predict(X_test)
roc_auc_score(y_test, pred)
#打印ROC曲線,計算AUC
fpr,tpr,threshold=metrics.roc_curve(y_test,pred)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,color='steelblue',alpha=0.5,edgecolor='black')
plt.plot(fpr,tpr,color='black',lw=1)
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.text(0.5,0.3,'ROC cur (area=%0.3f)' % roc_auc)
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.show()
#使用SMOTE進行重採樣
over_samples=SMOTE(random_state=1234)
over_samples_X,over_samples_y=over_samples.fit_sample(X_train,y_train)
#採樣先後樣本數對比
print(y_train.value_counts())
print(pd.Series(over_samples_y).value_counts())
#使用均衡數據訓練LR模型
lr2=LogisticRegression()
lr2.fit(over_samples_X,over_samples_y)
fpr,tpr,threshold=metrics.roc_curve(y_test,pred2)
roc_auc=metrics.auc(fpr,tpr)
plt.stackplot(fpr,tpr,color='steelblue',alpha=0.5,edgecolor='black')
plt.plot(fpr,tpr,color='black',lw=1)
plt.plot([0,1],[0,1],color='red',linestyle='--')
plt.text(0.5,0.3,'ORC cur (area=%0.3f)' % roc_auc)
plt.xlabel('1-Specificity')
plt.ylabel('Sensitivity')
plt.show()
複製代碼

正負樣本比例爲5:1

欠採樣前AUC:0.55

欠採樣後AUC:0.735

使用過採樣數據訓練後模型的AUC提高了20%!

參考資料

1 How to handle Imbalanced Classification Problems in machine learning?

www.analyticsvidhya.com/blog/2017/0…

2 SMOTE: Synthetic Minority Over-sampling Technique

3 數據不平衡問題——SMOTE算法賞析

blog.csdn.net/qq_33472765…

4 Imbalanced learn User Guide

imbalanced-learn.org/en/stable/u….

5 A scikit-learn-contrib to tackle learning from imbalanced data

glemaitre.github.io/talks/2018_…

最後順便發幾個招聘信息。

OPPO互聯網技術領域招聘多個崗位:

廣告後臺團隊專一於廣告投放管理、播放檢索、計費統計等廣告系統核心服務研發工做,誠邀具有分佈式系統架構設計與調優能力,對高可用/高併發系統有實踐經驗,對計算廣告有濃厚興趣的同窗加入咱們,共同建設智能廣告平臺。

簡歷投遞:chenquan#oppo.com

客戶端團隊致力於研究Android手機上應用、遊戲的商業化變現解決方案、協助應用、遊戲經過商業化SDK快速實現變現盈利,誠邀對於Android應用、遊戲商業化變現解決方案感興趣、滿三年開發經驗的Android應用開發者加入咱們、與團隊和業務一塊兒成長。

簡歷投遞:liushun#oppo.com

數據標籤團隊致力於穿透大數據來理解每一個OPPO用戶的商業興趣。數據快速拓展和深挖中,誠邀對數據分析、大數據處理、機器學習/深度學習、NLP等有兩年以上經驗的您加入咱們,與團隊和業務一同成長!

簡歷投遞:ping.wang#oppo.com

相關文章
相關標籤/搜索