CNN:逆伝搬計算
おつかれさまです。
前回はCNN(畳み込みニューラルネットワーク)についてアルゴリズムと順伝搬計算を行うコードを書いていきました。今回は、その誤差から微分値を計算し、逆伝搬でパラメータを更新してみたいと思います。
CNN逆伝搬計算の概要
今回のニューラルネットワークの構成をもう一度確認してみます。
ここの計算は、今まで隠れ層の出力HがCNN層のCに置き換わっただけで、特に問題はないですね。
次に、CNN層の微分を見てみます。考え方は今までと同じく、3段連鎖律で骨格となる微分式を求めます。
後ろ2つの項はsoftmax関数とクロスエントロピーの合成微分なので、今まで通りΔを使います。
一番前の項ですが、式をそのまま書き下してみます。
合成関数の微分ってことで、とりあえず中の「X_batch*F+Bc」をF微分したX_batchは外に出てきそうです。
んで、ReLU関数の微分です。もともと、入力が0以下であれば全部「0」、ずっと同じ値ということは傾きはゼロなので微分しても「0」。0より大きい値は入力値をそのまま返すので、「f(x)=x」となり、これを微分すれば全部1です。
つまり、「ReLU(X_batch*F+Bc)」の「X_batch*F+Bc」がマイナスであれば「0」、プラスなら「1」と処理してあげれば良さそうです。(ただ、正直この辺あんまよく分からないんですよね〜合ってんのかな?)
とりあえず、微分項の処理内容が見えてきたのでまとめてみます。
ReLUの微分はどう書けばいいのか分からんかったので「ReLU'」としています、0と1のやつです。
順伝搬を思い出してみるとFは畳み込みの計算に用いました。今はその逆をたどっているので、逆畳み込み演算が必要です。といっても、「X_batch」と計算された「ReLU’*Wo*Δ」で順伝搬のときに計算した畳み込み計算を行っているだけですが。
次にバイアスです、こちらも同じように微分を行います。
後ろの2項はFと同じですね、前の項もBcでの微分なので1です。ただしReLUの微分は残っています。
つまり、結果をまとめると、
確かに順伝搬のとき、Bcは畳み込み演算には関与していないので、Fのようにややこしい逆畳み込みはありません。なんか、よくできていますね、数学的にあたりまえなんでしょうけど。
コーディング(逆伝搬:出力層Fの微分)
さてさて、逆伝搬の処理が分かってきたので、この処理をコードに落としていきたいと思います。
前回の続きから、まずは出力層の微分値を求めます。
(forの中なので、その場所に応じて各コードはインテンドされています)
dWo = np.dot(FM_batch.T,(Y-T_batch)) dBo = np.reshape(np.sum(Y-T_batch, axis=0),(1,10))
ここは今まで通り、上の式をコードに落としただけです。CのところはFM_batchになっています。
コーディング(逆伝搬:CNN層の微分)
次に、Fの微分です。まずは「Wo*Δ」の部分をコードで書いておきます。
np.dot(Y-T_batch,Wo.T)
ReLU関数の部分の微分も書いてみます。ReLU関数の中身は「X_batch*F+Bc」でした。前回のコードで畳み込み計算をしたときにこの値を求めました。しかし、その値を保存せずに次の処理に進んでしまったのでこのままでは使えません。
もうあと付け感がハンパないですが、前回のコードに「X_batch*F+Bc」を保存するコードを加えます。
FM_bias_batch = np.empty((0,FM_size**2))
バッチごとに保存したいのでこれは「for M…」の外に記述します。欲しい値は「FM_bias = FM_storage+Bc」で計算されているので、この値を「FM_bias_batch」に保存するコードを足します。
FM_bias_batch = np.vstack((FM_bias_batch,FM_bias))
まー自分で書いててなんですが、言葉書きだとわけわからんので、また後で全体コード確認して下さい。。
で、この「FM_bias_batch」を使ってReLUの微分を書きます。ReLUの微分は、0以下が「0」で、0より大きければ「1」となります。ここもwhereを使って記述します。
np.where(FM_bias_batch<=0,0,1)
ここまで書ければ「ReLU'*Wo*Δ」をまとめてdeltaとして書いてしまいます。
delta = np.where(FM_bias_batch<=0,0,1)*np.dot(Y-T_batch,Wo.T)
これで「dF = X_batch*ReLU'*Wo*Δ」を「X_batch*delta」とまとめることができました。あとはこの2つの畳込み計算です。
畳み込み計算の流れは前回とほとんど同じです、まぁ逆方向なのでちょっと違いますが。まずは画像ごとに計算されたdFを保存するハコを準備します。このハコには各画像のdFがバッチ数だけ保存されます。
dF_batch = np.empty((0,F_size**2))
次はバッチごとの処理です。
for M in range (batch_size): img = np.reshape(X_batch[M],(IMG_size,IMG_size)) img_delta = np.reshape(delta[M],(FM_size,FM_size)) dF_storage = []
ここの処理で畳み込むのはX_batchとdeltaなので、この2つを2次元にreshapeします。
そして、画像の要素ごとのかけ算です。forのところで縦と横の計算回数が「F_size(=5)」になっているのと、X_batchからピックアップする画像サイズは「FM_size」になっているところだけ注意、あとは一緒かな。
for i in range(F_size): for j in range(F_size): pick_img = img[i:i+FM_size, j:j+FM_size] dF_storage = np.append(dF_storage,np.tensordot(img_delta,pick_img))
配列で保存されたdFを最初に設定したハコ「dF_batch」にnp.vstack詰め込みます。
dF_batch = np.vstack((dF_batch,dF_storage))
この時点でdF_batchの型は(batch_size, 25)になっています。最後にFを更新するためには(5, 5)のdFが必要なので、dF_batchを(5, 5)にreshapeします。ただ、dF_batchにはバッチ数だけ計算結果が詰まっているのでこれらを平均化してdFとします。reshapeとaverageをまとめて書きます。
dF = np.reshape(np.average(dF_batch,axis=0),(F_size,F_size))
これでようやくdFが求まりました。
コーディング(逆伝搬:出力層Bcの微分)
dBcは「1*ReLU'*Wo*Δ」になるのでdeltaがそのまま求める値になります。ただし、こちらもバッチ数だけデータが詰まっているので平均化してあげる必要があります。
dBc = np.reshape(np.average(delta,axis=0),(1,FM_size**2))
これでBcも更新できる形に整いました。
コーディング(パラメータの更新)
これですべてのパラメータの微分がそろったので、勾配降下法でパラメータを更新します。
Wo = function1.update(Wo,learning_rate,dWo) Bo = function1.update(Bo,learning_rate,dBo) F = function1.update(F,learning_rate,dF) Bc = function1.update(Bc,learning_rate,dBc)
微分さえ求められればなんてことないですね。
コーディング(残りのモロモロ)
まずは学習にかかる時間の計算、
end_time = time.time()
total_time = end_time - start_time
print(total_time)
ほんでからグラフ化、
#show graph
function1.plot_acc(accuracy_save)
function1.plot_loss(E_save)
最後に学習後のパラメータ保存、
#パラメータ保存 CNN_parameters = [Wo,Bo,F,Bc] path = '/Users/FUTOSHI/Desktop/cnn_test' name = '/CNN_ver6' function1.save(CNN_parameters,name,path)
ここは外部ファイルの関数を使ってちょちょっと書いてしまいます。とりあえずここまででコーディングは完了です。はぁ、おつかれさまでした。
まとめ
前回に引き続いてCNNのアルゴリズムとコードをまとめていきました。
なんかいろいろ調べてみたりもしたんですが、逆伝搬についてはあんまり詳しく書かれているサイトとか書籍とかなかったんで、自己流?というか今までのニューラルネットワークを構築した流れに沿って進めていきました。なので、正直この処理が正しいのかよく分かりません。
ミスってたり、抜けがあれば即効修正しますお。
次回はこのニューラルネットワークで学習、あとその精度を評価してみます。学習、時間かかりそうやなぁ。
全体コード:CNN
github.com
全体コード:function
github.com
CNN:順伝搬計算
おつかれさまです。
今回からCNN(Convolution Neural Network:畳み込みニューラルネットワーク)にトライしていこうと思います。なんせこのCNN、アルゴリズムがとてもややこしいし、時間もかかるので、何回かに分けてまとめていく予定です。データセットは今までどおりMNISTの手書き数字の認識問題を扱います。最終的にこのCNNを使えば99%を超える結果となるようです(ほんまかいな)。
CNNの大きな特徴は「画像のまま、ニューラルネットワークに突っ込む」です。これまでのニューラルネットワークではMNISTの画像(28pix, 28pix)を(1, 784)の型でニューラルネットにインプットしていました。しかし、CNNでは(28, 28)のままニューラルネットの計算を進めます。
とにかく、始めていきましょう。
CONTENTS
- ニューラルネットワークの設計
- CNN層での処理(畳み込み演算)
- CNN層での処理(バイアス付加とReLU関数)
- コーディング(ライブラリ、データ・セット)
- コーディング(初期設定)
- コーディング(イテレーション)
- コーディング(CNN層)
- コーディング(出力層、誤差計算、精度計算)
- まとめ
ニューラルネットワークの設計
データセットは今まで通りMNISTなので、ニューラルネットワークの設計から考えていきます。その骨格自体はできるだけシンプルぅなものにして、ひとつずつ構築していきたいと思います。
とりあえず、手始めにトライしてみるのはこんな感じ。
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)」で、その要素はランダムに設定されます。どのフィルターサイズがええねんってのは場面によって変わってきます。
フィルターをいくつか用意してゴニョゴニョってのはもうちょっと後で、とりあえず一番シンプルなところから進めます。
(28, 28)の型に変換されて、入力された画像にこのフィルターがかけられます。その演算方法は、左端から画像にフィルターを重ねて、位置のそろう要素どうしをかけ合わせて、その総和をとります。つまり、この計算で得られる値はひとつです。
数式で書くと頭痛がするので、イメージで攻めます。
すべてのフィルター要素を計算し終えれば、次はそのフィルターをひとつ横へずらします。そしてまた同じ計算を行います。
ただ、今回はCNN層の次がそのまま出力層なので、わざわざ画像にせず、配列のままで処理を進めていく予定です。
X_batchはバッチ数の画像が詰まっているので、ひとつずつ抜き出してこの処理を行います。
CNN層での処理(バイアス付加とReLU関数)
バイアスの付加はシンプルです。要素をランダムに設定したバイアスを計算された特徴マップに足すだけです。
ここまでで計算できた「X*F+B」をReLU関数に通します。ReLU関数はsigmoid関数の代用として最近よく使われるようになってきた活性化関数のようです。関数の式は0を境界にして2種類に場合分けされます。入力が0よりも大きいときは入力値をそのまま、入力が0より小さければ「0」を返します。
処理の流れ自体は簡単です、sigmoidみたいに指数関数かけて、割って、1足して、とかないのでラク。例えばこんな感じ。
じゃあ、なんでsigmoid関数だとダメなのかっていうと、今回は2層(CNN層と出力層)でシンプルにニューラルネットワークを設計していますが、本気を出すと5層、10層、それ以上…というように層を深くしていくことが求められるそうです。層が深くなったときにsigmoid関数だと微分値がどんどん小さくなってしまい、逆伝搬計算がうまくいかなくなります。この点を改善されたのがReLU関数ということです。
どれぐらいでsigmoidが使えなくなるのかは実際に比較してみないとよく分からないので、またこの辺のスキルが身に付いてきたらコードを書いてトライしてみようと思います。
とにかく、このような計算の流れでCNN層の計算は進んでいきます。
コーディング(ライブラリ、データ・セット)
では、ここまで準備してきたニューラルネットワークを実際にコードで書いていきたいと思います。
CNN層の処理以外は、以前に作成した関数の外部ファイルを使うことにします。
このファイルで使用するライブラリのインポートからです。
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)
次に、特徴マップのサイズを計算しておきます。入力画像とフィルターのサイズが分かれば下の式で計算できます。
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での処理はバッチから画像を抜き出して、計算して、ハコに保存する、を繰り返します。
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関数を処理の間にうまく挟み込んで画像を出力させます。
元の画像
畳み込み
バイアス付加
ReLU関数
バイアスを付加したあたりからなんの画像か分からなくなってきていますね、とにかく処理はうまくいってそうです。
コーディング(出力層、誤差計算、精度計算)
この後の流れはこれまでと同じです。
出力層の計算、
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層を追加したニューラルネットワークの順伝搬計算まで終えることができました。次回、逆伝搬の処理を進めて、実際にこのニューラルネットワークに学習をさせてみたいと思います。まぁ、この誤差を求めるアルゴリズムもややこしいんですよねぇ、頑張りましょう。。。
アルゴリズムの関数化2
おつかれさまです。
前回に続いて、もう少しだけ関数化の練習をしてみたいと思います。今回は、隠れ層を追加したニューラルネットワークの構築に使ったアルゴリズムを関数化してみます。まぁ、ほとんど前回やっちゃったんですけどね。
流れはほとんど同じなので、変更点だけピックアップしてコードを書いていこうと思います。
シグモイド関数の追加
まず、ここでは隠れ層を追加したので、隠れ層に使用した活性化関数:シグモイド関数を追加します。
def sigmoid(x): return 1 / (1 + np.exp(-x))
ここは問題ないですね。
全結合層の編集
次の変更点は、このシグモイド関数を用いた全結合層(隠れ層)の追加です。全結合層は前回で関数化が完了していますが、softmax関数しか対応していません。処理のアルゴリズムは同じですが、活性化関数だけが異なります。
def affine(img,W,B): return softmax(np.dot(img, W)+B)
この全結合処理を様々な活性化関数でも対応できるようにしてみます。
まず最初に、この層行われる計算のinput(上ではimgとしています)とWの行列の型が合っていなければ、そもそも計算ができません。ターミナル上にはエラーが表示されます。まぁこのエラーを参照してもいいんですが、英語だし、自分で分かるようにエラー文を表示させてみることにします。
def affine(input,W,B,f): if input.shape[1] != W.shape[0]: print('Affine Error: 入力とWの行列の型が合っていません') sys.exit() else: 〜活性化関数の処理〜
入力の変数はimgからinputとします。fは使いたい活性化関数を指定する変数です。
エラーの分岐はifを使って「入力の1行目(0行目はバッチ数)」と「Wの0行目」が一致するときとしないときで処理を分けます。一致しないときはエラー文「'Affine Error: 入力とWの行列の型が合っていません'」を出力してsys.exit()でプログラムを終了させます。この処理を入れることで、プログラムのどこでエラーが発生したのか分かりやすくなりました。
sys.exit()は実行中のプログラムを終了させるコマンドで、これを使うにはsysというモジュールが必要です。プログラムの最初に下記のインポートも追記しておきます。
import sys
では、活性化関数を使い分けるコードを書いていきたいと思います。とりあえず、「シグモイド関数」、「ソフトマックス関数」、「関数なし(XW+B)」、「入力の打ち間違い(エラー)」の4通り作ってみます。使い分けがあるので、ここも分岐のifを使います。4通りなのでelifが必要です。
else: if f == 'softmax': return softmax(np.dot(input, W)+B) elif f == 'sigmoid': return sigmoid(np.dot(input, W)+B) elif f == 'none': return np.dot(input, W)+B else: print('Affine Error: 関数が不明です') sys.exit()
fは活性化関数を指定する入力変数で、文字列で入力するようにします。なので「’’」がいります。文字の打ち間違えとかしちゃった場合、エラー文「'Affine Error: 関数が不明です'」を出力してプログラムを終了するようにします。
誤差逆伝搬の考慮
あと、考慮すべきアルゴリズムは逆伝搬計算に用いる微分式です。
dWo = np.dot(H.T,(Y-T_batch)) dBo = np.reshape(np.sum(Y-T_batch, axis=0),(1,10)) dWh = np.dot(X_batch.T,H*(1-H)*np.dot(Y-T_batch,Wo.T)) dBh = np.sum(H*(1-H)*np.dot(Y-T_batch,Wo.T), axis=0, keepdims=True)
このコードは損失関数EをパラメータW(B)で微分することに由来しています。
ここのコードもいい具合に一般化して関数にしてしまおうと考えているんですが、いいアイデアが思いつきません…。この逆伝搬が中間層(隠れ層)なのか出力層なのかで必要な変数が異なるので、うまくまとまらないんですよね〜。思いついたらまた記事にするので、今回はこのまま書いて動作確認することにします。
動作確認
これで関数の追加は完了です。関数を記述しているファイルは「function1.py」として保存しておきましょう。では、「MNIST_hidden.py」というファイルに隠れ層追加のニューラルネットワークのを記述してみます。
MNIST_hidden.py
github.com
function1.py
github.com
実際にプログラムを走らせてみると、前回と同じような結果が得られます。
ちなみに計算時間は、
6.0202858448028564
問題なく関数は動作してそうです。
まとめ
こんな感じで、まとめられそうなアルゴリズムがあればどんどん外部ファイルにエクスポートして、プログラムをスッキリ分かりやすく書いていければと思います。逆伝搬の計算は、ちょっとじっくり考えることにします。
アルゴリズムの関数化1
おつかれさまです。
処理を関数にして外部のファイルにまとめてしまって、コードの流れをすっきり見やすくさせよう、という試みです。
練習として、これまで作ったプログラムを使って、その中に書かれている処理を関数に書き直して、外部ファイルへ分離してみます。とりあえず、MNISTのシンプルニューラルネットワークのコードでトライしてみたいと思います。
関数化にトライ
では、既存のコードを上からなぞっていきながら、処理を関数に書き換えていくことにします。ニューラルネットワークのコードを書くのは「MNIST_Perceptron.py」で、外部ファイルとして関数を書き溜めていきのは「function.py」とします。この2つのファイルは同じフォルダに格納されています。
まずは「MNIST_Perceptron.py」に「function.py」のインポートして2つのファイルを紐付けます。
▼MNIST_Perceptron.py
import function
その他、関数以外でも使う場面があるので、必要なライブラリもインポートしておきます。
▼MNIST_Perceptron.py
import numpy as np import matplotlib.pyplot as plt import time
「function.py」にもライブラリのインポートが必要です。関数の中で用いるライブラリをインポートしておきます。
▼function.py
import numpy as np import matplotlib.pyplot as plt import pickle
MNISTのデータロードの処理
最初はMNISTデータのロードからでした、まずはもとのコードを確認。
#load dataset import pickle save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl' with open(save_file, 'rb') as f: dataset = pickle.load(f) train_img,train_label,test_img,test_label = dataset
次に、データセットの前処理を行いました。
#pixel normalization X_train, X_test = train_img/255, test_img/255 #transform_OneHot T_train = np.eye(10)[list(map(int,train_label))] T_test = np.eye(10)[list(map(int,test_label))]
「データロード〜前処理」は関数でひとまとめにできそうです。イメージはこんな感じ。
MNISTのpickleデータのPATHを入力変数として、出てくるのは前処理された各データX_train〜T_testまでです。なので、コードの骨格としては、こんな感じ。
def minist(PATH): ・・・・・・・・・ return [X_train,T_train,X_test,T_test]
ではここの処理を関数に書き換えていきます。
まず、pickleライブラリのインポートは外に出してimportの部分でまとめて行うことにします、なので関数の中には含みません。上のコードで「save_file」で記述したPATH部分を「PATH」という入力変数にして書いていきます。それ以外はコピペです。
▼function.py
def mnist(PATH): #mnist_load save_file = PATH with open(save_file, 'rb') as f: dataset = pickle.load(f) train_img,train_label,test_img,test_label = dataset #pixel normalization X_train, X_test = train_img/255, test_img/255 #transform_OneHot T_train = np.eye(10)[list(map(int,train_label))] T_test = np.eye(10)[list(map(int,test_label))] return [X_train,T_train,X_test,T_test]
この処理を「function.py」に記述します。
では、「function.py」に記述した関数を用いて、「MNIST_Perceptron.py」の内容を書いてみたいと思います。
▼MNIST_Perceptron.py
#mnist_dataset save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl' X_train,T_train,X_test,T_test = function.mnist(save_file)
なんと2行で書けてしまいました、これは見やすい。こんな感じで進めていきましょう。
MNISTデータの確認
少し本筋からはそれますが、データがきちんとロードされているか確認できるようにしておきましょう。データの内容(画像とラベルの出力)を確認するコードを書いてみます。
i=0 img = np.reshape(X_train[i],(28,28)) plt.figure() plt.imshow(img, cmap='gray_r') plt.show() print(T_train[i])
iは60000枚のうち、何番目の画像を出力するか、という値です。とりあえず0番目を出力してみます。
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
バッチリですね。ではこの内容を関数に落としていきます。
関数に入力する情報は画像のデータ(img)、正解ラベル(label)、そして順番(i)です。それぞれ「X_train、T_train、i」に対応しています。そして欲しい出力は、画像の表示(plt.show)と正解ラベルの表示(print(label))です。
▼function.py
def show(img,label,i): img = np.reshape(img[i],(28,28)) plt.figure() plt.imshow(img, cmap='gray_r') plt.show() print(label[i])
このコードを「function.py」に記述して準備は完了です。「MNIST_Perceptron.py」でに下記のコードを記述して動作確認をしてみます。
▼MNIST_Perceptron.py
function.show(X_train,T_train,0)
記述するコードはこれだけ、同じ結果が返ってきます。うまくいっているかどうかの確認も少ないコードで行えるのは良いことです。
まだ全然序盤ですが、これだけでも十分スッキリさせられました。どんどん進んでいきましょう。
活性化関数と損失関数の処理
次はシグモイド関数と損失関数の記述です。ただ、ここはもともと関数として記述していたので、そのまま「function.py」に書いてしまいましょう。
▼function.py
def softmax(z): return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True) def loss(y, t): delta = 1e-7 return -np.sum(np.multiply(t, np.log(y+delta)) + np.multiply((1 - t), np.log(1 - y+delta)))
今までと同じ記述です。
初期パラメータの設定とイテレーションの開始
ここまでいろいろと関数化して外部ファイルに逃してきましたが、ここはそのままです、触りようがないっす。
▼MNIST_Perceptron.py
#initial parameters W = np.random.randn(784, 10) B = np.random.randn(1,10) learning_rate = 0.001 E_save = [] accuracy_save = [] start_time = time.time()
次にイテレーションの開始、
▼MNIST_Perceptron.py
#iteration num_of_itr=3000 for i in range(num_of_itr):
ここもそのままですね。
ミニバッチ処理
続いてはミニバッチ処理です。まずはもとのコードの確認、イテレーションの中なのでインテンドされています。
train_size = X_train.shape[0] batch_size = 100 batch_mask = np.random.choice(train_size,batch_size) X_batch = X_train[batch_mask] T_batch = T_train[batch_mask]
ここも関数化できそうです。処理のイメージとしてはこんな感じ。
def batch(img,label,batch_size): ・・・・・・・・・ return [X_batch,T_batch]
ではコードに書いてみます、といってもほとんどコピペですが。追記するファイルは「function.py」です。
▼function.py
def batch(img,label,batch_size): img_size = img.shape[0] batch_mask = np.random.choice(img_size,batch_size) X_batch = img[batch_mask] T_batch = label[batch_mask] return [X_batch,T_batch]
この関数を用いるためには「batch_size」を定義しておかなければいけないので、イテレーションの外の初期パラメータのところに足しておきます。
▼MNIST_Perceptron.py
batch_size = 100
ではこの関数を用いてミニバッチの処理を書いてみます。
▼MNIST_Perceptron.py
X_batch,T_batch = function.batch(X_train,T_train,batch_size)
1行でまとまります、素晴らしい。
出力層の計算
今回は入力層と出力層しかないので、いきなり出力層を計算することになります。
Y = softmax(np.dot(X_batch, W)+B)
もともと1行なので、このままでもええやん、という感じですが、分かりやすく体裁を整えるという意味で関数化してみます。
入力データはバッチ処理された画像、そしてパラメータWとBです。今回は画像を扱っているので画像データですが、パラメータとの行列の型さえあっていれば、別になんでもいいです。基礎問題でトライしたようなインプットデータでも大丈夫です。
ここは複雑ではないので一気にコード化してみます。記述するのは「function.py」です。
▼function.py
def affine(img,W,B): return softmax(np.dot(img, W)+B)
affinは全結合という意味(らしい)です。この関数の中でsoftmaxを使っているので、softmaxよりも下の行で記述しなければいけないので注意です。これでもとのコードを書きなおしてみます。
▼MNIST_Perceptron.py
Y = function.affine(X_batch,W,B)
もともと1行なので、コード量は変わりませんが、意味するところは分かりやすくなりました。
損失量の計算
損失量Eについても同じように関数化してみます、こちらももともと1行なのでコード量の恩恵は少ないですが。
E = loss(Y,T_batch)/len(Y)
入力は出力層で計算されたYと正解ラベル、今回はT_batchにあたります。
▼function.py
def error(Y,label): return loss(Y,label)/len(Y)
では、もとのコードを書きなおします。
▼MNIST_Perceptron.py
E = function.error(Y,T_batch)
こちらもそのままですね。
あと、計算したEを保存する1行も付け足しておきます。これはもとのコードと同じです。
▼MNIST_Perceptron.py
E_save = np.append(E_save, E)
精度の計算
続いては精度の計算です。もとのコードを確認してみます。
Y_accuracy = np.argmax(Y, axis=1) T_accuracy = np.argmax(T_batch, axis=1) accuracy = 100*np.sum(Y_accuracy == T_accuracy)/batch_size
入力の情報は出力結果Y、そしてそれと比較するための正解ラベル、ここではT_batchになります。batch_sizeも必要ですが、Yの1列目がbatch_sizeにあたるので、この関数の中で再定義することにします。極力入力する変数は少なくしたいのです。ではこの内容を関数化してみます。
▼function.py
def accuracy(Y,label): batch_size = Y.shape[0] Y_accuracy = np.argmax(Y, axis=1) T_accuracy = np.argmax(label, axis=1) return 100*np.sum(Y_accuracy == T_accuracy)/batch_size
では、もとのコードを書きなおします。
▼MNIST_Perceptron.py
Acc = function.accuracy(Y,T_batch)
ここは1行でまとまりました、素晴らしい、なんか関数名と変数名が一致するとエラーが出るっぽいのでAccにして回避しています。
保存するコードも足しておきます。
▼MNIST_Perceptron.py
accuracy_save = np.append(accuracy_save, Acc)
パラメータの更新
次はパラメータの更新です。とりあえずもとのコードをみてみます。
dW = np.dot(X_batch.T,(Y-T_batch)) dB = np.reshape(np.sum(Y-T_batch, axis=0),(1,10)) W = W - learning_rate*dW B = B - learning_rate*dB
ここも1行ずつしかないので関数化によるメリットが少ないんですよねぇ、まぁここまでやったし、やります。
まずは微分式のところから。
▼function.py
def delta_w(img,label,Y): return np.dot(img.T,(Y-label)) def delta_b(label,Y): return np.reshape(np.sum(Y-label, axis=0),(1,10))
それからパラメータの更新、
▼function.py
def update(p,learning_rate,delta): return p - learning_rate*delta
これとかもう、そのまんまですし、お寿司。
まあ、とりあえずこれらを使ってもとのコードを書き直してみましょう。
▼MNIST_Perceptron.py
dW = function.delta_w(X_batch,T_batch,Y) dB = function.delta_b(T_batch,Y) W = function.update(W,learning_rate,dW) B = function.update(B,learning_rate,dB)
もう微分式と更新式をまとめて書いています。ごちゃごちゃした計算式がないので、ぱっと見は分かりやすそうです。
これでイテレーション部分の記述は完成です。最後に計算時間を測定するコードだけ足しておきます。ここは関数とか関係ないです。
end_time = time.time()
time = end_time - start_time
print(time)
グラフ化
学習の計算については記述が完了しました。あとはその結果をグラフ化するだけです。
ここも流れは同じです、もとのコードをみてみます。
#plot plt.figure() plt.title('ACCURACY') plt.xlabel("LEARNING NUMBER(EPOCH)") plt.ylabel("ACCURACY (%)") plt.xlim(0, 3000) plt.ylim(0, 100) plt.grid(True) plt.plot(accuracy_save, color='blue') plt.show()
まずは精度のグラフ、プロットするデータはaccuracy_saveです。ここも関数化するわけですが、まぁほとんどコピペです。入力データはaccuracy_saveだけです。
▼function.py
def plot_acc(accuracy_save): plt.figure() plt.title('ACCURACY') plt.xlabel("LEARNING NUMBER(EPOCH)") plt.ylabel("ACCURACY (%)") plt.xlim(0, 3000) plt.ylim(0, 100) plt.grid(True) plt.plot(accuracy_save, color='blue') plt.show()
損失量のグラフについても同じようにグラフ化してみます。
▼function.py
def plot_loss(E_save): plt.figure() plt.title('LOSS FUNCTION') plt.xlabel("LEARNING NUMBER(EPOCH)") plt.ylabel("LOSS VALUE") # plt.xlim(0, 3000) # plt.ylim(0, 100) plt.grid(True) plt.plot(E_save, color='blue') plt.show()
あとはこの関数をもとのコードに記述するだけ、
▼MNIST_Perceptron.py
#show graph
function.plot_acc(accuracy_save)
function.plot_loss(E_save)
ここはずいぶんスッキリしました、グラフのテンプレはいくつか作っておいてもいいかも。
プログラムの実行
さてさて、関数を使ってコードをスッキリさせることができたので、後は動作確認をしてみます。
結果↓
3.68457293510437
おお、以前と同じような結果が得られました、ひとまずは成功のようです。計算時間はちょっと遅くなったような…、関数を外に出したので、呼び出しにけっこう時間がかかるかなと思っていましたが、まぁ許容の範囲内ですかね。重たい計算をさせたときにどれぐらい差が出るのかってのを調べてみないといけないんですけどね。
まとめ
ひとまず関数化して外部ファイルにコードを逃がすことで、ニューラルネットワークの本質的な内容でコードを記述することができました。今回の練習で関数をたくさんつくりましたが、まだまだ足りないようです。ニューラルネットワークの学習を進める上でも必要なアルゴリズムをどんどん関数化してfunction.pyを大きくし、汎用性の高いファイルにできればと思います。
おそらく、何も考えずにファイルを大きくしていては計算時間の問題も出てくると思うので、そこは都度考慮しながら進めていきたいと思います。
とりあえず、もとのコードと今回編集したコードを載せておくので参考にしてみて下さい。
もとのコード
github.com
アレンジ後:function.py
github.com
アレンジ後:MNIST_Perceptron.py
github.com
他にもニューラルネットワークの形成に使えて、関数化できそうなアルゴリズムもあるので、もう少しトライしてみたいと思います。
関数の外部モジュール化
おつかれさまです。
これからCNN(畳み込みニューラルネットワーク)にいろいろとトライしていこうとしていますが、少し問題があります。それは、アルゴリズムが複雑で、コードに落とすとめちゃめちゃ長くなる、ということです。ダラダラと長く書かれたコードは可読性が下がり、「あ〜あのパラメータどこに書いたっけな〜」と無駄な時間を過ごしてしまいがちになります。
この解決策はいろいろあるのかもしれませんが、とりあえず処理を関数でモジュール化してしまう方法をトライしてみたいと思います。
やりたいこと
関数の部分を別のPythonファイルに書いてしまって、そこから使いたい関数をインポートする、ということをしてみたいと思います。Pythonの関数というと、def〜でまとまる処理の部分です。これまでだとsigmoid関数やloss関数の処理に使っていました。
やり方
同じフォルダに2つのPythonファイルを入れて、インポートしたい関数の書いたファイル名をimportで書けばできます、意外と簡単。
とりあえずやってみましょう、「function.py」と「test.py」というファイルを「function_test」というフォルダに突っ込みます。
function.pyには2次曲線の関数を定義してみます。
def math(x): return x**2
続いてtest.pyを書いていきます。
import function x = 2 y = function.math(x) print(y)
インポートするのは関数を定義しているファイルの名前で、「ファイル名.関数名()」で使えるようです。ターミナルでtest.pyを実行すると「4」という結果が返ってきました、うまくいってそうです。ファイルを実行後、フォルダに新たなファイルが作成されます。
あんまりよく分からないんですが、外部ファイルのインポートに関するファイルなんでしょう(勉強不足)。
この方法を使えば複雑な処理をファイルの外に出すことができるので、ニューラルネットワークの本質的な記述だけを書くことができます。兎にも角にも、練習がてらにこれまでのファイルを外部モジュールを使って書き直してみたいと思います。
※これは同じフォルダに2つのファイルが入っていないと実行できません。フォルダの階層を変えたり、全く別のフォルダに関数のファイルを置くにはまた別の処理が必要になります。
MNIST画像認識のNN正解率の検討
おつかれさまです。
入力と出力のみから構成されるシンプルなニューラルネットワーク、そしてそれに隠れ層を追加したニューラルネットワークで正解率の比較を行いました。しかしながら、結果は同じくらいの正解率にとどまり、あまり違いが見れませんでした。
シンプルニューラルネットワーク:86.64 % 隠れ層追加ニューラルネットワーク:87.08 %
ただ、学習経過のグラフを確認すると、もっとイテレーションを回すことでまだまだ正解率が上昇しそうな傾向が見られました。この検証はイテレーション回数を3000回でトライしたので、学習がまだまだ足りなかった可能性が考えられます。
とりあえず、単純にイテレーション回数を増加させてみるとどうなるのか確認してみたいと思います。
イテレーション回数の検証
これまでに作った2つのプログラムをくっつけてみます。これで同じ初期パラメータ、バッチ画像となるようにして、2つのニューラルネットワークを比較させてみました。
イテレーション回数は2000回〜500000回で適当に振り分けてみました。ちなみに、このときの学習率は0.001です。
こちらがその結果、左側が2000回のグラフで、右側が500000のグラフです。赤色が「シンプルニューラルネットワーク」、青色が「隠れ層追加ニューラルネットワーク」になります。
2000回のグラフは、まあ見たことあるわって感じですが、500000のグラフはなんじゃこりゃってなりますね。赤色の正解率は完全に飽和してしまっていますが、青色のグラフは、文字通り「青天井」でどんどん上がっています。何なら100%超えとるやんけ。
2000〜500000の間をアニメ化してみます、まぁ特に深い意味はないですが、隠れ層の正解率がぶりぶり上昇しているのが分かります。
ではそれぞれの学習で得たパラメータ(WとB)を使ってテスト画像で正解率を求めてみます。…これは気が遠くなる作業でした、まぁプログラムでだいたい自動化してたので待つだけでしたが。さすがに500000の計算は時間がかかった、一晩中CPUをあっちっちで走らせて計算させました(GPU使えたら楽になるんかなぁ、と初めて実感)。
こちらがその結果、
これは分かりやすい結果が得られました、この結果から得られる情報は以下の通り。
・ 訓練では青天井で正解率が上昇していますが、テスト結果では飽和します。 ・ 赤グラフは92%ほどで飽和しているのに対して、青グラフは96%まで上昇して飽和しています。 ・ 学習結果は100000〜150000回で飽和します。
やっぱり学習回数を増やせば結果は上昇するんですね、86%程度でわーいって言ってた頃が私にもありますた。
学習率の検証
正解率が飽和することが分かってくると、「じゃあさっさと飽和してくれよ、時間かかりすぎるんだよ」と思うのが世の常です。短い時間で学習を終えようとするなら、とりあえず学習率を上げてみることにします。最初のトライは学習率が0.001だったので、0.002で同じように計算してみます。
こちらがその結果、
おお、正解率は変わりませんが、なんだか飽和が早くなった気がします。このまま学習率を上げていってみましょう。めっちゃ時間かかるので、イテレーション回数は200000回にしています。
学習率を0.01まで上げると形が崩れてき始めているので、このあたりが限度なんですかね。
(もっと突っ込んで調べてみてもいいんですが、計算に時間がかかりすぎるんでこのあたりにしておきます)
まとめ
今回の検証で、最初に行ったテストは全然学習が足りていないことが分かりました。最終的な正解率は、シンプルニューラルネットワークだと約92%、隠れ層を追加すると96%まで上昇するようです、これってもうほとんど人間やんけ、ディープラーニングすげーな。
あとは、学習率を最適化することで、飽和する正解率に到達するまでの時間も短縮できることが分かりました。学習時間を短縮する他のアイディアとしては、パラメータの更新を勾配法からモーメンタム法などに変更してみるのも効果的かもしれません。またいつかトライしてみたいと思います。
しかしながら、96%を叩き出したこのニューラルネットワークも専門家からするとまだまだのようです。CNNという手法でこの正解率は99%を超えるところまで上昇できるようです。この後、CNNも頑張ってトライしてみよう。
MNIST手書き数字の画像識別(隠れ層NNの評価)
前回はMNISTの画像識別を行うためのニューラルネットワークに隠れ層を追加して学習を行いました。訓練の結果は80%そこそこであんまり向上した感じがなかったですが、とりあえずテストデータで精度を評価してみます。コードの流れは前回評価したときと同じです。
CONTENTS
コーディング
前回、学習済みのパラメータを保存するところまではやってしまったので、そのロードからです、まずはMNISTのデータセット。
#load dataset import pickle save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl' with open(save_file, 'rb') as f: dataset = pickle.load(f) train_img,train_label,test_img,test_label = dataset
完全にコピペなので、コメントはしょります。
続いて、学習済みのパラメータをロードします。
save_file = '/Users/FUTOSHI/Desktop/MNIST_test/Hidden.pkl' with open(save_file, 'rb') as f: parameters = pickle.load(f) Wh,Bh,Wo,Bo = parameters
パラメータの数が4つになっているので、順番と添字だけ間違えないようにご注意。
あとはほとんど同じなのでテンポよくいきます、詳しくは前回の記事をご参考。
まずはライブラリのインポート、
#library import import numpy as np import matplotlib.pyplot as plt import time
次にMNISTデータの前処理(正規化とOne-Hot化)です。
#pixel normalization X_train, X_test = train_img/255, test_img/255 #transform_OneHot T_train = np.eye(10)[list(map(int,train_label))] T_test = np.eye(10)[list(map(int,test_label))]
それから関数の定義、
#function def sigmoid(x): return 1 / (1 + np.exp(-x)) def softmax(z): return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True) def loss(y, t): delta = 1e-7 return -np.sum(np.multiply(t, np.log(y+delta)) + np.multiply((1 - t), np.log(1 - y+delta)))
正解と不正解の画像を振り分ける空行列を作っておきます。
correct_img = np.empty((0,784)) error_img = np.empty((0,784))
では、10000枚のテスト画像で正解率を計算していきます。
start_time = time.time() num_of_test = X_test.shape[0] #10000 #test start for i in range(num_of_test):
今回は隠れ層があるので、Hを計算してから出力Yを求めます。
#forward prop
H = sigmoid(np.dot(X_test[i],Wh)+Bh)
Y = softmax(np.dot(H, Wo)+Bo)
では正解かどうかを判断して、それぞれの行列に振り分けます。
#set accuracy Y_accuracy = np.argmax(Y) T_accuracy = np.argmax(T_test[i]) if Y_accuracy == T_accuracy: correct_img = np.vstack((correct_img,X_test[i])) else: error_img = np.vstack((error_img,X_test[i]))
いちおう、計算時間を測定するコードも添えておきます。
end_time = time.time()
最後に評価結果を出力するprintを書いておきます。
print(correct_img.shape) print(error_img.shape) print(100*correct_img.shape[0]/num_of_test) print(end_time - start_time)
今回も、
・正解画像枚数
・不正解画像枚数
・正解率
・計算時間
を出力させます。
これでコードが完成です。
プログラム実行
ではプログラムを走らせてみます。
(8708, 784) #正解枚数 (1292, 784) #不正解枚数 87.08 #正解率 196.74863386154175 #計算時間
相変わらずそこそこ計算時間がかかります。エンター押したら死んだ魚の目で正座待機です。
10000枚中、正解枚数は8708枚ということで、前回より若干良くなった気がしますが、ほとんど同じです。プログラム(アルゴリズム)自体は基礎問題で何度もトライした内容ですし、間違ってはいなさそう。前回もちらっと書きましたが、学習回数が足りてないのでは説があります。
ということで、学習(イテレーション)回数を変化させて正解率がどう変化するかこちらで検証してみました。
まとめ
今回は隠れ層を追加したシンプルなニューラルネットワークがどれほどの正解率を出すのか検証してみました。結果については考察の余地が残っているのでなんとも言えないところですが、ひとまずアルゴリズムとコードは書き上げることができました。
次回からはCNN(コンボリューション・ニューラルネットワーク)というものにチャレンジしていこうと思います。しかしながらこの手法、画像を画像のままニューラルネットワークに突っ込むという、けっこう複雑なアルゴリズムでできているので、コードを書くのもひと苦労ですし、愚直に書いているとミスを連発してしまいます。そこで、具体的にトライしていく前にいくつかのアルゴリズムを関数化して、外部ライブラリで保管して使えるようにしたいと思います。
このあたりのネタをコラム的にしばらくまとめていくことになりそうです。
アメリカの生活(仕事)がけっこう忙しくなってきて、ディープラーニングに割く時間が削られてきている現状なんですが、まぁマイペースで進めていきたい今日このごろです。
全体コード
github.com