社畜エンジニア発掘戦線

駆けだしAIエンジニア

CNN:順伝搬計算

おつかれさまです。

今回からCNN(Convolution Neural Network:畳み込みニューラルネットワーク)にトライしていこうと思います。なんせこのCNN、アルゴリズムがとてもややこしいし、時間もかかるので、何回かに分けてまとめていく予定です。データセットは今までどおりMNISTの手書き数字の認識問題を扱います。最終的にこのCNNを使えば99%を超える結果となるようです(ほんまかいな)。

CNNの大きな特徴は「画像のまま、ニューラルネットワークに突っ込む」です。これまでのニューラルネットワークではMNISTの画像(28pix, 28pix)を(1, 784)の型でニューラルネットにインプットしていました。しかし、CNNでは(28, 28)のままニューラルネットの計算を進めます。

とにかく、始めていきましょう。

CONTENTS

ニューラルネットワークの設計

データセットは今まで通りMNISTなので、ニューラルネットワークの設計から考えていきます。その骨格自体はできるだけシンプルぅなものにして、ひとつずつ構築していきたいと思います。

とりあえず、手始めにトライしてみるのはこんな感じ。

f:id:sutokun:20190524104415j:plain:w600

MNISTのデータは入力層「X」からインプットされ、隠れ層にあたるCNN層「C」を通過して、出力層「Y」からアウトプットされます。CNN層を通過するときに処理される演算は、まぁ特殊なんですが、大凡これまでと同じ「f(X*W+B)」の形です。ただし、CNN層で用いるパラメータ:重みWはF(フィルター)に置き換わります。そして、使用する活性化関数はsigmoidではなく「ReLU」になります。ReLUはランプ関数と読むらしい、またコードを書くときに具体的な処理をみていきます。

出力層はこれまで通り、softmax関数でコスト関数にはクロスエントロピーを使います。とりあえず、今回トライするニューラルネットワークは隠れ層をそのままごっそりCNN層に置き換える、という認識です。

とりあえず、コードを書く前にどのようなアルゴリズムで処理が進むのかざっくり見ていくことにします。

CNN層での処理(畳み込み演算)

CNN層で行う計算にはF(フィルター)と呼ばれるパラメータが必要です、巷ではカーネルとも呼ばれているそうな。これは今までの重みパラメータに相当するもので、 学習による更新の対象となります。フィルターは一般的に比較的小さい正方行列「(3, 3)とか(5, 5)」で、その要素はランダムに設定されます。どのフィルターサイズがええねんってのは場面によって変わってきます。

f:id:sutokun:20190524121127j:plain:w200

フィルターをいくつか用意してゴニョゴニョってのはもうちょっと後で、とりあえず一番シンプルなところから進めます。


(28, 28)の型に変換されて、入力された画像にこのフィルターがかけられます。その演算方法は、左端から画像にフィルターを重ねて、位置のそろう要素どうしをかけ合わせて、その総和をとります。つまり、この計算で得られる値はひとつです。

f:id:sutokun:20190524115335j:plain:w400

数式で書くと頭痛がするので、イメージで攻めます。

すべてのフィルター要素を計算し終えれば、次はそのフィルターをひとつ横へずらします。そしてまた同じ計算を行います。

f:id:sutokun:20190524120930j:plain:w400
この流れでインプット画像にすき間なくフィルターを重ねていき、それぞれ計算された値を並べて再度画像にします。こうしてできあがった畳み込み画像を特徴マップと言うそうです。

ただ、今回はCNN層の次がそのまま出力層なので、わざわざ画像にせず、配列のままで処理を進めていく予定です。

f:id:sutokun:20190525110554j:plain:w400

X_batchはバッチ数の画像が詰まっているので、ひとつずつ抜き出してこの処理を行います。

f:id:sutokun:20190524122213j:plain:w400
バッチ数だけこの処理を繰り返すとなると、演算量がえげつないことになるのが想像できます…。畳み込み処理の大まかな流れはこんな感じです。

CNN層での処理(バイアス付加とReLU関数)

バイアスの付加はシンプルです。要素をランダムに設定したバイアスを計算された特徴マップに足すだけです。

ここまでで計算できた「X*F+B」をReLU関数に通します。ReLU関数はsigmoid関数の代用として最近よく使われるようになってきた活性化関数のようです。関数の式は0を境界にして2種類に場合分けされます。入力が0よりも大きいときは入力値をそのまま、入力が0より小さければ「0」を返します。

f:id:sutokun:20190525110430j:plain:w600

処理の流れ自体は簡単です、sigmoidみたいに指数関数かけて、割って、1足して、とかないのでラク。例えばこんな感じ。

f:id:sutokun:20190525112317j:plain:w400

じゃあ、なんでsigmoid関数だとダメなのかっていうと、今回は2層(CNN層と出力層)でシンプルにニューラルネットワークを設計していますが、本気を出すと5層、10層、それ以上…というように層を深くしていくことが求められるそうです。層が深くなったときにsigmoid関数だと微分値がどんどん小さくなってしまい、逆伝搬計算がうまくいかなくなります。この点を改善されたのがReLU関数ということです。

どれぐらいでsigmoidが使えなくなるのかは実際に比較してみないとよく分からないので、またこの辺のスキルが身に付いてきたらコードを書いてトライしてみようと思います。

とにかく、このような計算の流れでCNN層の計算は進んでいきます。

コーディング(ライブラリ、データ・セット)

では、ここまで準備してきたニューラルネットワークを実際にコードで書いていきたいと思います。

CNN層の処理以外は、以前に作成した関数の外部ファイルを使うことにします。

github.com

このファイルで使用するライブラリのインポートからです。

import function1
import numpy as np
import time

それからMNISTデータのロードと前処理。

#minist_load
save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl'
X_train,T_train,X_test,T_test = function1.mnist(save_file)
コーディング(初期設定)

次は初期値の設定です。ちょくちょく使うので、もとの画像サイズ(28)を定義しておきます。

#initial settings
IMG_size =28

ではCNN層で使用するフィルタから設定しましょう。今回、要素数は(5, 5)の25のフィルタとします。

F_size = 5
F = np.random.randn(F_size,F_size)

次に、特徴マップのサイズを計算しておきます。入力画像とフィルターのサイズが分かれば下の式で計算できます。

f:id:sutokun:20190525114609j:plain:w400

FM_size = int(np.sqrt(X_train.shape[1])-F_size+1)

入力画像のサイズは元画像の要素数平方根をとって計算しています。この値は「整数」でないと、この後の処理で具合が悪いので、intで整数化しています。

次にバイアスの設定です。さっき計算した特徴マップのサイズを使って設定します。

Bc = np.random.randn(1,FM_size**2)

ここまででCNN層の初期値は設定できました。残りの必要なパラメータをセットしていきます。

出力層のパラメータ、CNN層からデータを受けるので特徴マップのサイズを引き継いでいます。

Wo = np.random.randn(FM_size**2, 10)
Bo = np.random.randn(1,10)

学習率とか、その他モロモロ。バッチサイズは100としておきます。

learning_rate = 0.001

E_save = []
accuracy_save = []
start_time = time.time()

batch_size = 100
コーディング(イテレーション

ではイテレーションの中身を書いていきましょう。とりあえず簡単にデバックできるようにイテレーション回数は1にしておきます。

#iteration
num_of_itr=1
for i in range(num_of_itr):

まずはバッチの設定、

    X_batch,T_batch = function1.batch(X_train,T_train,batch_size)
コーディング(CNN層)

さて、ここからが本題のCNN層の処理です。

CNNでの処理はバッチから画像を抜き出して、計算して、ハコに保存する、を繰り返します。

f:id:sutokun:20190525124408j:plain:w400
最初に計算される特徴マップを保存していく「ハコ」を準備しておきます。

    FM_batch = np.empty((0,FM_size**2))

では、具体的に処理を書いていきます。まずはバッチ数の中から順番に画像を抜き出す処理です。

    for M in range (batch_size):
        img = np.reshape(X_batch[M],(IMG_size,IMG_size))
        FM_storage = []

バッチ:X_batchから抜き出された画像は(1, 784)なので、imgとして(28, 28)に変換します。そして、計算された特徴マップの各要素を保存する配列「FM_storage」もここで定義しておきます。

        for i in range(FM_size):
            for j in range(FM_size):
                pick_img = img[i:i+F_size, j:j+F_size]
                FM_storage = np.append(FM_storage,np.tensordot(F,pick_img))

次はバッチから抜き出した画像(img)とフィルター(F)を畳み込んでいく処理です。フィルターは横方向と縦方向に移動させていくので、forは2回必要です。横も縦も、特徴マップのサイズ(FM_size)だけ計算が行われます。

もとの画像(img)からフィルターサイズだけ切り取った画像を「pick_img」としています。これとフィルター(F)を要素に対応させてかけ算し、総和を取ります。この処理はnumpyのtensordotという関数で処理してくれます、便利ィ!

計算された特徴マップの要素はFM_strageにnp.appendで保存されていきます。


この処理が完了すると、次はバイアスが足されます。

        FM_bias = FM_storage+Bc #Bias

エス、そのまま。

最後にReLU関数を通ります。ここでもnumpyにはwhereという便利な関数が備わっており、条件にあう要素をひっぱり出してきて、目的の値に置換してくれます。

        FM_relu = np.where(FM_bias<0,0,FM_bias) #ReLU

ここでは、「FM_biasの0より小さい要素」を探し出して「0」に置換しろ、それ以外は「FM_bias(そのまま)」の値を返せ、という流れです。

これで最終的な特徴マップが出来上がりました。しかし、これはまだバッチから抜き出したひとつの画像の処理です。これを「FM_batch」に追加して次の画像へ移ります。

        FM_batch = np.vstack((FM_batch,FM_relu))

バッチ内のすべての画像が処理できればCNN層は通過です。


いちおう、うまく計算できているか確認してみます。X_batchの要素数は膨大な数なので、もう要素の値を追いかけることはできませんが、行列の型だけでも確認してみます。

    print(X_batch.shape) #(100, 784)
    print(FM_batch.shape) #(100, 576)

もとの画像(X_batch)は784画素の画像が100枚、畳み込まれたFM_batchは576(24, 24)画素となり、枚数(バッチ数)は100のまま、うまくいってそうです。

各処理における画像も確認しておきます。以前、外部ファイルに保存したshow関数を処理の間にうまく挟み込んで画像を出力させます。

元の画像
f:id:sutokun:20190525140227p:plain:w300

畳み込み
f:id:sutokun:20190525140333p:plain:w300

バイアス付加
f:id:sutokun:20190525140415p:plain:w300

ReLU関数
f:id:sutokun:20190525140451p:plain:w300

バイアスを付加したあたりからなんの画像か分からなくなってきていますね、とにかく処理はうまくいってそうです。

コーディング(出力層、誤差計算、精度計算)

この後の流れはこれまでと同じです。

出力層の計算、

    Y = function1.affine(FM_batch,Wo,Bo,'softmax')

誤差の計算、

    E = function1.error(Y,T_batch)
    E_save = np.append(E_save, E)

精度の計算、

    Acc = function1.accuracy(Y,T_batch)
    accuracy_save = np.append(accuracy_save, Acc)

これで順伝搬計算がすべて完了です。

まとめ

ひとまずCNN層を追加したニューラルネットワークの順伝搬計算まで終えることができました。次回、逆伝搬の処理を進めて、実際にこのニューラルネットワークに学習をさせてみたいと思います。まぁ、この誤差を求めるアルゴリズムもややこしいんですよねぇ、頑張りましょう。。。


元の記事
週末のディープラーニング - 社畜エンジニア発掘戦線

Twitter
世界の社畜 (@sekai_syachiku) | Twitter