今回の目標
39 ~ライブラリ、ノード作成~に引き続き、層の作成を行います。
ここから本編
層作成の前に、39 ~ライブラリ、ノード作成~で作成した各クラスにtoStringメソッドを定義しました。
Nodeクラスの表記で入力数が1引かれているのは、this.inにはバイアスも含まれているからです。
// 活性化関数クラス(名前を返すだけ)
@Override
public String toString(){
return "";
}
// Nodeクラス
@Override
public String toString(){
String rtn = String.format("input: %d\n", this.in - 1);
rtn += String.format("output: %d\n", this.out);
rtn += String.format("activation function: %s", this.function.toString());
return rtn;
}
また、ノードや活性化関数にMatrixを適用しました。
詳しくは書きませんが、大体以下の通りです。
// ノードのコンストラクタ(fillは行列をその数字で埋める返り値なしのメソッド)
/**
* Constructor for this class.
* Number of inputs includes bias.
* @param input_num Number of inputs.
* @param output_num Number of outputs.
* @param type Type of activation function.
* @exception System.exit The specified activation function
* does not exist or misspecified.
*/
public Node(int input_num, int output_num, AF type){
in += input_num;
out = output_num;
w = new Matrix(new double[in][1]);
w.fill(1.0);
output = new Matrix(new double[output_num][1]);
switch (type) {
case RELU:
function = new ReLu();
break;
case SIGMOID:
function = new Sigmoid();
break;
case TANH:
function = new Tanh();
break;
default:
System.out.println("ERROR: The specified activation function is wrong");
System.exit(-1);
}
}
// ノードの順方向計算
/**
* Doing forward propagation.
* @param input Array of inputs.
* @return output array's element.
*/
public Matrix forward(Matrix input){
if (input.col != w.row){
System.out.println("node forward error");
System.exit(-1);
}
Matrix rtn = Matrix.dot(input, w);
return function.calcurate(rtn);
}
// 活性化関数の計算(これはReLu)
/**
* Actiation function execution.
* @param matrix linear transformationed matrix.
* @return output matrix.
*/
@Override
public Matrix calcurate(Matrix matrix){
Matrix rtn = matrix.clone();
for (int i = 0; i < rtn.row; i++){
for (int j = 0; j < rtn.col; j++){
rtn.matrix[i][j] = Math.max(0.0, matrix.matrix[i][j]);
}
}
return rtn;
}
Matrixクラスの内積計算にミスがありましたので修正しました。
/**
* Dot product for two matrices.
* @param a Matrix to be dot producted.
* @param b Matrix to dot product.
* @return New dot producted Matrix instance.
*/
public static Matrix dot(Matrix a, Matrix b){
if (a.col != b.row){
System.out.println("dot producting error");
System.exit(-1);
}
Matrix rtn = new Matrix(new double[a.row][b.col]);
double num = 0;
for (int i = 0; i < a.row; i++){
for (int j = 0; j < b.col; j++){
num = 0;
for (int k = 0; k < a.col; k++){
num += a.matrix[i][k] * b.matrix[k][j];
}
rtn.matrix[i][j] = num;
}
}
return rtn;
}
また、Matrixクラスに列追加メソッドを追加しました。
/**
* Append a number to the side of matrix.
* @param matrix Matrix to be appended.
* @param num number to append.
* @return A Matrix instance appended number.
*/
public static Matrix appendCol(Matrix matrix, double num){
Matrix rtn = new Matrix(new double[matrix.row][matrix.col+1]);
for (int i = 0; i < matrix.row; i++){
for (int j = 0; j < matrix.col+1; j++){
if (j != matrix.col){
rtn.matrix[i][j] = matrix.matrix[i][j];
}else{
rtn.matrix[i][j] = num;
}
}
}
return rtn;
}
また、Matrixクラスに横方向行列結合メソッドを追加しました。
これは1列以外の行列を受け付けません。
本当は入力全ての列数を調べたかったですが、for文回してると遅くなりそうなので止めました。
/**
* Stack matrices horizontally.
* @param matrices Matrices to stack.
* These should not have more than two columns.
* @return New Matrix instance stacked.
*/
public static Matrix hstack(Matrix ... matrices){
Matrix rtn = new Matrix(new double[matrices[0].row][matrices.length]);
for (int i = 0; i < rtn.row; i++){
for (int j = 0; j < rtn.col; j++){
rtn.matrix[i][j] = matrices[j].matrix[i][0];
}
}
return rtn;
}
また、Nodeクラスで出力数の定義は必要ないことに気づいたので消しました。
怒涛の修正でしたが、今後もこういうことばかりになると思います。
ディレクトリ構成
前回と全く同じです。
MyNet
├── layer // 層のパッケージ(今回作成)
├── matrix // 行列のパッケージ(前回作成)
├── nodes // ノードのパッケージ(前々回作成)
│ └── out_function // 活性化関数のパッケージ(前々回作成)
└── optimzer // 最適化関数のパッケージ(今は空)
層の定義
このシリーズでは層を、「ノードが一列に並んだもの」と定義します。
つまり、以下のようなネットワークがあった場合、これを「3層のネットワーク」と呼びます(線は省略して書いています)。
計算方法
計算方法について、備忘録も兼ねて書いておきます。
まず入力は二次元行列で、行が各データ、列が各パラメータです。
例えば以下のようになります。なお、値は全てこの時ぱっと思いついたものです。
これを入力層に入力します。
入力されると、右端に1が追加されます。これはバイアス用の仮の数です。これに変数をかけることでバイアスを調整します。その変数は重み行列内に入っています。
入力層の各ノードで重みとの内積が行われます。重みはn行1列で、nはパラメータ数+1、この場合5です。バイアスも重み行列に含めています。その後活性化関数を使いますが、ここではReLuということにして特に意識しないことにします。
ノードの出力は重み同様n行1列です。
この縦ベクトルのようなものがノードの数だけ出力されますので、それらを横方向に連結させます。
仮に入力層のすべてのノードの重みが1だった時、入力層の出力は以下のようになります。
これが次の層の入力になります。
これを繰り返すことで計算を行います。
層(親クラス)
指定した数のノードを用意して、各々のノードで指定した入力数ぶんの重み行列を用意します。
ここでは初期値は1です。
package layer;
import nodes.*;
import nodes.out_function.*;
/**
* Class for layer.
*/
public class Layer {
public Node[] nodes;
int nodes_num;
/**
* Constructor for this class.
* @param nodes_num
*/
public Layer(int nodes_num, int input_num, AF type){
this.nodes_num = nodes_num;
this.nodes = new Node[this.nodes_num];
for (int i = 0; i < this.nodes_num; i++){
nodes[i] = new Node(input_num, type);
}
}
@Override
public String toString(){
String str = String.format("nodes num: %d", this.nodes_num);
return str;
}
}
入力層
入力層を、本当に入力だけで定義している文献もありますが、ここではノードありで定義しています。
順方向計算を、上述した計算方法通りに実装しています。
入力行列にバイアス用の1を連結し、Nodeクラスで定義している順方向計算を行い、最後にそれぞれのノードの出力を連結して返しています。
package layer;
import nodes.out_function.*;
import matrix.*;
/**
* Class for input layer.
*/
public class Input extends Layer{
/**
* Constructor for this class.
* @param nodes_num
*/
public Input(int nodes_num, int input_num, AF type){
super(nodes_num, input_num, type);
}
/**
* Doing forward propagation.
* @param in input matrix.
* @return Matrix instance of output.
*/
public Matrix forward(Matrix in){
if (in.col + 1 != this.nodes[0].in){
System.out.println("input layer error");
System.exit(-1);
}
Matrix in_ = Matrix.appendCol(in, 1.0);
Matrix out[] = new Matrix[this.nodes.length];
for (int i = 0; i < nodes.length; i++){
out[i] = nodes[i].forward(in_);
}
return Matrix.hstack(out);
}
@Override
public String toString(){
String str = String.format("nodes num: %d", this.nodes_num);
return str;
}
}
全結合層と出力層
実はこの二つは、エラーメッセージ以外は入力層と全く同じです。
なぜ分けたかというと、今後必要になるかもしれないと思ったからです。
テスト
計算方法を説明したところで使った入力行列を実際に入れてみます。
もう一度入力行列の画像を貼ります。
プログラムは以下のようになりました。
import matrix.Matrix;
import nodes.out_function.*;
import layer.*;
public class test {
public static void main(String[] str){
// 入力行列
Matrix in = new Matrix(new double[5][4]);
in.matrix[0][0] = 0.2;
in.matrix[0][1] = 0.1;
in.matrix[0][2] = 0.3;
in.matrix[0][3] = 1.0;
for (int i = 1; i < in.row; i++){
in.matrix[i][0] = in.matrix[i-1][0] + 0.1;
in.matrix[i][1] = in.matrix[i-1][1] + 0.05;
in.matrix[i][2] = in.matrix[i-1][2] - 0.05;
in.matrix[i][3] = in.matrix[i-1][3] * 2.0;
}
// 入力層(ノード数、説明変数の数、活性化関数の種類)
Input layer = new Input(5, 4, AF.RELU);
System.out.println(layer.forward(in));
}
}
プログラム実行結果はこちら。
[[2.6000 2.6000 2.6000 2.6000 2.6000 ]
[3.7000 3.7000 3.7000 3.7000 3.7000 ]
[5.8000 5.8000 5.8000 5.8000 5.8000 ]
[9.9000 9.9000 9.9000 9.9000 9.9000 ]
[18.0000 18.0000 18.0000 18.0000 18.0000 ]]
Excelの結果はこちら。
同じ結果になっていることが分かります。
次に、三層のネットワークを作ってみました。
Sigmoid関数の計算方法が間違っていました。
マイナス乗すべきところを普通にべき乗していました。
import matrix.Matrix;
import nodes.out_function.*;
import layer.*;
public class test {
public static void main(String[] str){
// 入力行列(値は全て0.1)
Matrix in = new Matrix(new double[1][5]);
in.fill(0.1);
// ノード数3、4、3の3層ネットワーク
Input in_layer = new Input(3, 5, AF.RELU);
Dense de_layer = new Dense(4, 3, AF.SIGMOID);
Output ou_layer = new Output(3, 4, AF.TANH);
// 層ごとの出力をプリント
print(in);
Matrix matrix = in_layer.forward(in);
print(matrix);
matrix = de_layer.forward(matrix);
print(matrix);
matrix = ou_layer.forward(matrix);
print(matrix);
}
public static void print(Matrix m){
System.out.printf("row: %d, col: %d\n", m.row, m.col);
System.out.println(m);
System.out.println();
}
}
ノード数3、4、3の3層ネットワークですので、先ほども示した以下のようなネットワークができています。
実行結果はこちら。
row: 1, col: 5
[[0.1000 0.1000 0.1000 0.1000 0.1000 ]]
row: 1, col: 3
[[1.5000 1.5000 1.5000 ]]
row: 1, col: 4
[[0.0041 0.0041 0.0041 0.0041 ]]
row: 1, col: 3
[[0.7683 0.7683 0.7683 ]]
Excelで検算してみたところ、同じ結果になりました。
次回は
層ごとに組み立てるのは面倒なのでネットワーククラス作ります。