CNNの構築

以下のようなCNNを TensorFlow でこれから作っていきます。

重みとバイアスを作成する Helper 関数

重みとバイアスを生成する Helper 関数を作成しておきます。
重みは標準正規分布、バイアスは定数 0.1 で各値を初期化する設定にします。

def weight_variable(shape):
  initial = tf.truncated_normal(shape)
  return tf.Variable(initial)

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

入力

MNIST の各画像は 28×28 ピクセルのグレースケール画像ですが、
x_batch, t_batch = mnist.train.next_batch(Batchsize) で取得したとき、x_batch は (Batchsize, 784) という形状で入ってくるので、(28, 28, 1) という本来の形に形状を整えます。

x = tf.placeholder("float", [None, 784])
x_image = tf.reshape(x, [-1, 28, 28, 1])

畳み込み層、プーリング層の作成

同様に畳み込み層、プーリング層を生成する Helper 関数を作成しておきます。
tf.nn.conv2d(x, W, strides, padding) は入力 x に対して、重み w のカーネルによる畳み込み演算を行う計算グラフを作成します。
(バッチ数、幅、高さ、チャンネル数)というテンソルに対して畳み込みを行うので、ストライドも4次元の各方向指定する必要があります。
実際指定するのは、縦横方向のストライドなので、stride=[1, 縦方向のストライド, 横方向のストライド, 1]です。
今回のネットワークではストライドは畳み込み層で縦横共に1、プーリング層で縦横共に2とするので、関数内に直接書きます。
padding=’SAME’を指定すると、カーネルサイズに応じて縮小してしまう分をゼロパディングしてくれます。

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

第2層 (畳み込み層)

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
y_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)

第3層 (プーリング層)

y_pool1 = max_pool_2x2(y_conv1)

第4層 (畳み込み層)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
y_conv2 = tf.nn.relu(conv2d(y_pool1, W_conv2) + b_conv2)

第5層 (プーリング層)

y_pool2 = max_pool_2x2(y_conv2)

全結合層の作成

全結合層に渡すために (n, 7, 7, 64) を (n, 7 * 7 * 64) の形状にします。

y_pool2_flat = tf.reshape(y_pool2, [-1, 7 * 7 * 64])

あとは全結合層のみのチュートリアルと同様です。
第6層 (全結合層)

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
y_fc1 = tf.nn.relu(tf.matmul(y_pool2_flat, W_fc1) + b_fc1)

第7層 (全結合層)

0~9 の数字の確率値が出力となるように、活性化関数はSoftmax関数とします。

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y = tf.nn.softmax(tf.matmul(y_fc1, W_fc2) + b_fc2)

ほかは全結合層のみのチュートリアルと同様のため、省略します。

コード全体

# -*- coding: utf-8 -*-
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

def weight_variable(shape):
  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)

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
  
def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

# ニューラルネットワークを計算グラフで作成する
# 第1層 (入力層)
x = tf.placeholder("float", [None, 784])

# 形状変更
x_image = tf.reshape(x, [-1, 28, 28, 1])

# 第2層 (畳み込み層)
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
y_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)

# 第3層 (プーリング層)
y_pool1 = max_pool_2x2(y_conv1)

# 第4層 (畳み込み層)
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
y_conv2 = tf.nn.relu(conv2d(y_pool1, W_conv2) + b_conv2)

# 第5層 (プーリング層)
y_pool2 = max_pool_2x2(y_conv2)

# 形状変更
y_pool2_flat = tf.reshape(y_pool2, [-1, 7 * 7 * 64])

# 第6層 (全結合層)
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
y_fc1 = tf.nn.relu(tf.matmul(y_pool2_flat, W_fc1) + b_fc1)

# 第7層 (全結合層)
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y = tf.nn.softmax(tf.matmul(y_fc1, W_fc2) + b_fc2)

# 損失関数を計算グラフを作成する
t = tf.placeholder("float", [None, 10])
cross_entropy = -tf.reduce_sum(t * tf.log(y))

# 次の(1)、(2)を行うための計算グラフを作成する。
# (1) 損失関数に対するネットワークを構成するすべての変数の勾配を計算する。
# (2) 勾配方向に学習率分移動して、すべての変数を更新する。
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

# 初期化を行うための計算グラフを作成する。
init = tf.global_variables_initializer()

# テストデータに対する正答率を計算するための計算グラフを作成する。
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(t, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

# MNIST 入力データ
mnist = input_data.read_data_sets("data/", one_hot=True)

# セッションを作成して、計算グラフを実行する。
with tf.Session() as sess:
  
    # 初期化を実行する。
    sess.run(init)
  
    # 学習を実行する。
    for i in range(20000):
        x_batch, t_batch = mnist.train.next_batch(50)
        sess.run(train_step, feed_dict={x: x_batch, t: t_batch})
        
        if i % 100 == 0:
            result = sess.run(accuracy, feed_dict={x: mnist.test.images, t: mnist.test.labels})
            print(result)

    result = sess.run(accuracy, feed_dict={x: mnist.test.images, t: mnist.test.labels})
    print("accuracy:", result)

計算グラフの構築

以下のような2つ全結合(FC)層から構成される単純なニューラルネットワークを TensorFlow でこれから作っていきます。
ネットワークを構成するパラメータは第1層から第2層への結合の重み \(W\) 及びバイアス \(\mathbf{b}\) です。
0~9 の数字の確率値が出力となるように、第2層の活性化関数はシグモイド関数とします。



これを計算グラフで表すと以下のようになります。

Placeholder と Variable

placeholder は計算実行時に計算グラフに入力を与える場所です。
例えば、\(f(x) = Wx (W\in {{\mathbb{R}}^{m\times n}})\) という関数について考えた場合、
関数の引数 \(x\) にあたるものが placeholder であり、受け取れる引数は \(n \times l\) 行列に制限しておく必要があります。



Variable はネットワークを構成する変数で、学習する対象です。
最初になんらかの値で初期化しておく必要があります。

計算グラフの次元

これから作る計算グラフを行列式で表すと、次のようになります。



入力 \(X\) は行列がバッチ数、列が次元数に対応した行列です。


ミニバッチのページで紹介した式とは変数の順序が逆になっていますが、両辺の転置をとれば、同じ式であることが確かめられます。


TensorFlow の API

では、TensorFLow のAPIで作っていきます。
\(X\) は計算グラフの入力なので、tf.placeholder() 関数で作成します。

tf.placeholder(dtype, shape=None, name=None)

\(X\) は \(n \times 784\) の行列ですが、\(n\) はミニバッチ数なので、10とか100とかなんでも構いません。
None としておくことで、任意の数とできます。

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

重み \(W\) 及びバイアス \(\mathbf{b}\) は変数なので、tf.Variable クラスで作成します。

__init__(initial_value=None)
変数なので、tf.zeros() で零行列を与えて、初期化します。

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

次に演算子でこれらの入力や変数を繋げて計算グラフを作っていきます。
行列同士の積はtf.matmul() を使います。
* 演算子を使うと行列積でなく、要素ごとの積になってしまうので注意してください。
加算は+演算子で行えます。

tf.matmul(x, W) + b

tf.matmul(x, W) の結果は (n, 10) であり、一方 \(\mathbf{b}\) は (10) なので形状が異なります。
なので加算する際は、numpy 同様に Broadcasting が行われ、\(n \times 10\) の形状に変換した上で加算が行われます。

最後に活性化関数を適用します。
ソフトマックス関数はtf.nn.softmax()を使用します。

tf.nn.softmax(logits, dim=-1, name=None)

y = tf.nn.softmax(tf.matmul(x, W) + b)

損失関数の定義

MNIST は 0~9 の数字のどれか分類する問題なので、損失関数のページの多クラス分類問題にあたります。
この場合の損失関数は、次のクロスエントロピー関数になるのでした。
\[L(\mathbf{w})=-\sum\limits_{n=1}^{N}{\sum\limits_{k=1}^{K}{\log {{t}_{nk}}p(k|{{\mathbf{x}}_{n}})}}\]

tf.log() で要素ごとに log をとり、次に正解データとの要素ごとの積をとります。



最後に tf.reduce_sum() で全部の要素の総和を計算し、マイナスをとることでクロスエントロピー関数の式が完成します。
正解データも計算実行時に与える必要があるので、\(n \times 10\) の Placeholder を作ります。

t = tf.placeholder("float", [None, 10])
cross_entropy = -tf.reduce_sum(t * tf.log(y))

学習方法の定義

ニューラルネットワークの学習とは損失関数を最小化することです。
今回は先程計算グラフで作成した cross_entropy が最小化する目標関数になります。
TensorFlow では関数の最小化を行ういくつかのアルゴリズムが提供されていますが、今回は最急降下法を使用します。
これはtf.train.GradientDescentOptimizer クラスを使用します。
学習率 0.01 を与えてインスタンスを作成します。
次にminimize() を使用して、勾配ベクトルを計算し、勾配方向に移動して各変数を更新するという2段階の処理を行うための計算グラフを作成します。

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

初期化を行うための計算グラフを作成する。

各変数は使用する前になんらかの値を代入して初期化する必要があります。

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

としただけでは、初期化方法を指定しただけなので、tf.zeros() で作成した行列を \(W\) に代入するという計算グラフを作成します。
これは、global_variables_initializer() で行います。

init = tf.initialize_all_variables()

学習を実行する

必要なすべての計算グラフは揃いましたので、入力を計算グラフに流し込み、学習を実行していきます。
計算を実行するための環境 Session を tf.Session() で作成します。
まず sess.run(init) で変数の初期化を実行します。

ミニバッチ数を100として、mnist.train.next_batch(100) で入力データと正解データのペアを取得します。

x_batch, t_batch = mnist.train.next_batch(100)

順伝播→逆伝播→変数更新という流れを実行します。
計算グラフ train_step には入力 x と正解データ y の2つの placeholder があるので、そこに以下のように流し込むデータを指定して、この計算グラフを実行します。

sess.run(train_step, feed_dict={x: x_batch, t: t_batch})

学習結果を確認する。

学習結果を確認するため、正答率を計算するための計算グラフを作成します。
正解データは (n, 10) の行列で各行は [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] というように正解のラベルの箇所が1 (この場合は2が正解)で他は0です。
一方、ニューラルネットワークの出力 y も (n, 10) の行列で各行は [0.1, 0.05, 0.7, 0, 0.05, 0.02, 0.02, 0.02, 0.02, 0.02] というような確率値になっています。一番大きい値のものを予測したクラスとします。
これらはいずれも argmax() をとることで実現できます。
さらに equal() で正解と予測したラベルが等しいかどうか比較し、BOOLの列ベクトルを作成します。
TRUEならば正解、FALSEならば不正解を意味します。
これを float に変換するとTRUEは1.0、FALSEは0.0になります。
これを redece_mean() で全部の和をとり \(n\) で割ると、この出力が正答率になります。

コード全体

MNIST のダウンロード先のサイトがダウンしてしまったため、web.archive.orgから手動でMNISTデータをダウンロードする必要があります。

  • train-images-idx3-ubyte.gz
  • train-labels-idx1-ubyte.gz
  • t10k-images-idx3-ubyte.gz
  • t10k-labels-idx1-ubyte.gz

ダウンロードしたら、python ファイルと同じ階層に data というディレクトリを作成し、上記4つのファイルを入れます。
(解凍は必要ありません。)

# -*- coding: utf-8 -*-
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# ニューラルネットワークを計算グラフで作成する
# 第1層 (入力層)
x = tf.placeholder("float", [None, 784])

# 第2層 (全結合層)
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(x, W) + b)

# 損失関数を計算グラフを作成する
t = tf.placeholder("float", [None, 10])
cross_entropy = -tf.reduce_sum(t * tf.log(y))

# 次の(1)、(2)を行うための計算グラフを作成する。
# (1) 損失関数に対するネットワークを構成するすべての変数の勾配を計算する。
# (2) 勾配方向に学習率分移動して、すべての変数を更新する。
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

# 初期化を行うための計算グラフを作成する。
init = tf.global_variables_initializer()

# MNIST 入力データ
mnist = input_data.read_data_sets("data/", one_hot=True)

# セッションを作成して、計算グラフを実行する。
with tf.Session() as sess:

    # 初期化を実行する。
    sess.run(init)

    # 学習を実行する。
    for i in range(1000):
        x_batch, t_batch = mnist.train.next_batch(100)
        sess.run(train_step, feed_dict={x: x_batch, t: t_batch})

    # テストデータを利用して、正答率を計算する。
    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(t, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

    result = sess.run(accuracy, feed_dict={x: mnist.test.images, t: mnist.test.labels})
    print("accuracy:", result)