第5回 お勧めの、TensorFlow 2.0最新の書き方入門(エキスパート向け)TensorFlow 2+Keras(tf.keras)入門(2/2 ページ)

» 2020年03月10日 05時00分 公開
[一色政彦デジタルアドバンテージ]
前のページへ 1|2       

オプティマイザ(最適化用オブジェクト)

 まずはオプティマイザ(最適化アルゴリズム)から(リスト4-8-1)。

# 【初中級者向けとの比較用】学習方法を設定する
# model.compile(
#     tf.keras.optimizers.SGD(learning_rate=0.03),        # 最適化アルゴリズム
#     ……損失関数……,
#     ……評価関数……)

# ###【エキスパート向け】最適化アルゴリズムを定義する ###
optimizer = tf.keras.optimizers.SGD(learning_rate=0.03# 更新時の学習率

リスト4-8-1 オプティマイザ/最適化用オブジェクトの定義(エキスパート向け)

 ご覧の通り、全く同じコードとなっている。初中級者向けとエキスパート向けの違いは、tf.keras.optimizers名前空間(別名:エイリアス:tf.optimizers名前空間でもアクセス可能)のクラス(例:SGD)のインスタンスを、

  • compile()メソッドの引数に指定するか
  • 変数に代入するか

だけである。

損失関数

 次に損失関数(リスト4-8-2)。

# 【初中級者向けとの比較用】学習方法を設定する
# model.compile(
#     ……最適化アルゴリズム……,
#     'mean_squared_error',  # 損失関数
#     ……評価関数……)

# ###【エキスパート向け】損失関数を定義する ###
criterion = tf.keras.losses.MeanSquaredError()

リスト4-8-2 損失関数の定義(エキスパート向け)

 初中級者向けとエキスパート向けの違いは、

  • 文字列(例:'mean_squared_error')で、compile()メソッドの引数に指定するか
  • tf.keras.losses名前空間(エイリアス:tf.losses名前空間)のクラス(例:MeanSquaredError)のインスタンスを、変数に代入するか

である。

 なお変数名をcriterionとしたが、これは誤差からの損失を測る「基準(criterion)」を意味する。PyTorchではcriterionが慣例で、TensorFlowではloss_objectと命名することが多いようである(今回はPyTorchとも比較しやすいように、PyTorch寄りの命名をした)。

評価関数

 次は評価関数である(リスト4-8-3)。

# 【初中級者向けとの比較用】学習方法を設定する
# model.compile(
#     ……最適化アルゴリズム……,
#     ……損失関数……,
#     [tanh_accuracy])   # 評価関数

# ### 【エキスパート向け】評価関数を定義する ###
train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = TanhAccuracy(name='train_accuracy')
valid_loss = tf.keras.metrics.Mean(name='valid_loss')
valid_accuracy = TanhAccuracy(name='valid_accuracy')

リスト4-8-3 評価関数の定義(エキスパート向け)

 tanh_accuracy()関数やTanhAccuracyクラスは、本稿で独自に定義したもので、Colabノートブック側に記述がある(詳しくはそちらを参照してほしい)。

 初中級者向けとエキスパート向けの違いは、

  • tf.keras.metrics名前空間(エイリアス:tf.metrics名前空間)の関数名(例:binary_accuracy、今回は独自のtanh_accuracy)を、compile()メソッドの引数に指定するか
  • tf.keras.metrics名前空間(エイリアス:tf.metrics名前空間)のクラス(例:Mean)のインスタンスを、変数に代入するか

である。

 コード量がかなり増えてしまった印象がある。初中級者向けのcompile()メソッドは、自動的に損失(loss)は計算してくれるし、しかも訓練データと精度検証データは分けて処理してくれる。そのため、正解率(accuracy)のみを評価関数として指定すればよい。

 一方、エキスパート向けは、内部に隠蔽されていたそれらの変数を、手動で定義してあげる必要があるというわけだ。いずれもtf.keras.metrics名前空間をベースとしたクラスを使っているが、これによって後述の学習/評価の際に、損失や正解率の計算が楽になるというメリットがある。

 以上が、compile()メソッドに対応する部分である。次にfit()メソッドに対応する部分を見ていこう。

tf.dataデータセット

 まずは訓練データ/精度検証データとミニバッチ用のバッチサイズの指定である(リスト4-9-1)。

# 【初中級者向けとの比較用】入力データを指定して学習する
# model.fit(
#     X_train, y_train,                    # 訓練データ
#     validation_data=(X_valid, y_valid),  # 精度検証データ
#     batch_size=15,                       # バッチサイズ
#     ……エポック数……,
#     ……実行状況の出力モード……)

# ###【エキスパート向け】入力データを準備する ###

# NumPy多次元配列のデータ型をデータセット(テンソル)用に統一する
X_train_f32 = X_train.astype('float32') # np.float64型 → np.float32型
y_train_f32 = y_train.astype('float32') # 同上
X_valid_f32 = X_valid.astype('float32') # 同上
y_valid_f32 = y_valid.astype('float32') # 同上

# 「入力データ(X)」と「教師ラベル(y)」を、1つの「スライスデータセット(TensorSliceDataset)」にまとめる
train_sliced = tf.data.Dataset.from_tensor_slices((X_train_f32, y_train_f32)) # 訓練用
valid_sliced = tf.data.Dataset.from_tensor_slices((X_valid_f32, y_valid_f32)) # 精度検証用

# シャッフルして(訓練データのみ)、ミニバッチ用の「バッチデータセット(BatchDataset)」にする
train_dataset = train_sliced.shuffle(250).batch(15)
valid_dataset = valid_sliced.batch(15)
print(train_dataset)
# <BatchDataset shapes: ((None, 2), (None, 1)), types: (tf.float32, tf.float32)> ……などと表示される

リスト4-9-1 ミニバッチ用データセットの準備(エキスパート向け)

 初中級者向けとエキスパート向けの違いは、データとバッチサイズを、

  • fit()メソッドの引数に指定するか
  • tf.data機能を使ってミニバッチ用データセットを作成して、変数に代入するか

である。

 tf.dataはTensorFlow 2.x時代の入力データ管理機能(入力パイプライン)で、エキスパート向けにミニバッチ学習をするのであれば有用なツールである。ただし、上記の通り、コードは長くなる。

 コードの意味はコメントを読めば分かると思うので割愛する。ポイントは以下の通り。

  • データ型は「float32」に統一した方が扱いやすい(astype(データ型)メソッド利用)
  • tf.data.Dataset.from_tensor_slices()メソッドで、NumPy多次元配列データをtf.dataの世界に持ってこられる
  • 訓練時には基本的にシャッフルが必要。shuffle(バッファサイズ)メソッドのバッファサイズの部分には(基本的に)全データ件数以上を指定する
  • 最後にbatch(バッチサイズ)メソッドを呼び出して、訓練用/精度検証用のデータセットを生成すれば完了

 続いて、学習と評価の処理を見ていこう。ちなみに、このデータセットの準備や、学習と評価の処理は、PyTorchのコードにかなり似ている。「PyTorch入門: 第3回 PyTorchによるディープラーニング実装手順の基本」を見ると、ほとんど見分けが付かないくらい同じであることが分かるだろう。よって、コード内容の解説も基本的には同じような内容となる。

学習(1回分)

 fit()メソッドでは、学習と評価の処理は完全に隠蔽されており、対応するコード部分がないので、エキスパート向けのコードのみを示す(リスト4-9-2)。

import tensorflow.keras.backend as K

# ###【エキスパート向け】訓練する(1回分) ###
@tf.function
def train_step(train_X, train_y):
  # 訓練モードに設定
  training = True
  K.set_learning_phase(training)  # tf.keras内部にも伝える

  with tf.GradientTape() as tape: # 勾配をテープに記録
    # フォワードプロパゲーションで出力結果を取得
    #train_X                                   # 入力データ
    pred_y = model(train_X, training=training) # 出力結果
    #train_y                                   # 正解ラベル

    # 出力結果と正解ラベルから損失を計算し、勾配を求める
    loss = criterion(pred_y, train_y)     # 誤差(出力結果と正解ラベルの差)から損失を取得
  
  # 逆伝播の処理として勾配を計算(自動微分)
  gradient = tape.gradient(loss, model.trainable_weights)

  # 勾配を使ってパラメーター(重みとバイアス)を更新
  optimizer.apply_gradients(zip(gradient, model.trainable_weights)) # 指定されたデータ分の最適化を実施

  # 損失と正解率を算出して保存
  train_loss(loss)
  train_accuracy(train_y, pred_y)

# ###【エキスパート向け】精度検証する(1回分) ###
@tf.function
def valid_step(valid_X, valid_y):
  # 評価モードに設定(dropoutなどの挙動が評価用になる)
  training = False
  K.set_learning_phase(training)  # tf.keras内部にも伝える

  # フォワードプロパゲーションで出力結果を取得
  #valid_X                                   # 入力データ
  pred_y = model(valid_X, training=training) # 出力結果
  #valid_y                                   # 正解ラベル

  # 出力結果と正解ラベルから損失を計算
  loss = criterion(pred_y, valid_y)     # 誤差(出力結果と正解ラベルの差)から損失を取得
  # 評価時は勾配を計算しない

  # 損失と正解率を算出して保存
  valid_loss(loss)
  valid_accuracy(valid_y, pred_y)

リスト4-9-2 1回分の「訓練」と「精度検証」の処理(エキスパート向け)

 リスト4-9-2を見ると、

  • 訓練(学習)処理を実装したtrain_step関数
  • 精度検証(評価)処理を実装したvalid_step関数

がある。それぞれ、ミニバッチ学習の1回分の訓練/精度検証を行う。

勾配テープと自動微分

 まずは、train_step関数の中身を見てみよう。pred_y = model(train_X)というコードで推論が行われて予測値を取得している。loss = criterion(pred_y, train_y)というコードで、その予測値と正解ラベルを使って、損失(loss)を計算している。この2行のコードは、with tf.GradientTape() as tapeというスコープ内で実行されていることに注目してほしい。

 tf.GradientTapeクラスは、勾配テープGradient Tape)と呼ばれるTensorFlow 2.0の新機能である。「勾配」とは、損失を微分することによって得られる値(接線の傾き)のことである。「テープ」とは“カセットテープ”に音楽を記録するのをイメージするとよい(……なんでこんな古くさい命名にしたのだろうか)。つまり、スコープ内で取得した損失(loss)を使って、自動微分Automatic Differentiation)した際に、計算された勾配が記録されるようになる、というわけである。

 自動微分(すなわちバックプロパゲーション)を行っているのが、gradient = tape.gradient(loss, model.trainable_weights)というコードである。tapeは先ほどの勾配テープである。tape.gradient()メソッドで勾配を計算して、変数gradientに代入している。メソッドの引数には、損失(losss)と、モデル内の訓練可能な重み(model.trainable_weights)が指定されている。

重みの更新

 しかし、この段階ではまだ勾配が計算されただけで、モデル内の重みやバイアスは更新されていない。optimizer.apply_gradients(zip(gradient, model.trainable_weights))メソッドを呼び出すことで初めて更新される仕組みである。

損失と正解率

 以上で1回分の学習が終わった。よってここで、1回分の損失と正解率を計算して、状況を確認しておきたい。これを簡単に行うのが、前掲のリスト4-8-3で定義した評価関数である。訓練用の損失の評価関数はtrain_loss、正解率の評価関数はtrain_accuracyという変数に定義していた。これらを関数のように呼び出しているのが、train_loss(loss)train_accuracy(train_y, pred_y)という2行のコードである。これだけで、損失と正解率が(計算&)保存される。

精度検証の処理について

 valid_step関数の方は、学習するわけではないので勾配計算(with tf.GradientTape() as tapetape.gradient())や重みの更新(optimizer.apply_gradients())は不要である。損失(loss)のみを計算し、その結果を評価関数であるvalid_lossvalid_accuracy変数を使って保存すればよいだけである。

@tf.functionとAutoGraph

 最後に、それぞれの関数の上に@tf.functionというデコレーターが付与されていることに着目してほしい。これは必須ではないが、forループなどの繰り返し処理において、パフォーマンスが良くなる効果がある。実際に上記のコードで実行時間を計測したところ、@tf.functionなしの場合に「20.3秒」かかっていたコードが、@tf.functionを付けるだけで「3.65秒」になった。実に5.6倍も高速化されたことになる。

 @tf.functionによるパフォーマンス向上はケースバイケースであり、一概には言えないが、特にtrain_stepvalid_step関数に指定するのがお勧めである。他には、リスト4-1に示したtf.keras.Model派生クラス内のcallメソッドに付けることもできる。

 @tf.functionは、TensorFlow 2.0でEager(即時)実行モードがデフォルトになったために導入された機能である。Eager実行モードでは、説明済みだが、実行時に動的に計算グラフが作成される。しかし何度も動的に作成すると、当然、実行速度は遅くなってしまう。そこで、1回目の実行の際に計算グラフを構築したら、2回目はそれを静的な計算グラフのように使い回すと、より処理が高速化するだろう。@tf.functionはこれを行うための機能である。

 また@tf.functionは、Pythonのif文やfor文/while文などで書かれたコードを、計算グラフ用のtf.cond()関数やtf.while_loop()関数などに自動的に置き換える。この機能は、AutoGraph自動グラフ)と呼ばれている(「Grad」ではなく「Graph」、見間違いに注意)。

 ただし、副作用もあるので、どこにでも指定できるわけではなく、指定する関数のコードも注意深く書く必要がある。詳しくは「TensorFlow 2.0 での tf.function と AutoGraph | TensorFlow Core」を参照してほしい。

【コラム】訓練/評価モードについて

 本稿では、Dropout処理などに使えるtraining引数について説明した。TensorFlow 2.x時代のカスタムトレーニングでは、このような明示的な訓練/評価モードの指定が推奨されている。「推奨」扱いではあるが、この指定はコードを書く人が「確実」に制御した方がよい。

 というのも、tf.keras内部ではグローバルな状態管理として「Learning Phase」というフラグを管理しているからだ。例えばtf.keras.layers.BatchNormalizationクラスの内部では、フォワードプロパゲーションで呼び出された際にtraining引数が指定されなかった場合には、「Learning Phase」フラグが暗黙的に参照される仕様となっている。そのため、訓練/評価モードが意図しない形で働き、結果がおかしくなる可能性がある(TensorFlowのIssuesを見ると、このワナにはまる人は多そうである)。

 ちなみに、初中級者向けのfit()メソッドで訓練(True)と精度検証(False)を行ったときや、テスト(False)用のevaluate()メソッドを呼び出したときは、このフラグはtf.keras内部で自動的に切り替わるので、あまり気にする必要がない。あくまで問題となりそうなのは、エキスパート向けのカスタムトレーニングの場合である。

 そこでリスト4-9-2では念のため、training引数だけでなく、import tensorflow.keras.backend as K; K.learning_phase(TrueFalse)のようなコードで、「Learning Phase」フラグも同時に指定するようにしたので、参考にしてほしい。

 なお、「Learning Phase」フラグを取得するには、tf.keras.backend.learning_phase()関数を呼び出せばよい。


学習(ループ処理)

 いよいよ大詰めだ。ミニバッチ学習で、バッチごとに学習&評価する処理を、forループを使って記載すればよい。まずデータ全体に相当するエポックごとのforループを作り、その中にミニバッチごとのforループを作る、という2階層の構造を作る。これを行っているのが、リスト4-10である。

# 【初中級者向けとの比較用】入力データを指定して学習する
# model.fit(
#     ……訓練データ(入力)……, ……同(ラベル)……,
#     ……精度検証データ……,
#     ……バッチサイズ……,
#     epochs=100,  # エポック数
#     verbose=1)   # 実行状況の出力モード

# ###【エキスパート向け】学習する ###

# 定数(学習/評価時に必要となるもの)
EPOCHS = 100             # エポック数: 100

# 損失の履歴を保存するための変数
train_history = []
valid_history = []

for epoch in range(EPOCHS):
  # エポックのたびに、メトリクスの値をリセット
  train_loss.reset_states()      # 「訓練」時における累計「損失値」
  train_accuracy.reset_states()  # 「訓練」時における累計「正解率」
  valid_loss.reset_states()      # 「評価」時における累計「損失値」
  valid_accuracy.reset_states()  # 「評価」時における累計「正解率」

  for train_X, train_y in train_dataset:
    # 【重要】1ミニバッチ分の「訓練(学習)」を実行
    train_step(train_X, train_y)
          
  for valid_X, valid_y in valid_dataset:
    # 【重要】1ミニバッチ分の「評価(精度検証)」を実行
    valid_step(valid_X, valid_y)

  # ミニバッチ単位で累計してきた損失値や正解率の平均を取る
  n = epoch + 1                          # 処理済みのエポック数
  avg_loss = train_loss.result()         # 訓練用の平均損失値
  avg_acc = train_accuracy.result()      # 訓練用の平均正解率
  avg_val_loss = valid_loss.result()     # 訓練用の平均損失値
  avg_val_acc = valid_accuracy.result()  # 訓練用の平均正解率

  # グラフ描画のために損失の履歴を保存する
  train_history.append(avg_loss)
  valid_history.append(avg_val_loss)

  # 損失や正解率などの情報を表示
  print(f'[Epoch {n:3d}/{EPOCHS:3d}]' \
        f' loss: {avg_loss:.5f}, acc: {avg_acc:.5f}' \
        f' val_loss: {avg_val_loss:.5f}, val_acc: {avg_val_acc:.5f}')

print('Finished Training')
print(model.get_weights())  # 学習後のパラメーターの情報を表示

リスト4-10 「訓練」と「精度検証」をバッチサイズ単位でエポック回繰り返す(エキスパート向け)

 エポックごとのループはfor epoch in range(EPOCHS):というコードで、ミニバッチごとのループはfor train_X, train_y in train_dataset:for valid_X, valid_y in valid_dataset:というコードで記載されている。train_datasetvalid_dataset変数は、前掲のリスト4-9-1で作成したデータセットである。

 ミニバッチごとのループ内で、先ほどのリスト4-9-2で実装したtrain_step関数やvalid_step関数が呼び出されているのが分かる。これだけのコードで、ニューラルネットワークの学習と評価は実行可能である。

 その他のコードは、損失や正解率といった精度検証/状況表示のためのコードとなる。

評価(メトリクス)

 評価関数であるtrain_losstrain_accuracyvalid_lossvalid_accuracyには、共通して以下のようなメソッドがある(厳密にはtf.keras.metrics.Metricクラスのメソッド)。

  • reset_states()メソッド: 損失や正解率といったメトリクス(測定値)をリセット
  • result()メソッド: メトリクス(測定値)の取得

 これらを使って、エポックごとに、まずはメトリクスの値を初期化し、全バッチを処理後にメトリクスの値を取得し、その内容をprint()関数でColabページ上に出力している(図4-7)。

図4-7 Colabページ上への出力例 図4-7 Colabページ上への出力例

評価(推移グラフ)

 また、メトリクスのうち、損失の値を、各エポックの履歴としてtrain_historyvalid_history変数に保存している。このようにすることで、グラフを描画することも可能である(リスト4-11)。

import matplotlib.pyplot as plt

# 学習結果(損失)のグラフを描画
epochs = len(train_history)
plt.plot(range(epochs), train_history, marker='.', label='loss (Training data)')
plt.plot(range(epochs), valid_history, marker='.', label='loss (Validation data)')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

リスト4-11 損失値の推移グラフ描画

 これを実行すると、図4-8のように表示される。

図4-8 損失値の推移グラフ描画例 図4-8 損失値の推移グラフ描画例

次回について

 今回はSubclassing(サブクラス化)モデルの書き方について説明した。

 次回の今回を踏まえて、活性化関数/レイヤー/オプティマイザ/損失関数/評価関数のカスタマイズ方法を説明する。次回はこちら

「TensorFlow 2+Keras(tf.keras)入門」のインデックス

TensorFlow 2+Keras(tf.keras)入門

前のページへ 1|2       

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。