TensorFlowの中に、TensorFlow-Slim(tf.contrib.slim。以降、TF-Slim)というライブラリが含まれていることをご存知でしょうか。
この記事では、TF-Slimを簡単に紹介し、以下のような疑問に答えます。
- TensorFlowだけで手一杯なのにTF-Slimも勉強する必要があるの?
- TensorFlowを使わずにTF-Slimを使うことになるの?
- 「TensorFlow-Slim」を読もうと思ったけど長くて挫折しそう。もっと簡単に説明できないの?
では、行ってみましょう!
TF-Slimとは
TF-Slimは、TensorFlowのラッパーライブラリです。
TF-Slimの大きな目的は、「TensorFlow-Slim」の「Why TF-Slim?」に書かれているとおり4つあるのですが、この記事では以下の点に注目しました。
- TensorFlowよりも少ない文字数でモデルを定義する
文字数が少なければ、考えることも少なくなり、間違いも少なくなることが期待できるというわけです。
TF-Slimのインストール?
TF-Slimは、TensorFlowと一緒にインストールされます。
TF-Slimを使い始めるときに、あらためて何かをインストールする必要はありません。
それでは、モデルを定義するときのコードについて、変数の定義、レイヤーの定義、スコープの定義の3つに分けて見ていきましょう。
変数の定義
TensorFlowでは、tf.Variable()やtf.get_variable()を使って変数を定義します。
一方、TF-Slimを使うときは、slim.model_variable()、またはslim.variable()を使って変数を定義します。
ちなみに、slim.model_variable()は、定義した変数をtf.GraphKeys.MODEL_VARIABLESコレクションに追加してから、slim.variable()を実行する仕組みになっているだけで、ほかに違いはありません。
ここでは、tf.get_variable()と、slim.model_variable()を比べながら、変数の定義方法の違いを見ていきます。
TF-Slimを使わない場合は、以下のコードで変数を定義できます。
my_variable = tf.get_variable("my_var_tf", [10, 10, 3, 3])
TF-Slimを使う場合は、以下のコードです。
my_variable_slim = slim.model_variable("my_var_slim", [10, 10, 3, 3])
残念ながらTF-Slimのほうがちょっと長くなっていて、あまり違いがないように見えます。
そこで、2つのメソッドの入り口部分だけですが、ざっくり見比べてみましょう。
def get_variable(name, shape=None, dtype=None, initializer=None, regularizer=None, trainable=True, collections=None, caching_device=None, partitioner=None, validate_shape=True, # (3)model_variable()では指定できない use_resource=None, custom_getter=None, constraint=None # (3)model_variable()では指定できない ):
def model_variable(name, shape=None, dtype=dtypes.float32, # (1)get_variable()ではNone initializer=None, regularizer=None, trainable=True, collections=None, caching_device=None, device=None, # (2)get_variable()では指定できない partitioner=None, custom_getter=None, use_resource=None ):
上にも書きましたが、(1)~(3)の3点が違うようです。
(1)model_variable()は、dtypeの初期値がdtypes.float32になっている
これは、dtypes.float32を指定することが多いから、初期値をNone→dtypes.float32に変更して、1文字でも少なくしようという狙いがありそうです。
(2)model_variable()は、deviceが指定できる
deviceは、変数を配置するデバイスを指定するための引数です。
TensorFlowだけで、変数を配置するデバイスを指定するには、以下のように記述します。
with tf.device("/gpu:1"): my_variable_tf_gpu1 = tf.get_variable("my_var_tf_gpu1", [10, 10, 3, 3])
TF-Slimを使うと、以下のようになります。
my_variable_slim_gpu1 = slim.model_variable("my_var_slim_gpu1", [10, 10, 3, 3], device="/gpu:1")
TF-Slimの書きかたの方が読みやすいかな、という程度の違いですが、このような些細な違いも大事にしているのです。
(3)model_variable()は、validate_shape、constraintが指定できない
validate_shapeは、標準はTrueなのですが、Falseにするとshapeの大きさをチェックしなくなります。
当然チェックするため、指定する必要がありません。
constraintには、制約関数を指定できます。制約関数は、TensorFlowの学習時に変数の値を更新した後に適用される関数で、変数の範囲を制限するときに使います。
ただ、asynchronous distributed trainingを行うときは、制約関数を使用するのは問題があるそうです。
TF-Slimでconstraintを指定できないのは、制約関数を使わないことを標準とするためなのでしょう。
以上のように、2つのメソッドには違いがあることはわかりましたね。
通常使用する範囲では、少しの違いですが、その少しの違いも大事にしようというのがTF-Slimなのです。
レイヤーの定義
レイヤーの定義では、TF-Slimを使うと非常に短く書けます。
畳み込み層
まずは、TF-Slimを使わないで畳み込み層を定義するコードです。
W_conv1 = tf.Variable(tf.truncated_normal([5, 5, 1, 32], stddev=0.1)) b_conv1 = tf.Variable(tf.constant(0.1, shape=[32])) h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
TF-Slimを使うと、以下のとおり。
h_conv1_slim = slim.conv2d(x_image, 32, [5, 5])
重みやバイアスの定義が非常に短くなっています。
さらに、slim.conv2d()の標準の活性化関数がtf.nn.relu()になっているため、活性化関数の指定も省略できています。
プーリング層
次は、TF-Slimを使わないでプーリング層を定義するコード。
tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
TF-Slimを使うと、以下のようになります。
h_pool1_slim = slim.max_pool2d(h_conv1_slim, [2, 2])
ksizeやstridesで指定する[1, 2, 2, 1]を、[2, 2]にできていますね。
全結合層
そして、TF-Slimを使わないで全結合層を定義するコード。
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64]) W_fc1 = tf.Variable(tf.truncated_normal([7 * 7 * 64, 1024], stddev=0.1)) b_fc1 = tf.Variable(tf.constant(0.1, shape=[1024])) h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
TF-Slimを使うと…
h_pool2_flat_slim = slim.flatten(h_pool2_slim) h_fc1_slim = slim.fully_connected(h_pool2_flat_slim, 1024)
明らかに短く、考えることが減っています。
ドロップアウト層
続いて、TF-Slimを使わないでドロップアウト層を定義するコードです。
ここはほとんど変わりません。
keep_prob = tf.placeholder(tf.float32) h_fc1_drop = tf.nn.dropout(h_fc1_slim, keep_prob)
TF-Slimを使うと…
keep_prob = tf.placeholder(tf.float32) h_fc1_drop_slim = slim.dropout(h_fc1_slim, keep_prob)
出力層
最後に、出力層を定義するコードです。
W_fc2 = tf.Variable(tf.truncated_normal([1024, 10], stddev=0.1)) b_fc2 = tf.Variable(tf.constant(0.1, shape=[10])) y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
TF-Slimを使うと、以下のとおり。
y_conv = slim.fully_connected(h_fc1_drop_slim, 10, activation_fn=None)
全結合層と同じように短くなっていますね。
以上のように、レイヤーの定義では、TF-Slimのほうが短いコードで定義できるのです。
新しいスコープ
TF-Slimでは、arg_scopeというスコープが用意されています。
arg_scopeは、複数のレイヤーを定義するときに、指定する引数(argument)が同じことが多いため、繰り返して記載する手間を省くために、用意されました。
これは何より、コードを見た方が分かりやすいと思います。
すでに、slim.conv2d()を使って、3層の畳み込み層を定義しているコードです。
3層ともweights_initializerやweights_regularizerに同じ内容を指定していることに注目してください。
net = slim.conv2d(inputs, 64, [11, 11], 4, padding='SAME', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005), scope='conv1') net = slim.conv2d(net, 128, [11, 11], padding='VALID', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005), scope='conv2') net = slim.conv2d(net, 256, [11, 11], padding='SAME', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005), scope='conv3')
TF-Slimのarg_scopeを使うと、以下のようになります。
with slim.arg_scope([slim.conv2d], padding='SAME', weights_initializer=tf.truncated_normal_initializer(stddev=0.01) weights_regularizer=slim.l2_regularizer(0.0005)): net = slim.conv2d(inputs, 64, [11, 11], scope='conv1') net = slim.conv2d(net, 128, [11, 11], padding='VALID', scope='conv2') net = slim.conv2d(net, 256, [11, 11], scope='conv3')
slim.arg_scope()で、weights_initializerやweights_regularizerを指定しておくことで、with slim.arg_scope():の中でレイヤーを定義するときに、標準の引数を上書きするようなイメージになっていますね。
元のコードとゆっくり見比べれば、すんなり理解できるはずです。
まとめ
今回は、TF-Slimの基本的な考え方を説明しました。
また、コードの文字数が少なくなる例として、変数の定義方法とレイヤーの定義方法、それから、新しいarg_scopeという仕組みを紹介しました。
TensorFlowとTF-Slimは親和性が非常に高いので、たとえば畳み込み層だけをTF-Slimで書き替えることもできます。
少しずつ書き替えてみて理解を深めてもいいですし、全部を書き替えるのもいいでしょう。
好みのペースでTF-Slimにも挑戦してみてくださいね。
それでは。