NN初心者がTensorFlowのチュートリアルの内容を寄り道しながらこなす企画のパート2。
今回はDeep MNIST for Expertsのところ。前回は単純なモデルで91%程度の精度だったけど、今回は畳み込みニューラルネットワーク(convolutional neural network)で99%以上の精度を達成するらしい。
バージョンは0.7.1でPython3.4、GPU版を利用。OSはUbuntu系。
まずは前回の復習からスタートしている。
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について、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件)に対して実行するので、サイズが合わずエラーになるようだ。
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%に張り付くようになっている。
同じ図を90%以上のみに絞ってplotする。徐々に精度が良くなっていることがわかる。
今回のソースコードの精度が良いということはわかった。しかし素人の私はソースだけ見ても何がなんだか意味がわからない。
前回同様、今回も段階を踏んで理解していくことにする。
まず、最初に出てくる知らない関数、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)
確かに-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.
次に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)
下図が入力画像。
これが下図のような32個の出力になった。
これだとわかりづらいので、値が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使用前
conv2d使用後
お次は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。
画像だとピンと来ない部分もあるので、ワイヤーフレームで見比べてみる。
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] )
元画像
変換後
そもそも畳み込みのご利益を実感できてないので「こう変わるのかー」という感想しか持てないけど、とりあえず何をしているかはわかった。真面目に勉強すればそのうちわかるようになる気がする。
次は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の画像が出力された。
当然ながら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)
第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)
なんかよくわからない出力になってきた。
第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)
ここまで来ると何が何やらわからんね。
ここまでで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])
こうして1024次元の特徴が生成されたわけだけど、勉強不足でこの処理でなぜうまくいくのかがさっぱりわからない。本読んで勉強しないと。
ちょっと付いていけなくなってきているけど、この後は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と減らしていった画像を並べた。
後は出来上がった値に対して、前回も使った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つのコードが何をしているかはわかったが、真面目に勉強しないとなぜそういう処理をしているのか、なぜそれで精度が出るのかという点がさっぱりわからない。
おそらく人がやってる方法を真似してそれっぽいことはできるのだろうけど、もう少し「理解」してない応用分野に手を出せないので本やスライドを読みながらちまちま勉強はしていかないといけない。