JDeeplearningのプログラム概要(2)

プログラム本体こちら https://github.com/toyowa/jautoencoder
ここでは、JDeeplearningの個々のクラスの説明をしておく。クラス内の主なメソッド(メンバー関数)の説明をする。
<JDeepLearningクラス>
メインクラスだ。mainのスタティック関数が入っている。
public static void main(String[] argv)
冒頭で、自分、JDeepLearningクラスのインスタンスdplを作成する。
何よりも、プログラムを開始する関数がだ、ひたすらオプションの処理をしている。オプション処理後に、
dpl.dataProc.setTrainData(dpl.dataFileName);
で、データを読み込んでいる。
そして、オートエンコーダーなら
dpl.execAutoencoder(topology);
普通のニューラルネットならば、
dpl.execNeuralnet(topology);
という、自分のクラスのメソドを呼び出している。
void execAutoencoder(int[] topology)
オートエンコーダーの実行関数である。(1)で説明したように、隠れ層ごとのサブネットワークを形成する。形成してから、
execNeuralnet(topology)
を呼び出して、バックプロパゲーションでウェイトを作成し、隠れ層ごとにこれを繰り返して、最後は最終層へのウェイトをランダムに作成した、これまでのウェイトと合同させて、ファインチューニングのオリジナルなネットワークを形成して、
execNeuralnet(topology);
に渡している。
ここで、
static double[][][] reserved_weights;
というウェイトを保持するスタティックな3次元配列reserved_weightsが重要な役割を果たしている。第1次元は、レイヤー番号で、0番が第1レイヤーであることに注意してほしい。0番レイヤーには入力ウェイトがないから無駄にならないようにそうしている。第2次元は、そのレイヤーのニューロン番号を指定し、第3次元は、その中のウェイト番号である。配列をメモリー状にインスタンス化する時、ウェイト数は、ニューロンごと(レイーヤー数に基づいて)に異なっているのだが、各レイヤーのニューロン数の最大数で作ることにしている。そして、この配列にデータがあると、Neuronクラスがインスタンス化されるときに、ウェイトの初期値をランダムな数ではなく、ここにある値で初期化されるように作られている。
だから、実行オプションの-weightsでウェイト配列が指定されると、そのウェイトがこのスタティックな、reserved_weightsに保持され、自動的にNeuronクラスのインスタンス化の時にセットされるのだ。
void execNeuralnet(int[] topology)
ニューラルネットワークを起動する。基本、順方向に出力を作る作業と逆方向に、バックプロパゲーションを実行するという、二つの作業をやっている。テストの時は、順方向の作業しかしない。
冒頭で、関数引数のtopologyに基づいて、ニューラルネットワークを作り上げる。具体的には、のちに説明するNetクラスを作り、その中に、Layerクラスで、レイヤーを作り、さらにその中で、Neuronクラスで
ニューロンを作成するという入れ子構造でネットワークを作成している。つまり、Netのインスタンスの中にレイヤーの数だけLayerインスタンスを作り、各レイヤーの中に必要なニューロン数のNeuronインスタンスを作るわけである。そのかくNeuronインスタンスがウェイトを保持している。
オートエンコーダーで呼び出される場合は、出力の教師学習データが、入力データと同じなので、その辺りの場合分けがされている。
<Netクラス>
Netクラスは、ネットワークの構造と、内部機能を実現するメソッドを保持している。
メンバー変数は、
double [] outputs;
Layer [] layers;
int [] topology;
double adjusting;
で、最初が、出力値を保存している変数、次が、ネットワーク内のレイヤーを保持している変数、topologyは、Netクラスがインスタンス化される過程で与えられるネットワーク構造を保持している、最後のadjustingがバックプロパゲーションでウェイトを調整する係数なのだが、ほとんど、0.15のままにしていて、変更の気持ちがなかったので、このクラスのコンストラクタ内で値を与えて終わりにしていたが、これも実行時オプションで指定できるようにすべきだ。次の改訂ではそうしようと思う。
String makeAutoencoderData(Data dataProc)
オートエンコーダーで次の隠れ層のウェイト形成のための入力データを作成する。つまり、前の隠れ層の時の第1層のウェイトを使ってその前の入力データから出力データを作成するわけである。
tmpout_時間.txt
というファイル名にして保存している。
String printAllWeights(int iteration, String dataFileName)
ネットワークの全ウェイトを出力する。ファイル名は、
weight_時間.wgt
である。ここのレイヤーの出力、ニューロンの出力はそれぞれのクラスに下請けに出す。
void adjustingPreLayerWeightsOut(int layerNo)
ニューロンは、そのニューロンへの入力側のニューロンからの結合ウェイトと、出力側へのウェイトの両方を保持することになっている。なぜそうしているのかといえば、順方向への出力値の計算は、入力側のウェイトデータが必要で、誤差逆伝搬の計算においては、出力側のウェイトが必要だからである。ニューロンに関わる計算はニューロンクラスのメソッドで行われる。ただ、この入力側ウェイトと出力側ウェイトは、あるニューロンの出力ウェイトは次のレイヤーのニューロンの入力ウェイトになるので、両者は同じものでなければならない。それは、ここのニューロン内の計算でもダメで、レイヤーもまたがるので、このNetクラス内のメソドで整合性を取るようにしているのだ。
これを書いている時に改めても直したら、余計な場所で使われているの気づいて直した(このメモのおかげでバグフィックスできた!)。実質計算に影響はないが、微妙に速さが変わったかもしれない。試してみたが、計算結果には影響なく、速さの変化はわからない。
ただ、一つ大事なことを書いておくと、この、Deltaに基づくウェイトの訂正は、ネットワークの全てのDeltaを計算し終えてからにしなければならない。従ってその後に、ウェイトの整合性は図られる。
void getForwardOutput(double [] initVal)
ウェイトに基づく、順方向の出力値の計算をネットワーク全体で実施する。レイヤー、ニューロンに実際の計算は下請けさせている。
void execBackpropagation(double [] deltaE)
誤差逆伝搬の計算。実際の計算は、下請けに任せている。
<Layerクラス>
メンバー変数は以下の通りである。
Neuron[] neurons;
double[] layerOutput;
double[] layerDelta;
double adjusting;
int layerNo;
レイヤークラスは、ネットワーククラスとニューロンクラスの間を取り持っている感じで、大事な計算はあまりない。省略。
<Neuronクラス>
ニューロンクラスのメンバー変数は、
int neuronNo;
int layerNo;
double adjusting;
boolean inputNeuron = false;
boolean outputNeuron = false;
double [] weightsIn;
double [] weightsOut;
double [] prev_output;
double output;
double delta;
double value;
である。
コンストラクタで、ウェイトを組み込むのが大事な作業だ。JDeepLearningのクラスのところでで書いたが
JDeepLearning.reserved_weightsが入っていれば、それをウェイトに組み込み、なければ乱数で作成する。
乱数で作成する時に、単に0から1までの間の数字にすると、出力値が大きくなりすぎてダメになるので、その乱数を入力側のウェイト数で割って正規化する。これで、改善された。
weightsIn[i] = Math.random() / (double) inputNum;
となっている。
void getNeuronDelta(double[] prev_delta)
バックプロパゲーションでいうDelta値をこのニューロンについて計算する。入力値の合計valueで、シグモイド関数の微分値の値を計算する必要があるのだが、

delta = 0.0;
for (int i = 0; i < prev_delta.length; i++) {
//前の層のデルタに、そのニューロンとのウェイトをかけたもの
   delta += prev_delta[i] * weightsOut[i];
}
double ev = Math.exp(-value);
delta = delta * (ev / ((1 + ev) * (1 + ev)));

で、計算している。
void getNeuronOutput(double [] prev_output)
順方向の計算を行う関数だ。
<Dataクラス>
ファイルの入出力を担う関数だが、特別なことはないので省略する。データがどのように保存され読み込まれるのかが見れば分かるはずである。

JDeeplearningのプログラム概要(1)

https://github.com/toyowa/jautoencoder
で公開しているプログラムの覚書を書いておこうと思う。これから、ロボットのセンサーを増やす作業に入るので、どういうプログラムだったか忘れてしまいそうなので。この(1)では、全体的なことを書いておく。
1。データに関して
データのフォーマットは、
https://github.com/huangzehao/SimpleNeuralNetwork
のサイトのプログラムを参考にさせてもらった。(プログラムは、独立に私が自分で書いたものだ。プログラムは参考にするほど理解できなかったが、自分で書かないと使えないだろうと思ったから)
データの最初の1行は、
topology: 784 400 10
のようにネットワーク構造を書く。ここで、784が入力レイヤーのニューロン数。以下出力ユニットまで、にゅう論数を書いていく。レイヤーの数にもニューロン数にも制限はない。
その後に続いて、in:とout:の接頭語に続いて、交互に、入力データと出力データを空白を区切って書いていく。
MNISTの手書き数字データに関しては、データの読み取りは、
http://nonbiri-tereka.hatenablog.com/entry/2014/09/18/100439
を参考にさせていただいて、それを上記のデータフォーマットに書き直したものだ。
https://github.com/toyowa/jautoencoder/tree/master/MNIST
に変換用のプログラムをおいてある。
データプログラムは、実行時オプション -dataで指定できる。
2。ウェイトファイル
学習後にウェイトデータを吐き出し、テストの時はそのファイルを実行時オプション -weights で指定して読み込むことになる。ただし、ウェイトファイル名は、wgtのさフィックスがないと拒否するようにしている。データと間違わないように。データのさフィックスは特にチェックせずに読み込む。
吐き出されたウェイトファイル名には、そのファイルが作成された日時が秒までつくので、識別できると思う。ウェイトファイルの頭の部分に、その学習のネットワーク構造やパラメータや、繰り返し数などが書かれている。
-------------------
File name: [ weight_170603192205.wgt ]
Iteration: 179997
InputData: trainingData.txt
Topology: 784 300 150 10
Adjusting: 0.15
Label: Pre_neuron No. ->  Layer No. : Neuron No. = Weight
Weight: 0 -> 1 : 0 =    0.0003876341
Weight: 1 -> 1 : 0 =    0.0011203298
Weight: 2 -> 1 : 0 =    0.0004425993
Weight: 3 -> 1 : 0 =    0.0007930749
...........
...........
---------------------------------
なお、テストじゃない時にウェイトファイルを指定すると、そのウェイトを読み込んで、そこに書かれているそれまでの学習の実行の続きをやることになる。何回かに分けてやりたいとか、中間状態を見たい時には、そのような方法もできる。プログラムのデバッグの時は、一回だけ学習させるということもやった。実行時ぷションの、-maxiterで1を指定すると一回だけやる。
なお、-maxiterを指定しないと、データがある限り学習を続ける。
3。テスト
テストの時は、できたウェイトファイルとテスト用データの両方を実行時オプションで指定して、-test オプションを付け加えれば良い。当たり前だが、ウェイトファイルとテスト用データは、ネットワーク構造に関して整合的でなければならない。-weightsでウェイトを指定しないと、当然だが、ランダムに与えたウェイトでテストしてしまうので、テストは無意味だ。チェックして排除するようにすべきだったかもしれない。テスト結果はコンソールに出力される。
4。オートエンコーダー(Autoencoder):深層学習
実行時オプションで -auto をつけると、オートエンコーダーとバックプロパゲーションで学習する。オートエンコーダーとは、隠れ層へのウェイトを1層ずつ、一つの圧縮符号化器のように作っていくことだ。例えば、MNISTで、入力784ニューロン、300と150の隠れ層、出力層が10ニューロンだとしよう。それぞれのレイヤー(層)にA,B,C,Dという名前をつけよう。
まず、ABにA'というAと同じニューロン数のレイヤーをつけて、A'の学習データとして、Aと同じものを使うという作業をする。そして、ウェイトをバックプロパゲーションで形成するという作業をする。一見無意味なようだが、よく考えてみると重要な意味を持っている。AがA'で再現されるんだが、BはAよりもニューロン数が少ない。それが再現されるということは、Bの出力は、Aから入力されるデータの特色をすでになんらかの形で組み込んでいるということである。つまり、ニュー力データだけで、すでに学習してしまっているのである。これでAからBへのウェイトをまず作る。次にそこで最終的に得た出力を利用して、BCB'というネットワークを作成し、その先のネットワーク出力で、自己学習させるのである。Bの入力と、学習用のB'のデータはまた同じである。もちろんニューロン数も同じである。そうすると、BC間のウェイトがまたCで特徴が凝縮されるように形成される。最終的にこのようにできたAB間のウェイトBC間のウェイトを使って、元のABCDのネットワークを元々の学習用出力で学習させる。この時,CD間のウェイトは、私の場合、ランダムに作成したものにしているが、正解かどうかの確信はない。でも多分正しいだろう。これが、オートエンコーダーである。
少なくとも、MNISTのデータについては、うまく機能している。
-autoを指定すると、元のネットワークトポロジーが、5層でもそれ以上でも、同じようにやってくれる。試してないが、はずである。

ディープラーニング(Autoencoder)のJavaプログラムをMNISTテストする

ニューラルネットのC++プログラムをJAVAに書き換えたら、すさまじく速くなったということは先に書いた。
これでディープラーニングの入り口である、Autoencoder(自動符号化器)のプログラムを作成した。バグもほぼ取れているような感じなので、
https://github.com/toyowa/jautoencoder
に公開している。
MNISTデータは、入力は、28X28=784ニューロンで、出力は、数字ラベルの10ニューロン。そこで、隠れ層を、300ニューロンと150ニューロン挟んで、4層にした。これまでと同様に、MNISTの6万個の手書き数字データと10000個のテストデータを実行した。
結果は、
<正解数 = 9413 不正解数 = 587 正解率 = 0.9413>
だ。同じプログラムで、改めて実施した、隠れ層400ニューロンだけの、Autoencoder抜きの結果は、
<正解数 = 9310 不正解数 = 690 正解率 = 0.931  >
なので、明らかに、正解率は上昇した。
しかも、隠れ層が300-150で、Autoencoderなしにニューラルネットを実施した場合は、全くダメ、というか収束しないので、Autoencoderの効果は確かめられた。
正解率のヒストグラムは、以下のようなものである。400の方が、0.999以上の頻度は多いのだが、総合的パフォーマンスは300-150のAutoencoderの方が高い。理論的に予測されているように、Autoencoderを入れた方が、柔軟に認識ている感じだ。

ニューラルネット、JAVAが速すぎる

半日かけて、ニューラルネットのプログラムをC++から、JAVAに書き換えた。
驚いた。学習のスピードが100倍以上速くなった。私のC++のプログラムが遅すぎるのか。書き方がおかしいのか。いや、そういうレベルの問題ではないくらいに、速くなった。

ニューラルネット、Javaで書き直す

ここまでC++でやっておいてなんだが、プログラム全体をJavaで書き直そうかと思っている。C++のスレッドが、なんだかうまく動かない。ニューラルネットに並列処理は不可欠なのだが、C++ではダメなような気がしてきた。
Javaも十分早くなったし、Javaでニューラルネットのパッケージも作られているくらいだから、RaspberryPIでもjavaは使えるので。そうなると、ロボットのコントローラー全部をjavaにしてしまうかもしれない。

自己符号化器, Autoencoderをニューラルネットに組み込む

スレッド問題は保留にして、ニューラルネットワークが、ほぼ予定通り機能しているような感じだから、ディープラーニングに進む。
画像認識ではなく、ロボットの動作制御に組み込むことを想定すると、いかに数少ないデータで効率の良い判断力をつけるかが問題となる。そういう点では、まず、基本的な自己符号化器を扱えるようにした方が良いと感じた。
自己符号化器は、元々の入力データが持っている特徴を際立たせる事前作業を行うことで、階層が深くなってもその力を活かせるようにしている。プログラミングとしては、ニューラルネット、バックプロパゲーションが組み込まれていれば、拡張は容易だ。
ただ、世間では、あまりこの自己符号化器は使われなくなっているようだが、目的に依存するだろう。

隠れ層のニューロンを400にしたら正解率が落ちた(笑)

MNISTを利用した他の研究を見ていると、私のように隠れ層が100ニューロンというのは例外的に小さい。そこで、隠れ層を400にして60000個の学習データでウェイトを学習させ、同じように10000個のテストをやって見た。
結果、
<正解数 = 9201 不正解数 = 799 正解率 = 0.9201>
で、正解率が1%落ちた(笑)
ただ、正解の時に、どのようなユニット出力になっているかを見ると、さすが隠れ層を増やした結果だなと思わせる。

図を見てもらえば明らかなように、先の2つのケースに比べて、その数字のユニット出力が0.999以上、つまり、ほぼ1になっているのだ。認識した数字に対する確信が抜群に高いということだ。「これは間違いなく3だ」とか、ネットワークが言い放っているような感じだ。そのためか、若干汚い手書き文字に対して、厳密な態度を取っている「こんな数字4じゃないだろう!」みたいな上から目線といっても良い。しかし、認識率もそれほど落ちたわけではない。ただ、上昇しなかったのが悔しいだけだ。

ニューラルネットワークのC++プログラムの公開

作成したニューラルネットワークのC++プログラムを、
https://github.com/toyowa/neuralnet
に公開した。
Gnuのg++用のプログラムで、他では確かめていない。
ネットワーク全体は、Netクラスに、その層(レイヤー)はLayerクラスに、レイヤーの内容であるニューロンは、Neuronクラスになっている。それぞれ、オブジェクト化されて使われる。
細かい説明は、気が向いたらしよう。プログラムを使えるくらいの人は、見ればわかるだろうと思う。
余談だが、私は、このプログラムも含め、C++、JAVA、JAVASCRIPT、Html、PHPなどの開発は、全てNetbeans上でやっている。Netbeansは、最高の開発環境だと思っている。
このニューラルネットワークとバックプロパゲーションは、30年近く前に一度やっていたことなのだ。大きく変わったのは、コンピュータの凄まじい高速化だ。

MNIST手書き数字データで93%の識別率

作成した汎用ニューラルネットワークで、その辺りのパフォーマンスを図る標準データとなっているMNISTの手書き数字データをテストしてみた。
MNISTについては、
http://yann.lecun.com/exdb/mnist/
にデータそのものと解説がある。
手書き数字は、
こんな感じのもので、数字の一つ一つがデータ化されている。1ピクセルが1バイト(0-255)の値が与えられ、1文字、28X28ピクセルからできている。
データ数は、60000文字の学習用データと10000文字のテスト用データがある。それぞれ、ピクセルデータとそれが幾つの数字を表しているかというラベルデータがある。60000字でニューラルネットを学習させ、ネットワークウェイトを作成し、そのウェイトが、テスト用10000字を正しく認識するかどうかを調べるのである。
ニューラルネットワークは、入力レイヤーが、28X28の784ニューロン、隠れ層(中間レイヤー)は100ニューロン、出力は0から9までの値を出すので、10ニューロンにした。出力は、ネットワークがその画像について判定した値のニューロンだけが発火する(値1になる)ことを見越しているわけである。

78万4千個のウェイトからなる、従って、訓練手法であるバックプロパゲーション(誤差逆伝搬)で、6万個の文字について、毎回これだけのウェイトを微調整しながら最終的に望ましいウェイトを見つけるわけであるから、相当大きめのネットワークである。
データについては、それぞれのピクセルの値を、正ならば1層でないならば0に、ビット化したものと、(0-255)それぞれの値を0から1の間の数に正規化したものと2種類用意した。元のデータのままをネットワークに入れたら、途中で破綻している。正規化したものに全ての情報が入っているので、生データを使う必要はない。
予測的には、正規化して0から1の間の数字にした方が、情報を多く持っているので、良いパフォーマンスを示すのではないかと思われた。0と1にビット化すれば、情報を単純なものにしてしまうのだから。
学習に、3Gヘルツ、8コアの最高スペックのMac Proでも1時間以上かかった。と言ってもこのマックは16スレッド動かせるのだが、プログラムそのものがほとんど1スレッドで動かしているので、相当無駄にしているのだが。逆に、同時にいくつもの学習を同時にさせることはできる。
10000個のテスト用データのテスト結果は、以下のようである。
<正規化(0.0-1.0の間の値)されたデータを用いた場合>
正解数 = 9283 不正解数 = 717 正解率 = 0.9283
<ビット化(0.0か01.0の値)されたデータを用いた場合>
正解数 = 9300 不正解数 = 700 正解率 = 0.93
微妙に正解率がビット化した方がいい。誤差の範囲といってもいいが、何よりも、ビット化して情報を削ったにもかかわらず、正規化したものに匹敵するパフォーマンスを出していることが驚きである。MNISTに掲載されているパフォーマンスと比べるとやや低いが、何の微調整もしていない、ただ作成したものでいきなりテストしただけで、これだけのパフォーマンスを出せれば、私としては合格だ。
正解率は、最大出力を出したユニット番号が、その画像の数字に一致する場合に正解としているのだ、これだけでは、どこまで明確にその数字と判断しているのかがわかりにくい。そこで、正解の出力ユニットがどれくらいの値を出したのかをヒストグラムで示す。基本的にユニットは0から1の間の数字しか出さず、通常、関係ないときはほぼ0に近い値しか出さないことに注意されたい。
つまり、そのユニットが正解だという場合、ほぼ0.9以上の値を突然、出している(ニューロンが発火している)ということだ。このグラフを見ると、このヒストグラムで示されたパフォーマンスは、正規化されたデータの方が、より1に近い数字で、ヒストグラムがより高く立っているので、パフォーマンスが良いといってもいい。ビット化されたデータをそれよりやや低いところで、パフォーマンスを稼いでいる。
次は、ディープラーニングを組み込む。