今回の目標
前回作成した層を使って、ネットワーククラスを作成します。
ここから本編
修正点
Sigmoid.java
シグモイド関数の計算が間違っていました。
今までは以下のように計算していましたが、
$$y = \frac{1}{1+e^x}$$
正しくは
$$y = \frac{1}{1+e^{-x}}$$
です。
プログラムも修正しました。
というか「$$」で囲むことで $\LaTeX$ 使えるんですね知りませんでした。
Layer.java
空のコンストラクタを追加しました。
また、ネットワーククラスで用いるためのコンストラクタを新しく用意しました。こちらではノードのインスタンス化を行っていません。
/**
* Constructor for this class.
*/
public Layer(){
;
}
/**
* Constructor to call from the Network class.
* @param nodes_num number of nodes for this node.
* @param type type of activation function for this node.
*/
public Layer(int nodes_num, AF type){
this.nodes_num = nodes_num;
this.nodes = new Node[this.nodes_num];
this.AF = type;
}
ネットワーククラスで用いるため、順方向計算を仮に定義しました。
実際は使いません。
/**
* Doing forward propagation.
* In this class, nothing to do.
* @param in input matrix.
* @return Matrix instance of output.
*/
public Matrix forward(Matrix in){
return in;
}
cloneを追加。
@Override
public Layer clone(){
Layer rtn = new Layer(this.nodes_num, this.AF);
return rtn;
}
forwardメソッド以外は、他の層クラスにも追加しました。
Input.java
toStringを少し変更しました。
これはInputクラスだけに限らず、Output、Denseにも同じような変更をしました。
@Override
public String toString(){
String str = "Input\t";
str += String.format("nodes num: %d", this.nodes_num);
return str;
}
Node.java
今回の内容とは関係ありませんが、toStringを若干変更しました。
@Override
public String toString(){
String rtn = String.format("input: %d\n", this.in - 1);
rtn += String.format("activation function: %s", this.function.toString());
return rtn;
}
ディレクトリ構成
MyNet
├── layer // 層のパッケージ(すでに作成)
├── matrix // 行列のパッケージ(すでに作成)
├── network // ネットワークのパッケージ(今回作成)
├── nodes // ノードのパッケージ(すでに作成)
│ └── out_function // 活性化関数のパッケージ(すでに作成)
└── optimzer // 最適化関数のパッケージ(今は空)
Network.java
前回作成した層をコンストラクタの引数にとり、ネットワークを作成するクラスです。
インスタンス化としてはchainerやkerasに近い書き方になります。
package network;
import nodes.*;
import nodes.out_function.*;
import layer.*;
import matrix.*;
コンストラクタ
層をインスタンス化するときに一番面倒と感じたのは、ノード数と入力数をいちいち指定しなければいけないところです。
入力層以外でかつ全結合層なら、前の層のノード数が決まれば必然的に現在の層の入力数も決まります。なので、ネットワーククラスのコンストラクタに渡すのは、
- 入力層への入力数(説明変数の数)
- ノード数と活性化関数のみ指定した層のインスタンス配列
の二つだけにしたいと考えました。
ノードのインスタンス化の際は入力数の情報が必要となるので、層オブジェクトをネットワークのコンストラクタに渡す際にはまだインスタンス化したくない(というより、前の層の状態が不明である限り不可能)。
そういった理由で、上述部分で追加した層のコンストラクタを使用し、ノードがインスタンス化されていない状態の層オブジェクトを受け取ります。
そしてネットワーククラスのコンストラクタで、各層のノードをインスタンシエーションします。各層の入力数が分かっている状態ですので問題ありません。
また、コンストラクタを間違えた時用のエラー処理を施しました。
まだノードがインスタンス化されていないよね、ということを確認しています。
ネットワーククラスのインスタンシエーションはどうせ1プログラムで1回くらいしかしないだろうし、多少遅くなっても問題ないはずです。
public class Network {
/** Layer array for this network. */
public Layer[] layers;
/**
* Constructor for this class.
* @param input_num number of input.
* @param layers Each layers.
*/
public Network(int input_num, Layer ... layers){
for (int i = 0; i < layers.length; i++){
if (layers[i].nodes[0] != null){
System.out.println("Wrong constractor.");
System.exit(-1);
}
}
this.layers = new Layer[layers.length];
for (int i = 0; i < this.layers.length; i++){
this.layers[i] = layers[i].clone();
}
for (int i = 0; i < this.layers.length; i++){
AF type = this.layers[i].AF;
for (int j = 0; j < this.layers[i].nodes.length; j++){
this.layers[i].nodes[j] = new Node(input_num, type);
}
input_num = this.layers[i].nodes_num;
}
}
forward
順方向計算。各層での計算結果の行数と列数は移り変わるのでこういう形をとりました。
/**
* Doing forward propagation.
* @param in input matrix.
* @return Matrix instance of output.
*/
public Matrix forward(Matrix in){
Matrix rtn = in.clone();
for (int i = 0; i < this.layers.length; i++){
rtn = this.layers[i].forward(rtn);
// System.out.println(rtn);
}
return rtn;
}
toString
入力数と各層の情報。
@Override
public String toString(){
String rtn = String.format("input: %d\n", this.layers[0].nodes[0].in);
for (int i = 0; i < this.layers.length; i++){
rtn += "--------------------------------\n";
rtn += this.layers[i].toString();
rtn += "\n";
}
rtn += "--------------------------------\n";
return rtn;
}
summary
toStringで事足りるんですが、kerasっぽいほうが分かりやすいかなと思い実装しました。
あまり意味はないかもしれません。
/**
* Call toString method.
* @return Summary of this network.
*/
public String summary(){
return this.toString();
}
test.java
ネットワークを実際に動かしてみます。
説明変数の数は10で、3層のネットワーク。
ノード数はそれぞれ2、4、4。
活性化関数はそれぞれReLu、Sigmoid、Tanh。
順方向計算を行い結果を出力します。
なおこのプログラム実行の際、途中経過もコンソール出力するようにしています。
import matrix.Matrix;
import nodes.out_function.*;
import layer.*;
import network.*;
public class test {
public static void main(String[] str){
Network net = new Network(
10,
new Input(2, AF.RELU),
new Dense(4, AF.SIGMOID),
new Output(4, AF.TANH)
);
System.out.println(net.summary());
Matrix in = new Matrix(new double[5][10]);
for (int i = 0; i < in.row; i++){
for (int j = 0; j < in.col; j++){
in.matrix[i][j] = (double)i*2 + (double)j/2;
}
}
System.out.println(in);
Matrix out = net.forward(in);
System.out.println(out);
}
}
今回の入力行列はこちら。
この行列の末尾にバイアス用の1をつけ、入力層の各ノードが持つ重み行列と掛け算を行います。
入力層はノード数2の入力数11(説明変数の数+バイアスの数)ですので、重み行列は11行2列となります。
入力層の活性化関数はReLuなのでそれがこのまま入力層の出力となります。
(※ 実際は各ノード内で11行1列の重み行列と掛け算したのち、それぞれの結果を横方向に結合したものが入力層の出力となります)
これを中間層で、再度似たような計算を行います。
中間層はノード数4で入力数3ですので重み行列は3行4列です。
活性化関数(シグモイド)をかけます。
出力層で似たような計算を行います。
プログラムの実行結果はこちら。
ネットワークの概要を出力後、入力行列を出力し、各層の計算結果出力、最後に全体の結果を出力しているので出力層の計算結果が二回出力されています。
input: 11
--------------------------------
Input nodes num: 2
--------------------------------
Dense nodes num: 4
--------------------------------
Output nodes num: 4
--------------------------------
[[0.0000 0.5000 1.0000 1.5000 2.0000 2.5000 3.0000 3.5000 4.0000 4.5000 ]
[2.0000 2.5000 3.0000 3.5000 4.0000 4.5000 5.0000 5.5000 6.0000 6.5000 ]
[4.0000 4.5000 5.0000 5.5000 6.0000 6.5000 7.0000 7.5000 8.0000 8.5000 ]
[6.0000 6.5000 7.0000 7.5000 8.0000 8.5000 9.0000 9.5000 10.0000 10.5000 ]
[8.0000 8.5000 9.0000 9.5000 10.0000 10.5000 11.0000 11.5000 12.0000 12.5000 ]]
[[23.5000 23.5000 ]
[43.5000 43.5000 ]
[63.5000 63.5000 ]
[83.5000 83.5000 ]
[103.5000 103.5000 ]]
[[1.0000 1.0000 1.0000 1.0000 ]
[1.0000 1.0000 1.0000 1.0000 ]
[1.0000 1.0000 1.0000 1.0000 ]
[1.0000 1.0000 1.0000 1.0000 ]
[1.0000 1.0000 1.0000 1.0000 ]]
[[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]]
[[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]
[0.9999 0.9999 0.9999 0.9999 ]]
Excelの結果と見比べると、同じになっているのが分かると思います。
次回は
出力結果を正解と比較し誤差の計算を行うメソッドを追加しようと思います。
どのような形にするかはまだ考え中です。