Posted on

k-Fold Cross-Validation(交叉驗證)

一般所使用的方式 – Holdout method

這是一種非常基本且簡單的方法,我們將整個數據集分為兩部分,即訓練數據測試數據。顧名思義,我們在訓練數據上訓練模型,然後在測試集上進行評估。通常,訓練數據的大小設置為測試數據的兩倍以上,因此數據按70:30或80:20的比例進行分割。

在這種方法中,數據在分割之前首先被隨機洗牌。由於模型是在不同的數據點組合上訓練的,每次訓練時模型都會給出不同的結果,這可能是不穩定的原因。此外,我們永遠無法保證我們選擇的訓練集能夠代表整個數據集。

此外,當我們的數據集不是太大時,測試數據很可能包含一些我們丟失的重要信息,因為我們沒有在測試集上訓練模型。範例程式如下:

import tensorflow as tf
 
# 建立圖片資料集
dataset = tf.keras.utils.image_dataset_from_directory(
    'image_directory',
    labels='inferred',
    class_names=None,
    label_mode='int'
)
 
# 計算資料集的大小
dataset_size = tf.data.experimental.cardinality(dataset).numpy()
 
# 計算訓練集和測試集的大小
test_size = int(0.2 * dataset_size)
train_size = dataset_size - test_size
 
# 將資料集分割為訓練集和測試集
train_dataset = dataset.take(train_size)
test_dataset = dataset.skip(train_size)

改善方法 – k-Fold Cross-Validation

K 折交叉驗證是改進的一種方法。這種方法保證了我們模型的分數不依賴於我們選擇訓練集和測試集的方式。將數據集分為 k 個子集,並將保留方法重複 k 次。讓我們分步驟完成這個過程:

  1. 將整個數據集隨機分成 k 個折疊(子集)
  2. 對於數據集中的每個折疊,在數據集的 k – 1 個折疊上構建模型。然後,測試模型以檢查第 k 次折疊的有效性
  3. 重複此操作,直到每個 k 重都作為測試集
  4. k 記錄的準確度的平均值稱為交叉驗證準確度,並將作為模型的性能指標。

因為它確保來自原始數據集的每個觀察結果都有機會出現在訓練和測試集中,所以與其他方法相比,這種方法通常會產生偏差較小的模型。如果我們的輸入數據有限,這是最好的方法之一。 

這種方法的缺點是訓練算法必須從頭開始重新運行 k 次,這意味著進行評估需要 k 倍的計算量。

分層k-Fold – Stratified K Fold Cross Validation

在分類問題上使用 K Fold 可能會很棘手。由於我們隨機打亂數據,然後將其劃分為折疊,因此我們可能會得到高度不平衡的折疊,這可能會導致我們的訓練出現偏差。例如,讓我們以某種方式得到一個折疊,其中大多數屬於一個類(例如正類),而只有少數屬於負類。這肯定會破壞我們的訓練,為了避免這種情況,我們使用分層進行分層折疊。

分層是重新排列數據的過程,以確保每次折疊都能很好地代表整體。例如,在每個類包含 50% 數據的二元分類問題中,最好對數據進行排列,使得在每個折疊中,每個類包含大約一半的實例。

下面這張圖,是在分類問題上使用K-Fold折疊的方式,會發現因為不同分類隨機分組導致每個分組的數量不一,而破壞訓練的狀況

資料狀況: 共三種資料,每一種70張,共210張,訓練集168張,測試集42張

而改使用StratifiedKFold去做資料分組之後,能看到訓練過程更加順利

範例程式

以下為使用Tensorflow加上sklearn.model_selection去做資料分組的測試程式

import numpy as np
import pathlib
import tensorflow as tf
from sklearn.model_selection import LeaveOneOut,KFold
from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import LabelEncoder
from sklearn.datasets import load_iris

img_path = 'dice3'
train_ds = tf.keras.utils.image_dataset_from_directory(
  img_path,
  seed=7,
  batch_size=32)
X = []
y = []

for images, labels in train_ds:
    X.append(images.numpy())
    y.append(labels.numpy())

X = np.concatenate(X, axis=0)
y = np.concatenate(y, axis=0)
label_encoder = LabelEncoder()
y = label_encoder.fit_transform(y)

def class_names(path):
    return np.array(sorted([item.name for item in pathlib.Path(path).glob('*') if
                            "LICENSE.txt" != item.name]))
def create_model():
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Rescaling(1. / 255))
    model.add(tf.keras.layers.Conv2D(32, kernel_size=7, activation='relu'))
    model.add(tf.keras.layers.MaxPooling2D())
    model.add(tf.keras.layers.Conv2D(64, kernel_size=5, activation='relu'))
    model.add(tf.keras.layers.MaxPooling2D())
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(64, activation='relu'))
    model.add(tf.keras.layers.Dense(len(class_names(img_path))))
    model.compile(
        optimizer='adam',
        loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
        metrics=['accuracy'])
    return model

def make_dataset(X_data,y_data,n_splits):
    def gen():
        for train_index, test_index in KFold(n_splits).split(X_data):
            X_train, X_test = X_data[train_index], X_data[test_index]
            y_train, y_test = y_data[train_index], y_data[test_index]
            yield X_train,y_train,X_test,y_test

    return tf.data.Dataset.from_generator(gen, (tf.float64,tf.float64,tf.float64,tf.float64))

dataset=make_dataset(X,y,5)


for X_train,y_train,X_test,y_test in dataset:
    print(len(X_train), len(y_train), len(X_test), len(y_test))
    model = create_model()
    model.fit(X_train, y_train, epochs=10, batch_size=32, validation_data=(X_test, y_test))


若要改成使用分層K-Fold,則要使用sklearn.model_selection的StratifiedKFold

def make_dataset(X_data,y_data,n_splits):
    def gen():
        kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=123)
        for train_index, test_index in kfold.split(X_data, y_data):
            X_train, X_test = X_data[train_index], X_data[test_index]
            y_train, y_test = y_data[train_index], y_data[test_index]
            yield X_train,y_train,X_test,y_test

    return tf.data.Dataset.from_generator(gen, (tf.float64,tf.float64,tf.float64,tf.float64))

資料集長這樣

參考資料