TensorFlowのTensorboardを触ってみた

何気なく買った地球防衛軍4.1が楽しくて、サンダー！って言いそうになります。蟻はどんどん来てもいいけど、噛み付きだけは勘弁な！

最近はこういった機械学習といったそういったフレームワークを出すのが流行なんでしょうか、Microsoftも目的としては同じような、[DMLT](http://www.dmtk.io/)を出してきました。
その先陣を切ったTensorflowですが、もう何番煎じかわかんなくて出がらしすら出ねぇよって感じですが、やはりこういうものは触らないとわかりません。機械学習系は触ってもわかんない時のほうが多いですが。

とりあえず普通にインストール、とかもありますが、その辺は公式ページを見てもらったほうがいいです。公式オススメはvirtualenvです。後私はPythonistaではないのでﾌｰﾝでしたが、Tensorflowは現状2.7専用です。3.x系列への対応もIssueとして挙がっていて、対応は行ってるようですんで、そのうち出るんではないでしょうか。

http://tensorflow.org/get_started/os_setup.md

## Tensorflowのチュートリアル
Tensorflowが話題になったのは、そのチュートリアルの充実っぷりにもあると思います。ちゃんと一般的な機械学習（Beginner）と、Convolutional Networkを利用した学習のやり方、というのをそれぞれ公式に提供しています。

内容は全部英語ですが、数学関係の言葉を調べ調べやればそれほど困らないのではないでしょうか。一応私がやったBeginnerとDeepをコメント付きで乗せておきます。

### Beginner

```python
# coding: utf-8
import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

import tensorflow as tf

# Placeholder。ある値を保存するための変数的なもの。2次元ベクトルの最初の数がNoneなのは、任意の個数とするため。
# 2次元目が784なのはMNISTの画素数
x = tf.placeholder("float", [None, 784])

# WはWeightのW。二次元ベクトルの1次元目が画素数なのは、MNISTの配列の順序がインデックス→画素となっており、行列として
# 乗算するときにこれが最初になっていないと、結果が10列のベクトルにならない
W = tf.Variable(tf.zeros([784, 10]))
# こっちは一次元ベクトル。行なのか列なのかは関係なく、単純にインデックスの問題になる
b = tf.Variable(tf.zeros([10]))

# Wとbは両方共にVariableとされているが、この後の勾配降下法のところで、誤差逆伝播法によって、自動的に
# 都度更新されていく。もし誤差逆伝播法を使わない場合は、変数に対するassignが行われないので、
# 変数は初期値のままになる。

# matmulは行列の掛け算。順序がｘからWになっているのは、行列の形上そうするしかないので。
# 一次元ベクトル同士はそのまま足せるので、その結果とbをそのまま加算して、結果にsoftmaxを適用したものを
# 結果とする。
# softmaxの結果は、結果のベクトルの各要素に対してsoftmaxを行った結果になるので、ベクトルになる
y = tf.nn.softmax(tf.matmul(x, W) + b + a)

# yに対する正解のラベル。ラベルはそれぞれ要素10のベクトル。
y_ = tf.placeholder("float", [None, 10])

# yの各要素に対して対数を取ったベクトルと、ラベルの各行とをかけた結果＝スカラーを、全ての行分加算して、その結果の符号を判定させている。
cross_entropy = -tf.reduce_sum(y_*tf.log(y))

# # 勾配降下法を用いたBackpropagation Algorithmによるグラフ最適化を行う。かなりブラックボックス
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

# 全ての変数を初期化する。SessionはTensorFlowのプログラムを実際に実行するためのContext的なもの。
init = tf.initialize_all_variables()
sess = tf.Session()
sess.run(init)

writer = tf.train.SummaryWriter("./log", sess.graph_def)

# MNISTのデータセットから、100件ランダムに取得して、それに対して学習を実行させるということを繰り返す。
# ここで、xとy_にだけ値を入れているのは、ここは未知数＝Noneを入れており、ここを確定させないと動作させられないためと思われる。
# そのためのPlaceholder。
# なので、ここではそれぞれ[100, 784]と[100, 10]の行列が設定されることになる。
for i in range(1000):
    batch_xs, batch_ys = mnist.train.next_batch(100)

    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

# 学習結果であるｙと、ラベルであるy_のそれぞれのベクトルの中で、最大値をとるインデックスを比較している。
# あるラベルに1が立っている＝その数字であり、同時に必ずそれが最大値。
# それと比較して、学習結果の中で一番高い値＝その画像でどの数値と判定するかを示すので、そのインデックス同士が同一
# かを判定している
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))

# true/falseを1.0/0.0にそれぞれキャストして、加算した結果を平均する
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

sess.run(accuracy, feed_dict={x:mnist.test.images, y_: mnist.test.labels}))
```

### Deep

```python
# coding: utf-8
import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

import tensorflow as tf
sess = tf.InteractiveSession()

x = tf.placeholder("float", [None, 784])
y_ = tf.placeholder("float", [None, 10])

# 指定したShapeでWeight Variableを作成する。
def weight_variable(shape):
    # stddevは標準偏差。truncated_normalは、指定した平均（デフォルト０）
    # と、渡した標準偏差（デフォルト１）から、標準偏差の二倍以上の値
    # をtruncateして再度取得するようにする
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

# 二次元の畳み込みを行うための計算ノードを作成する。
# 二次元の畳み込みなので、inputの値を二次元＝幅と高さの単位で値を畳み込んだ値を作る
# tf.nn.conv2dは、ある形式の入力とフィルターの四次元テンソルから、
# inputの中身をフィルターで指定された幅、高さの単位でパッチにして、
# そのパッチごとにフィルタを通して次元を削減する。
# stridesは、最初と最後の要素は必ず１じゃないとならない。２つ目と３つ目の
# 要素が実際に利用され、パッチとする窓の動かす間隔を指定する。
# ここではStrideがどっちも１なので、水平・垂直方向に1ずつ動いていく。
# なので、入力と出力は同じサイズになる。
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

# 入力値の2x2の範囲における最大値をプールする。多分。
# カーネルのサイズとstridesのサイズが両方2x2なので、2x2の範囲で最大のものだけを
# 残すイメージ。なので、ここに入れられると、そのイメージのサイズはちょうど半分になって入れられる。
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1],
                          strides=[1,2,2,1], padding='SAME')

# 最初のレイヤー。
# weight_variableの[5,5,1,32]というのは、最初の二次元は畳み込みするときのパッチのサイズ。
# 3次元目が入力のチャンネル数で、4次元目が出力のチャンネル数になる。
W_conv1 = weight_variable([5,5,1,32])
# 出力チャンネル数が32個ということになる。
b_conv1 = bias_variable([32])

# xをreshapeする。どうreshapeするかというと、4次元のshapeが渡されているのでこの形にされる。
# 最初の値は、flattenされるという値になるので、xに入っている値の全てが一次元に展開される。
# 2次元目と3次元目は、それぞれイメージの幅と高さで、4次元目が色のチャンネル数。
# xはどんな値かというと、28x28の画像のflattenされたものが一次元配列中に入っているという、
# 二次元配列になっている。それを、784の部分を28x28に変換して、最終的にその中の要素が1個ずつ、
# という形にしている。
x_image = tf.reshape(x, [-1,28,28,1])

# weight,x_image,biasをReLU関数に適用して、結果をプールする。
# この処理を行うと、パッチされたデータはそれぞれ半分のサイズになる。
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

# 二層目。パッチのサイズは変わらないが、入力チャンネル数と出力チャンネル数がそれぞれ32倍と2倍になってる。
W_conv2 = weight_variable([5,5,32,64])
b_conv2 = weight_variable([64])

# conv2dの最初の引数に一層目のpoolが指定されていることに注目。こうすることで、一層目の
# 畳み込み結果のプールに入れられたそれぞれの値から、対応する入力チャンネルと出力チャンネルを
# 繋いで、パッチした結果をまたパッチしていくことができる
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

# h_pool2に入ったデータは、幅と高さがそれぞれ28/2/2 = 7になっている。
# それが二層目の出力チャンネル＝64あるので、その分が一直線にならんだものが1次元目で、
# 二次元目はそれを計算するニューロン数になる。
W_fc1 = weight_variable([7 * 7 * 64, 1024])
# こっちもWeightと同じニューロン数を受け取るようになる
b_fc1 = bias_variable([1024])

# h_pool2の段階では、[-1,7,7,1]という並び方になっているので、それを64個ずつ
# 重ねた二次元配列になるように変換する
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
# ここで、二次元行列同士を乗算して、結果として[-1, 1024]の二次元配列を得ている。これは事実上
# の結果となる。
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

# オーバーフィッティングを軽減するために、dropout層をつくる。
keep_prob = tf.placeholder("float")
# デフォルトでは、それぞれの要素が独立に保持されるか独立されるかの確率によって、
# ランダムにdropoutされる。
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

# ここからは使う値が違うくらいで、基本的には一緒。
# レイヤー的にはReadoutということで、Convolutional network から実際に利用する値を取り出している。
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

# ここからは一層型のモデルとほとんど一緒。ただし、OptimizerがADAMになっている。ADAMが何かは
# よくわからない。
cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
sess.run(tf.initialize_all_variables())

# 20000回とか大分数が伸びてるが、これは途中で半分くらいはdropoutされることを
# 想定しているため。
# 実行してみると、なんでかはよくわからないが、8thread使えるはずが4threadしか使わない
# 状態になっていた。多分CPU数（echo /proc/cpuinfoでは4つなので）？
# Core i7-4790 @ 3.60GHz で、おおよそ40分(!)ほどかかる。
for i in range(20000):
    batch = mnist.train.next_batch(50)
    if i % 100 == 0:
        train_accuracy = accuracy.eval(feed_dict={
            x:batch[0], y_: batch[1], keep_prob: 1.0})
        print("step %d, training accuracy %g"%(i, train_accuracy))
    train_step.run(feed_dict={x:batch[0], y_:batch[1], keep_prob:0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={x:mnist.test.images, y_:mnist.test.labels, keep_prob: 1.0}))

writer = tf.train.SummaryWriter("./log", sess.graph_def)
```

### 注意
コメントはつけてますが、私の理解＆ドキュメントに書いてあることを併せて書いてます。なので間違いなどはご容赦を。

## 実行速度

BeginnerとDeep（Convolutionalバージョン）ですが、ループ数が違うとはいえ、その速度差は圧倒的にBeginnerの方が早いです。余計なことしてないですからね。

実際に私のマシン（Core i7-4790 @ 3.60GHz、メモリ16GiB）で試してみたところ、以下のような結果でした。ちなみにTensorFlowでのCPUにおける並列数は、おそらくCPUコアの数自体で決まって、Hyperthread的なものは考慮されないようです。

```
Beginner       : 約5秒
Deep(CPU Only) : 40分(!?)
```

という、衝撃的な結果になりました。本当はGPU Enabledで試してみたかったのですが、うちのGentooに入るCUDA SDKではバージョンが足りなかったり進みすぎてたりで、CUDA自体のインストールから始めないとならなさそうだったのでとりあえずお預けです。おそらくDeep側はGPUとセットでやるか、Tensorflowが語っている分散環境でやらないと、実用的な速度にはならなさそうです。

## Tensorboardを使うときの注意点

こんなのついてるよ！って動画でもバリバリ使っている風景を見せてましたが、もちろん使えます。デスクトップアプリケーションだと勘違いしてたのは私だけじゃないはず。

Tensorboardを使う時の注意点は2つあって、まず実行時のグラフを見たいだけならば、公式で紹介されているようなものは必要なく、

```python
writer = tf.train.SummaryWriter("./log", sess.graph_def)
```

だけでOKです。こうすると `./log` の中に、events.out.tfevents.<unix秒っぽいの>.<hostname> というファイルが、実行されるたびに出来ます。サイズが妙に大きければ出来てます。

もうひとつ、Tensorboard自体はコマンドラインからサーバーを起動してアクセスしてもらうタイプのアプリケーションですが、そのディレクトリの指定時に **フルパス** じゃないとダメでした。これは罠だと思いました・・・。

## Tensorboardの構成

最近はこういうものをみると何で作ってんだろ？と気になってしまうので、後ろを覗いてみました。

使っているライブラリとしては、

- lodash
- d3
- plottable
- dagre
- graphlib

dagreなんて初めて聞きましたが、有向グラフを作成するためのライブラリだそうで。

そして個人的に一番驚いたのが、仕事で何回も見た記憶のある

```html
    <link rel="import" href="external/polymer/polymer.html">
```

という行を見た時。 **Polymerかよ！** って思わず突っ込んでしまいました。そりゃGoogleだもんな・・・。

そしてTensorboardの本体は更に驚きで、

```typescript
/// <reference path="../../../typings/tsd.d.ts" />
```

という行があるじゃありませんか・・・！TypeScript＋Polymerの合わせ技になってました。確かに、ReactなどよりはPolymerの方が、同じようなものを作ってもサクサク動く感覚はありますので、こういった重くなりそうなアプリケーションには向いているのかもしれません・・・。

元ソースは見てませんが、構成的におそらくは \*.ts と \*.htmlを vulcanizeして作ってんだと思います。この規模で使われているのを見ると、Polymerでもいいんだろうなぁとは思います。ただ、TensorboardはChromeでしか動きませんでしたのでその辺はまた別にご注意ください。

## 感想という名のまとめ

こういったフレームワークを触るのはこれが初めてでしたが、かなり直感的に触ることができるんじゃないかと思います。行列とベクトルの計算はできることに越したことはないとは思いますが、加減乗除がかなり直感的にかけるのは、数式をプログラミングしないとならない人たちにはかなり嬉しいんじゃないでしょうか。

個人的にはTensorboardのヌルヌル加減が気持ちよかったです。ああいうのがオープンソースで出てこられると、画面系の要求がまた高まりそうで怖いですが。

もうちょっとチュートリアルとか触ってみて、実際GPUを使ったらどれくらい速度が早くなるのか試してみようかと思います。
時間があれば、DMLTも触ってみようと思います。