最近讀的一篇英文博客,講的很不錯,于是便抽空翻譯成了中文。
[關于我在這篇文章中使用的術語可以在 Physionet 網站中找到。 本篇博客中用到的代碼可以在 github中找到]
幾個星期前我閱讀了一篇交叉驗證的技術文檔(Cross Validation Done Wrong), 在交叉驗證的過程中,我們希望能夠了解到我們的模型的泛化性能,以及它是如何預測我們感興趣的未知樣本的。基于這個出發點,作者提出了很多好的觀點(尤其是關于特征選擇的)。我們的確經常在進行交叉驗證之前進行特征選擇,但是需要注意的是我們在特征選擇的時候,不能將驗證集的數據加入到特征選擇這個環節中去。
但是,這篇文章并沒有涉及到我們在實際應用經常出現的問題。例如,如何在不均衡的數據上合理的進行交叉驗證。在醫療領域,我們所擁有的數據集一般只包含兩種類別的數據, 正常 樣本和 相關 樣本。譬如說在癌癥檢查的應用我們可能只有很小一部分病人患上了癌癥(相關樣本)而其余的大部分樣本都是健康的個體。就算不在醫療領域,這種情況也存在(甚至更多),比如欺詐識別,它們的數據集中的相關樣本和正常樣本的比例都有可能會是 1:100000。
手頭的問題
因為分類器對數據中類別占比較大的數據比較敏感,而對占比較小的數據則沒那么敏感,所以我們需要在交叉驗證之前對不均衡數據進行預處理。所以如果我們不處理類別不均衡的數據,分類器的輸出結果就會存在偏差,也就是在預測過程中大多數情況下都會給出偏向于某個類別的結果,這個類別是訓練的時候占比較大的那個類別。這個問題并不是我的研究領域,但是自從我在做早產預測的工作的時候經常會遇到這種問題。早產是指短于 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-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 的論文中用了很多圖來進行解釋這個問題的原理和解決方案,所以我建議大家可以去看看。
但是,我們有一定必須要清楚的是 使用 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。訓練的結果如下:
結果相當不錯。尤其是隨機森林在沒有做任何特征工程和調參的前提下 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.