オプティマイザ(最適化用オブジェクト)
まずはオプティマイザ(最適化アルゴリズム)から(リスト4-8-1)。
# 【初中級者向けとの比較用】学習方法を設定する
# model.compile(
# tf.keras.optimizers.SGD(learning_rate=0.03), # 最適化アルゴリズム
# ……損失関数……,
# ……評価関数……)
# ###【エキスパート向け】最適化アルゴリズムを定義する ###
optimizer = tf.keras.optimizers.SGD(learning_rate=0.03) # 更新時の学習率
ご覧の通り、全く同じコードとなっている。初中級者向けとエキスパート向けの違いは、tf.keras.optimizers名前空間(別名:エイリアス:tf.optimizers名前空間でもアクセス可能)のクラス(例:SGD)のインスタンスを、
だけである。
損失関数
次に損失関数(リスト4-8-2)。
# 【初中級者向けとの比較用】学習方法を設定する
# model.compile(
# ……最適化アルゴリズム……,
# 'mean_squared_error', # 損失関数
# ……評価関数……)
# ###【エキスパート向け】損失関数を定義する ###
criterion = tf.keras.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')
tanh_accuracy()関数やTanhAccuracyクラスは、本稿で独自に定義したもので、Colabノートブック側に記述がある(詳しくはそちらを参照してほしい)。
初中級者向けとエキスパート向けの違いは、
である。
コード量がかなり増えてしまった印象がある。初中級者向けの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)> ……などと表示される
初中級者向けとエキスパート向けの違いは、データとバッチサイズを、
である。
tf.dataはTensorFlow 2.x時代の入力データ管理機能(入力パイプライン)で、エキスパート向けにミニバッチ学習をするのであれば有用なツールである。ただし、上記の通り、コードは長くなる。
コードの意味はコメントを読めば分かると思うので割愛する。ポイントは以下の通り。
続いて、学習と評価の処理を見ていこう。ちなみに、このデータセットの準備や、学習と評価の処理は、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回分の訓練/精度検証を行う。
勾配テープと自動微分
まずは、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 tapeとtape.gradient())や重みの更新(optimizer.apply_gradients())は不要である。損失(loss)のみを計算し、その結果を評価関数であるvalid_loss/valid_accuracy変数を使って保存すればよいだけである。
@tf.functionとAutoGraph
最後に、それぞれの関数の上に@tf.functionというデコレーターが付与されていることに着目してほしい。これは必須ではないが、forループなどの繰り返し処理において、パフォーマンスが良くなる効果がある。実際に上記のコードで実行時間を計測したところ、@tf.functionなしの場合に「20.3秒」かかっていたコードが、@tf.functionを付けるだけで「3.65秒」になった。実に5.6倍も高速化されたことになる。
@tf.functionによるパフォーマンス向上はケースバイケースであり、一概には言えないが、特にtrain_step/valid_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(True/False)のようなコードで、「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()) # 学習後のパラメーターの情報を表示
エポックごとのループはfor epoch in range(EPOCHS):というコードで、ミニバッチごとのループはfor train_X, train_y in train_dataset:/for valid_X, valid_y in valid_dataset:というコードで記載されている。train_dataset/valid_dataset変数は、前掲のリスト4-9-1で作成したデータセットである。
ミニバッチごとのループ内で、先ほどのリスト4-9-2で実装したtrain_step関数やvalid_step関数が呼び出されているのが分かる。これだけのコードで、ニューラルネットワークの学習と評価は実行可能である。
その他のコードは、損失や正解率といった精度検証/状況表示のためのコードとなる。
評価(メトリクス)
評価関数であるtrain_loss/train_accuracy/valid_loss/valid_accuracyには、共通して以下のようなメソッドがある(※厳密にはtf.keras.metrics.Metricクラスのメソッド)。
これらを使って、エポックごとに、まずはメトリクスの値を初期化し、全バッチを処理後にメトリクスの値を取得し、その内容をprint()関数でColabページ上に出力している(図4-7)。
評価(推移グラフ)
また、メトリクスのうち、損失の値を、各エポックの履歴としてtrain_history/valid_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-8のように表示される。
今回はSubclassing(サブクラス化)モデルの書き方について説明した。
次回の今回を踏まえて、活性化関数/レイヤー/オプティマイザ/損失関数/評価関数のカスタマイズ方法を説明する。次回はこちら。
Copyright© Digital Advantage Corp. All Rights Reserved.