からっぽのしょこ

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

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

はじめに

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

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、3.6節「手書き文字認識」の内容になります。MNISTデータセットと学習済みパラメータを使った画像認識をPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

3.6 手書き数字認識

 本の目標はニューラルネットワークによる画像処理ですが、具体的に言うと手書き文字を認識することです。ここでは分析に用いる画像データを読み込みます。

 画像を読み込むための関数は(本で扱う内容でもないため)用意されています。その画像読み込み関数load_mnist()を読み込みます。詳しくは【MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ】の方で扱います。

 で!す!が!その前に確認です。本のまえがきviiページにあるURLにアクセスしてファイル(マスターデータ)をダウンロードしていますか?ここで利用するload_mnist()関数の他にも、必要なデータがまとめて公開されています。
 本を進めるのに必須のファイル類なので、まだしていなければ今からしましょう!

 (今現在だと)URLのページ【GitHub - oreilly-japan/deep-learning-from-scratch: 『ゼロから作る Deep Learning』(O'Reilly Japan, 2016)】にアクセスすると、右上に緑色で表示されている「Clone or download」から(そのページに表示されているch01やdatasetなどのファイルをまとめて)zipファイル形式でダウンロードできます。分からなければ「GitHub ファイル ダウンロード」などで検索してください。

 またそのダウンロードしたファイルをどこに保存するのかによって、本の通り(具体的にいうとsys.path.append(os.pardir)のままでは)できない場合があります。これも詳しくは【MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ】でご確認ください。

 このノートでは、ファイルの場所に関わらず実行できる方法で書いていきます。

# データ読み込み用のライブラリをインポート
import sys
import os

# カレントディレクトリを取得
os.getcwd()
'C:\\Users\\「ユーザー名」\\Documents\\・・・\\「スクリプトを保存しているフォルダ名」'

 この結果(現在の作業領域)が「deep-learning-from-scratch-master」フォルダであれば、このままload_mnist()をインポートできます。「ch01」から「ch08」であれば、sys.path.append(os.pardir)を実行した上で読み込めます。しかしそれ以外であれば、ファイルパス(ファイルの位置)を指定する必要があります。(おそらく今コードを書いているファイルを保存しているフォルダになっていると思われます。)

 sys.path.append()にファイルパスを指定することで、そこからファイルを読み込むことができます。ファイルパスは「deep-learning-from-scratch-master」フォルダのプロパティを開いて、「場所」をコピペ!ただし\(バックスラッシュあるいは円マーク)を2つずつに変更する必要があります。

# 「deep-learning-from-scratch-master」フォルダのファイルパスを指定
sys.path.append('C:\\Users\\「ユーザー名」\\Documents\\・・・\\deep-learning-from-scratch-master')

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


 読み込んだload_mist()を使ってMNIST(画像)データを取得します。

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


 取得したデータ数を確認しましょう。変数名.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だと確認できました。

 画像自体も確認しましょうということですが、この処理はここだけで行うものなので深追いはしないことにします。分析対象を実際に目で確認することは必須です。本の通り行ってください。

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

 皆様お疲れ様です!ようやくディープラーニングらしいことを試してみて第一部完です。

 ここまでディープラーニングとは何ぞや?という話はあまり登場しませんでした。ここで少しだけ触れておきます(これは機械学習全般の話です)。

 ディープラーニング(機械学習)では大きく分けて2つのステップがあります。1つ目は、データから学習するステップです。ここで言う学習とは、読み込んだデータにフィットするようにパラメータ(重みやバイアス)の値を調整することです。2つ目は、推論するステップです。推論とは、答の分からないデータに対して学習したパラメータを基に可能性の高い答を決めることです。

 本では、0から9までの「手書きされた数字(文字)の画像データ」とその手書き数字が0から9までの「どの数字なのかを示すラベルデータ」をセットで扱います。手書き文字とその正解ラベルを使って、重みとバイアスの調整(学習)を行います。(これは4章以降の内容で本のメインでもあります。)

 学習(いい感じの調整)の済んだパラメータがないと、精度の高い推論ができません。

 本で力を入れるのは学習ステップですが、ここでは(自分たちが何を目的としているのかをイメージするためにも)既に学習が済んだ重みとバイアスを使って、推論ステップをやってみよう!という訳です。

 ではやってみましょう(関数を組んで実行しましょう)!まずは画像とラベルデータを読み込む関数を作成します。

# MNISTデータを読み込む関数を実装
def get_data():
    
    # データの読み込み
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, flatten = True, one_hot_label = False)
    
    return x_test, t_test

 get_data()を使うことで、テスト画像とテストラベルを取得できます。

 次に、学習済みのパラメータを読み込む関数を作成します。学習済みパラメータは「ch03」フォルダ内の「sample_weight.pkl」です。「sample_weight.pkl」のファイルパスを指定して読み込みます。

# 学習済みのパラメータを読み込む関数を実装
def init_network():
    
    # ライブラリを読み込む
    import pickle
    
    # ファイルの位置を指定
    file_path = "C:\\Users\\「ユーザー名」\\Documents\\・・・\\deep-learning-from-scratch-master\\ch03\\sample_weight.pkl"
    
    # 学習済みのパラメータの読み込み
    with open(file_path, 'rb') as f:
        network = pickle.load(f)
        
        return network

 pickleを読み込んでおかないと動作しない(?)ので、インポートの処理も定義に含めておきます。

 ファイルパスの指定で長く(見にくく)なってしまうため、一旦変数file_pathに格納することにします(引数として指定できるようにした方がいいかもしれません)。

 これでいつでもinit_network()を使って学習済みパラメータを取得できます。

 最後に、手書き文字認識(推論)を行う関数を作成します。処理の流れ(関数の中身)は3.4節と(ほぼ)同じです(適宜確認してください)。

# 手書き数字から正解を予測する関数を実装
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

 分類問題では最後のソフトマックス関数は恒等関数に変えても問題ないとのことでした。この章の最後までできたらここに戻ってきて、softmax()identity_function()に変更して試してみましょう!苦労して関数を定義したことによる楽さも実感できると思います。

 ではいよいよニューラルネットワークによる推論(分類)を行います。まずはデータと学習済みパラメータを読み込みます。

# テスト画像とテストラベルを読み込む
x, t = get_data()

# 学習済みパラメータを読み込む
network = init_network()

# データ数を確認
print(len(x)) # 要素数を取得
print(len(t)) # 要素数を取得
print(network.keys()) # ディクショナリ型データのキーを取得
10000
10000
dict_keys(['b2', 'W1', 'b1', 'W2', 'W3', 'b3'])


 必要なデータが取得できていることを確認できたら、データを1つだけ取り出して試してましょう。(Pythonの1つ目とは0ですよ。)

 predict()の、第1引数にパラメータnetwork、第2引数に1つ目のデータx[0]を指定します。

# 推論
y = predict(network, x[0])
print(y)
print(np.round(y, 3)) # (分かりやすいように値を丸めた版)
print(np.sum(y)) # (一応総和が1となることを確認)
[8.4412488e-05 2.6350631e-06 7.1549421e-04 1.2586262e-03 1.1727954e-06
 4.4990808e-05 1.6269318e-08 9.9706501e-01 9.3744793e-06 8.1831159e-04]
[0.    0.    0.001 0.001 0.    0.    0.    0.997 0.    0.001]
1.0

 ニューラルネットワークによる推論の結果99%以上の確率で「7」を書いたものであると出力されました。

 (人の目で処理するのは大変なので、)この中で1番大きな値をnp.argmax()で返します。

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

 1つ目の手書き数字の最終的な推論結果は「7」であるとなりました。では正解を見てみましょう。

 1つ目の正解データはt[0]です。

# 1つ目の正解データ
print(t[0])
7

 お見事!正解データも「7」でした!この処理もコンピュータにもさせておきますか。

 (=は代入記号の役割を担っているので)等式記号は2つ重ねて==でした。

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

 推論結果が正しいと認識されました。

 推論の流れはこんな感じです。しかしこれを(データ数分)何回も繰り返すのは嫌ですよね。forを使ってデータ数len(x)回繰り返し処理することにします。
 また正解精度を測るために、正解した場合のみカウントすることにします。

# 正解数のカウントを初期化
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

 正解数accuracy_cntの初期値を0としておきます。そしてifを使って、予測が正しいp == t[i]ときだけ、正解数accracy_cntに1を加えます。

 このまま計算するなら次のようになりますね。

accuracy_cnt = accracy_cnt + 1

 勿論これでも行えますが、次のように書くこともできます。

accuracy_cnt += 1

 (同様にその変数からマイナスしたいときは-=とすることもできます。)

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

# 正解率を計算
accuracy_rate = accuracy_cnt / len(x) # (正解数) / (データ数)
print(accuracy_rate)
0.9352


# (折角なので人間にとって分かりやすく表示しましょうか)
print("Accurary:" + str(np.round(accuracy_rate * 100, 2)) + "%")
Accurary:93.52%

 推論精度は93.5%でした!イメージよりも高いでしょうか低いでしょうか?

 推論の流れは以上です。ですが確か3.4節で、一度に複数のデータをニューラルネットワークで処理できるように実装しましたよね?なのに今回は1データずつ処理しました。これは処理効率の面からみてもったいないです(あと折角作った機能を利用しないのも)。とはいえデータが多すぎてもメモリ効率の問題で処理できないこともあります。そこで次は、一部のデータを取り出して処理する方法を実装していきます。

3.6.3 バッチ処理

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

 バッチ処理(複数のデータを一度に処理)をするということは、行列の計算を行うことになります。

 行列の計算において、各変数の形状(次元数と要素数)が対応していないと計算できないので、変数を確認しておくのは重要でした。(また、複雑な処理をするときにエラーが起こると何が原因かを特定するのは面倒です。事前に処理を細かく分解して正しく動作するかを確認することは、トータルでみれば楽だったりします。)

 まずは、画像データと各重みを読み込んでそれぞれ形状を確認していきます。

# テスト画像を取得
x, _ = get_data()

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

# 重みを抽出
W1, W2, W3 = network['W1'], network['W2'], network['W3']

 (get_data()はテスト画像とテストラベルの2つを出力するので、受け皿となる変数も2つ用意する必要があります。1つ前のようにxtとすればいいのですが、ここで必要なのは画像データのみです。使わないデータを受け取る場合には、変数名を_(アンダースコア)とするのが慣例です(使わないことを明示できる)。)

# 画像データの各次元の要素数
x.shape
(10000, 784)

 .shapeメソッドの返り値は、1つ目の値が行数(1次元方向の要素数)、2つ目の値が列数(2次元方向の要素数)です。

 この例だとxは次のような形状であることが分かりました。

$$ \boldsymbol{\mathrm{x}} = \begin{pmatrix} x_{1,1} & \cdots & x_{1,784} \\ \vdots & \ddots & \vdots \\ x_{10000,1} & \cdots & x_{10000,784} \end{pmatrix} $$

 これは1万枚分の画像データがあり、各画像は784の画素の値であるという意味です。(縦が28、横も28の合計28×28=784個の点(画素)でできている画像データを横一列に並べなおした状態です。)

# 重みの各次元の要素数
print(W1.shape)
print(W2.shape)
print(W3.shape)
(784, 50)
(50, 100)
(100, 10)

 同様に重みW1, W2, W3は次の形状であることが分かりました。

$$ \boldsymbol{\mathrm{W}}^{(1)} = \begin{pmatrix} w_{1,1} & \cdots & w_{1,50} \\ \vdots & \ddots & \vdots \\ w_{784,1} & \cdots & w_{784,50} \end{pmatrix} ,\ \boldsymbol{\mathrm{W}}^{(2)} = \begin{pmatrix} w_{1,1} & \cdots & w_{1,100} \\ \vdots & \ddots & \vdots \\ w_{50,1} & \cdots & w_{50,100} \end{pmatrix} ,\ \boldsymbol{\mathrm{W}}^{(3)} = \begin{pmatrix} w_{1,1} & \cdots & w_{1,10} \\ \vdots & \ddots & \vdots \\ w_{100,1} & \cdots & w_{100,10} \end{pmatrix} $$

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

 3.6.2項の実装例に、データの一部を取り出す操作が加わります。

 変数名[(取り出す位置を示す)数値]で変数内のデータを取り出せました。今まではx[0]のように1つだけ取り出していましたが、複数の値を指定すると複数のデータを取り出すこともできます(ノートでは紹介しませんでしたが詳しくは1.3.4項「リスト」を確認してください)。
 また、数値列は:を使ってm:nとすることで、mからnまで1ずつ増えていくリストとなります。

# データを生成
x = np.arange(100)

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

# バッチデータの先頭のインデックス(番号)
i = 0

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

 これで(0から99までの値を持つ)データxの要素を、i番目の要素からbatch_size個取り出すことができます。そして次のii + batch_size + 1となればよい訳です。
 これはrange(0, len(x), batch_size)とすることで、0からデータ数(xの要素数)までをbatch_size間隔の数値リストを作り、その値を順番にiに代入することで行えます。

 ではこの処理を組み込んで推論を行います。

# テスト画像とテストラベルを読み込む
x, t = get_data()

# 学習済みパラメータを読み込む
network = init_network()

# 1回あたりのデータ数を指定
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("Accurary:" + str(np.round(accuracy_rate * 100, 2)) + "%")
Accurary:93.52%

 あくまで予測の効率の問題なので、予測精度に違いはありません。(処理時間に違いがあったはずです。)

 以上で3章のニューラルネットワークの基礎は終了です。また本全体でみたときの基礎編も修了です!次からは、ニューラルネットワークの処理を深めていきます。

参考文献

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

おわりに

 3章終了!!!

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

【次節の内容】

www.anarchive-beta.com