TF-Slim : TensorFlow 軽量ライブラリ Slim (2)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 04/27/2017
* 本ページは、github 上の TensorFlow-Slim の README.md の後半を動作確認・翻訳した上で適宜、補足説明したものです :
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/slim/README.md
* 前半部翻訳は こちら。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
モデルの訓練
TensorFlow モデルの訓練は、モデル、損失関数、勾配計算そして損失に関するモデルの重みの勾配を反復的に計算してそれに従って重みを更新する訓練ルーチンが必要です。TF-Slim は、訓練と評価ルーチンを実行する一般的な損失関数とヘルパー関数の両者のセットを提供します。
損失
損失関数は最小化したい量を定義します。分類問題のためには、これは典型的には真の分布と予測される確率分布の間のクラスに渡る交差エントロピーです。回帰問題のためには、これはしばしば予測値と真の値の平方和誤差 (SSD, sum-of-squares differences) です。
マルチタスク学習モデルのような、あるモデルでは同時に複数の損失関数の使用を必要とします。換言すれば、究極的に最小化される損失関数は様々な他の損失関数の総和です。例えば、各ピクセルのカメラからの深度 (= 原文: the depth from the camera of each pixel) に加えて画像の場面のタイプの両者を予測するというモデルを考えてください。このモデルの損失関数は分類損失と深度予測損失の総和になるでしょう。
TF-Slim は、損失モジュールを通して損失関数を定義して追跡するための使いやすい機構を提供します。VGG ネットワークを訓練することを望む単純なケースについて考えます :
import tensorflow as tf vgg = tf.contrib.slim.nets.vgg # 画像とラベルをロードする。 images, labels = ... # モデルを作成する。 predictions, _ = vgg.vgg_16(images) # 損失関数を定義して損失総和を得る。 loss = slim.losses.softmax_cross_entropy(predictions, labels)
この例では、(TF-Slim の VGG 実装を使用して) モデルを作成することから始めて、標準的な分類損失を追加しています。さて、複数の出力を生成するマルチタスク・モデルを持つケースに取り掛かりましょう :
# 画像とラベルをロードする。 images, scene_labels, depth_labels = ... # モデルを作成する。 scene_predictions, depth_predictions = CreateMultiTaskModel(images) # 損失関数を定義して損失総和を得ます。 classification_loss = slim.losses.softmax_cross_entropy(scene_predictions, scene_labels) sum_of_squares_loss = slim.losses.sum_of_squares(depth_predictions, depth_labels) # 次の2行は同じ効果を持ちます : total_loss = classification_loss + sum_of_squares_loss total_loss = slim.losses.get_total_loss(add_regularization_losses=False)
この例では、slim.losses.softmax_cross_entropy と slim.losses.sum_of_squares を呼び出すことにより追加した2つの損失を持ちます。それらを一緒に (total_loss) 加算するか slim.losses.get_total_loss() を呼び出すことで損失総和を得ることができます。これはどのように動作するのでしょう? TF-Slim を通して損失関数を作成する時、TF-Slim はその損失を損失関数の特別な TensorFlow コレクションに追加します。これは手動で損失総和を管理することを有効にするか、TF-Slim に貴方のためにそれらを管理させることを可能にします。
TF-Slim に貴方のために損失を管理させることを望み、しかしカスタム損失関数を持つことを望む場合はどうでしょう? loss_ops.py はまたこの損失を TF-Slim コレクションに追加する関数を持ちます。例えば :
# 画像とラベルをロードする。 images, scene_labels, depth_labels, pose_labels = ... # モデルを作成する。 scene_predictions, depth_predictions, pose_predictions = CreateMultiTaskModel(images) # 損失関数を定義して損失総和を得る。 classification_loss = slim.losses.softmax_cross_entropy(scene_predictions, scene_labels) sum_of_squares_loss = slim.losses.sum_of_squares(depth_predictions, depth_labels) pose_loss = MyCustomLossFunction(pose_predictions, pose_labels) slim.losses.add_loss(pose_loss) # TF-Slim に追加の損失について知らせる。 # 損失総和を計算する次の2つの方法は同値です : regularization_loss = tf.add_n(slim.losses.get_regularization_losses()) total_loss1 = classification_loss + sum_of_squares_loss + pose_loss + regularization_loss # (正則化損失はデフォルトで損失総和に含まれます。) total_loss2 = slim.losses.get_total_loss()
この例では、再び手動で損失総和関数を生成することもできるし、あるいは TF-Slim に追加の損失について知らせて TF-Slim のこの損失を処理させることもできます。
訓練ループ
TF-Slim は、learning.py で見つかる、モデル訓練のための単純でしかし強力なツールのセットを提供します。これらは、繰り返し損失を計測して、勾配を計算してモデルをディスクに保存する訓練関数を含み、もちろん勾配を操作するための幾つかの便利な関数も含みます。例えば、モデル、損失関数そして最適化スキームをひとたび指定すれば、最適化を実行するために slim.learning.create_train_op と slim.learning.train を呼び出すことができます :
g = tf.Graph() # モデルを作成して損失を指定する… ... total_loss = slim.losses.get_total_loss() optimizer = tf.train.GradientDescentOptimizer(learning_rate) # create_train_op は、損失を求めるたびに、 # update_ops が実行されて計算される勾配がまた適用されることを保証します。 train_op = slim.learning.create_train_op(total_loss, optimizer) logdir = ... # ここで checkpoint が保存されます。 slim.learning.train( train_op, logdir, number_of_steps=1000, save_summaries_secs=300, save_interval_secs=600):
この例では、slim.learning.train に train_op が提供され、これは (a) 損失を計算して (b) 勾配ステップを適用するために使用されます。logdir は checkpoint とイベントファイルが保存されるディレクトリを指定します。取られる勾配ステップの数は任意の数に制限できます。この場合、1000 ステップを取ることを求めています。最後に、save_summaries_secs=300 は 5 分毎に summaries を計算することを示し、save_interval_secs=600 は 10 分毎にモデル checkpoint を保存することを示します。
実施例: VGG16 モデルを訓練する
これを示すために、VGG ネットワークを訓練する次のサンプルを検証しましょう :
import tensorflow as tf slim = tf.contrib.slim vgg = tf.contrib.slim.nets.vgg ... train_log_dir = ... if not tf.gfile.Exists(train_log_dir): tf.gfile.MakeDirs(train_log_dir) with tf.Graph().as_default(): # データ・ロードをセットアップ : images, labels = ... # モデルを定義する : predictions = vgg.vgg16(images, is_training=True) # 損失関数を指定する : slim.losses.softmax_cross_entropy(predictions, labels) total_loss = slim.losses.get_total_loss() tf.summary.scalar('losses/total_loss', total_loss) # 最適化スキームを指定する : optimizer = tf.train.GradientDescentOptimizer(learning_rate=.001) # create_train_op は損失を得るためにそれを評価する時、 # update_ops が行なわれて勾配更新が計算されることを保証します。 train_tensor = slim.learning.create_train_op(total_loss, optimizer) # 実際に訓練を実行します。 slim.learning.train(train_tensor, train_log_dir)
既存モデルを再調整する
Checkpoint から変数を復元する簡単な復習
モデルが訓練された後は、それは与えられた checkpoint から変数を復元する tf.train.Saver() を使用して復元されます。多くの場合、tf.train.Saver() は全てあるいは 2, 3 だけの変数を復元する単純な機構を提供します。
# 幾つかの変数を作成する。 v1 = tf.Variable(..., name="v1") v2 = tf.Variable(..., name="v2") ... # 全ての変数を復元するための op を追加する。 restorer = tf.train.Saver() # 幾つかの変数を復元するための op を追加する。 restorer = tf.train.Saver([v1, v2]) # 後で、モデルを launch し、ディスクから変数を復元するために saver を使用し、 # そしてモデルである作業を行ないます。 with tf.Session() as sess: # ディスクから変数を復元する。 restorer.restore(sess, "/tmp/model.ckpt") print("Model restored.") # モデルである作業を行なう ...
より詳細は、変数 のページの「変数を復元する」と「保存し復元する変数を選択する」の項を見てください。
部分的にモデルを復元する
全体的に新しいデータセット上か新しいタスク上でさえも事前に訓練されたモデルを再調整することはしばしば望ましいです。これらの状況では、復元する変数のサブセットを選択するために TF-Slim のヘルパー関数を使用できます :
# 幾つかの変数を作成する。 v1 = slim.variable(name="v1", ...) v2 = slim.variable(name="nested/v2", ...) ... # 復元する変数のリストを得る (それは 'v2' のみを含みます)。 # これらは全て同値な方法です : variables_to_restore = slim.get_variables_by_name("v2") # あるいは variables_to_restore = slim.get_variables_by_suffix("2") # あるいは variables_to_restore = slim.get_variables(scope="nested") # あるいは variables_to_restore = slim.get_variables_to_restore(include=["nested"]) # あるいは variables_to_restore = slim.get_variables_to_restore(exclude=["v1"]) # 変数を復元するために使用される saver を作成します。 restorer = tf.train.Saver(variables_to_restore) with tf.Session() as sess: # 変数をディスクから復元する。 restorer.restore(sess, "/tmp/model.ckpt") print("Model restored.") # Do some work with the model ...
異なる変数名でモデルを復元する
checkpoint から変数を復元する時、Saver は checkpoint ファイルの変数名を見つけてそれらを現在のグラフ内の変数にマップします。上では、変数のリストを渡すことで saver を作成しました。この場合、checkpoint ファイルで見つける変数の名前は各提供される変数の var.op.name から暗黙的に得られます。
これは、checkpoint ファイルの変数名がグラフのそれらとマッチする時は上手く動作します。けれども、時々、変数が現在のグラフのものとは異なる名前の checkpoint からモデルを復元することを望みます。この場合、各 checkpoint 変数名から各グラフ変数にマップする辞書を Saver に提供しなければなりません。
次のサンプルを考えます、ここでは checkpoint 変数名が単純な関数経由で得られます :
# 'conv1/weights' は 'vgg16/conv1/weights' から復元されることを仮定します。 def name_in_checkpoint(var): return 'vgg16/' + var.op.name # 'conv1/weights' と 'conv1/bias' は 'conv1/params1' と 'conv1/params2' から復元されることを仮定します。 def name_in_checkpoint(var): if "weights" in var.op.name: return var.op.name.replace("weights", "params1") if "bias" in var.op.name: return var.op.name.replace("bias", "params2") variables_to_restore = slim.get_model_variables() variables_to_restore = {name_in_checkpoint(var):var for var in variables_to_restore} restorer = tf.train.Saver(variables_to_restore) with tf.Session() as sess: # ディスクから変数を復元する。 restorer.restore(sess, "/tmp/model.ckpt")
異なるタスク上でモデルを再調整する
事前訓練された VGG16 モデルを持つ場合を考えましょう。モデルは、1000 クラスを持つ ImageNet データセット上で訓練されました。けれども、20 クラスだけを持つ Pascal VOC データセットにそれを適用したいとします。そのためには、最終層を除く事前訓練されたモデルの値を使用して新しいモデルを初期化できます :
# Pascal VOC データをロードする image, label = MyPascalVocDataLoader(...) images, labels = tf.train.batch([image, label], batch_size=32) # モデルを作成する predictions = vgg.vgg_16(images) train_op = slim.learning.create_train_op(...) # ImageNet で訓練されたモデルがどこに保存されているかを指定します。 model_path = '/path/to/pre_trained_on_imagenet.checkpoint' # 新しいモデルがどこに存在するかを指定します : log_dir = '/path/to/my_pascal_model_dir/' # 畳込み層のみを復元します : variables_to_restore = slim.get_variables_to_restore(exclude=['fc6', 'fc7', 'fc8']) init_fn = assign_from_checkpoint_fn(model_path, variables_to_restore) # 訓練を開始します。 slim.learning.train(train_op, log_dir, init_fn=init_fn)
モデルを評価する
モデルをひとたび訓練したら (あるいはモデルが訓練中である間でさえも) モデルが実際にどの程度上手くやるのかを見たくなります。これは、モデル性能を等級分けする評価メトリック (= 測定基準) のセットとデータを実際にロードして、推論を実行して、結果を ground truth と比較して評価スコアを記録する、評価コードを選択することでなされます。このステップは一度実行されるか定期的に繰り返されます。
メトリック
メトリックをパフォーマンス尺度として定義します、これは損失関数ではありませんが (損失は訓練中に直接的に最適化されますので)、それはモデルを評価する目的で依然として興味があります。例えば、ログ損失を最小化することを望むかもしれませんが、興味あるメトリックは F1 スコア (テスト精度)、あるいは Intersection Over Union スコア (微分可能でないので、損失としては使用できません) かもしれません。TF-Slim は、モデルの評価を容易にするメトリック演算のセットを提供します。抽象的には、メトリックの値の計算は3つのパートに分割されます :
- 初期化: メトリックを計算するのに使用される変数を初期化する。
- 集計 (Aggregation): メトリックの計算に使用される演算 (sums, etc) を実行する。
- ファイナリゼーション (Finalization): (オプション) メトリック値を計算する最後の演算を実行する。例えば、means, mins, maxes, etc. を計算する。
例えば、mean_absolute_error を計算するためには、2つの変数、count と total 変数がゼロに初期化されます。集計の間には、予測とラベルの幾つかのセットを観測して、それらの絶対値差を計算して総和を total に追加します。他の値を観測するたびに、count はインクリメントされます。最後に、ファイナリゼーションの間には、平均を得るために total が count で除算されます。
次のサンプルはメトリックを宣言するための API を示します。メトリックはしばしば訓練セット (その上で損失は計算されました) とは異なるテストセット上で評価されますので、テストデータを使用しているものと仮定します :
images, labels = LoadTestData(...) predictions = MyModel(images) mae_value_op, mae_update_op = slim.metrics.streaming_mean_absolute_error(predictions, labels) mre_value_op, mre_update_op = slim.metrics.streaming_mean_relative_error(predictions, labels, labels) pl_value_op, pl_update_op = slim.metrics.percentage_less(mean_relative_errors, 0.3)
サンプルが示すように、メトリックの作成は2つの値を返します : value_op と update_op です。value_op は冪等 (べきとう = idempotent) な演算でメトリックの現在の値を返します。update_op は、メトリックの値を返すだけでなく上で述べた集計ステップを実行する演算です。
各 value_op と update_op の追跡は面倒でしょう。これを処理するために、TF-Slim は2つの便利な関数を提供します :
# value と update ops を2つのリストに集計する : value_ops, update_ops = slim.metrics.aggregate_metrics( slim.metrics.streaming_mean_absolute_error(predictions, labels), slim.metrics.streaming_mean_squared_error(predictions, labels)) # value と udpate ops を2つの辞書に集計する : names_to_values, names_to_updates = slim.metrics.aggregate_metric_map({ "eval/mean_absolute_error": slim.metrics.streaming_mean_absolute_error(predictions, labels), "eval/mean_squared_error": slim.metrics.streaming_mean_squared_error(predictions, labels), })
実施例: 複数のメトリックを追跡する
全てをまとめます :
import tensorflow as tf slim = tf.contrib.slim vgg = tf.contrib.slim.nets.vgg # データをロードする images, labels = load_data(...) # Define the network predictions = vgg.vgg_16(images) # 計算するためのメトリックを選択します : names_to_values, names_to_updates = slim.metrics.aggregate_metric_map({ "eval/mean_absolute_error": slim.metrics.streaming_mean_absolute_error(predictions, labels), "eval/mean_squared_error": slim.metrics.streaming_mean_squared_error(predictions, labels), }) # データの 1000 バッチを使用してモデルを評価する ; num_batches = 1000 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) sess.run(tf.local_variables_initializer()) for batch_id in range(num_batches): sess.run(names_to_updates.values()) metric_values = sess.run(name_to_values.values()) for metric, value in zip(names_to_values.keys(), metric_values): print('Metric %s has value: %f' % (metric, value))
metric_ops.py は layers.py や loss_ops.py を使用することなしに分離して利用可能であることに注意してください。
評価ループ
TF-Slim は評価モジュール ( evaluation.py ) を提供します、これは metric_ops.py モジュールからのメトリックを使用してモデル評価スクリプトを書くためのヘルパー関数を含みます。これらは、定期的に評価を実行し、データのバッチに渡りメトリックを評価し、そしてメトリック結果を出力して要約するための関数を含みます。例えば :
import tensorflow as tf slim = tf.contrib.slim # データをロードする images, labels = load_data(...) # ネットワークを定義する predictions = MyModel(images) # 計算するためのメトリックを選択する : names_to_values, names_to_updates = slim.metrics.aggregate_metric_map({ 'accuracy': slim.metrics.accuracy(predictions, labels), 'precision': slim.metrics.precision(predictions, labels), 'recall': slim.metrics.recall(mean_relative_errors, 0.3), }) # Create the summary ops such that they also print out to std output: summary_ops = [] for metric_name, metric_value in names_to_values.iteritems(): op = tf.summary.scalar(metric_name, metric_value) op = tf.Print(op, [metric_value], metric_name) summary_ops.append(op) num_examples = 10000 batch_size = 32 num_batches = math.ceil(num_examples / float(batch_size)) # global ステップをセットアップする。 slim.get_or_create_global_step() output_dir = ... # Where the summaries are stored. eval_interval_secs = ... # How often to run the evaluation. slim.evaluation.evaluation_loop( 'local', checkpoint_dir, log_dir, num_evals=num_batches, eval_op=names_to_updates.values(), summary_op=tf.summary.merge(summary_ops), eval_interval_secs=eval_interval_secs)
Authors
Sergio Guadarrama and Nathan Silberman
以上