摘要
SMOTE是一種綜合取樣人工合成資料演算法,用於解決資料類別不平衡問題(Imbalanced class problem),以Over-sampling少數類和Under-sampling多數類結合的方式來合成資料。本文將以Nitesh V. Chawla(2002)的論文為藍本,闡述SMOTE的核心思想以及實現其樸素演算法,在傳統分類器(貝葉斯和決策樹)上進行對比演算法效能並且討論其演算法改進的途徑。
1. 引言
類別不平衡是一種在分類器模型訓練過程中常見的問題之一,如通過大量胸透圖片來學習判斷一個人是否有癌症,又如在網路流日誌中學習檢測可能是攻擊行為的資料模式,這一類的任務中都是正常的類多於異常(診斷屬於癌症,屬於攻擊行為)的類,在類別不平衡資料下訓練出來的分類器要非常的小心,即使該分類器擁有很高的精度,因為它很可能會習得大部分的都是正常的,而我們可能需要的是它能夠最大程度的識別異常行為,哪怕精度低於前者。
為了解決這一問題,業內已經有以下5種公認的方法去擴充資料集[1],以至於類別均勻:
- 隨機的增大少數類的樣本數量。
- 隨機的增大特定少數類樣本的數量。
- 隨機的減少多數類樣本的數量。
- 隨機的減少特定多數類樣本的數量。
- 修改代價函式,使得少數類出錯的代價更高。
本文要介紹的SMOTE演算法就是一種綜合1,3方法的改進方式,它以每個樣本點的k個最近鄰樣本點為依據,隨機的選擇N個鄰近點進行差值乘上一個[0,1]範圍的閾值,從而達到合成資料的目的。這種演算法的核心是:特徵空間上鄰近的點其特徵都是相似的。它並不是在資料空間上進行取樣,而是在特徵空間中進行取樣,所以它的準確率會高於傳統的取樣方式。這也是為什麼到目前為止SMOTE以及其派生的演算法仍然是較為主流的取樣技術的原因。
如上圖所示,假設資料點A在特徵空間上有4個鄰近點,若N為2,則SMOTE會隨機選擇其中2個鄰近點B,C,分別計算A->B, A->C的距離,如圖中綠線和紅線所示,在綠線或紅線上的所有采樣點都是合理的,如點A1。為了確保資料點儘可能的多樣(不重疊),故乘上一個[0, 1]之間的隨機因子。
本文將會在第2章根據SMOTE的核心以及其虛擬碼實現該演算法,並應用在測試資料集上;第3章會使用第三方imbalanced-learn
庫中實現的SMOTE演算法進行取樣,以驗證我們實現的演算法的準確性,當然這個庫中的演算法要優於樸素的SMOTE演算法,之後我們會以決策樹和高斯貝葉斯分類器為工具,對測試原始資料、應用我們所實現的SMOTE取樣後產生的資料以及應用第三方庫SMOTE產生的資料三者分別產生的資料集進行效能比較;第4章會討論樸素SMOTE演算法更加魯棒和表現更好的優化途徑;第5章是對本文的總結。
2. 演算法分析與實現
下圖是在SMOTE論文中提出的虛擬碼,由兩個函式SMOTE(T, N, K)
和Populate(N, i, nnarray)
組成。
SMOTE
負責接受要取樣的類資料集X,返回一個經過SMOTE取樣後的資料集,大小為(N/100)*T
,函式有三個引數,分別是T: 需要處理的資料集X的樣本數量; N: 取樣比例,一般為100, 200, 300等整百數,對應即1倍,2倍,3倍;K: 為取樣的最近鄰數量,論文中預設為5
。SMOTE
程式碼思想非常簡單,掃描每一個樣本點,計算每一個樣本點的K個最近鄰,將每一個最近鄰樣本點的索引記錄在nnarray
中,之後傳入Populate(N, i, nnarray)
中即完成一個樣本點的取樣。
Populate
則負責根據nnarray
中的索引去隨機生成N
個與觀測樣本i
相似的樣本。該函式會計算隨機鄰近點nn
與觀測樣本i
點的每一個特徵之間的差距dif
,將其差距乘上一個[0, 1]隨機因子gap
,再將dif*gap
的值加上觀測點i
即完成了一個特徵的合成。
在Python中實現如下:
注:為了保證本文中所有程式碼的可復現性,設定的random_state
均為666
def NaiveSMOTE(X, N=100, K=5):
"""
{X}: minority class samples;
{N}: Amount of SMOTE; default 100;
{K} Number of nearest; default 5;
"""
# {T}: Number of minority class samples;
T = X.shape[0]
if N < 100:
T = (N/100) * T
N = 100
N = (int)(N/100)
numattrs = X.shape[1]
samples = X[:T]
neigh = NearestNeighbors(n_neighbors=K)
neigh.fit(samples)
Synthetic = np.zeros((T*N, numattrs))
newindex = 0
def Populate(N, i, nns, newindex):
"""
Function to generate the synthetic samples.
"""
for n in range(N):
nn = np.random.randint(0, K)
for attr in range(numattrs):
dif = samples[nns[nn], attr] - samples[i, attr]
gap = np.random.random()
Synthetic[newindex, attr] = samples[i, attr] + gap*dif
newindex += 1
return newindex
for i in range(T):
nns = neigh.kneighbors([samples[i]], K, return_distance=False)
newindex = Populate(N, i, nns[0], newindex)
return Synthetic
複製程式碼
這裡沒有采用矩陣運算,而是完完全全的按照論文中的方式復現(所以稱為NaiveSMOTE),其中最近鄰的計算我們使用scikit-learn
提供的NearestNeighbors
類完成。
接下來我們使用scikit-learn
中的make_classification
來生成測試分類資料集,模擬不平衡類資料,當然有興趣的讀者也可以去尋找論文中所使用的資料集。
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=500,
n_features=9,
n_redundant=3,
weights=(0.1, 0.9),
n_clusters_per_class=2,
random_state=666) # 為了可復現性
print(X.shape, y.shape) # ((500, 9), (500,))
# 檢視y的各類樣本數
print(view_y(y)) # class 0: 50 class 1: 450
複製程式碼
原資料集的分佈如下圖所示,其中紅色圓圈為正類即少數類,藍色×為負類即多數類。
將我們實現的NaiveSMOTE
應用在此測試資料上:
X_over_sampling = NaiveSMOTE(X[y==0], N=800)
print(X_over_sampling.shape) # (400, 9) 新增了400個樣本
# 將合成資料與原資料集合並
new_X = np.r_[X, X_over_sampling]
new_y = np.r_[y, np.zeros((X_over_sampling.shape[0]))]
print(new_X.shape, new_y.shape) # ((900, 9), (900,))
print(view_y(new_y)) # class 0: 450 class 1: 450
複製程式碼
接下來我們將原資料集與經過NaiveSMOTE
合成後的資料集進行比對:
可以很清晰的看見原來的類增大至一個滿意的水平,並且生成的類之間距離都相距不遠。
3. 演算法效能比對
本章我們將引入第三方庫imbalanced-learn中提供的SMOTE
類與依據論文實現的NaiveSMOTE
進行比較。兩者都是基於同一個論文的思想去實現的,只是第三方庫中實現的SMOTE
更為魯棒,並且能夠綜合考慮所有的類,是一種完全意義上的Combination of Over-sampling minority class and Under-sampling majority class
技術。因此我們引入它只為了驗證我們所復現的方法的準確性。
from imblearn.over_sampling import SMOTE
sm = SMOTE(random_state=666)
X_res, y_res = sm.fit_resample(X, y) # 即完成了合成
print(X_res.shape, y_res.shape) # ((900, 9), (900,))
複製程式碼
下圖對比imblearn
的SMOTE
與我們復現的NaiveSMOTE
生成的資料集:
能看出NaiveeSMOTE
合成的資料更加傾向於中部,而第三方的SMOTE
能夠綜合考慮全域性情況下方區域生成的資料要比NaiveSMOTE
的多。
接下來我們使用DecisionTree
和GaussianNaive
來驗證3個資料集(原資料集、NaiveSMOTE合成的資料集和第三方SMOTE合成的資料集)的ROC曲線,具體程式碼見附錄中的Notebook
檔案
原資料的ROC曲線
NaiveSMOTE生成的資料的ROC曲線
第三方SMOTE生成的資料的ROC曲線
可以看出NaiveSMOTE
與imblearn
的SMOTE
生成的資料的AUC
面積均大於原始資料的面積。imblearn
的SMOTE
生成的資料在GaussianNaiveBayes
分類器上的表現要好於NaiveSMOTE
所生成的資料訓練出來的分類器。
4. 演算法改進
這部分我們從NaiveSMOTE
的三個方面進行優化討論:
-
處理速度。
NaiveSMOTE
中有許多處都可以改成使用矩陣運算的方式,這樣會提高資料處理的速度。並且Populate
函式也顯得非常冗餘,可以用矩陣運算將其改寫。 -
全域性合理性。 全域性合理性包括兩個方面:合成資料比率的合理性和合成資料在全域性的合理性。
合成資料比率的合理性:在
NaiveSMOTE
中可以知道樣本的數量有N
合成比率來控制,只能合成其整數倍,本文中使用的資料集恰好是1:9
,只要合成原始資料的8倍即可是兩類都到達一個相對數量同等的水平,但是在現實資料集中大部分都不具備成倍的數量關係,因此可以考慮更換一個更好的生成比率,使得每個類均能處於相對數量近似的水平,避免出現合成後的原少數類變多數類的情況。合成資料在全域性的合理性:回想在
NaiveSMOTE
與imblearn SMOTE
各自合成的資料對比中可以發現,NaiveSMOTE
更加容易使得合成的資料聚集在某一樣本點附近,而imblearn SMOTE
所合成的資料更為稀疏且分佈均勻,更加接近原始資料的概率分佈。其原因在於NaiveSMOTE
在進行合成時只考慮原始的資料樣本,沒不考慮合成後的資料樣本會如何影響全域性資料。可以考慮在每次合成資料後將其加入資料集,在處理過程中將合成資料也加入考慮範圍。 -
魯棒性。 不難發現
NaiveSMOTE
僅能夠處理數值型的資料並且其距離計算公式很有可能產生誤解。在現實中有許多非數值型的資料,如性別
,職業
等等。當然可以將其全部重新編碼成可以應用數值處理的資料,如將性別進行OneHot
編碼,但是此時的距離計算公式就會出現誤解,可以考慮更換為歐氏距離、曼哈頓距離或者馬氏距離等。
Note:在對性別進行OneHot
編碼時情況如下:
男性: 0 1
女性: 1 0
複製程式碼
如果按照NaiveSMOTE
原始的距離計算公式,很有可能會將其理解為男性和女性的差距為1,因此產生誤解。
5. 結論
本文對三種資料進行對比,經過NaiveSMOTE
和imblearn SMOTE
合成後的資料在傳統分類器上的表現均好於原始資料(即不做任何修改),且imblearn SMOTE
在魯棒性上要高於NaiveSMOTE
。討論NaiveSMOTE
的不足與其可能的優化方向。建議在實際應用中優先考慮魯棒性更高的imlearn SMOTE
而不是自己造輪子,imblearn SMOTE
的實現更加符合主流標準。但不能因此就忽略了NaiveSMOTE
的意義,任何的優化有必要要基於原有的基礎。理解NaiveSMOTE
才能去更好的使用和優化它。