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層でもそれ以上でも、同じようにやってくれる。試してないが、はずである。