跳到主要內容

ML | 交叉驗證 Cross-Validation & Bootstrap

本篇文章是要記錄在機器學習當中常見的拆分資料的方法。作為資料科學工作者的我們都知道,選用合適的方式來將原始資料拆分成訓練組資料(Training set)、鑑效組資料(Validation set)、與測試組(Testing set),對於模型好壞有著重大的影響,而這一系列拆分資料的方法,即稱為「交叉驗證」(Cross-Validation, CV),同時也介紹如何使用 R 語言來執行常見的交叉驗證方法,包含:Holdout Method、K-Fold CV、Leave One out CV、Bootstrap。此外,文章後半段會針對時間序列資料的交叉驗證做介紹。

# Holdout CV
最簡單直觀的做法,即隨機把原始數據分為三組即可,不同的劃分會得到不同的最優模型,但該方法對模型的敏感度影響較大。如果僅做一次分割,則訓練集、驗證集和測試集的樣本數比例,還有分割後的資料分布是否和原始資料集的分布相同等等因素,皆會影響模型的敏感度。且各個子集能夠分配到的資料樣本數量就會受限於原始的資料量,較不利於樣本數較少的資料集。

# K-Fold CV
為解決 Holdout Method 的困境,於是便有了 K-Fold CV,即對 k 個不同分組訓練的結果進行平均來減少變異,模型效能就不會對原始數據拆分過於敏感。最好的 k 要設定多少,並沒有一定答案,但是從經驗法則來看,資料量小的時候,k 可以設大一點,這樣訓練集占整體比例就比較大,但訓練的模型個數也增多,需要更多的運算時間;相對地,資料量大的時候,k 可以設小一點。

# Leave One out CV
當資料分割的組數 k 等於總樣本數 m 時,即為 Leave one out CV。每次的測試集都只有一個樣本,進行 m 次訓練和預測。這個方法用於訓練的資料只比整體資料集少了一個樣本,因此最接近原始樣本的分布,對於降低原始資料分布對模型的影響非常有效,但同時也增加了訓練的複雜度以及所需時間,一般而言是在資料量較少的時候使用。

# Bootstrap
在統計學當中,也被稱作「自助重抽法」。其透過重複抽樣,可以避免 Holdout Method 造成的樣本減少問題,其次,Bootstrap 也可以於創造訓練資料的隨機性。該方法的優點是訓練資料集的樣本總數和原始資料集相同,並且仍有約 1/3 的資料不被訓練到,而可以作為測試集,對於樣本數少的資料集,就不用再由於拆分得更小而影響模型的效果。缺點是,透過這樣產生的訓練集,其資料分布很可能和原始資料不同,故採用此方法時,會引入估計偏誤。

----- ----- ----- ----- ----- ----- ----- ----- ----- -----

# R 語言實作範例
以下使用先介紹 {modelr} 套件當中的各項函數來進行交叉驗證,並以該套件內含的「heights」資料集做示範:

library(modelr) # package for CV
library(magrittr) # package for pipe operator

# Partitioning and sampling based on a specific indices by modelr::resample 使用 resample 函數來取出 heights 資料集當中的前 10 筆並將其格式化為 data.frame
resample(heights, 1:10) %>% as.data.frame()

# generate a 20% testing partition, 20% validation partition and a 70% training partition by modelr::resample_partition 使用 resample_partition 來將 heights 資料集拆分成 20% 的測試集;20% 的驗證集;60% 的訓練集,並且在使用 lapply 將三者之格式通通化為 data.frame
resample_partition(heights, c(test = 0.2, validation = 0.2, train = 0.6)) %>% 
lapply(as.data.frame) 

# bootstrap 
# 使用 modelr::bootstrap 對 heights 資料集進行100次的自助重抽
bootstrap(heights, 100)

# k-fold CV
# 使用 modelr::crossv_kfold 對 heights 資料集進行 10 等分的 k-fold CV
crossv_kfold(heights, 10)

# Monte Carlo CV
# 使用 modelr:: crossv_mc 對 heights 資料集隨機產生 100 份不同的 Holdout CV,並令每一份 Holdout CV 當中訓練資料集的比例為資料總數的 0.2
crossv_mc(heights, 100, test = 0.2)

# Leave-one-out CV
# 使用 modelr:: crossv_loo 對 heights 資料集進行 Leave-one-out CV
crossv_loo(heights)

# {models} 當中的 {model-quality} 內含套件提供了一系列檢定模型配適偏誤的函數,可以用於檢測不同的資料拆分對模型的影響,概略說明如下:

#  Three summaries are immediately interpretible on the scale of the response variable:
#     rmse() is the root-mean-squared-error
#     mae() is the mean absolute error
#     qae() is quantiles of absolute error.

#  Other summaries have varying scales and interpretations:
#     mape() mean absolute percentage error.
#     rsae() is the relative sum of absolute errors.
#     mse() is the mean-squared-error.
#     rsquare() is the variance of the predictions divided by the variance of the response.
----- ----- ----- ----- ----- ----- ----- ----- ----- -----


# 時間序列資料的交叉驗證
除了上述介紹的方法之外,這裡要特別提及如何對時間序列(Time Series)資料進行交叉驗證。由於時間序列的預測模型,在企業界當中高度的應用性,所以自然得該瞭解一下這一塊。

由於時間序列資料可能具備潛在的自我相關性(即原始資料集當中的樣本分佈並非獨立的),因此上述的幾項傳統的交叉驗證方法,就會用應用上的限制。對此我們通常採用所謂的巢狀交叉驗證(Nested Cross-Validation,或者也稱為 Moving Block Cross-Validation),其算是 Leave-one-out CV 的一種變體,概念可以直接參考下方兩張圖。上圖為一般的 Leave-one-out CV;下圖為 Nested CV。而常見的兩種巢狀交叉驗證方法則有:「預測後一半」(Predict Second Half)與「日前向鏈」(Day Forward-Chaining)兩種。


# Predict Second Half
這是最基礎的巢狀驗證方法,即原始資料集進行一次的分割,並按照時間的先後,前半部的資料作為訓練集與驗證集,後半部則作為測試集。前半部的切割比例則依照目標問題以及原始資料集而定,但驗證集的時間順必須在訓練集之後,其概念可參考下圖。該方法的優點是簡便,缺點是僅有一次性的測試集任意選擇會導致在獨立測試集上預測誤差的有偏估計。



# Day Forward-Chaining
為了解決 Predict Second Half 的困境,一個常用的方法就是進行多次訓練、測試分割,然後計算這些分割上的誤差平均值,Day Forward-Chaining 即為該作法的實踐,其概念可參考下圖。值得留意的是,Day Forward-Chaining 當中的「Day」,是指進行 Forward-Chaining 方法時的基準時間單位,對於不同的時間序列資料及問題目標,我們當然可以代換成其他的日期或者時間單位。



----- ----- ----- ----- ----- ----- ----- ----- ----- -----
# R語言實作範例

以下使用先介紹 {forecastHybrid} 套件當中的「tsPartition」函數來進行 Day Forward-Chaining CV,並以「AirPassengers」資料集做示範:

library(forecastHybrid)

# 以最早的 15 筆資料開始作為訓練集,接著後 3 筆做為測試集。每一次的迭代都會將前一次的測試集資料納入訓練集,並以接下來新的 3 筆資料作為新一期的測試集。其中,若將 rolling 參數設定為 True,則每一期迭代所產生的新的訓練集不會遞增,都維持 15 筆。
tsPartition( AirPassengers, rolling = F, windowSize = 15, maxHorizon = 3)

# 上述做法僅將原始資料拆分成訓練集與測試集。但如果我們要將訓練集再進一步切割出驗證集呢?可以使用以下作法:
Apt <- tsPartition( AirPassengers, rolling = F, windowSize = 15, maxHorizon = 3)

# 呼叫第 20 次迭代的訓練集
apt[[20]]$trainIndices


----- ----- ----- ----- ----- ----- ----- ----- ----- -----

參考資料: