Sequentialモデルの次に進んだ人が陥るTensorFlowパターン7選
この記事は「Python Advent Calendar 2022」の18日目の記事です!
はじめに
専攻は生物学ですが、趣味(?)で地震学の研究室にも行ってる大学4年生です。
研究室では、ディープラーニングを活用して地震を検出しています。
ちなみに、地震学というよりディープラーニング興味があります。
運よく実験の結果が出たので学会に参加してきたのですが、直前まで上手くいかずに泣きながらコードを書いていたので、記念に自分がハマった7つのパターンを挙げてみます。
Sequentialモデルの次に進んだ人が陥る7つのパターン
パターン1:グラフとeager executionが分からない
バージョン2のTensorFlowは基本的にeager executionとして動くため、TensorFlowを通常のPythonコードと同様に扱えます。しかし、モデルをコンパイルするとgraph executionとして実行されてしまうため、この違いを把握できていないと混乱してしまいます。
eager modeで動いている場合は、print
でデバッグしたりTensor.numpy
でテンソルの値を取得したりすることができます。
def func(tensor):
print(tensor) # tf.Tensor([1 2 3], shape=(3,), dtype=int32)
print(tensor.numpy()) # [1 2 3]
func(tf.constant([1, 2, 3]))
一方、tf.function
でPythonコードをラップするとgraph modeで動くようになります。graph modeは初回実行時のみPythonのように動き、同時に実行グラフを生成します。2回目以降の実行時は生成されたグラフをもとに動くため、print
やTensor.numpy
は使うことができません。
@tf.function
def func(tensor):
print(tensor)
func(tf.constant([1, 2, 3])) # Tensor("tensor:0", shape=(3,), dtype=int32)
func(tf.constant([1, 2, 3])) # グラフとして動くため、Pythonのprintでは出力されない
値を出力したい場合は、print
の代わりにtf.print
を使いましょう。
また、kerasモデルは基本的にgraph modeで動きます。ただしgraph modeではデバッグしづらいため、eager modeで挙動を確認してからgraph modeで実行するのが良いでしょう。以下のように書くことで、開発段階のモデルをeager modeで実行することができます。
# コンパイルの引数でモードを指定する
- model.compile(run_eagerly=False) # graph execution(デフォルト)
+ model.compile(run_eagerly=True) # eager execution
# もしくはグローバルでモードを指定する
- tf.config.experimental_run_functions_eagerly(False) # graph execution(デフォルト)
+ tf.config.experimental_run_functions_eagerly(True) # eager execution
eager executionやgraph execution、tf.function
についてはこちらで詳しく説明しています。
また、カスタムオブジェクトを使うときに__init__
・build
・call
の違いに混乱することがあるかもしれません。これらのメソッドは、以下のように呼び出されるタイミングが異なります。
class SimpleDense(Layer):
def __init__(self, units=32):
"""インスタンス生成時に呼び出される。"""
super(SimpleDense, self).__init__()
self.units = units
def build(self, input_shape):
"""モデルの初回実行時に呼び出される。"""
self.w = self.add_weight(shape=(input_shape[-1], self.units),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(self.units,),
initializer='random_normal',
trainable=True)
def call(self, inputs):
"""モデルの実行時に毎回呼び出される。"""
return tf.matmul(inputs, self.w) + self.b
パターン2:Dataset.mapのtf.py_functionが分からない
tf.data
はtf.data.Dataset.map
内の関数がグラフであるか否かにかかわらず、graph executionとして実行します。
前述の通りgraph executionではPythonコードを扱えないので、もしeager executionで実行したい場合(外部ライブラリなどを使用したい場合など)は以下のいずれかの方法を取りましょう。
-
AutoGraph
を用いてPythonコードをグラフに変換する。 -
tf.py_function
を使う(AutoGraph
と比較してパフォーマンスは低下する)。 -
tf.numpy_function
を使う。
パフォーマンスこそ低下しますが、tf.py_function
を使うと簡単にDataset.map
の中の処理をeager executionにすることができます。tf.py_function
では以下のように、Dataset.map
に渡したい関数をラップして使用します。
def func(x):
- # graph executionなのでprintは初回のみ実行され、numpytは使えない
- print(x)
+ # eager executionなのでprintが使え、numpyで値にアクセスできる
+ print(x.numpy())
return x, x + 5
def map_func(x):
""""
tf.py_functionでラップする。
funcはDataset.mapに渡す関数、inpはfuncの引数、Toutはfuncの戻り値の型。
"""
return tf.py_function(func=func, inp=[x], Tout=[tf.int64, tf.int64])
dataset = tf.data.Dataset.range(5)
- dataset = dataset.map(func)
+ dataset = dataset.map(map_func) # tf.py_functionでラップした関数を渡す
for data in dataset:
- pass # Tensor("args_0:0", shape=(), dtype=int64)
+ pass # 0, 1, 2, 3, 4
tf.numpy_functionは基本的にtf.py_functionと同様に使えます。ただ、tf.numpy_functionではTensor.numpy()
のように値にアクセスする必要はなく、直接値にアクセスすることができます。
AutoGraphは使ったことないのでよく分かりませんでした
パターン3:データセットの順番(対応関係)が崩れる
詳しくは別記事で紹介していますが、以下のようなコードには注意が必要です。
def func():
"""和が10になる値のタプルを返す関数"""
n = random.randint(0, 10) # Dataset.map内でrandomを扱っている
return n, 10 - n
def map_func(data):
"""mapの中で任意のPythonを書くためのラッパー"""
return tf.py_function(func=func, inp=[], Tout=[tf.int32, tf.int32])
def create_dataset():
"""入力用データセットと出力用データセットの作成"""
dataset = tf.data.Dataset.range(5)
dataset = dataset.shuffle(buffer_size=5) # Dataset.shuffleを使っている
dataset = dataset.map(map_func)
return dataset
dataset = create_dataset()
x = dataset.map(lambda x, y: x) # タプルのデータセットを入力と出力に分割
y = dataset.map(lambda x, y: y) # データセットを複数回呼び出している
for x, y in zip(x, y):
print(x, y)
Dataset.shuffle
やDataset.map
内でrandom
を使っている場合は、気を付けましょう。
パターン4:データセットが想定外の挙動をする
スコープが広いですが、Dataset
に関しては他にも注意すべきことがあります。
例えばDataset.shuffle
とDataset.batch
を使うときは、先にshuffle
した方が良いでしょう。
dataset = dataset.shuffle(buffer_size)
dataset = dataset.batch(batch_size) # シャッフルしてからバッチ化する
また、少量のデータセットで検証するときにdrop_remainder=True
にしていると、データを取得できずにハマります。
- # バッチサイズに満たないは数のデータセットは切り捨てられる
- dataset = dataset.batch(batch_size, drop_remainder=True)
+ dataset = dataset.batch(batch_size, drop_remainder=False)
さらに、データのコンタミにも注意が必要です。
パターン5:モデルのインスタンス変数が正しく更新されない
バッチ内の処理で完結せず学習全体で管理したい値には、tf.Variable
を使いましょう。
class Model(tf.keras.models.Model):
"""入力をそのまま返すだけのモデル"""
def __init__(self):
super(Model, self).__init__()
- self.counter = 0 # バッチの数をカウントする
+ self.counter = tf.Variable(0, trainable=False) # tf.Variableを使う
def call(self, data):
- self.counter += 0 # カウント
+ self.counter.assign_add(1) # Tensorはイミュータブルなので、assign_addを使う
return data
# モデルの学習
model = Model()
model.compile(loss=loss)
model.fit(dataset)
- print(model.counter) # 3
+ print(model.counter.numpy() # 5
上記の例ではバッチの数が5つですから、tf.Variable
を使わない場合に挙動がおかしくなることが分かります。
また、Tensor
がイミュータブルであることを忘れないようにしましょう。
tf.Variable
を更新するときにはassign_add
を使います。
パターン6:カスタムモデルが保存・読み込みできない
TensorFlow2では、モデルをSavedModel
またはHDF5
として保存することができます。ところがSavedModel
形式で保存しようとしたところ、公式ドキュメントの文章(以下)を読んだことでハマってしまいました。
カスタムのオブジェクト (クラスを継承したモデルやレイヤー) は保存や読み込みを行うとき、特別な注意を必要とします。以下のカスタムオブジェクトの保存*を参照してください。
......
カスタムオブジェクトの保存
SavedModel 形式を使用している場合は、このセクションをスキップできます。
上記の文章だと、SavedModel
形式を使う場合はカスタムオブジェクトが必要ないように思えます。しかし、実際はカスタムオブジェクトを別途保存する必要がありました。
(ドキュメントの書き方には今も納得してない…)
class Model(tf.keras.models.Model):
"""線形回帰するだけのモデル"""
def __init__(self):
super(Model, self).__init__()
self.relu = tf.keras.layers.Dense(1)
def call(self, data):
return self.relu(data)
+ def get_config(self): # カスタムオブジェクトを取得するメソッド
+ config = super(Model, self).get_config()
+ return config
x = tf.data.Dataset.from_tensors(np.random.rand(100, 1)) # 0-1のランダムな数値
y = x.map(lambda x: x * 1.2 + 0.3) # xを1.2倍して0.3を足す
dataset = tf.data.Dataset.zip((x, y))
dataset = dataset.batch(64)
model = Model()
model.compile(tf.keras.optimizers.Adam(learning_rate=0.1), "mse")
model.fit(dataset, epochs=100) # loss: 1.9847e-06
+ custom_objects = model.get_config() # カスタムオブジェクトの取得
model.save("/content/model")
model = tf.keras.models.load_model(
"/content/model",
+ custom_objects={"Model": Model} # カスタムオブジェクトの指定
)
model.evaluate(dataset) # loss: 1.9847e-06
上記のように、サブクラスの中でカスタムオブジェクトを取得するメソッドを定義しましょう。その後、実際に取得したカスタムオブジェクトをモデル読み込み時に渡すことで、モデルを保存・読み込みすることができます。
パターン7:PyTorchに浮気したくなる
最後のパターンは、数字を縁起の良い7に合わせるためだけに入れました。
たぶんTensorFlowに冷たくあしらわれた、多くの人が経験するパターンでしょう。
おわりに
泣きながらコードを書く人が少しでも減りますよう祈っています。
実験は計画的に。
明日の「Python Advent Calendar 2022」の記事を担当するのは@aipacommanderさんです。お楽しみに!
参考
-
パターン1:グラフとeager executionが分からない
-
パターン2:Dataset.mapのtf.py_functionが分からない
-
パターン3:データセットの順番(対応関係)が崩れる
-
パターン4:データセットが想定外の挙動をする
-
パターン5:モデルのインスタンス変数が正しく更新されない
-
パターン6:カスタムモデルが保存・読み込みできない
-
パターン7:PyTorchに浮気したくなる
Discussion