TF-Slim : TensorFlow 軽量ライブラリ Slim
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 04/16/2017
* 本ページは、github 上の TensorFlow-Slim の README.md の前半を動作確認・翻訳した上で適宜、補足説明したものです:
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/slim/README.md
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
TF-Slim は TensorFlow で複雑なモデルを定義し、訓練しそして評価するための軽量ライブラリです。tf-slim のコンポーネントは、tf.contrib.learn のような他のフレームワークに加えて native tensorflow と自由にミックスできます。
使い方
import tensorflow.contrib.slim as slim
何故 TF-Slim なのか?
TF-Slim はニューラルネットワークの構築、訓練そして評価することを単純にします :
- ユーザに、ボイラープレート・コードを省くことでモデルを遥によりコンパクトに定義することを可能にします。これは 引数スコーピング と数多くの高位な 層 と 変数 の使用を通して成されます。これらのツールは可読性と保守性を高めて、ハイパーパラメータ値のコピー & ペーストからのエラーの可能性を減らし、そしてハイパーパラメータ調整を単純化します。
- 一般に使用される正則化項 (= regularizer) の提供によりモデルの開発を単純化します。
- 幾つかの広く利用されているコンピュータ・ビジョン・モデル (e.g., VGG, AlexNet) が slim で開発されてユーザに 利用可能 となっています。これらはブラックボックスとして利用可能ですし、様々な方法で拡張可能です、e.g. 異なる内部層に “multiple heads” を追加することによって。
- Slim は複雑なモデルを拡張することを簡単にし、既存のモデル・チェックポイントの断片を使用することで訓練アルゴリズムをウォームスタートすることも簡単にしおます。
TF-Slim の各種コンポーネントはどのようなものでしょうか?
TF-Slim は独立的に存在するように設計された幾つかのパーツから構成されます。これらは以下の主要な断片 (詳細は後述) を含みます。
- arg_scope : arg_scope と命名された新しいスコープを提供します、これは (そのスコープ内で) 特定の演算のためのデフォルト引数を定義することをユーザに可能にします。
- data : TF-Slim の データセット 定義、データ・プロバイダー、parallel_reader と デコーディング・ユティリティを含みます。
- evaluation : モデルを評価するためのルーチンを含みます。
- layers : tensorflow を使用してモデルを構築するための高位な層を含みます。
- learning : モデルを訓練するためのルーチンを含みます。
- losses : 一般的に使用される損失関数を含みます。
- metrics : 人気のある評価メトリックを含みます。
- nets : VGG と AlexNet モデルのような人気のあるネットワーク定義をふくみます。
- queues : QueueRunners を簡単にそして安全に開始してクローズするためのコンテキスト・マネージャを提供します。
- regularizers : 重み正則化項を含みます。
- variables : 変数作成と操作のための便利なラッパーを提供します。
モデルを定義する
モデルは簡潔に言えば TF-Slim を使用してその変数、層とスコープを結合することで定義できます。これらの要素の各々は以下のように定義されます。
変数
native TensorFlow の変数作成は事前定義値か初期化機構 (e.g. ガウシアンからランダムにサンプリング) を必要とします。その上、変数が GPU のような特定のデバイス上に作成される必要があるのであれば、その指定は明示的にされなければなりません 。変数作成のために必要なコードを軽減するために、TF-Slim は variables.py で薄いラッパー関数のセットを提供します、これは呼び出し側が変数を簡単に定義することを可能にします。
例えば、重み変数を作成しそれを切断正規分布を使用して初期化し、l2_loss で正則化してそれを CPU 上に置くためには、次を宣言する必要があるだけです :
weights = slim.variable('weights', shape=[10, 10, 3 , 3], initializer=tf.truncated_normal_initializer(stddev=0.1), regularizer=slim.l2_regularizer(0.05), device='/CPU:0')
native TensorFlow では、2つのタイプの変数があることに注意してください : レギュラー (regular) 変数とローカル (transient = 一時) 変数です。変数の大半はレギュラー変数です : 一度作成されれば、それらは saver を使用してディスクに保存されます。ローカル変数は session の間だけ存在してディスクに保存はされません。
TF-Slim ではモデル変数を定義することにより更に変数を区別していて、これはモデルのパラメータを表す変数です。モデル変数は学習の間に訓練か再調整されて評価または推論時にはチェックポイントからロードされます。サンプルは slim.fully_connected または slim.conv2d layer で作成された変数を含みます。非モデル変数 (= Non-model variables) はその他全ての変数で学習か評価時に使用されますが実際に推論を実行する際には必要とされません。例えば、global_step は学習と評価時に使用する変数ですが、実際にはモデルの一部ではありません。同様に、移動平均変数はモデル変数を反映しますが、移動平均はそれ自身はモデル変数ではありません。
モデル変数とレギュラー変数の両者は TF-Slim 経由で簡単に作成されて取得されます :
# モデル変数 weights = slim.model_variable('weights', shape=[10, 10, 3 , 3], initializer=tf.truncated_normal_initializer(stddev=0.1), regularizer=slim.l2_regularizer(0.05), device='/CPU:0') model_variables = slim.get_model_variables() # レギュラー変数 my_var = slim.variable('my_var', shape=[20, 1], initializer=tf.zeros_initializer()) regular_variables_and_model_variables = slim.get_variables()
これはどのように動作するのでしょうか?TF-Slim の層経由かあるいは直接に slim.model_variable 関数経由でモデル変数を作成する時、TF-Slim は tf.GraphKeys.MODEL_VARIABLES コレクションに変数を追加します。
貴方自身のカスタム層や変数作成ルーチンを持ち、依然として TF-Slim がモデル変数を管理するあるいは知っていることを望む場合はどうでしょう?TF-Slim はモデル変数をそのコレクションに追加するための便利な関数を提供します。
my_model_variable = CreateViaCustomCode() # TF-Slim に追加の変数について知らせます。 slim.add_model_variable(my_model_variable)
層
TensorFlow 演算のセットが非常に広範囲に渡る一方で、ニューラルネットワークの開発者は典型的には、モデルを “層”, “損失”, “メトリック”, そして “ネットワーク” のようなより高いレベルの概念の見地から考えます。畳込み層、完全結合層あるいは BatchNorm 層のような層は単一の TensorFlow 演算よりもより抽象的で典型的には幾つかの演算を含みます。更に、よりプリミティブな演算とは異なり、層は通常は (但し常にではなく) それに関係する変数 (調整可能なパラメータ) を持ちます。例えば、畳込み層は幾つかの低いレベルの演算から構成されます :
- 重みとバイアス変数を作成する。
- 前の層からの入力で重みを畳み込む。
- 畳み込みの結果にバイアスを加算する。
- 活性化関数を適用する。
そのままの TensorFlow コードだけを使用すると、これはなかなか面倒です :
input = ... with tf.name_scope('conv1_1') as scope: kernel = tf.Variable(tf.truncated_normal([3, 3, 64, 128], dtype=tf.float32, stddev=1e-1), name='weights') conv = tf.nn.conv2d(input, kernel, [1, 1, 1, 1], padding='SAME') biases = tf.Variable(tf.constant(0.0, shape=[128], dtype=tf.float32), trainable=True, name='biases') bias = tf.nn.bias_add(conv, biases) conv1 = tf.nn.relu(bias, name=scope)
このコードを繰り返し複写する必要性を軽減するために、TF-Slim はニューラルネットワーク層のより抽象化されたレベルで定義された多くの便利な演算を提供します。例えば、上のコードと相当する TF-Slim コードの呪文を比較してください :
input = ... net = slim.conv2d(input, 128, [3, 3], scope='conv1_1')
TF-Slim はニューラルネットワークを構築する多数のコンポーネントのための標準実装を提供します。これらは以下を含みます (訳注 : リンク先はすべて同じ layers.py です) :
層 | TF-Slim |
BiasAdd | slim.bias_add |
BatchNorm | slim.batch_norm |
Conv2d | slim.conv2d |
Conv2dInPlane | slim.conv2d_in_plane |
Conv2dTranspose (Deconv) | slim.conv2d_transpose |
FullyConnected | slim.fully_connected |
AvgPool2D | slim.avg_pool2d |
Dropout | slim.dropout |
Flatten | slim.flatten |
MaxPool2D | slim.max_pool2d |
OneHotEncoding | slim.one_hot_encoding |
SeparableConv2 | slim.separable_conv2d |
UnitNorm | slim.unit_norm |
TF-Slim はまた repeat と stack と呼ばれる2つのメタ演算も提供します、これはユーザに同じ演算を繰り返し実行することを可能にします。例えば、VGG ネットワークからの次のスニペットを考えます、この層(群)はプーリング層間に幾つかの畳込みを連続的に実行します :
net = ... net = slim.conv2d(net, 256, [3, 3], scope='conv3_1') net = slim.conv2d(net, 256, [3, 3], scope='conv3_2') net = slim.conv2d(net, 256, [3, 3], scope='conv3_3') net = slim.max_pool2d(net, [2, 2], scope='pool2')
このコードの複写を減らす一つの方法は for ループを通すことでしょう :
net = ... for i in range(3): net = slim.conv2d(net, 256, [3, 3], scope='conv3_' % (i+1)) net = slim.max_pool2d(net, [2, 2], scope='pool2')
これは TF-Slim の repeat 演算の使用によって見やすくなります :
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3') net = slim.max_pool2d(net, [2, 2], scope='pool2')
slim.repeat は in-line の同じ引数に適用されるだけでなく、slim.conv2d の続く各呼び出しに割り当てられたスコープにアンダースコアと反復数が追加されたような、スコープを展開する (= unroll) ためにも十分にスマートです。より具体的には、上の例のスコープは ‘conv3/conv3_1’, ‘conv3/conv3_2’ と ‘conv3/conv3_3’ のように命名されるでしょう。
更に、TF-Slim の slim.stack 演算は、層のスタックあるいはタワーを作成するために異なる引数で同じ演算を繰り返し適用することを呼び出し側に可能にします。slim.stack はまた作成された各演算のために新しい tf.variable_scope を作成します。例えば、多層パーセプトロン (MLP) を作成する単純な方法は :
# Verbose way: x = slim.fully_connected(x, 32, scope='fc/fc_1') x = slim.fully_connected(x, 64, scope='fc/fc_2') x = slim.fully_connected(x, 128, scope='fc/fc_3') # Equivalent, TF-Slim way using slim.stack: slim.stack(x, slim.fully_connected, [32, 64, 128], scope='fc')
この例では、slim.stack は関数の一つの呼び出しの出力を次へ渡して slim.fully_connected を3回呼び出しています。けれども、各呼び出しにおける隠れユニットの数は 32 から 64 へ 128 へ変わります。複数の畳込みのタワーを単純化するために stack を使用できます :
# Verbose way: x = slim.conv2d(x, 32, [3, 3], scope='core/core_1') x = slim.conv2d(x, 32, [1, 1], scope='core/core_2') x = slim.conv2d(x, 64, [3, 3], scope='core/core_3') x = slim.conv2d(x, 64, [1, 1], scope='core/core_4') # Using stack: slim.stack(x, slim.conv2d, [(32, [3, 3]), (32, [1, 1]), (64, [3, 3]), (64, [1, 1])], scope='core')
スコープ
TensorFlow ( name_scope, variable_scope ) における幾つかのスコープ機構のタイプに加えて、TF-Slim は arg_scope と呼ばれる新しいスコーピング機構を追加します。この新しいスコープは、一つまたはそれ以上の演算と (arg_scope で定義される演算の各々に渡される) 引数のセットを指定することをユーザに可能にします。この機能はサンプルにより上手く説明されます。次のコード・スニペットを考えましょう :
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')
これらの3つの畳込み層が同じハイパーパラメータの多くを共有することは明らかです。2つは同じ padding を持ち、3つ全てが同じ weights_initializer と weight_regularizer を持ちます。このコードは読みづらく括り出されるべき多くの繰り返される値を含みます。一つの解法は変数を使用してデフォルト値を指定することです :
padding = 'SAME' initializer = tf.truncated_normal_initializer(stddev=0.01) regularizer = slim.l2_regularizer(0.0005) net = slim.conv2d(inputs, 64, [11, 11], 4, padding=padding, weights_initializer=initializer, weights_regularizer=regularizer, scope='conv1') net = slim.conv2d(net, 128, [11, 11], padding='VALID', weights_initializer=initializer, weights_regularizer=regularizer, scope='conv2') net = slim.conv2d(net, 256, [11, 11], padding=padding, weights_initializer=initializer, weights_regularizer=regularizer, scope='conv3')
この解法は3つ全ての畳込みが正確に同じパラメータ値を共有することは保証しますが、コードの乱雑さを完全には減じていません。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')
サンプルが示すように、arg_scope の使用はコードをよりきれいに単純にしてそして保守を容易にします。引数値が arg_scope で指定される一方でローカルで上書きできることにも気がつくでしょう。特に、padding 引数が ‘SAME’ に指定される一方で、2つ目の畳込みは ‘VALID’ の値でオーバーライドしています。
arg_scopes はまたネストすることも可能で同じスコープで複数の演算も使用できます。例えば :
with slim.arg_scope([slim.conv2d, slim.fully_connected], activation_fn=tf.nn.relu, weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005)): with slim.arg_scope([slim.conv2d], stride=1, padding='SAME'): net = slim.conv2d(inputs, 64, [11, 11], 4, padding='VALID', scope='conv1') net = slim.conv2d(net, 256, [5, 5], weights_initializer=tf.truncated_normal_initializer(stddev=0.03), scope='conv2') net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc')
この例では、最初の arg_scope は同じ weights_initializer と weights_regularizer 引数をそのスコープの conv2d と fully_connected 層に適用しています。2つ目の arg_scope では、追加のデフォルト引数が conv2d に対してのみ指定されています。
実施例: VGG16 層を指定する
TF-Slim 変数、演算とスコープを結合することで、通常は非常に複雑なネットワークを非常に少ないコード行で書くことができます。例えば、VGG アーキテクチャ全体は単に次のようなスニペットで定義されます :
def vgg16(inputs): with slim.arg_scope([slim.conv2d, slim.fully_connected], activation_fn=tf.nn.relu, weights_initializer=tf.truncated_normal_initializer(0.0, 0.01), weights_regularizer=slim.l2_regularizer(0.0005)): net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1') net = slim.max_pool2d(net, [2, 2], scope='pool1') net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2') net = slim.max_pool2d(net, [2, 2], scope='pool2') net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3') net = slim.max_pool2d(net, [2, 2], scope='pool3') net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4') net = slim.max_pool2d(net, [2, 2], scope='pool4') net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5') net = slim.max_pool2d(net, [2, 2], scope='pool5') net = slim.fully_connected(net, 4096, scope='fc6') net = slim.dropout(net, 0.5, scope='dropout6') net = slim.fully_connected(net, 4096, scope='fc7') net = slim.dropout(net, 0.5, scope='dropout7') net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc8') return net
前半の翻訳はここまです。Training Models 以後の後半の翻訳は こちら。
以上