Contents
はじめに
この記事では、TensorFlow(以降TFと呼びます) にてGPU使用時における再現性確保の方法を紹介します。注意が必要な点としては、TF2.2~でのみ同じ対応が可能である点です。
現状、同じ実行環境においては再現性が確認できています。しかし、違う実行環境で動かした場合、似た値にはなりますが、出力結果の再現性は確保できません。(Python,TF,CUDAを合わせても再現しませんでした)
考えられる要因は、GPUの違いに問題があると考えています。
実験使用コード: https://github.com/tomo0106/tf_reproducible_test
実行環境
- TF 2.3.0 (2.2~)
- Python 3.6.9
- CUDA 10.1
- NVIDIA製GPU
再現性確保に必要な要素
- グローバルシードの固定
- オペレーションレベルシードの固定
- GPU内の計算順の固定
それぞれについて、これから説明していきます。
グローバルシードと操作レベルシードについて
それぞれについて、公式には以下の説明があります。それによれば、実験を再現するためには、グローバルシードとオペレーションレベルシードを固定する必要があるようです。
Operations that rely on a random seed actually derive it from two seeds: the global and operation-level seeds.
Its interactions with operation-level seeds is as follows:
If neither the global seed nor the operation seed is set: A randomly picked seed is used for this op.
If the graph-level seed is set, but the operation seed is not: The system deterministically picks an operation seed in conjunction with the graph-level seed so that it gets a unique random sequence. Within the same version of tensorflow and user code, this sequence is deterministic. However across different versions, this sequence might change. If the code depends on particular seeds to work, specify both graph-level and operation-level seeds explicitly.
If the operation seed is set, but the global seed is not set: A default global seed and the specified operation seed are used to determine the random sequence.
If both the global and the operation seed are set: Both seeds are used in conjunction to determine the random sequence.
link
グローバルシードの固定方法
以下のコードを書くだけです。これもTFのバージョン依存のようなので、詳細はこちらで確認してください。TF1.x系では、tf.set_random_seed()だったようですが、TF2.x系から上記の名前に変更されました。これを呼び出すことでグローバルシードの固定ができます。上記の引用で説明があるように、グローバルシードを設定し、操作レベルシードが設定されていない場合、呼び出されてから決定論的にシードが決定されるため、呼び出すタイミングによって動作が変わります。
# Sets the global random seed.
tf.random.set_seed(seed)
オペレーションレベルシードの固定方法
オペレーションレベルシードは、各レイヤーの初期化の際の乱数を固定するものです。学習重みパラメータの初期化は、決められた分布にしたがう乱数によって行われます。その乱数シードを固定することで、再現性を実現します。tf.kerasでモデルを構築する場合、tf.keras.initializersがその初期化方法にあたります。バイアスの初期化は、デフォルトで0初期化が設定されているため、別途シードの固定は必要ありません。
# Sets the operation-level random seed.
# creat model
operation_lebel_seed=0
initializer=tf.keras.initializers.GlorotUniform(seed=operation_lebel_seed)
model = tf.keras.Sequential([
tf.keras.layers.Input((28, 28, 1)),
tf.keras.layers.Conv2D(filters=8, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer),
tf.keras.layers.MaxPool2D(2),
tf.keras.layers.Conv2D(filters=16, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer),
tf.keras.layers.MaxPool2D(2),
tf.keras.layers.Conv2D(filters=32, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation="relu",kernel_initializer=initializer),
tf.keras.layers.Dense(10,kernel_initializer=initializer),
tf.keras.layers.Softmax()
])
GPU内の計算順の固定
GPUを使用する場合、行列計算が平行処理で行われるため学習の高速化が図ることができます。しかし、ここで問題となるのは、並列計算の順番です。これを固定しなければ、実験の再現はできません。この処理は、NVIDIAのcudnnに依存しているため、以前は非常に厄介でしたが、TF2.2からは容易になりました。TF2.2以前におけるGPU利用時の計算順の固定方法については、ここで開発されているようです。バージョンによってかなり操作が異なるので、ドキュメントを注意して読んでください。
一応TF 2.1からこの環境変数が実装されている模様です。(MaxPoolにも乱数要素があるらしいです…?)
Environment variable TF_DETERMINISTIC_OPS has been added. When set to “true” or “1”, this environment variable makes tf.nn.bias_add operate deterministically (i.e. reproducibly), but currently only when XLA JIT compilation is not enabled. Setting TF_DETERMINISTIC_OPS to “true” or “1” also makes cuDNN convolution and max-pooling operate deterministically. This makes Keras ConvD and MaxPoolD layers operate deterministically in both the forward and backward directions when running on a CUDA-enabled GPU.
TF2.1_Release_note
以下のコードでGPU内の計算順の固定ができます。
import tensorflow as tf
import os
os.environ['TF_DETERMINISTIC_OPS'] = '1'
# Now build your graph and train it
再現性確認用コード
ここでは、google colabを用いて再現性が確保できているか確認してみます。
各種ライブラリの読み込み
# 各種ライブラリの読み込み
import tensorflow as tf
import matplotlib.pyplot as plt
import os
import numpy as np
Google colabの動作環境確認
# 割り当てられたGPUの確認,ランダムにGPUが割り振られるので,人と違う場合がある
!nvidia-smi
# Google colabのPythonバージョン確認,自分の環境であればいらないのでコメントアウトする
!python3 --version # -> Python 3.6.9
# TFのバージョン確認
print("TensorFlow version -> ",tf.__version__) # TensorFlow version -> 2.3.0
# Google colab用cudaバージョン確認
!nvcc -V
GPU内の計算順の固定とグローバルシードの指定
# GPU内の計算順を固定するかどうかのフラグを指定
# 環境変数['TF_DETERMINISTIC_OPS']を'1'または'True'を設定することで固定できる
os.environ['TF_DETERMINISTIC_OPS'] = '1'
# グローバルシードの指定
global_seed=1234
tf.random.set_seed(global_seed)
データセットの準備
# データの読み込み
(X_train,y_train),(X_test,y_test)=tf.keras.datasets.mnist.load_data()
# データを[0~255]から[0~1]に整形
X_train=(X_train).astype(np.float32)/255.
X_test=(X_test).astype(np.float32)/255.
# [batch,height,width]->[batch,height,width,channel]に整形.畳み込みに渡す行列は4次元を受け取る
X_train = np.expand_dims(X_train, axis=3) #(60000, 28, 28) ->(60000, 28, 28, 1)
X_test = np.expand_dims(X_test, axis=3) ##(10000, 28, 28) ->(10000, 28, 28, 1)
y_train = tf.keras.utils.to_categorical(y_train, 10) # [60000,] ->[10000,10]
y_test = tf.keras.utils.to_categorical(y_test, 10) # [10000,] ->[10000,10]
画像表示用関数。データの形状に注意してください。matplotlibは、グレースケール画像の場合、2次元のデータでのみ表示が可能です。ex) (batch,height,width,channel) -> (height,width)への変更が必要です。
def show(a_img, cmap=None):
plt.figure()
plt.imshow(a_img, cmap=cmap)
plt.axis('off')
plt.show()
シーケンシャルモデルの構築と動作レベルシードの固定
# 動作レベルシードの固定
operation_lebel_seed=0
# 乱数の初期化方法と,その際の乱数生成のためのシードを固定
initializer=tf.keras.initializers.GlorotUniform(seed=operation_lebel_seed)
sequential_model = tf.keras.Sequential([
tf.keras.layers.Input((28, 28, 1)),
tf.keras.layers.Conv2D(filters=8, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer),
tf.keras.layers.MaxPool2D(2),
tf.keras.layers.Conv2D(filters=16, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer),
tf.keras.layers.MaxPool2D(2),
tf.keras.layers.Conv2D(filters=32, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation="relu",kernel_initializer=initializer),
tf.keras.layers.Dense(10,kernel_initializer=initializer),
tf.keras.layers.Softmax()
])
#モデル構造をprint
sequential_model.summary()
ファンクショナルモデルの書き方で同一モデルを構築
inputs = tf.keras.layers.Input((28, 28, 1))
x = tf.keras.layers.Conv2D(filters=8, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer)(inputs)
x = tf.keras.layers.MaxPool2D(2)(x)
x = tf.keras.layers.Conv2D(filters=16, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer)(x)
x = tf.keras.layers.MaxPool2D(2)(x)
x = tf.keras.layers.Conv2D(filters=32, kernel_size=3, strides=1, padding="valid", activation="relu",kernel_initializer=initializer)(x)
x = tf.keras.layers.Flatten()(x)
x = tf.keras.layers.Dense(128, activation="relu",kernel_initializer=initializer)(x)
x = tf.keras.layers.Dense(10,kernel_initializer=initializer)(x)
x = tf.keras.layers.Softmax()(x)
functional_model = tf.keras.Model(inputs, x)
functional_model.summary()
モデル学習のためのもろもろ準備。Adam()等の多くの最適化関数は、学習を高速化するために以前の勾配等を利用するので、繰り返し利用する場合は、再定義し初期化する必要があります。
# 誤差関数の設定
loss_fn = tf.keras.losses.CategoricalCrossentropy(from_logits=True)
# 最適化関数の設定
seq_optimizer = tf.keras.optimizers.Adam()
fun_optimizer=tf.keras.optimizers.Adam()
# モデルのコンパイル
sequential_model.compile(optimizer=seq_optimizer, loss=loss_fn, metrics="accuracy")
functional_model.compile(optimizer=fun_optimizer, loss=loss_fn, metrics="accuracy")
学習する前の推論結果を確認します。まだ学習していないので、完全にランダムな出力結果が得られるはずです。つまり、mnistでは10個のクラス分類なので、それぞれ0.1程度の出力を示すと考えられる。
# 画像の確認 -> 7
show(np.squeeze(X_test[0]), cmap="gray")
# 処理結果は0始まりなので,8番目が数字の7の予測結果になる
# ここは,初期化の乱数を指定しているため,必ず同じになる.
print("Sequential model's output before learning ", sequential_model(X_test[:1]))
print("\nFunctional model's output before learning ", functional_model(X_test[:1]))
シーケンシャルモデルの学習実行
print("\nStart to train sequential model...")
sequential_model.fit(X_train, y_train, batch_size=16, epochs=3)
ファンクショナルモデルの学習実行
print("\nStart to train functional model...")
functional_model.fit(X_train, y_train, batch_size=16, epochs=3)
学習後の結果を学習前と比較
# 学習後の結果を確認
print("Sequential model's output after learning ", sequential_model(X_test[:1]))
print("\nFunctional model's output after learning ", functional_model(X_test[:1]))
# 精度の確認
test_accuracy = tf.keras.metrics.CategoricalAccuracy(name='test_accuracy')
y_pred_seq = sequential_model(X_test)
test_accuracy(y_test, y_pred_seq)
print("\nSequential model test accuracy ", test_accuracy.result().numpy())
test_accuracy = tf.keras.metrics.CategoricalAccuracy(name='test_accuracy')
y_pred_fun = functional_model(X_test)
test_accuracy(y_test, y_pred_fun)
print("Functional model test accuracy ", test_accuracy.result().numpy())
おわりに
この記事では、TF2.2~において、GPU使用した場合でも再現性を確保する方法を紹介しました。評価実験では、再現性を確保することは重要です。この記事がだれかの助けになれば幸いです。もし、間違いがあればご指摘お願いします。