1. 程式人生 > 其它 >譯文 | 在使用過取樣或欠取樣處理類別不均衡資料後,如何正確做交叉驗證?

譯文 | 在使用過取樣或欠取樣處理類別不均衡資料後,如何正確做交叉驗證?

最近讀的一篇英文部落格,講的很不錯,於是便抽空翻譯成了中文。

[關於我在這篇文章中使用的術語可以在 Physionet (http://www.physionet.org/pn6/tpehgdb/)網站中找到。 本篇部落格中用到的程式碼可以在 github(https://github.com/marcoalt/Physionet-EHG-imbalanced-data)中找到]

幾個星期前我閱讀了一篇交叉驗證的技術文件(Cross Validation Done Wrong)(http://www.alfredo.motta.name/cross-validation-done-wrong/), 在交叉驗證的過程中,我們希望能夠了解到我們的模型的泛化效能,以及它是如何預測我們感興趣的未知樣本的。基於這個出發點,作者提出了很多好的觀點(尤其是關於特徵選擇的)。我們的確經常在進行交叉驗證之前進行特徵選擇,但是需要注意的是我們在特徵選擇的時候,不能將驗證集的資料加入到特徵選擇這個環節中去。

但是,這篇文章並沒有涉及到我們在實際應用經常出現的問題。例如,如何在不均衡的資料上合理的進行交叉驗證。在醫療領域,我們所擁有的資料集一般只包含兩種類別的資料, 正常 樣本和 相關 樣本。譬如說在癌症檢查的應用我們可能只有很小一部分病人患上了癌症(相關樣本)而其餘的大部分樣本都是健康的個體。就算不在醫療領域,這種情況也存在(甚至更多),比如欺詐識別,它們的資料集中的相關樣本和正常樣本的比例都有可能會是 1:100000。

手頭的問題

因為分類器對資料中類別佔比較大的資料比較敏感,而對佔比較小的資料則沒那麼敏感,所以我們需要在交叉驗證之前對不均衡資料進行預處理。所以如果我們不處理類別不均衡的資料,分類器的輸出結果就會存在偏差,也就是在預測過程中大多數情況下都會給出偏向於某個類別的結果,這個類別是訓練的時候佔比較大的那個類別。這個問題並不是我的研究領域,但是自從我在做早產預測的工作的時候(https://medium.com/40-weeks/37-772d7f519f9)經常會遇到這種問題。早產是指短於 37 周的妊娠,大部分歐洲國家的早產率約佔 6-7%,美國的早產率為 11%,因此我們可以看到資料是非常不均衡的。

我最近無意中發現兩篇關於早產預測的文章,他們是使用 Electrohysterography (EHG)資料來做預測的。作者只使用了一個單獨的 EHG 橫截面資料(通過捕獲子宮電活動獲得)訓練出來的模型就聲稱在預測早產的時候具備很高的精度( [2], 對比沒有使用過取樣時的 AUC = 0.52-0.60,他的模型的 AUC 可以達到 0.99 ).

這個結果給我們的感覺像是 過擬合和錯誤的交叉驗證 所造成的,在我解釋原因之前,讓我們先來觀看下面的資料:

這四張密度圖表示的是他所用到的四個特徵的在兩個類別上的分佈,這兩個類別為正常分娩與早產(f = false,表示正常分娩,使用紅色的線表示;t = true, 則表示為早產,用藍色的線表示)。我們從圖中可以看到這四個特徵並沒有很強的區分兩個類別的能力。他所提取出來的特徵在兩個特徵上的分佈基本上就是重疊的。我們可以認為這是一個無用輸入,無用輸出的例子,而不是說這個模型缺少資料。

只要稍微思考一下該問題所在的領域,我們就會對 auc=0.99 這個結果提出質疑。因為區分正常分娩和早產沒有一個很明確的區分。假設我們設定 37 周就為正常的分娩時間。 那麼如果你在第 36 周後的第 6 天分娩,那麼我們則標記為早產。反之,如果在 37 周後 1 天妊娠,我們則標記為在正常的妊娠期內。 很明顯,這兩種情況下區分早產和正常分娩是沒有意義的,37 周只是一個慣例,因此,預測結果會大受影響並且對於分娩時間在 37 周左右的樣本,結果會非常不精確。

在這裡可以下載到所使用的資料集。在這篇文章中我會重複的展示資料集中的一部分特點,並且展示我們在過取樣的情況下該如何進行合適的交叉驗證。希望我在這個問題上所提出的一些矯正方案能夠在未來讓我們避免再犯這樣的錯誤。

資料集、特徵、效能評估和交叉驗證技術

資料集

我們使用的資料來自於盧布林雅那醫學中心大學婦產科,資料中涵蓋了從1997 年到 2005 年斯洛維尼亞地區的妊娠記錄。他包含了從正常懷孕的 EHG 截面資料。 這個資料是非常不均衡的,因為 300 個記錄中只有 38 條才是早孕。 更加詳細的資訊可以在 [3] 中找到。簡單來說,我們選擇 EHG 截面的理由是因為 EHG 測量的是子宮的電活動圖,而這個活動圖在懷孕期間會不斷的變化,直到導致子宮收縮分娩出孩子。因此,研究推斷非侵入性情況下監測懷孕活動可以儘早的發現哪些孕婦會早產。

特徵與分類器

在 Physionet 上,你可以找到所有關於該研究的原始資料,但是為了讓下面的實驗不那麼複雜,我們用到的是作者提供的另外一份資料來進行分析,這份資料中包含的特徵是從原始資料中篩選出來的,篩選的條件是根據特徵與 EHG 活動之間的相關頻率。我們有四個特徵(EHG訊號的均方根,中值頻率,頻率峰值和樣本熵,這裡(http://physionet.mit.edu/pn6/tpehgdb/tpehgdb.pdf) 有關如何計算這些特徵值的更多資訊)。據收集資料集的研究人員所說,大部分有價值的資訊都是來自於渠道 3,因此我將使用從渠道 3 預提取出來的特徵。詳細的資料集也在 github (https://github.com/marcoalt/Physionet-EHG-imbalanced-data)可以找到。因為我們是要訓練分類器分類器,所以我使用了一些常見的訓練分類器的演算法:邏輯迴歸、分類樹、SVM 和隨機森林。在部落格中我不會做任何特徵選擇,而是將所有的資料都用來訓練模型。

評測指標

在這裡我們使用 召回率 , 真假率 和 AUC 作為評測指標,關於指標的含義可以檢視 wikipedia(https://en.wikipedia.org/wiki/Sensitivity_and_specificity)

交叉驗證

我決定使用 留一法 來做交叉驗證。這種技術在使用資料集時或者當欠取樣時不會有任何錯誤的餘地。但是,當過取樣時,情況又會有點不一樣,所以讓我們看下面的分析。

類別不均衡的資料

當我們遇到資料不均衡的時候,我們該如何做:

  • 忽略這個問題
  • 對佔比較大的類別進行欠取樣
  • 對佔比較小的類別進行過取樣

忽略這個問題

如果我們使用不均衡的資料來訓練分類器,那麼訓練出來的分類器在預測資料的時候總會返回資料集中佔比最大的資料所對應的類別作為結果。這樣的分類器具備太大的偏差,下面是訓練這樣的分類器所對應的程式碼:

#leave one participant out cross-validation
results_lr <- rep(NA, nrow(data_to_use))
results_tree <- rep(NA, nrow(data_to_use))
results_svm <- rep(NA, nrow(data_to_use))
results_rf <- rep(NA, nrow(data_to_use))
 for(index_subj  in 1:nrow(data_to_use))
{   
#remove subject to validate   training_data <- data_to_use[-index_subj, ]   
training_data_formula <- training_data[, c("preterm", features)]    
#select features in the validation set   
validation_data <- data_to_use[index_subj, features]    
#logistic regression   
glm.fit <- glm(preterm ~.,                  
data = training_data_formula,                  
family = binomial)   
glm.probs <- predict(glm.fit, validation_data, type = "response")
  predictions_lr <- ifelse(glm.probs < 0.5, "t", "f") 
results_lr[index_subj] <- predictions_lr    
#classification tree   tree.fit <- tree(preterm ~.,                    
data = training_data_formula)   
predictions_tree <- predict(tree.fit, validation_data, type = "class")   
results_tree[index_subj] <- predictions_tree    
#svm   svm <- svm(preterm ~.,              
data = training_data_formula   )   
predictions_svm <- predict(svm, validation_data)   
results_svm[index_subj] <- predictions_svm    
#random forest      
 rf <- randomForest(preterm ~.,                      
data = training_data_formula)   
predictions_rf <- predict(rf, validation_data)   
results_rf[index_subj] <- predictions_rf    }

從上面的程式碼可以看出,在每次迭代中,我只需選擇 index_subj 下標所對應的資料作為驗證集,然後使用剩餘的資料(即訓練資料)構建模型。結果如下圖所示

如預期的那樣,分類器的偏差太大,召回率為零或非常接近零,而真假率為1或非常接近於1,即所有或幾乎所有記錄被檢測為會正常分娩,因此基本沒有識別出早產的記錄。下面的實驗則使用了欠取樣的方法。

對大類樣本進行欠取樣

處理類別不平衡資料的最常見和最簡單的策略之一是對大類樣本進行欠取樣。 儘管過去也有很多關於解決資料不均衡的辦法(例如,對具體樣本進行欠取樣,例如“遠離決策邊界”的方法)[4],但那些方法都不能改進在簡單隨機選擇樣本的情況下有任何效能上的提升。 因此,我們的實驗將從佔比較大的類別下的樣本中隨機選擇 n 個樣本,其中 n 的值等於佔比較小的類別下的樣本的總數,並在訓練階段使用它們,然後在驗證中排除掉這些樣本。

程式碼如下:

#leave one participant out cross-validation
results_lr <- rep(NA, nrow(data_to_use))
results_tree <- rep(NA, nrow(data_to_use))
results_svm <- rep(NA, nrow(data_to_use))
results_rf <- rep(NA, nrow(data_to_use))
rows_preterm <- sum(data_to_use$preterm == " t         ") #weird string, haven't changed it for now for(index_subj  in 1:nrow(data_to_use))
{   
#remove subject to validate   
training_data <- data_to_use[-index_subj, ]   
training_data_preterm <- training_data[training_data$preterm == " t         ", ]   
training_data_term <- training_data[training_data$preterm == " f         ", ]    
#get subsample to balance dataset   
indices <- sample(nrow(training_data_term), rows_preterm)   
training_data_term <- training_data_term[indices, ]   
training_data <- rbind(training_data_preterm, training_data_term)    
#select features in the training set   
training_data_formula <- training_data[, c("preterm", features)]
 #select features in the validation set   
validation_data <- data_to_use[index_subj, features]    
#logistic regression   glm.fit <- glm(preterm ~.,                  
data = training_data_formula,                  
family = binomial)   
glm.probs <- predict(glm.fit, validation_data, type = "response")   
predictions_lr <- ifelse(glm.probs < 0.5, "t", "f")   results_lr[index_subj] <- predictions_lr    #classification tree   
tree.fit <- tree(preterm ~.,                    
data = training_data_formula)   
predictions_tree <- predict(tree.fit, validation_data, type = "class")
results_tree[index_subj] <- predictions_tree    #svm   svm <- svm(preterm ~.,                     
data = training_data_formula   )   
predictions_svm <- predict(svm, validation_data)   results_svm[index_subj] <- predictions_svm    #random forest         
rf <- randomForest(preterm ~.,                                      
data = training_data_formula,                                      
sampsize = c(nrow(training_data_preterm), nrow(training_data_preterm)))   
predictions_rf <- predict(rf, validation_data)   

results_rf[index_subj] <- predictions_rf }

如上所述,上面的程式碼與之前最大的不同的是在每次迭代的時候,我們從佔比較大的類別下的樣本中選取了 n ,然後使用這個 n 個樣本和佔比類別較小的樣本組成了訓練集來訓練我們的分類器。結果如下圖所示:

通過欠取樣,我們解決了資料類別不均衡的問題,並且提高了模型的召回率,但是,模型的表現並不是很好。其中一個原因可能是因為我們用來訓練模型的資料過少。一般來說,如果我們的資料集中的類別越不均衡,那麼我們在欠取樣中拋棄的資料就會越多,那麼就意味著我們可能拋棄了一些潛在的並且有用的資訊。現在我們應該這樣問我們自己,我們是否訓練了一個弱的分類器,而原因是因為我們沒有太多的資料?還是說我們依賴了不好的特徵,所以就算資料再多對模型也沒有幫助?

對少數類樣本過取樣

如果我們在 交叉驗證 之前進行過取樣會導致 過擬合 的問題。那麼產生這個問題的原因是什麼呢?讓我們來看下面的一個關於過取樣的簡單例項。

最簡單的過取樣方式就是對佔比類別較小下的樣本進行重新取樣,譬如說建立這些樣本的副本,或者手動製造一些相同的資料。現在,如果我們在交叉驗證之前做了過取樣,然後使用留一法做交叉驗證,也就是說我們在每次迭代中使用 N-1 份樣本做訓練,而只使用 1 份樣本驗證。 但是我們注意到在其實在 N-1 份的樣本中是包含了那一份用來做驗證的樣本的。所以這樣做交叉驗證完全違背了初衷。 讓我們用圖形化的方式來更好的審視這個問題。

最左邊那列表示的是原始的資料,裡面包含了少數類下的兩個樣本。我們拷貝這兩個樣本作為副本,然後再進行交叉驗證。在迭代的過程,我們的訓練樣本和驗證樣本會包含相同的資料,如最右那張圖所示,這種情況下會導致過擬合或誤導的結果,合適的做法應該如下圖所示。

也就是說我們每次迭代做交叉驗證之前先將驗證樣本從訓練樣本中分離出來,然後再對訓練樣本中少數類樣本進行過取樣(橙色那塊圖所示)。在這個示例中少數類樣本只有兩個,所以我拷貝了三份副本。這種做法與之前最大的不同就是訓練樣本和驗證樣本是沒有交集的。因為我們獲得一個比之前好的結果。即使我們使用其他的交叉驗證方法,譬如 k-flod ,做法也是一樣的。

這是一個簡單的例子,當然我們也可以使用更加好的方法來做過取樣。其中一種使用的過取樣方法叫做 SMOTE 方法,SMOTE 方法並不是採取簡單複製樣本的策略來增加少數類樣本, 而是通過分析少數類樣本來建立新的樣本 的同時對多數類樣本進行欠取樣。正常來說當我們簡單複製樣本的時候,訓練出來的分類器在預測這些複製樣本時會很有信心的將他們識別出來,你為他知道這些複製樣本的所有邊界和特點,而不是以概括的角度來刻畫這些少數類樣本。但是,SMOTE 可以有效的強制讓分類的邊界更加的泛化,一定程度上解決了不夠泛化而導致的過擬合問題。在 SMOTE 的論文(https://www.jair.org/media/953/live-953-2037-jair.pdf)中用了很多圖來進行解釋這個問題的原理和解決方案,所以我建議大家可以去看看。

但是,我們有一定必須要清楚的是 使用 SMOTE 過取樣的確會提升決策邊界,但是卻並沒有解決前面所提到的交叉驗證所面臨的問題。 如果我們使用相同的樣本來訓練和驗證模型,模型的技術指標肯定會比取樣了合理交叉驗證方法所訓練出來的模型效果好。也就是說我在上面所舉的例子對應的問題是仍然存在的。 下面讓我們來看一下在交叉驗證之前進行過取樣會得出怎樣的結果。

錯誤的使用交叉驗證和過取樣

下面的程式碼將會先進行過取樣,然後再進入交叉驗證的迴圈,我們使用 SMOTE 方法合成了我們的樣本:

data_to_use <- tpehgdb_features
data_to_use_smote <- SMOTE(preterm ~ . , cbind(data_to_use[, c("preterm", features)]), k=5, perc.over = 600)
metrics_all <- data.frame()
 #leave one participant out cross-validation
results_lr <- rep(NA, nrow(data_to_use_smote))
results_tree <- rep(NA, nrow(data_to_use_smote))
results_svm <- rep(NA, nrow(data_to_use_smote))
results_rf <- rep(NA, nrow(data_to_use_smote))
for(index_subj  in 1:nrow(data_to_use_smote))
{   
#remova subject to validate
 training_data <- data_to_use[-index_subj, ]    
#no need to balance the dataset anymore        
#select features in the training set   training_data_formula <- training_data[, c("preterm", features)]    #select features in the validation set   
validation_data <- data_to_use_smote[index_subj, features]    
#logistic regression   
glm.fit <- glm(preterm ~.,                  
data = training_data_formula,                  
family = binomial)   
glm.probs <- predict(glm.fit, validation_data, type = "response")   
predictions_lr <- ifelse(glm.probs < 0.5, "t", "f")   
results_lr[index_subj] <- predictions_lr    
#classification tree   tree.fit <- tree(preterm ~.,                    
data = training_data_formula)   
predictions_tree <- predict(tree.fit, validation_data, type = "class")   
results_tree[index_subj] <- predictions_tree    
#svm   
svm <- svm(preterm ~.,              
data = training_data_formula
  )   
predictions_svm <- predict(svm, validation_data)   
results_svm[index_subj] <- predictions_svm    
#random forest         
rf <- randomForest(preterm ~.,                      
data = training_data_formula)   
predictions_rf <- predict(rf, validation_data)   
results_rf[index_subj] <- predictions_rf    }
 metrics_lr <- data.frame(binary_metrics(as.numeric(as.factor(results_lr)), as.numeric(data_to_use_smote$preterm), class_of_interest = 2))
metrics_lr[, c("classifier")] <- c("logistic_regression")
metrics_all <- rbind(metrics_all, metrics_lr)
metrics_tree <- data.frame(binary_metrics(results_tree, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))
metrics_tree[, c("classifier")] <- c("tree") metrics_all <- rbind(metrics_all, metrics_tree)
 metrics_svm <- data.frame(binary_metrics(results_svm, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))
metrics_svm[, c("classifier")] <- c("svm") metrics_all <- rbind(metrics_all, metrics_svm)
 metrics_rf <- data.frame(binary_metrics(results_rf, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))
metrics_rf[, c("classifier")] <- c("random_forests")
metrics_all <- rbind(metrics_all, metrics_rf)  

R 包中的 SMOTE 函式在這裡可以檢視 DMwR(https://cran.r-project.org/web/packages/DMwR/DMwR.pdf)。訓練的結果如下:

結果相當不錯。尤其是隨機森林在沒有做任何特徵工程和調參的前提下 auc 的值達到了 0.93 ,但是與前面不同的是我們使用了 SMOTE 方法進行欠取樣,現在這個問題的核心在於我們應該在什麼時候使用恰當的方法,而不是使用哪種方法。在交叉驗證之前使用過取樣的確獲得很高的精度,但模型已經 過擬合 了。你看,就算是最簡單的分類樹都可以獲得 0.84 的 AUC 值。

正確的使用過取樣和交叉驗證

正確的在交叉驗證中配合使用過擬合的方法很簡單。就和我們在交叉驗證中的每次迴圈中做特徵選擇一樣,我們也要在每次迴圈中做過取樣。 根據我們當前的少數類建立樣本,然後選擇一個樣本作為驗證樣本,假裝我們沒有使用在訓練集中的資料來作為驗證樣本,這是毫無意義的。 這一次,我們在交叉驗證迴圈中過取樣,因為驗證集已經從訓練樣本中移除了,因為我們只需要插入那些不用於驗證的樣本來合成數據,我們交叉驗證的迭代次數將和樣本數一樣,如下程式碼所示:

data_to_use <- tpehgdb_features
 metrics_all <- data.frame()
 #leave one participant out cross-validation
results_lr <- rep(NA, nrow(data_to_use))
results_tree <- rep(NA, nrow(data_to_use))
 results_svm <- rep(NA, nrow(data_to_use))
results_rf <- rep(NA, nrow(data_to_use))
 for(index_subj  in 1:nrow(data_to_use))
{   
#remove subject to validate   
training_data <- data_to_use[-index_subj, ]
training_data_smote <- SMOTE(preterm ~ . , cbind(training_data[, c("preterm", features)]), k=5, perc.over = 600)    
#no need to balance the dataset anymore        
#select features in the training set
training_data_formula <- training_data_smote[, c("preterm", features)]    
#select features in the validation set   
validation_data <- data_to_use[index_subj, features]    
#logistic regression   
glm.fit <- glm(preterm ~.,                 
data = training_data_formula,                  
family = binomial)   
glm.probs <- predict(glm.fit, validation_data, type = "response")   
predictions_lr <- ifelse(glm.probs < 0.5, "t", "f")   
results_lr[index_subj] <- predictions_lr    
#classification tree   tree.fit <- 
tree(preterm ~.,                    
data = training_data_formula)   
predictions_tree <- predict(tree.fit, validation_data, type = "class")   
results_tree[index_subj] <- predictions_tree    #svm   svm <- svm(preterm ~.,              
data = training_data_formula   )   
predictions_svm <- predict(svm, validation_data)   
results_svm[index_subj] <- predictions_svm
  #random forest         
rf <- randomForest(preterm ~.,                      

data = training_data_formula) predictions_rf <- predict(rf, validation_data) results_rf[index_subj] <- predictions_rf }

最後,使用了 SMOTE 過取樣技術和合適交叉驗證下模型的結果如下所示:

如之前所說,更多的資料並沒有解決任何的問題,對於使用“智慧”的過取樣。它帶來了非常高的精確度,但那是過擬合。下面是一些關於召回率和真假率指標的結果的分析和總結可以看看。

召回率

真假率

正如我們所看到,分別使用合適的過取樣(第四張圖)和欠取樣(第二張圖)在這個資料集上訓練出來的模型差距並不是很大。

總結

在這篇文章中,我使用了不平衡的 EHG 資料來預測是否早產,目的是講解在使用過取樣的情況下該如何恰當的進行交叉驗證。關鍵是過取樣必須是交叉驗證的一部分,而不是在交叉驗證之前來做過取樣。

總結一下,當在交叉驗證中使用過取樣時,請確保執行了以下步驟從而保證訓練的結果具備泛化性:

  • 在每次交叉驗證迭代過程中,驗證集都不要做任何與特徵選擇,過取樣和構建模型相關的事情
  • 過取樣少數類的樣本,但不要選擇已經排除掉的那些樣本。
  • 用對少數類過取樣和大多數類的樣本混合在一起的資料集來訓練模型,然後用已經排除掉的樣本做為驗證集
  • 重複 n 次交叉驗證的過程,n 的值是你訓練樣本的個數(如果你使用留一交叉驗證法的話)

關於EHG 資料、妊娠、分娩和早產分類的一份宣告

顯然,分析結果並不意味著利用 EHG 資料檢測是否早產是不可能的。只能說明一個橫截面記錄和這些基本特徵並不夠用來區分早產。這裡最可能需要的是多重生理訊號的縱向記錄(如EHG、ECG、胎兒心電圖、hr/hrv等)以及有關活動和行為的資訊。多引數縱向資料可以幫助我們更好地理解這些訊號在懷孕結果方面的變化,以及對個體差異的建模,類似於我們在其他複雜的應用中所看到的,從生理學的角度來看,這是很不容易理解的。

在 Bloom,我們正致力於更好地建模這些變數,以有效地預測早產風險。然而,這一問題的內在侷限性,僅僅關乎參考值是如何定義的(例如,37周這個閾值是非常武斷的),因此需要小心地分析近乎完美的分類,正如我們在這篇文章中所看到的那樣。

引用文獻

[1] Fergus, Paul, et al. "Prediction of preterm deliveries from EHG signals using machine learning." (2013): e77154. PloS one.

[2] Ren, Peng, et al. "Improved Prediction of Preterm Delivery Using Empirical Mode Decomposition Analysis of Uterine Electromyography Signals." PloS one. 10.7 (2015): e0132116.

[3] Fele-Žorž, Gašper, et al. "A comparison of various linear and non-linear signal processing techniques to separate uterine EMG records of term and pre-term delivery groups." Medical & biological engineering & computing 46.9 (2008): 911-922.

[4] Japkowicz, N. (2000). The Class Imbalance Problem: Significance and Strategies. In Proceedings of the 200 International Conference on Artificial Intelligence (IC-AI’2000): Special Track on Inductive Learning Las Vegas, Nevada.

[5] Chawla, Nitesh V., et al. "SMOTE: synthetic minority over-sampling technique."Journal of artificial intelligence research (2002): 321-357.