からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

3.6:手書き数字認識【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事は、3.6節「手書き数字認識」の内容です。MNISTデータセットと学習済みパラメータを使って手書き数字を分類します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

3.6 手書き数字認識

 3章では、ニューラルネットワークの推論処理で用いるパーツ(関数)を実装してきました。この節では、実際に手書き数字データを分類します。

 ところで、「まえがき(viiページ)」にURLが載っているサポートページ(GitHub)からソースコードのファイルをダウンロードしていますか?ここから先を進めるには必須なので、「GitHub - oreilly-japan/deep-learning-from-scratch: 『ゼロから作る Deep Learning』(O'Reilly Japan, 2016)」にアクセスして右上の緑の「Code」を開いて「Download ZIP」から「deep-learning-from-scratch-master」をダウンロードしましょう。

 利用するライブラリを読み込みます。

# 3.6節で利用するライブラリ
import numpy as np

 またこの節では、シグモイド関数とソフトマックス関数の計算を行います。そのため、「3.2.4:シグモイド関数の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」で実装したsigmoid()と「3.5:ソフトマックス関数の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」で実装したsoftmax()の関数定義を実行しておく必要があります。

3.6.1 MNISTデータセットの読み込み

 MNIST(手書き数字)データセットを読み込みます。MNISTデータセットの取得は、サポートページ(GitHub)からダウンロードした「deep-learning-from-scratch-master」に専用の関数が実装されています。ファイルのダウンロード方法や読み込み方法、load_mnist()の使い方については「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」を参照してください。

 本では、「deep-learning-from-scratch-master」内の各章のフォルダにスクリプトやノートブックを保存していることを想定しています。3章の場合であれば「ch03」フォルダ内に今コードを書いている.pyファイルや.ipynbファイルを保存しておきます。その場合は、本の方法で(os.pardirを使って)読み込めます。

 それ以外の場合は、次のように「deep-learning-from-scratch-master」のファイルパスをsys.path.append()に指定します。

# ファイルパス指定用のライブラリを読み込む
import sys

# 「deep-learning-from-scratch-master」フォルダのファイルパスを指定
sys.path.append('../deep-learning-from-scratch-master')

# 「dataset」フォルダ内の「mnist.py」にアクセスして`load_mnist()`を読み込む
from dataset.mnist import load_mnist

 詳しくは【3.6.1項のPythonノート】を参照してください。

 確認のため、読み込んだload_mist()を使ってMNIST(手書き数字)データを読み込んでみましょう。flatten=Truenormalize=Falseとします。

# MNISTデータセットを取得
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

 訓練用の画像データをx_train、訓練用のラベルデータをt_train、テスト用の画像データをx_test、テスト用のラベルデータをt_testとします。

 取得したデータの数を確認しましょう。変数名.shapeで、配列の形状(各軸の要素数)が返ってきます。

# データの確認
print(x_train.shape) # 訓練画像
print(t_train.shape) # 訓練ラベル
print(x_test.shape) # テスト画像
print(t_test.shape) # テストラベル
(60000, 784)
(60000,)
(10000, 784)
(10000,)

 訓練データが60,000枚、テストデータが10,000枚です。1枚の画像は、784個のピクセルの値です。

 引数や画像データについても【3.6.1項のPythonノート】を参照してください。

3.6.2 ニューラルネットワークの推論処理

 ディープラーニングでは、大きく分けて2つのステップがあります。1つは、データにフィットするようにパラメータ(重みやバイアス)を調整する学習ステップです。これは4・5章で扱います。もう1つは、学種したパラメータを使って答の分からないデータに対して予測する推論ステップです。この項では、実際に推論を行ってみます。

 手書き数字は0から9の10種類の文字です。各数字に対応した10個のクラスを設定し、入力した各画像をクラスに分類します。上手く対応するクラスに割り当てられたら、入力した画像を認識できたというわけです。

・関数の作成

 推論処理に利用する3つの関数を作成しておきます。それぞれ「データの取得」「学習済みパラメータの取得」「3層のニューラルネットワーク」の処理をまとめた関数です。

 テスト用の画像データとラベルデータを出力する関数を作成します。

# テスト用のデータを出力する関数を作成
def get_data():
    # MNISTデータセットを読み込み
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    
    # テスト用のデータを出力
    return x_test, t_test

 load_mnist()で訓練用とテスト用のデータを読み込み、テスト用のデータのみ出力します。データに設定は、normalize=True(0から1の値に変換)、flatten=True(画像を1次元配列に変換)、one_hot_label=False(正解ラベルをスカラで出力)とします。

 学習済みのパラメータを出力する関数を作成します。

# pickleファイル用のライブラリ
import pickle

# 学習済みのパラメータを読み込む関数を実装
def init_network():
    # ファイルパスを指定
    file_path = '../deep-learning-from-scratch-master/ch03/sample_weight.pkl'
    
    # 学習済みのパラメータの読み込み
    with open(file_path, 'rb') as f:
        network = pickle.load(f)
        
        # 学習済みパラメータを格納したディクショナリを出力
        return network

 pickleファイルとして保存されている学習済みパラメータを読み込んで出力します。そのため、pickleライブラリを読み込んでおきます。
 学習済みパラメータは、「ch03」フォルダ内の「sample_weight.pkl」です。「sample_weight.pkl」のファイルパスをopen()に指定して読み込みます。ここでも、現在の作業フォルダが「ch03」であれば、本の通りで行えます。異なる場合は、それに応じたパスを指定する必要があります。
 ファイルパスについては【3.6.1項のPythonノート】を参照してください。

 手書き数字認識(推論)を行うための3層のニューラルネットワークを作成します。

# 手書き数字から正解を予測する関数を実装
def predict(network, x):
    # ディクショナリから学習済みパラメータを取得
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    # 第1層の計算
    a1 = np.dot(x, W1) + b1 # 重み付き和
    z1 = sigmoid(a1) # 活性化
    
    # 第2層の計算
    a2 = np.dot(z1, W2) + b2 # 重み付き和
    z2 = sigmoid(a2) # 活性化
    
    # 第3層の計算
    a3 = np.dot(z2, W3) + b3 # 重み付き和
    y = softmax(a3) # 活性化
    
    # 推論結果(ニューラルネットワークの出力)を出力
    return y

 3.4節で実装した3層のニューラルネットワークforward()とほとんど同じです。出力層(最終層)の活性化関数にソフトマック関数softmax()を使う点が異なります。3.2.4項で実装したsigmoid()と3.5.2項で実装したsoftmax()の関数定義を再度実行しておく必要があります。

 以上で、ニューラルネットワークの推論処理を行う準備ができました。ではいよいよ手書き文字認識を行います。

・処理の確認

 必要な関数を実装できたので、データを1つだけ取り出して試してましょう。

 get_data()でテスト用のデータを取得します。

# テスト画像とテストラベルを取得
x, t = get_data()
print(x.shape)
print(t.shape)
(10000, 784)
(10000,)

 画像データをx、ラベルデータをtとします。

 画像のデータ番号nを1つ指定して、n番目の画像データx[n]を確認します。

# データ番号を指定
n = 0

# n番目のデータの一部を確認
print(x[n, 250:300])
[0.         0.         0.         0.         0.         0.
 0.         0.         0.2627451  0.44705883 0.28235295 0.44705883
 0.6392157  0.8901961  0.99607843 0.88235295 0.99607843 0.99607843
 0.99607843 0.98039216 0.8980392  0.99607843 0.99607843 0.54901963
 0.         0.         0.         0.         0.         0.
 0.         0.         0.         0.         0.         0.
 0.         0.         0.         0.         0.         0.06666667
 0.25882354 0.05490196 0.2627451  0.2627451  0.2627451  0.23137255
 0.08235294 0.9254902 ]

 normalize=Trueを指定すると、各ピクセルは0から1の値をとります。1に近いほどしっかりと書かれたピクセルを意味します。

 数式で表すと次の形状です。ただし、Pythonのインデックスに合わせて添字を0から割り当てます。

$$ \mathbf{x} = \begin{pmatrix} x_{0} & \cdots & x_{783} \end{pmatrix} $$

 要素数(列数)は、縦横のピクセル数$28 \times 28 = 274$です。$\mathbf{x}$をニューラルネットワークに入力します。

 init_network()で学習済みパラメータを取得します。items()メソッドで、ディクショナリのキーと値を取り出せます。

# 学習済みパラメータを取得
network = init_network()

# キーと値(パラメータ)の形状を確認
for key, value in network.items():
    print(key)
    print(value.shape)
b2
(100,)
W1
(784, 50)
b1
(50,)
W2
(50, 100)
W3
(100, 10)
b3
(10,)

 第1層の入力(ニューラルネットワークの入力)xの形状は(784,)です。
 そして、第1層の重みW1(784,50)、第1層のバイアスb1(50,)なので、第1層の重み付き和a1(50,)になります。a1を活性化したz1を第2層に入力します。活性化関数では形状は変わりません。
 同様に、第2層の重みW2(50,100)、第2層のバイアスb2(100,)なので、第2層の重み付き和a1と出力z1(100,)になります。
 最後に、第3層の重みW3(100,10)、第3層のバイアスb3(10,)なので、第3層の重み付き和a3と出力(ニューラルネットワークの出力)y(10,)になります。

 (順番がバラバラですが、)各層において「入力の要素数と重みの行数」、「重みの列数とバイアスの要素数」が一致しているのを確認できます(3.3.2項)。

 こちらも数式で表現すると次の形状です。

$$ \begin{aligned} &\mathbf{W}^{(1)} = \begin{pmatrix} w_{0,0} & \cdots & w_{0,49} \\ \vdots & \ddots & \vdots \\ w_{783,0} & \cdots & w_{783,49} \end{pmatrix} &\mathbf{b}^{(1)} = \begin{pmatrix} b_0 & \cdots & b_{49} \end{pmatrix} \\ &\mathbf{W}^{(2)} = \begin{pmatrix} w_{0,0} & \cdots & w_{0,99} \\ \vdots & \ddots & \vdots \\ w_{49,0} & \cdots & w_{49,99} \end{pmatrix} &\mathbf{b}^{(2)} = \begin{pmatrix} b_0 & \cdots & b_{99} \end{pmatrix} \\ &\mathbf{W}^{(3)} = \begin{pmatrix} w_{0,0} & \cdots & w_{0,9} \\ \vdots & \ddots & \vdots \\ w_{99,0} & \cdots & w_{99,9} \end{pmatrix} &\mathbf{b}^{(3)} = \begin{pmatrix} b_0 & \cdots & b_{9} \end{pmatrix} \end{aligned} $$

 第1層(入力層)の重みの行数は、ピクセルの数に対応しています。また、第3層(出力層)の重みの列数とバイアスの要素数は、0から9の数字に対応しています。中間層(隠れ層)のニューロン数は自由に設定できます。

 predict()の第1引数にパラメータnetwork、第2引数に1つのデータx[n]を渡して、推論結果を出力します。

# 推論
y = predict(network, x[n])
print(np.round(y, 3))
print(np.sum(y))
[0.    0.    0.001 0.001 0.    0.    0.    0.997 0.    0.001]
1.0

 ニューラルネットワークの出力yは、入力xがどのクラスなのかを示す確率分布なのでした。この結果は、0番目の画像に書かれた数字はは99.7%の確率で「7」であると予測しています。

 np.argmax()を使って、予測された数字(値が最大のインデックス)を抽出します。

# 最大値を抽出
p = np.argmax(y)
print(p)
7

 n番目の手書き数字は「7」であると予測(分類・認識)しました。(ちなみに、このpは何からきたpなんだろう?確率ではないです。)

 n番目の正解ラベルt[n]を確認します。

# 正解ラベルを確認
print(t[n])
7

 正しく認識できていたことが分かりました。

 プログラム上でも正しい結果なのかを確認します。予測pと正解レベルt[n]==で比較します。

# 予測結果と正解ラベルを比較
print(p == t[n])
True

 推論結果が正しいと認識できました。

 以上が推論(手書き文字認識)で行う処理です。

・実装

 処理の確認ができたので、推論処理を実装します。

 先ほど確認した処理を、for文を使ってデータ数len(x)回繰り返し行います。
 また、予測が正しい(p == t[i]True)の場合にaccuracy_cnt1を加えることで、正解数をカウントします。始めに、値が0の変数accuracy_cntを作成しておく必要があります。

# テスト画像とテストラベルを取得
x, t = get_data()

# 学習済みパラメータを取得
network = init_network()

# 正解数のカウントを初期化
accuracy_cnt = 0

# データごとに処理
for i in range(len(x)):
    # 推論(予測)
    y = predict(network, x[i])
    
    # 確率が最大のインデックスを抽出
    p = np.argmax(y)
    
    # 正解のときカウント
    if p == t[i]:
        accuracy_cnt += 1

# 正解数を表示
print(accuracy_cnt)
9352

 正解数をカウントする処理に関して、次のようにaccuracy_cnt + 1の結果をaccuracy_cntに再代入することもできます。

# カウント
accuracy_cnt = accracy_cnt + 1

 ここでは、次のように代入演算子+=を使って同じ計算をしています。

# カウント
accuracy_cnt += 1

 全く同じ処理なわけではなく内部的には異なる挙動をしているのですが、ここでは説明を省略します。

 正解数accuracy_cntをデータ数len(x)で割って、正解率(認識精度)を確認しましょう。

# 正解率(認識精度)を計算
accuracy_rate = accuracy_cnt / len(x)
print(accuracy_rate)
0.9352

 93.5%の数字(データ)を認識できました。

 人の目に分かりやすいように文字列を加工して出力します。

# 正解率(認識精度)を表示
print('Accurary:' + str(np.round(accuracy_rate * 100, 2)) + '%')
Accurary:93.52%

 str()で文字列型に変換できます。また、+で文字列同士を結合できます。

 さて、結果はイメージよりも高いでしょうか低いでしょうか?

 以上で、ニューラルネットワークを用いて手書き数字認識を行えました。ただし、1データずつ処理するのは効率的ではありません。次項では、一度に複数のデータを処理します。

3.6.3 バッチ処理

 全てのデータから一部のデータを取り出してまとめたものをバッチデータと呼びます。そして、データ全体を一定数ずつに分割して、その一部ずつ(バッチごとに)順番に処理する方法をバッチ処理と言います。

 この項では、前項の推論処理をバッチ処理で行います。

・処理の確認

 バッチデータを扱う処理を確認します。

 バッチサイズを100とすると、入力$\mathbf{x}$は次の行列になります。(行列なので太字の大文字$\mathbf{X}$で表記してもいいのですが、プログラム上のxに合わせて先ほどと同じ小文字の太字で表記します。)

$$ \mathbf{x} = \begin{pmatrix} x_{0,0} & \cdots & x_{0,783} \\ \vdots & \ddots & \vdots \\ x_{99,0} & \cdots & x_{99,783} \end{pmatrix} $$

 列数は変わらず行数がバッチサイズになりました。$\mathbf{x}$に対応する重みは次の行列でした。

$$ \mathbf{W}^{(1)} = \begin{pmatrix} w_{0,0} & \cdots & w_{0,49} \\ \vdots & \ddots & \vdots \\ w_{783,0} & \cdots & w_{783,49} \end{pmatrix} $$

 $\mathbf{x}$の列数が変わらないため、先ほどの$\mathbf{W}^{(1)}$のまま計算できます。以降の計算も同じなので省略します。
 ただし、出力$\mathbf{y}$もバッチサイズ行10列の行列になります。

 各データの形状に問題がないことを(図3-27と合わせて)確認できたので、バッチデータを取り出す処理を確認します。

 分かりやすいように、0から99の100個の値を持つxを作成します。

# 仮の入力を作成
x = np.arange(100)
print(x)
print(len(x))
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
 96 97 98 99]
100

 xから要素を10個ずつ取り出すことを考えます。

 バッチサイズをbatch_sizeとして値を指定します。

 変数名[(インデックスを示す)整数]で変数から指定した要素を取り出せるのでした。これまではx[0]のように1つだけ取り出していましたが、複数の値を指定すると複数の要素を取り出せます。また、m:nを指定すると、m番目からn番目未満の要素を取り出せます。

 よって、x[i:i+batch_size]とすると、i番目からbatch_size個の要素をxから取り出せます。

# 1回あたりのデータ数を指定
batch_size = 10

# バッチデータの先頭のインデックスを指定
i = 0

# データの一部を取り出す
print(x[i:i+batch_size])
[0 1 2 3 4 5 6 7 8 9]

 iの値を試行ごとに更新する必要があります。次の試行では、ii + batch_sizeとなればよいわけです。

 これはrange(0, len(x), batch_size)とすることで行えます。「第1引数」から「第2引数未満」までの値を「第3引数」の間隔で作成します。つまり、0からデータ数len(x)までの値でbatch_size間隔の数列を作成します。range()の代わりにnp.arange()を使って試します。

# 各試行の最初のインデックスを作成
print(np.arange(0, len(x), batch_size))
[ 0 10 20 30 40 50 60 70 80 90]

 この値をfor文で順番にiの値とします。

 この2つの処理を組み合わせることで、順番にバッチデータを取り出せます。

# バッチデータを抽出
for i in range(0, len(x), batch_size):
    batch_x = x[i:i+batch_size]
    print(batch_x)
[0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28 29]
[30 31 32 33 34 35 36 37 38 39]
[40 41 42 43 44 45 46 47 48 49]
[50 51 52 53 54 55 56 57 58 59]
[60 61 62 63 64 65 66 67 68 69]
[70 71 72 73 74 75 76 77 78 79]
[80 81 82 83 84 85 86 87 88 89]
[90 91 92 93 94 95 96 97 98 99]

 10個ずつ順番に全ての要素を取り出せました。

 以上がバッチデータを抽出する処理です。

・実装

 処理の確認ができたので、バッチデータに対する推論処理を実装します。

# テスト画像とテストラベルを取得
x, t = get_data()

# 学習済みパラメータを取得
network = init_network()

# バッチサイズを指定
batch_size = 100

# 正解数を初期化
accuracy_cnt = 0

# バッチ学習
for i in range(0, len(x), batch_size):
    
    # バッチデータを取得
    x_batch = x[i:i+batch_size]
    
    # 推論(予測)
    y_batch = predict(network, x_batch)
    
    # 確率が最大のインデックスを抽出
    p = np.argmax(y_batch, axis = 1)
    
    # 正解数をカウント
    accuracy_cnt += np.sum(p == t[i:i+batch_size])
    
    # 途中経過を表示
    print('data num:' + str(i) + ', acc:' + str(np.sum(p == t[i:i+batch_size]) / len(x_batch)))

# 推論結果(正解率)を表示
print("Accurary:" + str(np.round(accuracy_cnt / len(x) * 100, 2)) + "%")
data num:0, acc:0.96
data num:100, acc:0.98
data num:200, acc:0.93
(省略)
data num:9800, acc:0.89
data num:9900, acc:0.91
Accurary:93.52%

 途中経過として、各バッチデータに対する認識精度を表示しています。あくまで処理効率の問題なので、先ほどの結果と認識精度は変わりません。

 (正確には、3.5.2項で実装したsoftmax()では全てのデータに対して正規化してしまうので、データごとに正規化するように実装した「deep-learning-from-scratch-master」の「common」の(この資料ではバッチ版ソフトマックス関数として実装した)softmax()を使う必要があります。ただし、推論処理ではソフトマックス関数は結果に影響しないのでした。全体を正規化しても各要素の大小関係は変わりません。よってこの結果には、softmax()は影響していません。)

 以上で、3章の内容は終了です。この章では、学習済みパラメータを使ってニューラルネットワークの推論処理を実装しました。次章では、パラメータの学習を行います。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 3章終了!!!

 5月末に書き終わった3章までの内容を修正しながらのはてなへの転載作業が6月4日に完了しました(この記事は6月10日に自動投稿されているはずです)。これから続きを書いていきます。このストックが尽きるまでに次の分を用意できるのでしょうか!?

  • 2021.07.29:加筆修正しました。


【次節の内容】

www.anarchive-beta.com