標籤:

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

原文地址:DEALING WITH IMBALANCED DATA: UNDERSAMPLING, OVERSAMPLING AND PROPER CROSS-VALIDATION

原文作者:Marco Altini

譯者:edvardhua

校對者:lileizhenshuai, lsvih

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

[關於我在這篇文章中使用的術語可以在 Physionet網站中找到。 本篇博客中用到的代碼可以在 github中找到]

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

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

手頭的問題

因為分類器對數據中類別佔比較大的數據比較敏感,而對佔比較小的數據則沒那麼敏感,所以我們需要在交叉驗證之前對不均衡數據進行預處理。所以如果我們不處理類別不均衡的數據,分類器的輸出結果就會存在偏差,也就是在預測過程中大多數情況下都會給出偏向於某個類別的結果,這個類別是訓練的時候佔比較大的那個類別。這個問題並不是我的研究領域,但是自從我在做早產預測的工作的時候(medium.com/40-weeks/37-)經常會遇到這種問題。早產是指短於 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信號的均方根,中值頻率,頻率峰值和樣本熵,這裡 有關如何計算這些特徵值的更多信息)。據收集數據集的研究人員所說,大部分有價值的信息都是來自於渠道 3,因此我將使用從渠道 3 預提取出來的特徵。詳細的數據集也在github可以找到。因為我們是要訓練分類器分類器,所以我使用了一些常見的訓練分類器的演算法:邏輯回歸、分類樹、SVM 和隨機森林。在博客中我不會做任何特徵選擇,而是將所有的數據都用來訓練模型。

評測指標

在這裡我們使用 召回率 , 真假率 和 AUC 作為評測指標,關於指標的含義可以查看 wikipedia

交叉驗證

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

類別不均衡的數據

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

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

忽略這個問題

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

#leave one participant out cross-validationnresults_lr <- rep(NA, nrow(data_to_use))nresults_tree <- rep(NA, nrow(data_to_use))nresults_svm <- rep(NA, nrow(data_to_use))nresults_rf <- rep(NA, nrow(data_to_use))n for(index_subj in 1:nrow(data_to_use))n{ n#remove subject to validate training_data <- data_to_use[-index_subj, ] ntraining_data_formula <- training_data[, c("preterm", features)] n#select features in the validation set nvalidation_data <- data_to_use[index_subj, features] n#logistic regression nglm.fit <- glm(preterm ~., ndata = training_data_formula, nfamily = binomial) nglm.probs <- predict(glm.fit, validation_data, type = "response")n predictions_lr <- ifelse(glm.probs < 0.5, "t", "f") nresults_lr[index_subj] <- predictions_lr n#classification tree tree.fit <- tree(preterm ~., ndata = training_data_formula) npredictions_tree <- predict(tree.fit, validation_data, type = "class") nresults_tree[index_subj] <- predictions_tree n#svm svm <- svm(preterm ~., ndata = training_data_formula ) npredictions_svm <- predict(svm, validation_data) nresults_svm[index_subj] <- predictions_svm n#random forest n rf <- randomForest(preterm ~., ndata = training_data_formula) npredictions_rf <- predict(rf, validation_data) nresults_rf[index_subj] <- predictions_rf }n

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

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

對大類樣本進行欠採樣

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

代碼如下:

#leave one participant out cross-validationnresults_lr <- rep(NA, nrow(data_to_use))nresults_tree <- rep(NA, nrow(data_to_use))nresults_svm <- rep(NA, nrow(data_to_use))nresults_rf <- rep(NA, nrow(data_to_use))nrows_preterm <- sum(data_to_use$preterm == " t ") #weird string, havent changed it for now for(index_subj in 1:nrow(data_to_use))n{ n#remove subject to validate ntraining_data <- data_to_use[-index_subj, ] ntraining_data_preterm <- training_data[training_data$preterm == " t ", ] ntraining_data_term <- training_data[training_data$preterm == " f ", ] n#get subsample to balance dataset nindices <- sample(nrow(training_data_term), rows_preterm) ntraining_data_term <- training_data_term[indices, ] ntraining_data <- rbind(training_data_preterm, training_data_term) n#select features in the training set ntraining_data_formula <- training_data[, c("preterm", features)]n #select features in the validation set nvalidation_data <- data_to_use[index_subj, features] n#logistic regression glm.fit <- glm(preterm ~., ndata = training_data_formula, nfamily = binomial) nglm.probs <- predict(glm.fit, validation_data, type = "response") npredictions_lr <- ifelse(glm.probs < 0.5, "t", "f") results_lr[index_subj] <- predictions_lr #classification tree ntree.fit <- tree(preterm ~., ndata = training_data_formula) npredictions_tree <- predict(tree.fit, validation_data, type = "class")nresults_tree[index_subj] <- predictions_tree #svm svm <- svm(preterm ~., ndata = training_data_formula ) npredictions_svm <- predict(svm, validation_data) results_svm[index_subj] <- predictions_svm #random forest nrf <- randomForest(preterm ~., ndata = training_data_formula, nsampsize = c(nrow(training_data_preterm), nrow(training_data_preterm))) npredictions_rf <- predict(rf, validation_data) nresults_rf[index_subj] <- predictions_rf }n

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

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

對少數類樣本過採樣

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

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

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

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

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

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

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

下面的代碼將會先進行過採樣,然後再進入交叉驗證的循環,我們使用 SMOTE 方法合成了我們的樣本:

data_to_use <- tpehgdb_featuresndata_to_use_smote <- SMOTE(preterm ~ . , cbind(data_to_use[, c("preterm", features)]), k=5, perc.over = 600)nmetrics_all <- data.frame()n #leave one participant out cross-validationnresults_lr <- rep(NA, nrow(data_to_use_smote))nresults_tree <- rep(NA, nrow(data_to_use_smote))nresults_svm <- rep(NA, nrow(data_to_use_smote))nresults_rf <- rep(NA, nrow(data_to_use_smote))nfor(index_subj in 1:nrow(data_to_use_smote))n{ n#remova subject to validaten training_data <- data_to_use[-index_subj, ] n#no need to balance the dataset anymore n#select features in the training set training_data_formula <- training_data[, c("preterm", features)] #select features in the validation set nvalidation_data <- data_to_use_smote[index_subj, features] n#logistic regression nglm.fit <- glm(preterm ~., ndata = training_data_formula, nfamily = binomial) nglm.probs <- predict(glm.fit, validation_data, type = "response") npredictions_lr <- ifelse(glm.probs < 0.5, "t", "f") nresults_lr[index_subj] <- predictions_lr n#classification tree tree.fit <- tree(preterm ~., ndata = training_data_formula) npredictions_tree <- predict(tree.fit, validation_data, type = "class") nresults_tree[index_subj] <- predictions_tree n#svm nsvm <- svm(preterm ~., ndata = training_data_formulan ) npredictions_svm <- predict(svm, validation_data) nresults_svm[index_subj] <- predictions_svm n#random forest nrf <- randomForest(preterm ~., ndata = training_data_formula) npredictions_rf <- predict(rf, validation_data) nresults_rf[index_subj] <- predictions_rf }n metrics_lr <- data.frame(binary_metrics(as.numeric(as.factor(results_lr)), as.numeric(data_to_use_smote$preterm), class_of_interest = 2))nmetrics_lr[, c("classifier")] <- c("logistic_regression")nmetrics_all <- rbind(metrics_all, metrics_lr)nmetrics_tree <- data.frame(binary_metrics(results_tree, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))nmetrics_tree[, c("classifier")] <- c("tree") metrics_all <- rbind(metrics_all, metrics_tree)n metrics_svm <- data.frame(binary_metrics(results_svm, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))nmetrics_svm[, c("classifier")] <- c("svm") metrics_all <- rbind(metrics_all, metrics_svm)n metrics_rf <- data.frame(binary_metrics(results_rf, as.numeric(data_to_use_smote$preterm), class_of_interest = 2))nmetrics_rf[, c("classifier")] <- c("random_forests")nmetrics_all <- rbind(metrics_all, metrics_rf) n

R 包中的 SMOTE 函數在這裡可以查看DMwR。訓練的結果如下:

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

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

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

data_to_use <- tpehgdb_featuresn metrics_all <- data.frame()n #leave one participant out cross-validationnresults_lr <- rep(NA, nrow(data_to_use))nresults_tree <- rep(NA, nrow(data_to_use))n results_svm <- rep(NA, nrow(data_to_use))nresults_rf <- rep(NA, nrow(data_to_use))n for(index_subj in 1:nrow(data_to_use))n{ n#remove subject to validate ntraining_data <- data_to_use[-index_subj, ]ntraining_data_smote <- SMOTE(preterm ~ . , cbind(training_data[, c("preterm", features)]), k=5, perc.over = 600) n#no need to balance the dataset anymore n#select features in the training setntraining_data_formula <- training_data_smote[, c("preterm", features)] n#select features in the validation set nvalidation_data <- data_to_use[index_subj, features] n#logistic regression nglm.fit <- glm(preterm ~., ndata = training_data_formula, nfamily = binomial) nglm.probs <- predict(glm.fit, validation_data, type = "response") npredictions_lr <- ifelse(glm.probs < 0.5, "t", "f") nresults_lr[index_subj] <- predictions_lr n#classification tree tree.fit <- ntree(preterm ~., ndata = training_data_formula) npredictions_tree <- predict(tree.fit, validation_data, type = "class") nresults_tree[index_subj] <- predictions_tree #svm svm <- svm(preterm ~., ndata = training_data_formula ) npredictions_svm <- predict(svm, validation_data) nresults_svm[index_subj] <- predictions_svmn #random forest nrf <- randomForest(preterm ~., ndata = training_data_formula) npredictions_rf <- predict(rf, validation_data) nresults_rf[index_subj] <- predictions_rf }n

最後,使用了 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.

推薦閱讀:

這五種觸手可及的技術很快將改變你的生活
H3智能健康體脂秤評測:功能多樣,性價比高
讓 AI 來協助你畫畫
量子物理推動機器學習

TAG:人工智能 |