MNIST

MNIST は手書き数字のデータセットです。
手軽に扱うことができるので、機械学習の Hello World 的存在になっています。

データの特徴

28×28ピクセル、グレースケールの手書き数字画像
学習データ6万枚、テストデータ1万枚

ダウンロード方法

Yann LeCun’s のページで配布されていたのですが、現在ダウンロードできなくなっているので web.archive.org から入手します。

  • train-images-idx3-ubyte.gz : 学習用画像データ
  • train-labels-idx1-ubyte.gz : 学習用ラベルデータ
  • t10k-images-idx3-ubyte.gz : テスト用画像データ
  • t10k-labels-idx1-ubyte.gz : テスト用ラベルデータ

データの構造

.gz を解凍すると、以下のようなバイナリファイルが入っています。

  • train-images-idx3-ubyte : 学習用画像データ
  • train-labels-idx1-ubyte : 学習用ラベルデータ
  • t10k-images-idx3-ubyte : テスト用画像データ
  • t10k-labels-idx1-ubyte : テスト用ラベルデータ

MNISTの画像データは28×28ピクセルの画像が枚数分含まれています。
先頭16バイトがヘッダでそのあとに画像が28×28=728バイト区切りで続いています。
ヘッダは4バイト区切りでマジックナンバー、画像枚数、画像は高さ、画像の幅の情報が入っています。
画像部分は1バイトがグレースケールの1ピクセルを表し、1行目、2行目、…、28行目というように入っています。

MNISTのラベルデータは画像に対応したラベルが含まれています。
先頭8バイトがヘッダでそのあとにラベルが1バイト区切りで続いています。
ヘッダは4バイト区切りでマジックナンバー、画像枚数の情報が入っています。
マジックナンバーはなんの意味があるのか不明です。ファイルの整合性を確認したりするのでしょうか。

Pythonコード

Pythonコードで書くと以下のようになります。
gzip を解凍するコードは含めてないので、手動で解凍し、MNISTデータと同じ階層で実行してください。

# -*- coding: utf-8 -*-
import struct

import cv2
import numpy as np

def get_data(img_file, label_file):
    with open(img_file, 'rb') as file:
        # ヘッダ16バイトを読みこみ、4つの uint8 として解釈する。
        magic, num, rows, cols = struct.unpack(">4I", file.read(16))
        print("magic={}, num={}, rows={}, cols={}".format(magic, num, rows, cols))

        # 残り全部を読み込み、1次元配列を作成した後、num x rows x cols に変形する。
        imgs = np.fromfile(file, dtype=np.uint8).reshape(num, rows, cols)

    with open(label_file, 'rb') as file:
        # ヘッダ8バイトを読みこみ、2つの uint8 として解釈する。
        magic, num = struct.unpack(">2I", file.read(8))
        print("magic={}, num={}".format(magic, num))

        # 残り全部を読み込み、1次元配列を作成する。
        labels = np.fromfile(file, dtype=np.uint8)
    return [{"img": img, "label": label} for img, label in zip(imgs, labels)]


if __name__ == '__main__':
    train_data = get_data("train-images-idx3-ubyte", "train-labels-idx1-ubyte")
    test_data = get_data("t10k-images-idx3-ubyte", "t10k-labels-idx1-ubyte")

    # 表示する
    for data in train_data:
        window_name = "label {}".format(data["label"])
        cv2.imshow(window_name, data["img"])
        cv2.waitKey(0)
        cv2.destroyWindow(window_name)

計算グラフの構築

以下のような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)

numpy の Broadcasting

numpy の ndarray の基本演算を紹介する.

要素ごとの加算

numpy.add(x, y) または x + y で要素ごとの加算を行える.

>>> a = np.array([[1, 1, 1],
                  [2, 2, 2]])
>>> b = np.array([[1, 1, 1],
                  [2, 2, 2]])
>>> np.add(a, b)
array([[2, 2, 2],
       [4, 4, 4]])

要素ごとの減算

numpy.subtract(x, y) または x – y で要素ごとの減算を行える.

>>> a = np.array([[3, 3, 3],
                  [2, 2, 2]])
>>> b = np.array([[1, 1, 1],
                  [1, 1, 1]])
>>> np.subtract(a, b)
array([[2, 2, 2],
       [1, 1, 1]])

要素ごとの乗算

numpy.multiply(x, y) または x * y で要素ごとの乗算を行える.

>>> a = np.array([[1, 2, 3],
                  [1, 2, 3]])
>>> b = np.array([[3, 3, 3],
                  [2, 2, 2]])
>>> np.multiply(a, b)
array([[3, 6, 9],
       [2, 4, 6]])

要素ごとの除算

Python 2 と Python 3 で除算の挙動が異なるため,注意する必要がある.
Python 2 でも from __future__ import division とした場合は Python 3 の挙動になる.

演算 Python 2 Python 3
numpy.divide(x, y) または x / y (Python 2) 余りを切り捨てる 余りを切り捨てない
numpy.true_divide(x, y) または x / y (Python 3) 余りを切り捨てる 余りを切り捨てる
numpy.floor_divide(x, y) または x // y (Python 3) 余りを切り捨てる 余りを切り捨てる
>>> a = np.array([[2, 4, 6],
                  [8, 10, 12]])
>>> b = np.array([[2, 2, 2],
                  [2, 2, 2]])
>>> np.divide(a, b)
array([[ 1.,  2.,  3.],
       [ 4.,  5.,  6.]])
>>> np.floor_divide(7, 3)
2
>>> np.floor_divide([1., 2., 3., 4.], 2.5)
array([ 0.,  0.,  1.,  1.])

符号反転

numpy.negative(x) または -x で符号を反転する.

>>> np.negative([1., -1.])
array([-1.,  1.])

要素ごとのべき乗

numpy.negative(x) または -x で符号を反転する.

>>> a = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
>>> b = [1.0, 2.0, 3.0, 3.0, 2.0, 1.0]
>>> np.power(a, b)
array([  1.,   4.,  27.,  64.,  25.,   6.])

numpy の Broadcasting

numpy の Broadcasting という機能を紹介する.
異なる形状の ndarray 同士で演算を行おうとした場合に,演算前に形状を揃える Broadcasting が行われる.
次の2つのルールを適用した結果,演算対象の ndarray の形状が一致させることができれば,Broadcast が可能となる.

ルール1: 次元数 ndim を揃える.

演算対象の中で,一番次元数が大きい ndarray より小さい次元数を持つ ndarray は1で埋めて一番大きい次元数を揃える.

ルール2: 形状 shape を揃える.

演算対象の中で,一番次元ごとの数を見ていき,その数が1の場合のみ最も大きいものに合わせる.
その際,要素はその次元の同じ値で埋める.

ルール1,ルール2 を適用した結果,形状が一致した場合は Broadcasting 可能である.
以下にいくつかできる例を出す.

ルール1,ルール2 を適用しても形状が一致しない場合は Broadcasting はできず,演算を行おうとした場合はエラーになります.

OKなコード例

>>> a = np.array([[1, 2, 1],
                  [1, 2, 1]])
>>> b = np.array([2, 1, 2])
>>> a.shape
(2, 3)
>>> b.shape
(3,)
>>> c = a + b
>>> c
array([[3, 3, 3],
       [3, 3, 3]])
>>> c.shape
(2, 3)
>>> a = np.zeros((1, 3, 1, 5))
>>> b = np.zeros((3, 3, 2, 5))
>>> c = np.zeros((3, 1, 1))
>>> d = a + b + c
>>> d.shape
(3, 3, 2, 5)

NGなコード例

>>> a = np.zeros((2, 3, 5))
>>> b = np.zeros((3))
>>> c = a + b
ValueError: operands could not be broadcast together with shapes (2,3,5) (3,)

Python 3 スタイルの文字列フォーマット

Python 3 スタイルの文字列フォーマットのまとめ.

基本

>>> "{} {}".format("apple", "pen")
'apple pen'

順番指定

>>> "{1} {0}".format("apple", "pen")
'pen apple'

オブジェクトの __str__(), __repr__() 呼び出し

class MyClass(object):

    def __str__(self):
        return 'str'

    def __repr__(self):
        return 'repr'

print('{}'.format(MyClass()))    # str
print('{!s}'.format(MyClass()))  # str
print('{!r}'.format(MyClass()))  # repr

文字列のアラインメント,パディング

文字数を指定すると,不足分はスペースでパディングされる.

デフォルト

>>> "[{:10}]".format("apple")
'[apple     ]'

左寄せを指定

>>> "[{:<10}]".format("apple")
'[apple     ]'

右寄せを指定

>>> "[{:>10}]".format("apple")
'[     apple]'

中央寄せを指定

>>> "[{:^10}]".format("apple")
'[  apple   ]'

パディングする文字

デフォルト

>>> "[{:<10}]".format("apple")
'[apple     ]'

ハイフンを指定

>>> "[{:-<10}]".format("apple")
'[apple-----]'

文字数

文字数を制限する

先頭4文字を表示する.

>>> "[{:.4}]".format("pineapple")
'[apple-----]'

先頭4文字を表示し,10文字中余った部分はパディングする.

>>> "[{:10.4}]".format("pineapple")
'[pine      ]'

数字

文字数を制限する

int .

>>> "{:d}".format(1)
'1'

float 型.

>>> "{:f}".format(3.14)
'3.140000'

パディングする.

>>> "[{:4d}]".format(1)
'[   1]'

パディングする.

>>> "{:08.2f}".format(3.14159265)
'00003.14'

符号.

>>> "[{:+4d}]".format(1)
'[  +1]'