学生による学生のためのデータサイエンス勉強会

【ゼロから作るディープラーニング#5】手書き数字認識【p72-82】

 6月1日に【ゼロから作るディープラーニング】の勉強会がありました。今回私の担当分は、手書き数字認識の部分で、これまでの活性化関数やニューラルネットワークの計算などを用いて、順伝播(=予測)を行うという内容です。
 ニューラルネットワークを用いて適切な予測を行うには、重みとバイアスを最適なものにする学習を行う必要がありました。しかし、第3章までではまだ学習について学んでいないため、今回は学習済みパラメータ(学習後の重みとバイアス)を使用し、順伝播が実際にどのように行われるのかを知るというのが目標です。

MNISTデータセット

 手書き数字認識を使用して、ニューラルネットワークの予測について学ぶということでした。このために、手書き数字認識に使うデータセットの説明をまずしたいと思います。今回使うのはMNISTデータセットというもので、機械学習の分野で最も有名であり、簡単な実装から論文として発表される研究まで、様々な場所で利用されています。

MNISTの基本情報

 MNISTの基本情報は以下の通りです。

MNISTデータセットの読み込みと確認

 次にMNISTデータセットの読み込みと確認を行います。「ゼロつく」のサンプルコードにあるload_mnist関数を使用しますが、これは「ゼロから作るディープラーニング」独自に作成された関数ですので、今後使う機会は少ないかもしれません。しかし、今回使うload_mnist関数にはデータの前処理に関する引数が存在し、それがとても重要になってくるので、しっかり確認していきます。
 まず、本に記載されているMNISTデータセットの読み込みのコードは以下の通りです。

from dataset.mnist import load_mnist
# 最初の呼び出しは数分待ちます・・・
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False, one_hot_label=False)

# 各々のデータの形状を出力
print(x_train.shape) # 訓練用画像(60000,784)
print(t_train.shape) # 訓練用画像の正解ラベル(60000,)
print(x_test.shape) # テスト用画像(10000,784)
print(t_test.shape) # テスト用画像の正解ラベル(10000,)

 まず、dataset/mnist.pyload_mnist関数のimportを行います。その後、mnistデータの読み込みを行い、続いて、読み込んだデータの確認を行っています。読み込んだデータは訓練用に60000件,テスト用に10000件になっています。
 ここで、load_mnist関数の引数を見ると、以下のものがあります。

  • 引数1(flatten)
    • 入力画像を平らにする(1次元配列にする)かどうかを設定します
  • 引数2(normalize)
    • 入力画像を0.0~1.0の値に正規化するかどうかを設定します。
  • 引数3(one_hot_label)
    • 正解ラベルをone-hot表現として格納するかどうかを設定します。

 これらについて以下でもう少し詳しく見ていきます。

Flatten(平坦化)

 load_mnist関数でデータを読み込む際に、flattenの指定に応じてデータの形状を変換します。具体的には、

  • Falseのとき、28×28の2次元配列
  • Trueのとき、784(=28×28)の1次元配列

と変換を行います。
 実は、全結合層にデータを流す際には、データの形状を1次元配列にしなければいけません。これは全結合層での行列計算の都合上、1データごとに、1次元配列の形でなければ、上手く計算出来ないのが理由です。
 平坦化の方法ですが、例えば、28×28の2次元配列があったとすると、1次元配列にするためには、0行目の配列の後ろに、1行目の配列をくっつけ、その1行目の後ろに2行目の配列をくっつける、というのを繰り返して1次元配列に変換していきます。

Normalize(正規化)

 normalizeの引数の指定に応じてデータの正規化を行うか指定します。具体的には、

  • Falseの場合は、入力画像のピクセルは元の0~255のまま
  • Trueの場合は、入力画像を0.0~1.0の値にスケーリングする

と変換が行われます。
 正規化は一般的に、複数の特徴量の間にある単位の違いを統一する目的で行われます。また、正規化には様々な方法があり、データセットに応じて使い分ける必要があります。今回の例では、画像データで各ピクセルが0~255の値をとることが分かっていたので、単純に、255で割るという処理を行いました。その結果、各ピクセル値が0.0~1.0の間をとるようにスケーリング出来たわけです。

 ここで、疑問に思うかもしれませんが、「数値の単位を揃えることが目的なら、範囲(0~255)が決まっている画像に正規化は必要ないんじゃないの?」という点です。確かに私もそう思います。現に、特に正規化しなくても(Falseで試してみてください)ある程度高い精度は出ます。しかし、比較するとやはり正規化した場合の精度の方が高くなっています。これはおそらく、色々実験した結果から正規化した方が良い結果が出やすいという経験的なものなのかもしれません。(間違っていたらすいません)

 あと、活性化関数が取り得る値。sigmoidであれば、-6~6程度でそれ以外は(0か1で一定)、reluであれば、0を境界に出力が分かれますよね。これらのことから、特徴量や重みの受け取る値を0付近のある程度小さい値で想定しているのではないかとも取れます。

 正規化ですが、実際には、行わなくても良い結果が出たりもするので、よく分からない部分もありますが、基本的には行った方が良いというのが結論です。

One_hot_label

 load_mnist関数でデータを読み込む際に、one_hot_labelの指定に応じて正解ラベルの形状を変換します。具体的には、

  • Falseのとき、7, 2といった単純に正解となるラベル
  • Trueのとき、[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]のように、正解となるラベルだけが1でそれ以外は0の配列

 ニューラルネットワークでは、正解ラベルをOneHotLabelとすることが多いです。これは、後の損失関数の計算に効いてきます。分類問題において、出力層の出力は、そのクラスである確率として出力されるということを学びましたが、この出力(予測)と正解ラベルとの誤差を取るためには、正解ラベルがOneHotLabelの形式となっている必要があります。

前処理

 ここまで、MNISTデータセットと同時に、予測を行う前(学習前も同じ)にデータを変換する必要があることを学びました。上記の変換を総称して、前処理と言ったりします。以下は「ゼロつく」からの引用になります。

 前処理はNNにおいて、実践的によく用いられます。前処理の有効性は、識別性能の向上や学習の高速化など、多くの実験によって示されています。先ほどの例では、前処理として各ピクセルの値を255で割るだけの単純な正規化を行いました。実際には、データ全体の分布を考慮した前処理を行うことが多くあります。例えば、データ全体の平均や標準偏差を利用して、データ全体が0を中心に分布するように移動させたり、データの広がりをある範囲に収めたりといった正規化を行います(標準化とも呼ばれる)。それ以外にも、データ分布の形状を均一にするといった方法(白色化: whitening)などがあります。

「ゼロつく」P76あたり

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

 さて、MNISTデータの確認と前処理が終わったところで、次は、学習済みの重みを使用して順伝播を行ってみましょう。順伝播のプログラムは以下の通りです。

from common.functions import sigmoid, softmax
from dataset.mnist import load_mnist
import pickle
import numpy as np

# データの読み込み
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

# ニューラルネットワークの設定
def init_network():
    with open("lib/sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network

# 予測(推論)を行う関数
def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1   # 隠れ層(1つ目)の計算
    z1 = sigmoid(a1)          # 隠れ層(1つ目)の結果を活性化関数に通す
    a2 = np.dot(z1, W2) + b2  # 隠れ層(2つ目)の計算
    z2 = sigmoid(a2)          # 隠れ層(2つ目)の結果を活性化関数に通す
    a3 = np.dot(z2, W3) + b3  # 出力層の計算
    y = softmax(a3)           # 出力層の結果をsoftmaxに通す

    return y

# データの読み込み
x, t = get_data()

# ニューラルネットワークの設定
network = init_network()

# 予測的中した個数を保存する変数
accuracy_cnt = 0

# テストデータの10,000個分1個ずつ予測を行う
for i in range(len(x)):
    y = predict(network, x[i])
    p = np.argmax(y)  # 最も確率の高い要素のインデックスを取得
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x))) # Accuracy:0.932

 プログラムは概ね以下の手順になっています。

  1. 最初にMNISTデータセットを取得し、ネットワークを生成する
  2. xに格納された画像データを1枚ずつfor文で取り出し、predict()関数によって分類を行う
    • 結果は各ラベルの確率がNumpy配列として出力される
    • 例えば、[0,1, 0.3, 0.2, ・・・. 0.04]のような配列が出力される
  3. 上記の配列の中で最も大きな値のインデックス(要素番号)を取り出し、それを予測結果とする 
    • 配列中の最大値のインデックスを取得するには、np.argmax(x)を使う  
    • np.argmax(x)は引数xに与えられた配列で最大の値を持つ要素のインデックスを取得する
  4. 最後にNNが予測した答えと正解ラベルとを比較して正解した割合を認識精度(accuracy)とする

 これによって、テストデータの予測が行え、その結果の精度が0.932となることが確認出来ました。
 プログラムを見ると、今回はデータを1件ずつ予測しているのが分かります。しかし、1件ずつ順伝播を行うというのは実はあまり効率的ではありません。そのため、次に、データをまとめて順伝播する方法を紹介したいと思います。

バッチ処理

 今までは、以下のように、1×784のデータ(画像1枚分)ずつ予測するということを行っていました。

 上記は、784の要素からなる1次元配列(元は28×28の2次元配列)が入力され、1次元の配列(要素数10)が出力されるという流れになっています。これは画像データを1枚だけ入力したときの処理の流れです。 
 画像を複数枚まとめて入力する場合を考えましょう。例えば、100枚の画像をまとめて、1回のpredict()関数で処理したいと思います。そのため、xの形状を100×784として、100枚のデータをまとめて入力データとすることが出来ます。

 入力データの形状は100×784、出力データの形状は100×10になります。これは、100枚分の入力データの結果が一度に出力されることを表しています。例えば、x[0]とy[0]には0番目の画像とその推論の結果、x[1]とy[1]には1番目の画像とその結果、・・・というように格納されています。   なお、ここで説明したような、まとまりのある入力データをバッチ(batch)と呼びます。バッチ処理を行うことで、処理時間を短縮することが出来ます
 以下がバッチ処理のコードです。

# バッチ処理を実装したコード
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]          # [0:100], [100:200], ..., [9900:10000]
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i+batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

 注目する点は、rangeをbatch_sizeごとに飛ばし飛ばしで回していることです。そして、x_batchに100件ずつスライスしたテストデータを格納し、その100件分の予測結果をy_batchに格納します。その後、np.argmaxを行ごとにとり、その結果と正解ラベルを比較し、正解数をカウントしています。これでループは100回で済むことになります。

まとめ

 ここまでの章で活性化関数、ニューラルネットワークの計算、データの前処理、バッチ処理、順伝播(予測)について学びました。次回以降で、ニューラルネットワークの学習に踏み出していくことになります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です