TensorFlow 2.x(2.0以降)時代のモデルの書き方として、tf.keras.Modelサブクラス化モデルの書き方を詳しく解説。@tf.functionやAutoGraph、勾配テープといったTensorFlow 2.0の新機能についても触れる。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
TensorFlow 2.x(2.0以降)には、主に3種類のAPIがあり、6通りの書き方がある。
このうち、(1)〜(3)を前回説明した。引き続き今回は(4)を、次回は(5)〜(6)を解説する*1。それではさっそく説明に入ろう。※脚注や図、コードリストの番号は前回からの続き番号としている(前回・今回・次回は、切り離さず、ひとまとまりの記事として読んでほしいため連続性を持たせている)。
*1 (4)〜(6)としていたが、記事ボリュームがあるので、(4)と(5)〜(6)でさらに分割した。本連載はディープラーニングについては入門者をターゲットとして意識しているが、次回まで(全3回)はTensorFlowの書き方のバリエーション(プログラミング方法の知識)について具体的に紹介する。今後の連載を読み進めるうえで、知識として書き方をざっと理解してほしい。
公式チュートリアルでは、「初心者向け」と「エキスパート向け」という2つのクイックスタートが用意されており、これは前掲の図における(1)と(4)に該当する。本連載ではそれに合わせて、初心者は(1)、初中級者以上は(4)をお勧めとしている。
(4)や(5)のSubclassing(サブクラス化)モデルは、「Pythonを使ってコーディングしている」感がより強まる。それに伴い、どうしてもコード量が飛躍的に増えがちである。しかしこれは、(1)や(2)のSequentialモデルだとメソッド内に隠蔽(いんぺい)されていた学習処理を細かく制御したり、柔軟なカスタマイズができたりするというメリットの裏返しでもある。
どの書き方にもメリット/デメリットはあるので、まずは使える書き方や、気に入った書き方から始めてみるのもよいだろう。特にKerasに慣れている人の中には、「(4)/(5)の書き方は受け入れられない」と感じる人も少なくないかもしれない。そうであれば、今後も高度な実装には(3)のFunctional APIを使い続ければ問題ない。
前回〜今回〜次回では、書き方を一通り、知識として知ってもらうことを目的としている。決して無理に特定の書き方を使わせようとしているわけではないので、そういう目的で読んでいただけるとうれしい。
今回も前回に引き続き、Python(バージョン3.6)と、ディープラーニングのライブラリ「TensorFlow」の最新版2.1を利用する。また、開発環境にGoogle Colaboratory(以下、Colab)を用いる。
前提条件の準備は前回と同じなので、説明を割愛する。詳しくはColabのノートブックを確認してほしい。
それでは準備が整ったとして、(4)を説明していこう。
この方法は、前回説明した(3)Functional APIと共通する部分が多く、ほとんどが流用できる。具体的にはまず、class NeuralNetwork(tf.keras.Model):というコード(NeuralNetworkクラス)を追記すればよい。リスト4-1の太字部分を中心に見てほしい。
# ### 活性化関数を変数(ハイパーパラメーター)として定義 ###
# 変数(モデル定義時に必要となる数値)
activation1 = layers.Activation('tanh' # 活性化関数(隠れ層用): tanh関数(変更可能)
, name='activation1' # 活性化関数にも名前付け
)
activation2 = layers.Activation('tanh' # 活性化関数(隠れ層用): tanh関数(変更可能)
, name='activation2' # 活性化関数にも名前付け
)
acti_out = layers.Activation('tanh' # 活性化関数(出力層用): tanh関数(固定)
, name='acti_out' # 活性化関数にも名前付け
)
# tf.keras.Modelをサブクラス化してモデルを定義
class NeuralNetwork(tf.keras.Model):
# ### レイヤーを定義 ###
def __init__(self, *args, **kwargs):
super(NeuralNetwork, self).__init__(*args, **kwargs)
# 入力層は定義「不要」。実際の入力によって決まるので
# 隠れ層:1つ目のレイヤー
self.layer1 = layers.Dense( # 全結合層
#input_shape=(INPUT_FEATURES,), # 入力層(定義不要)
name='layer1', # 表示用に名前付け
units=LAYER1_NEURONS) # ユニットの数
# 隠れ層:2つ目のレイヤー
self.layer2 = layers.Dense( # 全結合層
name='layer2', # 表示用に名前付け
units=LAYER2_NEURONS) # ユニットの数
# 出力層
self.layer_out = layers.Dense( # 全結合層
name='layer_out', # 表示用に名前付け
units=OUTPUT_RESULTS) # ユニットの数
# ### フォワードパスを定義 ###
def call(self, inputs, training=None): # 入力と、訓練/評価モード
# 「出力=活性化関数(第n層(入力))」の形式で記述
x1 = activation1(self.layer1(inputs)) # 活性化関数は変数として定義
x2 = activation2(self.layer2(x1)) # 同上
outputs = acti_out(self.layer_out(x2)) # ※活性化関数は「tanh」固定
return outputs
# ### モデルの生成 ###
# model = tf.keras.Model(inputs=inputs, outputs=outputs
# , name='model_constructor' # モデルにも名前付け
# )
model = NeuralNetwork() # モデルの生成
# ### 以上でモデル設計は完了 ###
#model.summary() # モデル内容の出力はできない!(後述)
独自のNeuralNetworkクラスを追加したうえで、「レイヤーを定義」するコード群はコンストラクターとなる__init__()メソッド内にまとめ、(順伝播の)「フォワードパスを定義」するコード群はdef call(self, inputs)メソッドにまとめている。メソッド化したことで、self.やreturn outputsといったコードが必要になっている。手順をまとめると次のようになる。
この2つのメソッドは、適切なタイミングで自動的に呼び出される仕組みになっている。
ちなみに、3番はdef call(self, inputs, training):と、引数trainingを追加することもできる。この引数は、訓練時の呼び出しか(True)、精度検証(=評価)時の呼び出しか(False)、によって処理を切り分けるのに利用できる。例えば評価時はDropout処理を無効にすることなどが考えられる。trainingの値は、モデルの推論時に指定できるので、後ほど再度説明する。
(3)Functional APIと比べると、「入力層」の記述がなくなっている。これは、def call(self, inputs):メソッドの引数inputsに実際のTensorFlowテンソルデータが来るので、わざわざ用意しなくてよくなったためである。
続いて「モデルの生成」に関しては、model = NeuralNetwork()と、サブクラスを単にインスタンス化するだけでよくなっている。
以上、(3)Functional APIと比べると確かにコードの行数は増えたが、コードにまとまりができて見やすくなっているとも言える。こうやってコードを見直すと、Subclassingモデルも全く難しくなく、良いと感じられるのではないだろうか。
リスト4-1の最終行にあるmodel.summary()というコードはコメントアウトしていたが、これは実行するとエラーになるからである(リスト4-2)。
# モデルの内容を出力
model.summary() # エラー! ← 計算グラフが構築できていないため
# エラー出力例
# ValueError: This model has not yet been built. Build the model first by calling `build()` or calling `fit()` with some data, or specify an `input_shape` argument in the first layer(s) for automatic build.
前回も説明したが、(1)/(2)のSequentialモデルや(3)のFunctional APIは宣言型(いわゆる“Define-and-Run”)なのでモデルを定義&インスタンス化した段階で計算グラフが静的に構築される。一方、(4)/(5)のSubclassingモデルは、命令型(いわゆる“Define-by-Run”)なので実行した段階で計算グラフが動的に構築される仕様である。
方法1
つまり、まだ実行していないから、計算グラフが構築されておらず、モデルの内容と構成図の出力ができない、というわけだ。であれば、推論を1回実行して、計算グラフを動的に作成すればうまくいくはずである(リスト4-3)。
model1 = NeuralNetwork(name='subclassing_model1') # モデルの生成
# 方法1: 推論して動的にグラフを構築する
temp_input = [[0.1,-0.2]] # 仮の入力値
temp_output = model1.predict(temp_input) # 推論の実行
# モデルの内容を出力
model1.summary()
# モデルの構成図を表示
tf.keras.utils.plot_model(model, show_shapes=True, show_layer_names=True, to_file='model.png')
from IPython.display import Image
Image(retina=False, filename='model.png')
Subclassingモデルの推論方法については後述するが、リスト4-3ではこれまで通り、predict()メソッドを使って推論している。この結果、図4-1に示すモデル内容と、図4-2に示す構成図の出力が得られた。
出力で気になるのは、図4-2の構成図が正確に描画されない点である。モデルの生成時に引数nameに指定した名前も反映されていない。(今後改善される可能性はあるが)この方法による構成図の出力は諦めた方がいいだろう。
方法2
他に方法はないのだろうか。前掲のリスト4-2のエラー出力例に「build()を呼び出すか」という記述があった。そこでbuild()を呼び出してみたのが、リスト4-4である。
model2 = NeuralNetwork(name='subclassing_model2') # モデルの生成
# 方法2: 入力形状を指定して計算グラフを構築する
model2.build( # モデルのビルド
input_shape=(None,2)) # 入力の形状(=入力層)※タプル形式
# モデルの内容を出力
model2.summary()
# モデルの構成図を表示
tf.keras.utils.plot_model(model2, show_shapes=True, show_layer_names=True, to_file='model.png')
from IPython.display import Image
Image(retina=False, filename='model.png')
build()メソッドでは、引数input_shape引数の指定が必須である。input_shapeには(ミニバッチサイズ, 入力データ次元)の形式で指定する。「ミニバッチサイズ」は未知なのでNoneとしている。「入力データ次元」はX座標とY座標の2つなので2としている。
この結果、図4-3に示すモデル内容と、図4-4に示す構成図の出力が得られた。
モデルの生成時に引数nameに指定した名前は反映されるようになったが、2番目の方法でも、モデルの構成図は適切には出力されない。Subclassingモデルの基本機能では、こういった制約があるのだ。
方法3
しかし、裏技テクニックを使えば、(3)のFunctional APIと同じような詳しいモデル内容と構成図を出力することも可能である(リスト4-5)。
model3 = NeuralNetwork() # モデルの生成
# 「仮のモデル」をFunctional APIで生成する独自関数
def get_functional_model(model):
# このコードは、「リスト3-1のFunctional API」とほぼ同じ
x = layers.Input(shape=(INPUT_FEATURES,), name='layer_in')
temp_model = tf.keras.Model(
inputs=[x],
outputs=model.call(x), # ※サブクラス化したモデルの`call`メソッドを指定
name='subclassing_model3') # 仮モデルにも名前付け
return temp_model
# Functional APIの「仮のモデル」を取得
f_model = get_functional_model(model3)
# モデルの内容を出力
f_model.summary()
# モデルの構成図を表示
tf.keras.utils.plot_model(f_model, show_shapes=True, show_layer_names=True, to_file='model.png')
from IPython.display import Image
Image(retina=False, filename='model.png')
リスト4-5がやっていることは、(4)のSubclassingモデルで作成したモデル(model3)をベースに、(3)のFunctional APIのモデルとして再作成した「仮のモデル」(f_model)を使って、その内容と構成図を出力している、ということである。Functional API化されているので、当然ながら、詳しい情報が出力できるというわけだ。なお、公式に説明されている方法ではない(念のため)。
出力は図4-5、図4-6のようになる。
以上で、「(4)Modelクラスのサブクラス化」の説明は終わりである。しかし学習や推論については省略した。これらは、(1)のSequentialモデルと同様の記述が可能だが、よりエキスパート向けの書き方があるので、少し長くなるが、これについても説明しておきたい。
初中級者向け書き方の復習
ではまずは、(1)のSequentialモデルで紹介した「学習と推論の書き方」を復習しておこう。リスト4-6は、前回の「リスト1-4 学習方法を設定、学習、推論(予測)するコード(共通)」の再掲となっている。
# 学習方法を設定し、学習し、推論(予測)する
model.compile(tf.keras.optimizers.SGD(learning_rate=0.03), 'mean_squared_error', [tanh_accuracy])
model.fit(X_train, y_train, validation_data=(X_valid, y_valid), batch_size=15, epochs=100, verbose=1)
model.predict([[0.1,-0.2]])
よって説明は割愛する。これと比較しながら、エキスパート向けの書き方だとどうなるかを見ていこう。
推論
まずは簡単に使える「推論」から説明する(リスト4-7)。
# 【初中級者向けとの比較用】推論する
# python_input2 = [[0.1,-0.2]]
# predict2 = model.predict(python_input2)
# print(predict2)
# # [[0.9845763]] ……などと表示される
# ###【エキスパート向け】推論する ###
python_input1 = [[0.1,-0.2]] # 入力値(Pythonリスト値/NumPy多次元配列値)
# Pythonリスト値やNumPy多次元配列値はテンソルにいったん変換する必要がある
tensor_input1 = tf.convert_to_tensor(python_input1, dtype=tf.float32)
#tensor_input1 = tf.constant([[0.1,-0.2]]) # 入力値(定数)
#tensor_input1 = tf.Variable([[0.1,-0.2]]) # 入力値(変数)
predict1 = model(tensor_input1) # ※テンソルの入力しか受け付けない
print(predict1) # ※テンソルが出力される
# tf.Tensor([[0.9845763]], shape=(1, 1), dtype=float32) ……などと表示される
推論(予測:predict)を実行しているのは太字の部分である。model.predict()メソッドの呼び出しが、model()という呼び出しに変わっている点に注目してほしい。modelはNeuralNetworkクラスのオブジェクトであるが、関数のように呼び出すと、クラス内部にある__call__()特殊メソッドが呼び出される仕組みだ。その__call__()特殊メソッドは、内部で(リスト4-1で実装した)call()メソッドを呼び出すことで、フォワードプロパゲーション(順伝播)が実行されて予測値が計算されることになる(=推論)。
注意点として、tf.keras機能のpredict()メソッドは、Pythonリスト値やNumPy多次元配列値を入力に受け取り、出力する。一方、model()関数は、TensorFlowテンソルを入力に受け取り、出力する、という違いがある。つまり、エキスパート向けのmodel()関数の方が、よりTensorFlow内部に近い処理ということになる。
TensorFlowテンソルの取得方法として、下記の3つの方法をリスト4-7に含めているので確認しておこう。
ところで、前掲の4-1の説明ではdef call(self, inputs, training):というコードでtraining値を受け取れると説明した。この値は、model(tensor_input1, training=True)のような形で、推論時に指定できる。訓練時にフォワードプロパゲーションを実行する場合はtraining=True、評価時はtraining=Falseを指定すればよい。
学習について
さて、推論は以上である。以下では学習を説明する。学習の書き方について、下記の2種類があることは前回説明済みである。
「学習」は、1行1行の内容が大事になっているので、次のページでは1行ごとに細かく分けて説明していく。
オプティマイザ(最適化用オブジェクト)
Copyright© Digital Advantage Corp. All Rights Reserved.