1. 程式人生 > >Imblearn package study(不平衡資料處理之過取樣、下采樣、綜合取樣)

Imblearn package study(不平衡資料處理之過取樣、下采樣、綜合取樣)

Imblearn package study

1. 準備知識

Sparse input

For sparse input the data is converted to the Compressed Sparse Rows representation (see scipy.sparse.csr_matrix) before being fed to the sampler. To avoid unnecessary memory copies, it is recommended to choose the CSR representation upstream.

1.1 Compressed Sparse Rows(CSR) 壓縮稀疏的行

稀疏矩陣中存在許多0元素, 按矩陣A進行儲存會佔用很大的空間(記憶體).

CSR方法採取按行壓縮的辦法, 將原始的矩陣用三個陣列進行表示:

data = np.array([1, 2, 3, 4, 5, 6])
indices = np.array([0, 2, 2, 0, 1, 2])
indptr = np.array([0, 2, 3, 6])

data陣列: 儲存著矩陣A中所有的非零元素;

indices陣列: data陣列中的元素在矩陣A中的列索引

indptr陣列: 儲存著矩陣A中每行第一個非零元素在data

陣列中的索引.

from scipy import sparse
mtx = sparse.csr_matrix((data,indices,indptr),shape=(3,3))
mtx.todense()

Out[27]: 
matrix([[1, 0, 2],
        [0, 0, 3],
        [4, 5, 6]])

為什麼會有針對不平衡資料的研究? 當我們的樣本資料中, 正負樣本的資料佔比極其不均衡的時候, 模型的效果就會偏向於多數類的結果. 具體的可參照官網利用支援向量機進行視覺化不同正負樣本比例情況下的模型分類結果.

2. 過取樣(Over-sampling)

2.1 實用性的例子

2.1.1 樸素隨機過取樣

針對不平衡資料, 最簡單的一種方法就是生成少數類的樣本, 這其中最基本的一種方法就是: 從少數類的樣本中進行隨機取樣來增加新的樣本, RandomOverSampler 函式就能實現上述的功能.

from sklearn.datasets import make_classification
from collections import Counter
X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
                           n_redundant=0, n_repeated=0, n_classes=3,
                           n_clusters_per_class=1,
                           weights=[0.01, 0.05, 0.94],
                           class_sep=0.8, random_state=0)
Counter(y)
Out[10]: Counter({0: 64, 1: 262, 2: 4674})

from imblearn.over_sampling import RandomOverSampler

ros = RandomOverSampler(random_state=0)
X_resampled, y_resampled = ros.fit_sample(X, y)


sorted(Counter(y_resampled).items())
Out[13]:
[(0, 4674), (1, 4674), (2, 4674)]

以上就是通過簡單的隨機取樣少數類的樣本, 使得每類樣本的比例為1:1:1.

2.1.2 從隨機過取樣到SMOTEADASYN

相對於取樣隨機的方法進行過取樣, 還有兩種比較流行的取樣少數類的方法: (i) Synthetic Minority Oversampling Technique (SMOTE); (ii) Adaptive Synthetic (ADASYN) .

SMOTE: 對於少數類樣本a, 隨機選擇一個最近鄰的樣本b, 然後從a與b的連線上隨機選取一個點c作為新的少數類樣本;

ADASYN: 關注的是在那些基於K最近鄰分類器被錯誤分類的原始樣本附近生成新的少數類樣本

from imblearn.over_sampling import SMOTE, ADASYN

X_resampled_smote, y_resampled_smote = SMOTE().fit_sample(X, y)

sorted(Counter(y_resampled_smote).items())
Out[29]:
[(0, 4674), (1, 4674), (2, 4674)]

X_resampled_adasyn, y_resampled_adasyn = ADASYN().fit_sample(X, y)

sorted(Counter(y_resampled_adasyn).items())
Out[30]:
[(0, 4674), (1, 4674), (2, 4674)]

2.1.3 SMOTE的變體

相對於基本的SMOTE演算法, 關注的是所有的少數類樣本, 這些情況可能會導致產生次優的決策函式, 因此SMOTE就產生了一些變體: 這些方法關注在最優化決策函式邊界的一些少數類樣本, 然後在最近鄰類的相反方向生成樣本.

SMOTE函式中的kind引數控制了選擇哪種變體, (i) borderline1, (ii) borderline2, (iii) svm:

from imblearn.over_sampling import SMOTE, ADASYN
X_resampled, y_resampled = SMOTE(kind='borderline1').fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[31]:
[(0, 4674), (1, 4674), (2, 4674)]

2.1.4 數學公式

SMOTE演算法與ADASYN都是基於同樣的演算法來合成新的少數類樣本: 對於少數類樣本a, 從它的最近鄰中選擇一個樣本b, 然後在兩點的連線上隨機生成一個新的少數類樣本, 不同的是對於少數類樣本的選擇.

原始的SMOTE: kind='regular' , 隨機選取少數類的樣本.

The borderline SMOTE: kind='borderline1' or kind='borderline2'

此時, 少數類的樣本分為三類: (i) 噪音樣本(noise), 該少數類的所有最近鄰樣本都來自於不同於樣本a的其他類別; (ii) 危險樣本(in danger), 至少一半的最近鄰樣本來自於同一類(不同於a的類別); (iii) 安全樣本(safe), 所有的最近鄰樣本都來自於同一個類.

這兩種型別的SMOTE使用的是危險樣本來生成新的樣本資料, 對於 Borderline-1 SMOTE, 最近鄰中的隨機樣本b與該少數類樣本a來自於不同的類; 不同的是, 對於 Borderline-2 SMOTE , 隨機樣本b可以是屬於任何一個類的樣本;

SVM SMOTE: kind='svm', 使用支援向量機分類器產生支援向量然後再生成新的少數類樣本.

3. 下采樣(Under-sampling)

3.1 原型生成(prototype generation)

給定資料集S, 原型生成演算法將生成一個子集S, 其中|S| < |S|, 但是子集並非來自於原始資料集. 意思就是說: 原型生成方法將減少資料集的樣本數量, 剩下的樣本是由原始資料集生成的, 而不是直接來源於原始資料集.

ClusterCentroids函式實現了上述功能: 每一個類別的樣本都會用K-Means演算法的中心點來進行合成, 而不是隨機從原始樣本進行抽取.

from imblearn.under_sampling import ClusterCentroids

cc = ClusterCentroids(random_state=0)
X_resampled, y_resampled = cc.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[32]:
[(0, 64), (1, 64), (2, 64)]

ClusterCentroids函式提供了一種很高效的方法來減少樣本的數量, 但需要注意的是, 該方法要求原始資料集最好能聚類成簇. 此外, 中心點的數量應該設定好, 這樣下采樣的簇能很好地代表原始資料.

3.2 原型選擇(prototype selection)

與原型生成不同的是, 原型選擇演算法是直接從原始資料集中進行抽取. 抽取的方法大概可以分為兩類: (i) 可控的下采樣技術(the controlled under-sampling techniques) ; (ii) the cleaning under-sampling techniques(不好翻譯, 就放原文, 清洗的下采樣技術?). 第一類的方法可以由使用者指定下采樣抽取的子集中樣本的數量; 第二類方法則不接受這種使用者的干預.

3.2.1 Controlled under-sampling techniques

RandomUnderSampler函式是一種快速並十分簡單的方式來平衡各個類別的資料: 隨機選取資料的子集.

from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=0)
X_resampled, y_resampled = rus.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[33]:
[(0, 64), (1, 64), (2, 64)]

通過設定RandomUnderSampler中的replacement=True引數, 可以實現自助法(boostrap)抽樣.

import numpy as np

np.vstack({tuple(row) for row in X_resampled}).shape
Out[34]:
(192L, 2L)

很明顯, 使用預設引數的時候, 採用的是不重複取樣;

from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=0, replacement=True)
X_resampled, y_resampled = rus.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[33]:
[(0, 64), (1, 64), (2, 64)]

np.vstack({tuple(row) for row in X_resampled}).shape
Out[34]:
(181L, 2L)

NearMiss函式則添加了一些啟發式(heuristic)的規則來選擇樣本, 通過設定version引數來實現三種啟發式的規則.

from imblearn.under_sampling import NearMiss
nm1 = NearMiss(random_state=0, version=1)
X_resampled_nm1, y_resampled = nm1.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[35]:
[(0, 64), (1, 64), (2, 64)]

下面通過一個例子來說明這三個啟發式的選擇樣本的規則, 首先我們假設正樣本是需要下采樣的(多數類樣本), 負樣本是少數類的樣本.

NearMiss-1: 選擇離N個近鄰的負樣本的平均距離最小的正樣本;

NearMiss-2: 選擇離N個負樣本最遠的平均距離最小的正樣本;

NearMiss-3: 是一個兩段式的演算法. 首先, 對於每一個負樣本, 保留它們的M個近鄰樣本; 接著, 那些到N個近鄰樣本平均距離最大的正樣本將被選擇.

3.2.2 Cleaning under-sampling techniques

TomekLinks : 樣本x與樣本y來自於不同的類別, 滿足以下條件, 它們之間被稱之為TomekLinks; 不存在另外一個樣本z, 使得d(x,z) < d(x,y) 或者 d(y,z) < d(x,y)成立. 其中d(.)表示兩個樣本之間的距離, 也就是說兩個樣本之間互為近鄰關係. 這個時候, 樣本x或樣本y很有可能是噪聲資料, 或者兩個樣本在邊界的位置附近.

TomekLinks函式中的auto引數控制Tomek’s links中的哪些樣本被剔除. 預設的ratio='auto' 移除多數類的樣本, 當ratio='all'時, 兩個樣本均被移除.

3.2.2.2 Edited data set using nearest neighbours

EditedNearestNeighbours這種方法應用最近鄰演算法來編輯(edit)資料集, 找出那些與鄰居不太友好的樣本然後移除. 對於每一個要進行下采樣的樣本, 那些不滿足一些準則的樣本將會被移除; 他們的絕大多數(kind_sel='mode')或者全部(kind_sel='all')的近鄰樣本都屬於同一個類, 這些樣本會被保留在資料集中.

print sorted(Counter(y).items())

from imblearn.under_sampling import EditedNearestNeighbours
enn = EditedNearestNeighbours(random_state=0)
X_resampled, y_resampled = enn.fit_sample(X, y)

print sorted(Counter(y_resampled).items())

Out[36]:
[(0, 64), (1, 262), (2, 4674)]
[(0, 64), (1, 213), (2, 4568)]

在此基礎上, 延伸出了RepeatedEditedNearestNeighbours演算法, 重複基礎的EditedNearestNeighbours演算法多次.

from imblearn.under_sampling import RepeatedEditedNearestNeighbours
renn = RepeatedEditedNearestNeighbours(random_state=0)
X_resampled, y_resampled = renn.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[37]:
[(0, 64), (1, 208), (2, 4551)]

RepeatedEditedNearestNeighbours演算法不同的是, ALLKNN演算法在進行每次迭代的時候, 最近鄰的數量都在增加.

from imblearn.under_sampling import AllKNN
allknn = AllKNN(random_state=0)
X_resampled, y_resampled = allknn.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[38]:
[(0, 64), (1, 220), (2, 4601)]

3.2.2.3 Condensed nearest neighbors and derived algorithms

CondensedNearestNeighbour 使用1近鄰的方法來進行迭代, 來判斷一個樣本是應該保留還是剔除, 具體的實現步驟如下:

  1. 集合C: 所有的少數類樣本;
  2. 選擇一個多數類樣本(需要下采樣)加入集合C, 其他的這類樣本放入集合S;
  3. 使用集合S訓練一個1-NN的分類器, 對集合S中的樣本進行分類;
  4. 將集合S中錯分的樣本加入集合C;
  5. 重複上述過程, 直到沒有樣本再加入到集合C.
from imblearn.under_sampling import CondensedNearestNeighbour
cnn = CondensedNearestNeighbour(random_state=0)
X_resampled, y_resampled = cnn.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[39]:
[(0, 64), (1, 24), (2, 115)]

顯然, CondensedNearestNeighbour方法對噪音資料是很敏感的, 也容易加入噪音資料到集合C中.

from imblearn.under_sampling import OneSidedSelection
oss = OneSidedSelection(random_state=0)
X_resampled, y_resampled = oss.fit_sample(X, y)

print(sorted(Counter(y_resampled).items()))
Out[39]:
[(0, 64), (1, 174), (2, 4403)]

NeighbourhoodCleaningRule 演算法主要關注如何清洗資料而不是篩選(considering)他們. 因此, 該演算法將使用

from imblearn.under_sampling import NeighbourhoodCleaningRule
ncr = NeighbourhoodCleaningRule(random_state=0)
X_resampled, y_resampled = ncr.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[39]:
[(0, 64), (1, 234), (2, 4666)]

3.2.2.4 Instance hardness threshold

InstanceHardnessThreshold是一種很特殊的方法, 是在資料上運用一種分類器, 然後將概率低於閾值的樣本剔除掉.

from sklearn.linear_model import LogisticRegression
from imblearn.under_sampling import InstanceHardnessThreshold
iht = InstanceHardnessThreshold(random_state=0,
                                estimator=LogisticRegression())
X_resampled, y_resampled = iht.fit_sample(X, y)

print(sorted(Counter(y_resampled).items()))
Out[39]:
[(0, 64), (1, 64), (2, 64)]

4. 過取樣與下采樣的結合

在之前的SMOTE方法中, 當由邊界的樣本與其他樣本進行過取樣差值時, 很容易生成一些噪音資料. 因此, 在過取樣之後需要對樣本進行清洗. 這樣, 第三節中涉及到的TomekLinkEditedNearestNeighbours方法就能實現上述的要求. 所以就有了兩種結合過取樣與下采樣的方法: (i) SMOTETomek and (ii) SMOTEENN.

from imblearn.combine import SMOTEENN
smote_enn = SMOTEENN(random_state=0)
X_resampled, y_resampled = smote_enn.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[40]:
[(0, 4060), (1, 4381), (2, 3502)]


from imblearn.combine import SMOTETomek
smote_tomek = SMOTETomek(random_state=0)
X_resampled, y_resampled = smote_tomek.fit_sample(X, y)

print sorted(Counter(y_resampled).items())
Out[40]:
[(0, 4499), (1, 4566), (2, 4413)]

5. Ensemble的例子

5.1 例子

一個不均衡的資料集能夠通過多個均衡的子集來實現均衡, imblearn.ensemble模組能實現上述功能.

EasyEnsemble 通過對原始的資料集進行隨機下采樣實現對資料集進行整合.

from imblearn.ensemble import EasyEnsemble
ee = EasyEnsemble(random_state=0, n_subsets=10)
X_resampled, y_resampled = ee.fit_sample(X, y)

print X_resampled.shape
print sorted(Counter(y_resampled[0]).items())
Out[40]:
(10L, 192L, 2L)
[(0, 64), (1, 64), (2, 64)]

EasyEnsemble 有兩個很重要的引數: (i) n_subsets 控制的是子集的個數 and (ii) replacement 決定是有放回還是無放回的隨機取樣.

與上述方法不同的是, BalanceCascade(級聯平衡)的方法通過使用分類器(estimator引數)來確保那些被錯分類的樣本在下一次進行子集選取的時候也能被取樣到. 同樣, n_max_subset 引數控制子集的個數, 以及可以通過設定bootstrap=True來使用bootstraping(自助法).

from imblearn.ensemble import BalanceCascade
from sklearn.linear_model import LogisticRegression
bc = BalanceCascade(random_state=0,
                    estimator=LogisticRegression(random_state=0),
                    n_max_subset=4)
X_resampled, y_resampled = bc.fit_sample(X, y)

print X_resampled.shape

print sorted(Counter(y_resampled[0]).items())
Out[41]:
(4L, 192L, 2L)
[(0, 64), (1, 64), (2, 64)]

5.2 Chaining ensemble of samplers and estimators

在整合分類器中, 裝袋方法(Bagging)在不同的隨機選取的資料集上建立了多個估計量. 在scikit-learn中這個分類器叫做BaggingClassifier. 然而, 該分類器並不允許對每個資料集進行均衡. 因此, 在對不均衡樣本進行訓練的時候, 分類器其實是有偏的, 偏向於多數類.

from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
bc = BaggingClassifier(base_estimator=DecisionTreeClassifier(),
                       random_state=0)
bc.fit(X_train, y_train) 

y_pred = bc.predict(X_test)
confusion_matrix(y_test, y_pred)
Out[35]:
array([[   0,    0,   12],
       [   0,    0,   59],
       [   0,    0, 1179]], dtype=int64)

BalancedBaggingClassifier 允許在訓練每個基學習器之前對每個子集進行重抽樣. 簡而言之, 該方法結合了EasyEnsemble 取樣器與分類器(如BaggingClassifier)的結果.

from imblearn.ensemble import BalancedBaggingClassifier
bbc = BalancedBaggingClassifier(base_estimator=DecisionTreeClassifier(),
                                ratio='auto',
                                replacement=False,
                                random_state=0)
bbc.fit(X, y) 

y_pred = bbc.predict(X_test)
confusion_matrix(y_test, y_pred)
Out[39]:
array([[  12,    0,    0],
       [   0,   55,    4],
       [  68,   53, 1058]], dtype=int64)

6. 資料載入

imblearn.datasets 包與sklearn.datasets 包形成了很好的互補. 該包主要有以下兩個功能: (i)提供一系列的不平衡資料集來實現測試; (ii) 提供一種工具將原始的平衡資料轉換為不平衡資料.

6.1 不平衡資料集

from collections import Counter
from imblearn.datasets import fetch_datasets
ecoli = fetch_datasets()['ecoli']
ecoli.data.shape

print sorted(Counter(ecoli.target).items())
Out[40]:
[(-1, 301), (1, 35)]

6.2 生成不平衡資料

make_imbalance 方法可以使得原始的資料集變為不平衡的資料集, 主要是通過ratio引數進行控制.

from sklearn.datasets import load_iris
from imblearn.datasets import make_imbalance
iris = load_iris()
ratio = {0: 20, 1: 30, 2: 40}
X_imb, y_imb = make_imbalance(iris.data, iris.target, ratio=ratio)

sorted(Counter(y_imb).items())
Out[37]:
[(0, 20), (1, 30), (2, 40)]

#當類別不指定時, 所有的資料集均匯入
ratio = {0: 10}
X_imb, y_imb = make_imbalance(iris.data, iris.target, ratio=ratio)

sorted(Counter(y_imb).items())
Out[38]:
[(0, 10), (1, 50), (2, 50)]

#同樣亦可以傳入自定義的比例函式
def ratio_multiplier(y):
    multiplier = {0: 0.5, 1: 0.7, 2: 0.95}
    target_stats = Counter(y)
    for key, value in target_stats.items():
        target_stats[key] = int(value * multiplier[key])
    return target_stats
X_imb, y_imb = make_imbalance(iris.data, iris.target,
                              ratio=ratio_multiplier)

sorted(Counter(y_imb).items())
Out[39]:
[(0, 25), (1, 35), (2, 47)]

以上就是在研究不平衡(不均衡)資料時所查詢的一些資料, 內容更多的是來自於Imblearn包的官方使用者手冊, 主要涉及一些下采樣、過取樣的方法與技術.

參考資料