程式碼範例 / Keras 快速入門 / Keras 模型中的可重現性

Keras 模型中的可重現性

作者: Frightera
建立日期 2023/05/05
上次修改日期 2023/05/05
說明: 示範 Keras 模型中的隨機權重初始化和可重現性。

ⓘ 此範例使用 Keras 3

在 Colab 中檢視 GitHub 來源


簡介

此範例示範如何在 Keras 模型中控制隨機性。有時您可能希望在不同執行中重現完全相同的結果,以進行實驗或除錯問題。


設定

import json
import numpy as np
import tensorflow as tf
import keras
from keras import layers
from keras import initializers

# Set the seed using keras.utils.set_random_seed. This will set:
# 1) `numpy` seed
# 2) backend random seed
# 3) `python` random seed
keras.utils.set_random_seed(812)

# If using TensorFlow, this will make GPU ops as deterministic as possible,
# but it will affect the overall performance, so be mindful of that.
tf.config.experimental.enable_op_determinism()

Keras 中的權重初始化

Keras 中的大多數圖層都有 kernel_initializerbias_initializer 參數。這些參數可讓您指定用於初始化圖層變數權重的策略。下列內建初始化器可作為 keras.initializers 的一部分使用

initializers_list = [
    initializers.RandomNormal,
    initializers.RandomUniform,
    initializers.TruncatedNormal,
    initializers.VarianceScaling,
    initializers.GlorotNormal,
    initializers.GlorotUniform,
    initializers.HeNormal,
    initializers.HeUniform,
    initializers.LecunNormal,
    initializers.LecunUniform,
    initializers.Orthogonal,
]

在可重現的模型中,模型的權重應在後續執行中以相同的值初始化。首先,我們將檢查當使用相同的 seed 值多次呼叫初始化器時,它們的行為方式。

for initializer in initializers_list:
    print(f"Running {initializer}")

    for iteration in range(2):
        # In order to get same results across multiple runs from an initializer,
        # you can specify a seed value.
        result = float(initializer(seed=42)(shape=(1, 1)))
        print(f"\tIteration --> {iteration} // Result --> {result}")
    print("\n")
Running <class 'keras.src.initializers.random_initializers.RandomNormal'>
    Iteration --> 0 // Result --> 0.000790853810030967
    Iteration --> 1 // Result --> 0.000790853810030967
Running <class 'keras.src.initializers.random_initializers.RandomUniform'>
    Iteration --> 0 // Result --> -0.02175668440759182
    Iteration --> 1 // Result --> -0.02175668440759182
Running <class 'keras.src.initializers.random_initializers.TruncatedNormal'>
    Iteration --> 0 // Result --> 0.000790853810030967
    Iteration --> 1 // Result --> 0.000790853810030967
Running <class 'keras.src.initializers.random_initializers.VarianceScaling'>
    Iteration --> 0 // Result --> 0.017981600016355515
    Iteration --> 1 // Result --> 0.017981600016355515
Running <class 'keras.src.initializers.random_initializers.GlorotNormal'>
    Iteration --> 0 // Result --> 0.017981600016355515
    Iteration --> 1 // Result --> 0.017981600016355515
Running <class 'keras.src.initializers.random_initializers.GlorotUniform'>
    Iteration --> 0 // Result --> -0.7536736726760864
    Iteration --> 1 // Result --> -0.7536736726760864
Running <class 'keras.src.initializers.random_initializers.HeNormal'>
    Iteration --> 0 // Result --> 0.025429822504520416
    Iteration --> 1 // Result --> 0.025429822504520416
Running <class 'keras.src.initializers.random_initializers.HeUniform'>
    Iteration --> 0 // Result --> -1.065855622291565
    Iteration --> 1 // Result --> -1.065855622291565
Running <class 'keras.src.initializers.random_initializers.LecunNormal'>
    Iteration --> 0 // Result --> 0.017981600016355515
    Iteration --> 1 // Result --> 0.017981600016355515
Running <class 'keras.src.initializers.random_initializers.LecunUniform'>
    Iteration --> 0 // Result --> -0.7536736726760864
    Iteration --> 1 // Result --> -0.7536736726760864
Running <class 'keras.src.initializers.random_initializers.OrthogonalInitializer'>
    Iteration --> 0 // Result --> 1.0
    Iteration --> 1 // Result --> 1.0

現在,讓我們檢視當兩個不同的初始化器物件具有相同的 seed 值時,它們的行為方式。

# Setting the seed value for an initializer will cause two different objects
# to produce same results.
glorot_normal_1 = keras.initializers.GlorotNormal(seed=42)
glorot_normal_2 = keras.initializers.GlorotNormal(seed=42)

input_dim, neurons = 3, 5

# Call two different objects with same shape
result_1 = glorot_normal_1(shape=(input_dim, neurons))
result_2 = glorot_normal_2(shape=(input_dim, neurons))

# Check if the results are equal.
equal = np.allclose(result_1, result_2)
print(f"Are the results equal? {equal}")
Are the results equal? True

如果未設定 seed 值 (或使用不同的 seed 值),則兩個不同的物件會產生不同的結果。由於隨機 seed 在筆記本的開頭設定,因此結果在循序執行中會相同。這與 keras.utils.set_random_seed 有關。

glorot_normal_3 = keras.initializers.GlorotNormal()
glorot_normal_4 = keras.initializers.GlorotNormal()

# Let's call the initializer.
result_3 = glorot_normal_3(shape=(input_dim, neurons))

# Call the second initializer.
result_4 = glorot_normal_4(shape=(input_dim, neurons))

equal = np.allclose(result_3, result_4)
print(f"Are the results equal? {equal}")
Are the results equal? False

result_3result_4 將會不同,但是當您再次執行筆記本時,result_3 將會與先前執行中的值相同。result_4 也是如此。


模型訓練過程中的可重現性

如果您想要重現模型訓練過程的結果,則需要在訓練過程中控制隨機性來源。為了展示真實的範例,本節使用 tf.data,其中使用平行對應和混洗運算。

為了開始,讓我們建立一個簡單的函式,該函式會傳回 Keras 模型的歷史物件。

def train_model(train_data: tf.data.Dataset, test_data: tf.data.Dataset) -> dict:
    model = keras.Sequential(
        [
            layers.Conv2D(32, (3, 3), activation="relu"),
            layers.MaxPooling2D((2, 2)),
            layers.Dropout(0.2),
            layers.Conv2D(32, (3, 3), activation="relu"),
            layers.MaxPooling2D((2, 2)),
            layers.Dropout(0.2),
            layers.Conv2D(32, (3, 3), activation="relu"),
            layers.GlobalAveragePooling2D(),
            layers.Dense(64, activation="relu"),
            layers.Dropout(0.2),
            layers.Dense(10, activation="softmax"),
        ]
    )

    model.compile(
        optimizer="adam",
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
        jit_compile=False,
    )
    # jit_compile's default value is "auto" which will cause some problems in some
    # ops, therefore it's set to False.

    # model.fit has a `shuffle` parameter which has a default value of `True`.
    # If you are using array-like objects, this will shuffle the data before
    # training. This argument is ignored when `x` is a generator or
    # [`tf.data.Dataset`](https://tensorflow.dev.org.tw/api_docs/python/tf/data/Dataset).
    history = model.fit(train_data, epochs=2, validation_data=test_data)

    print(f"Model accuracy on test data: {model.evaluate(test_data)[1] * 100:.2f}%")

    return history.history


# Load the MNIST dataset
(train_images, train_labels), (
    test_images,
    test_labels,
) = keras.datasets.mnist.load_data()

# Construct tf.data.Dataset objects
train_ds = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
test_ds = tf.data.Dataset.from_tensor_slices((test_images, test_labels))
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
 11490434/11490434 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step

請記住,我們在函式開頭呼叫了 tf.config.experimental.enable_op_determinism()。這使得 tf.data 運算具有確定性。但是,使 tf.data 運算具有確定性會產生效能成本。如果您想瞭解更多相關資訊,請查看此官方指南

以下簡短摘要說明了這裡的狀況。模型具有 kernel_initializerbias_initializer 參數。由於我們在筆記本的開頭使用 keras.utils.set_random_seed 設定了隨機 seed,因此初始化器將在循序執行中產生相同的結果。此外,TensorFlow 運算現在已變成確定性。您通常會使用具有數千個硬體執行緒的 GPU,這會導致發生不確定性行為。

def prepare_dataset(image, label):
    # Cast and normalize the image
    image = tf.cast(image, tf.float32) / 255.0

    # Expand the channel dimension
    image = tf.expand_dims(image, axis=-1)

    # Resize the image
    image = tf.image.resize(image, (32, 32))

    return image, label

tf.data.Dataset 物件具有 shuffle 方法,可對資料進行混洗。此方法具有 buffer_size 參數,可控制緩衝區的大小。如果您將此值設定為 len(train_images),則會對整個資料集進行混洗。如果緩衝區大小等於資料集的長度,則元素將以完全隨機的順序進行混洗。

將緩衝區大小設定為資料集長度的主要缺點是,填滿緩衝區可能需要一些時間,具體取決於資料集的大小。

以下簡短摘要說明了這裡的狀況:1) shuffle() 方法會建立指定大小的緩衝區。2) 資料集的元素會隨機混洗並放置到緩衝區中。3) 然後,緩衝區的元素會以隨機順序傳回。

由於已啟用 tf.config.experimental.enable_op_determinism(),並且我們在筆記本的開頭使用 keras.utils.set_random_seed 設定了隨機 seed,因此 shuffle() 方法將在循序執行中產生相同的結果。

# Prepare the datasets, batch-map --> vectorized operations
train_data = (
    train_ds.shuffle(buffer_size=len(train_images))
    .batch(batch_size=64)
    .map(prepare_dataset, num_parallel_calls=tf.data.AUTOTUNE)
    .prefetch(buffer_size=tf.data.AUTOTUNE)
)

test_data = (
    test_ds.batch(batch_size=64)
    .map(prepare_dataset, num_parallel_calls=tf.data.AUTOTUNE)
    .prefetch(buffer_size=tf.data.AUTOTUNE)
)

第一次訓練模型。

history = train_model(train_data, test_data)
Epoch 1/2
 938/938 ━━━━━━━━━━━━━━━━━━━━ 73s 73ms/step - accuracy: 0.5726 - loss: 1.2175 - val_accuracy: 0.9401 - val_loss: 0.1924
Epoch 2/2
 938/938 ━━━━━━━━━━━━━━━━━━━━ 89s 81ms/step - accuracy: 0.9105 - loss: 0.2885 - val_accuracy: 0.9630 - val_loss: 0.1131
 157/157 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.9553 - loss: 0.1353
Model accuracy on test data: 96.30%

讓我們將結果儲存到 JSON 檔案中,然後重新啟動核心。重新啟動核心後,我們應該會看到與前一次執行相同的結果,其中包含訓練和測試資料的指標和損失值。

# Save the history object into a json file
with open("history.json", "w") as fp:
    json.dump(history, fp)

請勿執行上方的儲存格,以免覆寫結果。再次執行模型訓練儲存格並比較結果。

with open("history.json", "r") as fp:
    history_loaded = json.load(fp)

逐一比較結果。您會看到它們相等。

for key in history.keys():
    for i in range(len(history[key])):
        if not np.allclose(history[key][i], history_loaded[key][i]):
            print(f"{key} not equal")

結論

在本教學課程中,您學習了如何在 Keras 和 TensorFlow 中控制隨機性來源。您也學習了如何重現模型訓練過程的結果。

如果您想每次都以相同的權重初始化模型,則需要設定圖層的 kernel_initializerbias_initializer 參數,並為初始化器提供 seed 值。

由於數值誤差累積 (例如在 RNN 圖層中使用 recurrent_dropout),可能仍然會有一些不一致之處。

可重現性取決於環境。如果您在具有相同環境的相同電腦上執行筆記本或程式碼,則會得到相同的結果。