roombaの日記

読書・非線形科学・プログラミング・アート・etc...

TensorFlowでのSoftmax回帰の実装・可視化・識別器の騙し方

概要

先日の記事では深層学習用の自作PCを構築する方法について書きました。
これからは主にTensorFlowを用いてさまざまなモデルを実装していこうと考えています。

roomba.hatenablog.com

まずは基本的なソフトマックス回帰(他クラスのロジスティクス回帰)を実装し、有名なMNISTデータセットについて手書き数字の識別を行います。これさえできれば、ディープなニューラルネットワークも自然な拡張として簡単に実装することが可能です。
この記事では、TensorFlowの公式のチュートリアル*1を参考にしつつ、

  • 一般的なニューラルネットワークへの拡張を容易にすること
  • TensorFlowで重要となるComputational Graphの概念を明確にし、Graphの構築と実行をわかりやすく分離すること
  • 強力な可視化ツールであるTensorBoardの使い方も同時に理解できるようにすること
  • Computational Graph上での機能のまとまりを明確にするために名前空間(name scope)を適切に設定すること

を意識した実装を行いました。また、それだけではつまらないので、

  • 重みの可視化
  • 学習済みの識別器をだます
  • 識別器が自信を持って分類したサンプルほど正答率が高いか検証する

といった実験を行っています。

このブログの内容は、Jupyter Notebook (IPython Notebook) 形式でも公開しています。
Jupyter Notebook Viewer



目次:

f:id:roomba:20170420205250p:plain

1. MNISTデータセットの用意

以下のように簡単にデータセットを使うことができます。

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

可視化してみます。

# 表示してみる
import matplotlib.pyplot as plt
%matplotlib inline

fig = plt.figure(figsize=(12,6))

for i, img in enumerate(mnist.train.images[:25]):
    ax = fig.add_subplot(5,5,i+1)
    ax.imshow(img.reshape(28, 28), interpolation="none")

f:id:roomba:20170420205721p:plain

2. Softmax Regressionの実装

TensorFlowのプログラムは以下の2つの段階からなります。

  • Computational Graphを構築(計算の依存関係の定義)
  • Computational Graphを実行(ここで初めて具体的な計算が行われる)

順番に実装していきます。

そもそもなぜこのような手順が必要なのか、誤差逆伝播はどう行われているのか、ということが分からない場合は以下の記事をおすすめします。
Calculus on Computational Graphs: Backpropagation -- colah's blog

2.1 準備

tensorflowをimportするほか、可視化ツールTensorBoardのためのログの保存先の指定、テンソルの情報のログを取るための関数定義などを行います。

# import
import tensorflow as tf
# ログ保存用のディレクトリがあれば一度削除し、新たに作りなおす
LOG_DIR = "/tmp/softmax"
if tf.gfile.Exists(LOG_DIR):
    tf.gfile.DeleteRecursively(LOG_DIR)
tf.gfile.MakeDirs(LOG_DIR)

# TensorBoardのための準備
def variable_summaries(var):
    """ テンソルに様々なsummaryを付加する(TensorBoardでの可視化に利用)
    Args:
        var: summaryを付加したいテンソル
    Returns:
        なし(summary opの付加を行うだけの関数)
    """
    with tf.name_scope('summaries'):
        mean = tf.reduce_mean(var)
        tf.summary.scalar('mean', mean)
        with tf.name_scope('stddev'):
            stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean)))
        tf.summary.scalar('stddev', stddev)
        tf.summary.scalar('max', tf.reduce_max(var))
        tf.summary.scalar('min', tf.reduce_min(var))
        tf.summary.histogram('histogram', var)

2.2 Computational Graphの構築

まずは、データを順次与えるためにplaceholderを用意します。MNISTデータセットは手書き文字画像とラベルから構成されているため、それぞれに対してplaceholderを与えます。
さらに、学習可能なモデルのパラメータをtf.Variableとして宣言します。

with tf.name_scope("input"):
    # データを与えるためのplaceholder
    x = tf.placeholder(tf.float32, [None, 784], name = "image") # input image
    y_ = tf.placeholder(tf.float32, [None, 10], name = "label") # label
     
with tf.name_scope("variables"):
    # 学習可能なパラメータ
    with tf.name_scope("Weight"):
        W = tf.Variable(tf.zeros([784, 10]))
        variable_summaries(W)
    with tf.name_scope("Bias"):
        b = tf.Variable(tf.zeros([10]))
        variable_summaries(b)

次に、入力からラベルの推論(inference)、誤差関数の計算、モデルの訓練、モデルの評価を行うComputational Graphの一部を作るための関数を定義します。

def inference(images, weights, biases):
    """ 画像からそのクラスを予想する
    Args: 
        images: 画像のplaceholder
        weights: 重みのVariable
        biases: バイアスのVariable
    Returns:
        logits: 計算されたlogit
    """
    with tf.name_scope("logits"):
        logits = tf.matmul(images, weights) + biases
        tf.summary.histogram("logits_histogram", logits)
    return logits

def loss(logits, labels):
    """ logitとlabelから誤差関数(交差エントロピー)を計算する
    Args: 
        logits:識別器の予測
        labels:正しいラベル
    Returns:
        cross_entropy: 交差エントロピー
    """
    with tf.name_scope("loss"):
        cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=logits), name="cross_entropy_mean")
        tf.summary.scalar('cross_entropy', cross_entropy) # ログをとる
    return cross_entropy

def train(loss, learning_rate):
    """ 訓練用のopを生成する
    Args:
        loss
        learning_rate
    Returns:
        The op for training
    """
    with tf.name_scope("train"):
        optimizer = tf.train.GradientDescentOptimizer(learning_rate)
        train_op = optimizer.minimize(loss)
    return train_op

def evaluation(logits, labels):
    """ logitsがlabelsを予測する精度を評価する
    Args:
        logits
        labels
    Returns:
        正解率
    """
    with tf.name_scope("evaluation"):
        correct = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1))
        accuracy = tf.reduce_mean(tf.cast(correct, tf.float32), name="accuracy")
        tf.summary.scalar("accuracy", accuracy) # ログをとる
    return accuracy

部分的なComputational Graphを作るための関数が定義できたので、Computational Graphの全体を構築します。各量の依存関係を宣言するようなイメージです。

# グラフを構築
logits = inference(x, W, b)
loss = loss(logits, y_)
train_op = train(loss, 0.1)
accuracy = evaluation(logits, y_)
summary = tf.summary.merge_all() # すべてのsummaryをひとまとめにするop

これで以下のようなComputational Graphを構築したことになります。この図を表示させる方法は後ほど記します。
f:id:roomba:20170420204530p:plain

2.3 Computational Graphの実行

ここまではComputational Graphを構築しただけであって、具体的なデータに対して何か計算が行われたわけではありません。値を評価したり学習を行うためにはまずSessionを作り、global_variable_initializer()を実行することで変数を初期化します。

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

ログをファイルに出力するためのwriterをこのあたりで作っておきましょう。

# summaryをファイルに出力するためのwriter
train_writer = tf.summary.FileWriter(LOG_DIR+"/train", sess.graph) # sess.graphを渡すことでComputational GraphをTensorBoardに可視化
test_writer = tf.summary.FileWriter(LOG_DIR+"/test")

いよいよ学習を行います。データセットからミニバッチを順次取り出し、train_opを実行することで学習(重みやバイアスの更新)が行われます。定期的にテストデータに対しての性能を記録してみます。

MAX_STEP = 2000
BATCH_SIZE = 100
for step in range(MAX_STEP):
    batch_xs, batch_ys = mnist.train.next_batch(BATCH_SIZE)
    sess.run(train_op, feed_dict={x: batch_xs, y_: batch_ys}) # trainを実行
    # 100stepごとにテスト結果を表示
    if step % 100 == 0:
        test_loss, test_accuracy = sess.run([loss, accuracy], feed_dict={x: mnist.test.images, y_: mnist.test.labels})
        print("step: {}, test_accuracy: {}, test_loss: {}".format(step, test_accuracy, test_loss))
        # ログの実行
        train_summary_str = sess.run(summary, feed_dict={x: batch_xs, y_: batch_ys})
        test_summary_str = sess.run(summary, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
        train_writer.add_summary(train_summary_str, step)
        test_writer.add_summary(test_summary_str, step)
train_writer.close()
test_writer.close()

テストデータに対する正答率は以下のように表示できます。テストデータをplaceholderにfeedしてやり、accuracyを評価しています。単純なモデルなので92%程度の正答率でした。

print("Accuracy: {}".format(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels})))

3. TensorBoardによる可視化

ここまでのプログラムを実行すると、指定した場所(ここでは/tmp/softmax)にログが保存されます。これをもとにTensorBoardを開くことで様々なデータを可視化することができます。

3.1 TensorBoardの起動

$ tensorboard --logdir=/tmp/softmax

をターミナルで実行します。

Starting TensorBoard 41 on port 6006

のように表示されるので、ブラウザで
http://localhost:6006/
を開けばOKです。

3.2 TensorBoardの画面のスクリーンショット

  • tf.summary.scalar()によって記録した値の変化は以下のように折れ線グラフで見ることができます。横軸は経過stepで、これは誤差関数の減少過程を示しています。対数グラフにもボタン1つで変換できます。

f:id:roomba:20170420204535p:plain

  • tf.summary.histogram()によって記録した量の分布の時間変化は以下のように表示されます。ソフトマックスにかける前のモデルの出力(logit)がstepの経過とともにある程度の広がりを見せるようになっていることがわかります。

f:id:roomba:20170420204533p:plain

  • 同じくtf.summary.histogram()によって記録した量のヒストグラムは、以下のようにも可視化されます。左側を見ると、バイアスの値がstepの経過(奥から手前)に伴ってばらばらの値を取るようになっていることがわかります。

f:id:roomba:20170420204534p:plain

  • さらに、tf.summary.FileWriter()にsess.graphを渡すことで、Computational Graphを可視化することが可能です。先程もちらっと載せましたが、以下のようになっています。

f:id:roomba:20170420204530p:plain
Computational GraphをTensorBoard上でわかりやすく表示するためには、tf.name_scope()を使って名前空間を適切に設定してやることが必要です。うまく設定してやれば各量の依存関係が明確にわかります。例えば上の図では、

  • logits(ソフトマックスにかける前のモデルの出力)はWeight, Bias, images(手書き文字画像)から計算できる
  • lossはlogitsとlabel(正しいラベル)の比較により定まる
  • evaluationもlogitsとlabelの比較により定まる

ということが分かります。TensorFlowでモデルを定義するときは、このような図をあらかじめ思い浮かべながらComputational Graphを構築していくと良いでしょう。

4. 重みの可視化

Softmax回帰は、

  • 重み行列Wに含まれる各ベクトル(今回は10クラスなので10個)と入力画像との内積をとる
  • バイアスを足してSoftmaxする

という計算によって事後確率を求めています。したがって、「最もiと認識されやすい画像」は重みのi番目のベクトル(i=0, 1, ..., 9)を定数倍したものになります。可視化してみましょう。

fig = plt.figure(figsize=(12,6))

for i in range(W.eval().shape[1]):
    ax = fig.add_subplot(2,5,i+1)
    ax.imshow(W.eval()[:, i].reshape(28, 28), interpolation="none")

左上から順に、最も0と認識されやすい画像、...、最も9と認識されやすい画像となっています。
f:id:roomba:20170420205250p:plain

5. 識別器をだましてみる

様々な文字画像に先述の重み画像を混ぜあわせることを考えます。
すると、人間の目には依然として正しく識別できるにもかかわらず、識別器は誤って識別するようになってしまいます。例えば"7"に対応する重みを混ぜあわせると、多くの(7でない)画像を"7"と予想するようになるのです。
以下の記事も参考にしてください。
blog.openai.com

# "7"に対応する重みをテスト用画像に足し合わせて表示する
import numpy as np
fake_x = np.array([img + W.eval()[:, 7] for img in mnist.test.images[:25]])

fig = plt.figure(figsize=(12,6))
for i, img in enumerate(fake_x):
    ax = fig.add_subplot(5,5,i+1)
    ax.imshow(img.reshape(28, 28), interpolation="none")

f:id:roomba:20170420213811p:plain
この画像は左上から7, 2, 1, 0, 4, ...であることが容易にわかると思います。しかし、今回のモデルに識別させてみると大半を"7"と答えてしまいます。

print(sess.run(tf.argmax(logits, 1), feed_dict={x: fake_x}))
# 実行結果:[7 2 7 7 7 7 7 7 7 7 0 7 7 0 7 7 7 7 7 7 7 6 7 7 7]

6. 自信があるほどよく当たるのか

Softmax回帰では、入力画像を観測した時の各クラスの事後確率を推定し、事後確率が最大となるクラスをモデルの予想としています。事後確率の最大値が90%となる入力画像と50%となる入力画像では、自信を持って答えた前者の方が正答率が高いはずです。それを確かめてみましょう。

posteriors = sess.run(tf.nn.softmax(logits), feed_dict={x: mnist.test.images})# テストデータに対してモデルが出力した事後確率
is_true = np.argmax(posteriors, 1) == np.argmax(mnist.test.labels, 1) # 正解したデータにのみTrueが入ったarray
# 正しく識別できたデータについて、正しいクラスに対する事後確率がどのような値をとっていたかを表すヒストグラムを描画
plt.hist(np.max(posteriors[is_true], 1))

f:id:roomba:20170420214230p:plain

正しく識別できたデータに対しては、90%以上の自信を持っていたことが多いようです。では、識別に失敗したデータについて、間違えて予測したクラスに対する事後確率がどのような値をとっていたかを表すヒストグラムを描画してみましょう。

plt.hist(np.max(posteriors[~is_true], 1))

f:id:roomba:20170420214330p:plain

やはり、正しく識別できたデータよりも自信がなかったようです。自信の強さ(予想クラスに対して弾き出した事後確率)と正答率の関係をグラフにしてみましょう。

# 事後確率のクラス間の最大値を0.1刻みにしたもの
floor_posteriors = np.array(map(lambda x: int(x*10)/10., np.max(posteriors, 1)))

# モデルの予想した事後確率と、正解(1)不正解(0)をたばねたもの
count = np.concatenate((np.vstack(floor_posteriors), np.vstack(is_true.astype(int))), axis=1)

# 事後確率が最大となるクラスにおける事後確率ごとに、正答率を記録する
h = []
for i in range(1, 10):
    a = np.array(filter(lambda x: x[0]==i/10., count))
    if a.any():
        h.append([i/10., np.sum(a[:, 1])/a.shape[0] ])
h = np.array(h)

plt.xlabel("Max posterior")
plt.ylabel("Accuracy")
plt.plot(h[:, 0], h[:, 1])

f:id:roomba:20170420214553p:plain
予想通りの結果ですね。

7. おわりに

次回は、同じMNISTデータセットに対して

のどちらかを用いて識別を行ってみたいと思います。

この記事の内容については以下の文献やWebサイトが参考になります。

  • 理論的な内容

  • 全体的な内容

MNIST For ML Beginners  |  TensorFlow

Visualizing MNIST: An Exploration of Dimensionality Reduction - colah's blog

Visual Information Theory -- colah's blog

  • TensorBoardの使い方

TensorBoard: Graph Visualization  |  TensorFlow