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

## イントロ
これは、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" に共有変数関係が詳しく説明されています．）

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

```py
# 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は，見事にスコープを無視してくれました．

```py
 # 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() を使って，共有変数を使うやり方を見ていきたいと思います．

```py
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" を指定しました．  

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

```py
with tf.variable_scope("my_scope2"):
    v5 = tf.get_variable("var4", shape=[1], initializer=v4_init)
```

```text
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` オプションを用います．

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

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

```py
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](https://www.tensorflow.org/versions/r0.12/how_tos/variable_scope/index.html) のドキュメントでは，以下の使用例を参考にせよとあります．

- 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側は次のように表すことができます．

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

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

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

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

```py
# 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クラスですが，以下のようにしました．


```py
# 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の定義文を分岐で処理しています．ネットワーク定義部は次のようになります．

```py
# 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データでの計算実行例を示しますが，まず重み共有をしない場合，次のようになります．

```text
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
```

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


```text
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](https://qiita-image-store.s3.amazonaws.com/0/74152/1f505838-3b99-4825-7c3c-14bd1d160fe8.png)

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

まず，隠れ層（全結合層）と出力層のクラスを定義します．

```py
# 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
```

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

```py
# 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` フラグをたてます．

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

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

```py
    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の実行結果が下記になりました．

```text
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の独立分類器構成で，正しいアンサンブルを行った結果が下記になります．

```text
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](https://gist.github.com/tomokishii/42146ecc450c9e7228c3bdd1ccb9e408) に upload しました．）


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