TensorFlowのチュートリアルをやってみる(2)

概要

NN初心者がTensorFlowのチュートリアルの内容を寄り道しながらこなす企画のパート2。

今回はDeep MNIST for Expertsのところ。前回は単純なモデルで91%程度の精度だったけど、今回は畳み込みニューラルネットワーク(convolutional neural network)で99%以上の精度を達成するらしい。

バージョンは0.7.1でPython3.4、GPU版を利用。OSはUbuntu系。

@CretedDate 2016/03/21
@Versions python3.4.3 tensorflow0.7.1, CudaToolkit7.5

前回の復習

まずは前回の復習からスタートしている。

import tensorflow as tf

# MNISTのデータ読み込み
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

# Placeholderの用意
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

# Variableの用意
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

# softmaxと交差エントロピー
y = tf.nn.softmax(tf.matmul(x,W) + b)
cross_entropy = -tf.reduce_sum(y_*tf.log(y))
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

# train
sess = tf.InteractiveSession()
tf.initialize_all_variables().run()
for i in range(1000):
    batch = mnist.train.next_batch(50)
    train_step.run(feed_dict={x: batch[0], y_: batch[1]})

# 正解率の確認
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
corrects = sess.run(correct_prediction, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
corrects.mean()
  #=> 0.90920000000000001

これで約91%の精度が出た、というのが前回やったこと。

Placeholder

今回のチュートリアルでは前回のコードについて、いくつか注釈が追加されている。

まず、placeholderについて、shapeを[None, 784]と[None, 10]で用意している。

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

1次元目はバッチサイズで上記のようにNoneにしておくと可変サイズに対応できるらしい。

784 is the dimensionality of a single flattened MNIST image, and None indicates that the first dimension, corresponding to the batch size, can be of any size.

(訳: 784はMNISTの画像(訳注:28*28=784)を一次元にしたもの。Noneの方(最初の次元の方)はbatch sizeに対応し、どんなサイズでも取りうる)

今回、学習する際のバッチサイズは50を設定している。

batch = mnist.train.next_batch(50)

試しにplaceholderのバッチサイズを、下記のように50に設定するとどうなるだろうか。

# Placeholderを50に
x = tf.placeholder(tf.float32, shape=[50, 784])
y_ = tf.placeholder(tf.float32, shape=[50, 10])

これで実行するとtrainは通ったけど、accuracyを取るところでエラーになった。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
corrects = sess.run(correct_prediction, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
  #=> ValueError: Cannot feed value of shape (10000, 784) for Tensor 'Placeholder:0', which has shape '(50, 784)'

trainする際は50ずつ入れているが、correct_predictionの方では全件(10000件)に対して実行するので、サイズが合わずエラーになるようだ。

Variable

Varibaleとして設定したWとb。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

両者の意味はこう記載されている。

W is a 784x10 matrix (because we have 784 input features and 10 outputs) and b is a 10-dimensional vector (because we have 10 classes)

(訳: Wは784x10の行列。784の入力と10の出力に対応する。bは10次元のベクター。10個のクラスに対応する)

tf.matmul(x, W) + b のようにWがweight、bがbiasになる。

注意書きとしてVariableを利用する場合は、必ずinitializeしておかないといけないと書かれている。

sess.run(tf.initialize_all_variables())

上記と等価となるコードとして、exampleのソースの中ではこんな書き方がされてた。

sess = tf.InteractiveSession()
tf.initialize_all_variables().run()

InteractiveSessionの場合はdefault_sessionが登録されるので、上記のような記述でもいけるようだ。普通にsess = tf.Session()した場合はdefault session設定されてないぞ的なエラーになる。

InteractiveSessionの主な用途は、computation graphを随時出せることらしいけど、まだ使ったことがないのでそのありがたみがわからない。後日使ってみよう。

通常のSessionでも下記のようにdefault sessionは設定することができる。

sess = tf.Session()
with sess.as_default():
    tf.initialize_all_variables().run()

畳み込みニューラルネットワーク

復習はこのへんにしておいて、本題の畳み込みニューラルネット(以下CNN)の方もやってみる。

世の中には正解率が99%超えてる手法がいくつもある中で、先ほどのSoftmaxによる正解率91%というのはたいへんよろしくない( It's almost embarrassingly bad)数字らしい。

最良の手法では99.7%まで出せるらしいけど、今回のCNNでは99.2%程度になる。

ソースコード的にはこんな感じになる。

import tensorflow as tf

# Session確立
sess = tf.InteractiveSession()

# データ読み込み
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

# placeholder
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

# variable
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
sess.run(tf.initialize_all_variables())

# 第一層のweightsとbiasのvariable
W_conv1 = tf.Variable(tf.truncated_normal([5, 5, 1, 32], stddev=0.1))
b_conv1 = tf.Variable(tf.constant(0.1, shape=[32]))

# 画像を28x28にreshape
x_image = tf.reshape(x, [-1,28,28,1])

# 第一層のconvolutionalとpool
h_conv1 = tf.nn.relu(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME') + b_conv1)
h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

# 画像を784の一次元から28x28の二次元に変換する
# 4つめの引数はチャンネル数
x_image = tf.reshape(x, [-1,28,28,1])

# 第二層
W_conv2 = tf.Variable(tf.truncated_normal([5, 5, 32, 64], stddev=0.1))
b_conv2 = tf.Variable(tf.constant(0.1, shape=[64]))
h_conv2 = tf.nn.relu(tf.nn.conv2d(h_pool1, W_conv2, strides=[1, 1, 1, 1], padding='SAME') + b_conv2)
h_pool2 = tf.nn.max_pool(h_conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

# 第一層と第二層でreduceされてできた特徴に対してrelu
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_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

# Dropout
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

# 出来上がったものに対してSoftmax
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.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

# 交差エントロピー
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))

# 今回はGradientDescentOptimizerではなく、AdamOptimizer
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

# accuracyを途中確認するための入れ物
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# variable初期化
sess.run(tf.initialize_all_variables())

# たまにaccuracyを確認しつつtrain
accracies = []
for i in range(20000):
  batch_x, batch_y = mnist.train.next_batch(100)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={x:batch_x, y_: batch_y, keep_prob: 1.0})
    accracies.append(train_accuracy)
    print("step %d, training accuracy %g"%(i, train_accuracy))
  train_step.run(feed_dict={x: batch_x, y_: batch_y, keep_prob: 0.5})

# accuracyを表示
print("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
  #=> 0.9923

見事、99.23%まで精度を上げることができている。

チュートリアルではnext_batchを50ごとにしているけど、training_accuracy出しているところが1%刻みになった方が見やすいという理由で100に変更している。(結果は大きくは変わらないはず)

accuracyの変遷をplotするとこんな感じになる。X軸の数は実際には×100した回数ループしている。100回目のループの時点で87%まで上昇し、その後は徐々に100%に張り付くようになっている。

accuracy

同じ図を90%以上のみに絞ってplotする。徐々に精度が良くなっていることがわかる。

accuracy

今回のソースコードの精度が良いということはわかった。しかし素人の私はソースだけ見ても何がなんだか意味がわからない。

前回同様、今回も段階を踏んで理解していくことにする。

tf.truncated_normal

まず、最初に出てくる知らない関数、truncated_normalの動きを確認する。

W_conv1 = tf.Variable(tf.truncated_normal([5, 5, 1, 32], stddev=0.1))

名前からして切断正規分布に関するものらしい。

単体で実行すると下記のような結果が取れる。

sess.run( tf.truncated_normal([5, 2], stddev=0.1) )
  #=> array([[ 0.17652784,  0.16546515],
  #=>        [-0.08882108,  0.13568364],
  #=>        [-0.07246944,  0.15960082],
  #=>        [ 0.06714137, -0.02665557],
  #=>        [-0.05413774, -0.06482775]], dtype=float32)

本例ではstddevが0.1に設定されている。実行してみると-0.2〜0.2の間の数値がランダムに出力されているようだ。

切断正規分布は定義域(A〜Bの間に収まる)をどこかで決める必要があるけど、ここではstddevの2倍が設定されるらしい。指定範囲から外れる場合は再選択される。

試しに10万回呼び出してヒストグラムを書いてみる。

x = sess.run( tf.truncated_normal([100000, 1], stddev=-0.1) )
plt.hist(x, bins=100)

hist truncated normal

確かに-0.2〜0.2を外れる値が無理やり切断された感があるグラフになっている。

この-0.2〜0.2の分布で設定されたランダムな値が、(5, 5, 1, 32)のshapeでtf.Variableに設定され、第一層のWeightとされている。

対するbiasの方は tf.constant(0.1, shape=[32]) と固定値が設定されている。

別に初期値をrandomにしなくても0でもいいんじゃね、と思ったけどチュートリアルの説明によると大事なことらしい。

One should generally initialize weights with a small amount of noise for symmetry breaking, and to prevent 0 gradients.

tf.nn.conv2d

次にconv2dの動きを確認する。名前からして2D画像の畳み込みをする機能だと思われる。

tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME')

公式ドキュメントによると、ここで指定している引数はinput, filter, strides, paddingの4つ。

実際にこれを使うと画像にどういった変化が発生するのか、画像1つに対して実行して入力と出力の差を確認してみる。

# 画像1個に対して実行
c2d = tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME')
sess.run(tf.initialize_all_variables())
batch_x, batch_y = mnist.train.next_batch(1)
result = sess.run( c2d, feed_dict={x: batch_x, y_: batch_y} )

# shapeを確認
result.shape
  #=> (1, 28, 28, 32)

28 * 28 * 1だった画像が、28 * 28 * 32になっている。

出力結果を確認してみる。

# 入力画像
plt.imshow(batch_x.reshape(28, 28), cmap = cm.Greys_r, vmin=-1.0, vmax=1.0)

# 出力結果
f, axarr = plt.subplots(4, 8)
for idx in range(32):
    img = result[0, :, :, idx]
    axarr[int(idx / 8)][idx % 8].imshow(img.reshape(28, 28), cmap = cm.Greys_r, vmin=-1.0, vmax=1.0)
    axarr[int(idx / 8)][idx % 8].get_xaxis().set_visible(False)
    axarr[int(idx / 8)][idx % 8].get_yaxis().set_visible(False)

下図が入力画像。

plot

これが下図のような32個の出力になった。

plot

これだとわかりづらいので、値が1でshapeの4つめが1のfilterを噛ませる単純な例で実行してみて、どのように変化したかをグラフで比較してみる。

# 5, 5, 1, 1で設定
W_conv1 = tf.Variable(tf.ones([5, 5, 1, 1]))
c2d = tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME')
sess.run(tf.initialize_all_variables())
result = sess.run( c2d, feed_dict={x: batch_x, y_: batch_y} )

# 使用前
f, axarr = plt.subplots(28, 1, sharex=True, sharey=True)
for line in range(28):
    batch_line = batch_x[0].reshape((28, 28))[line]
    axarr[line].plot(batch_line)
    axarr[line].get_xaxis().set_visible(False)

# 使用後
f, axarr = plt.subplots(28, 1, sharex=True, sharey=True)
for line in range(28):
    result_line = result[0, :, line, 0]
    axarr[line].plot(result_line)
    axarr[line].get_xaxis().set_visible(False)

conv2d使用前

plot

conv2d使用後

plot

tf.nn.relu

お次はrelu。Rectifierという名前でWikipediaに載っている。

公式サイトの説明によると、下記のようなもの。

Computes rectified linear: max(features, 0).

単純に0以下であれば0とするみたいなものらしい。

sess.run( tf.nn.relu( [0.0, 1.0, -1.0, 0.5, -0.5]) )
  #=> array([ 0. ,  1. ,  0. ,  0.5,  0. ], dtype=float32)

これの何が嬉しいかは、チュートリアルやWikipediaにいくつか理由が書いてある。シグモイド関数より速いとか、ランダムで50%の隠れ層が働くことになるとか。しかし素人的にはそう書かれても今ひとつピンとは来ない。

今回reluが使われているのはこんな式。

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)

b_conv1は定数で0.1。畳み込みして0.1足したところにreluすることになる。

試しに1画像に対して実行して、どういった変化が起きるか確認してみる。

# 1つだけ画像を入れて見てみる
batch_x, batch_y = mnist.train.next_batch(1)
result = sess.run(h_conv1, feed_dict={x: batch_x, y_: batch_y})

# 1つの28*28の画像に対して、32個のアウトプットが出てくる
result.shape
  #=> (1, 28, 28, 32)

# 出来上がった画像を表示
f, axarr = plt.subplots(4, 7)
for idx in range(28):
    arr = axarr[int(idx / 7)][idx % 7]
    arr.imshow(result[0, :, :, idx], cmap = cm.Greys_r, vmin=0.0, vmax=1.0)

下記が出力された32枚。0.0のところが黒で、四隅あたりの濃いグレーは0.1。

plot

画像だとピンと来ない部分もあるので、ワイヤーフレームで見比べてみる。

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 元画像
org_img = np.array([(i, j, batch_x[0].reshape(28, 28)[i, j]) for j in range(28) for i in range(28)])
fig = plt.figure().gca(projection='3d')
fig.plot_wireframe(org_img[:, 0], org_img[:, 1], org_img[:, 2] )

# 出力画像の1枚目
result_img1 = np.array([(i, j, result[0, i, j, 0]) for j in range(28) for i in range(28)])
fig = plt.figure().gca(projection='3d')
fig.plot_wireframe(result_img1[:, 0], result_img1[:, 1], result_img1[:, 2] )

# 出力画像の1枚目
result_img2 = np.array([(i, j, result[0, i, j, 1]) for j in range(28) for i in range(28)])
fig = plt.figure().gca(projection='3d')
fig.plot_wireframe(result_img2[:, 0], result_img2[:, 1], result_img2[:, 2] )

元画像

plot

変換後

plotplot

そもそも畳み込みのご利益を実感できてないので「こう変わるのかー」という感想しか持てないけど、とりあえず何をしているかはわかった。真面目に勉強すればそのうちわかるようになる気がする。

tf.nn.relu

次はnn.max_pool。2x2でプーリングするらしい。

h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

今回も1画像だけで実行して変化を見てみる。

# 1画像だけ実行
sess.run(tf.initialize_all_variables())
result = sess.run(h_pool1, feed_dict={x: batch_x, y_: batch_y})

# shapeを確認
result.shape
  #=> (1, 14, 14, 32)

# 画像を確認
f, axarr = plt.subplots(4, 8)
for idx in range(32):
    axarr[int(idx / 8)][idx % 8].imshow(result[0, :, :, idx], vmin=0.0, vmax=result.max(), cmap = cm.Greys_r)

2x2でpoolingしたので半分の14x14の画像が出力された。

plot

当然ながら4x4で実行すれば、さらに半分の7x7のサイズになる。

h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 4, 4, 1], strides=[1, 4, 4, 1], padding='SAME')
sess.run(tf.initialize_all_variables())
result = sess.run(h_pool1, feed_dict={x: batch_x, y_: batch_y})

# shapeを確認
result.shape
  #=> (1, 7, 7, 32)

# 画像を確認
f, axarr = plt.subplots(4, 8)
for idx in range(32):
    axarr[int(idx / 8)][idx % 8].imshow(result[0, :, :, idx], vmin=0.0, vmax=result.max(), cmap = cm.Greys_r)

plot

第2層のtf.nn.conv2d

第1層まで終わった。次は第2層。行う処理は第1層と同じ。

まずはconv2dのところまで流してみる。

W_conv2 = tf.Variable(tf.truncated_normal([5, 5, 32, 64], stddev=0.1))
b_conv2 = tf.Variable(tf.constant(0.1, shape=[64]))
h_conv2 = tf.nn.relu(tf.nn.conv2d(h_pool1, W_conv2, strides=[1, 1, 1, 1], padding='SAME') + b_conv2)

第1層ではfilterのin_channels(shapeの3つめ)が1だったが、第1層の処理で32に増加しているので、in_channelsが32に、そしてout_channel(shapeの4つめ)が64になっている。

画像のサイズは(14, 14)になっているので、画像1個に対して流すと結果は(1, 14, 14, 64)で得られることになる。

sess.run(tf.initialize_all_variables())
result = sess.run(h_conv2, feed_dict={x: batch_x, y_: batch_y})

# shapeの確認
result.shape
  #=> (1, 14, 14, 64)

# 画像を確認
f, axarr = plt.subplots(8, 8)
for idx in range(64):
    axarr[int(idx / 8)][idx % 8].imshow(result[0, :, :, idx], vmin=0.0, vmax=result.max(), cmap = cm.Greys_r)

plot

なんかよくわからない出力になってきた。

第2層のtf.nn.max_pool

第1層の時と同じようにmax_poolを実行する。

h_pool2 = tf.nn.max_pool(h_conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

今回も[1, 2, 2, 1]でやるようだ。ということはshapeは7 * 7になるはず。

# 画像1枚に対して実行
sess.run(tf.initialize_all_variables())
result = sess.run(h_pool2, feed_dict={x: batch_x, y_: batch_y})

# shape確認
result.shape
  #=> (1, 7, 7, 64)

# 結果確認
f, axarr = plt.subplots(8, 8)
for idx in range(64):
    axarr[int(idx / 8)][idx % 8].imshow(result[0, :, :, idx], vmin=0.0, vmax=result.max(), cmap = cm.Greys_r)

plot

ここまで来ると何が何やらわからんね。

Densely Connected Layer

ここまでで1つの画像が(7, 7, 64)のshapeに変換された。次はこれを一次元に直してreluするらしい。

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_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

reluのところで[画像数, 3136=7*7*64]の形式のh_pool2に対して、W_fc2(1024チャンネル)をtf.matmulしているので、出力されるshapeは[画像数, 1024]になる、というのはわかるのだけど、これで何が取り出せるのだろう?

sess.run(tf.initialize_all_variables())
result = sess.run(h_fc1, feed_dict={x: batch_x, y_: batch_y})

# shapeの確認
result.shape
  #=> (1, 1024)

# なんとなくplot
plt.plot(result[0])

plot

こうして1024次元の特徴が生成されたわけだけど、勉強不足でこの処理でなぜうまくいくのかがさっぱりわからない。本読んで勉強しないと。

Dropout

ちょっと付いていけなくなってきているけど、この後はDropoutして、Softmaxするところなのでわかりやすい。

まずはDropout。前回実行したh_fc1から間引きすることになる。両者を比較してみる。

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
sess.run(tf.initialize_all_variables())

# place_holder keep_probに対して、0.8を指定
result = sess.run(h_fc1_drop, feed_dict={x: batch_x, y_: batch_y, keep_prob: 0.2})

# 画像表示
plt.plot(result[0])

左端が元画像。そこからkeep_prob=0.8, 0.5, 0.2と減らしていった画像を並べた。

plot plot plot plot

AdamOptimizer

後は出来上がった値に対して、前回も使ったsoftmaxで学習させることになるのだけど、若干変わった点として、GradientDescentOptimizerではなくAdamOptimizerが使われている。

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)
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

Adamってなんだ、初耳だぞ。と思ったら2015年に出た比較的新しいアルゴリズムらしい。CNNではよく使われるようで、勾配を計算して調整していくあたりはSGDと似たようなものっぽいけど、実装が簡単な割に収束が速いとか。

まとめ

TensorflowのチュートリアルにあるCNNによる画像認識のコードを追いかけてみた。1つ1つのコードが何をしているかはわかったが、真面目に勉強しないとなぜそういう処理をしているのか、なぜそれで精度が出るのかという点がさっぱりわからない。

おそらく人がやってる方法を真似してそれっぽいことはできるのだろうけど、もう少し「理解」してない応用分野に手を出せないので本やスライドを読みながらちまちま勉強はしていかないといけない。