作者: Frightera
建立日期 2023/05/05
上次修改日期 2023/05/05
說明: 示範 Keras 模型中的隨機權重初始化和可重現性。
此範例示範如何在 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 中的大多數圖層都有 kernel_initializer
和 bias_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_3
和 result_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_initializer
和 bias_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_initializer
和 bias_initializer
參數,並為初始化器提供 seed
值。
由於數值誤差累積 (例如在 RNN 圖層中使用 recurrent_dropout
),可能仍然會有一些不一致之處。
可重現性取決於環境。如果您在具有相同環境的相同電腦上執行筆記本或程式碼,則會得到相同的結果。