【TensorFlow】ネットワークを簡潔に書けるTF-Slimとは

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_shapeconstraintが指定できない

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_initializerweights_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にも挑戦してみてくださいね。

それでは。

この記事を書いた人

侍エンジニア塾は「人生を変えるプログラミング学習」をコンセンプトに、過去多くのフリーランスエンジニアを輩出したプログラミングスクールです。侍テック編集部では技術系コンテンツを中心に有用な情報を発信していきます。

目次