Python
機械学習
MachineLearning
TensorFlow

TensorFlow の名前空間を理解して共有変数を使いこなす

More than 1 year has passed since last update.

イントロ

これは、TensorFlow Advent Calendar 2016 の9日目の記事です.

2015年の11月に公開されたTensorFlowですが,公開当初から「名前空間」の機能がサポートされていました.これはTensorBoardによるグラフ視覚化において使われますが,もちろんそのためだけにあるわけではありません.名前空間は,識別子の管理に非常に有効です.強力な「名前空間」サポートというとC++を思い出しますが,C++の教則本(独習C++)から引用します.

名前空間(namespace)の目的は識別子の名前を局所化し,名前の競合を避けることです.C++のプログラミング環境では,変数,関数,クラスの名前が急増を続けてきました.名前空間が登場する前は,これらのすべての名前がグローバルな名前空間の中で場所を取り合い,多くの競合が発生していました.

一方,Pythonの変数スコープは,ローカル,グローバル(global),+α (nonlocal) と最低限のものしか持っていませんので,TensorFlowのコア部分をC++で書いていたGoogleのエンジニアが,C++レベルの名前空間サポートをTensorFlowに実装しよう,と考えるのは自然と思われます.

Neural Networkも層があまり多くないMLP(Multi-Layer Perceptron)等では,名前管理は困りませんが,深いCNNや,RNNで見られる大きなモデルに対しては,重み共有もありますので,きちんとした変数スコープが必要となります.また,スケールアップを考えると(私自信はほとんど経験ありませんが)Multi-Device(GPU),クラスタといった分散環境にコードを適用させなければなりません.ここでも変数スコープが必要となります.

本記事では,TensorFlowの「名前空間」をしっかりと理解する,という目的で関連APIを確認していきたいと思います.
(プログラミング環境は,TensorFlow 0.11.0, Python 3.5.2, Ubuntu 16.04LTS になります.)

TensorFlow 変数スコープの引っ掛かりポイント

きちんとドキュメントを読めば変数スコープは難しくありませんが,「あいまい」に理解していると以下の点が引っかかるかと思います.

  • スコープの定義用に,tf.name_scope() と tf.variable_scope() があるけど何が違うの?
  • TensorFlowの変数定義に tf.Variable() を使うと覚えているけど,tf.get_variable() は,いつ使うの?

先に答えを書きますが,設問1への解答は,tf.name_scope() はより汎用的に使うスコープ定義で,tf.variable_scope()は,変数(識別子)を管理するための専用のスコープ定義になります.また,設問2への解答は,tf.Variable() は,よりプリミティブ(低レベル)の変数定義であるのに対し, tf.get_variable() は,変数スコープを考慮した(より高レベルの)変数定義になります.

(関連ドキュメント,TesorFlow - "HOW TO" - "Sharing Variable" に共有変数関係が詳しく説明されています.)

以下,コードを動かして詳細を調べてみたいと思います.

# tf.name_scope
with tf.name_scope("my_scope"):
    v1 = tf.get_variable("var1", [1], dtype=tf.float32)
    v2 = tf.Variable(1, name="var2", dtype=tf.float32)
    a = tf.add(v1, v2)

print(v1.name)  # var1:0
print(v2.name)  # my_scope/var2:0
print(a.name)   # my_scope/Add:0

まず,tf.name_scope() を使って,その中で変数を定義しました.TensorFlowが管理する識別子は,後半に出力させていますが,その出力を print文の右にコメントで表示しています.tf.Variable() で定義した変数v2と,加算演算aに対しては,きちんと "my_scope" のスコープが定義されています.一方で, tf.get_varible() で定義したv1は,見事にスコープを無視してくれました.

 # tf.variable_scope
with tf.variable_scope("my_scope"):
    v1 = tf.get_variable("var1", [1], dtype=tf.float32)
    v2 = tf.Variable(1, name="var2", dtype=tf.float32)
    a = tf.add(v1, v2)

print(v1.name)  # my_scope/var1:0
print(v2.name)  # my_scope_1/var2:0  ... スコープ名がupdateされている.
print(a.name)   # my_scope_1/Add:0   ... update後のスコープ名が維持されている.

次に tf.variable_sope() を使いました.(一つ前のスニペットと今回のスニペットは,連続して動かしていることに注意してください.)
tf.get_variable() で定義した変数v1 は,ねらい通りに "my_scope" が変数名についた形で識別子が作られました.また,その下の変数v2と,演算aは,(tf.variable_scope("my_scope") としたにもかかわらず)"my_scope_1" が加わっています.この理由は,本来(プログラム初期状態であったら)"my_scope" をつけるはずでしたが,すでに同じ識別子("my_scope/var2:0")が一つ前のコードスニペットで使用済みであったため,自動で "my_scope_1" にupdateしたためです.(スコープ名のupdate ("my_scope" -> "my_scope_1")後のステートメント a = tf.add(v1, v2) では,このスコープ("my_scope_1")が維持されるようです.)

ややこしくなってきたので,少し整理します.

  • tf.name_scope() は,汎用的に用いる名前スコープ定義です.(ご存知の通り,TensorBoardへの出力は,この識別子設定を用います.)
  • tf.variable_scope() は,変数の管理に用いるスコープ定義です.(関数名 variable_scope そのままですが...)
  • tf.get_variable() は,変数名の識別子(新規か?重複がないか?)を管理しながら,変数の定義を行います.必ず tf.variable_scope() とセットで使います.

上記2つのスニペットでは,実験のため状況を複雑にしていましたが,「tf.get_variable() は,tf.variable_scope()とセットで使う」という基本を守れば,特に難しくありません.

では,tf.get_variable() を使って,共有変数を使うやり方を見ていきたいと思います.

with tf.variable_scope("my_scope2"):
    v4_init = tf.constant_initializer([4.])
    v4 = tf.get_variable("var4", shape=[1], initializer=v4_init)

print(v4.name)  # my_scope2/var4:0

まず,スコープ "my_scope2" の中で,変数 v4 を定義しました.tf.get_variable() では,変数イニシャライザを指定して変数を定義します.ここでは,定数のイニシャライザを使って,v4 に 4. が入るステートメントとしました.TensorFlowへの識別子は,第1引数で "var4" を指定しました.

次に同じ識別子で変数を確保することを試行してみます.

with tf.variable_scope("my_scope2"):
    v5 = tf.get_variable("var4", shape=[1], initializer=v4_init)
ValueError: Variable my_scope2/var4 already exists, disallowed. Did you mean to set reuse=True in VarScope? Originally defined at:

  File "name_scope_ex.py", line 47, in <module>
    v4 = tf.get_variable("var4", shape=[1], initializer=v4_init)

予定通りです.ValueErrorが発生しました.「同じ識別子で変数を取るのはおかしいのでは?」というエラーです.同じ識別子を使っての変数割当は,以下のように reuse オプションを用います.

with tf.variable_scope("my_scope2", reuse=True):
    v5 = tf.get_variable("var4", shape=[1])
print(v5.name)  # my_scope2/var4:0

あるいは,以下のようにしてもokです.

with tf.variable_scope("my_scope2"):
    tf.get_variable_scope().reuse_variables()
    v5 = tf.get_variable("var4", shape=[1])
print(v5.name)  # my_scope2/var4:0

以上,tf.variable_scope()tf.get_variable() の基本機能を確認しました.

共有変数の例 - 自己符号化器(Autoencoder)

さて,共有変数の使用例をみていきたいと思いますが,TensorFlow - Sharing Variable のドキュメントでは,以下の使用例を参考にせよとあります.

  • models/image/cifar10.py, Model for detecting objects in images.
  • models/rnn/rnn_cell.py, Cell functions for recurrent neural networks.
  • models/rnn/seq2seq.py, Functions for building sequence-to-sequence models.

いずれも相当なコード量ですので,今回は,これらとは別の自己符号化器(以下,Autoencoder) の重み共有を取り上げたいと思います.Autoencoderのencode側 / decode側は次のように表すことができます.

y = f(\textbf{W}x + \textbf{b})  \\
\hat{x} = \tilde{f}(\tilde{\textbf{W}}y + \tilde{\textbf{b}})

このような対称な形のAutoencoderでは次のような重み共有を用いることができます.

\tilde{\textbf{W} } = \textbf{W} ^{\mathrm{T}}

以上のような構成ネットワークをTensorFlowの共有変数を用いて実装してみます.最初にEncoderクラスを定義します.

# Encoder Layer   
class Encoder(object):
    def __init__(self, input, n_in, n_out, vs_enc='encoder'):
        self.input = input
        with tf.variable_scope(vs_enc):
            weight_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.05)
            W = tf.get_variable('W', [n_in, n_out], initializer=weight_init)
            bias_init = tf.constant_initializer(value=0.0)
            b = tf.get_variable('b', [n_out], initializer=bias_init)
        self.w = W
        self.b = b

    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.sigmoid(linarg)

        return self.output

変数スコープをオプション vs_enc で指定して設定し,その中で tf.get_variable()Wを定義しています.次にDecoderクラスですが,以下のようにしました.

# Decoder Layer
class Decoder(object):
    def __init__(self, input, n_in, n_out, vs_dec='decoder'):
        self.input = input
        if vs_dec == 'decoder': # independent weight
            with tf.variable_scope(vs_dec):
                weight_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.05)
                W = tf.get_variable('W', [n_in, n_out], initializer=weight_init)
        else:                   # weight sharing (tying)
            with tf.variable_scope(vs_dec, reuse=True):     # set reuse option
                W = tf.get_variable('W', [n_out, n_in])
                W = tf.transpose(W)

        with tf.variable_scope('decoder'):  # in all case, need new bias
            bias_init = tf.constant_initializer(value=0.0)
            b = tf.get_variable('b', [n_out], initializer=bias_init)
        self.w = W
        self.b = b

    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.sigmoid(linarg)

        return self.output

大部分はEncoderクラスと同じですが,変数Wの定義文を分岐で処理しています.ネットワーク定義部は次のようになります.

# make neural network model
def make_model(x):
    enc_layer = Encoder(x, 784, 625, vs_enc='encoder')
    enc_out = enc_layer.output()
    dec_layer = Decoder(enc_out, 625, 784, vs_dec='encoder')
    dec_out = dec_layer.output()

    return enc_out, dec_out

Decoderオブジェクトを生成する際,vs_dec='decoder' を指定するか,このオプションを省略すれば,重み変数 W は新規に確保され,上記 vs_dec='encoder' のようにEncoderで用いた変数スコープと同じにした場合,重み変数は,共有変数としてW を再使用するように実装しました.(再使用する場合は,ネットワークと整合させるように W を転置します.)

MNISTデータでの計算実行例を示しますが,まず重み共有をしない場合,次のようになります.

Training...
  step, loss =      0:  0.732
  step, loss =   1000:  0.271
  step, loss =   2000:  0.261
  step, loss =   3000:  0.240
  step, loss =   4000:  0.234
  step, loss =   5000:  0.229
  step, loss =   6000:  0.219
  step, loss =   7000:  0.197
  step, loss =   8000:  0.195
  step, loss =   9000:  0.193
  step, loss =  10000:  0.189
loss (test) =  0.183986

重み共有をした場合は,次の通りです.

Training...
  step, loss =      0:  0.707
  step, loss =   1000:  0.233
  step, loss =   2000:  0.215
  step, loss =   3000:  0.194
  step, loss =   4000:  0.186
  step, loss =   5000:  0.174
  step, loss =   6000:  0.167
  step, loss =   7000:  0.154
  step, loss =   8000:  0.159
  step, loss =   9000:  0.152
  step, loss =  10000:  0.152
loss (test) =  0.147831

重み共有の設定により,同じ学習回数での損失(cross-entropy)低下が速くなっています.ネットワークの自由度が約半分ですので,予想通りの結果と言えます.

やや複雑なモデルの例 - 2つのMLPを持つ分類器

もう一つ,やや複雑なモデルを考えてみます.(といってもそれほど複雑ではないのですが...) 扱うデータは上と同様,MNISTを用います.今回は,多クラス分類を行います.分類器としては,隠れ層 x2, 出力層 x1のMLP(Multi-layer Perceptron) を用いました.下図は,TensorBoard のグラフ・チャートです.

Fig. Graph of 2 MLP networks
mnist_2nets_60.png

(TensorBoard については使いこなせていません.大まかなイメージとしてとらえて下さい.)

まず,隠れ層(全結合層)と出力層のクラスを定義します.

# Full-connected Layer   
class FullConnected(object):
    def __init__(self, input, n_in, n_out, vn=('W', 'b')):
        self.input = input

        weight_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.05)
        bias_init = tf.constant_initializer(value=0.0)
        W = tf.get_variable(vn[0], [n_in, n_out], initializer=weight_init)
        b = tf.get_variable(vn[1], [n_out], initializer=bias_init)
        self.w = W
        self.b = b
        self.params = [self.w, self.b]

    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.relu(linarg)

        return self.output
#

# Read-out Layer
class ReadOutLayer(object):
    def __init__(self, input, n_in, n_out, vn=('W', 'b')):
        self.input = input

        weight_init = tf.random_normal_initializer(mean=0.0, stddev=0.05)
        bias_init = tf.constant_initializer(value=0.0)
        W = tf.get_variable(vn[0], [n_in, n_out], initializer=weight_init)
        b = tf.get_variable(vn[1], [n_out], initializer=bias_init)
        self.w = W
        self.b = b
        self.params = [self.w, self.b]

    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.softmax(linarg)  

        return self.output

クラスコンストラクタのオプションで,変数名をセットするようにしていますが,ここでは変数共有の操作を行っていません.次に,ネットワーク定義を行う部分が以下です.

# Create the model
def mk_NN_model(scope='mlp', reuse=False):
    '''
      args.:
        scope   : variable scope ID of networks
        reuse   : reuse flag of weights/biases
    '''
    with tf.variable_scope(scope, reuse=reuse):
        hidden1 = FullConnected(x, 784, 625, vn=('W_hid_1','b_hid_1'))
        h1out = hidden1.output()
        hidden2 = FullConnected(h1out, 625, 625, vn=('W_hid_2','b_hid_2'))
        h2out = hidden2.output()    
        readout = ReadOutLayer(h2out, 625, 10, vn=('W_RO', 'b_RO'))
        y_pred = readout.output()

    cross_entropy = -tf.reduce_sum(y_*tf.log(y_pred))

    # Regularization terms (weight decay)
    L2_sqr = tf.nn.l2_loss(hidden1.w) + tf.nn.l2_loss(hidden2.w)
    lambda_2 = 0.01
    # the loss and accuracy
    with tf.name_scope('loss'):
        loss = cross_entropy + lambda_2 * L2_sqr
    with tf.name_scope('accuracy'):
        correct_prediction = tf.equal(tf.argmax(y_pred,1), tf.argmax(y_,1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    return y_pred, loss, accuracy

この関数で,オプションとして変数スコープ scope と変数共有フラグである reuse を取るような仕様としました.2つのMLPネットワークで,重み共有は,次のようにスコープ名を揃えてreuse フラグをたてます.

    y_pred1, loss1, accuracy1 = mk_NN_model(scope='mlp1')
    y_pred2, loss2, accuracy2 = mk_NN_model(scope='mlp1', reuse=True)

重み共有しない場合は,以下のように設定します.(当たり前の構文ではありますが...)

    y_pred1, loss1, accuracy1 = mk_NN_model(scope='mlp1')
    y_pred2, loss2, accuracy2 = mk_NN_model(scope='mlp2')

計算実験としては,以下の2つのケースを行いました.

  1. 訓練(Train)データは2つに分割し,2つの分類器 'mlp1', 'mlp2' に供給する.
    2つの分類器は,重み共有を設定する.'mlp1'の訓練,'mlp2'の訓練とシリアルに行い.最終的なパラメータを用いてテスト(Test)データを分類する.
  2. 訓練(Train)データは2つに分割し,2つの分類器 'mlp1', 'mlp2' に供給する.'mlp1' と 'mlp2' は独立した(共有設定していない)ネットワークで,それぞれ学習を行う.テストデータはそれぞれの分類器にかけ,その結果をaveragingして,最終分類結果を得る.

重み共有の実験がしたかったので,2つの分類器の層数,ユニット数は同じにする必要があります.但し,全く同じ分類器ではつまらないので,オプティマイザーを別々のものとし,また学習率も微妙に調整しています.

まず,ケースNo.1の実行結果が下記になりました.

Training...
  Network No.1 :
  step, loss, accurary =      0: 178.722,   0.470
  step, loss, accurary =   1000:  22.757,   0.950
  step, loss, accurary =   2000:  15.717,   0.990
  step, loss, accurary =   3000:  10.343,   1.000
  step, loss, accurary =   4000:   9.234,   1.000
  step, loss, accurary =   5000:   8.950,   1.000
  Network No.2 :
  step, loss, accurary =      0:  14.552,   0.980
  step, loss, accurary =   1000:   7.353,   1.000
  step, loss, accurary =   2000:   5.806,   1.000
  step, loss, accurary =   3000:   5.171,   1.000
  step, loss, accurary =   4000:   5.043,   1.000
  step, loss, accurary =   5000:   4.499,   1.000
accuracy1 =   0.9757
accuracy2 =   0.9744

Network No.2 の学習開始時に,lossが若干増えていますが,No.1 の学習開始時の値比べてかなり小さい点に着目ください.これは,重み共有の結果,(No.1の学習結果を引き継ぎ)パラメータがNo.2の初めから適正化されたところからスタートしたことを示しています.但し,最終的に得られた分類精度,accuracy2 = 0.9744 は,accuracy1 からの向上がなく,この ”アンサンブル学習 もどき”が失敗であることが分かりました.

考えてみれば当然で,分類器として同じものを2回の学習で使いまわしている状況となっていて,
単に学習データを2回に分割して供給しただけですので,これではアンサンブルによる精度向上を期待できません.

ケースNo.2の独立分類器構成で,正しいアンサンブルを行った結果が下記になります.

Training...
  Network No.1 :
  step, loss, accurary =      0: 178.722,   0.470
  step, loss, accurary =   1000:  15.329,   0.990
  step, loss, accurary =   2000:  12.242,   0.990
  step, loss, accurary =   3000:  10.827,   1.000
  step, loss, accurary =   4000:  10.167,   0.990
  step, loss, accurary =   5000:   8.178,   1.000
  Network No.2 :
  step, loss, accurary =      0: 192.382,   0.570
  step, loss, accurary =   1000:  10.037,   0.990
  step, loss, accurary =   2000:   7.590,   1.000
  step, loss, accurary =   3000:   5.855,   1.000
  step, loss, accurary =   4000:   4.678,   1.000
  step, loss, accurary =   5000:   4.693,   1.000
accuracy1 =   0.9751
accuracy2 =   0.9756
accuracy (model averaged) =   0.9810

期待通り,個々の分類器で 0.975 前後にあった精度(正答率)がモデル平均により 0.980 と若干ですが良くなっています.

(今回作成したコードは,Gist に upload しました.)

少し横道にそれた感がありますが,変数スコープと共有変数の使い方のイメージをつかんでいただけたかと思います.小さいモデルでは,あまり変数スコープを使って変数を管理する必要性はないと思いますが,大きなモデルでは,変数スコープと共有変数を使いたくなる状況があるはずです.他のDeep Learning Frameworkにあまり見られない,TensorFlowの特長ですので,ぜひ使ってみて下さい!