【DL4J】はじめてのJavaディープラーニング（全結合ニューラルネットワークを用いた手書き文字認識編）

こんにちは。
海の日だというのに東京は未だ涼しい日が続いています。

本日2019年7月15日の東京の最高気温は25℃、最低気温は19℃でした。
気象庁の記録によると1981年から2010年の東京の平均最高・最低気温は以下の通り。

| 平均最高気温 | 平均最低気温 |
|:--|:--|
| 29.0℃ | 21.7℃ |

( 引用元: https://www.data.jma.go.jp/obd/stats/etrn/view/nml_sfc_d.php?prec_no=44&block_no=47662&year=&month=7&day=&view=p1 )

個人的には今の気候は過ごしやすくて好きですが、いつまで続くでしょうかね。

# Deeplearning4j / DL4J

さて、話は変わって本稿では**Skymind**が開発している**Deeplearning4j**、略して**DL4J**の紹介をします。
その名の通りDL4Jは**Java**や**Scala**、**Kotlin**などの**JVM言語で動作するディープラーニング開発フレームワーク**です。
ディープラーニングのフレームワークとしては他にGoogleのTensorFlowやそれに統合されたKeras、FaceBookのPyTorch、Preferred NetworksのChainerなどが有名ですね。
これらのフレームワークは基本的にPythonで開発されることを想定とし、少ないコード数で手軽に研究開発を行うことができます。

DL4Jはどちらかというと、企業のシステム等で広く用いられているJVM言語で記述できることから、**エンタープライズ向けのフレームワーク**として差別化されています。**HadoopやSparkなどのビッグデータ分析基盤とネイティブに連携**できることも売りのひとつです。

では、DL4Jを用いたニューラルネットワークの構築のサンプルを見てみましょう。

# DL4Jのサンプルコード

DL4Jのサンプルコードは公式のレポジトリに豊富に公開されています。
https://github.com/deeplearning4j/dl4j-examples

ちょっと試すにはスケールが大きすぎるので、今回は一部のコードのみフォークしてきた以下のリポジトリをクローンしてください。

```shell:
$ git clone https://github.com/kmotohas/oreilly-book-dl4j-examples-ja
```

こちらは、DL4J自体の作者であるAdam Gibsonらが執筆した["Deep Learning − A Practitioner's Approach"](https://www.amazon.co.jp/dp/B074D5YF1D)の日本語版である[「詳説Deep Learning − 実務者のためのアプローチ」](https://www.amazon.co.jp/dp/4873118808/)に関連するサンプルコードの公開レポジトリです。

<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135044/c58a51bd-9686-6f2f-29a1-3b622aa913e5.png"/>

シンプルな例として、定番のMNISTという手書き数字データセットを多層パーセプトロン（MLP、ゆるい定義では全結合ニューラルネットワークとも）で認識する[MLPMnistTwoLayerExample.java](https://github.com/kmotohas/oreilly-book-dl4j-examples-ja/blob/master/dl4j-examples/src/main/java/org/deeplearning4j/examples/feedforward/mnist/MLPMnistTwoLayerExample.java)の中身を見ていきましょう。

なお、サンプルはIntellij IDEAなどの統合開発環境を用いて動かすことを推奨しますが、Mavenなどのビルドツールを用いてコマンドラインで動かすことも可能です。

# MLPMnistTwoLayerExample.javaの概観

次のコードは、冒頭の`import`文を省略したコードの全体です。

```java:
public class MLPMnistTwoLayerExample {

    private static Logger log = LoggerFactory.getLogger(MLPMnistSingleLayerExample.class);

    public static void main(String[] args) throws Exception {
        //number of rows and columns in the input pictures
        final int numRows = 28;
        final int numColumns = 28;
        int outputNum = 10; // number of output classes
        int batchSize = 64; // batch size for each epoch
        int rngSeed = 123; // random number seed for reproducibility
        int numEpochs = 15; // number of epochs to perform
        double rate = 0.0015; // learning rate

        //Get the DataSetIterators:
        DataSetIterator mnistTrain = new MnistDataSetIterator(batchSize, true, rngSeed);
        DataSetIterator mnistTest = new MnistDataSetIterator(batchSize, false, rngSeed);


        log.info("Build model....");
        MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
            .seed(rngSeed) //include a random seed for reproducibility
            .activation(Activation.RELU)
            .weightInit(WeightInit.XAVIER)
            .updater(new Nesterovs(rate, 0.98)) //specify the rate of change of the learning rate.
            .l2(rate * 0.005) // regularize learning model
            .list()
            .layer(0, new DenseLayer.Builder() //create the first input layer.
                    .nIn(numRows * numColumns)
                    .nOut(500)
                    .build())
            .layer(1, new DenseLayer.Builder() //create the second input layer
                    .nIn(500)
                    .nOut(100)
                    .build())
            .layer(2, new OutputLayer.Builder(LossFunction.NEGATIVELOGLIKELIHOOD) //create hidden layer
                    .activation(Activation.SOFTMAX)
                    .nIn(100)
                    .nOut(outputNum)
                    .build())
            .build();

        MultiLayerNetwork model = new MultiLayerNetwork(conf);
        model.init();
        model.setListeners(new ScoreIterationListener(5));  //print the score with every iteration

        log.info("Train model....");
        for( int i=0; i<numEpochs; i++ ){
        	log.info("Epoch " + i);
            model.fit(mnistTrain);
        }


        log.info("Evaluate model....");
        Evaluation eval = new Evaluation(outputNum); //create an evaluation object with 10 possible classes
        while(mnistTest.hasNext()){
            DataSet next = mnistTest.next();
            INDArray output = model.output(next.getFeatures()); //get the networks prediction
            eval.eval(next.getLabels(), output); //check the prediction against the true class
        }

        log.info(eval.stats());
        log.info("****************Example finished********************");
    }
}
```

このクラスの`main`メソッドは大きく分けて、次の4つの部分から成り立っています。

1. `DataSetIterator`の準備
- `MultiLayerConfiguration`の設定
- `MultiLayerNetwork`の構築
- 構築したニューラルネットワークモデルの訓練
- 訓練したモデルの性能評価

それぞれの部分について順番に解説します。

## 1. `DataSetIterator`の準備

ディープラーニングでモデルの訓練を行うというのは、データセットをモデルに入力し、期待される出力と実際の出力の差を誤差として、それを最小化するようにパラメーターを更新する作業のことです。

DL4Jではデータを反復的にモデルに食わせるためのイテレーターとして[`DataSetIterator`](https://github.com/eclipse/deeplearning4j/blob/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/dataset/api/iterator/DataSetIterator.java)というクラスを用意しています。（実際はJVM版Numpyとも呼べるND4Jライブラリーに[実装](https://github.com/eclipse/deeplearning4j/blob/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/dataset/api/iterator/DataSetIterator.java)があります。`java.util.Iterator`および`java.io.Serializable`を継承しています。）

手書き文字数字のMNISTデータセットには、70,000枚の手書き数字画像と正解ラベル（画像に描いてある数字。0,1,2,3,...,9の情報）が含まれています。一般的にはこれらが分割され、60,000枚が訓練用のデータセット、10,000枚が性能評価のテストデータセットとして用いられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135044/0977dfe9-6be1-c68f-9ed5-b8505317b435.png)

( 引用元: https://weblabo.oscasierra.net/python/ai-mnist-data-detail.html )

以下の図のように、学習率などのハイパーパラメーターチューニング用に検証用データにさらに分割することもありますが、今回は扱いません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135044/3ee02ecf-7cd2-3c2d-93a5-96e55225406d.png)

( 引用元: https://www.procrasist.com/entry/10-cross-validation )

DL4Jでは他のフレームワークと同様にMNIST専用のイテレーターが用意されています。なお、CIFAR-10やTiny ImageNetなど、その他有名なデータセットに対するイテレーターもあります。詳しくは[公式のドキュメント](https://deeplearning4j.org/docs/latest/deeplearning4j-nn-iterators)を参照してください。

自前の画像やCSVなどのデータセット用の`RecordReaderDataSetIterator`や、シーケンスデータ用の`SequenceRecordReaderDataSetIterator`などの情報も同ページにあります。

```java:
        //Get the DataSetIterators:
        DataSetIterator mnistTrain = new MnistDataSetIterator(batchSize, true, rngSeed);
        DataSetIterator mnistTest = new MnistDataSetIterator(batchSize, false, rngSeed);
```

こちらでは、訓練用のイテレーターとテスト用のイテレーターを用意しています。[`MnistDataSetIterator`のソースコード](https://github.com/eclipse/deeplearning4j/blob/master/deeplearning4j/deeplearning4j-data/deeplearning4j-datasets/src/main/java/org/deeplearning4j/datasets/iterator/impl/MnistDataSetIterator.java)から、今回用いているコンストラクタを引用します。

```java:
public MnistDataSetIterator(int batchSize, boolean train, int seed)
```

引数はそれぞれ以下の通りです。

- `int batchSize`: ミニバッチの大きさ、つまり訓練の一度のイテレーションでモデルに入力するサンプル数
- `boolean train`: 訓練データかテストデータかを示す真偽値
- `int seed`: データセットをシャッフルする際の乱数シード

## 2. `MultiLayerConfiguration`の設定

こちらがニューラルネットワークの設計を行なっている部分です。Kerasの用にシーケンシャルにレイヤーを積み重ねていくには`MultiLayerConfiguration`を用います。

なお、複雑な分岐を持ったネットワークを構築したい場合には`ComputationGraphConfiguration`を用います。Kerasのfunctional APIのようなものです。詳しくは[こちらのドキュメント](https://deeplearning4j.org/tutorials/01-multilayernetwork-and-computationgraph)などを参照してください。

```java:
        MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
            .seed(rngSeed) //include a random seed for reproducibility
            .activation(Activation.RELU)
            .weightInit(WeightInit.XAVIER)
            .updater(new Nesterovs(rate, 0.98)) //specify the rate of change of the learning rate.
            .l2(rate * 0.005) // regularize learning model
            .list()
            .layer(0, new DenseLayer.Builder() //create the first input layer.
                    .nIn(numRows * numColumns)
                    .nOut(500)
                    .build())
            .layer(1, new DenseLayer.Builder() //create the second input layer
                    .nIn(500)
                    .nOut(100)
                    .build())
            .layer(2, new OutputLayer.Builder(LossFunction.NEGATIVELOGLIKELIHOOD) //create hidden layer
                    .activation(Activation.SOFTMAX)
                    .nIn(100)
                    .nOut(outputNum)
                    .build())
            .build();
```

`MultiLayerConfiguration`はいわゆる[Builderパターン](https://qiita.com/disc99/items/840cf9936687f97a482b)で実装されています。`.<parameter>`の形式でパラメーターを指定してネットワークをカスタマイズすることができます。

```java:
        MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
            .seed(rngSeed) //include a random seed for reproducibility
            .activation(Activation.RELU)
            .weightInit(WeightInit.XAVIER)
            .updater(new Nesterovs(rate, 0.98)) //specify the rate of change of the learning rate.
            .l2(rate * 0.005) // regularize learning model
            .list()
```

上半分ではネットワーク全体に対するパラメータを設定しています。具体的には以下の設定をしています。

- `.seed(rngSeed)`で乱数シードの設定
- `.activation(Activation.RELU)`で各レイヤーの活性化関数をReLU関数に設定
  - レイヤーごとに別々の活性化関数を指定することもできます
- `.weightInit(WeightInit.XAVIER)`でニューラルネットの重みパラメーターの初期化方法を「XAVIERの初期化」に設定
- `.updater(new Nesterovs(rate, 0.98))`で最適化アルゴリズム（アップデーター）をNesterovsの加速法に設定
  - 引数はそれぞれ学習率とモーメンタム
- `.l2(rate * 0.005)`でL2正則化のパラメーターを設定

```java:
            .layer(0, new DenseLayer.Builder() //create the first input layer.
                    .nIn(numRows * numColumns)
                    .nOut(500)
                    .build())
            .layer(1, new DenseLayer.Builder() //create the second input layer
                    .nIn(500)
                    .nOut(100)
                    .build())
            .layer(2, new OutputLayer.Builder(LossFunction.NEGATIVELOGLIKELIHOOD) //create hidden layer
                    .activation(Activation.SOFTMAX)
                    .nIn(100)
                    .nOut(outputNum)
                    .build())
            .build();
```

下半分ではニューラルネットワークのレイヤーの構成を指定しています。

0番目の接続に、入力が $28\times 28=$ 784次元、出力が500次元の`DenseLayer`（全結合層）を用いています。
MNISTの画像は高さ28pixel、幅28pixel、白黒なのでチャンネル数は1です。これを全結合層に入力するため、一般には $28\times 28$ の行列からベクトルに変換する必要があります。ただし、DL4JのMNIST用のイテレーターではすでにフラットになった収録されているため、この作業は不要となっています。
ここでの出力の500次元という数字には意味はなく、自由に設定できるハイパーパラメーターです。この数字が最適値であるとは限りません。

1番目の接続も同様に、入力が500次元（0番目の出力と同じ値）、出力が100次元の`DenseLayer`を設定しています。くどいようですが、100次元という数字はえいやで決めた値であり、意味はありません。

2番目の接続は特別で、`OutputLayer`を用いています。
入力は前の接続と同様に100次元、出力はデータのラベル数の10（0から9まで）を指定しています。
活性化関数は`Activation.SOFTMAX`で上書きしており、損失関数として`LossFunction.NEGATIVELOGLIKELIHOOD`を設定しています。
ソフトマックス関数は入力された値を確率（合計が1の正の値）として変換するときに用いられる関数で、多クラス分類問題を解くときにNegative Log Likelihood（負の対数尤度）とセットで用いられます。

ここで設定したモデルのイメージは以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135044/0a9fa535-ee78-9310-77c5-a9044618a6de.png)

## 3. `MultiLayerNetwork`の構築

`MultiLayerConfiguration`を引数として`MultiLayerNetwork`のインスタンスを作成するとニューラルネットワークが出来上がります！

```java:
        MultiLayerNetwork model = new MultiLayerNetwork(conf);
        model.init();
```

## 4. 構築したニューラルネットワークモデルの訓練

```java:
        model.setListeners(new ScoreIterationListener(5));  //print the score with every iteration
        for( int i=0; i<numEpochs; i++ ){
        	log.info("Epoch " + i);
            model.fit(mnistTrain);
        }
```

あとは訓練データのイテレーターを引数に`MultiLayerNetwork`の`fit(DataSetIterator iterator)`を呼べばニューラルネットワークの訓練を行うことができます。
訓練データは一度だけ用いるのではなく、基本的に複数回繰り返します。この繰り返しの単位をエポックと呼びます。

リスナーをセットして訓練状況を監視することも可能です。KerasのCallbackのイメージです。
`ScoreIterationListener(int printIterations)`は指定した回数のiteration（DL4Jの用語としては、重みパラメーターの一回の更新が 1 iterationです）が終わる毎にスコア（損失関数の値）を標準出力に表示します。

ここら辺の用語は[公式の用語集](https://skymind.ai/wiki/glossary)で確認できます。
なお、1000個のサンプルを含むデータセットをミニバッチサイズ100で訓練するとき、1エポックは10 iterationです。30エポック数分訓練するとき、300 iterationに相当します。

訓練が終わったときだけでなく途中にもモデルを保存したいときには`CheckpointListener`を用いたり、途中で性能評価を行いときには`EvaluativeListener`を用いることもできます。その他のリスナーに関しては[公式のドキュメント](https://deeplearning4j.org/docs/latest/deeplearning4j-nn-listeners)を参照してください。

## 5. 訓練したモデルの性能評価

```java:
        Evaluation eval = new Evaluation(outputNum); //create an evaluation object with 10 possible classes
        while(mnistTest.hasNext()){
            DataSet next = mnistTest.next();
            INDArray output = model.output(next.getFeatures()); //get the networks prediction
            eval.eval(next.getLabels(), output); //check the prediction against the true class
        }

        log.info(eval.stats());
```

テストデータを用いたモデルの性能評価には[`public Evaluation(int numClasses)`](https://github.com/eclipse/deeplearning4j/blob/master/deeplearning4j/deeplearning4j-nn/src/main/java/org/deeplearning4j/eval/Evaluation.java)のインスタンスを用います。
テスト用のイテレーター`mnistTest`を回し、`getFeatures()`メソッドでデータのベクトルを取得し、訓練したモデルの推論を[`public INDArray output(INDArray input)`](https://github.com/eclipse/deeplearning4j/blob/master/deeplearning4j/deeplearning4j-nn/src/main/java/org/deeplearning4j/nn/multilayer/MultiLayerNetwork.java)で行います。
この推論結果とテストデータのラベルを[`public void eval(INDArray realOutcomes, INDArray guesses)`](https://github.com/eclipse/deeplearning4j/blob/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/evaluation/classification/Evaluation.java)メソッドで比較し、`eval.stats()`で結果を表示するとaccuracy/precision/recall/F1 scoreの値を確認できます。

詳しくは[公式のドキュメント](https://deeplearning4j.org/docs/latest/deeplearning4j-nn-evaluation)を参照してください。

# おわりに

長くなりましたが、これで[MLPMnistTwoLayerExample.java](https://github.com/kmotohas/oreilly-book-dl4j-examples-ja/blob/master/dl4j-examples/src/main/java/org/deeplearning4j/examples/feedforward/mnist/MLPMnistTwoLayerExample.java)の解説はおわりです。

1. `DataSetIterator`の準備
- `MultiLayerConfiguration`の設定
- `MultiLayerNetwork`の構築
- 構築したニューラルネットワークモデルの訓練
- 訓練したモデルの性能評価

といった段階を踏むことでJavaやScalaなどでも簡単にディープラーニングの訓練や評価を行うことができます。何か不明点やコメントありましたら、下のコメント欄もしくは[Gitterのdeeplearning4j-jpチャンネル](https://gitter.im/deeplearning4j/deeplearning4j/deeplearning4j-jp)にお願いします。

さらなる詳細な情報に関してはオライリージャパンの[詳説Deep Learning − 実務者のためのアプローチ](https://www.amazon.co.jp/dp/4873118808/)をお勧めします。